> 文章列表 > Java 进阶(15)线程安全集合

Java 进阶(15)线程安全集合

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();;}}
}