J.U.C
是java.util.concurrent
的简写,里面提供了很多线程安全的集合。
CopyOnWriteArrayList
介绍
CopyOnWriteArrayList
相比于ArrayList
是线程安全的,字面意思是写操作时复制
。CopyOnWriteArrayList
使用写操作时复制
技术,当有新元素需要加入时,先从原数组拷贝一份出来。然后在新数组里面加锁添加,添加之后,将原来数组的引用指向新数组。
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
的add
操作是在加锁的保护下完成的。加锁是为了多线程对CopyOnWriteArrayList
并发add
时,复制多个副本,把数据搞乱。
public E get(int index) {
return get(getArray(), index);
}
以上代码显示get
是没有加锁的如果出现并发
get
,会有以下3中情况。
- 如果写操作未完成,那么直接读取原数组的数据;
- 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
- 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
CopyOnWriteArrayList
多线程代码演示。
package com.rumenz.task;
import java.util.List;
import java.util.concurrent.*;
//线程安全
public class CopyOnWrireArrayListExample {
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static List list=new CopyOnWriteArrayList();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final Integer j=i;
executorService.execute(()->{
try{
semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("size:"+list.size());
}
private static void update(Integer j) {
list.add(j);
}
}
//size:5000
CopyOnWriteArrayList
使用场景
- 由于在
add
的时候需要拷贝原数组,如果原数组内容比较多,比较大,可能会导致young gc
和full gc
。 - 不能用于实时读的场景,像拷贝数组,新增元素都需要时间,所以调用
get
操作后,有可能得到的数据是旧数据,虽然CopyOnWriteArrayList
能做到最终一致性,但是没有办法满足实时性要求。 CopyOnWriteArrayList
适合读多写少的场景,比如白名单,黑名单等场景CopyOnWriteArrayList
由于add
时需要复制数组,所以不适用高性能的互联网的应用。
CopyOnWriteArraySet
介绍
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList();
}
CopyOnWriteArraySet
底层是用CopyOnWriteArraySet
来实现的。可变操作(add,set,remove等)都需要拷贝原数组进行操作,一般开销很大。迭代器支持hasNext()
,netx()
等不可变操作,不支持可变的remove
操作,使用迭代器速度很快,并且不会与其它线程冲突,在构造迭代器时,依赖不变的数组快照。
CopyOnWriteArraySet
多线代码演示
package com.rumenz.task;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
//线程安全
public class CopyOnWrireArraySetExample {
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Set set=new CopyOnWriteArraySet();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final Integer j=i;
executorService.execute(()->{
try{
semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("size:"+set.size());
}
private static void update(Integer j) {
set.add(j);
}
}
//size:5000
CopyOnWriteArraySet
使用场景
- 适用于set大小一般很小,读操作远远多于写操作的场景
ConcurrentSkipListSet
public ConcurrentSkipListSet() {
m = new ConcurrentSkipListMap();
}
ConcurrentSkipListSet
是jdk6
新增的类,支持自然排序,位于java.util.concurrent
。ConcurrentSkipListSet
都是基于Map
集合的,底层由ConcurrentSkipListMap
实现。在多线程环境下,
ConcurrentSkipListSet
的add
,remove
,contains
是线程安全的。但是对于批量操作addAll
,removeAll
,containsAll
并不能保证原子操作,所以是线程不安全的,原因是addAll
,removeAll
,containsAll
底层调用的还是add
,remove
,contains
方法,在批量操作时,只能保证每一次的add
,remove
,contains
是原子性的(即在进行add
,remove
,contains
,不会被其它线程打断),而不能保证每一次批量操作都不会被其它线程打断,因此在addAll
、removeAll
、retainAll
和containsAll
操作时,需要添加额外的同步操作。
public boolean addAll(Collection extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
public boolean removeAll(Collection> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean containsAll(Collection> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
ConcurrentSkipListSet
代码演示
package com.rumenz.task;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
//线程安全
public class CopyOnWrireArrayListExample {
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Set set= new ConcurrentSkipListSet();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final Integer j=i;
executorService.execute(()->{
try{
semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("size:"+set.size());
}
private static void update(Integer r) {
set.add(r);
}
}
//size:5000
ConcurrentHashMap
ConcurrentHashMap
中key
和value
都不允许为null
,ConcurrentHashMap
针对读操作做了大量的优化。在高并发场景很有优势。在多线程环境下,使用
HashMap
进行put
操作会引起死循环,导致CPU
利用率到100%
,所以在多线程环境不能随意使用HashMap
。原因分析:HashMap
在进行put
的时候,插入的元素超过了容量就会发生rehash
扩容,这个操作会把原来的元素hash
到新的扩容新的数组,在多线程情况下,如果此时有其它线程在进行put
操作,如果Hash
值相同,可能出现在同一数组下用链表表示,造成闭环,导致get
的时候出现死循环,所以是线程不安全的。
HashTable
它是线程安全的,它涉及到多线程的操作都synchronized
关键字来锁住整个table
,这就意味着所有的线程都在竞争同一把锁,在多线程环境下是安全的,但是效率很低。
HashTable
有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMapJDK1.7版本的核心思想。
ConcurrentHashMap
代码演示案例
package com.rumenz.task;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
//线程安全
public class ConcurrentHashMapExample {
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Map map=new ConcurrentHashMap();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final Integer j=i;
executorService.execute(()->{
try{
semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("size:"+map.size());
}
private static void update(Integer j) {
map.put(j, j);
}
}
//size:5000
ConcurrentSkipListMap
ConcurrentSkipListMap
内部使用SkipList
结构实现。跳表是一个链表,但是通过跳跃式的查找方式使得插入
,读取
数据时的时间复杂度变成O(log n)
。跳表(SkipList):使用空间换时间的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。
ConcurrentSkipListMap
代码案例
package com.rumenz.task;
import java.util.Map;
import java.util.concurrent.*;
//线程安全
public class ConcurrentSkipListMapExample {
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Map map=new ConcurrentSkipListMap<>();
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final Integer j=i;
executorService.execute(()->{
try{
semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("size:"+map.size());
}
private static void update(Integer j) {
map.put(j, j);
}
}
//size:5000
ConcurrentHashMap
与ConcurrentSkipListMap
的对比
ConcurrentHashMap
比ConcurrentSkipListMap
性能要好一些。ConcurrentSkipListMap
的key
是有序的,ConcurrentHashMap
做不到。ConcurrentSkipListMap
支持高并发,它的时间复杂度是log(N)
,和线程数无关,也就是说任务一定的情况下,并发的线程越多,ConcurrentSkipListMap
的优势就越能体现出来。
关注微信公众号:【入门小站】解锁更多知识点