JUC锁框架概述
Lock接口,Lock实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作
ReadWriteLock接口,维护一对相关的锁,一个用于只读,一个用于写入,当没有write时读取锁可以由多个reader线程同时保持,写入锁是独占锁
AbstractQueuedSynchonizer,AQS,其为实现依赖于先进先出等待队列的阻塞锁和相关同步器提供一个框架。
AbstractOwnableSynchonizer,可以由线程以独占方式拥有同步器,此类为创建锁和相关同步器提供基础。
工具类
CountDownLatch
是一个常用类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待(用减法实现,减法计数器)
它的调用者线程会被挂起,同时计数减一,当计数为0时某个线程才能开始执行
CyclicBarrier
回环栅栏,也是一个常用类,允许一组线程互相等待,直到达到某个公共屏障点,在涉及一组固定大小的线程的程序中这个就很有用。
需要注意的是CyclicBarrier与CountDownLatch不同的是他是到的公共屏障点才开始一起执行,所以在它释放了等待线程之后是可以进行重用的。
CyclicBarrier
中最重要的方法是await方法,它有两种实现。
public int await()
:挂起当前线程直到所有线程都为Barrier状态再同时执行后续的任务。public int await(long timeout, TimeUnit unit)
:设置一个超时时间,在超时时间过后,如果还有线程未达到Barrier
状态,则不再等待,让达到Barrier状态的线程继续执行后续的任务。
Semaphore
计数信号量,维护了一个许可集(实际上是对可用许可的号码进行计数),提供一个许可范围,只有获得了许可才能继续执行,可以选择公平锁和非公平锁
ReentrantReadWriteLock
一般情况下通常使用java提供的synchronized或ReentrantLock来独占式获取锁,但大部分业务是读数据,写数据很少,这种情况下还是用独占锁很容易性能低下,这时候ReentrantReadWriteLock就可以提高性能。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有读线程和其他的写线程都会被阻塞,支持公平性选择,可重入,可降级
锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
写锁的同步语义是通过重写AQS中的tryAcquire方法实现的
当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。
同步状态的高16位表示读锁被获取的次数,低16位表示写状态
读锁是一种共享锁,是通过重写AQS的tyrAcquireShared和tryReleaseShared方法实现的
需要注意的是当写锁被其他线程获取之后,读锁会获取失败
ReentrantLock
可重入锁,他是基于AQS实现的,AQS是一种提供了原子式管理同步状态,阻塞和唤醒线程功能以及队列模型的简单框架。
它支持公平锁和非公平锁,公平锁的加锁流程:
1.加锁
2.acquire(1)
3.tryacquire
非公平锁中的加锁流程(AQS):AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。
1、AQS:通过cas设置变量State成功(0->1)则获取锁成功,将当前线程设置成独占线程,如果cas设置失败则获取锁失败进入到Acquire方法进行后续处理
(共享锁是cas自减)(AQS中的,ReentrantLock没有,reentrantlock直接调用acquire)
2、Acquire中用tryAcquire尝试获取锁,参数为次数(tryAcquire由自定义同步器实现)
3、getState==0 是的话就判定cas是否修改共享资源state成功,是的话就获取锁成功,否的话就去判定锁线程是否是自己,是的话就重入,不是则获取锁失败,进入队列排队等候addwaiter
4、存在排队等候机制,addwaiter就是加入到等待队列,线程继续等待,仍然保留获取锁的可能
,在放入队列的线程会去不断获取锁,加入没获取锁且前驱结点是头节点就回去判定是否需要被阻塞(前驱节点waitStatus是否为为-1),防止无限循环,这时候就park
AQS中的队列是CLH变体的虚拟双向队列(双向链表)(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
cancelAcquire(取消,无效化)
获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED
(1) 当前节点是尾节点。
则直接前移然后next置为null
(2) 当前节点是Head的后继节点。
next指向自己
(3) 当前节点不是Head的后继节点,也不是尾节点。
next指向自己,并且原本的next节点变成pred节点的next
为什么不更改prev指针,是因为有可能导致指向一个已经移除队列的Node
解锁:
- 通过ReentrantLock的解锁方法Unlock进行解锁。
- Unlock会调用内部类Sync的Release方法,该方法继承于AQS。
- Release中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
- 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。
ReentrantLock和synchronized的区别
两者都是可重入锁
synchronized依赖于JVM,而ReentrantLock依赖于API
ReentrantLock可以等待可中断,能实现公平锁(默认是非公平锁),能实现选择性通知(指定条件唤醒线程)
ReentrantLock需要unlock
ReentrantLock依赖AQS实现,synchronized是监视器模式
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖AQS | 监视器模式 |
灵活性 | 支持响应中断,超时,尝试获取锁 | 不灵活 |
释放机制 | 必须显式调用unlock() | 自动释放 |
锁类型 | 公平锁&非公平锁(默认) | 非公平锁 |
条件队列 | 可多个 | 一个 |
可重入性 | 可重入 | 可重入 |
公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
独享锁和排他锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。(state值通常是0或1)
共享锁是指该锁可被多个线程所持有。(state就是持有锁数量)
乐观锁和悲观锁
代表 cas和sync
cas虽然很高效,但有三个问题,
1、ABA问题
2、cas操作不成功,一直自旋导致cpu开销很大
3、只能保证一个共享变量的原子操作
JUC常问
什么是 CAS 吗?
CAS(Compare And Swap)
指比较并交换。CAS
算法CAS(V, E, N)
包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值。在且仅在 V 值等于 E值时,才会将 V 值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 有什么缺点?
ABA
问题 :通过版本号来解决- 自旋问题
- 范围不能灵活控制
什么是 AQS 吗?
AbstractQueuedSynchronizer
抽象同步队列简称AQS
,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS
实现的。
了解 AQS 共享资源的方式吗?
- 独占式:只有一个线程能执行,具体的Java实现有
ReentrantLock
。 - 共享式:多个线程可同时执行,具体的Java实现有
Semaphore
和CountDownLatch
。
CyclicBarrier 和 CountdownLatch 有什么异同?
相同点:都能阻塞一个或一组线程,直到某个预设的条件达成发生,再统一出发。
但是它们也有很多不同点,具体如下。
- 作用对象不同:
CyclicBarrier
要等固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch
只需等待数字倒数到 0,也就是说CountDownLatch
作用于事件,但CyclicBarrier
作用于线程;CountDownLatch
是在调用了countDown
方法之后把数字倒数减 1,而CyclicBarrier
是在某线程开始等待后把计数减 1。 - 可重用性不同:
CountDownLatch
在倒数到 0 并且触发门闩打开后,就不能再次使用了,除非新建一个新的实例;而CyclicBarrier
可以重复使用。CyclicBarrier
还可以随时调用 reset 方法进行重置,如果重置时有线程已经调用了 await 方法并开始等待,那么这些线程则会抛出BrokenBarrierException
异常。 - 执行动作不同:
CyclicBarrier
有执行动作barrierAction
,而CountDownLatch
没这个功能。
synchronized 和 lock 有什么区别?
synchronized
可以给类,方法,代码块加锁,而lock
只能给代码块加锁。synchronized
不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock
需要手动自己加锁和释放锁,如果使用不当没有unLock
去释放锁,就会造成死锁。- 通过
lock
可以知道有没有成功获取锁,而synchronized
无法办到。
怎么防止死锁?
- 尽量使用
tryLock(long timeout,TimeUnit unit)
的方法(ReentrantLock 、ReenttranReadWriteLock
)设置超时时间,超时可以退出防止死锁。 - 尽量使用
java.util.concurrent
并发类代替手写锁。 - 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。