多线程进阶-JUC

191 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天

集合类不安全

List不安全

ArrayList 在并发情况下是不安全的!

会报 java.util.ConcurrentModificationException 并发修改异常

解决办法:

  1. List list = new Vector<>();
  2. Listlist = Collections.synchronizedList(new ArrayList<>());
  3. List list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题

CopyOnWriteArrayListVector厉害在哪里?

Vector底层是使用synchronized关键字来实现的:效率特别低下。

image-20221008095558826

CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!

image-20221008095610442

set不安全

image-20221008101403837

和List、Set同级的还有一个BlockingQueue 阻塞队列;

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;

解决方案还是两种:

使用Collections工具类的synchronized包装的Set类 使用CopyOnWriteArraySet 写入复制的JUC解决方案

 // java.util.ConcurrentModificationException  并发修改异常
 public class SetTest {
     public static void main(String[] args) {
       //  Set<String> set = Collections.synchronizedSet(new HashSet<>());//解决方案1
         Set<String> set = new CopyOnWriteArraySet<>();//解决方案2
 ​
         for (int i = 1; i < 10; i++) {
             new Thread(()->{
                 set.add(UUID.randomUUID().toString().substring(0,5));
                 System.out.println(set);
             },String.valueOf(i)).start();
 ​
         }
     }
 }
 ​

hasSet的底层是什么?

image-20221008101648042

image-20221008101833610

add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储

hashSet就是使用了hashmap key不能重复的原理

 //PRESENT是什么? 是一个常量  不会改变的常量  无用的占位
 private static final Object PRESENT = new Object();

Map不安全

image-20221008103546218

 // java.util.ConcurrentModificationException  并发修改异常
 ​
 public class MapTest {
     public static void main(String[] args) {
         //map 是这样用的吗?不是,工作中不用HshMap
         //默认等价于:  new HashMap<>(16,0.75);
         //Map<String , String> map = Collections.synchronizedMap(new HashMap<>());
         ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
 ​
         for (int i = 1; i < 30; i++) {
             new Thread(()->{
                 map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                 System.out.println(map);
             },String.valueOf(i)).start();
         }
     }
 ​
 ​
 }

ConcurrentHashMap底层实现原理

  1. ConcurrentHashMap的整体架构

image-20221008104957613

如图,这是ConcurrentHashMap在jdk1.8中的存储结构,它是由数据,单项链表,红黑树来构成,当我们去初始化一个ConcurrentHashMap实例的时候,默认会初始化一个长度等于16的数组,由于ConcurrentHashMap它的核心仍然是Hash表,所以必然会存在Hash冲突的问题,所以ConcurrentHashMap采用链式寻址的方式,来解决Hash表的冲突,当Hash冲突比较多的时候,会造成链表长度较长的问题,这种会使得ConcurrentHashMap中的一个数组元素的查询复杂度会增加,所以在jdk1.8里面,引入了红黑树的机制,当数组长度大于64并且链表长度大于等于8的时候,单向链表会转化成红黑树,另外随着ConcurrentHashMap的一个动态扩容,一旦链表的长度小于8,红黑树会退化成单向链表

  1. ConcurrentHashMap的基本功能

image-20221008105038359

ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap是一样的,但是ConcurrentHashMap在HashMap的基础上提供了并发安全的一个实现。并发安全的主要实现主要通过对于Node节点去加锁,来保证数据更新的安全性

  1. ConcurrentHashMap在性能方面的优化

如何在并发性能和数据安全性之间去做好平衡,在很多地方都有类似的设计,比如说cpu的三级缓存,mysql的buffer_poor,Synchronized的一个锁升级等等,ConcurrentHashMap也做了类似一个优化,主要体现在几个方面

  • 在jdk1.8里面ConcurrentHashMap锁的粒度,是数组中的某一个节点,而在jdk1.7里面。它锁定的是Segment,锁的范围要更大,所以性能上它会更低。
  • 引入红黑树这样一个机制,去降低了数据查询的时间复杂度,红黑树的时间复杂度实是O(logn)

image-20221008105203333

如图,当数组长度不够的时候,ConcurrentHashMap它需要对数组进行扩容,而在扩容时间上,ConcurrentHashMap引入了多线程并发扩容的一个实现,简单来说多个线程对原始数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而去整体的提升了扩容过程中的数据迁移的一个效率

  • ConcurrentHashMap它有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下去实现元素个数的累加,性能是非常低的,所以ConcurrentHashMap这个方面做了两个优化

image-20221008105336263

如图,当线程竞争不激烈的时候,直接采用CAS的方式,来实现元素个数的一个递增

如果线程竞争化比较激烈的情况下,使用一个数组来维护元素个数,如果要增加总的元素个数的时候,直接从数组中随机选择一个,在通过CAS算法来实现原子递增,它的核心思想是引入数组来实现并发更新的一个负载