并发编程最佳学习路线
【Java基础】多线程从入门到掌握
【Java多线程】线程通信
【Java多线程】JUC之CAS机制与原子类型(Atomic)
【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)
了解高并发必须知道的概念【Java多线程】高并发修炼基础之高并发必须了解的概念
了解Java并发包Concurrent发展简述【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)
了解锁的分类【Java多线程】成神之路中必须要了解的锁分类
为什么要用锁?
- 锁可以解决并发执行任务执行过程中对 共享数据顺序访问、修改的场景 。比如对同时对一个账户进行 扣款 或者 转账 。
【Java多线程】重温并发BUG的源头之可见性、原子性、有序性
【Java多线程】成神之路中必须要了解的锁分类的第一节第8小点
二.内置锁-synchronized- 内置锁通过文章 【Java多线程】内置锁(Synchronized)的前世今生 了解即可
锁机制用于保证操作的原子性、可见性、顺序性。 JDK1.5的concurrent并发包 中新增了 Lock接口以及相关实现类来实现锁功能 ,最明显的特性就是需要显式的申请锁和释放锁。比synchronized更加灵活。
显示锁的释放锁的操作一定要放到finally块中,否则可能会因为异常导致锁永远无法释放!这是显式锁最明显的缺点。
1.Lock特性 1.1.显示加锁、解锁- synchronized 关键字是自动进行加锁、解锁的,而Lock的具体实现需要 lock() 和 unlock()方法配合 try/finally 语句块来完成,来手动加锁、解锁。
像 synchronized 和 ReentrantLock 都是可 重入锁 ,可重入性表明 了锁的分配机制是 基于线程的分配 ,而 不是基于方法调用 的分配。
- 可重入锁:又名 递归锁 ,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在 Java 中线程获得对象锁的操作是 以线程为单位的 ,而不是以方法调用为单位的。但 获取锁和释放锁必须要成对出现 。
- 如 ReentrantLock ,它是基于 AQS(AbstractQueueSyncronized) 实现的, AQS 是基于 volitale 和 CAS 实现的 ,其中 AQS 中维护一个 valitale 类型的变量 state 来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的 。
- 当线程因为获取锁而进入 阻塞状态 , 外部是可以中断该线程的 , 调用方通过捕获nterruptedException可以捕获中断
- 如 ReentrantLock 中的 lockInterruptibly() 方法 可以使线程在被 阻塞 时响应中断
- 假设: 线程1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间的任务,另一个 线程2 通过 interrupt() 方法 就可以立刻 打断 线程1的执行 ,来获取 线程1 持有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。
- 如 ReentrantLock 中的 lockInterruptibly() 方法 可以使线程在被 阻塞 时响应中断
- synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态, 而 Lock的具体实现,可以 设置线程获取锁的等待超时时间 ,通过 方法返回值 判断是否成功获取锁,来 避免死锁
提供公平锁和非公平锁2种选择。
-
公平锁( 默认 ) :线程将按照他们发出 发出申请锁的顺序 来获取锁,先进先出, 不允许插队
-
非公平锁: 允许插队 ,当一个线程请求获取锁时,如果这个锁是 可用 的,那这个线程将 跳过所在队列里等待线程并获得锁。
- 如: synchronized 关键字是一 种非公平锁 ,先抢到锁的线程先执行。而 ReentrantLock的构造方法中允许设置 true/false 来实现公平、非公平锁 ,如果设置为 true ,则线程获取锁要遵循 "先来后到" 的规则,每 次都会构造一个 线程 Node ,然后到双向链表的 "尾巴"后面排队,等待前面的 Node 释放锁资源。
- 考虑这么一种情况:A线程持有锁,B线程请求这个锁,因此B线程被挂起;A线程释放这个锁时,B线程将被唤醒,因此再次尝试获取锁; 与此同时,C线程也请求获取这个锁,那么C线程很可能在B线程被完全唤醒之前 获得、使用以及释放 这个锁。这是种双赢的局面,B获取锁的时刻(B被唤醒后才能获取锁)并没有推迟,C更早地获取了锁,并且吞吐量也获得了提高。 在大多数情况下,非公平锁的性能要高于公平锁的性能。
-
另外,这个公平性是针对 线程 而言的,不能依赖此来实现业务上的公平性,应该由开发者自己控制,比如通过 FIFO队列 来保证公平。
- 允许读锁和写锁分离,读锁与写锁互斥,但是多个读锁可以共存, 即一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行 。适用于 读远大于写 的场景
- 即:读读共享、写写互斥、读写互斥
关于读写锁的一些知识:
1.重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。
2.写锁可以降级为读锁,顺序是: 先获得写锁再获得读锁,然后释放写锁 ,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不行
3. 读锁被线程持有时排斥任何的写锁,而线程持有写锁则是完全的互斥.这一特性最为重要, ,对于 读多写少 的场景使用此类才可以提高并发量。
4.不管是读锁还是写锁都支持 响应中断
5.写锁支持Condition且用于与ReentrantLock一样, 而读锁则不能使用Condition ,否则抛出UnsupportedOperationException异常。
1.6.丰富的API提供了多个方法来获取锁相关的信息,可以帮助开发者监控和排查问题
- isFair() :判断锁是否是公平锁
- isLocked() :判断锁是否被任何线程获取了
- isHeldByCurrentThread() :判断锁是否被当前线程获取了
- hasQueuedThreads() :判断是否有线程在等待该锁
- getHoldCount() :查询当前线程占有lock锁的次数
- getQueueLength() :获取正在等待此锁的线程数
- void lock() :在线程获取锁时如果锁已被其他线程获取,则进行 等待
Lock lock = new ReentrantLock();//获取锁
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
lock.unlock(); //释放锁
}
- boolean tryLock() : 尝试获取锁 ,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false
- 我们可以根据是否能获取到锁来决定后续程序的行为。该方法会立即返回,在拿不到锁时也不会一直等待,通常我们用 if 语句 判断 tryLock() 的返回结果 , 根据是否获取到锁来执行不同的业务逻辑 ,使用方法如下。
Lock lock = new ReentrantLock();//获取锁
//如果能获取到锁
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
}
//如果不能获取锁,则做其他事情
else {
}
利用 tryLock() 方法我们还可以解决死锁问题
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
//自旋
while (true) {
//如果能获取锁1
if (lock1.tryLock()) {
try {
//如果能获取锁2
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
//释放锁2
lock2.unlock();
}
}
} finally {
//释放锁1
lock1.unlock();
}
}
//如果不能能获取锁,线程休眠若干秒
else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
-
boolean tryLock(long time, TimeUnit unit): 可响应中断并且有超时时间的尝试获取锁 ,在拿不到锁时会等待一定的时间,如果在时间结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。
这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时 ,在等待了一段指定的超时时间后, 线程会主动放弃获取这把锁 ,避免永久等待。 等待获取锁的期间,也可以 随时中断线程 ,这就避免了死锁的发生。
-
void lockInterruptibly(): 可响应中断的去获取锁 ,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程占用),那么当前线程便会开始 等待 ,除非它 等到了这把锁或者是在等待的过程中被中断了 ,否则这个线程便会一直在这里执行这行代码。一句话总结就是, 除非当前线程在获取锁期间被 中断 ,否则便会 一直尝试获取 直到获取到为止。
顾名思义, lockInterruptibly() 是可以 响应中断 的。相比于不能响应中断的 synchronized 锁 , lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时, 保持对中断的响应 。我们可以把这个方法理解为 超时时间是无穷长的 tryLock(long time, TimeUnit unit) ,因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断 ,只不过 lockInterruptibly() 永远不会超时。
public void lockInterruptibly() {
try {
lock.lockInterruptibly();
try {
System.out.println("操作资源");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
- void unlock() :释放当前线程占用的锁, 必须使用finally块保证发生异常时锁一定被释放
- Condition newCondition() :创建绑定到此 Lock 实例的 Condition实例 ,用于替代wait()/notify()/notifyAll()方法, 实现线程的等待通知机制
Condition对象的await()/signal()/signalAll() 的功能和wait()/notify()/notifyAll()一样
独占锁 的具体实现,拥有上面列举Lock 除读写锁之外的所有特性 ,使用比较简单
原理:通过 AQS 的方式来实现的, 通过 Unsafe 包提供的 CAS 操作来进行 锁状态(state)的竞争。然后通过 LockSupport.park(this) 进行 park 住线程,在 unpack() 唤醒在 AQS 队列头的对象让他去竞争锁。
.
在并发包中, 使用 ReetrantLock 的地方有:
- CyclicBarrier
- DelayQueue
- linkedBlockingDeque
- ThreadPoolExecutor
- ReentrantReadWriteLock
- StampedLock
public class ReentrantLockTest {
// 声明独占锁实例,构造函数传入true为公平锁,false为非公平锁(默认)
private final ReentrantLock lock = new ReentrantLock();
public void test() {
//申请获取锁
lock.lock();
try {
//方法主体业务逻辑
} finally {
// 必须要释放锁,unlock与lock成对出现
lock.unlock();
}
}
}
ReetrantLock 加锁和解锁的过程入下图所示:
2.2.ReentrantReadWriteLockReentrantReadWriteLock是 读写锁 的具体实现,拥有上面列举Lock的所有特性。拥有2把锁,一把是 WriteLock (写锁,独占锁) ,一把是 ReadLock(读锁,共享锁) 。 读锁 可以允许 多个不同线程重入 ,但对于 写锁 ,同时只能有 一个线程重入 , 把读和写操作分离开来了,粒度更细,所以在性能上有所提高. 并且 写锁可降级为读锁,反之不行 。
- 原理 :
ReentrantReadWriteLock也是基于 AQS 实现的
- 读写锁中的加锁、释放锁也是基于 Sync (继承于 AQS ) ,并且主要使用 AQS 中的 state 和node 中的 waitState 变量进行实现的。
- 实现读写锁与实现普通互斥锁的主要区别在于 : 需要分别记录读锁状态及写锁状态,并且等待队列中需要区别处理两种加锁操作。
- ReentrantReadWriteLock 中将 AQS 中的 int 类型的 state 分为 高 16 位 与 低16 位 分别记录读锁和写锁的状态,如下图所示:
- ReadLock(读锁)是 共享锁(乐观锁)
- WriteLock(写锁)是 悲观锁(排他锁、互斥锁)
- 特点:
- 读读共享( 最大特性 )、读写互斥、写写互斥。
- 当一个线程拥有写锁时,不释放写锁的情况下,再占有读锁,此时写锁会被 降级 为读锁.
- 在公平模式下 , 无论读锁还是写锁的申请都需要按照 先进先出 的原则, 非公平模式下 , 写锁无条件插队.
- 场景:常用于 读多写少 的场景,即请求读操作的线程 多而频繁 而请求写操作的线程极 少且间隔长 , !!!但在读写都相当频繁的场景并不能体现出性能优势
- 缺点: 基于 读读共享(不互斥) 的特性极有可能造成 写线程饥饿 。比如, R1线程 此时持有读锁且在进行读取操作, W1线程 请求写锁所以需要排队等候,在 R1 释放锁之前,如果 R2,R3,...,Rn 不断的到来请求读锁,因为读读共享,所以他们不用等待马上可以获得锁,如此下去 W1永远无法获得写锁,一直处于饥饿状态 。
public class CachedData {
//声明读写锁实例
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Object data;
private volatile boolean cachevalid;
void processCachedData() {
//申请获取读锁
rwl.readLock().lock();
if (!cachevalid) {
//在获取写锁之前必须释放读锁
rwl.readLock().unlock();
//申请获取写锁
rwl.writeLock().lock();
try {
//重新检查状态,因为另一个线程可能已经获得写锁并在我们之前更改了状态。
if (!cachevalid) {
data= "数据";
cachevalid = true;
}
//降级通过在释放写锁之前获取读锁
rwl.readLock().lock();
} finally {
//释放写锁,仍然保持读锁
rwl.writeLock().unlock();
}
}
try {
//具体处理缓存数据
use(data);
} finally {
//释放度锁
rwl.readLock().unlock();
}
}
}
2.3.StampedLock
StampedLock是JDK1.8新增的一种 读写锁 ,是对 ReentrantReadWriteLock 的增强版,提供 2种读模式 : 乐观读和悲观读 。
- 乐观读 允许读的过程中也可以获取写锁后写入! 这样一来,我们读的数据就可能不一致, 因此需要一点额外的代码来判断读的过程中是否有写入。
- 乐观锁的意思就是 乐观地认为读的过程中大概率不会有写入 ,因此被称为乐观锁。反过来,悲观锁则是 读的过程中拒绝有写入操作 ,也就是 写入必须等待 。显然乐观锁的并发效率更高,但 一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行 。
public class Point {
//声明锁实例
private final StampedLock stampedLock = new StampedLock();
//横坐标
private double x;
//纵坐标
private double y;
public void move(double deltaX, double deltaY) {
//获取写锁
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
//释放写锁
stampedLock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
// 获得一个乐观读锁
long stamp = stampedLock.tryOptimisticRead();
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
// 检查乐观读锁后是否有其他写锁发生
if (!stampedLock.validate(stamp)) {
// 获取一个悲观读锁
stamp = stampedLock.readLock();
try {
currentX = x;
currentY = y;
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
2.4.Condition
Condition是在 JDK1.5加入concurrent包 的,用来替代传统的 Object的wait()、notify() 实现线程间的通信,相比使用Object的wait()、notify(),使用Condition的 await()、signal() 方法实现线程间通信更加安全和高效。 通过Lock+Condition组合使用可以实现 等待通知机制 , 阻塞队列BlockQueue 实际上是使用了 Condition 来进实现线程通信的。
- await() 必须在获取锁之后的调用 ,表示释放当前锁,阻塞当前线程;等待其他线程调用锁的 signal()或signalAll() 唤醒线程重新去获取锁。
- Lock配合Condition ,可以实现 synchronized 与 对象的 wait(),notify()、notifyAll() 同样的效果 ,来进行线程间基于共享变量的通信。 但优势在于同一个锁可以由多个条件队列,当某个条件满足时,只需要唤醒对应的条件队列即可,避免无效的竞争。
- 调用await()和signal()方法,都必须在lock保护之内,就是说 必须在lock.lock()和lock.unlock之间才可以使用
使用 ReentrantLock+Condition 实现一个简单的阻塞队列
public class BoundedBuffer {
//声明可重入锁
final Lock lock = new ReentrantLock();
//如果队列满时用于挂起put线程/唤醒take线程的Condition
final Condition notFull = lock.newCondition();
//如果队列为空时用于挂起take线程/唤醒put线程的Condition
final Condition notEmpty = lock.newCondition();
//存储元素的容器
final Object[] items = new Object[100];
int putptr, takeptr, count; // 进队元素索引,出队元素索引 ,元素总量
public void put(Object x) throws InterruptedException {
//获取锁
lock.lock();
try {
//元素总量等于容器大小(容器满了)
while (count == items.length) {
//挂起put元素线程
notFull.await();
}
//---------容器未满--------
//当前位置putptr插入元素
items[putptr] = x;
//如果当前取的元素索引 等于 容器大小,从对首开始存
if (++putptr == items.length) {
putptr = 0;
}
//元素总量自增
++count;
//唤醒take元素线程
notEmpty.signal();
} finally {
//释放锁
lock.unlock();
}
}
public Object take() throws InterruptedException {
//获取锁
lock.lock();
try {
//元素总量等于0(为空满了)
while (count == 0) {
//挂起take元素线程
notEmpty.await();
}
//---------容器不为空--------
//获取队首元素
Object x = items[takeptr];
//如果当前取的元素索引 等于 容器大小,从对首开始取
if (++takeptr == items.length) {
takeptr = 0;
}
//元素总量自减
--count;
//唤醒put元素线程
notFull.signal();
return x;
} finally {
//释放锁
lock.unlock();
}
}
}
测试
public static void main(String[] args) {
BoundedBuffer buffer = new BoundedBuffer();
new Thread(() -> {
try {
while (true) {
int item = new Random().nextInt(10000);
buffer.put(item);
log.info("生产者:{}生产了一个元素:{}", Thread.currentThread().getName(),item);
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "producer").start();
new Thread(() -> {
try {
while (true) {
Object item = buffer.take();
log.info("消费者:{}消费了一个元素:{}", Thread.currentThread().getName(),item);
TimeUnit.MILLISECONDS.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "consumer1").start();
new Thread(() -> {
try {
while (true) {
Object item = buffer.take();
log.info("消费者:{}消费了一个元素:{}", Thread.currentThread().getName(),item);
TimeUnit.MILLISECONDS.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "consumer2").start();
}
拓展·
- Condition 实现
- 每个 Condition 对象包含一个等待队列。等待队列中的节点复用了同步器中同步队列中的节点。 Condition 对象的 await()和 signal()操作就是对等待队列以及同步队列的操作。
- await()操作 :将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,最后进入等待状态。
- signal()操作 :会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前会将节点移到同步队列中。唤醒后的节点尝试竞争锁 (自旋) 。
BlockingQueue阻塞队列实际上是一个 生产者/消费者模型 ,当队列长度大于指定的最大值,生产线程就会被阻塞;反之当队列元素为空时,消费线程就会被阻塞;同时当消费成功时,就会唤醒阻塞的生产者线程;生产成功就会唤醒消费者线程;
- 实现原理: 内部使用就是 ReentrantLock + Condition 来实现的可以参照上面的 BoundedBuffer 示例。
俗称: (同步计数器/闭锁) ,可以使 一个线程等待其他线程全部执行完毕后再执行。 类似join()的效果。
- 声明对象时需要初始化需要等待线程数 ,调用 countDown() 方法等待线程数减1,当数值减为0时 ,就会唤醒所有因为调用 await() 方法而阻塞的线程。
- 可以达到一组线程等待另外一组线程都执行完成的效果。
俗称: 同步屏障 ,可以使 一组线程互相等待,“直到所有线程到达某个公共的执行点后在继续执行”。
- 声明该对象时需要初始化等待线程数 ,调用 await() 方法会使得线程阻塞,直到 指定数量的线程都调用await方法时,所有被阻塞的线程会被唤醒,继续执行 。
与CountDownLatch的区别是:CountDownLatch是 一组线程等待另外一组线程执行完在执行,而CyclicBarrier是 一组线程之间相互等待,直到所有线程执行到某个点在执行 。
2.8.Semaphore称之为 信号量 ,与 互斥锁ReentrantLock用法类似 ,区别就是Semaphore 共享的资源是多个,允许多个线程同时竞争成功。
2.9.初识AQS原理AQS 是 AbstractQueuedSynchronizer 的缩写,中文为 抽象队列同步器 ,是用来 构建各类锁和同步器的基础实现 。内部维护了 共享变量state (int类型) 和 双向队列 (包含头指针和尾指针)
基于AQS构建的同步器
- ReentrantLock
- ReentrantReadWriteLock
- Semaphore
- CountDownLatch
- SynchronusQueue
- FutureTask
AQS模型如下图:
1.AQS并发问题解决方案
原子性
- 内部通过 Unsafe.compareAndSwapXXX 实现 CAS 更新 state 和 队列指针 (prev/next/head/tail)
- 内部依赖CPU提供的原子指令
可见性与有序性
- volatile 修饰 state 与 队列指针 (prev/next/head/tail)
线程阻塞与唤醒
- Unsafe.park
- Unsafe.parkNanos
- Unsafe.unpark
Unsafe类是在 sun.misc包 下,不属于Java标准。提供了 内存管理、对象实例化、数组操作、CAS操作、线程挂起与恢复等功能 ,Unsafe类提升了Java运行效率,增强了Java语言底层的操作能力。很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等
AQS内部有2种模式: 独占模式(独占锁)和共享模式(共享锁)
- AQS 的设计是基于 模板方法 的,使用者需要 继承 AQS 并重写指定的方法 。不同的自定义队列同步器竞争共享资源的方式不同,比如 可重入、公平性等都是子类来实现。
- 自定义队列同步器在实现时 只需要实现共享资源state的获取与释放方式即可 ,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),由AQS 内部 实现。
2.独占模式
一个线程 唤醒后继节点
AQS提供的独占模式相关的方法
// 获取独占锁(线程阻塞直至获取成功) public final void acquire(int arg) // 获取独占锁,可被中断 public final void acquireInterruptibly(int arg) // 获取独占锁,可被中断 和 指定超时时间 public final boolean tryAcquireNanos(int arg, long nanosTimeout) // 释放独占锁(释放锁后,将等待队列中第一个等待节点唤醒 ) public final boolean release(int arg)
AQS子类需要实现的独占模式相关的方法
// 尝试获取独占锁,成功则返回true,失败则返回false。 protected boolean tryAcquire(int arg) // 尝试释放独占锁,成功则返回true,失败则返回false。 protected boolean tryRelease(int arg)
获取独占锁的流程
-
调用子类 tryAcquire 尝试获取锁,获取成功,直接返回
-
通过自旋CAS将当前线程封装成节点加入 队列末尾
-
循环等待或尝试 tryAcquire 获取锁
- 判断前置节点如果为 head ,则尝试获取锁
- 根据队列中节点状态,决定是否需要阻塞当前线程
- tryAcquire 获取锁成功后,将当前节点设置为 head 并 返回
-
如果当前线程中断或超时,则执行 cancelAcquire
CANCELED Head
释放独占锁的流程
3.共享模式
- 多个线程 都能够获取到锁
- 锁释放后需要 唤醒后继节点
- 锁获取后如果 还有资源 ,需要 唤醒后继共享节点
AQS提供的共享模式相关的方法
// 获取共享锁(线程阻塞直至获取成功) public final void acquireShared(int arg) // 获取共享锁,可被中断 public final acquireSharedInterruptibly(int arg) // 获取共享锁,可被中断 和 指定超时时间 public final tryAcquireSharedNanos(int arg, long nanosTimeout) // 获取共享锁 public final boolean releaseShared(int arg)
AQS子类需要实现的共享模式相关的方法
// 尝试获取共享锁。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 protected int tryAcquireShared(int arg) // 尝试释放共享锁,如果释放后允许唤醒后续等待节点返回true,否则返回false。 protected boolean tryReleaseShared(int arg)
获取共享锁的流程
-
调用子类 tryAcquireShared 尝试获取锁,获取成功,直接返回
-
通过自旋CAS将当前线程封装成节点加入 队列末尾
-
循环等待或尝试 tryAcquireShared 获取锁
- 判断前置节点如果为 head ,则尝试获取锁
- 根据队列中节点状态,决定是否需要阻塞当前线程
- tryAcquireShared 获取锁成功后,将当前节点设置为 head
- 如果资源有 剩余 或者原先的 head 节点状态为 SIGNAL / PROPAGATE ,则调用 doReleaseShared
- 如果当前head节点状态为 SIGNAL ,唤醒后继节点
- 如果当前head节点状态为 ZERO ,将 head 节点状态置为 PROPAGATE
-
如果当前线程中断或超时,则执行 cancelAcquire
CANCELED Head
释放共享锁的流程
等待队列中节点的状态变化
4.ReentrantLock示例
tryAcquire逻辑
tryRelease逻辑
四.总结- 对于 单机环境 我们在 JVM内进行并发控制我们可以使用 synchronized (内置锁) 和 RentrantLock 。
- 对于 自增 或者 原子数据累加 我们可以使用 Unsafe 提供的原子类,比如 AtomicInteger , AtomicLong
- 对于数据库的话,对于用户金额扣除的场景我们可以使用 乐观锁(版本号) 的方式来进行控制
update table_name set amount = 100, version = version + 1 where id = 1 and version = 1;
- 对于 分布环境景 下可以使用 Redis 或者 Zk 实现 分布式锁 。实现分布式场景下的并发控制。



