Java线程安全的实现-3个同步器

Java线程安全的实现-3个同步器

2020-04-15    11'47''

主播: 橙汁儿橙汁儿。

167 3

介绍:
1.实现线程安全的方式: (1)锁:synchronized、LockSupport阻塞锁、ReentrantLock、乐观锁 CAS (2)线程本地变量:ThreadLocal、ThreadLocalRandom 工具类 (3)原子操作类:AtomicLong、LongAdder (4)同步器:信号量 Semphore、CountDownLatch、回环屏障CyclicBarrier 2.LockSupport: 为使用它的线程关联一个许可证,默认情况下线程是不持有许可证的。(源码注释里写了:许可证最多有一个。)调用 park()方法时,如果线程未持有许可证,就会被阻塞挂起。 调用 unpark() 方法时,会使线程持有许可证,所以就会唤醒之前因为调用 park() 方法被阻塞的线程。 3.AQS 抽象同步队列: 是 JUC 包中类的基础。是个抽象类,AQS 是双向链表队列,线程同步的实现是通过对 volatile 修饰的 int 类型的 state 状态值。根据操作 state 的是一个线程还是多个线程,分为 独占 execlusive 和 共享 shared。 独占是说资源会和具体线程关联,只有一个线程会成功获取资源,其他失败的线程会被封装成 EXECLUSIVE 类型的节点尾插到AQS队列,并用 LockSupport.park() 将线程阻塞挂起;共享是说资源不会和具体线程关联,只要符合条件的线程就可以 CAS 获取资源, 不符合条件的线程会被封装成 SHARED 类型的 node 尾插到队列。 获取资源 acquire() 会调用 tryAcquire(),实际上是通过 CAS 修改 state 值,。 释放资源 release()会调用 tryRealse(), 实际上也是通过 CAS 修改 state 的值,然后调用 LockSupport.unpark()激活被阻塞的线程。 tryAcquire() 和 tryRelease() 都是由子类覆写的,这是模板设计模式。 AQS 中有个内部类 ConditionObject,每个 ConditionObject 对象有一个 条件队列,这是单向队列。比如说 Lock 接口的实现类 ReentrantLock 对象 .newConditon() 实际上就是创建了一个ConditionObject,ConditionObject+lock 就类似于 Object + synchronized 监视器锁。调用 await() 方法的前提是先要处于同步块中,也就是调用过 lock() 获取锁,之后会把线程封装成 CONDITION 节点尾插到 条件队列中,用 LockSupport.park() 方法阻塞挂起,并释放锁;而调用 signal() 方法的前提也是要先获取到锁,然后会条件队列的出队,也就是 队头的 等待时间最长的 线程,出队后会入队到 AQS 的阻塞队列中(因为 会被放到条件队列的肯定是调用了 await() 方法的,是释放了锁的。) 再来说说两个问题: (1)为什么 AQS 要设计成双向队列? 我的理解是:在 release 释放资源的时候,需要用 LockSupport.unpark() 唤醒一个线程,线程是有状态 waitstates 的,SIGAL 表示当前节点被释放后,它的后继节点需要被唤醒, 所以一个线程是否被唤醒得看它的 prev 前节点是否是 SIGLAL 状态 ,所以需要 prev。 (2)在释放 doRelease()方法中,要  从头 head 开始遍历,如果 是 SIGNAL,会用 CAS 将状态值修改为 0 ,成功的话就会进入 unparkSuccessor()方法,而 unparkSuccessor() 方法中,如果后继节点是 CANCELL 1:因为超时或中断被放到队列中/线程被取消了,会从 tail 向前遍历。     这是因为: node.prev = pred 早早执行的,而可能还未执行.next 绑定后继节点,如果出现唤醒操作,从头部开始遍历就可能因为后继节点还未绑定,无法通过 .next 获取到下一个节点信息,也就找不到真正需要 unpark 的节点),而从尾部开始扫描则不会导致该问题。 4.ReentrantLock 可重入独占锁,state 值表示 获取到锁的次数。如果有公平性和非公平性两种实现,分别对应 FairSync 和 nonFairSync,都是继承了 AQS,默认是非公平性的。 非公平性:获取锁的顺序与请求锁的顺序无关,比如线程 A 先使用 lock() 方法,state 值不为0,资源的持有者线程也不是 A ,那么 A 就会被阻塞挂起,放到 AQS 队列中;接下来线程 B 使用 lock() 时,state 值为0 ,它就可以获取到资源,这样的话,A 先请求资源,却不是先获取到资源的。 公平性:线程尝试获取资源时,会调用 hasQueuePredecesser,只有当 AQS 队列为 null ,或者 队列头 head 为当前线程时,才可以获取资源,这样的话,获取资源是串行化的,保证请求顺序与获取顺序一致。 5.线程本地变量 除了之前的 ThreadLocal ,还有一个工具类 ThreadLocalRandom,它是继承了 Random类的,求随机数的思路:根据 seed ,求新的 seed,再求随机数,Random 类保证线程安全是使用 CAS 自旋,只会有一个线程成功获取到 seed,其他线程之后只能用新的seed作为自己的seed,再去产生新的 seed 和 随机数,而 CAS 自旋会带来资源的消耗。 Thread 类有个属性 threadlocalRandomSeed,随机数种子,在使用 ThreadLocalRandom 时才会被创建,这样多线程产生随机数就是用各自的 随机数种子变量,而求随机数时与系统当前时间 System.currenttime 有关,保证随机性。 6.AtomicLong 可以指定初值,有递增、递减、CompareAndSet 方法,维护了一个 volatile 修饰的 long 类型的变量,通过 CAS 更新变量,直到 CAS 成功为止。 7.JDK 1.8新增的 LongAdder 不能指定初值,从 0 开始,add() 方法可传入正数、负数,decrement 实现递减。 内部维护了 volatile 修饰的 base 基值 和 一个 cells 数组(也是 volatile 修饰的),如果无线程冲突时,就对 base 进行 CAS 更新操作,一旦出现冲突,就会 初始化 cells 数组(懒加载机制),初始长度是 2,每次访问 数组的下标,是由 threadlocalRandomProbe 探针 与 数组长度 -1 相与 求得的,如果多个线程求得的下标相同,就会进行 2 倍扩容,重新计算下标;所以数组长度是2 的幂次方,可以避免伪共享,而且小于 CPU 个数,出现冲突时 不会自旋,而是一个线程对应一个 cell 。求和 sum() 时是将 base 和 cells 数组所有的值累加。 要注意一点,求和时数组不加锁,如果这时有修改或扩容,会导致结果不准确。(面试就不要说这点了,被问到再说。) 8.信号量 Semphore 计时器是递增的,比如有两个子线程,在构造方法中传入 0 ,主线程中 使用 acquire(2)会阻塞,子线程每次调用 release()方法会使计时器 +1, 计时器加到 2 主方法继续向下执行。 9.CountDownLatch 计时器是递减的,使用它比 挨个join() 优雅。比如有两个子线程,主线程中 使用 await(2) 表示需要同步的线程数 为 2,那么子线程每次 countDown() 会使计时器 -1,计时器到 0 时,会调用 doReleasedShared() 唤醒因为 await() 而被阻塞的线程,也就是 主线程。 10.回环屏障 CyclicBarrier 回环是指 计数器可以重置,重用。 线程调用 await() 方法就会阻塞,而 所有的线程都调用 await() 之后,线程们会冲破屏障,向下进行。 ------------------------------------------------------------------------------ 三个同步器中,回环屏障 CyclicBarrier 中是有条件队列的。       回环屏障 CyclicBarrier 的 await() 方法,内部调用了 doWait() 方法,当一个线程 调用了 dowait() 方法后,首先会获取 独占锁 Lock,比方说 创建 CyclicBarrier 时传递的参数是 10 ,那么后面 9 个调用线程会被阻塞。然后当前获取到 锁 的 线程 会对计数器 count 递减,递减后 count=index=9,这时 index != 0,如果没有设置超时时间,当前线程会被放到 条件变量 trip 的条件阻塞队列,当前线程会被挂起并释放获取的 Lock 锁;如果设置了超时时间,当前线程也会被放入条件变量的条件队列 并 释放锁,不同的是当前线程会在指定事件超时后被自动激活。 第一个线程释放锁之后,被阻塞的 9 个线程中会有一个竞争到 锁,执行和第一个线程一样的操作,直到最后一个线程获取到锁,这时已经有 9 个线程 在 trip 条件队列中,最后 count=0,就会执行构造方法中传入的任务,再唤醒其他 9 个线程,并重置 CyclicBarrier 。 ------------------------------------------------------------------------------