想了解更多JUC的知识
——JUC并发编程合集
1. List不安全
-
ArrayList在并发情况下是不安全的
public class ListTest { public static void main(String[] args) { List<Object> arrayList = new ArrayList<>(); for(int i=1;i<=10;i++){ new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(arrayList); },String.valueOf(i)).start(); } } }
-
解决方案:
-
List list = new Vector<>();
-
List list = Collections.synchronizedList(new ArrayList<>());
-
Collections.synchronizedList(new ArrayList<>())源码:
-
这个方法会根据你传入的List是否实现RandomAccess这个接口来返回的SynchronizedRandomAccessList还是SynchronizedList.
-
SynchronizedList实现了List接口,并重写了里面的方法,将里面的方法用
synchronized
修饰,但是listIterator(),iterator()没有加,所以在使用的时候需要加上synchronized。(下面是部分源码)
-
-
List<String> list = new CopyOnWriteArrayList<>();
-
CopyOnWriteArrayList:写入时复制, COW 计算机程序设计领域的一种优化策略
-
特征:
-
线程安全的,多线程环境下可以直接使用,无需加锁
-
通过锁 + 数组拷贝 + volatile 关键字保证了线程安全
-
数组拷贝:每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常
-
volatile:一旦数组被修改,其它线程立马能够感知到
-
-
-
工作流程:从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组
-
加锁
-
从原数组中拷贝出新数组
-
在新数组上进行操作,并把新数组赋值给数组容器
-
解锁
为什么使用CopyOnWriteArrayList而不使用Vector?
Vector底层使用的是synchronized机制,效率很低;而CopyOnWriteArrayList使用的是lock锁机制
-
-
-
2. Set不安全
-
同List一样,在多线程情况下,普通的Set集合是线程不安全的,也会出现ConcurrentModificationException并发修改异常。
-
解决方案有:
- 使用Collections工具类的synchronized包装的Set类
Set<String> set = Collections.synchronizedSet(new HashSet<>());
- 使用CopyOnWriteArraySet 写入复制的JUC解决方案
Set<String> set = new CopyOnWriteArraySet<>();
- HashSet的底层是什么?看源码:
- 不难得出,HashSet的底层其实是HashMap,而HashSet的add方法底层其实就是HashMap里的put方法
- PRESENT:
- 使用Collections工具类的synchronized包装的Set类
3. Map不安全
-
在并发线程下,普通的HashMap是不安全的,也会出现ConcurrentModificationExecption并发修改异常
-
Map<String,Object> map = new HashMap<String,Object>(16,0.75f);
-
解决方法:
- 使用Collections工具类的synchronized包装的Map类
Map<String,Object> map = Collections.synchronizedMap(new HashMap<>());
- 使用ConcurrentHashMap
Map map = new ConcurrentHashMap<>();
- 使用Collections工具类的synchronized包装的Map类
3.1 ConcurrentHashMap
- ConcurrentHashMap是线程安全的数组,是HashTable的替代品,同为线程安全,其性能要比HashTable更好。
JDK 7
-
底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性,
头插法
-
实现原理:
- 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。
- segment继承自ReentrantLock锁。 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
- 可以通过构造函数指定,数组扩容不会影响其他的segment,get无需加锁,volatile保证内存可见性
-
get()操作:HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。
- 将key传入get方法中,先根据key的hashcode的值找到对应的segment段。
- 再根据segment中的get方法再次hash,找到HashEntry数组中的位置。
- 最后在链表中根据hash值和equals方法进行查找。
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
-
put()操作:
- 将key传入put方法中,先根据key的hashcode的值找到对应的segment段
- 再根据segment中的put方法,加锁lock()。
- 再次hash确定存放的hashEntry数组中的位置
- 在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。
JDK 8
-
底层数据结构:Synchronized + CAS(compare and swap) +Node +红黑树。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS。
尾部插入
为什么在有Synchronized 的情况下还要使用CAS?
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能.
-
锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容。
-
读操作无锁 :
- Node的val和next使用volatile修饰,读写线程对该变量互相可见
- 数组用volatile修饰,保证扩容时被读线程感知
-
get()操作:get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。
- 计算hash值,定位到Node数组中的位置
- 如果该位置为null,则直接返回null
- 如果该位置不为null,再判断该节点是红黑树节点还是链表节点
- 如果是红黑树节点,使用红黑树的查找方式来进行查找
- 如果是链表节点,遍历链表进行查找
-
put()操作:
- 先判断Node数组有没有初始化,如果没有初始化先初始化initTable();
- 根据key的hashcode的值进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
- 如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
- 如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
- 如果是红黑树,就按照红黑树的结构进行插入。