同步容器类与并发容器类
1.同步容器类
同步容器类包括Vector和HashTable,这两个都是JDK早期的容器。后来在JDK1.2也引入一个功能与之类似的类,这些同步的封装容器类是由Collections.synchronizedXXX等工厂方法创建的。这些类实现线程安全的方式是:将他们的状态封装起来,并对每个公有方法都进行同步,使得每次只有线程能访问容器的状态。
1.查看HashTable同步原理---所有方法加synchronized关键字修饰
public synchronized V get(Object key) { } public synchronized V put(K key, V value) {
}
2. Collections.synchronizedXXX 是返回一个同步容器(同步容器都是Collections的静态内部类---所有的方法内部同步代码块,互斥锁mutex是一个普通的Object)
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { private static final long serialVersionUID = -7754090372962971524L; final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } ........public int hashCode() { synchronized (mutex) {return list.hashCode();} } public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } }
mutex是继承自SynchronizedCollection的一个普通的Object,用做锁。mutex有互斥的意思。
其实所有的元素都存在其内部的成员属性,比如 synchronizedList 有一个成员属性 list。 SynchronizedMap有一个成员属性 Map。我们调用其方法实际内部调用的是真正的存放数据的成员属性集合的方法。
1.1 同步容器类的问题:
同步容器类虽然是线程安全的,但是某些情况下需要加同步来保护复合操作(也就是一个方法中对容器进行多个操作),有可能在两次操作的中间时间容器被修改造成异常。如下;
package cn.qlq.thread.eighteen; import java.util.Vector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cn.qlq.thread.seventeen.Demo5; public class Demo1 { private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class); public static void main(String[] args) throws InterruptedException { final Vector<String> vector = new Vector<>(); vector.add("11111111");// 添加元素 Thread.sleep(1 * 1000); new Thread(new Runnable() { @Override public void run() { Demo1.getLast(vector); } }).start(); // 删除元素 Thread.sleep(1 * 100); new Thread(new Runnable() { @Override public void run() { Demo1.removeLast(vector); } }).start(); } public static Object getLast(Vector<String> list) { int index = list.size() - 1; LOGGER.info("getLast:index ->{}", index); // 休眠等待第二个线程删除 try { Thread.sleep(5 * 100); } catch (InterruptedException e) { e.printStackTrace(); } return list.get(index); } public static void removeLast(Vector<String> list) { int index = list.size() - 1; list.remove(index); LOGGER.info("成功删除元素,index{},threadName->{}", index, Thread.currentThread().getName()); } }
结果:
18:44:16 [cn.qlq.thread.seventeen.Demo5]-[INFO] getLast:index ->0
18:44:16 [cn.qlq.thread.seventeen.Demo5]-[INFO] 成功删除元素,index0,threadName->Thread-1
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
at java.util.Vector.get(Vector.java:744)
at cn.qlq.thread.eighteen.Demo1.getLast(Demo1.java:46)
at cn.qlq.thread.eighteen.Demo1$1.run(Demo1.java:22)
at java.lang.Thread.run(Thread.java:745)
单看getLast方法与removeLast方法是没有任何问题的,但是在getLast方法中获取到index之后,线程停顿过程中,另一个线程删除了集合中仅有的一个元素,所以getLast线程休眠完之后获取最后一个元素的时候报错。在多线程环境中,这种复合操作也是极易造成问题的。
解决办法: 同步使getLast和removeLast方法变为原子操作。
(1)在两个静态方法加synchronized关键字同步方法(对Demo1.class加锁)
public static synchronized Object getLast(Vector<String> list) { int index = list.size() - 1; LOGGER.info("getLast:index ->{}", index); // 休眠等待第二个线程删除 try { Thread.sleep(5 * 100); } catch (InterruptedException e) { e.printStackTrace(); } return list.get(index); } public static synchronized void removeLast(Vector<String> list) { int index = list.size() - 1; list.remove(index); LOGGER.info("成功删除元素,index{},threadName->{}", index, Thread.currentThread().getName()); }
(2)代码快中对参数vector进行加锁:
public static Object getLast(Vector<String> list) { synchronized (list) { int index = list.size() - 1; LOGGER.info("getLast:index ->{}", index); // 休眠等待第二个线程删除 try { Thread.sleep(5 * 100); } catch (InterruptedException e) { e.printStackTrace(); } return list.get(index); } } public static synchronized void removeLast(Vector<String> list) { synchronized (list) { int index = list.size() - 1; list.remove(index); LOGGER.info("成功删除元素,index{},threadName->{}", index, Thread.currentThread().getName()); } }
补充:所以我们平时在使用for循环遍历vector的时候也有可能出现问题,如下:
final Vector<String> vector = new Vector<>(); for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); }
上面代码在执行的过程中的正确性依赖于运气,即在调用size和get之间没有线程会修改Vector。但是在多线程环境中,很有可能出现这种交替执行的问题,将会抛出ArrayIndexOutOfBoundsException异常。我们可以在遍历过程中加锁控制,这样虽然会牺牲一些伸缩性,但是可以防止其他线程在迭代期间修改vector。如下:
final Vector<String> vector = new Vector<>(); synchronized(vector){ for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } }
1.2 迭代器与ConcurrentModificationException (并发修改异常--一种运行时异常)
在对并发容器进行迭代,无论是直接迭代还是Java5引入的增强for循环(其原理也是通过迭代器遍历),对容器类进行迭代的标准方式都是使用迭代器Iterator。如果有其他线程并发地修改容器,那么即使是使用迭代器也无法在迭代期间对容器加锁。在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且他们表现出的行为是"及时失败(fail-fast)"。这也意味着,当它们发现在迭代过程中被修改时,就会抛出一个
ConcurrentModificationException 异常。说白了ConcurrentModificationException异常只有在迭代器迭代过程中,如果容器被修改(包括addElement、removeElementAt、insert等操作)在迭代器再次调用迭代器的方法时会抛出并发修改异常。原因是容器自身的修改会改变modCount的值,在迭代器的操作过程中发现modCount不等于expectedModCount的值,所以抛出异常。
Fail-Fast 机制
我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
这种及时失败的迭代器并不是完善的处理机制,而只是"善意地"捕获并发错误,因此只能作为并发问题的预警指示器。它们的实现方式是:将计数器的变化与容器关联起来,如果在迭代器期间计数器被修改,那么hasNext或next将抛出ConcurrentModificationException。然而,这种检查是在没有同步的情况下进行的,因此可能会看到失效的计数值,而迭代器可能没有意识到已经发生了修改。
一种办法就是在迭代过程中加锁,这样就可以避免其他线程在迭代过程中修改容器,另一种办法就是"克隆"容器,在副本上进行迭代,由于副本封闭在线程内,因此其他线程不会在迭代期间对其进行修改。
即使是单个线程迭代器访问也了能报并发修改异常,如下代码:
package cn.qlq.thread.eighteen; import java.util.Vector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cn.qlq.thread.seventeen.Demo5; public class Demo2 { private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class); public static void main(String[] args) throws InterruptedException { final Vector<String> vector = new Vector<>(); vector.add("111"); vector.add("222"); vector.add("333"); for (String string : vector) { if ("111".equals(string)) { vector.remove(string); } } } }
结果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1156)
at java.util.Vector$Itr.next(Vector.java:1133)
at cn.qlq.thread.eighteen.Demo2.main(Demo2.java:18)
解释:Vector内部维护一个modCount(继承自AbstractList),其内部类Itr迭代器中(实现迭代器接口)有一个成员变量一个expectedModCount,expectedModCount是期望修改的次数,一开始的时候是等于集合中数组的个数,modCount是实际修改的次数,一开始也是等于集合中元素的个数。所以在 vector.remove(string); 之后modCount变为4,而迭代器中的expectedModCount还是3,因此在调用iterator.next()的时候抛出并发修改异常。
Vector.remove(obj)调用removeElement(obj)方法,此方法中将modCount加一,也就是修改次数加了一次。
public synchronized boolean removeElement(Object obj) { modCount++; int i = indexOf(obj); if (i >= 0) { removeElementAt(i); return true; } return false; }
查看迭代器的next()方法:首先检查实际修改次数与期望修改次数是否不一致,不一致抛出并发修改异常。
public E next() { synchronized (Vector.this) { checkForComodification(); int i = cursor; if (i >= elementCount) throw new NoSuchElementException(); cursor = i + 1; return elementData(lastRet = i); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
解决办法:(采用迭代器的remove()方法可以避免出现并发修改异常)
public static void main(String[] args) throws InterruptedException { final Vector<String> vector = new Vector<>(); vector.add("111"); vector.add("222"); vector.add("333"); Iterator<String> iterator = vector.iterator(); while(iterator.hasNext()){ String next = iterator.next(); if ("111".equals(next)) { iterator.remove(); } } System.out.println(vector); }
结果:
[222, 333]
解释:迭代器删除之前必须先调用iterator.next()方法
cursor默认为0。返回元素之后,cursor加一,同时将未加一的cursor赋值给lastRet。删除的时候也是根据上次返回的删除,所以调用删除之前必须先执行next()方法。cursor始终比lastRet大一。
比如第一次访问next()之后: cursor为1,lastRet为0,返回下标为lastRet的元素(返回第一个元素)。调用remove()方法的时候删除下标为lastRet的元素,删除第一个元素。
next():将cursor赋值给lastRet,将cursor加一,返回元素的时候返回下标为lastRet的元素。
原码查看,查看迭代器的remove()方法:(删除完元素之后将 expectedModCount与modCount 设为相同的值,所以在下次不会出现并发修改异常)
public void remove() { if (lastRet == -1) throw new IllegalStateException(); synchronized (Vector.this) { checkForComodification(); Vector.this.remove(lastRet); expectedModCount = modCount; } cursor = lastRet; lastRet = -1; }
2.并发容器类
Java5提供了许多并发容器类来进行同步容器的性能。同步容器(上面的Vector和HashTable)将所有对容器状态的访问都串行化,以实现它们的线程安全。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。
另一方面,并发容器类是针对多个线程并发访问设计的。在Java5中增加了ConcurrentHashMap,用来代替同步并且基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步的list。在新的ConcurrentMp接口中增加了一些对常见复合操作的支持,例如若没有则添加、替换、以及有条件删除等。(并发容器代替同步容器可以极大地提高伸缩性并降低风险)。Java6中引入了ConcurrentSkipListMap和ConcurrentSkipListSet分别作为同步的SortedMap和SortedSet的并发替代品(例如用synchronizedMap包装的TreeMap或TreeSet)。
2.1 ConcurrentHashMap
同步容器类在执行每个操作期间都需要获得容器的锁,所以变为串行的工作模式。
与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的枷锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个同步锁上并且使得每次只有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的线程可以并发地修改Map。
ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
ConcurrentHashMap与其他并发容器类一起增加了同步容器类:它们提供的迭代器不会抛出并发修改异常。ConcurrentHashMap返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。但是也有一些缺点,对于一些操作整个map的方法,比如size、isEmpty,这些方法发的语义被略微减弱了以反映容器的并发性。事实上size可能返回的是一个不精确的值,但事实上size和isEmpty这样的方法在并发容器的用处很小,因为它们的返回值总在变化。
与HashTable和synchronizedMap相比,ConcurrentHashMap有着更多的优势以及更少的坏处。在大多数情况下,应该用ConcurrentHashMap代替同步Map。只有当应用程序需要加锁Map以进行独占访问时,才应该使用同步Map。
ConcurrentHashMap原理解析:
上面说了分段锁(Lock Striping),实际ConcurrentHashMap内部包含一个Segment[],Segment(中文有片段的意思)内部包含HashEntry,真正的数据就存放在HashEntry中。(Segment和HashEntry都是ConcurrentHashMap的一个静态内部类)
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { final Segment<K,V>[] segments; ...
static final class Segment<K,V> extends ReentrantLock implements Serializable { transient volatile HashEntry<K,V>[] table; ... } static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; HashEntry(int hash, K key, V value, HashEntry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
... } }
其结构可以理解为下图:
Segment是一个继承ReentrantLock 的可重入锁,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
以put元素为例查看其执行过程:(各种反编译不同,在这里就直接查看JDK自带的src.lib包下面的源码)
@SuppressWarnings("unchecked") public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }
是求出所在的段Segment,然后将具体的添加操作交给Segment段。下面查看Segment的put操作:
final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1; if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
- 是否需要扩容。在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阀值,数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
- 如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容
参考:https://www.cnblogs.com/ITtangtang/p/3948786.html
2.2 额外的原子Map操作
一些常见的原子操作,如"若没有则添加"、"若相等则移除"、"若相等则替换"等操作,都已经实现为原子操作并且在ConcurrentMap接口中声明。所以如果需要在现有的同步map实现这样的功能,可以考虑使用ConcurrentMap。
例如:
ConcurrentHashMap<Object, Object> concurrentHashMap = new ConcurrentHashMap<>(); // 若没有则添加 concurrentHashMap.putIfAbsent("1", "0101"); System.out.println(concurrentHashMap); // 再次put如果存在会覆盖原值 concurrentHashMap.put("1", "111"); System.out.println(concurrentHashMap); // 替换,不管原来的值 concurrentHashMap.replace("1", "1111"); System.out.println(concurrentHashMap); // 替换,如果原来的key为1,值为111就将值替换为111 concurrentHashMap.replace("1", "1111", "111"); System.out.println(concurrentHashMap); // 替换,如果原来的key为1,值为111就删除 concurrentHashMap.remove("1", "111"); System.out.println(concurrentHashMap);
结果:
{1=0101}
{1=111}
{1=1111}
{1=111}
{}
2.3 CopyOnWriteArrayList
CopyOnWriteArrayList用于代替同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁会复制。(类似地 CopyOnWriteArraySet的作用是替代同步Set)。
"写入时复制(copy-on-write)"容器的线程安全性在于,在每次修改元素时,都会创建一个新的容器副本,从而实现可变性。"写入时复制"容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需要确保数组内容的可见性。因此,多个线程可以同时对这个容器进行迭代,彼此不会干扰,并且不会抛出并发修改异常,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作带来的影响。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
缺点就是每次修改容器都会复制底层数组,这需要一定的开销,特别是当容器规模较大的时候。仅当迭代操作远远多于修改操作,才应该使用"写入时复制"容器。
以add(E e)为例查看源码:(获取到锁之后复制数据出来之后进行修改,修改完成之后调用setArray方法重新将元素的引用指向修改后的容器)
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
以get(int index)为例查看源码:
public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; } final Object[] getArray() { return array; }
可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁。
例如:(迭代中修改容器没有报错--但是此容器的迭代器不能删除元素)
package cn.qlq.thread.eighteen; import java.util.concurrent.CopyOnWriteArrayList; public class Demo4 { public static void main(String[] args) throws InterruptedException { CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>(); strings.add("1"); strings.add("2"); strings.add("3"); System.out.println(strings); for (String string : strings) { if ("2".equals(string)) { strings.remove(string); strings.add("22"); } } System.out.println(strings); } }
结果:
[1, 2, 3]
[1, 3, 22]
注意:此容器的迭代器不能删除元素
CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>(); strings.add("1"); strings.add("2"); strings.add("3"); System.out.println(strings); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()) { String next = iterator.next(); if ("2".equals(next)) { iterator.remove(); } } System.out.println(strings);
结果:
[1, 2, 3]Exception in thread "main"
java.lang.UnsupportedOperationException
at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1040)
at cn.qlq.thread.eighteen.Demo4.main(Demo4.java:18)
查看迭代器源码:
private static class COWIterator<E> implements ListIterator<E> { /** Snapshot of the array */ private final Object[] snapshot; /** Index of element to be returned by subsequent call to next. */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public boolean hasPrevious() { return cursor > 0; } @SuppressWarnings("unchecked") public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } @SuppressWarnings("unchecked") public E previous() { if (! hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor-1; } public void remove() { throw new UnsupportedOperationException(); } public void set(E e) { throw new UnsupportedOperationException(); } public void add(E e) { throw new UnsupportedOperationException(); } }