Java 进阶(15)线程安全集合
CopyOnWriteArrayList
线程安全的ArrayList,加强版读写分离。
写有锁,读⽆锁,读写之间不阻塞,优于读写锁。
写⼊时,先copy⼀个容器副本、再添加新元素,最后替换引⽤。
使⽤⽅式与ArrayList⽆异。
示例:
public class TestCopyOnWriteArrayList {public static void main(String[] args) {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();//创建一个线程池ExecutorService pool= Executors.newFixedThreadPool(5);for (int i = 0; i < 5; i++) {pool.submit(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10; j++) {list.add("content"+new Random().nextInt(100));}}});}//关闭线程池//4关闭线程池pool.shutdown();while(!pool.isTerminated()){}//5打印结果System.out.println("元素个数:"+list.size());for (String string : list) {System.out.println(string);}}
}
CopyOnWriteArrayList如何做到线程安全的
CopyOnWriteArrayList使⽤了⼀种叫写时复制的⽅法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷⻉⼀份出来,然后在新的数组做写操作,写完之后,再将原来的数组引⽤指向到新数组。
当有新元素加⼊的时候,如下图,创建新数组,并往新数组中加⼊⼀个新元素,这个时候,array这个引⽤仍然是指向原数组的。
CopyOnWriteArrayList 的整个add操作都是在锁的保护下进⾏的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
CopyOnWriteArrayList 的 add 操作的源代码如下:
public boolean add(E e) {//1、先加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;//2、拷⻉数组Object[] newElements = Arrays.copyOf(elements, len + 1);//3、将元素加⼊到新数组中newElements[len] = e;//4、将array引⽤指向到新数组setArray(newElements);return true;} finally {//5、解锁lock.unlock();}
}
由于所有的写操作都是在新数组进⾏的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分⼏种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引⽤还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引⽤已经指向了新的数组,那么直接从新数组中读取数据。
可见, CopyOnWriteArrayList 的读操作是可以不用加锁的。
CopyOnWriteArraySet
CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent(若没有则增加)方法
CopyOnWriteArraySet介绍
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。
和CopyOnWriteArrayList类似,CopyOnWriteArraySet具有以下特性:
1. 它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
2. 它是线程安全的。
3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
4. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作。
5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
public class TestCopyOnWriteArraySet {public static void main(String[] args) {//1创建集合CopyOnWriteArraySet<String> set=new CopyOnWriteArraySet<>();//2添加元素set.add("pingguo");set.add("huawei");set.add("xiaomi");set.add("lianxiang");set.add("pingguo");//3打印System.out.println("元素个数:"+set.size());System.out.println(set.toString());}
}
ConcurrentHashMap
初始容量默认为16段(Segment),使⽤分段锁设计。
不对整个Map加锁,⽽是为每个Segment加锁。
当多个对象存⼊同⼀个Segment时,才需要互斥。
最理想状态为16个对象分别存⼊16个Segment,并⾏数量16。
使⽤⽅式与HashMap⽆异。
示例:
public class TestConcurrentHashMap {public static void main(String[] args) {ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();for (int i = 0; i < 5; i++) {new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10; j++) {map.put(Thread.currentThread().getName()+"--->"+j,j+"");System.out.println(map);}}}).start();;}}
}