本文章主要介绍四个工具类:ReentranLock,CountDonwLatch,Semaphore,CyclicBarrier
文章的前置知识:AQS底层原理
详情可以参考我的往期文章:通过可重入锁ReentranLock弄懂AQS-CSDN博客
ReentranLock-可重入锁
ReentranLock是什么
ReentranLock是什么
是一个可重入的独占锁
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类
ReentranLock的底层是由AQS来实现的
ReenrtranLock默认是什么锁
ReenrtranLock默认使用非公平锁
说一下ReentranLock公平锁和非公平锁的区别在哪
公平锁
如果使用公平锁(即通过 ReentrantLock(boolean fair)
构造函数并传入 true
创建的 ReentrantLock
实例),等待队列里获取锁是按照先后顺序的
原理:
公平锁在尝试获取锁时,会先检查等待队列中是否有其他线程在等待
如果有,当前线程会乖乖地进入等待队列尾部进行等待,直到前面的线程依次释放锁并轮到自己,才会去尝试获取锁
非公平锁
如果使用非公平锁(使用无参构造函数创建的 ReentrantLock
实例,或者通过 ReentrantLock(false)
创建),等待队列里获取锁不是严格按照先后顺序的。
原理:
非公平锁在获取锁时,不管等待队列中是否有其他线程在排队等待,新到来的线程都会先尝试直接获取锁
如果此时锁刚好被释放,新线程就有可能 “插队” 成功,直接获取到锁,而不是让等待队列中的第一个线程获取锁。只有当直接获取锁失败时,线程才会进入等待队列
区别
公平锁:队列里面有节点等待就乖乖进入队列
非公平锁:即使队列里面有节点等待,我们也可以进行插队和队列里的节点竞争锁
说一下ReentranLock的底层
底层是基于我们的AQS抽象队列同步器来实现的
我们默认是非公平锁
ReentranLock中的state一开始被初始化为0,如果大于0说明目前资源正在被占用
我们的多线程竞争锁的时候,是争夺我们的state资源变量
这个state变量被volatile修饰符修饰了
然后我们竞争成功的就拿到锁,失败的就按照争夺锁的时间顺序放到我们的CLH队列里面
然后在里面自旋前一个变量的状态
synchronized和ReentranLock有什么区别
共同点
都是可重入锁
例如我们这个类的实例对象
我们调用内部被synchronzied对象修饰的方法
我们就会获取当前实例对象的锁
synchronized依赖于JVM而ReentranLock依赖于API
synchronized依赖于JVM虚拟机实现,并且在虚拟机层面进行优化
ReentranLock在JDK层面实现,需要调用lock()unlock()方法来配合
ReentranLock比synchronized增加了一些高级功能
- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来指定是否是公平的。
- 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
可中断锁和不可中断锁有什么区别
- 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。 - 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁
Semaphore-信号量
Semaphore有什么用
什么是Semaphore
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源
而
Semaphore
(信号量)可以用来控制同时访问特定资源的线程数量
两种模式:
公平模式
非公平模式
semaphore对应的两种构造方法如下:
public Semaphore(int permits) {sync = new NonfairSync(permits);
}public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore
通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)
Semaphore调用流程
state数量一开始初始化为permits
Semaphore
的构造函数使用 permits
参数初始化 AQS 的 state
变量,该变量表示可用的许可数量
当线程调用 acquire()
方法尝试获取许可时,state
会原子性地减 1
如果 state
减 1 后大于等于 0,则 acquire()
成功返回,线程可以继续执行
如果 state
减 1 后小于 0,表示当前并发访问的线程数量已达到 permits
的限制,该线程会被放入 AQS 的等待队列并阻塞,而不是自旋等待
当其他线程完成任务并调用 release()
方法时,state
会原子性地加 1
release()
操作会唤醒 AQS 等待队列中的一个或多个阻塞线程。这些被唤醒的线程将再次尝试 acquire()
操作,竞争获取可用的许可。因此,Semaphore
通过控制许可数量来限制并发访问的线程数量,而不是通过自旋和共享锁机制
Semaphore的原理是什么
是共享锁的一种实现
我们使用CAS去操作
调用semaphore.release();
,线程尝试释放许可证,并使用 CAS 操作去修改 state
的值 state=state+1
释放许可证成功之后,同时会唤醒同步队列中的一个线程
被唤醒的线程会重新尝试去修改 state
的值 state=state-1
,如果 state>=0
则获取令牌成功,否则重新进入阻塞队列,挂起线程
CountDownLatch-倒计时器
CountDownLatch有什么用
CountDownLatch
允许 count
个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch
是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch
使用完毕后,它不能再次被使用
CountDownLatch的原理是什么
共享锁的一种实现
我们的state一开始会被初始化成某个数字
使用countDown()方法:以CAS的操作来减少state
调用await()方法:如果state不为0,说明任务还没有执行完毕,await()方法会一直阻塞。也就是说await()方法最后的语句不会被执行,直到state的值被减为0,或者await()线程被中断,该线程才会从阻塞中被唤醒
当state为0时,await()方法之后的语句才会执行
CountDonwLatch的两种用法
CountDownLatch 的两种典型用法:
- 某一线程在开始运行前等待 n 个线程执行完毕 : 将
CountDownLatch
的计数器初始化为 n (new CountDownLatch(n)
),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()
),当计数器的值变为 0 时,在CountDownLatch 上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - 实现多个线程开始执行任务的最大并行性:注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的
CountDownLatch
对象,将其计数器初始化为 1 (new CountDownLatch(1)
),多个线程在开始执行任务前首先coundownlatch.await()
,当主线程调用countDown()
时,计数器变为 0,多个线程同时被唤醒。
用过CountDownLatch吗?什么场景下用的?
场景:
我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理
CyclicBarrier-循环栅栏
CyclicBarrier有什么用
让一组线程到达一个屏障时阻塞,直到最后一个线程到达屏障时屏障才会开门
开门后所有被屏障拦截的线程才会继续干活
CyclicBarrier的原理是什么
CyclicBarrier
内部通过一个 count
变量作为计数器,count
的初始值为 parties
属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1
如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们的方法