容器是所有编程中的基础工具,这其中也包括并发编程。出于这个原因,像Vector和Hashtable这类早期容器具有许多synchronized方法,当他们用于非多线程的应用程序中时,便会导致不可接受的开销。在Java1.2中,新的容器类库是不同步的,并且Collections类提供了各种static的同步的装饰方法,从而来同步不同类型的容器。尽管这是一种改进,因为它使你可以选择在你的容器中是否要使用同步,但是这种开销仍旧是基于synchronized加锁机制的。Java SE5特别添加了新容器,通过使用更灵巧的技术消除加锁,从而提高线程的安全的性能。
这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改后的结果即可。修改是在容器数据结构的某一部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。
在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成时,一个原子性操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException,因此你不必编写特殊的代码去防范这种异常,就像你以前必须作的那样。
迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。
CopyOnWriteArraySet将使用CopyOnWriteArrayList来实现其免锁行为。
ConcurrentHashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,任何修改在完成之前,读取者仍旧不能看到它们。ConcurrentHashMap不会抛出ConcurrentModificationException异常。
CopyOnWriteArrayList适合用在“读多,写少”的“并发”应用中,换句话说,它适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在“扩容”的概念,每次写操作(add or remove)都要copy一个副本,在副本的基础上修改后改变array引用,所以称为“CopyOnWrite”,因此在写操作是加锁,并且对整个list的copy操作时相当耗时的,过多的写操作不推荐使用该存储结构。
一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。
源码分析 在ConcurrentHashMap的remove,put操作还是比较简单的,都是将remove或者put操作交给key所对应的segment去做的,所以当几个操作不在同一个segment的时候就可以并发的进行。
public V remove(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).remove(key, hash, null); }
而segment中的remove操作除了加锁之外和HashMap中的remove操作基本无异
V remove(Object key, int hash, Object value) { lock(); try { int c = count - 1; HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) { V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned. ++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile } } return oldValue; } finally { unlock(); } }
package tij; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Created by huaox on 2017/4/21. * */ public class CopyOnWriteArrayListDemo { private static class ReadTask implements Runnable { List<String> list; ReadTask(List<String> list) { this.list = list; } public void run() { for (String str : list) { System.out.println(str); } } } private static class WriteTask implements Runnable { List<String> list; int index; WriteTask(List<String> list, int index) { this.list = list; this.index = index; } public void run() { list.remove(index); list.add(index, "write_" + index); } } public void run() { final int NUM = 5; List<String> list = new ArrayList<String>(); for (int i = 0; i < NUM; i++) { list.add("main_" + i); } ExecutorService executorService = Executors.newFixedThreadPool(NUM); for (int i = 0; i < NUM; i++) { executorService.execute(new ReadTask(list)); executorService.execute(new WriteTask(list, i)); } executorService.shutdown(); } public static void main(String[] args) { new CopyOnWriteArrayListDemo().run(); } }
输出结果:
Exception in thread "pool-1-thread-3" java.util.ConcurrentModificationException main_0 main_1 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) main_2 main_3 at java.util.ArrayList$Itr.next(ArrayList.java:851) main_4 main_0 main_1 write_2 main_3 main_4 main_0 main_1 write_2 write_3 main_4 main_0 write_0 write_1 at tij.CopyOnWriteArrayListDemo$ReadTask.run(CopyOnWriteArrayListDemo.java:22) write_2 write_3 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) write_4 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Process finished with exit code 0
免锁容器的源码为赋值底层数组,然后赋新值,改引用
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(); } }
修改为CopyOnWriteArrayList后
public void run() { final int NUM = 5; List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < NUM; i++) { list.add("main_" + i); } ExecutorService executorService = Executors.newFixedThreadPool(NUM); for (int i = 0; i < NUM; i++) { executorService.execute(new ReadTask(list)); executorService.execute(new WriteTask(list, i)); } executorService.shutdown(); }
输出结果:
main_0
main_1
main_2
main_3
main_4
main_0
main_1
write_2
main_3
main_4
write_0
main_1
write_2
write_3
main_4
write_0
main_1
write_2
write_3
write_4
write_0
write_1
write_2
write_3
write_4
Process finished with exit code 0