文章目录
一.前言
【Java多线程】JUC之CAS机制与原子类型(Atomic)
【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)
了解高并发必须知道的概念
了解Java并发包Concurrent发展简述
【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)
了解锁的分类
为什么要用锁?
- 锁可以解决并发执行任务执行过程中对
共享数据顺序访问、修改的场景
。比如对同时对一个账户进行扣款
或者转账
。
线程安全三大特性
【Java多线程】重温并发BUG的源头之可见性、原子性、有序性
【Java多线程】成神之路中必须要了解的锁分类的第一节第8小点
二.内置锁-synchronized
- 内置锁通过文章 【Java多线程】内置锁(Synchronized)的前世今生 了解即可
三.显示锁-Lock
锁机制用于保证操作的原子性、可见性、顺序性。 JDK1.5的concurrent并发包
中新增了 Lock接口以及相关实现类来实现锁功能
,最明显的特性就是需要显式的申请锁和释放锁。比synchronized更加灵活。
显示锁的释放锁的操作一定要放到finally块中,否则可能会因为异常导致锁永远无法释放!这是显式锁最明显的缺点。
1.Lock特性
1.1.显示加锁、解锁
- synchronized 关键字是自动进行加锁、解锁的,而Lock的具体实现需要
lock() 和 unlock()方法配合 try/finally 语句块来完成,来手动加锁、解锁。
1.1.可重入
像 synchronized
和 ReentrantLock
都是可 重入锁 ,可重入性表明 了锁的分配机制是 基于线程的分配
,而 不是基于方法调用
的分配。
- 可重入锁:又名
递归锁
,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在 Java 中线程获得对象锁的操作是以线程为单位的
,而不是以方法调用为单位的。但获取锁和释放锁必须要成对出现
。 - 如
ReentrantLock
,它是基于AQS(AbstractQueueSyncronized)
实现的,AQS 是基于 volitale 和 CAS 实现的
,其中 AQS 中维护一个valitale 类型的变量 state
来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的 。
1.2.可响应中断
- 当线程因为获取锁而进入
阻塞状态
,外部是可以中断该线程的
,调用方通过捕获nterruptedException可以捕获中断
- 如
ReentrantLock 中的
lockInterruptibly()
方法 可以使线程在被阻塞
时响应中断- 假设:
线程1
通过lockInterruptibly()
方法获取到一个可重入锁,并执行一个长时间的任务,另一个线程2
通过interrupt() 方法
就可以立刻打断 线程1的执行
,来获取线程1
持有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。
- 假设:
- 如
1.3.可设置等待超时时间
- synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态, 而 Lock的具体实现,可以
设置线程获取锁的等待超时时间
,通过方法返回值
判断是否成功获取锁,来避免死锁
1.4.锁的公平性
提供公平锁和非公平锁2种选择。
-
公平锁(
默认
) :线程将按照他们发出发出申请锁的顺序
来获取锁,先进先出,不允许插队
-
非公平锁:
允许插队
,当一个线程请求获取锁时,如果这个锁是可用
的,那这个线程将跳过所在队列里等待线程并获得锁。
- 如:
synchronized
关键字是一种非公平锁
,先抢到锁的线程先执行。而ReentrantLock
的构造方法中允许设置true/false 来实现公平、非公平锁
,如果设置为 true ,则线程获取锁要遵循"先来后到"
的规则,每 次都会构造一个线程 Node
,然后到双向链表的"尾巴"
后面排队,等待前面的 Node 释放锁资源。 - 考虑这么一种情况:A线程持有锁,B线程请求这个锁,因此B线程被挂起;A线程释放这个锁时,B线程将被唤醒,因此再次尝试获取锁; 与此同时,C线程也请求获取这个锁,那么C线程很可能在B线程被完全唤醒之前 获得、使用以及释放 这个锁。这是种双赢的局面,B获取锁的时刻(B被唤醒后才能获取锁)并没有推迟,C更早地获取了锁,并且吞吐量也获得了提高。
在大多数情况下,非公平锁的性能要高于公平锁的性能。
- 如:
-
另外,这个公平性是针对
线程
而言的,不能依赖此来实现业务上的公平性,应该由开发者自己控制,比如通过FIFO队列
来保证公平。
1.5.读写锁
- 允许读锁和写锁分离,读锁与写锁互斥,但是多个读锁可以共存, 即一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行 。适用于
读远大于写
的场景- 即:读读共享、写写互斥、读写互斥
关于读写锁的一些知识:
1.重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。
2.写锁可以降级为读锁,顺序是: 先获得写锁再获得读锁,然后释放写锁
,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不行
3. 读锁被线程持有时排斥任何的写锁,而线程持有写锁则是完全的互斥.这一特性最为重要,
,对于 读多写少
的场景使用此类才可以提高并发量。
4.不管是读锁还是写锁都支持 响应中断
5.写锁支持Condition且用于与ReentrantLock一样, 而读锁则不能使用Condition
,否则抛出UnsupportedOperationException异常。
1.6.丰富的API
提供了多个方法来获取锁相关的信息,可以帮助开发者监控和排查问题
- isFair() :判断锁是否是公平锁
- isLocked() :判断锁是否被任何线程获取了
- isHeldByCurrentThread() :判断锁是否被当前线程获取了
- hasQueuedThreads() :判断是否有线程在等待该锁
- getHoldCount() :查询当前线程占有lock锁的次数
- getQueueLength() :获取正在等待此锁的线程数
1.7.常用方法
- 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()一样
2.锁的使用
2.1.ReentrantLock
独占锁
的具体实现,拥有上面列举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.ReentrantReadWriteLock
ReentrantReadWriteLock是 读写锁
的具体实现,拥有上面列举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; /** * 移动坐标 * @param deltaX * @param deltaY */ public void move(double deltaX, double deltaY) { //获取写锁 long stamp = stampedLock.writeLock(); try { x += deltaX; y += deltaY; } finally { //释放写锁 stampedLock.unlockWrite(stamp); } } /** * 计算距离原点平台根 * @return */ 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; // 进队元素索引,出队元素索引 ,元素总量 /** * 添加元素 * * @param x * @throws InterruptedException */ 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(); } } /** * 获取元素 * * @return * @throws InterruptedException */ 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()操作 :会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前会将节点移到同步队列中。唤醒后的节点尝试竞争锁
(自旋)
。
2.5.BlockingQueue
BlockingQueue阻塞队列实际上是一个 生产者/消费者模型
,当队列长度大于指定的最大值,生产线程就会被阻塞;反之当队列元素为空时,消费线程就会被阻塞;同时当消费成功时,就会唤醒阻塞的生产者线程;生产成功就会唤醒消费者线程;
- 实现原理: 内部使用就是
ReentrantLock + Condition
来实现的可以参照上面的BoundedBuffer
示例。
2.6.CountDownLatch
俗称: (同步计数器/闭锁)
,可以使 一个线程等待其他线程全部执行完毕后再执行。
类似join()的效果。
声明对象时需要初始化需要等待线程数
,调用countDown()
方法等待线程数减1,当数值减为0时 ,就会唤醒所有因为调用await()
方法而阻塞的线程。- 可以达到一组线程等待另外一组线程都执行完成的效果。
2.7.CyclicBarrier
俗称: 同步屏障
,可以使 一组线程互相等待,“直到所有线程到达某个公共的执行点后在继续执行”
。
声明该对象时需要初始化等待线程数
,调用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
实现分布式锁
。实现分布式场景下的并发控制。