-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
84 lines (84 loc) · 214 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[java.util集合(collection)之HashMap源码分析(jdk1.8)]]></title>
<url>%2F2019%2F07%2F04%2Fcollection-hashmap-sourcecode%2F</url>
<content type="text"><![CDATA[.example tbody td { text-align: center; } .example thead th { text-align: center; } 1. 简介 HashMap为键值对(key -> value)结构存储,其中key是唯一、不重复的、无序的,key和value都允许为null,只允许一条记录的key为null,允许多条记录的value为null。HashMap一般通过key来定位value,当put操作时,传入相同key会覆盖value。 HashMap默认初始容量为16;默认负载因子为0.75;位桶数组table的大小总是为2的n次方。 当位桶上的结点数大于8时会转为红黑树,当位桶上的结点数小于6时红黑树转为链表。 当map中键值对数量超过临界阈值thread(容量 * 装载因子)时,将会进行扩容(位桶数组扩容)。 位桶数组扩容是一个比较耗性能的操作,故而在初始化HashMap的时候可以给一个大致的数值(估算map大小),避免频繁的扩容。 HashMap是线程不安全的,只可在单线程环境下使用,若需在多线程环境使用,主要方法有: ①使用Map m = Collections.synchronizedMap(new HashMap(…))返回一个同步的Map,内部通过synchronized来保证同步;②使用java.util.concurrent.ConcurrentHashMap,建议优先使用;③使用java.util.Hashtable,效率低,不建议使用。 从下图可以得知,HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable这些接口。 继承AbstractMap抽象类,提供了多数已经实现的通用处理方法,其中存在一个未实现的方法entrySet()。 实现Map接口,提供了List接口的所有方法实现。 实现Cloneable接口,支持可拷贝,即覆盖了函数clone()。 实现java.io.Serializable接口,支持序列化。 本文的涉及注解整理于此: 注[1]:HashMap的tableSizeFor(int cap)方法分析 注[2]:HashMap中的hash算法。 注[3]:HashMap中为何位桶数组table的大小必须为2的n次方? 注[4]:HashMap的put(K key, V value)方法操作流程分析。 注[5]:HashMap的扩容resize()方法详解。 注[6]:HashMap扩容resize时位桶为单向链表情况,链表进行拆分并复制到新table的流程分析。 2. 基本结构 jdk1.6/jdk1.7:采用位桶+链表实现使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(哈希冲突),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。在哈希冲突严重时,位桶中的链表可能会很长,会导致put/get操作都可能需要遍历这个链表,即时间复杂度在最差情况下会退化到O(n)。 jdk1.8:采用位桶+链表+红黑树实现使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构。当同一位桶中的节点数不超过8个时,使用单向链表存储;若超过8个时,会将链表转为红黑树,以提升查询效率,put/get的操作的时间复杂度最差只有O(log n)。当位桶中节点结构为红黑树时,若节点数降低以致不足6个时,将转为链表结构。 3. 属性及存储模型3.1. 静态常量属性DEFAULT_INITIAL_CAPACITY1static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认初始容量,规定必须为2的幂。1<<4相当于1 * 2^4 = 16。 MAXIMUM_CAPACITY1static final int MAXIMUM_CAPACITY = 1 << 30; 规定最大容量,当构造函数中指定的容量值比MAXIMUM_CAPACITY大,将会限定为该值大小。1 << 30相当于1 * 2^30。 DEFAULT_LOAD_FACTOR1static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认装载因子为0.75,当构造函数未指定装载因子时会使用。若该值太高,虽会提高空间利用率,但会加大查找开销。 TREEIFY_THRESHOLD1static final int TREEIFY_THRESHOLD = 8; 树化阈值,当一个桶中的节点数超过该值时,桶中链表将转化为红黑树。默认指定为8,以免频繁转换。 UNTREEIFY_THRESHOLD1static final int UNTREEIFY_THRESHOLD = 6; 树还原链表阈值,默认指定为6,当一个桶中的节点数小于等于这个值时,桶中红黑树将转化为链表。默认指定为6,该值应比TREEIFY_THRESHOLD小,以免频繁转换。 MIN_TREEIFY_CAPACITY1static final int MIN_TREEIFY_CAPACITY = 64; 树化时最小桶数组大小,默认指定为64。在桶中链表转为红黑树前,会先判断当桶数组大小超过该值时才会进行转换操作,否则优先进行扩容。避免哈希表建立初期哈希冲突较多情况导致桶中节点数过多,从而引发频繁树化情况。为了避免进行扩容和树化之间选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD。 3.2. 私有属性table1transient Node<K,V>[] table; 存储元素的位桶数组,Node类型数组,其大小总是为2的幂。 entrySet1transient Set<Map.Entry<K,V>> entrySet; 存储具体元素的Set集,用于迭代。而Set keySet和Collection values定义在java.util.AbstractMap中。 size1transient int size; map中实际存储的键值对的数量,需要注意的是这个并非为位桶数组的大小。 modCount1transient int modCount; 操作数,map发生结构性变化时都会增加。用于迭代操作时检查fail-fast,若迭代过程中发现modCount变化了,则会抛出ConcurrentModificationException异常。 threshold1int threshold; 临界阈值(容量 * 装载因子),当map中键值对数量超过该值时,将会进行扩容(位桶数组扩容)。 loadFactor1final float loadFactor; 装载因子。 3.3. 存储模型 Node节点(单向链表结构): 123456789101112131415161718192021222324252627282930313233343536static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表结构,存储下一个元素 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判断两个节点是否相等,若key和value都相等,返回true。当与自身比较时为为true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; }} TreeNode节点(红黑树): 1234567891011121314151617181920static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links // 父节点 TreeNode<K,V> left; // 左子树 TreeNode<K,V> right; // 右子树 TreeNode<K,V> prev; // needed to unlink next upon deletion // 上一个同级节点 boolean red; // 颜色属性,true 红色 TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } // 返回当前节点的根节点 final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } ...} 4. 构造方法HashMap的构造方法有4种,主要涉及到的参数有:初始容量、装载因子和用来初始化的map。 HashMap(int initialCapacity, float loadFactor)构造一个指定初始容量和装载因子的构造方法。1234567891011121314151617181920212223242526272829public HashMap(int initialCapacity, float loadFactor) { // 若指定初始容量小于0,则抛出IllegalArgumentException异常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 若指定初始容量大于规定最大容量(2^30),则限定初始容量为规定最大容量值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 若指定装载因子小于等于0或者非数字,则抛出IllegalArgumentException异常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化装载因子 this.loadFactor = loadFactor; // 初始化临界阈值大小 // 之所以threshold不是等于tableSizeFor(initialCapacity) * this.loadFactor,是因为table的初始化被推迟到了put方法中,put方法中会对threshold重新计算 this.threshold = tableSizeFor(initialCapacity);}// 返回大于等于initialCapacity的最小的2次幂static final int tableSizeFor(int cap) { int n = cap - 1; // >>> 操作符表示无符号右移,高位取0 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;} 注[1]:HashMap的tableSizeFor(int cap)方法分析。该方法是返回一个大于等于参数cap的最小的2的整数次幂,如参数为7,则返回8(2^3)。>>>为无符号右移,高位补0(如 1010>>>1=0101)。|为位或操作,参加位运算的两个数只要有一个为1,其值为1(如 0|0=0,0|1=1,1|0 =1,1|1=1)。 具体分析如下:1)int n = cap - 1;之所以让cap-1,是为了防止cap为2的幂次倍情况。若cap为2的幂次倍且没有cap-1操作,则计算完后的值会是我们所需值的2倍,如cap为8且无cap-1操作情况,结果会为16,应为8。2)n |= n >>> 1;右移1位后位或:由于n大于0,则其二进制形式中必有一位为1。通过无符号右移1位后,其最高位的1右移1位,然后位或操作,会使得n的二进制形式中最高位1右侧1位的数也变成1。如n为01xx xxxx,操作后为011x xxxx。3)n |= n >>> 2;右移2位后位或:通过无符号右移2位后,其最高位的连续两个1右移了2位,然后与原来的n位或,会使得n的二进制形式中最高位连续两个1右侧2位数也变成1,此时高位共有4个1。如n为011x xxxx,操作后为0111 1xxx。4)n |= n >>> 4;右移4位后位或:此时会将n中最高位连续的4个1右移4位,然后位或,此时n中二进制形式的最高位会有8个连续的1。5)n |= n >>> 8;右移8位后位或:此时会将n中最高位连续的8个1右移8位,然后位或,此时n中二进制形式的最高位会有16个连续的1。6)n |= n >>> 16;右移16位后位或:此时会将n中最高位连续的16个1右移16位,然后位或,此时n中二进制形式的最高位会有32个连续的1。这时,已经大于了MAXIMUM_CAPACITY(2^30),故到此为止。7)return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;若计算后的n小于0,则返回1;否则,当n大于等于MAXIMUM_CAPACITY则返回MAXIMUM_CAPACITY,当n小于MAXIMUM_CAPACITY则返回n+1。当n大于0且小于MAXIMUM_CAPACITY时,让n加1是为了得到2的幂次倍。 具体示例:如cap为129,其二进制形式为0000 0000 1000 0001。 n = cap-1 = 128 0000 0000 1000 0000(n1) n |= n >>> 1 0000 0000 0100 0000(n2:n1无符号右移1位)0000 0000 1100 0000(n3:n1 位或 n2) n |= n >>> 2 0000 0000 0011 0000(n4:n3无符号右移2位)0000 0000 1111 0000(n5:n3 位或 n4) n |= n >>> 4 0000 0000 0000 1111(n6:n5无符号右移4位)0000 0000 1111 1111(n7:n5 位或 n6) n |= n >>> 8 0000 0000 0000 0000(n8:n7无符号右移8位)0000 0000 1111 1111(n9:n7 位或 n8) n |= n >>> 16 0000 0000 0000 0000(n10:n9无符号右移16位)0000 0000 1111 1111(n11:n9 位或 n10) n = n + 1 0000 0001 0000 0000(n12:n11加1后为256,aka 2^8) HashMap(int initialCapacity)构造一个指定初始容量的构造方法。1234public HashMap(int initialCapacity) { // 使用指定初始容量和默认装载因子,调用HashMap(int initialCapacity, float loadFactor)来初始化 this(initialCapacity, DEFAULT_LOAD_FACTOR);} HashMap()构造一个使用默认装载因子且未设置初始容量的构造函数。1234public HashMap() { // 使用默认装载因子初始化loadFactor this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted} HashMap(Map<? extends K, ? extends V> m)将传入的map集合转为HashMap。12345678910111213141516171819202122232425262728293031323334public HashMap(Map<? extends K, ? extends V> m) { // 使用默认装载因子初始化loadFactor this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { // 获取参数map的实际大小 int s = m.size(); if (s > 0) { // 判断当前map中的table是否已初始化,若没有 if (table == null) { // pre-size /*获取所需容量: 由于实际大小=容量*loadFactor,故通过s/loadFactor获取所需容量。 最后+1.0F是因为除后一般都为小数,加1后会int强转丢失小数位,即使除后为整数多加1也无伤大雅。 */ float ft = ((float)s / loadFactor) + 1.0F; // 若所需容量小于MAXIMUM_CAPACITY,则转为int型;否则,取MAXIMUM_CAPACITY int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 判断得到的t是否大于临界阈值,若大于,则调用tableSizeFor()对threshold重新初始化 if (t > threshold) threshold = tableSizeFor(t); } // 若table非空(已初始化),参数map的实际大小大于threshold时,会进行扩容操作 else if (s > threshold) resize(); // 具体扩容方法,后续会详解 // 遍历参数map,将其所有key和value添加至当前map中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); // 添加key和value方法,后续会详解 } }} 5. 常用方法V put(K key, V value) public V put(K key, V value):将键值对key-value添加到map中,若key存在,则用新值覆盖旧值并返回旧值,否则,直接添加新值并返回null。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}// 获取key的哈希值static final int hash(Object key) { int h; // 获取key的hashCode: // 若key为null,则直接返回0; // 若key非空,则先取key的hashCode()给h,再将h与h右移16位后的数做异或,目的为减少hash冲突 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/** * 具体向map中添加元素的实现方法 * * @param hash key的hash值 * @param key 待存储的键 * @param value 待存储的值 * @param onlyIfAbsent 决定待存储的key在已存在的情况下,是否用新值覆盖原有的旧值。true则保留旧值;false则覆盖旧值。 * @param evict true则代表map创建后才调用该方法;false则代表说明table处于creation mode,在map创建时调用了该方法,如构造函数 * @return 若key存在,返回旧值;否则,返回null */final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; // 记录当前元素待放的位桶数组(正常指向table;扩容后则指向扩容后的新table) Node<K,V> p; // 记录当前元素待放节点位置(初始为目标位桶的首节点;后续用于遍历位桶中的链表中节点) int n, i; // n:位桶数组长度;i:当前元素在位桶数组的索引位置 // 1.若table为空(未初始化)或其长度为0,则进行扩容(table不会在构造函数中初始化,而是延迟到put操作中) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 扩容并返回扩容后的大小 // 2.计算当前元素所属位桶的索引位置,若该索引上节点为空,则创建一个新节点 // (n - 1) & hash 是为了将hash映射到位桶索引范围内(0~n-1),确定当前元素要放到哪个位桶中去 if ((p = tab[i = (n - 1) & hash]) == null) // 创建一个新节点,将当前元素放进来,其中next指向null tab[i] = newNode(hash, key, value, null); else { // 3.执行至此,说明元素要放的目标位桶中非空 Node<K,V> e; // 记录了当前元素待放的节点位置 K k; // 3.1.若目标位桶中第一个节点的hash值和key值与待放节点(参数中的hash和key)均相等,则令节点e记录下该节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 3.2.若目标位桶中节点结构为红黑树,则将元素放入树中 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 3.3.若目标位桶中节点结构为链表,顺序遍历链表中节点,直至将元素放入合适的位置(链表末尾或覆盖hash和key相同的节点) else { // 顺序遍历链表中所有节点: for (int binCount = 0; ; ++binCount) { // 3.3.1.若直至遍历到链表尾节点,仍未发现目标key,则在链表末尾新建一个节点并将当前元素放入 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 在链表末尾创建新节点后,判断若节点数量超过树化阈值(8个),则将当前位桶中链表转为红黑树结构 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 3.3.2.若在遍历过程中找到了目标key,则退出循环,其中节点e指向的就是目标key所属节点位置 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 执行至此,要么: // 1)待存储元素的key不存在,即节点e为空,直接插入链表末尾或添加到红黑树中 // 2)待存储元素的key已存在,即节点e记录了待放入节点的位置,需等待后续操作来覆盖所记录节点中的值 // 若节点e非空,即待存储元素的key已存在: if (e != null) { // existing mapping for key V oldValue = e.value; // 记录旧值 // 若参数onlyIfAbsent为false或旧值为null,则用新值覆盖旧值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 该方法为空方法,会于LinkedHashMap中使用到,用于后处理操作 return oldValue; // 返回旧值 } } // 执行至此,说明处于待存储元素的key不存在情况 ++modCount; // modCount加1 // map的实际大小加1,并判断若超过阈值,则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 空方法 return null; // 待存储元素的key不存在情况,返回null}/** * 位桶扩容方法 * 扩容时机:table初始化时;table大小超过threshold * 每次扩容都会新建一个table,新table的大小是原table的2倍 * 扩容时,会将原table中的节点会重新哈希到新table中:索引位置相同 或 与原位置相差一个原table大小 */final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 令oldTab指向当前map中的位桶数组table int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 若原容量大于0,即位桶数组非空 if (oldCap > 0) { // 若原容量大于等于MAXIMUM_CAPACITY,则更新threshold为Integer最大值,不再扩容,直接返回oldTab if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 先将原容量左移1位(即扩容2倍),若扩容后容量小于MAXIMUM_CAPACITY且扩容前容量大于等于默认初始容量(16),则临界阈值也扩容2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 执行到此,说明当前map是扩容而非初始化 newThr = oldThr << 1; // double threshold 扩容临界阈值为原来的2倍 } // 若原容量小于等于0且原临界阈值大于0,则将新容量newCap设置为原临界阈值大小 else if (oldThr > 0) // initial capacity was placed in threshold // 通过带参构造方法(HashMap(int initialCapacity, float loadFactor)和HashMap(int initialCapacity))会满足此if条件,这2个方法会调用tableSizeFor()对指定初始容量进行处理返回一个大于等于指定初始容量最小的2次幂,然后赋给临界阈值threshold,故这里将oldThr还给newCap newCap = oldThr; // 若原容量小于等于0且原临界阈值小于等于0,即首次初始化,设置容量和临界阈值的默认值 else { // zero initial threshold signifies using defaults // 通过无参构造方法(HashMap())会满足此if条件 newCap = DEFAULT_INITIAL_CAPACITY; // newCap为16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // newThr为12 } // 若newThr为0,满足该if条件有以下可能: // 1)原容量oldCap大于0且小于默认初始容量(16),初始化时指定的容量小于默认值16 // 2)使用带参构造方法初始化,第一次使用put if (newThr == 0) { float ft = (float)newCap * loadFactor; // 计算新的临界阈值:新得到的容量*装载因子 // 获取新的临界阈值newThr:若新的容量newCap和ft都小于MAXIMUM_CAPACITY,则将ft转为int型,否则取Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 将临界阈值threshold设置为新临界阈值newThr threshold = newThr; // 以上操作,计算出了新的table容量newCap和临界阈值newThr @SuppressWarnings({"rawtypes","unchecked"}) // 创建一个大小为newCap的位桶数组newTab Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 更新table指向新创建的位桶数组newTab if (oldTab != null) { // 遍历旧map(oldTap)中数据,复制到新map(newTab)中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; // 获旧map位桶数组中j位置上的节点给e,若非空,则开始数据复制 if ((e = oldTab[j]) != null) { oldTab[j] = null; // 先将旧map位桶数组中j位置节点置空,方便GC // 判断旧位桶数组上的每一个节点情况,分类处理: // 1.若节点e的next指向null,说明当前位桶中只有一个节点,直接将节点e放入新位桶数组中,新索引位置为e.hash & (newCap - 1) if (e.next == null) // 让e的hash值位与新位桶容量减1的值,即可得到要放入新位桶数组的索引位置 newTab[e.hash & (newCap - 1)] = e; // 2.若节点e为红黑树节点TreeNode,则调用TreeNode的split()方法从根节点出发进行 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 3.若节点e为大于1的单向链表,则进行链表复制操作 else { // preserve order // lo链表:存储e.hash&oldCap为0情况的节点 Node<K,V> loHead = null, loTail = null; // hi链表:存储e.hash&oldCap为非0情况的节点 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍历位桶中在同一索引处的节点,根据e.hash&oldCap是否为0,分别存储到lo链表和hi链表中 do { next = e.next; // 记录节点e的next指向 // 若节点e的hash值位与旧位桶容量后得到的值为0,则将节点e采用尾插法存储到lo链表中 if ((e.hash & oldCap) == 0) { if (loTail == null) // 若loTail为空,令loHead指向节点e loHead = e; else // 否则,loTail的next指向节点e loTail.next = e; loTail = e; // loTail指向节点e } // 若节点e的hash值位与旧位桶容量后得到的值非0,则将节点e采用尾插法存储到hi链表中 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 置lo链表尾部指向null,并将lo链表的所有节点都挂到新位桶数组的j处 // (j:即旧位桶索引位置) if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 置hi链表尾部指向null,并将hi链表的所有节点都挂到新位桶数组的j+oldCap处 // (j+oldCap:即旧位桶索引位置+旧位桶数组长度) if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; // 返回新位桶数组} 【注2】:HashMap中的hash算法。在JDK1.8的HashMap中,hash算法为:1234static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} 首先获取key的hashCode()值,然后将获取的hashCode值无符号右移16位(不论正负,高位均补0),再将右移后的值与原hashCode值做异或运算,计算出该key的hash值。 在putVal()方法中,通过(n - 1) & hash来计算当前对象在位桶数组中的位置,这里hash值为上面计算得到值,n为位桶数组的大小(固定为2的幂次方),(n - 1) & hash等价于hash % n,通过这么计算以确保索引置落在位桶数组范围内0 ~ n-1。123456final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); ...} 具体示例如下(key=-100): h=key.hashCode()h>>>16(h=key.hashCode())^(h>>>16) 1111 1111 1111 1111 1111 1111 1001 1100(n1:key的hashCode()值)0000 0000 0000 0000 1111 1111 1111 1111(n2:n1无符号右移16位)1111 1111 1111 1111 0000 0000 0110 0011(n3:n1异或n2,即key的hash值) n=16n-1(n-1)&hash 0000 0000 0000 0000 0000 0000 0001 0000(n4:若map容量为16)0000 0000 0000 0000 0000 0000 0000 1111(n5:n4 - 1)0000 0000 0000 0000 0000 0000 0000 0011(n6:n5 & n3,即3) 这里的hash算法计算过程中,右移16位,刚好是32位(hash值为int型)的一半,利用自身高半位与低半位做异或,以混合原hash值高位和地位,增大低位随机性,使得高位也参与哈希运算,从很大程度上减少碰撞率。这种方式称为“扰动函数”,减少哈希冲突,具体可参读Peter Lawley的文章《An introduction to optimising a hashing strategy》。【注3】:HashMap中为何位桶数组table的大小必须为2的n次方?主要是为了在取模和扩容时做优化。由【注2】可知,通过(n - 1) & hash来计算索引值,相对于hash % n来说,&操作的效率要高于%操作。若长度为2的n次方,则减1后的二进制形式必为…1111,在与hash值与操作后,结果区间为位桶数组范围内,运算效率更高。倘若位桶数组大小非2的n次方,假设为15,减1后为14(1110),最后一位为0,在做与操作后,最后一位会始终为0,使得存放元素的位置减少,造成空间浪费,自然也就会增大碰撞几率。扩容操作则是利用位桶数组大小与hash值做与操作,判断结果是否为0,以区分扩容后的位置。【注4】:HashMap的put(K key, V value)方法操作流程分析。put方法实际上是通过putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法来实现插入key-value,具体流程为:1.判断table是否为null(table未初始化)或其长度是否为0,若是,则进行扩容;2.计算当前元素待插入位桶数组的索引值,i = (n - 1) & hash;3.判断table[i](待插目标位桶的首节点)是否为null,若为null,则在该位置新建一个节点并直接添加待插元素,然后转向6,否则,转向4;4.判断目标位桶首节点元素与待插元素的key是否相同,若key相同,则令节点e记录该节点,后续执行覆盖操作,然后转向7,否则转向5;5.判断目标位桶中节点结构是否为红黑树,若是,则调用putTreeVal()方法来处理并用节点e记录返回值(若是新建节点则返回null,否则会返回匹配到的节点),然后转向7,否则转向6;6.遍历目标位桶中链表的所有节点: 6.1.若直至链表尾节点也未发现key相同的节点,则在链表尾部新建一个节点并添加待插元素,然后会判断若当前节点数超过指定树化阈值,则会转为红黑树结构,以提升查询速度,再跳出循环; 6.2.若找到了key相同的节点,则跳出循环,其中节点e对应的就是该待覆盖节点; 跳出循环后,转向7;7.判断节点e是否为null,若非null,则说明待插元素的key已存在,节点e就记录了待覆盖节点,此时若参数onlyIfAbsent为false或旧值为null,则用新值覆盖旧值(这里onlyIfAbsent传递过来为false,执行覆盖操作),最后返回旧值,结束流程。8.插入成功(待存插元素的key不存在情况),增加modCount和size,然后判断size是否大于临界阈值threshold,若是则扩容,最后返回null,结束流程。具体put方法流程图如下:【注5】:HashMap的扩容resize()方法详解。扩容时机:①table初始化时,构造函数中并没有对table进行初始化,而是延迟到了第一次put操作中。②table大小超过临界阈值threshold(装载因子位桶容量)时,如向map中添加元素,当size大于threshold,则将进行扩容。扩容结果:①每次扩容操作都会新建一个table,且新table大小是原来的2倍。②扩容时,针对位桶中节点结构的不同会有不同的处理方式:若位桶中仅有一个节点,则直接重哈希到新table中;若位桶中结构为红黑树,则调用TreeNode的split()方法对树进行修剪,将元素复制到新table中;若位桶中结构为单向链表,则会将原table中的节点会重新哈希到新table中,相对于原节点位置:索引位置相同 或 与原位置相差一个原table大小。扩容流程:1.获取原table容量oldCap和原临界阈值oldThr;2.判断原容量是否大于0,若是,则转向3,否则转向5;3.判断原容量是否大于最大容量,若是,则设置临界阈值threshold为Integer最大值,并返回原table,结束流程,否则,转向4;4.计算新容量newCap为oldCap的2倍,然后判断newCap是否小于最大容量且oldCap是否大于等于默认初始容量16,若是,则设置新临界阈值newThr为oldThr的2倍,然后转向7,否则,也转向7;5.判断oldThr是否大于0,若是,则设置newCap为oldThr,然后转向7,否则,转向6;6.设置newCap为默认初始值16、newThr为默认装载因子默认初始值=12;7.判断newThr是否等于0,若是,则计算新的临界阈值(ft=newCap*loadFactor),判断newCap是否小于最大容量且ft是否小于最大容量,若是,则设置newThr为新临界阈值ft,否则为Integer的最大值;8.设置临界阈值threshold为newThr,并创建新table(newTab),其大小为newCap;9.判断oldTab是否为非null,若是,则转向10,否则转向11;10.遍历oldTab,将原数据复制到newTab中: 10.1.判断当前位桶中是否为非null,若是则转向10.2,否则结束本次循环; 10.2.判断当前位桶中首节点是否为单节点,若是,则计算新索引位置(e.hash&(newCap-1)),并复制到newTab中,然后结束本次循环,否则,转向10.3; 10.3.判断当前位桶中结构是否为红黑树,若是,则调用TreeNode的split()方法进行树修剪,并复制到newTab中,然后结束本次循环,否则,转向10.4; 10.4.此种情况,当前位桶为单向链表结构,遍历该链表,并使用尾插法分别得到lo链表和hi链表,然后分别将lo链表复制到newTab中在原索引的位置,将hi链表复制到newTab中在原索引加oldCap的位置,结束本次循环; 跳出循环,然后转向11;11.返回newTab,结束流程。具体resize方法流程图如下:【注6】:HashMap扩容resize时位桶为单向链表情况,链表进行拆分并复制到新table的流程分析。扩容操作时的链表拆分主要代码分为三大部分,其中第一部分为:12Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;这里定义了4个Node的引用,从命名上分为lo链表和hi链表,主要用于记录链表拆分成2个链表的引用,其中loHead和loTail对应lo链表的头节点和尾节点,hiHead和hiTail对应hi链表的头节点和尾节点。第二部分为:1234567891011121314151617181920Node<K,V> next;do { next = e.next; if ((e.hash & oldCap) == 0) { // lo链表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { // hi链表 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; }} while ((e = next) != null);这里为一个循环,遍历当前位桶上的所有节点,判断节点应该插到lo链表还是hi链表,这里使用的是尾插法。如何判断节点所属链表,是通过(e.hash & oldCap) == 0条件来决定,这里oldCap为原位桶数组大小(2的n次方),利用oldCap和hash值做位与操作,看结果若为0,则将节点插入lo链表,否则,插入hi链表。这里,由于oldCap必为2的n次方,例如其二进制形式一般为10000,只有高位为1,其余都为0。故而e.hash & oldCap的结果是否为0,取决于oldCap高位为1的位置对应于hash值的那个位置是否为1还是0。我们知道会有多个节点(形成单链表)存在于一个位桶中,是由于位桶数组大小减1后位与hash值((n - 1) & hash)计算得到的索引值一致所导致的。如下面一组数据: key hash 二进制形式 (n - 1) & hash,若n=16 1 1 0000 0001 0000 11110000 0001—————0000 0001 17 17 0001 0001 0000 11110001 0001—————0000 0001 33 33 0010 0001 0000 11110010 0001—————0000 0001 49 49 0011 0001 0000 11110011 0001—————0000 0001 65 65 0100 0001 0000 11110100 0001—————0000 0001 由上面一组key得知,1、17、33、49和65计算得到索引值都为1,故而都置于一个位桶中形成单链表。由此可知,同一位桶中的所有节点对应oldCap的高位为1的位置(即oldCap为16,高位从右往左数第5位)是按0101…的顺序排列,即上表中二进制形式一列中红色标注的位置,再做链表拆分操作时(判定条件(e.hash & oldCap) == 0),第一个判定条件为0,插入lo链表,第二个判定条件为1,插入hi链表,第三个插入lo链表,第四个插入hi链表,以此类推,均匀拆分成了2个链表。 第三部分为:12345678if (loTail != null) { loTail.next = null; newTab[j] = loHead;}if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead;} 这一部分主要是将处理好的lo链表和hi链表插入到新位桶数组中, 若lo链表非空,则将整个lo链表放到新位桶数组中索引为j的位置(即原索引位置);若hi链表非空,则将整个hi链表放到新位桶数组中索引为j+oldCap的位置(即原索引位置+原位桶数组大小的位置)。 整个操作的图示为: To be continued… 参考资料[1] JDK1.8 HashMap源码分析. https://www.cnblogs.com/xiaoxi/p/7233201.html.[2] HashMap源码注解 之 静态工具方法hash()、tableSizeFor()(四). https://blog.csdn.net/fan2012huan/article/details/51097331.[3] 深入理解HashMap(四): 关键源码逐行分析之resize扩容. https://segmentfault.com/a/1190000015812438.[4] JDK 源码中 HashMap 的 hash 方法原理是什么?. https://www.zhihu.com/question/20733617.]]></content>
<categories>
<category>java集合系列(java.util包)</category>
</categories>
<tags>
<tag>java集合</tag>
<tag>java.util.List</tag>
</tags>
</entry>
<entry>
<title><![CDATA[三种方式实现扫码登录]]></title>
<url>%2F2019%2F05%2F23%2Fqrcode-scan-login%2F</url>
<content type="text"><![CDATA[1. 需求描述目前大多网站都提供扫码登录功能,无需在网页端输入任何账号和密码信息,只需通过手机上的app,如微信、淘宝等,通过使用app上的扫码功能去扫面页面登录提供的二维码图片,即可完成登录。这不仅提高了安全性,也增加了用户的使用方便性。关于二维码与手机是如何绑定、网页端如何识别出手机端进行了扫码操作、以及网页端在扫码成功后如何获取到用户信息等问题,这些都会在接下来的篇幅中娓娓道来。 2. 扫码登录原理扫码登录功能主要由网页端、服务器和手机端组成。当我们访问网页端的登录页面时,会向服务器请求获取二维码登录的请求。服务器接收到请求后,会生成一个uuid,并记录下来,然后将该uuid返回网页端。网页端接收到返回结果后,会生成一张二维码图片,其中返回的uuid信息也会融入二维码中。接下来,网页端会不断向服务器发送请求询问该二维码的状态,若使用手机成功扫码,网页端将会收到登录成功和用户信息;若超过一定时间仍无其他操作,网页端将会收到二维码失效的信息,需要重新刷新生成新的二维码。下面以淘宝为例,简述下扫码登录过程:打开淘宝的登录界面: https://login.taobao.com/member/login.jhtml ,页面显示用于登录的二维码图片。会发现网页端会不断发送请求检查二维码的状态,请求地址为: https://qrlogin.taobao.com/qrcodelogin/qrcodeLoginCheck.do ,其中传递的参数lgToken就是全局唯一的uuid。若没有扫码,返回结果为:1{"code":"10000","message":"login start state","success":true});} 若二维码失效,返回结果为:1{"code":"10004","message":"QRCode expired!code=1, msg=data not exist","success":true});} 若成功扫码,返回结果为:1{"code":"10001","message":"mobile scan QRCode success","success":true});} 3. 实现扫码登录下面实现用三种方式来实现扫码登录,分别为:轮询、长轮询和Websocket。 轮询:网页端按照指定时间间隔不断向服务器发送请求,服务器接收到请求后马上响应信息并关闭连接。优点在于后台程序易于编写;缺点在于会产生大量无用请求,浪费带宽和服务器资源,且更需要更快的处理速度。 长轮询:网页端向服务器发送请求,服务器接收到请求后会进行阻塞,直到有新的信息更新(如扫码成功)或者超时,网页端收到响应的结果后会按需继续向服务器发送新的请求。优点在于不会频繁的请求,耗费资源较小;缺点在于服务器在阻塞请求时会增加资源消耗,且更需要处理并发能力。 Websocket:轮询和长轮询存在无法主动推送数据的缺陷,而Websocket可以做到在网页端和服务器建立连接后,在信息更新后,服务端可以主动推送信息给网页端。优点在于实现了双向通信;缺点在于后台实现逻辑较为复杂。从兼容性角度上考虑:轮询 > 长轮询 > Websocket;从性能角度上考虑:Websocket > 长轮询 > 轮询。 具体代码实现: https://github.com/ForThe77/qrcode-scan-login 3.1. 轮询轮询方式是指前端会每隔一段时间就主动给服务端发送一次二维码状态的查询请求,后端需要更快的处理速度,争取在下一次请求到来前完成响应。具体流程图如下所示,其中红色标识部分为轮询相关操作。 流程详释:用户访问登录页面时,会先请求服务器获取全局唯一的uuid。123456789/** * 初始化二维码信息,并添加至缓存池 * @return uuid */private String initQRCodeInfo() { String uuid = UUID.randomUUID().toString().replaceAll("-",""); PoolCache.init(uuid); return uuid;} 后端会将生成的uuid记录到缓存池中,并初始化为“未扫描”状态。1234567/** * 初始化某个二维码数据,设置uuid为key,新建QRCodeInfo为value。 * @param uuid */public static void init(String uuid) { CACHE_MAP.put(uuid, new QRCodeInfo(Constants.QRCodeStatus.NOT_SCAN));} 这里缓存池用Map容器来存储:1private static final Map<String, QRCodeInfo> CACHE_MAP = new ConcurrentHashMap<>(); 缓存池会定时去清理过期的数据,这里利用Timer实现每隔一段时间去清理过时的二维码数据。12345678910111213141516171819202122232425262728293031/** * 初始化:开启一个线程专门用于清理缓存池中的过时数据 */@PostConstructpublic void init() { LOGGER.info("PoolCache::init(): Start a new Thread to clean pool cache."); Timer timer = new Timer("Scheduler-CleanPoolCache"); timer.schedule(new TimerTask() { @Override public void run() { LOGGER.info("Start to clean the pool cache."); try { if (!CACHE_MAP.isEmpty()) { Long currentTime = System.currentTimeMillis(); for (Map.Entry<String, QRCodeInfo> entry : CACHE_MAP.entrySet()) { if (Constants.PoolCacheConfig.QRCODE_TIMEOUT < currentTime - entry.getValue().getCreateTime()){ // 去除缓存池中的过期数据 CACHE_MAP.remove(entry.getKey()); // 若为websocket方式,则发送二维码失效消息给客户端 QrcodeWebsocket.sendMessage(entry.getKey(), new ResultDto<>(false, new QRCodeInfo(null, Constants.QRCodeStatus.INVALID, null), "The QR code is invalid, please retry it.")); } } } } catch (Exception e) { LOGGER.error("Clean the pool cache error!", e); } } }, Constants.PoolCacheConfig.CLEAN_DELAY_TIME, Constants.PoolCacheConfig.CLEAN_INTERVAL_TIME);} 生成二维码的方法是通过前端实现,以减少服务器压力。将返回的uuid融入到二维码的链接中,当使用手机扫描二维码时,会将该uuid传递到后台,以保证对应二维码状态的正确更新。1234567891011121314151617181920/** * 生成二维码 */function generateQRCode() { var uuid = $('#uuid').val(); var url = ctx + '/scan/' + uuid + '?tabId=' + tabId; console.log('The QR code is:' + url); var $qrimg = clearQrInfo(); $qrimg.qrcode({ render: "canvas", width: 256, height: 256, correctLevel: 0, // 纠错等级 text: url, background: '#ffffff', foreground: '#000000', src: 'img/alpaca.jpg' }); updateTip('Please scan the QR code...');} 关于轮询的实现是由前端间隔发送查询请求,通过JS的setInterval(function, milliseconds)方法实现每隔多少毫秒调用一次制定方法,可通过调用clearInterval()来停止。 3.2. 长轮询长轮询是指前端主动给服务端发送二维码状态的查询请求,后端会按情况对请求进行阻塞,直至二维码信息更新或超时。当前端接收到返回结果后,若二维码仍未被扫描,则会继续发送查询请求,直至状态变化(失效或扫码成功)。具体流程图如下所示,其中红色标识部分为长轮询相关操作。长轮询发送请求到后台,若当前二维码仍处于未扫描状态,则会发生阻塞,直至超时或者二维码状态更新。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071/** * 长轮询检查(通过wait+notifyAll来实现服务端等待) * @param uuid * @return */@RequestMapping("checkByLongPool")@ResponseBodypublic ResultDto checkByLongPool(String uuid) { ResultDto<QRCodeInfo> resultDto = new ResultDto<>(); if (null == uuid || 0 == uuid.length()) { resultDto.setFlagAndMsg(false, "The parameter is null while pooling!"); LOGGER.warn("轮询时,输入参数uuid为空!"); return resultDto; } LOGGER.info(MessageFormat.format("查询二维码状态({0}),检测是否登录。", uuid)); if (!PoolCache.contains(uuid)) { // uuid对应数据为空,二维码失效 resultDto.setAll(true, new QRCodeInfo(null, Constants.QRCodeStatus.INVALID, null), "The QR code is invalid, please retry it."); LOGGER.info(MessageFormat.format("该二维码({0})已失效!", uuid)); } else { QRCodeInfo qrCodeInfo = PoolCache.get(uuid); qrCodeInfo.hold(); // hold操作 resultDto.setFlagAndData(true, qrCodeInfo); } return resultDto;}/** * @Title: 二维码信息描述类 * @Description: * @author: Roy */public class QRCodeInfo implements Serializable { ... /** * 根据状态判断是否需要hold(同步方法) * @return */ public synchronized void hold() { try { if (Constants.QRCodeStatus.NOT_SCAN.equals(status)) { new Thread(() -> { try { Thread.sleep(Constants.PoolCacheConfig.LONGPOOL_DELAY_TIME); } catch (InterruptedException e) { e.printStackTrace(); } this.notifyQRCodeInfo(); }).start(); // 若处于“未扫描”状态,则进入等待 this.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } /** * 唤醒当前对象对应锁住的所有线程 */ public synchronized void notifyQRCodeInfo() { try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } } ...} 长轮询前端请求逻辑为每次得到响应后,若二维码状态仍处于“未扫描”状态,则继续调用请求方法。123456789101112131415161718192021222324252627282930313233343536373839/** * 长轮询 */function checkByLongPool() { $.get(ctxPath + '/checkByLongPool', {uuid: $('#uuid').val()}, function (res) { var isContinuePolling = true; if (!res || !res.flag) { console.log(res.msg || 'The result of poolCheck() is error!'); } if (res.data) { var status = res.data.status; switch (status) { case '2': console.log('The QR code is invalid, please retry it!'); isContinuePolling = false; clearInterval(intervalOfCheckByPool); $('.qrcode_area .qrcode .qrmask').show(); updateTip('The QR code is invalid, please retry it!'); break; case '1': console.log('Scan the QR code successfully!'); isContinuePolling = false; clearInterval(intervalOfCheckByPool); updateTip('Scan the QR code successfully!'); window.location.href = ctxPath + '/scanSuccess'; break; case '0': console.log('The QR code has not been scanned.'); break; default: break; } } if (isContinuePolling && '2' === tabId) { // 再次发送查询请求 checkByLongPool(); } });} 3.3. WebsocketWebsocket是指前端在生成二维码后,会与后端建立连接,一旦后端发现二维码状态变化,可直接通过建立的连接主动推送信息给前端。具体流程图如下所示,其中红色标识部分为Websocket相关操作。后端Websocket配置代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104/** * @Title: 开启WebSocket支持 * @Description: 首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。 * @author: Roy */@Configurationpublic class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }}/** * 二维码 Websocket配置 */@ServerEndpoint("/qrcodeWebsocket/{uuid}") // 将目前的类定义成一个websocket服务器端@Componentpublic class QrcodeWebsocket { private static final Logger LOGGER = LoggerFactory.getLogger(QrcodeWebsocket.class); // 用来记录当前在线连接数 private static int onlineCount = 0; // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; // 当前Websocket存储的连接数据:uuid -> websocket数据 private static final ConcurrentMap<String, QrcodeWebsocket> WEBSOCKET_MAP = new ConcurrentHashMap<>(); /** * 连接建立成功时调用 * @param uuid * @param session */ @OnOpen public void onOpen(@PathParam("uuid") String uuid, Session session) { this.session = session; WEBSOCKET_MAP.put(uuid, this); addOnlineCount(); LOGGER.info(MessageFormat.format("onOpen(${0})... onlineCount: {1}", uuid, getOnlineCount())); } /** * 连接关闭时调用 * @param uuid */ @OnClose public void onClose(@PathParam("uuid") String uuid) { WEBSOCKET_MAP.remove(uuid); subOnlineCount(); LOGGER.info(MessageFormat.format("onClose(${0})... onlineCount: {1}", uuid, getOnlineCount())); } /** * 接收客户端消息后调用 * @param message * @param session */ @OnMessage public void onMessage(String message, Session session) { LOGGER.info("onMessage()... message: " + message); } /** * 发生错误时调用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { LOGGER.error("onError()..."); error.printStackTrace(); } /** * 发送消息给客户端 * @param message * @throws IOException */ private void sendMessage(Object message) throws IOException { String msgJson = ""; if (null != message) { msgJson = JSON.toJSONString(message); } this.session.getBasicRemote().sendText(msgJson); } /** * 根据uuid,发送消息给指定客户端 * @param uuid * @param message * @throws IOException */ public static void sendMessage(String uuid, Object message) throws IOException { LOGGER.info(MessageFormat.format("发送消息给{0},消息为:{1}", uuid, message)); QrcodeWebsocket qrcodeWebsocket = WEBSOCKET_MAP.get(uuid); if (null != qrcodeWebsocket) { qrcodeWebsocket.sendMessage(message); } else { LOGGER.warn(MessageFormat.format("发送消息给{0},发送失败,无相关连接!", uuid)); } } private static synchronized void addOnlineCount() { QrcodeWebsocket.onlineCount++; } private static synchronized void subOnlineCount() { QrcodeWebsocket.onlineCount--; } private static synchronized int getOnlineCount() { return QrcodeWebsocket.onlineCount; }} 前端Websocket的配置代码如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556/** * websocket方式 */function checkByWebsocket() { //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { var url = "ws://localhost:9999" + ctxPath + "/qrcodeWebsocket/" + $('#uuid').val(); websocket = new WebSocket(url); } else { alert('Websocket is not supported in current browser.'); } //连接发生错误的回调方法 websocket.onerror = function () { console.log("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function (event) { console.log("WebSocket连接成功 " + event.currentTarget.url); }; //接收到消息的回调方法 websocket.onmessage = function (event) { var res = JSON.parse(event.data); console.log("WebSocket接收到消息:" + res); if (!res || !res.flag) { console.log(res.msg || 'The result of checkByWebsocket() is error!'); } if (res.data) { var status = res.data.status; switch (status) { case '2': console.log('The QR code is invalid, please retry it!'); $('.qrcode_area .qrcode .qrmask').show(); updateTip('The QR code is invalid, please retry it!'); break; case '1': console.log('Scan QR code successfully!'); updateTip('Scan QR code successfully!'); window.location.href = ctxPath + '/scanSuccess'; break; case '0': console.log('The QR code has not been scanned.'); break; default: break; } } }; //连接关闭的回调方法 websocket.onclose = function (event) { console.log("WebSocket连接关闭 " + event.currentTarget.url); }; //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { websocket.close(); };} 参考资料[1] 实现扫码登陆的最简单方案与原理. https://mp.weixin.qq.com/s/UAZrnacesveJpEHf9VZL3Q.[2] java实现简单扫码登录功能(模仿微信网页版扫码). https://blog.csdn.net/gentlu/article/details/78592571.]]></content>
<categories>
<category>小功能Demo实现系列</category>
</categories>
<tags>
<tag>功能实现</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring事务用法详解]]></title>
<url>%2F2019%2F04%2F29%2Fspring-transaction-use%2F</url>
<content type="text"><![CDATA[1. 数据库事务基础知识在使用Spring开发过程中,我们常会使用到Spring事务管理,它提供了灵活方便的事务管理功能,但这些功能都是基于底层数据库本身的事务处理机制功工作的。因此欲深入了解Spring事务的管理和配置,有必要先了解下数据库基本的事务知识。 1.1. 事务特性(ACID)Spring事务中存在四种特性:原子性、一致性、隔离性和持久性。在这些事务特性中,数据“一致性”为最终目标,其他特性都是为实现这个目标方法和手段。数据库一般采用重执行日志保证原子性、一致性和持久性,采用数据库锁机制保证事务的隔离性。 原子性(Atomicity):将一个事务中的多个数据库操作捆绑成一个不可分割的原子单元。即对于一个事务的操作,要么全部执行,要么全部不执行。只有当整个事务的所有操作都执行成功,才会提交,否则即使整个事务中只要有一个操作失败,就算是已执行的操作也都必须都回滚到初始状态。 一致性(Consitency):当事务完成时,必须保证所有数据都处于一致状态,即数据不会被破坏。如从A账户转账100元到B账户,无论操作是否成功,A和B的存款总额总是不变的。 隔离性(Isolation):在并发操作数据时,不同的事务会有不同的数据操作,且它们的操作不会相互干扰。数据库规定了多种隔离级别,隔离级别越低,并发性越好,干扰越大会导致数据一致性变差;而隔离性越高,并发性越差,数据一致性越好。 持久性(Durability):一旦事务成功完成提交后,整个事务的数据都会持久化到数据库中,且结果不受系统错误影响,即使系统崩溃,也可通过某种机制恢复数据。 1.2. 数据并发问题数据库中某块数据可能会同时被多个并发事务同时访问,若没有采取必要的隔离措施,可能会导致各种并发问题,破坏数据完整性。这些问题主要如下: 脏读(Dirty Read):A事务读取到B事务尚未提交的更改数据,并在此基础上操作(B可能回滚)。比如,B事务取款操作会将账户上的余额进行更改且尚未提交,此时A事务查询到B事务尚未提交的账户余额,然后B事务回滚,账号余额恢复到更改之前,而A事务读取到的仍是B事务更改后的金额,若A事务在此基础上做操作,则会导致数据“变脏”。 不可重复读(Unrepeatable Read):A事务先后读取同一条记录,在两次读取之间该条记录被B事务修改并提交,则会导致A事务两次读取的数据不同。比如,A事务先查询账户余额,在下次读取之前,此时B事务卡在中间修改了账户余额并提交,然后A事务在读取账户余额时会发现两次读取金额不一致。 幻读(Phantom Read):A事务先后按相同查询条件去读取数据,在两次读取之间被B事务插入了新的满足条件的数据并提交,则会导致A事务两次读取的结果不同。比如,A事务按条件去查询当前账户中已绑定的卡情况,在下次查询之前,此时B事务卡在中间对该账户新增一张卡,然后A事务在按相同条件查询时,会发现多了一张卡。 1.3. 事务隔离级别事务隔离级别分为四种,如下: 读未提交(Read Uncommitted):可以读取到未提交的数据。当一个事务已经写入一行数据但未提交,此时其他事务可以读到这个尚未提交的数据。 读已提交(Read Committed):不可以读取到未提交的数据,只能读到已提交的数据。 重复读(Repeatable Read):保证多次读取的数据都是一致的。 串读(Serializable):最严格的事务隔离级别,不允许事务并行执行,只允许串行执行。事务执行时,如读操作和写操作都会加锁,好似事务就是以串行方式执行。 不同事务隔离级别能够解决数据并发问题的能力是不同的,具体对应关系如下所示: 隔离级别脏读不可重复读幻读Read Uncommitted√(允许)√√Read Committed×(不允许)√√Repeatable Read××√Serializable××× 2. 代码验证简述下面将以具体代码实例来演示Spring事务中@Transactional的每个参数的使用情况,代码结构主要分为Service和Dao层,由Spring负责依赖注入和注解式事务管理,Dao层由Mybatis实现,分别配置了双数据源Oracle和MySQL,其中Oracle对应的事务管理器限定符为oracleTM,MySql对应的为mysqlTM。当使用Spring事务注解@Transactional且未指定value(事务管理器)时,将会以默认的事务管理器来处理(以加载顺序,首先加载的作为默认事务管理器)。 Oracle和MySql分别新增了两张相同的表:T_SERVER1和T_SERVER2。这两张表的结构完全一致,共有2个字段:ID(varchar(32) not null primary key)和NAME(varchar(50))。 Bean层:因为所有表结构都一致,故采用同一个Bean——Server类。 12345678910public class Server { private String id; private String name; public Server() { } public Server(String name) { this.name = name; } // 省略get和set方法...} Dao层: Dao层代码分为Oracle和MySQL对应的Mapper接口,Oracle对应的Mapper接口(Server1OracleDao接口和Server2OracleDao接口)为:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/** * Server1 Dao(基于Oracle) */public interface Server1OracleDao { /** * 向T_SERVER1中插入一条新数据,其中主键id为32位sys_guid * @param server */ @Insert("insert into T_SERVER1 values (sys_guid(),#{name})") void save(Server server); /** * 查询T_SERVER1中的所有数据 * @return */ @Select("select * from T_SERVER1") List<Server> getAllServers(); /** * 根据主键id,查询T_SERVER1中的数据 * @Options注解能够设置缓存信息 * useCache = true,表示会缓存本次查询结果 * flushCache = Options.FlushCachePolicy.FALSE,表示查询时不刷新缓存 * timeout = 10000,表示查询结果缓存10000秒 * @param id * @return */ @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) @Select("select * from T_SERVER1 where id=#{id}") Server getServerById(@Param("id") String id); /** * 根据主键id,更新T_SERVER1中的name * @param name * @param id */ @Update("update T_SERVER1 set name=#{name} where id=#{id}") void updateServerNameById(@Param("name") String name, @Param("id") String id);}/** * Server2 Dao(基于Oracle) */public interface Server2OracleDao { /** * 向T_SERVER2中插入一条新数据,其中主键id为32位sys_guid * @param server */ @Insert("insert into T_SERVER2 values (sys_guid(),#{name})") void save(Server server);} MySQL对应的Mapper接口(Server1MysqlDao接口)为:1234567891011/** * Server2 Dao(基于Oracle) */public interface Server2OracleDao { /** * 向T_SERVER2中插入一条新数据,其中主键id为32位sys_guid * @param server */ @Insert("insert into T_SERVER2 values (sys_guid(),#{name})") void save(Server server);} Service层:具体Service层代码将视不同情况来分别列举,下面将详述。 3. Spring事务-传播行为(propagation)Spring事务大多特性都是基于底层数据库的功能来完成的,但是Spring的事务传播行为却是Spring凭借自身框架来实现的功能,它是Spring框架独有的事务增强特性。所谓事务传播行为就是指多个事务方法相互调用时,事务如何在这些方法间传播。Spring提供了七种事务传播行为,下面将详解每一种传播行为。 事务传播行为类型 说明 PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。若当前没有事务,则新建一个事务,若已经存在于一个事务中,则加入到这个事务中。这是最常见的选择。 PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中。总是会启动一个新的事务,若当前没有事务,则新建一个事务,若已经存在于一个事务中,则会将当前事务挂起。 PROPAGATION_NESTED 表示当前方法运行于嵌套事务中。若已经存在于一个事务中,则会在嵌套事务中运行(相当于子事务),且子事务不会影响父事务和其他子事务,但是父事务会影响其所有子事务;若当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 PROPAGATION_SUPPORTS 表示当前方法不需要事务上下文。如果当前没有事务,就以非事务方式执行,若已经存在于一个事务中,则加入到这个事务中。 PROPAGATION_NOT_SUPPORTED 表示当前方法不应该运行在事务中。总是以非事务方式运行,若已经存在于一个事务中,则会将当前事务挂起。 PROPAGATION_MANDATORY 表示当前方法必须在事务中运行。总是想以事务方式运行,若已经存在于一个事务中,则加入到这个事务中,若当前没有事务,则会抛出异常。 PROPAGATION_NEVER 表示当前方法不应该运行于事务上下文中。总是不想以事务方式运行,若已经存在于一个事务中,则会抛出异常,若当前没有事务,则以非事务方式运行。 验证Spring事务传播行为的Service层接口和实现类、验证Spring事务传播的两个Service类、以及测试方法为:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586/** * spring事务传播行为 测试接口 */public interface iTransactionPropagation { // 具体接口方法,下面将分情况描述...}/** * spring事务传播行为 测试实现类 */@Servicepublic class TransactionPropagationImpl implements iTransactionPropagation { /** * Server1 service */ @Autowired private iServer1Service server1Service; /** * Server2 service */ @Autowired private iServer2Service server2Service; /** * Server1 dao(基于Oracle) */ @Resource private Server1OracleDao server1OracleDao; /** * Server2 dao(基于Oracle) */ @Resource private Server2OracleDao server2OracleDao; // 具体接口实现方法,下面将分情况描述...}/** * Server1接口(验证Spring事务传播) */public interface iServer1Service { // 具体接口实现方法,下面将分情况描述...}/** * Server1实现类(验证Spring事务传播) */@Servicepublic class Server1ServiceImpl implements iServer1Service { /** * Server1 Dao(基于Oracle) */ @Resource private Server1OracleDao server1OracleDao; // 具体接口实现方法,下面将分情况描述...}/** * Server2接口(验证Spring事务传播) */public interface iServer2Service { // 具体接口实现方法,下面将分情况描述...}/** * Server2实现类(验证Spring事务传播) */@Servicepublic class Server2ServiceImpl implements iServer2Service { /** * Server2 Dao(基于Oracle) */ @Resource private Server2OracleDao server2OracleDao; // 具体接口实现方法,下面将分情况描述...}/** * 测试类:Spring事务传播行为 */@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath*:/META-INF/spring/applicationContext.xml"})public class TransactionPropagationTest { /** * Spring事务传播行为测试类 */ @Autowired private iTransactionPropagation transactionPropagation; // 具体测试方法,下面将分情况描述...} 这里针对的是不同service类之间方法的调用。 3.1. PROPAGATION_REQUIRED为Server1Service和Server2Service的相应方法加上Propagation.REQUIRED属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** *有事务(传播行为=REQUIRED) * @param server */ @Override @Transactional(propagation = Propagation.REQUIRED) public void saveRequired(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=REQUIRED) * @param server */ @Override @Transactional(propagation = Propagation.REQUIRED) public void saveRequired(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=REQUIRED),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.REQUIRED) public void saveRequiredException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.REQUIRED修饰的内部方法,以验证事务传播特性。 3.1.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得出:外围方法未开启事务,Propagation.REQUIRED修饰的内部方法会启动一个新的事务,且开启的事务相互独立、互不干扰。1234567891011121314151617181920212223242526272829/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=REQUIRED) * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1方法和server2方法各自在自己的事务中独立运行,外围方法的异常不影响内部方法的插入。 */@Overridepublic void noTransactionException_required_required() { server1Service.saveRequired(new Server("服务1")); server2Service.saveRequired(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=REQUIRED),最后抛出异常 * * 结果:“服务1”插入,“服务2”未插入。 * 外围方法未开启事务,server1方法和server2方法各自在自己的事务中独立运行, * 其中server2方法抛出异常只会回滚server2中操作,而server1方法不受影响。 */@Overridepublic void noTransaction_required_requiredException() { server1Service.saveRequired(new Server("服务1")); server2Service.saveRequiredException(new Server("服务2"));} 3.1.2. 外围方法开启事务当外围方法开启事务,三种验证方法及结果情况如下所示。由此可得出:外围方法开启事务,Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,并与外围方法属于同一事务,只要一个方法回滚,整个事务均回滚。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=REQUIRED),最后抛出异常 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1方法和server2方法的内部事务均加入外围方法事务, * 外围方法抛出异常,外围方法和内部方法均回滚。 */@Override@Transactionalpublic void transactionException_required_required() { server1Service.saveRequired(new Server("服务1")); server2Service.saveRequired(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=REQUIRED),最后抛出异常 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1方法和server2方法的内部事务均加入外围方法事务, * server2方法的内部事务抛出异常,外围方法感知异常致使整体事务回滚。 */@Override@Transactionalpublic void transaction_required_requiredException() { server1Service.saveRequired(new Server("服务1")); server2Service.saveRequiredException(new Server("服务2"));}/**验证方法3: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=REQUIRED),最后抛出异常,并被捕获 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1方法和server2方法的内部事务均加入外围方法事务, * server2方法的内部事务抛出异常并在外围方法中捕获,即使server2方法被catch不被外围方法感知,整个事务依然回滚。 * (同一事务中所有方法只要有一个感知到异常,整体事务都回滚) */@Override@Transactionalpublic void transaction_required_requiredExceptionTry() { server1Service.saveRequired(new Server("服务1")); try { server2Service.saveRequiredException(new Server("服务2")); } catch (Exception e) { e.printStackTrace(); }} 3.2. PROPAGATION_REQUIRES_NEW为Server1Service和Server2Service的相应方法加上Propagation.REQUIRES_NEW属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=REQUIRES_NEW) * @param server */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveRequiresNew(Server server) { server1OracleDao.save(server); } }@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=REQUIRES_NEW) * @param server */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveRequiresNew(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=REQUIRES_NEW),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveRequiresNewException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.REQUIRES_NEW修饰的内部方法,以验证事务传播特性。 3.2.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得出:外围方法未开启事务,Propagation.REQUIRES_NEW修饰的内部方法会启动一个新的事务,且开启的事务相互独立、互不干扰。123456789101112131415161718192021222324252627/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=REQUIRES_NEW) * ->server2方法:开启事务(传播行为=REQUIRES_NEW) * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1和server2分别会启动自己的事务独立运行,即使外围方法抛出异常,也不会影响内部方法(不会回滚)。 */@Overridepublic void noTransactionException_requiresNew_requiresNew() { server1Service.saveRequiresNew(new Server("服务1")); server2Service.saveRequiresNew(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=REQUIRES_NEW) * ->server2方法:开启事务(传播行为=REQUIRES_NEW),最后抛出异常 * * 结果:“服务1”插入,“服务2”未插入。 * 外围方法未开启事务,server1和server2分别会启动自己的事务独立运行,其中server2方法中抛出异常会回滚,但不会影响server1的。 */@Overridepublic void noTransaction_requiresNew_requiresNewException() { server1Service.saveRequiresNew(new Server("服务1")); server2Service.saveRequiresNewException(new Server("服务2"));} 3.2.2. 外围方法开启事务当外围方法开启事务,三种验证方法及结果情况如下所示。由此可得出:外围方法开启事务,Propagation.REQUIRES_NEW修饰的内部方法仍会启动一个新的事务,且与外围方法事务和内部方法事务之间均相互独立、互不干扰。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2.1方法:开启事务(传播行为=REQUIRES_NEW) * ->server2.2方法:开启事务(传播行为=REQUIRES_NEW) * * 结果:“服务1”未插入,“服务2.1”和“服务2.2”均插入。 * 外围方法开启事务,server1与外围方法是同一事务,而server2.1和server2.2是分别新建的独立事务, * 当外围方法抛出异常时,与外围方法是同一事务的server1会回滚,但server2.1和server2.2不会回滚。 */@Override@Transactionalpublic void transactionException_required_requiresNew_requiresNew() { server1Service.saveRequired(new Server("服务1")); // 与外围方法是同一事务,会回滚 server2Service.saveRequiresNew(new Server("服务2.1")); // 新建事务,不会回滚 server2Service.saveRequiresNew(new Server("服务2.2")); // 新建事务,不会回滚 throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2.1方法:开启事务(传播行为=REQUIRES_NEW) * ->server2.2方法:开启事务(传播行为=REQUIRES_NEW),最后抛出异常 * * 结果:“服务1”未插入,“服务2.1”插入,“服务2.2”未插入。 * 外围方法开启事务,server1与外围方法是同一事务,而server2.1和server2.2是分别新建的独立事务, * 当server2.2抛出异常时,server2.2的事务会回滚,外围方法也会感知到异常,server1也会回滚, * 而server2.1在新建的独立事务中,不会回滚。 */@Override@Transactionalpublic void transaction_required_requiresNew_requiresNewException() { server1Service.saveRequired(new Server("服务1")); // 与外围方法是同一事务,会回滚 server2Service.saveRequiresNew(new Server("服务2.1")); // 新建事务,不会回滚 server2Service.saveRequiresNewException(new Server("服务2.2")); // 新建事务,会回滚}/**验证方法3: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2.1方法:开启事务(传播行为=REQUIRES_NEW) * ->server2.2方法:开启事务(传播行为=REQUIRES_NEW),最后抛出异常,并捕获 * * 结果:“服务1”插入,“服务2.1”插入,“服务2.2”未插入。 * 外围方法开启事务,server1与外围方法是同一事务,而server2.1和server2.2是分别新建的独立事务, * 当server2.2抛出异常时,server2.2的事务会回滚,外围方法catch住了这个异常,故server1不会回滚, * server2.1在新建的独立事务中,也不会回滚。 */@Override@Transactionalpublic void transaction_required_requiresNew_requiresNewExceptionTry() { server1Service.saveRequired(new Server("服务1")); // 与外围方法是同一事务,不会回滚 server2Service.saveRequiresNew(new Server("服务2.1")); // 新建事务,不会回滚 try { server2Service.saveRequiresNewException(new Server("服务2.2")); // 新建事务,会回滚 } catch (Exception e) { e.printStackTrace(); }} 3.3. PROPAGATION_NESTED为Server1Service和Server2Service的相应方法加上Propagation.NESTED属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=NESTED) * @param server */ @Override @Transactional(propagation = Propagation.NESTED) public void saveNested(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** *有事务(传播行为=NESTED) * @param server */ @Override @Transactional(propagation = Propagation.NESTED) public void saveNested(Server server) { server2OracleDao.save(server); } /** *有事务(传播行为=NESTED),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.NESTED) public void saveNestedException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.NESTED修饰的内部方法,以验证事务传播特性。 3.3.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得:外围方法未开启事务,Propagation.PROPAGATION_NESTED和Propagation.PROPAGATION_REQUIRED作用相同,修饰内部的方法分别会启动自己的事务,且启动的事务相互独立、互不干扰。12345678910111213141516171819202122232425262728/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=NESTED) * ->server2方法:开启事务(传播行为=NESTED) * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1和server2分别在自己的事务中独立运行,外围方法的异常不影响内部方法的插入。 */@Overridepublic void noTransactionException_Nested_Nested() { server1Service.saveNested(new Server("服务1")); server2Service.saveNested(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=NESTED) * ->server2方法:开启事务(传播行为=NESTED),最后抛出异常 * * 结果:“服务1”插入,“服务2”未插入。 * 外围方法未开启事务,server1和server2分别在自己的事务中独立运行, * 其中server2中抛出异常,其事务会回滚,但是不会影响server1的事务。 */@Overridepublic void noTransaction_Nested_NestedException() { server1Service.saveNested(new Server("服务1")); server2Service.saveNestedException(new Server("服务2"));} 3.3.2. 外围方法开启事务当外围方法开启事务,三种验证方法及结果情况如下所示。由此可得:外围方法开启事务,Propagation.PROPAGATION_NESTED修饰的内部方法属于外围事务的子事务,外围父事务回滚,则其所有子事务都回滚,若其中一个子事务回滚,则不会影响外围父事务和其他内部事务。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=NESTED) * ->server2方法:开启事务(传播行为=NESTED) * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,内部事务是外围事务的子事务,外围方法抛出异常,导致其子事务(server1和server2)也需要回滚。 */@Override@Transactionalpublic void transactionException_Nested_Nested() { server1Service.saveNested(new Server("服务1")); server2Service.saveNested(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=NESTED) * ->server2方法:开启事务(传播行为=NESTED),最后抛出异常 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,内部事务是外围事务的子事务,内部方法server2中抛出异常,使得server2会回滚, * 而外围方法可以感知到异常,会使其所有子事务都回滚,故而server1也会回滚。 */@Override@Transactionalpublic void transaction_Nested_NestedException() { server1Service.saveNested(new Server("服务1")); server2Service.saveNestedException(new Server("服务2"));}/**验证方法3: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=NESTED) * ->server2方法:开启事务(传播行为=NESTED),最后抛出异常,并捕获 * * 结果:“服务1”插入,“服务2”未插入。 * 外围方法开启事务,内部事务是外围事务的子事务,内部方法server2中抛出异常,使得server2会回滚, * 而外围方法由于catch住了异常,无法感知到异常,故而server1不会回滚。 */@Override@Transactionalpublic void transaction_Nested_NestedExceptionTry() { server1Service.saveNested(new Server("服务1")); try { server2Service.saveNestedException(new Server("服务2")); } catch (Exception e) { e.printStackTrace(); }} 【注1】:Spring事务传播行为中PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW和PROPAGATION_NESTED的区别?REQUIRED是默认的事务传播行为。 REQUIRED、REQUIRES_NEW和NESTED这3种传播行为在修饰内部方法时,若外围方法无事务,都会新建一个新事务,且事务之间相互独立、互不干扰。 当外围方法有事务情况,REQUIRED和NESTED修饰的内部方法都属于外围方法事务,若外围方法抛出异常,都会回滚。但是,REQUIRED是加入外围事务,与外围方法属于同一事务,不管谁抛出异常,都会回滚;而NESTED是属于外围事务的子事务,有单独的保存点(savepoint),被NESTED修饰的内部方法(子事务)抛出异常会回滚,但不会影响到外围方法事务。 无论外围方法是否有事务,REQUIRES_NEW和NESTED修饰的内部方法抛出异常都不会影响到外围方法事务。当外围方法有事务情况,由于NESTED是嵌套事务,其修饰的内部方法为子事务,一旦外围方法事务回滚,会影响其所有子事务都回滚;而由于REQUIRES_NEW修饰的内部方法为启动一个新事务来实现的,故而内部事务和外围事务相互独立,外围事务回滚并不会影响到内部事务。 3.4. PROPAGATION_SUPPORTS为Server1Service和Server2Service的相应方法加上Propagation.SUPPORTS属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=SUPPORTS) * @param server */ @Override @Transactional(propagation = Propagation.SUPPORTS) public void saveSupports(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=SUPPORTS) * @param server */ @Override @Transactional(propagation = Propagation.SUPPORTS) public void saveSupports(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=SUPPORTS),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.SUPPORTS) public void saveSupportsException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.SUPPORTS修饰的内部方法,以验证事务传播特性。 3.4.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得出:外围方法未开启事务,Propagation.SUPPORTS修饰的内部方法以非事务方式运行,即使出现异常,也不会回滚。123456789101112131415161718192021222324252627/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=SUPPORTS) * ->server2方法:开启事务(传播行为=SUPPORTS) * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1和server2以非事务方式运行,外围方法抛出的异常不会影响server1和server2。 */@Overridepublic void noTransactionException_Supports_Supports() { server1Service.saveSupports(new Server("服务1")); server2Service.saveSupports(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=SUPPORTS) * ->server2方法:开启事务(传播行为=SUPPORTS),最后抛出异常,并捕获 * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1和server2以非事务方式运行,即使server2中抛出异常,server1和server2都不会回滚。 */@Overridepublic void noTransaction_Supports_SupportsException() { server1Service.saveSupports(new Server("服务1")); server2Service.saveSupportsException(new Server("服务2"));} 3.4.2. 外围方法未开启事务当外围方法开启事务,三种验证方法及结果情况如下所示。由此可得出:外围方法开启事务,Propagation.SUPPORTS修饰的内部方法会加入外围事务,任一事务回滚,整个事务均会回滚。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=SUPPORTS) * ->server2方法:开启事务(传播行为=SUPPORTS) * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1和server2加入外围事务,当外围方法抛出异常,server1和server都会回滚。 */@Override@Transactionalpublic void transactionException_Supports_Supports() { server1Service.saveSupports(new Server("服务1")); server2Service.saveSupports(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=SUPPORTS) * ->server2方法:开启事务(传播行为=SUPPORTS),最后抛出异常 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1和server2加入外围事务,其中server2中抛出异常,影响所有事务都回滚。 */@Override@Transactionalpublic void transaction_Supports_SupportsException() { server1Service.saveSupports(new Server("服务1")); server2Service.saveSupportsException(new Server("服务2"));}/**验证方法3: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=SUPPORTS) * ->server2方法:开启事务(传播行为=SUPPORTS),最后抛出异常,并捕获 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1和server2加入外围事务,其中server2抛出异常,虽然在外围方法中catch住了,所有事务仍会都回滚。 */@Override@Transactionalpublic void transaction_Supports_SupportsExceptionTry() { server1Service.saveSupports(new Server("服务1")); try { server2Service.saveSupportsException(new Server("服务2")); } catch (Exception e) { e.printStackTrace(); }} 3.5. PROPAGATION_NOT_SUPPORTED为Server1Service和Server2Service的相应方法加上Propagation.NOT_SUPPORTED属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=NOT_SUPPORTED) * @param server */ @Override @Transactional(propagation = Propagation.NOT_SUPPORTED) public void saveNotSupported(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=NOT_SUPPORTED) * @param server */ @Override @Transactional(propagation = Propagation.NOT_SUPPORTED) public void saveNotSupported(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=NOT_SUPPORTED),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.NOT_SUPPORTED) public void saveNotSupportedException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.NOT_SUPPORTED修饰的内部方法,以验证事务传播特性。 3.5.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得出:外围方法未开启事务,Propagation.NOT_SUPPORTED修饰的内部方法以非事务方式运行,不会被影响而回滚。12345678910111213141516171819202122232425/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=SUPPORTS) * * 结果:“服务1”插入。 * 外围方法未开启事务,server1以非事务方式运行,即使外围方法抛出异常,也不会回滚。 */@Overridepublic void noTransactionException_notSuppored() { server1Service.saveNotSupported(new Server("服务1")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=NOT_SUPPORTED) * ->server2方法:开启事务(传播行为=NOT_SUPPORTED),且抛出异常 * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,被NOT_SUPPORTED修饰的server1和server2,即使server2中抛出异常,server1和server2都会以非事务方式运行,不会回滚。 */@Overridepublic void noTransaction_notSuppored_notSupporedException() { server1Service.saveNotSupported(new Server("服务1")); server2Service.saveNotSupportedException(new Server("服务2"));} 3.5.2. 外围方法开启事务当外围方法开启事务,两种验证方法及结果情况如下所示。由此可得出:外围方法开启事务,Propagation.NOT_SUPPORTED修饰的内部方法以非事务方式运行,不会被影响而回滚。12345678910111213141516171819202122232425262728293031/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=NOT_SUPPORTED) * * 结果:“服务1”未插入,“服务2”插入。 * 外围方法开启事务,server1加入外围事务,server2以非事务方式运行, * 当外围方法抛出异常时,server1会回滚,server2不受影响。 */@Override@Transactionalpublic void transactionException_required_notSuppored() { server1Service.saveRequired(new Server("服务1")); // 与外围事务同事务,会回滚 server2Service.saveNotSupported(new Server("服务2")); // 非事务,不会回滚 throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=REQUIRED) * ->server2方法:开启事务(传播行为=NOT_SUPPORTED),最后抛出异常 * * 结果:“服务1”未插入,“服务2”插入。 * 外围方法开启事务,server1加入外围事务,server2以非事务方式运行, * 其中server2中抛出异常,但server2不会回滚,而外围方法会感知到异常,影响server1会回滚。 */@Override@Transactionalpublic void transaction_required_notSupporedException() { server1Service.saveRequired(new Server("服务1")); // 与外围事务同事务,会回滚 server2Service.saveNotSupportedException(new Server("服务2")); // 非事务,不会回滚} 3.6. PROPAGATION_MANDATORY为Server1Service和Server2Service的相应方法加上Propagation.MANDATORY属性。1234567891011121314151617181920212223242526272829303132333435363738@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=MANDATORY) * @param server */ @Override @Transactional(propagation = Propagation.MANDATORY) public void saveMandatory(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=MANDATORY) * @param server */ @Override @Transactional(propagation = Propagation.MANDATORY) public void saveMandatory(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=MANDATORY),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.MANDATORY) public void saveMandatoryException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.MANDATORY修饰的内部方法,以验证事务传播特性。 3.6.1. 外围方法未开启事务当外围方法未开启事务,一种验证方法及结果情况如下所示。由此可得出:外围方法未开启事务,当外围方法调用Propagation.MANDATORY修饰的内部方法会抛出异常。123456789101112/**验证方法1: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=MANDATORY) * * 结果:“服务1”未插入。 * 外围方法未开启事务,外围方法调用server1时,会抛出异常 * (org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory') */@Overridepublic void noTransaction_Mandatory() { server1Service.saveMandatory(new Server("服务1"));} 3.6.2. 外围方法开启事务当外围方法开启事务,三种验证方法及结果情况如下所示。由此可得出:外围方法开启事务,Propagation.MANDATORY修饰的内部方法会加入外围事务,任一事务回滚,整个事务均会回滚。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/**验证方法1: * 外围方法:开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=MANDATORY) * ->server2方法:开启事务(传播行为=MANDATORY) * * 结果:“服务1”和“服务2”均未插入。 * 外围方法未开启事务,内部方法事务加入外围事务,外围方法抛出异常,server1和server2均会回滚。 */@Override@Transactionalpublic void transactionException_Mandatory_Mandatory() { server1Service.saveMandatory(new Server("服务1")); server2Service.saveMandatory(new Server("服务2")); throw new RuntimeException();}/**验证方法2: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=MANDATORY) * ->server2方法:开启事务(传播行为=MANDATORY),最后抛出异常 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法未开启事务,内部方法事务加入外围事务,其中server2中抛出异常,server2回滚,外围方法感知到异常,也会导致server1回滚。 */@Override@Transactionalpublic void transaction_Mandatory_MandatoryException() { server1Service.saveMandatory(new Server("服务1")); server2Service.saveMandatoryException(new Server("服务2"));}/**验证方法3: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=MANDATORY) * ->server2方法:开启事务(传播行为=MANDATORY),最后抛出异常,并捕获 * * 结果:“服务1”和“服务2”均未插入。 * 外围方法开启事务,server1和server2加入外围事务,其中server2中抛出异常,虽然在外围方法中catch住了,所有事务仍会都回滚。 */@Override@Transactionalpublic void transaction_Mandatory_MandatoryExceptionTry() { server1Service.saveMandatory(new Server("服务1")); try { server2Service.saveMandatoryException(new Server("服务2")); } catch (Exception e) { e.printStackTrace(); }} 3.7. PROPAGATION_NEVER为Server1Service和Server2Service的相应方法加上Propagation.NEVER属性。12345678910111213141516171819202122232425262728293031323334353637@Servicepublic class Server1ServiceImpl implements iServer1Service { // 省略其他... /** * 有事务(传播行为=NEVER) * @param server */ @Override @Transactional(propagation = Propagation.NEVER) public void saveNever(Server server) { server1OracleDao.save(server); }}@Servicepublic class Server2ServiceImpl implements iServer2Service { // 省略其他... /** * 有事务(传播行为=NEVER) * @param server */ @Override @Transactional(propagation = Propagation.NEVER) public void saveNever(Server server) { server2OracleDao.save(server); } /** * 有事务(传播行为=NEVER),且存在异常 * @param server */ @Override @Transactional(propagation = Propagation.NEVER) public void saveNeverException(Server server) { server2OracleDao.save(server); throw new RuntimeException(); }} 具体代码验证分为两种场景:一种是外围方法未开启事务,另一种是外围方法开启事务,这两种会调用Propagation.NEVER修饰的内部方法,以验证事务传播特性。 3.7.1. 外围方法未开启事务当外围方法未开启事务,两种验证方法及结果情况如下所示。由此可得:外围方法未开启事务,当外围方法调用Propagation.NEVER修饰的内部方法,内部方法会以非事务方式运行,不会被影响而回滚。12345678910111213141516171819202122232425/**验证方法1: * 外围方法:未开启事务,最后抛出异常 * ->server1方法:开启事务(传播行为=NEVER) * * 结果:“服务1”插入。 * 外围方法未开启事务,server1以非事务方式运行,即使外围方法抛出异常,也不受影响,不会回滚。 */@Overridepublic void noTransactionException_never() { server1Service.saveNever(new Server("服务1")); throw new RuntimeException();}/**验证方法2: * 外围方法:未开启事务 * ->server1方法:开启事务(传播行为=NEVER) * ->server2方法:开启事务(传播行为=NEVER),最后抛出异常 * * 结果:“服务1”和“服务2”均插入。 * 外围方法未开启事务,server1和server2均以非事务方式运行,即使server2中抛出异常,也不受影响,均不会回滚。 */@Overridepublic void noTransaction_never_neverException() { server1Service.saveNever(new Server("服务1")); server2Service.saveNeverException(new Server("服务2"));} 3.7.2. 外围方法开启事务当外围方法开启事务,一种验证方法及结果情况如下所示。由此可得:外围方法开启事务,当外围方法调用Propagation.NEVER修饰的内部方法,会抛出异常。12345678910111213/**验证方法1: * 外围方法:开启事务 * ->server1方法:开启事务(传播行为=NEVER) * * 结果:“服务1”未插入。 * 外围方法开启事务,当调用被NEVER修饰的server1内部方法时,会抛出异常 * (org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never')。 */@Override@Transactionalpublic void transaction_never() { server1Service.saveNever(new Server("服务1"));} 4. Spring事务-隔离级别(isolation)当多个事务同时操作同一数据库的记录时,这就会涉及并发控制和数据库隔离性问题了,其中隔离级别是数据库的事务特性ACID的一部分。Spring事务定义的隔离级别共有5个:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。下面将详述每种隔离级别。 4.1. DEFAULTSpring默认隔离级别,使用后端数据库默认的隔离级别。大多数数据库默认的事务隔离级别是Read committed,比如Sql Server、Oracle,MySQL的默认隔离级别是Repeatable read。 4.2. READ_UNCOMMITTED读未提交:允许脏读,也就是一个事务可以读取到其他事务未提交的记录。隔离性最弱,并发性最高。见下图(MySQL环境),事务B更新数据后且尚未提交,事务A能读取到事务B未提交的数据“server1”,但是之后事务B回滚,此时事务A再次读取到的数据为之前的旧数据“服务1”,因此事务A读取到的数据就不是有效的,这种情况称为脏读。除了脏读,还会存在不可重复读和幻读的问题。需要注意的是,当我们基于Oracle数据库来通过Spring设置隔离级别为READ_UNCOMMITTED和REPEATABLE_READ时会有问题,具体如下:12345678910111213141516/**READ_UNCOMMITTED(读未提交)[Oracle]:A事务可以读取到B事务未提交的事务记录(B事务可能回滚)。 * 隔离性最低、并发性最好。存在脏读、不可重复读和幻读问题。 * * Oracle支持READ COMMITTED和SERIALIZABLE这两种事务隔离级别,默认为READ COMMITTED。 * 若以Isolation.READ_UNCOMMITTED或Isolation.REPEATABLE_READ访问,则会抛出如下异常: * org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; * nested exception is java.sql.SQLException: 仅 READ_COMMITTED 和 SERIALIZABLE 是有效的事务处理级 */@Override@Transactional(value = "oracleTM", isolation = Isolation.READ_UNCOMMITTED)public void readUncommittedByOracle() { System.out.println("开始 READ_UNCOMMITTED[Oracle]..."); List<Server> serverList = serverOracleDao.getAllServers(); System.out.println("serverList: " + serverList); System.out.println("结束 READ_UNCOMMITTED[MySQL]...");} 当使用基于Oracle且设置隔离级别为READ_UNCOMMITTED和REPEATABLE_READ时,会抛出异常:org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: 仅 READ_COMMITTED 和 SERIALIZABLE 是有效的事务处理级。这是因为,Oracle不支持READ_UNCOMMITTED和REPEATABLE_READ这两种事务隔离级别,支持READ_COMMITTED和SERIALIZABLE,默认隔离级别为READ_COMMITTED。 4.3. READ_COMMITTED读已提交:一个事务只能读取到已经提交的记录,不能读取到未提交的记录。因此,脏读问题不会再出现,但可能出现其他问题。READ_COMMITTED解决了脏读问题。见下图(MySQL环境),事务B更新数据后且未提交,此时事务A读取到的是旧数据,接着事务B提交后,事务A再次读取到的是新数据,两次读取到的数据不一致,这种情况称为不可重复读。除了不可重复读问题,还存在幻读问题。 下面事务执行过程中,事务A设置为Read Committed并开始事务,当事务A查询某个id时,事务B可以直接更新指定id而无需等待,说明查询操作不会加锁;当事务A更新指定id时,事务B会出现等待,直至事务A提交后才会执行更新操作,说明更新操作会加锁。 4.4. REPEATABLE_READ重复读:一个事务可以多次从数据库读取某条记录,而且多次读取的那条记录都是一致的。REPEATABLE_READ解决了脏读和不可重复读问题。见下图(MySQL环境),在事务A前两次查询之间,事务B更新和插入数据并自动提交,发现事务A两次读取数据一致。这是因为,MySQl的存储引擎InnoDB通过多版本并发控制(MVCC,Multi-Version Concurrency Control)机制解决了该问题,实现了同一事务中多次读取某条记录(即使这条记录被其他事务更新或插入)的结果始终保持一致。但是,当事务A尚未提交,并插入id为’333’的数时,提示插入失败显示主键重复,说明该记录已存在。当事务A提交后,在以相同条件进行查询,可以发现事务B更新和插入后的数据。 4.5. SERIALIZABLE串读:事务执行时,会在涉及数据上加锁,强制事务排序,使之不会相互冲突。隔离性最强,并发性最弱。见下图(MySQL环境),事务A隔离级别设置为Serializable,并查询表中id为’111’的数据,该操作将会锁住被读取行数据,当事务B尝试去更新表中id为’111’的数据时,会一直等待,直至事务A提交,才会执行更新操作;而当事务B去更新id为’222’的数据时,不受影响,直接更新完,即不同行锁不会相互影响。 下面串行事务中,事务A隔离级别设置为Serializable,并查询整个表的数据,将会对整个表加锁,因此当事务B进行更新操作或者插入操作时,都将进入等待,直至事务A提交,才能开始进行操作。 5. Spring事务-超时(timeout)Spring事务参数timeout为超时时间,默认值为-1,指没有超时限制。如果超过设置的超时时间,事务还没有完成的话,则会抛出事务超时异常TransactionTimedOutException,并回滚事务。在下面的事务超时测试示例中,事务超时时间设置为2秒。在saveServer1_saveServer2_sleep()方法中,sleep操作(为了模拟超时场景)放在两个保存操作之后,在执行完两个保存之后出现超时情况,此时由结果可知,两个保存操作均插入。在saveServer1_sleep_saveServer2()方法中,sleep操作放在两个保存操作之间,在执行完第一个保存之后出现超时情况,此时由结果可知,两个保存操作均未插入。结论:Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。如下代码中,事务超时区间为事务开始到第二个保存操作,之后的操作超时将不会引起事务回滚。因此,当设置了超时参数,需要考虑到重要的操作不要放到最后执行,或是在操作最后加上一个无关紧要的Statement操作。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556/** * Spring超时测试 实现类 */@Servicepublic class TimeoutImpl implements iTimeout { @Resource private Server1OracleDao server1Dao; @Resource private Server2OracleDao server2Dao; /** * 超时时间设置为2(单位为秒,默认为-1,表示无超时无限制)。 * 执行顺序为:saveServer1 --> saveServer2 --> sleep 5 秒 * 结果:“服务1”和“服务2”均插入。 * 事务没有因为超时而回滚(事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。) * 原因:Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。所以在在执行Statement之外的超时无法进行事务回滚。 * @throws InterruptedException */ @Override @Transactional(timeout = 2, rollbackFor = Exception.class) public void saveServer1_saveServer2_sleep() throws InterruptedException { System.out.println("\n开始保存 Server1..."); server1Dao.save(new Server("服务1")); System.out.println("结束保存 Server1..."); System.out.println("\n开始保存 Server2..."); server2Dao.save(new Server("服务2")); System.out.println("结束保存 Server2..."); System.out.println("\n开始等待..."); Thread.sleep(5000); System.out.println("结束等待..."); } /** * 超时时间设置为2(单位为秒)。 * 执行顺序为:saveServer1 --> sleep 5 秒 --> saveServer2 * 结果:“服务1”和“服务2”均未插入。 * 事务成功回滚,抛出事务超时异常org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Thu Apr 25 11:39:06 CST 2019 * 总结:重要的操作不要放到最后一个Statement后面,尽量放到Statement中间,或是在操作后加上一个无关紧要的Statement操作。 * @throws InterruptedException */ @Override @Transactional(timeout = 2, rollbackFor = Exception.class) public void saveServer1_sleep_saveServer2() throws InterruptedException { System.out.println("\n开始保存 Server1..."); server1Dao.save(new Server("服务1")); System.out.println("结束保存 Server1..."); System.out.println("\n开始等待..."); Thread.sleep(5000); System.out.println("结束等待..."); System.out.println("\n开始保存 Server2..."); server2Dao.save(new Server("服务2")); System.out.println("结束保存 Server2..."); }} 6. Spring事务-只读(readOnly)Spring的只读事务readOnly参数设置为true时,说明当前方法没有增改删的操作,Spring会优化这个方法,即使用了一个只读的connection,效率会高很多。建议使用场景为:当前方法查询量较大,且确保不会出现增改删情况;防止当前方法会出现增改删操作。在如下示例中可知,设置为只读事务,基于MySQl执行保存操作会抛出异常,而基于Oracle执行保存操作则成功插入,不受readOnly参数影响。Spring的只读事务并不是一个强制指令,它相当于一个提醒,提醒数据库当前事务为只读事务,不包含增改删操作,那么数据库则可能会根据情况进行一些特定的优化,如不考虑加相应的锁,减轻数据库的资源消耗。当然,并不是所有的数据库都支持只读事务,默认情况下在设置只读参数后,Oracle依旧可以进行增改删操作。1234567891011121314151617181920212223242526272829303132333435/** * Spring事务只读测试 实现类 */@Servicepublic class ReadOnlyImpl implements iReadOnly { /** * Server1 dao(基于Oracle) */ @Resource private Server1OracleDao server1OracleDao; /** * Server1 dao(基于MySQL) */ @Resource private Server1MysqlDao server1MysqlDao; /** * 基于MySQL - 只读 * 执行保存操作失败,会报错。 * ### Error updating database. Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed */ @Override @Transactional(value = "mysqlTM", readOnly = true) public void saveServerByMysql() { server1MysqlDao.save(new Server("服务1")); } /** * 基于Oracle - 只读 * 执行保存操作成功,不受只读设置影响。 */ @Override @Transactional(value = "oracleTM", readOnly = true) public void saveServerByOracle() { server1OracleDao.save(new Server("服务1")); }} 7. Spring事务-回滚规则(rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName)Spring事务的回滚规则,如rollbackFor、rollbackForClassName、noRollbackFor和noRollbackForClassName,指定了遇到什么异常进行回滚,或者遇到什么异常不回滚。 rollbackFor:设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。(默认为RuntimeException)。 rollbackForClassName:设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。 noRollbackFor:设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。 noRollbackForClassName:设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。 这些回滚规则均可指定单一异常类或者多个异常类,如: rollbackFor和noRollbackFor指定单一异常类形式为:@Transactional(rollbackFor=RuntimeException.class),@Transactional(noRollbackFor=RuntimeException.class) rollbackFor和noRollbackFor指定多个异常类形式为:@Transactional(rollbackFor={RuntimeException.class, Exception.class}),@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) rollbackForClassName和noRollbackForClassName指单一异常类名称形式为:@Transactional(rollbackForClassName="RuntimeException"),@Transactional(noRollbackFor="RuntimeException") rollbackForClassName和noRollbackForClassName指多个异常类名称形式为:@Transactional(rollbackForClassName={"RuntimeException", "Exception"}),@Transactional(noRollbackFor={"RuntimeException", "Exception"}) 参考资料[1] 陈雄华. Spring 3.x 企业应用开发实战[M]. 电子工业出版社. 2012.[2] Spring事务传播行为详解. https://segmentfault.com/a/1190000013341344#articleHeader14.[3] Spring事务隔离级别简介及实例解析. https://www.jb51.net/article/134466.htm.[4] MySQL的四种事务隔离级别. https://www.cnblogs.com/huanongying/p/7021555.html.[5] Spring官方文档-事务. https://docs.spring.io/spring/docs/5.0.9.RELEASE/spring-framework-reference/data-access.html#transaction.[6] Spring事务采坑 —— timeout. https://blog.csdn.net/qq_18860653/article/details/79907984.[7] Spring 使用注解方式进行事务管理. https://www.cnblogs.com/younggun/p/3193800.html.]]></content>
<categories>
<category>Spring框架系列</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java.util集合(collection)之LinkedList源码分析(jdk1.8)]]></title>
<url>%2F2019%2F04%2F18%2Fcollection-linkedlist-sourcecode%2F</url>
<content type="text"><![CDATA[1. 简介LinkedList是基于双向链表实现。它是一种可以在任意位置进行高效地插入和移除操作的有序序列。LinkedList是线程不安全的,若需在多线程环境使用,主要方法有:① 使用List list = Collections.synchronizedList(new LinkedList(…));② 使用ConcurrentLinkedQueue;③ 使用synchronized关键字。 LinkedList在jdk1.6时为带有头结点的双向循环链表,jdk1.7和jdk1.8为不带头结点的普通的双向链表,示意图如下: 从下图可以得知,LinkedList继承于AbstractSequentialList,实现了List、Deque、Cloneable、java.io.Serializable这些接口。 继承AbstractSequentialList抽象类,提供序列化访问,只支持按次序访问,不像AbstractList那样支持随机访问。 实现List接口,提供了List接口的所有方法实现。 实现Deque接口,使得LinkedList具有双端队列特质。 实现Cloneable接口,支持可拷贝,即覆盖了函数clone()。 实现java.io.Serializable接口,支持序列化。 2. 属性与存储模型2.1. 属性size1transient int size = 0; 实际元素个数,存放当前链表有多少个节点。 first1transient Node<E> first; 指向链表的第一个节点的引用。Invariant: (first == null && last == null) || (first.prev == null && first.item != null)。 last1transient Node<E> last; 指向链表的最后一个节点的引用。Invariant: (first == null && last == null) || (last.next == null && last.item != null)。 2.2. 存储模型Node为LinkedList的内部类,是实际存放元素的地方。12345678910private static class Node<E> { E item; // 元素 Node<E> next; // 下一个节点 Node<E> prev; // 上一个节点 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }} 3. 构造方法LinkedList提供了二种方式的构造函数,分别如下: LinkedList() public LinkedList():无参构造函数,构造一个空列表。12public LinkedList() {} LinkedList(Collection<? extends E> c) public LinkedList(Collection<? extends E> c):构造一个包含指定集合的列表。1234public LinkedList(Collection<? extends E> c) { this(); // 调用无参构造函数 addAll(c); // 将指定集合c添加至当前链表末尾} 这里addAll(int index, Collection<? extends E> c)方法逻辑详见下面常用方法的分析,点击此处跳转。 4. 常用方法总述在学习LinkedList的常用方法时,其内部主要的辅助方法主要有: private void linkFirst(E e):在链表头部插入一个新元素。 void linkLast(E e):在链表尾部插入一个新元素。 void linkBefore(E e, Node<E> succ):在某个非空节点前插入一个新元素。 private E unlinkFirst(Node<E> f):移除链表中的第一个节点,并返回旧值。 private E unlinkLast(Node<E> l):移除链表中的最后一个节点,并返回旧值。 E unlink(Node<E> x):移除链表的一个非空节点,并返回旧值。 add(E e) public boolean add(E e):添加指定值为e的节点至当前链表的尾部。123456789101112131415161718192021public boolean add(E e) { linkLast(e); // 添加一个值为e的新节点至链表尾部 return true;}// 添加一个新节点至链表末尾,并更新first和或last指向void linkLast(E e) { // 记录原尾节点位置给l,且l为final类型,不可更改 final Node<E> l = last; // 生成一个新节点:前驱指向当前链表的尾节点,值为e,后继指向null final Node<E> newNode = new Node<>(l, e, null); // 更新last指向新节点newNode last = newNode; if (l == null) // 若l为null,说明刚添加的newNode为第一个节点,将first指向第一个节点newNode first = newNode; else // 若l非null,则将l的后继指向新节点newNode l.next = newNode; // 更新size加1 size++; // 更新modCount加1 modCount++;} 整体流程:记录当前链表的last位置为l –> 生成一个新节点(前驱指向链表尾节点,值为e,后继指向null) –> 更新last指向新生成节点;若l为null,更新first指向新生成节点,否则,令链表中原尾节点指向新生成节点 –> 更新size和modCount都加1 –> 添加成功,返回true。 【注1】 LinkedList链表调用add(E e)方法添加新元素时结构变化过程以及示意图。LinkedList新增元素的示例代码如下:123List<String> list = new LinkedList<>();list.add("a");list.add("b"); 根据上述代码执行过程,具体结构变化示意图如下: add(int index, E element) public void add(int index, E element):在指定位置index插入一个值为element的新节点。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859public void add(int index, E element) { // 检查待插位置index是否越界[0,size] checkPositionIndex(index); if (index == size) // 若待插位置index为链表尾部,则调用linkLast()插至末尾 linkLast(element); else // 若待插位置index非链表尾部,则调用linkBefore()插至中间 linkBefore(element, node(index));}// 越界检查private void checkPositionIndex(int index) { if (!isPositionIndex(index)) // 如果index不在0~size范围,则抛出IndexOutOfBoundsException异常 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}// 检查参数index是否处于一个有效的位置[0,size]private boolean isPositionIndex(int index) { return index >= 0 && index <= size;}// 越界信息输出private String outOfBoundsMsg(int index) { return "Index: "+index+", Size: "+size;}// 返回链表中index处的节点Node<E> node(int index) { // assert isElementIndex(index); // 判断index是否小于整个链表长度的一半(size>>1,右移1位,相当于size/2), // 判断要插入的位置是距离链表头近还是链表尾近,找到原index处的节点并返回 if (index < (size >> 1)) { // 距离链表头近情况 Node<E> x = first; // 从头节点开始往后遍历,寻找index处的节点 for (int i = 0; i < index; i++) x = x.next; return x; } else { // 距离链表尾近情况 Node<E> x = last; // 从尾节点往前遍历,寻找index处的节点 for (int i = size - 1; i > index; i--) x = x.prev; return x; }}// 在非空节点succ前插入一个值为e的新节点void linkBefore(E e, Node<E> succ) { // assert succ != null; // 记录succ的前驱指向 final Node<E> pred = succ.prev; // 生成一个新节点newNode:前驱指向pred指向的位置,值为e,后继指向succ(即新节点插在节点succ前面) final Node<E> newNode = new Node<>(pred, e, succ); // 令succ的前驱指向新节点newNode succ.prev = newNode; if (pred == null) // 若pred指向为null,说明新节点插在第一位,需更新first指向新节点newNode first = newNode; else // 若pred指向非空,更新pred的后继指向新节点newNode pred.next = newNode; // 更新size加1 size++; // 更新modCount加1 modCount++;} 整体流程:检查待插位置index是否越界 –> 若待插位置index等于链表中节点大小,则将新节点插入至链表末尾,否则插入至链表中间。这里插至尾部的方法为linkLast(E e),前面add(E e)方法中已有详细分析,此处不再赘述;而插至中间的方法为linkBefore(E e, Node succ),在插入之前需要调用node(int index)方法找到待插位置index的节点。【注2】 LinkedList中如何根据索引定位到指定节点数据? 由于ArrayList基于数组可直接根据索引找到对应节点,而LinkedList基于链表,只有通过遍历才能找到对应的节点。为了更快速的找到index处的节点,通过判断index处于链表的前半段还是后半段,来决定是从头部往后遍历寻找还是从尾部往前遍历。linkBefore(E e, Node succ)执行流程为:记录节点succ(新节点将插在该节点前面)的前驱指向为pred –> 生成一个新节点(前驱指向pred,值为e,后继指向succ) –> 令succ的前驱指向新节点;若pred为null,更新first指向新生成节点,否则,令pred的后继指向新生成节点 –> 更新size和modCount都加1。linkBefore(E e, Node succ)方法执行示意图如下: addAll(Collection<? extends E> c)和addAll(int index, Collection<? extends E> c) public boolean addAll(Collection<? extends E> c):将指定集合c中的所有元素插入至当前链表末尾。 public boolean addAll(int index, Collection<? extends E> c):将指定集合c插入链表中index处位置。addAll()有两个重载函数,其中addAll(Collection<? extends E> c)内部会调用addAll(int index, Collection<? extends E> c),故此着重分析addAll(int index, Collection<? extends E> c)方法。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051public boolean addAll(Collection<? extends E> c) { // 将集合c插至链表中size处位置,即插至尾部 return addAll(size, c);}public boolean addAll(int index, Collection<? extends E> c) { // 检查待插位置index是否越界 checkPositionIndex(index); // 将参数集合c转为Object型数组 Object[] a = c.toArray(); // 令numNew为参数集合c的长度 int numNew = a.length; // 若待插集合c为空,则返回false if (numNew == 0) return false; // pred指向待插节点位置的前一个节点,succ指向待插节点位置的后一个节点 Node<E> pred, succ; if (index == size) { // 若插至尾部,则令succ指向null,pred指向链表中的尾节点last succ = null; pred = last; } else { // 若插至中间,则令succ指向index处的节点,pred指向index处前一个节点 succ = node(index); pred = succ.prev; } // 遍历集合中所有元素,使其按次序插入链表中 for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; // 待插元素转型 // 新生成一个节点:前驱指向pred,值为e,后继指向null Node<E> newNode = new Node<>(pred, e, null); if (pred == null) // 若pred指向null,说明待插位置为首位节点,需更新first指向新节点 first = newNode; else // 若pred指向非null,则令pred的后继指向新节点 pred.next = newNode; // 移动pred指向新节点,使得下一个元素接着插入至当前新节点的后面 pred = newNode; } if (succ == null) { // 若是插入尾部,更新last指向pred last = pred; } else { // 若是插至中间,令参数集合c中最后一个元素生成的节点pred的后继指向succ,succ的前驱指向pred pred.next = succ; succ.prev = pred; } // 更新size加参数集合c的长度 size += numNew; // 更新modCount加1 modCount++; return true;} 整体流程:检查待插位置是否越界 –> 将参数集合c转为Object型数组a,并获取数组长度给numNew –> 若数组a长度为0,则返回false,否则继续执行插入操作 –> 判断index与当前链表长度size是否相等,来决定pred和succ的指向(pred指向待插节点位置的前一个节点,succ指向待插节点位置的后一个节点) –> 遍历待插所有元素,按次序分别生成新节点,并让新节点前驱指向前一个节点,前一个节点后继指向新节点 –> 若succ指向null,则更新last指向最后一个节点,否则,插入的最后节点的后继与succ的前驱相互指向 –> 更新size加上已插元素数以及modCount加1 –> 所有元素插入成功,返回true。具体执行流程示意图如下所示: set(int index, E element) public E set(int index, E element):将链表中索引位置index处元素替换为元素值E。123456789101112131415161718192021public E set(int index, E element) { // 越界检查 checkElementIndex(index); // 取出指定index处的节点赋给x(node(int index)方法上面已分析过) Node<E> x = node(index); // 取出指定index处的旧值赋给oldVal E oldVal = x.item; // 将参数中指定元素element赋给index处元素 x.item = element; // 返回旧值oldVal return oldVal;}// 检查索引index是否越界,若越界,则抛出IndexOutOfBoundsException异常private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}// 对索引index进行越界检查,是否属于[0,size)private boolean isElementIndex(int index) { return index >= 0 && index < size;} 整体流程:检查待替换位置是否越界 –> 从链表头/尾处循环遍历取出待替换位置的节点 –> 取出旧值并暂存 –> 替换新元素 –> 返回刚暂存的旧值。 element()、getFirst()和getLast() public E element():获取链表中的第一个节点的元素值。 public E getFirst():获取链表中的第一个节点的元素值。 public E getLast():获取链表中最后一个节点的元素值。123456789101112131415161718192021222324public E element() { // 通过调用getFirst()获取头节点的元素 return getFirst();}// 获取链表中第一个元素的值public E getFirst() { // 获取链表头节点,并赋给f final Node<E> f = first; // 若f为空,则抛出NoSuchElementException异常 if (f == null) throw new NoSuchElementException(); // 返回头节点f的元素值 return f.item;}// 获取链表中最后一个元素的值public E getLast() { // 获取链表尾节点,并赋给l final Node<E> l = last; // 若l为空,则抛出NoSuchElementException异常 if (l == null) throw new NoSuchElementException(); // 返回尾节点l的元素值 return l.item;} remove(int index) public E remove(int index):移除并返回指定索引index处的元素。1234567891011121314151617181920212223242526272829303132public E remove(int index) { // 对待删索引index进行越界检查 checkElementIndex(index); // 通过node(int index)获取指定索引index处的节点,然后通过unlink(Node<E> x)移除该节点 return unlink(node(index));}// 移除非空节点x,并返回旧值E unlink(Node<E> x) { // assert x != null; final E element = x.item; // 记录待删节点x的元素 final Node<E> next = x.next; // 记录待删节点x的后继 final Node<E> prev = x.prev; // 记录待删节点x的前驱 if (prev == null) { // 若待删节点的前驱为空,表明待删节点x为头节点,需重新调整头节点指向待删节点的后继 first = next; } else { // 若待删节点的前驱非空,即待删节点x为非头节点 prev.next = next; // 调整待删节点的前一个节点的后继指向其后一个节点 x.prev = null; // 置空待删节点的前驱指向,切断结点的前驱指针 } if (next == null) { // 若待删节点的后继为空,表明待删节点x为尾节点,需重新调整尾节点指向待删节点的前驱 last = prev; } else { // 若待删节点的后继非空,即待删节点x为非尾节点 next.prev = prev; // 调整待删节点的后一个节点的前驱指向其前一个节点 x.next = null; // 置空待删节点的后继指向,切断结点的后继指针 } // 至此,待删节点的前一个节点和后一个节点已建立了双向连接,且待删节点前后指向都已切断 x.item = null; // 待删节点元素值赋空 size--; // 链表大小减1 modCount++; // modCount加1 return element; // 返回待删节点的旧值} 整体流程:越界检查 –> 遍历获取待删索引处的节点 –> 调整待删节点的前驱指向(若待删节点为头节点,则调整头节点指向待删节点的后一个节点;否则,调整待删节点的前一个节点的后继指向待删节点的后一个节点,并置空待删节点的前驱指向) –> 调整待删节点的后继指向(若待删节点为尾节点,则调整尾节点指向待删节点的前一个节点;否则,调整待删节点的后一个节点的前驱指向待删节点的前一个节点,并置空待删节点的后继指向) –> 置空待删节点元素值,并使链表大小减1和modCount加1 –> 返回待删节点旧值。具体执行流程示意图如下所示: removeFirstOccurrence(Object o)、removeLastOccurrence(Object o)和remove(Object o) public boolean removeFirstOccurrence(Object o):移除链表中第一次出现的指定元素o(从头往后遍历),成功移除返回true,未找到则返回false。 public boolean removeLastOccurrence(Object o):移除链表中第一次出现的指定元素o(从尾往前遍历),成功移除返回true,未找到则返回false。 public boolean remove(Object o):移除链表中第一次出现的指定元素o(从头往后遍历),成功移除返回true,未找到则返回false。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// 移除第一次出现的元素(从前往后遍历),实际调用remove(Object o)方法实现public boolean removeFirstOccurrence(Object o) { return remove(o);}public boolean remove(Object o) { // 按指定待删元素o是否为空,分两种情况来操作 if (o == null) { // 若待删元素o为空 // 从链表的头节点开始往后遍历,一旦发现存元素为空的节点,就调用unlink()移除该节点,并返回true for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { // 若待删元素o非空 // 从链表的头节点开始往后遍历,一旦发现存在元素与待删元素o相等,就调用unlink()移除该节点,并返回true for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } // 若在链表中未找到待删元素o,则返回false return false;}// 移除第一次出现的元素(从后往前遍历)public boolean removeLastOccurrence(Object o) { if (o == null) { // 若待删元素o为空 // 从链表的尾节点开始往前遍历,一旦发现存元素为空的节点,就调用unlink()移除该节点,并返回true for (Node<E> x = last; x != null; x = x.prev) { if (x.item == null) { unlink(x); return true; } } } else { // 若待删元素o为非空 // 从链表的尾节点开始往前遍历,一旦发现存在元素与待删元素o相等,就调用unlink()移除该节点,并返回true for (Node<E> x = last; x != null; x = x.prev) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;} remove()、pop()、removeFirst()和removeLast() public E remove():移除并返回链表的头元素。 public E pop():移除并返回链表的头元素。 public E removeFirst():移除并返回链表的头元素。 public E removeLast():移除并返回链表的尾元素。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364// 实际就是调用removeFirst()方法来移除头元素public E remove() { return removeFirst();}// 实际就是调用removeFirst()方法来移除头元素public E pop() { return removeFirst();}// 移除头节点,并返回旧值public E removeFirst() { // 记录头节点给f final Node<E> f = first; // 若头节点为空,则抛出NoSuchElementException异常 if (f == null) throw new NoSuchElementException(); return unlinkFirst(f);}// 移除链表中的第一个节点,并返回旧值// 使用前提:参数节点f为头节点,且f非空private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; // 记录待删节点f的元素赋给element final Node<E> next = f.next; // 记录待删节点f的后继指向赋给next f.item = null; // 置空待删节点f的元素 f.next = null; // help GC // 置空待删节点f的后继指向 first = next; // 重新调整头节点first指向待删节点f的下一个节点 if (next == null) // 若待删节点f的后继指向为空,说明待删节点f为尾节点(实际上,当前待删节点f的前驱和后继都指向空) // 重新调整尾节点last指向空 last = null; else // 若待删节点f的后继指向为非空,待删节点f的后一个节点的前驱指向为空 next.prev = null; size--; // 链表size减1 modCount++; // modeCount加1 return element; // 返回待删节点f的旧值}// 移除尾节点,并返回旧值public E removeLast() { // 记录尾节点给l final Node<E> l = last; // 若尾节点为空,则抛出NoSuchElementException异常 if (l == null) throw new NoSuchElementException(); return unlinkLast(l);}// 移除链表中的最后一个节点,并返回旧值// 使用前提:参数节点l为尾节点,且l非空private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; // 记录待删节点l的元素赋给element final Node<E> prev = l.prev; // 记录待删节点l的前驱指向赋给prev l.item = null; // 置空待删节点l的元素 l.prev = null; // help GC // 置空待删节点l的前驱指向 last = prev; // 重新调整尾节点last指向待删节点l的前一个节点 if (prev == null) // 若待删节点l的前驱指向为空,说明待删节点l为头节点(实际上,当前待删节点l的前驱和后继都指向空) // 重新调整头节点first指向空 first = null; else // 若待删节点l的前驱指向为非空,待删节点l的前一个节点的后继指向为空 prev.next = null; size--; // 链表size减1 modCount++; // modCount加1 return element; // 返回旧值} 整体流程:unlinkFirst(移除链表中的第一个节点,并返回旧值,要求参数节点f为头节点且非空):置空待删节点f的元素和后继指向 –> 调整头节点first指向待删节点f的后一个节点 –> 若待删节点f的后继指向为空,则调整尾节点last指向为空;否则,待删节点f的后一个节点的前驱指向为空 –> 链表size减1,modCount加1 –> 返回待删节点f的旧值。unlinkLast(移除链表中的最后一个节点,并返回旧值,要求参数节点l为尾节点且非空):置空待删节点l的元素和后继指向 –> 调整尾节点last指向待删节点l的前一个节点 –> 若待删节点l的前驱指向为空,则调整头节点first指向为空;否则,待删节点l的前一个节点的后继指向为空 –> 链表size减1,modCount加1 –> 返回待删节点l的旧值。 clear() public void clear():清空链表中的所有元素,头节点和尾节点都置为空,链表大小size置为0。1234567891011121314151617public void clear() { // Clearing all of the links between nodes is "unnecessary", but: // - helps a generational GC if the discarded nodes inhabit // more than one generation // - is sure to free memory even if there is a reachable Iterator // 从头往后开始遍历,将所有节点的元素、后继和前驱都置空 for (Node<E> x = first; x != null; ) { Node<E> next = x.next; x.item = null; x.next = null; x.prev = null; x = next; } first = last = null; // 置空头节点和尾节点 size = 0; // 链表size值为0 modCount++; // modeCount加1} contains(Object o)、indexOf(Object o)和lastIndexOf(Object o) public boolean contains(Object o):判断链表中是否包含元素o,包含返回true,否则为false。 public int indexOf(Object o):返回指定元素o在链表中第一次出现的索引位置(从头往后遍历)。 public int lastIndexOf(Object o):返回指定元素o在链表中第一次出现的索引位置(从尾往前遍历)。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// 判断链表中是否存在指定元素opublic boolean contains(Object o) { // 通过调用indexOf()方法获取指定元素o的索引位置,若返回结果非-1,则说明链表中含有该元素 return indexOf(o) != -1;}// 返回指定元素o第一次出现的索引位置(从头往后遍历)public int indexOf(Object o) { int index = 0; // 初始化index为0 if (o == null) { // 若待查元素o为空 // 从头往后遍历,每次遍历index加1,直至找到为空的节点,并返回index for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { // 若待查元素o为非空 // 从头往后遍历,每次遍历index加1,直至找到与待查元素o相等的元素节点,并返回index for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } // 未找到待查元素o,则返回-1 return -1;}// 返回指定元素o第一次出现的索引位置(从尾往前遍历)public int lastIndexOf(Object o) { int index = size; // 初始化index为链表长度 if (o == null) { // 若待查元素o为空 // 从尾往前遍历,每次遍历index减1,直至找到为空的节点,并返回index for (Node<E> x = last; x != null; x = x.prev) { index--; if (x.item == null) return index; } } else { // 若待查元素o为非空 // 从尾往前遍历,每次遍历index减1,直至找到与待查元素o相等的元素节点,并返回index for (Node<E> x = last; x != null; x = x.prev) { index--; if (o.equals(x.item)) return index; } } // 未找到待查元素o,则返回-1 return -1;} offer(E e)、offerFirst(E e)、offerLast(E e)和 push(E e) public boolean offer(E e):在链表尾部增加一个新元素,成功返回true。 public boolean offerFirst(E e):在链表头部增加一个新元素,成功返回true。 public boolean offerLast(E e):在链表尾部增加一个新元素,成功返回true。 public void push(E e):在链表头部增加一个新元素,无返回值。1234567891011121314151617181920212223242526272829303132333435363738public boolean offer(E e) { // 调用add(E e)实现在链表尾部增加一个元素为e的新节点 return add(e);}public boolean offerFirst(E e) { // 调用addFirst(E e)实现在链表头部增加一个元素为e的新节点 addFirst(e); return true;}public boolean offerLast(E e) { // 调用addLast(E e)实现在链表尾部增加一个元素为e的新节点 addLast(e); return true;}public void push(E e) { // 调用addFirst(E e)实现在链表头部增加一个元素为e的新节点 addFirst(e);}// 在链表头部插入一个新元素epublic void addFirst(E e) { linkFirst(e);}// 添加一个新节点至链表头部,并更新first和或last指向private void linkFirst(E e) { // 记录原头节点位置给f,且f为final类型,不可更改 final Node<E> f = first; // 生成一个新节点:前驱指向null,值为e,后继指向当前链表的头节点 final Node<E> newNode = new Node<>(null, e, f); // 更新first指向新节点newNode first = newNode; if (f == null) // 若f为null,说明刚添加的newNode为最后一个节点,将last指向最后一个节点newNode last = newNode; else f.prev = newNode; // 若f非null,则将f的前驱指向新节点newNode size++; // 更新链表长度加1 modCount++; // 更新modCount加1} poll()、pollFirst()和pollLast() public E poll():移除并返回链表的头元素。 public E pollFirst(): 移除并返回链表的头元素。 public E pollLast(): 移除并返回链表的尾元素。1234567891011121314151617public E poll() { // 记录头节点 final Node<E> f = first; // 若头节点为空,则返回空;否则,调用unlinkFirst()移除头节点,并返回旧值 return (f == null) ? null : unlinkFirst(f);}// 与poll()功能一致public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f);}public E pollLast() { // 记录尾节点 final Node<E> l = last; // 若尾节点为空,则返回空;否则,调用unlinkLast()移除尾节点,并返回旧值 return (l == null) ? null : unlinkLast(l);} peek()、peekFirst()和peekLast() public E peek():返回头节点元素(不删除)。 public E peekFirst():返回头节点元素(不删除)。 public E peekLast(): 返回尾节点元素(不删除)。123456789101112131415public E peek() { final Node<E> f = first; // 若头节点为空,则返回空;否则,返回头节点的元素 return (f == null) ? null : f.item;}// 与peek()功能一致public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item; } public E peekLast() { final Node<E> l = last; // 若尾节点为空,则返回空;否则,返回尾节点的元素 return (l == null) ? null : l.item;} toArray()和toArray(T[] a) public Object[] toArray():将整个链表转为Object型数组。 public <T> T[] toArray(T[] a):将整个链表转为指定类型的数组。123456789101112131415161718192021222324252627282930// 链表转Object型数组public Object[] toArray() { // 创建一个Object型数组,大小为链表长度 Object[] result = new Object[size]; int i = 0; // 从头往后遍历,将链表中元素按顺寻加入数组result中 for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; // 返回转换后的数组 return result;}// 链表转T型数组(泛型方法)public <T> T[] toArray(T[] a) { // 若参数数组a的长度小于链表长度,则通过反射创建一个和链表长度一样的T型数组 if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance( a.getClass().getComponentType(), size); int i = 0; // 将参数数组a赋给Object型数组result Object[] result = a; // 从头往后遍历,将所有元素依次添加到数组result中 for (Node<E> x = first; x != null; x = x.next) result[i++] = x.item; // 若数组a的长度大于链表长度,则将a[size]设置为null // 在调用方在知道链表无非空元素时,有助于确定链表长度 if (a.length > size) a[size] = null; // 返回转换后的数组 return a;} clone() public Object clone():返回一个链表的克隆对象。需要注意的是,调用LinkedList会返回链表的一个Object型克隆对象,链表中的元素不会被克隆,而是直接引用之前的元素。123456789101112131415161718192021222324public Object clone() { // 调用超类clone()方法,返回一个LinkedList对象 LinkedList<E> clone = superClone(); // Put clone into "virgin" state // 将克隆后对象的状态置为初始状态 // 置头节点和尾节点为null、链表长度和modeCount为0 clone.first = clone.last = null; clone.size = 0; clone.modCount = 0; // Initialize clone with our elements // 从头往后遍历整个链表,将所有元素依次加入克隆对象中 for (Node<E> x = first; x != null; x = x.next) clone.add(x.item); // 返回克隆对象 return clone;}// 调用超类Object的clone()方法,并将得到的Object对象转为LinkedList类型private LinkedList<E> superClone() { try { return (LinkedList<E>) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e); }}]]></content>
<categories>
<category>java集合系列(java.util包)</category>
</categories>
<tags>
<tag>java集合</tag>
<tag>java.util.List</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java.util集合(collection)之ArrayList源码分析(jdk1.8)]]></title>
<url>%2F2019%2F04%2F02%2Fcollection-arraylist-sourcecode%2F</url>
<content type="text"><![CDATA[1. 简介ArrayList是基于数组实现,是一个可变大小的动态数组。ArrayList是线程不安全的,只可在单线程环境下使用,若需在多线程环境使用,主要方法有:① 通过Collections.synchronizedList(List list)方法返回一个线程安全的ArrayList类;② 使用java.util.concurrent.CopyOnWriteArrayList;③ 使用synchronized关键字。 从下图可以得知,ArrayList继承于AbstractList,实现了List、RandomAccess、Cloneable、java.io.Serializable这些接口。 继承AbstractList,实现List。定义了对数组的基本操作,如增加、删除、修改、遍历等。 实现RandomAccess,支持随机访问。RandmoAccess是List实现所使用的标记接口,使算法能够在随机和顺序访问List中性能更加高效。 实现Cloneable,支持可拷贝。Cloneable接口相当于标记接口,只有实现该接口的类,并在类中重写Object的clone方法,然后通过该类调用clone方法才能成功,若没有实现Cloneable接口,则会抛出CloneNotSupportedException异常。 实现java.io.Serializable,支序列化。Serializable接口为一个空接口,为实现该接口的对象提供标准的序列化与反序列化操作。 本文的涉及注解整理于此: 注[1]:ArrayList种的elementData属性为什么被transient修饰? 注[2]:ArrayList(Collection<? extends E> c)构造函数里有这样一句注释:c.toArray might (incorrectly) not return Object[] (see 6260652) 注[3]:ArrayList在add时为什么扩容1.5倍? 注[4]:ArrayList在add时存在线程安全性问题? 注[5]:ArrayList的扩容机制(jdk1.8)? 注[6]:Arrays.copyOf与System.arraycopy的区别? 注[7]:为什么说ArrayList查询快,增删慢? 注[8]:ArrayList中remove(Object o)方法可能无法删除对象问题? 注[9]:ArrayList中removeAll和retainAll的区别?以及removeAll的具体操作图解。 注[10]:ArrayList使用toArray()转数组抛异常问题?如何正确转换为所需类型数组? 注[11]:ArrayList中fail-fast机制?ArrayList使用iterator遍历时可能会抛出ConcurrentModificationException? 2. 属性DEFAULT_CAPACITY1private static final int DEFAULT_CAPACITY = 10; 默认容量为10。当ArrayList初始化时没有指定大小时,则使用该缺省容量值。 EMPTY_ELEMENTDATA1private static final Object[] EMPTY_ELEMENTDATA = {}; 共享常量空对象数组。当ArrayList构造方法显示指定初始容量为0时,会将EMPTY_ELEMENTDATA赋给elementData数组。 DEFAULTCAPACITY_EMPTY_ELEMENTDATA1private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 共享常量空对象数组。当ArrayList构造方法没有显示指定初始容量时,会将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给elementData数组。与EMPTY_ELEMENTDATA区别是:当第一个元素被加进来时,它知道如何扩容(用处在add(E e)中有体现)。 elementData1transient Object[] elementData; elementData为 “Object[]类型的数组”,是ArrayList中数据实际存储的地方。为什么说ArrayList底层是一个动态数组? 我们可以通过带指定初始容量的构造函数ArrayList(int initialCapacity)来初始化elementData数组大小,或者通过不带参数的构造函数ArrayList()来创建默认容量为10的ArrayList。elementData数组的容量会随元素数据的增加而动态增大,具体动态增大方式可参考ensureCapacityInternal(int minCapacity)方法,下面也会对此方法进行解析。注[1]:ArrayList种的elementData属性为什么被transient修饰?当一个对象被序列化时,不会序列化被transient关键字修饰的变量的值。然而ArrayList又是可被序列化的类,作为存储实际数据的elementData数组,若无法进行序列化,那么在反序列化时ArrayList难道会丢失原先的数据?实际上,ArrayList在序列化的时候会调用writeObject(java.io.ObjectOutputStream s),直接将size和element写入ObjectOutputStream;反序列化时调用readObject(java.io.ObjectInputStream s),从ObjectInputStream获取size和element,再恢复到elementData。(私有的writeObject和readObject是通过反射被调用的:ObjectInputStream.readObject() --> ObjectInputStream.readObject0() --> 经过switch选择到TC_OBJECT,ObjectInputStream.readOrdinaryObject() --> ObjectInputStream.readSerialData() --> ObjectStreamClass.invokeReadObject(): readObjectMethod.invoke(obj, new Object[]{ in })反射调用ArrayList中的readObject())之所以不直接对elementData序列化,而通过上述方式来实现序列化,是因为:elementData作为一个缓存数组,并不是所有地方都存储满了数据,而是预留一些容量,等需要时再扩容。由此可知数组中有些地方可能没有存储实际元素,通过上述方式实现序列化时,数组中只有实际存储的数据会被序列化,而不是整个数组中的数据,这样可以一定程度上降低空间和时间的消耗。 size1private int size; 动态数组的实际大小,ArrayList包含的元素数量。 3. 构造方法ArrayList提供了三种方式的构造函数,分别如下: ArrayList(int initialCapacity) public ArrayList(int initialCapacity):构造一个指定初始容量的空ArrayList。12345678910111213public ArrayList(int initialCapacity) { if (initialCapacity > 0) { // 初始容量大于0时,创建指定初始容量的Object数组赋给elementData this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 初始容量等于0时,将EMPTY_ELEMENTDATA空对象数组赋给elementData this.elementData = EMPTY_ELEMENTDATA; } else { // 初始容量小于0时,抛出IllegalArgumentException异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); }} ArrayList() public ArrayList():构造一个无参,使用默认容量为10的空ArrayList。1234public ArrayList() { // 没有显示指定初始容量时,将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给elementData this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;} ArrayList(Collection<? extends E> c) public ArrayList(Collection<? extends E> c):构造一个包含指定Collection型的集合参数。123456789101112131415public ArrayList(Collection<? extends E> c) { // 将Collection类型参数c转为数组赋给elementData elementData = c.toArray(); // 将转为数组的elementData的长度赋给size,并判断参数集合是否为空 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) // 若参数集合非空但是没有成功转为Object数组,则复制数组并转为Object型数组 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. // 若参数集合为空,则将EMPTY_ELEMENTDATA赋给elementData this.elementData = EMPTY_ELEMENTDATA; }} 注[2]:ArrayList(Collection<? extends E> c)构造函数里有这样一句注释:c.toArray might (incorrectly) not return Object[] (see 6260652),意思是c.toArray可能无法正确返回Object[]。可参见官方bug描述,bug编号为6260652。由上述源码可知,c.toArray()将参数集合转为数组并赋给elementData。若该数组非空,还需判断该数组类型是否为Object型数组,若不是,则需将该数组复制并转为Object型数组。由此,可说明c.toArray()可能返回非Object型数组,具体原因分析如下:123456789101112131415161718192021public class Test { public static void main(String[] args) { // test1: List<String> list1 = new ArrayList<>(); list1.add("aaa"); list1.add("bbb"); Object[] objArr1 = list1.toArray(); System.out.println("objArr1: " + objArr1.getClass()); // objArr1: class [Ljava.lang.Object; // test2: List<String> list2 = new TestList<>(); Object[] objArr2 = list2.toArray(); System.out.println("objArr2: " + objArr2.getClass()); // objArr2: class [Ljava.lang.String; objArr2[0] = new Object(); // java.lang.ArrayStoreException: java.lang.Object }}class TestList<E> extends ArrayList<E> { @Override public Object[] toArray() { return new String[]{"aaa", "bbb"}; }} 测试1中,list1.toArray()会调用ArrayList自身的toArray()方法返回Object型数组。测试2中,list2.toArray()会调用TestList自身实现的toArray()方法返回String型数组,objArr2实际上是String[]类型,因为抽象类或接口的具体类型取决于实例化时所使用的子类类型。在执行objArr2[0] = new Object()时则会抛出异常,因为这种向下转型会存在安全性问题。总之,可能造成c.toArray返回非Object[]类型的原因是:toArray()方法可能会被覆盖重新实现,返回非Object[]类型。 4. 常用方法add(E e) public boolean add(E e):将指定元素添加到列表尾部。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public boolean add(E e) { // size + 1:存多少个元素,就分配多少空间资源,保证不浪费空间资源 ensureCapacityInternal(size + 1); // Increments modCount!! // 添加新元素e到elementData的末尾,然后size自增1 elementData[size++] = e; return true;}// 确认ArrayList的容量大小private void ensureCapacityInternal(int minCapacity) { // 当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则minCapacity取默认容量10和minCapacity的最大值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}// 判断是否需要扩容private void ensureExplicitCapacity(int minCapacity) { // 操作数+1,该变量主要是用来实现fail-fast机制 modCount++; // overflow-conscious code // 若所需容量最小值>实际数组的长度,则扩容数组 if (minCapacity - elementData.length > 0) grow(minCapacity);}// 扩容操作private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 新容量newCapacity为旧容量oldCapacity(数组长度)的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // >>位运算右移1位相当于除以2 // 若扩充容量仍小于所需容量最小值(newCapacity<minCapacity),则让新容量newCapacity等于所需容量最小值minCapacity if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 若新容量newCapacity大于数组最大容量MAX_ARRAY_SIZE,则进行大容量重新分配 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: // 将原来的数据复制到新的数组,且新数组的容量为新容量newCapacity elementData = Arrays.copyOf(elementData, newCapacity);//Arrays.copyOf:返回一个新的长度的数组对象,拷贝了原数组中的元素}// 根据所需容量最小值,重新计算容量值private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); // 若所需容量最小值minCapacity大于数组最大容量MAX_ARRAY_SIZE,则返回Integer最大值,否则返回数组最大容量 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;} 整体流程:确认ArrayList的容量(检查是否需要扩容,扩容方法为Arrays.copyOf())(①若elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,则取默认容量10;②正常扩充为原来1.5倍)–> 将元素插入elementData数组指定位置–> 将集合中实际容量加1,即size+1。 注[3]:ArrayList在add时为什么扩容1.5倍?ArrayList在扩容的时候既要考虑时间开销,还要考虑空间开销。其中,时间开销来自于申请新的内存和拷贝数组;空间开销来自于申请的空间多于需要的空间。从时间效率上来看,应尽量减少扩容的次数,一次扩充的越多越好;而从空间效率上来看,应尽量按需扩充,即需要多少元素就扩充多少容量。由此可知,这两个效率因素是互相矛盾的,需要平衡考虑。在实际中,时间效率的优先级别往往高于空间效率,故而,不可能一次扩容一个,而一次扩容50%,应该是兼顾各个因素的一个结果。 注[4]:ArrayList在add时存在线程安全性问题?ArrayList在执行add操作时,数据的更新主要分为两步来执行:①将元素插入elementData数组指定位置(elementData[size] = e);②将集合中实际容量加1(size+1)。该过程不能保证在多线程环境下具有原子性,存在线程安全性问题。举例说明:假设在多线程环境下有两个线程A和B,线程A将元素放入elementData数组中索引为0的位置,此时线程A暂停。线程B开始运行,也向elementData数组中添加数据,而此时size为0,故而也会将数据插入索引为0的位置。接着,线程A和B都开始运行,都使size+1。现在来看,ArrayList中的实际元素只有1个,而size等于2,这就存在线程不安全了。 注[5]:ArrayList的扩容机制(jdk1.8)?ArrayList的扩容发生在add操作时,由上述源码分析可知,具体有以下几个阶段:①在add()方法中调用ensureCapacityInternal(size + 1)来确定扩容所需的最小容量值minCapacity。参数size+1是为了保证空间尽量不被浪费,是元素添加后的实际容量。若elementData为默认的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则取默认容量和size+1后容量的最大值作为minCapacity。②调用ensureExplicitCapacity(minCapacity)来确定是否需要扩容。首先将操作数modCount+1,然后判断minCapacity是否大于当前elementData数组的长度,若是,则说明需要进行扩容。③扩容方法调用grow(minCapacity)来实现。首先将原elementData数组长度增加1.5倍,若增加后的容量小于参数minCapacity,则将minCapacity赋给新容量newCapacity,否则新容量newCapacity为增加1.5倍后容量。然后判断新容量newCapacity若大于数组最大容量MAX_ARRAY_SIZE,则调用hugeCapacity(minCapacity)来重新分配。最后得到确定的新容量newCapacity,调用Arrays.copyOf(elementData, newCapacity)进行数据复制和扩容。 实际运行过程中,第一次扩充为默认容量10,当实际存储到第11个元素时,会扩充其1.5倍,为15。 add(int index, E element) public void add(int index, E element):在指定位置index,插入一个新元素element。123456789101112131415161718192021public void add(int index, E element) { // 检查待插位置index是否越界 rangeCheckForAdd(index); // 空间检查,按需扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 数组复制,复制过程相当于将数组中从index开始往后的所有数据都向后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;}// 检查索引index是否越界,若大于size或小于0,则抛出IndexOutOfBoundsException异常private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}// java.lang.System#arraycopy:native方法// 复制指定源数组src到目标数组dest,复制从src的srcPos索引开始,复制的个数是length,复制到dest的索引从destPos开始。public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 整体流程:检查待插index是否越界 –> 空间检查,按需扩容(等同于add(E e)方法中)–> 扩容完,调用System.arraycopy()方法:从index起始到数组末尾,将所有元素往后移1位 –> 将元素插入elementData数组指定位置 –> 将集合中实际容量加1,即size+1。 注[6]:Arrays.copyOf与System.arraycopy的区别?Arrays.copyOf不只是复制数组元素,还创建了一个新的数组对象。 System.arrayCopy只复制已有的数组元素。Arrays.copyOf的内部实现是用的System.arraycopy:12345678910111213141516public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass());}/** * 在其内部创建了一个新的数组,然后调用System.arrayCopy()向其复制内容,返回出去。 */public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { // 创建一个newLength大小新的数组 T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 将original内容复制到copy中去,并且长度为newLength System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;} 由上可知,System.arraycopy方法会因为新数组长度比旧数组长度小而报IndexOutOfBoundsException;而Arrays.copyOf则不会因此报错,因为Arrays.copyOf 的返回值是在内部new好的copy数组,而该copy数组new的大小就等于newLength。由此对System.arraycopy方法的描述,可知,ArrayList在随机位置进行插入(即调用add(int index, E element)方法)时,每次都会移动数组中的元素,随着数据量增大,花费的时间也必然会增加。这也就是常说的,ArrayList在插入时的效率比较差,不及LinkedList。 addAll(Collection<? extends E> c) public boolean addAll(Collection<? extends E> c):将指定的集合c添加到列表尾部。12345678910111213public boolean addAll(Collection<? extends E> c) { // 将参数集合c转为Object型数组 Object[] a = c.toArray(); int numNew = a.length; // 空间检查,按需扩容 // size+numNew:numNew为待添加集合长度,与当前数组size之和为所需最小容量值 ensureCapacityInternal(size + numNew); // Increments modCount // 将a数组的所有元素都复制到elementData中的尾部 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; // a数组长度非0(非空)时返回true,否则返回false return numNew != 0;} 整体流程:将待添加集合c转为数组a –> 空间检查,按需扩容 –> 将a数组中所有元素复制到elementData数组尾部 –> 将集合中实际容量增加a数组长度大小,即size+a.length。 addAll(int index, Collection<? extends E> c) public boolean addAll(int index, Collection<? extends E> c):在指定位置index,插入集合c。1234567891011121314151617181920212223public boolean addAll(int index, Collection<? extends E> c) { // 待插index越界检查 rangeCheckForAdd(index); // 将参数集合c转为Object型数组 Object[] a = c.toArray(); // 待插数组c的长度,即需要后移的位数 int numNew = a.length; // 空间检查,按需扩容 ensureCapacityInternal(size + numNew); // Increments modCount // 计算需移动的元素数量 int numMoved = size - index; if (numMoved > 0) // 将当前数组elementData中从index开始往后的所有元素都向后移动numNew位 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 将待插数组a的所有元素复制到elementData数组中(从index位置开始) System.arraycopy(a, 0, elementData, index, numNew); size += numNew; // a数组长度非0(非空)时返回true,否则返回false return numNew != 0;} 整体流程:检查待插index是否越界 –> 将待添加集合c转为数组a –> 空间检查,按需扩容 –> 将当前数组elementData从index开始往后的所有元素向后移动待查数组a的长度 –> 将待插数组a的所有元素复制到从index开始的elementData数组中 –> 将集合中实际容量增加a数组长度大小,即size+a.length。 get(int index) public E get(int index):获取index位置的元素。123456public E get(int index) { // 检查参数index是否越界 rangeCheck(index); // 返回当前数组elementData中index位置的元素 return elementData(index);} 整体流程:检查是否越界 –> 获取当前数组elementData中index处的元素。 注[7]:为什么说ArrayList查询快,增删慢?查询快:由get(int index)方法源码可知,ArrayList底层为Object数组,在内存中是一片连续的空间,查询时可直接根据数组的首地址+偏移量访问到第index个元素在内存中的位置。增删慢:增删操作时,需要移动数组中的元素,随着数据量增大,花费的时间也必然会增加。其中增加操作尤指随机添加元素add(int index, E element)方法。 set(int index, E element) public E set(int index, E element):替换指定位置index处的元素值为element。12345678910public E set(int index, E element) { // 越界检查 rangeCheck(index); // 取出index处旧值赋给oldValue E oldValue = elementData(index); // 将elementData中index处元素值改为element elementData[index] = element; // 返回旧值 return oldValue;} 整体流程:检查是否越界 –> 记录index处旧值 –> 替换index处为新值 –> 返回旧值。 remove(int index) public E remove(int index):移除指定位置index处的元素。123456789101112131415161718public E remove(int index) { // 越界检查 rangeCheck(index); // 操作数+1 modCount++; // 取出index处旧值赋给oldValue E oldValue = elementData(index); // 需移动的元素个数 int numMoved = size - index - 1; if (numMoved > 0) // 将elementData数组中index后的所有元素都向前移动一位 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 消除过期对象的引用 elementData[--size] = null; // clear to let GC do its work // 返回旧值 return oldValue;} 整体流程:检查是否越界 –> 操作数+1,记录index处旧值 –> 将elementData数组中index后的所有元素都向前移动一位 –> 将集合中实际容量size减1 –> 将数组最后1位设置为null,让GC回收。 remove(Object o) public boolean remove(Object o):移除集合中第一次出现的对象o(若存在)。12345678910111213141516171819202122232425262728293031public boolean remove(Object o) { // 移除操作根据参数对象o是否为空,分两种情况处理 if (o == null) { // 若参数对象o为null for (int index = 0; index < size; index++) // 遍历所有元素,判断是否为null,若是则移除 if (elementData[index] == null) { fastRemove(index); return true; // 移除一次就返回 } } else { // 若参数对象o为非null for (int index = 0; index < size; index++) // 遍历所有元素,通过equals判断是否与参数对象o相等,若是则移除 if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false;}// 移除指定位置index的元素// 与remove(int index)基本一致,区别在于没有越界检查和返回旧值private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work} 整体流程:判断参数对象o是否为null,若为null –> 遍历集合中所有元素,判断是否为null –> 若找到为null的元素,则执行快速移除并返回true;若不为null –> 遍历集合中所有元素,判断是否与参数对象o相等 –> 若找到与o相等的元素,则执行快速移除并返回true。 注[8]:ArrayList中remove(Object o)方法可能无法删除对象问题?先看如下示例,list增加了3个TestObject对象后,再调用remove(Object o )方法进行删除,试想下打印结果是什么?123456789101112131415161718192021222324252627public class Test { public static void main(String[] args) { List<TestObject> list = new ArrayList<>(); list.add(new TestObject(1, "111")); list.add(new TestObject(2, "222")); list.add(new TestObject(3, "333")); list.remove(new TestObject(2, "222")); list.forEach(System.out::println); }}class TestObject { private int index; private String value; TestObject(int index, String value) { this.index = index; this.value = value; } @Override public String toString() { return "TestObject{" + "index=" + index + ", value='" + value + '\'' + '}'; }} 输出为:123TestObject{index=1, value='111'}TestObject{index=2, value='222'}TestObject{index=3, value='333'} 这一项“{index=2, value='222'}”并没有被删除,为什么呢?由remove(Object o )方法源码可知,ArrayList在删除对象时,需要先判断待删对象是否存在于集合元素中,这种判断时通过equals实现。而上述TestObject类使用的是默认Object类的equals方法,该默认方法是通过this == obj来判断两个对象是否为同一个对象,虽然上述示例中比较的两个对象的内的值都一样,但由于都是重新new创建的对象,所以这里equals比较时返回false。所以,当我们需要使用remove(Object o)方法时,需要对操作的类重写equals和hashCode方法。之所以还需要对hashCode方法进行重写,是因为若要对该操作类使用到HashMap或HashSet这类散列数据结构时,只重写equals没有重写hashCode的话会出错。也有相关规范提及:若两个对象经过equals比较后相同,那么它们的hashCode也一定相同,故而一般hashCode和euqals需要同时重写。重写euqals和hashCode后的TestObject类如下:1234567891011121314151617181920212223242526272829class TestObject { private int index; private String value; TestObject(int index, String value) { this.index = index; this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestObject that = (TestObject) o; if (index != that.index) return false; return Objects.equals(value, that.value); } @Override public int hashCode() { int result = index; result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @Override public String toString() { return "TestObject{" + "index=" + index + ", value='" + value + '\'' + '}'; }} 重写后,再执行上面程序,此时可以移除“{index=2, value='222'}”,输出如下:12TestObject{index=1, value='111'}TestObject{index=3, value='333'} removeAll(Collection<?> c)和retainAll(Collection<?> c) public boolean removeAll(Collection<?> c):移除当前集合中存在于指定参数集合c中的所有元素。 public boolean retainAll(Collection<?> c):只保留当前集合中存在于指定参数集合c中的所有元素,换言之,移除当前集合中未存在于参数集合c中的所有元素。removeAll是用来去除在指定集合中的元素,可用于排除值;retainAll是用来去除不在指定集合中的元素,可用于取交集。之所以将这两个方法放一起说,是因为它门都调用了相同的方法batchRemove,这点体现出了代码复用。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869public boolean removeAll(Collection<?> c) { // 判空,为空抛出空指针异常 Objects.requireNonNull(c); return batchRemove(c, false);}public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true);}// 检查参数对象obj是否为空,为空则抛出NullPointerException异常,否则返回该对象public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj;}private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; // r指读取计数,w指写入计数 int r = 0, w = 0; boolean modified = false; try { // 遍历当前集合,①若参数集合c不包含elementData中某元素(即c.contains(elementData[r])为false,removeAll),则elementData保留该元素,否则,不保留该元素,即删除c中存在的元素; // ②若参数集合c包含elementData中某元素(即c.contains(elementData[r])为true,retainAll),则elementData保留该元素,否则,不保留该元素,即删除c中不存在的元素; for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. // 为了保持和AbstractCollection的兼容性 // 若try中遍历未执行完而抛出异常,则r必然不等于size,此时为了最大程度保证数据一致性,会将后面还未比对的数据都保留下来 if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } // [0,w)之间记录了需要保留的数据,从w开始往后的所有数据都要清空 if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; // 操作数 + (size - w) modCount += size - w; // 集合实际容量为w size = w; modified = true; } } return modified;}// 判断当前集合是否包含参数对象opublic boolean contains(Object o) { return indexOf(o) >= 0;}// 获取参数对象o在当前集合中首次出现的索引位置public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } // 返回-1,代表指定参数对象o不存在于当前集合中 return -1;} 整体流程:removeAll:判空 –> 遍历当前集合,若参数集合c不包含elementData中某元素,则保留该元素在elementData中 –> 若r不等于size,则保留后面未比对的数据 –> 清空从w开始往后的所有数据 –> 操作数更新(modCount += size - w) –> 集合容量更新(size=w)。retainAll:判空 –> 遍历当前集合,若参数集合c包含elementData中某元素,则保留该元素在elementData中 –> 若r不等于size,则保留后面未比对的数据 –> 清空从w开始往后的所有数据 –> 操作数更新(modCount += size - w) –> 集合容量更新(size=w)。 注[9]:ArrayList中removeAll和retainAll的区别?以及removeAll的具体操作图解。具体区别及源码分析可见上述描述。removeAll方法的操作图解如下: clear() public void clear():清除集合中的所有元素。12345678910public void clear() { // 操作数+1 modCount++; // clear to let GC do its work // 遍历集合所有元素,然后将其设为null for (int i = 0; i < size; i++) elementData[i] = null; // 将集合长度设为0 size = 0;} 整体流程:操作数+1 –> 遍历所有元素,并置为null –> 设置集合大小为0。 trimToSize() public void trimToSize():修剪掉预留元素。1234567891011public void trimToSize() { // 操作数+1 modCount++; // 当集合实际长度size小于数组elementData长度时,才需要修剪下 if (size < elementData.length) { // 当集合实际长度size为0时,给elementData赋空数组,否则将elementData复制为一个新的长度为size的数组,修剪掉多余的容量 elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); }} 整体流程:操作数+1 –> 当集合实际长度size小于数组长度时,开始修剪 –> 修剪:若集合长度size为0,则置为空数组,否则将数组elementData复制为一个长度为size的新数组。 toArray()和toArray(T[] a) public Object[] toArray():将集合转为Object型数组。 public <T> T[] toArray(T[] a):将集合转为所需要类型的数组。1234567891011121314151617public Object[] toArray() { // 返回一个新的Object型数组,它的内容和长度与当前集合的内容一致 return Arrays.copyOf(elementData, size);}public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: // 当参数数组长度小于当前数组实际长度,则直接返回一个内容和长度与elementData一致的新数组 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 当参数数组长度大于等于当前数组实际长度,则将当前数组elementData复制到新数组a上 System.arraycopy(elementData, 0, a, 0, size); // 若新数组长度大于当前数组实际长度,则将a[size]置为null if (a.length > size) a[size] = null; // 返回新数组a return a;} 注[10]:ArrayList使用toArray()转数组抛异常问题?如何正确转换为所需类型数组?当我们想要将集合List转为数组时,若使用如下的写法,则会报java.lang.ClassCastException异常。因为toArray()方法返回的Object类型数组,不能将Object[]转为String[]。123456List<String> list = new ArrayList<>();list.add("a");list.add("b");list.add("c");// java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;String[] array = (String[]) list.toArray(); 要想解决这种异常问题,可以通过遍历Object数组的所有元素,分别对这些元素进行强转,如下代码所示:12345Object[] arrayObj = list.toArray();for (int i = 0; i < arrayObj.length; i++) { String str = (String) arrayObj[i]; System.out.println(str);} 当然更好的方法是采用toArray(T[] a),代码示例如下:12345// 方法1:String[] arr = new String[list.size()];String[] arr1 = list.toArray(arr);// 方法2:String[] arr2 = list.toArray(new String[0]); 5. 遍历方式ArrayList支持3种遍历方式。123456789101112131415// 1.for循环遍历for (int i = 0; i < list.size(); i++) { String value = list.get(i); System.out.println(value);}// 2.foreach遍历for (String s : list) { System.out.println(s);}// 3.iterator遍历Iterator<String> it = list.iterator();while (it.hasNext()) { String value = it.next(); System.out.println(value);} 注[11]:ArrayList中fail-fast机制?ArrayList使用iterator遍历时可能会抛出ConcurrentModificationException?fail-fast称为“快速失败”,它是Java集合中的一种错误机制。当使用iterator遍历集合过程中,倘若该集合的结构被修改,如ArrayList的add和remove方法,则有可能会抛出ConcurrentModificationException异常,从而产生fail-fast。具体fail-fast示例代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748List<Integer> list = new ArrayList<>();// 初始化集合:先向集合中添加0,1,2,...,9共10个数字for (int i = 0; i < 10; i++) { list.add(i);}/** * 单线程环境 */Iterator<Integer> it = list.iterator();int i = 0;Integer v;while (it.hasNext()) { if (3 == i) { list.remove(i); } v = it.next(); System.out.println(v); i++;}/** * 多线程环境 */// 一个线程通过iterator遍历集合new Thread(() -> { Iterator<Integer> it = list.iterator(); Integer v; while (it.hasNext()) { v = it.next(); System.out.println(v + ","); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }}).start();// 一个线程在i为3时,调用remove修改集合new Thread(() -> { int i = 0; while (10 > i) { if (3 == i) { list.remove(i); } i++; }}).start(); 以上分为单线程和多线程两种环境情况,分别执行都会抛出java.util.ConcurrentModificationException异常。产生这种异常,主要是因为在操作Iterator,下面分析下ArrayList中Iterator源码。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... // 通过调用iterator()方法,new了一个ArrayList子类Itr(迭代器实现类) public Iterator<E> iterator() { return new Itr(); } // Itr是Iterator的实现类 private class Itr implements Iterator<E> { // 下一个待返回的元素索引,默认为0 int cursor; // index of next element to return // 上一个已返回的元素索引,默认为-1 int lastRet = -1; // index of last element returned; -1 if no such // 对集合修改次数的期望值,初始值为modCount int expectedModCount = modCount; // 判断是否还存在下一个元素,若cursor等于集合大小size,则说明已到末尾 public boolean hasNext() { return cursor != size; } // 获取下一个元素值 @SuppressWarnings("unchecked") public E next() { // 判断expectedModCount是否等于modCount,若不等于则说明集合结构已被修改,抛出ConcurrentModificationException checkForComodification(); int i = cursor; // 判断当前待返回元素下标是否大于等于集合大小size,若是,则说明待返回元素不存在,抛出NoSuchElementException if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; // 判断当前待返回元素下标是否大于等于集合长度,若是,则抛出ConcurrentModificationException if (i >= elementData.length) throw new ConcurrentModificationException(); // cursor自增1 cursor = i + 1; // 返回元素,并将lastRet重新赋值为刚返回元素的下标 return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } ... } 有上述源码分析可知,当调用Itr的next()和remove()时,都会执行checkForComodification()方法,若modCount不等于expectedModCount,则会抛出ConcurrentModificationException异常,这里才是产生fail-fast的根源。何时会导致modCount不等于expectedModCount? 当ArrayList实例调用iterator()时,会返回一个新创建的iterator迭代器,其中expectedModCount初始化为当前集合中的modCount。当调用next()方法时会通过checkForComodification()方法去检查,若在调用next()前调用了集合中可能导致结构变化的方法(如add()和remove())或是另一个线程修改了集合结构,导致集合中的modCount变化,从而致使expectedModCount不等于modCount,抛出ConcurrentModificationException异常。一言以蔽之,当调用iterator遍历集合过程中,若集合中modCount变化(调用集合的add()、remove()和clear()等方法),则会抛出ConcurrentModificationException异常,引发fail-fast。fail-fast是一种错误检测机制,但并不能保证该机制一定会被触发。若需使用可能引发fail-fast的集合,建议使用CopyOnWriteArrayList。 参考资料[1] Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例. http://www.cnblogs.com/skywang12345/p/3308556.html#a1.[2] ArrayList中elementData为什么被transient修饰? https://blog.csdn.net/zero__007/article/details/52166306.[3] Java笔记—c.toArray might (incorrectly) not return Object[] (see 6260652)官方Bug. https://blog.csdn.net/gulu_gulu_jp/article/details/51457492.[4] jdk1.8ArrayList主要方法和扩容机制(源码解析). https://blog.csdn.net/u010890358/article/details/80515284.[5] Java 集合系列04之 fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法). https://www.cnblogs.com/skywang12345/p/3308762.html.]]></content>
<categories>
<category>java集合系列(java.util包)</category>
</categories>
<tags>
<tag>java集合</tag>
<tag>java.util.List</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java集合框架总述(java.util包)]]></title>
<url>%2F2019%2F03%2F28%2Fcollection-frame-diagram%2F</url>
<content type="text"><![CDATA[1. java.util中collection集合java.util下的集合框架主要基于Collection接口,它是最基本的集合接口,定义了集合的基本操作。Collection接口继承了Iterable接口,说明Collection的子类都可以实现遍历操作,通过Iterable接口中的 Iterator<T> iterator() 方法可返回一个Iterator(迭代器)进行遍历,通常用法如下:1234Iterator it = collection.iterator(); // 获取集合的迭代器while(it.hasNext()) { // 集合中是否还有元素 Object obj = it.next(); // 返回下一个元素} 此外,java 8新增了forEach方法,也可实现遍历操作。 Collection接口下的子接口主要有:List(序列)、Set(集)和Queue(队列)。List的特点是元素有序、可重复的,其实现的常用集合类有ArrayList、LinkedList、Vector和Stack。 ArrayList:基于数组实现,线程不安全。查找快,增删慢。 LinkedList:基于链表实现,线程不安全。增删快,查找慢。 Vector:基于数组实现,线程安全。底层实现类似于ArrayList,区别是内部很多方法使用synchronized关键字来实现线程安全,效率低于ArrayList。不建议使用。 Stack:继承于Vector,实现了“栈”结构(LIFO,后进先出)。基于数组实现,线程安全。亦可通过ArrayDeque或LinkedList来实现“栈”结构。不建议使用。 Set的特点是元素无序、不可重复的,其实现的常用集合类有HashSet、LinkedHashSet和TreeSet。 HashSet:底层其实是固定value的HashMap。元素无序、线程不安全、不可重复、值可允许为Null。 LinkedHashSet:继承于HashSet,基于LinkedHashMap实现,使用链表来维护元素顺序。元素有序、线程不安全、不可重复、值可允许为Null。因为通过链表维护元素顺序,故而LinkedHashSet相较于HashSet在插入时性能稍逊而迭代访问时性能较好。 TreeSet:是SortedSet接口的实现类,基于TreeMap实现,可以确保集合中元素处于排序状态,它支持自然排序(默认)和定制排序(Comparator)。 Queue的特点是元素有序、可重复的,其实现的常用集合类有PriorityQueue、ArrayDeque和LinkedList。 PriorityQueue:一种基于优先级堆的极大优先级队列(基于数组的完全二叉树),是一个比较标准的队列实现类(FIFO,先进先出),区别于标准队列的地方是PriorityQueue是按照队列元素大小重新排序,而非入队顺序。它能保证每次取出的都是队列中权值最小的元素,其中默认排序为自然顺序排序(即升序排序),亦可通过Comparator来自定义排序方式。它不允许Null、可重复、线程不安全。 ArrayDeque:是Deque(双端队列,支持同时从两端添加或移除元素,亦可作LIFO队列(栈))接口的实现类 + 基于数组实现 = 基于循环数组实现的双端队列。查找快,增删慢,线程不安全。ArrayDeque可作栈来使用,效率高于Stack;亦可作队列使用,相对LinkedList更高效。 LinkedList:既实现了List接口,也实现了Deque接口,具备List、队列和栈的特性。 2. java.util中map集合 java.util下的Map主要特点是键值对的形式,一个key对应一个value,且key不可重复。其常用实现类有HashMap、LinkedHashMap、TreeMap、HashTable和weakHashMap等。 HashMap:jdk1.6/1.7采用位桶+链表实现,jdk1.8采用位桶+链表+红黑树实现。它允许key和value为Null,key不可重复,线程不安全,不保证key有序。 LinkedHashMap:是HashMap的子类,内部维护了一个双向链表,保存了记录的插入顺序。LinkedHashMap的遍历速度和实际数据有关,而HashMap是和容量有关。 TreeMap:是SortedMap接口的实现类,能够按key进行排序,它支持自然排序(默认)和定制排序(Comparator)。 HashTable:继承了Dictionary抽象类,与HashMap类似。不允许key或value为Null,线程安全(各个方法上增加synchronize关键字)。因为专门适用多线程场景的ConcurrentHashMap,故而不建议使用HashTable。 weakHashMap:和HashMap类似。它的键是“弱键”,key和value都可以是Null。它的特点是:当除了自身有对key引用外,此key没有其他引用情况,会在下次进行增删改查操作时丢弃该键值对,使该键被回收,节约内存,适用于需要缓存的场景。 EnumMap:专门为枚举类型定做的Map实现,它只能接收同一枚举类型的实例作为键值。它使用数组来存放与枚举类型对应的值,这种实现非常高效。不允许key为Null,但允许value为Null。 IdentityHashMap:利用Hash表来实现Map接口。它允许key重复,但是key必须是两个不同的对象。与HashMap不同的是,它重写了hashCode方法不是使用Object.hashCode(),而是System.identityHashCode()方法(根据对象在内存中的地址计算出来的一个数值)。 3. java.util.concurrent中集合To be continued… 参考资料[1] Java:集合,Collection接口框架图. https://www.cnblogs.com/nayitian/p/3266090.html.[2] 浅谈WeakHashMap. http://www.importnew.com/23182.html.[3] Java枚举(enum)详解:Java声明枚举类型、枚举(enum)类、EnumMap 与 EnumSet. http://c.biancheng.net/view/1100.html]]></content>
<categories>
<category>java集合系列(java.util包)</category>
</categories>
<tags>
<tag>java集合</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一、Zookeeper简介、三种搭建模式和配置文件详解]]></title>
<url>%2F2019%2F03%2F25%2Fone-zookeeper-deploy%2F</url>
<content type="text"><![CDATA[table th:first-of-type { width: 100px; } 1. 简介Zookeeper缘起于非开源的Google的Chubby,雅虎模仿Chubby开发了ZooKeeper,实现了类似的分布式锁管理,并捐给了Apache,作为是Hadoop和Hbase的重要组件。ZooKeeper是一种用于分布式应用程序的分布式开源协调服务,它主要是用来解决分布式应用中经常遇到的一些数据一致性问题。它的一致性、可靠性和容错性保证了其能够在大型分布式系统中稳定的表现,并不会因为某一个节点服务宕机而导致整个集群崩溃。它可提供的功能包括:配置维护、域名服务、分布式同步、组服务等。Zookeeper集群中的角色主要有: Leader:为zk集群的核心,负责集群内部的调度、投票的发起和决策和系统状态更新登。 Follower:接收Client请求、转发请求给Leader和参与投票等。 Observer:充当观察者角色,功能与Follower基本一致,不同点在于它不参与任何形式的投票,它只提供非事务请求服务。 Zookeeper维护一个具有层次关系的数据结构,类似于文件系统,名称是由斜杠(/)分隔的路径元素序列,ZooKeeper名称空间中的每个节点都由路径标识。 2. 安装配置zookeeper的相关资源如下: 官网:http://zookeeper.apache.org/ 下载:https://archive.apache.org/dist/zookeeper/ zookeeper的安装之前,需确保java环境运行正常。zookeeper有三种搭建方式:单机模式、伪集群模式和集群模式。 2.1. 单机模式解压:将下载好的zookeeper-*.tar.gz解压到指定安装目录下:12tar -zxvf zookeeper-3.4.10.tar.gz #解压zookeeper压缩包cd zookeeper-3.4.10 #进入zookeeper根目录 主要目录结构: bin:一些执行脚本命令,其中,.sh为linux环境下脚本,.cmd为windows下脚本。 conf:存放配置文件和日志配置文件。 contrib:一些附加功能,用于操作zk的工具包。 dist-maven:mvn编译后目录。 docs:相关操作文档。 lib:zk依赖的jar包。 recipes:一些代码示例demo。 src:源文件。 配置文件:将conf目录下zoo_sample.cfg复制一份并重命名为zoo.cfg:1cp conf/zoo_sample.cfg conf/zoo.cfg 修改配置文件zoo.cfg:123456tickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/zookeeper-3.4.10/dataDir #zk数据保存目录dataLogDir=/usr/local/zookeeper/zookeeper-3.4.10/dataLogDir #zk日志保存目录,当不配置时与dataDir一致clientPort=2181 #客户端访问zk的端口 配置文件中参数详解见“3.配置文件详解”。 配置环境变量:为方便操作,可对zk配置环境变量,linux环境下在/etc/profile文件最后追加:123ZOOKEEPER_HOME=/usr/local/zookeeper/zookeeper-3.4.10PATH=$PATH:$ZOOKEEPER_HOME/binexport ZOOKEEPER_HOME PATH 为立即生效配置,通过执行如下命令:1source /etc/profile 启动zk服务:1zkServer.sh start 启动信息:123ZooKeeper JMX enabled by defaultUsing config: /usr/local/zookeeper/zookeeper-3.4.10/bin/../conf/zoo.cfgStarting zookeeper ... STARTED 查看zk状态:1zkServer.sh status 输出状态信息:123ZooKeeper JMX enabled by defaultUsing config: /usr/local/zookeeper/zookeeper-3.4.10/bin/../conf/zoo.cfgMode: standalone #说明当前为单机模式 关闭zk服务:1zkServer.sh stop 关闭信息:123ZooKeeper JMX enabled by defaultUsing config: /usr/local/zookeeper/zookeeper-3.4.10/bin/../conf/zoo.cfgStopping zookeeper ... STOPPED 2.2. 伪集群模式zookeeper还可以在单机上运行多个zk实例,实现单机伪集群的搭建,即单机环境下模拟zk集群的运行。现在在单机上搭建一个3个节点的伪分布式环境,需要配置3个配置文件(zoo1.cfg、zoo2.cfg、zoo3.cfg,分别代表3个节点的配置信息)。在配置过程中,必须保证各个配置文件中的端口号(clientPort)不能冲突,zk数据及日志保存目录(dataDir、dataLogDir)也不能一样。除此之外,还需要在每个节点对应的dataDir中创建一个名为myid的文件,并写入一个数字以标识当前的zk实例。在一台单机上部署3个节点的伪集群模式的zookeeper环境,假设3台zk服务分别为server1、server2和server3,对应3个配置文件分别为zoo1.cfg、zoo2.cfg和zoo3.cfg,这些重点的配置文件信息描述如下:conf/zoo1.cfg:1234567891011tickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataDir1dataLogDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataLogDir1clientPort=2181#server.id=host:port1:port2,其中id为server id,对应myid;host为ip或主机名称;port1为用于followers连接到leader的端口; port2为leader选举时使用的端口.server.1=127.0.0.1:2287:3387server.2=127.0.0.1:2288:3388server.3=127.0.0.1:2289:3389 创建myid文件,并写入server id:1echo "1" > cluster-data/dataDir1/myid conf/zoo2.cfg:1234567891011tickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataDir2dataLogDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataLogDir2clientPort=2182#server.id=host:port1:port2,其中id为server id,对应myid;host为ip或主机名称;port1为用于followers连接到leader的端口; port2为leader选举时使用的端口.server.1=127.0.0.1:2287:3387server.2=127.0.0.1:2288:3388server.3=127.0.0.1:2289:3389 创建myid文件,并写入server id:1echo "2" > cluster-data/dataDir2/myid conf/zoo3.cfg:1234567891011tickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataDir3dataLogDir=/usr/local/zookeeper/zookeeper-3.4.10/cluster-data/dataLogDir3clientPort=2183#server.id=host:port1:port2,其中id为server id,对应myid;host为ip或主机名称;port1为用于followers连接到leader的端口; port2为leader选举时使用的端口.server.1=127.0.0.1:2287:3387server.2=127.0.0.1:2288:3388server.3=127.0.0.1:2289:3389 创建myid文件,并写入server id:1echo "3" > cluster-data/dataDir3/myid 启动zk服务:1234567891011121314151617zkServer.sh start conf/zoo1.cfg#运行server1实例,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo1.cfgStarting zookeeper ... STARTEDzkServer.sh start conf/zoo2.cfg#运行server2实例,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo2.cfgStarting zookeeper ... STARTEDzkServer.sh start conf/zoo3.cfg#运行server3实例,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo3.cfgStarting zookeeper ... STARTED 启动后,可通过jps命令,查看zk运行情况:123418866 QuorumPeerMain18967 Jps18936 QuorumPeerMain18894 QuorumPeerMain 其中,QuorumPeerMain为zk集群的启动入口类,用来加载配置启动QuorumPeer线程。 查看zk个节点状态:1234567891011121314151617zkServer.sh status conf/zoo1.cfg#查看server1状态,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo1.cfgMode: followerzkServer.sh status conf/zoo2.cfg#查看server2状态,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo2.cfgMode: leaderzkServer.sh status conf/zoo3.cfg#查看server3状态,输出信息如下:ZooKeeper JMX enabled by defaultUsing config: conf/zoo3.cfgMode: follower 从返回的状态信息可知,server2为leader、server1和server3为follower。 2.3. 集群模式在真实环境中,为提供可靠的zookeeper分布式环境,通常一台机器上只部署一个zk服务。在zk集群中,若超过半数以上的服务节点可用,则整个zk集群服务是可用的,故而其节点数通常为大于等于3的奇数(2n+1)。现在分别在3台机器上搭建zk分部署环境,3台机器情况如下: zk服务标识 ip myid server1 192.168.56.100 1 server2 192.168.56.101 2 server3 192.168.56.102 3 集群配置方式与上面两种方式类似,为方便操作可在每台机器上配置zookeeper的环境变量,每台机器上的配置文件也都相同。conf/zoo.cfg:1234567891011tickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/zookeeper-3.4.10/dataDirdataLogDir=/usr/local/zookeeper/zookeeper-3.4.10/dataLogDirclientPort=2182#server.id=host:port1:port2,其中id为server id,对应myid;host为ip或主机名称;port1为用于followers连接到leader的端口; port2为leader选举时使用的端口.server.1=192.168.56.100:2288:3388server.2=192.168.56.101:2288:3388server.3=192.168.56.102:2288:3388 在每台机器上的/usr/local/zookeeper/zookeeper-3.4.10/dataDir中创建myid文件,并写入server id:server1为1、server2为2、server3为3。 在每台机器上分别启动zk服务:1zkServer.sh start 3. 配置文件详解 3.1. conf/zoo_sample.cfg:zoo_sample.cfg为zookeeper的核心配置文件,需要将其修改为zoo.cfg。其中各个参数的解释如下: 3.1.1. Minimum Configuration(必要配置参数) 参数名 说明 tickTime 默认为2000。zk中基本时间单位长度(ms),zk中的时间都以该时间为基础,是该时间的倍数,如最小的session过期时间就是tickTime的两倍。服务器与服务器或客户端与服务器之间维持心跳的时间间隔,即每个tickTime会发送一个心跳,通过该心跳可以监视机器的工作状态、控制follower和leader的通信时间等。 dataDir 默认为/tmp/zookeeper。该默认目录仅为样例,不建议直接使用。存储快照文件snapshot的目录,即保存数据的目录,默认情况下,zk会将写数据的日志文件也存储在该目录。zk的数据在内存中以树形结构进行存储,而快照为每隔一段时间就会把整个DataTree的数据序列化后存储在磁盘中。 clientPort 默认为2181。客户端连接zk服务器的端口,zk会监听这个端口,接受客户端的访问请求。 3.1.2. Advanced Configuration(可选的高级配置项,更细化的控制)参数名说明dataLogDir(No Java system property)存储事务日志文件的目录。默认情况下在没有配置该参数时,zk会将事务日志和快照数据都存储在dataDir中,但是实际中最好将这两者的分开存储。因为当zk进行频繁读写操作时,会产生大量事务日志信息,将这两者分开存储可以提高性能,当然分不同磁盘进行存储可以进一步提高整体性能。在zk工作过程中,针对所有事务操作,在返回客户端“事务成功”的响应前,zk会保证已经将本次操作的事务日志写到磁盘上,只有这样,事务才会生效。globalOutstandingLimit默认为1000。(Java system property: zookeeper.globalOutstandingLimit)最大请求堆积数,即等待处理的最大请求数量的限制。当有很多客户端不断请求服务端时,可能导致请求堆积和内存耗尽,为避免这种情况,可通过设置该参数来限制同时处理的请求数。snapCount默认为100000。(Java system property: zookeeper.snapCount)用于配置相邻两次数据快照之间的事务操作次数,即ZooKeeper会在snapCount次事务操作(事务日志输出)之后进行一次数据快照。当新增log条数(事务日志)达到 snapCount/2 + Random.nextInt(snapCount/2) 时(log条数在[snapCount/2+1, snapCount]区间),将触发一次快照,此时zk会生成一个snapshot.*文件,同时创建一个新的事务日志文件log.*,同时log计数重置为0,以此循环。使用随机数的原因:让每个服务器生成快照的时间随机且可控,避免所有服务端同时生成快照(生成快照过程中将阻塞请求)。preAllocSize默认为64M,单位为KB。(Java system property: zookeeper.preAllocSize)用于配置zk事务日志文件预分配的磁盘空间大小。每当剩余空间小于4K时,将会再次预分配,如此循环。如果生成快照比较频繁时,可适当减小snapCount大小。比如,100次事务会新产生一个快照,新产生快照后会用新的事务日志文件,假设每次事务操作的数据量最多1KB,那么preAllocSize设置为1000KB即可。故而preAllocSize常与snapCount相关协调配置。maxClientCnxns默认为60。3.4.0版本之前默认为10。(No Java system property)限制单个客户端与单台服务器之间的并发连接数(在socket层级),根据IP区分不同的客户端。设置为0,可取消该限制。配置该参数可用来阻止某种类型的DoS攻击,包括文件描述符资源耗尽。clientPortAddress无默认值。New in 3.3.0。指定侦听clientPort的address(ipv4, ipv6 or hostname),默认情况下,clientPort会绑定到所有IP上。在物理server具有多个网络接口时,可以设置特定的IP。minSessionTimeoutmaxSessionTimeout默认为2*tickTime和20*tickTime。New in 3.3.0。(No Java system property)Session超时时间限制,如果客户端设置的超时时间不在这个范围(即2*tickTime~20*tickTime),那么会被强制设置为最大或最小时间。fsync.warningthresholdms默认为1000,单位为毫秒。New in 3.3.4。( Java system property: zookeeper.fsync.warningthresholdms)当zk进行事务日志(WAL)fsync操作消耗的时间大于该参数,则在日志打印报警日志。autopurge.snapRetainCount默认为3。New in 3.4.0。(No Java system property)配置清理文件时需要保留的文件数目,会分别清理dataDir和dataLogDir目录下的文件。因为client与zk交换时会产生大量日志,且zk也会将内存数据存储为快照文件,这些数据不会自动删除,通过autopurge.snapRetainCount和autopurge.purgeInterval这两个参数搭配使用可以自动清理日志。autopurge.purgeInterval默认为0,单位为小时。New in 3.4.0。(No Java system property)清理频率,配置多少小时清理一次。需要填写一个大于等于1的整数,默认是0,表示不开启自动清理功能。若在集群处于忙碌的工作状态时开始自动清理,可能会影响zk集群性能,由于zk暂时无法设置时间段(在集群不忙的时候)来开启清理,故而有时会直接禁用该功能,通过在服务器上配置cron来进行清理操作。syncEnabled默认为true。New in 3.4.6, 3.5.0。(Java system property: zookeeper.observer.syncEnabled)观察者像参与者一样默认记录事务并将快照写入磁盘,这可以减少重新启动时观察者的恢复时间。可设置false来关闭该功能。 3.1.3. Cluster Options(集群控制参数) 参数名 说明 electionAlg默认为3。(No Java system property)配置zk的选举算法。“0”为基于原始的UDP的LeaderElection,“1”为基于UDP和无认证的的FastLeaderElection,“2”为基于UDP和认证的FastLeaderElection,“3”为基于TCP的FastLeaderElection。目前,0、1和2的选举算法的实现已经弃用,并有意从下个版本中移除,故而该参数应该用处不大了。initLimit默认为10。(No Java system property)集群中follower和leader之间初始连接时能容忍的最多心跳数(总时间长度即为10*2000=20000ms,即20s),当follower启动时会与leader建立连接并完成数据同步,leader允许follower在initLimit的心跳数内完成该工作。通常使用默认值即可,但是随着集群数据量的增大,follower启动时与leader的同步时间也会随之增大,使之可能无法在规定时间内完成数据同步,故而此情况下需适当调大该参数。leaderServes默认为yes。(Java system property: zookeeper.leaderServes)配置leader是否接受client连接请求。当zk集群超过3台机器,可以设置为“no”,让leader专注于协调集群中的机器,以提高集群性能。server.x=[hostname]:nnnnn[:nnnnn], etc(No Java system property)“server.id=host:port1:port2” 表示不同ZK服务器的配置。这里的id为server id,对应myid;host为ip或主机名称;port1为用于followers连接到leader的端口;port2为leader选举时使用的端口(当electionAlg为0时,port2不再必要)。若要在单台机器上测试搭建集群服务,需要设置不同的端口,避免端口冲突。syncLimit默认为5。(No Java system property)集群中follower和leader之间通信时能容忍的最多心跳数(总时间长度即为5*2000=10000ms,即10s),即follower和leader之间发送消息时请求和回应的最长时间不能超过syncLimit*tickTime毫秒。在集群工作时,leader会与所有follower进行心跳检测来确认存活状态,若leader在syncLimit*tickTime时间范围内没有收到响应,则认为follower已经掉线,无法和自己进行同步。当集群网络质量较差(如延时问题和丢包问题等),可适当调大该参数。group.x=nnnnn[:nnnnn](No Java system property)对整个大的zk集群进行分组,x为组id,nnnnn是zk集群中各个服务id。如果你给集群中的实例分组的话,各个组之间不能有交集,并且要保证所有组的并集就是整个zk集群。若zk集群分为3组,只要其中两个是稳定的,整个集群状态即为稳定的(过半原则,有2n+1个组,只要有n+1个组是稳定状态,整个集群则为稳定状态);选举leader时,每组为一票,当组内大多数投票,则投票成功。例子可见:http://zookeeper.apache.org/doc/current/zookeeperHierarchicalQuorums.htmlweight.x=nnnnn(No Java system property)常与group搭配使用,用于调节组内单个节点的权重。默认每个节点的权重为1,若为0则不参与选举。例子可见:https://www.jianshu.com/p/405f07b97550cnxTimeout默认为5,单位为秒。(Java system property: zookeeper.cnxTimeout)用于为leader选举通知打开连接的超时时间。仅在electionAlg的值为3时有用。4lw.commands.whitelist默认除了“wchp”和“wchc”之外的所有四字命令。New in 3.4.10。(Java system property: zookeeper.4lw.commands.whitelist)配置四字命令白名单,zk不处理未出现在list上的四字命令。多个以逗号分隔,如:4lw.commands.whitelist=stat, ruok, conf, isro;若需开启所有命令,则为4lw.commands.whitelist=*。ipReachableTimeout时间值,单位为毫秒。New in 3.4.11。(Java system property: zookeeper.ipReachableTimeout)配置当解析hostname时ip地址的可达超时时间。当解析hostname时,且hosts表和DNS服务中hostname对应着多个ip,则默认zk会使用第一个ip,且不会去检查可达性;而当该参数配置了一个大于0的值,zk则会依次判断hostname对应的所有ip是否可达(InetAddress.isReachable(long timeout)),若不可达,则判断下一个,若都不可达,则使用第一个ip。tcpKeepAlive默认为false。New in 3.4.11。(Java system property: zookeeper.tcpKeepAlive)配置用来选举的TCP连接是否为长连接。true为开启长连接。 3.1.4. Authentication & Authorization Options(身份认证和相关授权的配置项) 参数名 说明 zookeeper.DigestAuthenticationProvider.superDigest默认禁用。New in 3.2。(Java system property only: zookeeper.DigestAuthenticationProvider.superDigest)允许zk集群管理员以“super”的身份来访问znode层级结构,且“super”用户没有ACL检查。“org.apache.zookeeper.server.auth.DigestAuthenticationProvider”可生成superDigest,其可通过调用参数“super:<password>”来实现。在启动集群中每台服务器时,可提供生成的“super:<data>”作为系统属性。zk客户端会传递一个“digest”和“super:<password>”认证数据进行身份认证。需要注意,认证数据会以普通文本(明文)的形式传递给服务器,建议仅在本地(非网络中)和加密连接中使用。isroNew in 3.4.0。检查server是否处于只读状态。当回复“ro”时,代表在只读模式;当回复“rw”时,代表在非只读模式(可读可写)。gtmk获取当前10进制64位有符号数值形式的trace mask。stmk设置当前的trace mask。trace mask是为64位,每一位的标识表示开启或禁用server上特定类别的跟踪日志记录。Log4J必须设置为TRACE级别以看到trace日志信息。trace mask的每一位的含义如下:0b0000000000:保留位,留以后用。0b0000000010:记录client的请求, 不包括ping请求。0b0000000100:保留位,留以后用。0b0000001000:记录client的ping请求。0b0000010000:记录当前leader的信息,不包括ping请求。0b0000100000:记录client sessions的创建、移除和认证。0b0001000000:记录向client sessions传递的监控事件。0b0100000000: 保留位,留以后用。0b1000000000: 保留位,留以后用。默认的trace mask为0b0100110010。调用stmk命令时, server会将设置后的trace mask以十进制数值的形式返回回来。一个使用perl调用stmk命令的例子:$ perl -e "print 'stmk', pack('q>', 0b0011111010)" | nc localhost 2181250 3.1.5. Experimental Options/Features(实验性配置项) 参数名 说明 Read Only Mode Server 默认为false。New in 3.4.0。(Java system property: readonlymode.enabled)配置为true,zk启用只读模式服务支持。在这种模式下,ROM clients仍然可以从zk读取值,但是不能写入值并查看来自其他客户机的更改。更多细节参见ZOOKEEPER-784。 3.1.6. Unsafe Options(不安全配置项) 参数名 说明 forceSync(Java system property: zookeeper.forceSync)默认情况下,zk要求先更新事务日志,再执行事务操作。若该参数设置为no,zk将不再需要等更新完事务日志后再执行事务操作。jute.maxbuffer(Java system property: jute.maxbuffer)配置可在一个znode中存储数据的最大容量。默认为0xfffff,或低于1M。注意若变更此值,所有server都要同步修改。skipACL(Java system property: zookeeper.skipACL)配置跳过ACL检查。这可以提高吞吐量,但会对所有人开放完全访问数据树,很不安全。quorumListenOnAllIPs若设置为true,zk服务将监听所有本地可用的ip地址,不仅仅是配置文件中server list。它会影响处理ZAB协议和 Fast Leader Election协议的连接。默认为false。 3.1.7. Communication using the Netty framework(使用Netty框架进行通信)Netty是一个基于NIO的客户机/服务器通信框架,它简化了(通过直接使用NIO) java应用程序网络级通信的许多复杂性。此外,Netty框架内置了对加密(SSL)和身份验证(证书)的支持。这些都是可选的功能,可以单独打开或关闭。在版本3.4之前,ZooKeeper一直都是直接使用NIO,但是在版本3.4及以后的版本中,Netty作为NIO(替换)的选项得到了支持。NIO仍然是默认值,但是基于Netty的通信可以通过将环境变量“zookeeper.serverCnxnFactory”设置为“org.apache.zookeeper.server.NettyServerCnxnFactory”来代替NIO。您可以在客户端或服务器上设置此项,或者两者都设置。 参考资料[1] Zookeeper Overview. http://zookeeper.apache.org/doc/r3.4.10/zookeeperOver.html.[2] ZooKeeper Administrator’s Guide. http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_configuration.[3] zookeeper调优(遇到就添加). https://my.oschina.net/u/3049601/blog/1809785.[4] zookeeper日志各类日志简介. https://www.cnblogs.com/jxwch/p/6526271.html.[5] ZooKeeper: 简介, 配置及运维指南. https://www.cnblogs.com/neooelric/p/9230967.html.[6] ZooKeeper学习第二期–ZooKeeper安装配置. https://www.cnblogs.com/sunddenly/p/4018459.html.]]></content>
<categories>
<category>zookeeper系列</category>
</categories>
<tags>
<tag>zookeeper</tag>
</tags>
</entry>
</search>