AbstractQueuedSynchronizer 源码分析、学习笔记、设计思想学习;仅供参考
并发前言
在程序中如果有多个线程执行对同一资源进行操作,我们讲发生了并发行为。而我们常说的并发操作指的是写,因为并发读往往考虑的是服务器资源,而写操作会影响到程序是否正常运行。处理并发写,处理的方法无非这几种思想:
几个 demo
描述:让两个线程完成对一个数累加
demo1:先看一个有问题的 demo:
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t1.join();
t2.join();
// 这里num输出结果小于 2 * 1000000
System.out.println("num: " + num);
}
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
num++;
}
}
}
demo2:修改后的 demo:
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t1.join();
t2.start();
t2.join();
// 输出正确结果 2 * 1000000
System.out.println("num: " + num);
}
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
num++;
}
}
}
demo3:使用 ReenTrantLock 的 demo:
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t1.join();
t2.join();
// 输出正确结果 2 * 1000000
System.out.println("num: " + num);
}
static class MyThread extends Thread {
@Override
public void run() {
try {
// 加锁
lock.lock();
for (int i = 0; i < 1000000; i++) {
num++;
}
} finally {
// 释放锁
lock.unlock();
}
}
}
demo 分析
num++ 这个操作,在底层指令操作其实是分成了好几步执行的。
demo1 有问题是因为 t1 对值修改了,但是 t2 对该值进行了重复操作。
demo2 能达到正确的效果,正是我们一开始提到的排队思想,我们使用了 join()
方法,手动让线程排队执行了。
demo3 也能实现正确的效果,是因为我们用了互斥思想,进行加锁,同一时刻只有一个线程在执行。
AQS 设计思想
AbstractQueuedSynchronizer 简称 AQS 抽象队列同步器,从名称看出来它是一个抽象类、具有队列功能、是可以同步的。JUC 下大多数并发工具包是基于它来实现的,它定义了基本的操作接口,可以让使用者自定义实现功能。上面的 ReenTrantLock 正是基于 AQS 实现的。
AQS 的源码点进去看有很多,有点复杂,好在注释比代码多。先抓它的设计思想、核心要点,再看细节。
- 有一个 FIFO 的队列;
- 有一个 int state 变量来表示当前锁的状态;
- 有一个 CHL 队列(CHL 三个人名首字母缩写),该队列非常重要,后面展开
- AQS 类中的 Node 里的属性大多用 volatile 修饰,保证了可见性
- 获取锁,获取不到就放到队列
ReentrantLock 源码 Debug
AQS 源码要点
先看 AQS 几个重要点:
Node 节点,存在队列中,它有以下属性:
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
/** waitStatus 值的状态 表示该线程已取消 */
static final int CANCELLED = 1;
/** waitStatus 值的状态 表示该线程需要释放锁(唤醒) */
static final int SIGNAL = -1;
/** waitStatus 值的状态 表示该线程需要条件等待 */
static final int CONDITION = -2;
// 不知道干嘛的
static final int PROPAGATE = -3;
volatile int waitStatus;
// 队列的前继节点
volatile Node prev;
// 队列的前继节点
volatile Node next;
// 当前线程
volatile Thread thread;
Node nextWaiter;
state 属性 ```java // 0表示空闲,1表示被使用了,大于1表示重入了,重入一次加1 private volatile int state;
protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } ```
ReentrantLock#lock();获取锁操作
首先会用 CAS 操作,对 state 进行赋值,赋值成功说明当前是空闲的,将自己设置为持有锁的线程。
如果没有赋值成功,会尝试获取锁
再次获取下,万一有人释放了呢? 如果不等于 0 ,判断当前执行的线程是不是自己,加 1 操作,ReenTrantLock 可重入的功能。
如果 tryAcquire() 返回了 false,说明没有获取到锁,那么执行 addWaiter(Node.EXCLUSIVE) 加入到队列中
如果队列已经有节点了,那么将自己放到队尾,只是个队列的入队操作
入队成功后,死循环去获取,不断重试,也就是自旋操作。
这里是超级重点!!!
挂起当前线程