J.U.C.并发工具包的使用
1. AQS原理
1.1 概述
全称是 AbstractQueuedSynchronizer,抽象同步队列,简称 AQS ,是Java并发包的根基,J.U.C.并发包中的锁(lock)就是基于AQS实现的。是阻塞式锁和相关的同步器工具的框架(其他工具都是它的子类)。AQS是一个接口,这个接口定义了一系列规则,而是否要设定公平锁与否由实现它的类来决定。
特点:
-
用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源 -
提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList。
-
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet。
通过子类继承AQS父类,子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
tryAcquire
tryRelease
tryAcquireShared
tryReleaseShared
isHeldExclusively
1.2 实现不可重入锁
【AQSTest3.java】用AQS自己实现一个独占锁
// 我的自定义锁 (不可重入锁)
class myLock implements Lock{
// 【独占锁】的自定义 同步器类 (做给自定义锁调用)
// 锁要实现的大部分功能都是通过同步器类完成的,这边的自定义继承了AQS
class MySync extends AbstractQueuedSynchronizer {
@Override // 尝试获得锁
protected boolean tryAcquire(int arg) {
// 用cas,是因为你尝试加锁时,可能别的线程也在尝试加锁,保证原子性
if(compareAndSetState(0, 1)) {
// 加上了锁(0 -> 1),并且设置Owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true; }
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 注意这两个方法顺序。 因为setState里面是volatile的
setExclusiveOwnerThread(null);
setState(0);
return true; }
@Override // 是不是【持有】独占锁
protected boolean isHeldExclusively() {
// state 为1时表示持有独占锁
return getState() == 1;
}
public Condition newCondition(){
// ConditionObject是上面继承的那个的内部类,可以直接使用
return new ConditionObject();
}
}
// 下面通过MySync实现下面锁的方法
private MySync sync = new MySync();
@Override // 加锁 (如果不成功会放到队列中等待)
public void lock() {
sync.acquire(1);
}
@Override // 加锁,但可打断 (以前syn不能打断,容易死锁)
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override // 尝试加锁 (【只试一次】,失败返回false)
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override // 尝试加锁,带超时时间
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
// 上面写的tryRelease不会去唤醒等待队列中阻塞的线程。
// 这边看源码,除了两个操作,还会去唤醒【正在阻塞的线程】
sync.release(1);
}
@Override // 创建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
2. ReentrantLock 原理
P238
2. 1 非公平锁实现原理
2. 1. 1 加锁解锁流程
Ctrl + F12 File structure
Ctrl + Alt + B 查找实现
加锁流程总结:
- 默认非公平锁,加锁时用compareAndSetState去改变状态变成1,修改成功,Owner线程就会改成当前线程。
- 第二个线程来,出现竞争,CAS失败进去else的acquire(1),去做tryAcquire,相当于再试一次,还是不行就构造Node双向队列(懒惰)。这个等待队列再第一次创建节点时,会创两个,第一个是哨兵节点,不关联线程,第二个会让当前线程进入acquireQueue。
- 这个逻辑是这样,它会在一个死循环里不断尝试获得锁(最后一试),失败的话进入park阻塞。如果这个时候自己紧邻着head节点(排第二),还能再试一次,去tryAcquire。如果这个时候再失败,进入shouldParkAfterFailedAcquire,就会把前驱节点的waitStatus状态改成-1(初始0),-1意味着有责任唤醒它的后继节点(也就是当前线程不能获得锁了,得去阻塞,但得给一个节点将来唤醒它),这次返回false。再把acquireQueue的流程再走一遍,就会返回true。
- true了就会进入parkAndCheckInterrupt,就会让当前线程阻塞住。
解锁流程总结:
- 如果无竞争,从unlock调用同步器的release方法,进入tryRelease,成功就设置state为0,Owner为null。
- 判断当前队列不为null,且head的waitStatus为-1,就要去唤醒后继节点,去unpark恢复运行,回到这个线程(T1)的acquireQueue过程的parkAndCheckInterrupt(之前阻塞着),这时被唤醒,又进入循环(在for(;😉),就tryAcquire,改变state和Owner。
- 把原来T1的节点清空设置为头结点,原来头结点断开连接(可被GC)。
- 因为非公平锁,如果有竞争,新线程不在Node队列,也想获得锁,就会和刚才T1竞争,如果新线程赢了,新线程被设置为Owner,state为1。T1再次进入acquireQueue过程,获取锁失败,重新park阻塞。
2. 2 可重入原理
如果state是0,就CAS改成1,获得锁。如果等于1,再判断当前线程是不是Owner线程,是就是发生锁重入,就让state加1,相当于锁进入2次。
释放锁的时候,就state-1,当state为1时free为false,不解锁。如果1的时候再想-1,free才会变成true,把Owner改成null。
2. 3 可打断原理
2. 3. 1 不可打断模式(默认)
在此模式下,即使它被打断,被唤醒了,但他仍然在循环里,去尝试获得锁,失败了再进入阻塞队列(一直在AQS队列里),一直要等到获得锁后方能得知自己被其他线程打断了。
2. 3. 2 可打断模式
不一样的是从parkAndCheckInterrupt被唤醒后,会抛出异常。不进入for(;😉,不在AQS队列里再等了,就可以停止去等待锁。
2. 4 公平锁实现原理
2. 4. 1 非公平:
![[uTools_1689491940081.png]]
2. 4. 2 公平:
CAS前多做一个事,检查AQS队列中是否有前驱节点,没有才去竞争。
2. 5. 条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。
类似于synchronized的wait-notify。
2. 5. 1 await流程
![[uTools_1689071066344.png]]
![[uTools_1689071149967.png]]
总结
- 如果T1线程持有锁,调用await,就进入ConditionObject的addConditionWaiter流程。
- 就是把线程加到条件变量的双向链表里,把新的Node状态设为-2(等待状态)。
- 下面进入fullyRelease流程,释放同步器(节点)的所有锁,锁重入的话全去掉。
- unpark AQS队列的下一个等待的节点,去竞争锁。
- 成功了,就会park阻塞T1。
2. 5. 2 signal 流程
![[uTools_1689071173998.png]]
![[uTools_1689071189243.png]]
总结(T1唤醒T0)
1.先检查是否是锁的持有者,是才能唤醒别的线程。
2. 进入ConditionObject的doSignal流程,去取条件变量等待队列的第一个Node,把它从条件变量的链表中断开,加到竞争锁的AQS队列的尾部。
3. 把原来的最后一个的WaitStatus改成-1,加进去的T0对应的改成0。
3. 读写锁
3. 1 ReentrantReadWriteLock使用
当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能(一个优化,读不修改数据)。 类似于数据库中的 select …from … lock in share mode(类似于共享锁)
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法。
3. 1. 1 注意事项
- 读锁不支持条件变量。
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待。
- 重入时降级支持:即持有写锁的情况下去获取读锁。
![[uTools_1689071805862.png]]
3. 2 StampedLock
为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用。
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
Semaphore:信号量,用来限制能同时访问共享资源的线程上限。
3. 2. 1 基本使用
ReentrantLock是独占的,而信号量应用是,现在有多个共享资源,也允许多个线程使用,只是想限制能同时访问共享资源的线程上限。(停车 管理车位上限)
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 3. 获取许可, 此信号量
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
3. 2. 2 应用
使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可。
它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)。
适用于一个线程对应一个资源。 比如一个线程是对应一个数据库连接的(new Semaphore(poolSize)
,让许可数与资源数一致)。
![[uTools_1689072687060.png]]
Semaphore优化:
![[uTools_1689072756406.png]]
用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的。
3. 2. 3 原理 △△
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一。
- 刚开始,permits(state)为 3,这时 5 个线程来获取资源。
![[uTools_1689072927014.png]] - 假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞。
![[uTools_1689072964509 1.png]] - 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态。
4. CountdownLatch
倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时。
4. 1 基本使用
其中构造参数用来初始化等待计数值,await() (一般是main线程)用来等待计数归零,countDown() 用来让计数减一。
![[uTools_1689073210074 1.png]]
主线程要等三个线程里把计数减到0,才会await被唤醒,去运行。
改进:
上面调用join(),也可以实现,但join比较底层,使用繁琐,这边属于高级API。以后不自己创建线程,而是用线程池来创建并获取线程达到线程的重用。此时,会发现里面的线程一直在运行,并不会轻易结束。
CyclicBarrier
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置计数个数,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足计数个数时,继续执行。 起到一个同步的作用。
CyclicBarrier 与 CountDownLatch 的主要区别在于==CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』 ==。