前言
宏观了解AQS
AQS是AbstractQueuedSynchronizer
类(抽象的队列同步器)的缩写
- 实现了读锁和写锁的分离
- 与synchronized关键字底层实现相似度较高(80%以上)
- 剩下20%是扩展的功能,如Condition和可重入锁(ReentrantLock)、ReentrantReadWriteLock。。
- JUC中的组件大部分都源自AQS
AQS大致是做什么的?
- 提供一个框架,用于实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件等)。 此类旨在成为大多数依赖单个atomic int值来表示state(状态)的同步器的根基(基础)。 子类必须定义会更改此state(状态)的受保护的方法,并定义该state(状态)在获取或释放此对象方面的含义。 鉴于这些,此类中的其他方法执行所有等待和阻塞机制。 子类可以维护其他state(状态)字段,但只有使用方法getState 、 setState和compareAndSetState操作的原子更新的int值才会在同步方面进行跟踪。
- 里面有先进先出 (FIFO) 等待队列
- 实现同步机制
- 用atomic int值来表示state(状态)
- 子类需要定义会改变state 的protected 方法
- 不同子类的state(状态)含义可能不同
ReentrantLock是如何使用AQS的
前言
关于公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入阻塞队列, 如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
AQS在ReentrantLock中的角色
AQS是ReentryLock成员变量Sync
可重入锁获取锁的源码分析
- 先看ReentrantLock的构造方法
- Sync有两个(版本)分支,公平和非公平
先看公平版Sync,线程是如何获取锁的
- 关于ReentryLock中的status的含义
public class AQSTest {
private final Lock lock = new ReentrantLock();
private void method(){//执行method时,会重入5次锁,也就是status会等于5
try{
lock.lock();
Thread.sleep(2);
method2();//每个method中都有lock
method3();//也就是一个线程可以重入多个锁
method4();
method5();
System.out.println("method is over by "+Thread.currentThread().getName());
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
private void method2() {
try {
lock.lock();
Thread.sleep(2);
System.out.println("method2 is over by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
private void method3() {
try {
lock.lock();
Thread.sleep(2);
System.out.println("method3 is over by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
private void method4() {
try {
lock.lock();
Thread.sleep(2);
System.out.println("method4 is over by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
private void method5() {
try {
lock.lock();
Thread.sleep(2);
System.out.println("method4 is over by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
public static void main(String[] args) {
AQSTest aqsTest = new AQSTest();
Runnable runnable = aqsTest::method;
IntStream.range(0,10).forEach(i -> new Thread(runnable).start());
}
}
复制代码
有几个lock就有几个unlock,否则会发生死锁,千万别想上面那样写。只是为了方便说明state的含义,才出此下策。
再看非公平版Sync,线程是如何获取锁的
- 其实和公平版Sync区别不大
可重入锁获释放锁的分析
- 既然获得锁是state+1,那么释放锁就是state-1。
小结
对于ReentrantLock来说,其执行逻辑如下所示:
- 尝试获取对象的锁,如果获取不到(意味着已经有其他线程持有了锁,并且尚未释放),那么它就会进入到AQS的阻塞队列当中。
- 如果获取到,那么根据锁是公平锁还是非公平锁来进行不同的处理:
- 如果是公平锁,那么线程会直接放置到AQS阻塞队列的末尾。
- 如果是非公平锁,那么线程会首先尝试进行CAS操作,如果成功,则直接获取到锁;如果失败,则与公平锁的处理方式一致,被放到阻塞队列末尾。
- 当锁被释放时(调用了unlock方法),那么底层会调用release方法对state成员变量值进行减一操作,如果减一后,state值不为0,那么release操作就执行完毕;如果减一操作后,state值为0,则调用LockSupport的unpark方法唤醒该线程后的等待队列中的第一个后继线程(pthread _mutex_unlock),将其唤醒,使之能够获取到对象的锁(release时,对于公平锁与非公平锁的处理逻辑是一致的);之所以调用release方法后state值可能不为零,原因在于ReentrantLock是可重入锁,表示线程可以多次调用lock方法,导致每调用一次, state值都会加一。
- 对于ReentrantLock来说,所谓的上锁,本质上就是对AQS中的state成员变量的操作∶对该成员变量+1,表示上锁;对该成员变量-1,表示释放锁。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END