并发编程笔记——并发容器、 同步⼯具类、原子操作类

并发容器

  1. BlockingQueue
    1.1 ArrayBlockingQueue
    数字组实现的环形队列,核⼼为1个ReentrantLock锁外加两个条件Condition notEmpty和Condition notFull
    1.2 LinkedBlockingQueue
    是⼀种基于单向链表的阻塞队列,因为队头和队尾是2个指针分开操作的,所以⽤了2把锁+2个条件,同时有1个AtomicInteger的原⼦变量记录count数。
    1.3 PriorityBlockingQueue
    按照元素的优先级从⼩到⼤出队列的,实现Comparable接⼝,核心是堆排序
    1.4 DelayQueue
    按延迟时间从⼩到⼤出队的PriorityQueue,实现Delayed接⼝,Delayed接口继承了 Comparable 接⼝
    1.5 SynchronousQueue
    先调put(…),线程会阻塞;直到另外⼀个线程调⽤了take(),两个线程才同时解锁,反之亦然。两种实现:TransferQueue(遵循配对的原则),TransferStack(遵循配对的原则)

  2. BlockingDeque
    ⼀个阻塞的双端队列接⼝,该接⼝只有⼀个实现,就是
    LinkedBlockingDeque,核⼼是⼀个双向链表

  3. CopyOnWrite
    CopyOnWrite指在“写”的时候,不是直接“写”源数据,⽽是把数据拷⻉⼀份进⾏修改,再通过悲观锁或者乐观锁的
    ⽅式写回。为了在“读”的时候不加锁。
    3.1 CopyOnWriteArrayList
    核⼼数据结构是⼀个数组
    3.2 CopyOnWriteArraySet
    内部是封装的⼀个 CopyOnWriteArrayList,并保证所有元素都不重复

  4. ConcurrentLinkedQueue/Deque
    AQS内部的阻塞队列实现原理:基于双向链表,通过对head/tail进⾏CAS操作,实现⼊队和出队。

  5. ConcurrentHashMap
    HashMap通常的实现⽅式是“数组+链表”,这种⽅式被称为“拉链法”。ConcurrentHashMap在这个基本原理之上进⾏了各种优化。引⼊了红⿊树。
    如果头节点是Node类型,则尾随它的就是⼀个普通的链表;如果头节点是TreeNode类型,它的后⾯就是⼀颗红⿊树,TreeNode是Node的⼦类。
    链表和红⿊树之间可以相互转换:初始的时候是链表,当链表中的元素超过某个阈值时,把链表转换成红⿊树;反之,当红⿊树中的元素个数⼩于某个阈值时,再转换为链表。

为什么要做这种设计?
5.1. 使⽤红⿊树,当⼀个槽⾥有很多元素时,其查询和更新速度会⽐链表快很多,Hash冲突的问题由此得到较好 的解决。
5.2. 加锁的粒度,并⾮整个ConcurrentHashMap,⽽是对每个头节点分别加锁,即并发度,就是Node数组的⻓ 度,初始⻓度为16。
5.3. 并发扩容,这是难度最⼤的。当⼀个线程要扩容Node数组的时候,其他线程还要读写

  1. ConcurrentSkipListMap/Set
    6.1 ConcurrentSkipListMap
    是 key 有序的,实现了NavigableMap接⼝,此接⼝⼜继承了SortedMap接⼝。核心数据结构是跳查表。
    6.2 ConcurrentSkipListSet
    ConcurrentSkipListSet只是对ConcurrentSkipListMap的简单封装

同步⼯具类

  1. Semaphore
    Semaphore也就是信号量,提供了资源数量的并发访问控制。类似于⼤学⽣到⾃习室抢座,资源有限,先到先得,后到排队。
 new Semaphore(X); 初始化资源
 semaphore.acquire(); 占领资源
 semaphore.release(); 释放资源
  1. CountDownLatch
    假设⼀个主线程要等待5个 Worker 线程执⾏完才能退出,可以使⽤CountDownLatch来实现
 new CountDownLatch(X); 初始化
 latch.await();当前程序等待
 latch.countDown(); 每个Worker线程执行完打报告

await()调⽤的是AQS 的模板⽅法,只要state != 0,调⽤await()⽅法的线程便会被放⼊AQS的阻塞队列,进⼊阻塞状态。
总结:由于是基于AQS阻塞队列来实现的,所以可以让多个线程都阻塞在state=0条件上,通过countDown()⼀直减 state,减到0后⼀次性唤醒所有线程。如下图所示,假设初始总数为M,N个线程await(),M个线程countDown(),减到0之后,N个线程被唤醒。

  1. CyclicBarrier
    Barrier——栅栏。类似于开会,等人到齐了才能开始。
new CyclicBarrier(X);  初始化
barrier.await();  等待(人到齐自动唤醒)

CyclicBarrier基于ReentrantLock+Condition实现

  1. CyclicBarrier是可以被重⽤的。一个线程可以多次调用await(),等第一批到齐,完成后,又进入下一轮次的等待。每⼀轮被称为⼀个Generation,就是⼀次同步点。
  2. CyclicBarrier 会响应中断。线程没有到⻬,如果有线程收到了中断信号,所有阻塞的线程也会被唤醒(breakBarrier()⽅法)。
  1. Exchanger
    Exchanger⽤于线程之间交换数据
new Exchanger<>(); 初始化
exchanger.exchange(X); 线程A调用change方法,获得Y
exchanger.exchange(Y); 线程B调用change方法,获得X

核⼼机制和Lock⼀样,也是CAS+park/unpark。

  1. Phaser
    功能⽐CyclicBarrier和CountDownLatch更加强⼤
    ⽤Phaser替代CountDownLatch
new Phaser(X); 初始化
phaser.awaitAdvance(0); 主线程等待
phaser.arrive(); 每个子线程完成打报告

⽤Phaser替代CyclicBarrier

new Phaser(X); 初始化
phaser.arriveAndAwaitAdvance();  到达同步点,等待其他线程

Phaser新特性
特性1:动态调整线程个数
CyclicBarrier 所要同步的线程个数是在构造⽅法中指定的,之后不能更改,⽽ Phaser 可以在运⾏期间动态地调
整要同步的线程个数。Phaser 提供了下⾯这些⽅法来增加、减少所要同步的线程个数。

register() // 注册⼀个
bulkRegister(int parties) // 注册多个
arriveAndDeregister() // 解除注册

特性2:层次Phaser
多个Phaser可以组成树状结构,可以通过在构造⽅法中传⼊⽗Phaser来实现。
作用场景:类似于任务分解,一个大任务分解成多个小任务,等每个小任务完成了才能执行大任务

原理:CAS操作和Treiber Stack(不再是AQS了)

原子操作类(Atomic类)

  1. AtomicInteger和AtomicLong
    核心:使用的Unsafe类的方法,该方法基于CAS实现
  2. AtomicBoolean和AtomicReference
    核心:AtomicBoolean调用的AtomicInteger类的方法,传入参数变成0/1,
    AtomicReference和AtomicInteger类似,只是泛型从基础类型包装类变成了Object类。
  3. AtomicStampedReference和AtomicMarkableReference
    使用场景:解决AtomicReference所解决不了的ABA问题
    核心:
    AtomicMarkableReference内部加了个boolean的标识,通过标识辨别值是否有被更改(一定程度解决ABA问题)
    AtomicStampedReference内部加了个int版本号,通过版本号辨别值是否有被更改(完全解决ABA问题)
  4. AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和
    AtomicReferenceFieldUpdater

    为什么需要AtomicXXXFieldUpdater?
    如果⼀个类是⾃⼰编写的,则可以在编写的时候把成员变量定义为Atomic类型。但如果是⼀个已经有的类,在不能更改其源代码的情况下,要想实现对其成员变量的原⼦操作,就需要AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。
    案例:
		AtomicIntegerFieldUpdater<A> ageField = AtomicIntegerFieldUpdater.newUpdater(A.class, "age");
        A a = new A();
        a.setAge(0);
        int nextAge = ageField.getAndIncrement(a);
        System.out.println(nextAge); //1
限制条件:类成员变量不能是私有的、必须是volatile修饰的
解决办法:使用代理类
  1. AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray
    Concurrent包提供了AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray三个数组元素的原⼦操作。注意,这⾥并不是说对整个数组的操作是原⼦的,⽽是针对数组中⼀个元素的原⼦操作⽽⾔。
    核心:CAS⽅法直接调⽤VarHandle中native的getAndAdd⽅法
  2. Striped64与LongAdder
    从JDK 8开始,针对Long型的原⼦操作,Java⼜提供了LongAdder、LongAccumulator;针对Double类型,Java提供了DoubleAdder、DoubleAccumulator。他们都继承了Striped64。
    核心:AtomicLong内部是⼀个volatile long型变量,这个变量被拆分成了多份(Cell),由多个线程对这个变量进⾏CAS操作。写操作使用负载均衡策略,一个线程只修改其中某一份;读操作只需将全部的值相加即可。其中为了解决“伪共享问题”,每个Cell使用了@jdk.internal.vm.annotation.Contended修饰来达到“缓存填充”的目的,通俗说来就是让每个Cell在CPU缓存里都是单独的一行。
  3. LongAccumulator
    LongAccumulator的原理和LongAdder类似,只是功能更强⼤,可以⾃⼰定义⼀个⼆元操作符,并且可
    以传⼊⼀个初始值。
  4. DoubleAdder与DoubleAccumulator
    DoubleAdder 其实也是⽤ long 型实现的,因为没有 double 类型的 CAS ⽅法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值