AQS原理
什么是AQS?
AbstractQueuedSynchronizer,JDK提供的一个抽象类;
这些全是 AbstractQueuedSynchronizer 的子类; AbstractQueuedSynchronizer 是 JDK 中用来构建同步并发的基础组件;
AQS 中有一个比较重要的同步变量:private volatile int state // 同步状态;
不管是 JDK 还是我们自己,在实现一个同步类的时候,都要围绕着这个 state 来做文章,修改它的值来表示当前的同步状态发生了变化;
为什么我们在使用各种同步类的时候而没有感受到 AQS 的存在呢?因为 AQS 在使用方式上采用的是继承的方式,而且是在同步工具类的内部定义了一个静态内部类来继承 AQS,这个同步工具类把内部类暴露的方法进行了一层封装,使我们感受不到 AQS 的存在;
所以说 AQS 采用的是 模板方法 的设计模式来实现;
模板方法设计模式
在阎宏博士的《JAVA与模式》一书中开头是这样描述模板方法(Template Method)模式的:
模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。
代码示例:
模板抽象类
csharp
public abstract class AbsCar {
public abstract void makeLight(); // 造车灯
public abstract void makeDoor(); // 造车门
public abstract void makeGlass(); // 造车玻璃
public void make() {
makeLight();
makeDoor();
makeGlass();
}
}
抽象实现子类1
csharp
public class AudiCar extends AbsCar {
@Override
public void makeLight() {
System.out.println("make audi light");
}
@Override
public void makeDoor() {
System.out.println("make audi door");
}
@Override
public void makeGlass() {
System.out.println("make audi glass");
}
}
抽象实现子类2
csharp
public class BMWCar extends AbsCar {
@Override
public void makeLight() {
System.out.println("make bmw light");
}
@Override
public void makeDoor() {
System.out.println("make bmw door");
}
@Override
public void makeGlass() {
System.out.println("make bmw glass");
}
}
抽象实现子类。。。
具体调用
java
public class MakeCar {
public static void main(String[] args) {
AbsCar car = new AudiCar();
AbsCar car1 = new BMWCar();
car.make();
car1.make();
}
}
Android 源码中的自定义 View 就是模板方法模式,onDraw() onMeasure() onLayout();
如果我们需要自己实现一些锁,那么就需要遵照这个模板方法模式来实现;比如你想实现独占锁,那么就需要实现 AQS 中的 tryAcquire,如果你想实现共享同步锁,那么就实现 AQS 中的 tryAcquireShared 方法;
AQS核心思想(CLH队列锁)
拿到锁的线程在执行的时候,另外的线程需要排队,那么所有要排队的线程都打包成一个QNode(所有线程都会放入一个链表(QNode)中)
QNode包含三个元素:1、当前线程本身,2、myPred,3、locked;
myPred:链表上的指针,指向前驱节点;
locked:表示当前需要获得锁;
假设QNode中的线程A要获得锁,于是采用类似 CAS 的算法,把自己加在已有的链表的尾巴上,让 myPred 指向前一个节点,同时 locked 变成 true
线程A把自己挂在链表的尾部,形成一个新的尾节点,这样线程 A 也支持能锁了;其他线程也是重复这样的操作;
那么节点 A 和节点 B 怎么才能拿到锁呢?A 节点和 B 节点都有一个指针(myPred)指向前一个节点,myPred 本身会不停的自旋,检测前一个节点有没有释放掉锁,如果前一个节点的 locked = false 了,说明前一个节点已经把锁释放了,当前节点的线程可以拿到锁了;
myPred不会一直自旋下去,而是自旋一定的次数(一般是2-3次)之后,如果还没拿到锁,就会把当前线程挂起,进入阻塞状态,并不会一直不停的自旋下去;
自旋逻辑源码如下:
compareAndSetTail 进行入队操作,如果入队不成功,就会调用 enq() 进行自旋操作;
当自旋一定的次数之后,如果还不成功,就会挂起;
ReenTrantLock 公平锁、非公平锁
公平锁说的是:老老实实的在 QNode 链表队尾排队;
非公平锁说的是:可以插队;
显示锁中的公平锁(FairSync)和非公平锁(NonFairSync)的实现;
FairSync -> 拿锁之前会判断下当前链表中是不是有元素在等待;
NonFairSync -> 拿锁之前不判断,直接进行 compareAndSetState 进行拿锁;
ReenTrantLock的可重入
什么是可重入? 可以递归调用就是可重入;
JMM(Java Memory Model)
概念:现代 CPU为了提高运行速度,引入了一批 Cache(L1 2 3)「高速缓存区」,为了管理这批 Cache,Java 中单独提出了 Java 内存模型。预先将 CPU 需要的数据读取到 Cache 中;
三级缓存:
-
第一级存放 cpu 最快使用的指令或者数据,只需要1.2ns;
-
自带第二级缓存,需要5.5ns;
-
多核 CPU 共享三级缓存 需要15.9ns;
Java内存模型中引入了两个抽象概念
工作内存、主内存 这两个抽象概念是很多存储设备的一个综合,比如说 这个工作内存包括了 CPU 寄存器、CPU 高速缓存、甚至还包括了主内存;工作内存 99% 包括了 CPU 高速缓存,1%可能包括了主内存;
例如:多线程执行累加操作,当声明一个 count 变量的时候,它会创建在主内存中,然后每个线程都将 count 复制一个变量副本到自己独享的工作内存中,JMM规定每个线程的工作内存是独享的,都只能操作自己工作内存中的这个 count 变量副本,操作完之后再写回主内存中,不允许线程访问主内存,也不允许线程访问其他线程的工作内存;
JMM 导致的并发安全问题
java
public class VolatileTest {
public int count;
public void add() {
count ++;
}
private static class Count extends Thread {
VolatileTest volatileTest;
public Count(VolatileTest volatileTest) {
this.volatileTest = volatileTest;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 10000; i++) {
volatileTest.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
VolatileTest volatileTest = new VolatileTest();
Count count1 = new Count(volatileTest);
Count count2 = new Count(volatileTest);
count2.start();
count1.start();
Thread.sleep(50);
System.out.println(volatileTest.count);
}
}
打印结果并不是期望的 20000;
原因:当我们有两个线程执行 count = count + 1 的操作的时候,线程 A 将 count 从主内存读到自己的工作内存中,线程 B 也将 count 从主内存读到自己的工作内存中,分别在自己的工作内存中进行 +1 操作,操作完成之后,线程 A 和 线程 B 都要将结果写回到主内存中,理想的结果是 2,但是出现结果为 1 的现象,这就产生了线程不安全的问题;
所以 JMM 模型会牵扯到开发中常见的两个问题:可见性、原子性;
可见性:线程 A 改了 count 的值,线程 B 也改了 count 的值,但是线程 A 和线程 B 之间是互相看不到对方对 count 的修改,这就是线程A和线程B 所存在的可见性问题;
如何解决可见性问题:volatile 关键字;
volatile 关键字有三个作用:
- 强迫从主内存中读取一次 ;
- 变量修改之后强迫马上刷新到主内存 ;
-
其修饰的共享变量进行写操作的时候,会使用 CPU 提供的 Lock: 前缀指令;
-
这个 Lock: 将当前处理器缓存的数据写回到系统内存;
-
这个写回内存的操作会使其他在 CPU 里缓存了该内存地址的数据无效;
- 抑制重排序保证可见性;
但是针对的是:一个线程写,多个线程读的时候 volatile 才没有问题;
我们给 count 加上 volatile 关键字,继续执行;
arduino
public volatile int count;
当我们使用 volatile 关键字之后,结果仍然不是我们想要的结果,这就是所说的原子性问题;
volatile 只是强迫从内存中读了以及算完之后强迫写回内存,但是我们的计算过程(count++)并不是一次就能搞定的;
原子性:线程 B 在执行 count++ 的时候,由于这个操作(count++)不是原子操作,那么这个过程是可以被打断的(比如上下文切换),当 B 被打断的时候,A 可能又继续执行了,当 A 将新的数据写回主内存的时候,B 继续执行 count++ 操作,当执行完写回主内存的时候,就发生了数据异常,因为操作不是原子操作,就存在着被打断的可能,这就是原子性问题;
如何解决原子性问题:加锁;
也就是说:synchronized 同时保证了原子性和可见性,而 volatile 可以说是 JDK 提供的最最轻量级的同步机制,只能保证它的可见性,保证不了它的原子性;
操作流水线、指令重排序
现代 CPU 中有各种高速缓存,CPU 需要把数据读到高速缓存中,就会有一批数据在这个高速缓存区,那么现代 CPU 就引入了这个操作流水线和指令重排序,换句话说,CPU并不像我们想象的那样一次只能执行一条指令,它可以同时一次执行多条指令,例如:
而且 CPU 还可能提前就把 volatileTest.count 进行了获取,并把它放到重排序缓存区,等到真正执行到System.out.println() 的时候,再把它从重排序缓存区中取出放到指定的指令位置;
这就是操作流水线和指令重排序;在单线程上不管是CPU还是JVM,无论它怎么重排序,结果一定是符合我们的要求的,CPU和JMM都进行了保证;但是在多线程上,这种重排序就可能造成一种混乱的现象;
所以 volatile 还有一个功能就是:抑制重排序;
有 volatile 关键字修饰的变量进行写操作的时候会使用CPU提供的Lock前缀指令;
手写 ReenTrantLock,可重入
独占锁也是显示锁,而显示锁我们有在 如何应对Android面试官->线程和进程,手写ThreadLocal 中讲解,显示锁都实现了 Lock 接口,所以我们自定义的独占锁需要实现 Lock 接口;
java
/**
* 线程、进程手写ThreadLocal章节有介绍显示锁都是实现的 Lock 接口
* */
public class CustomReetrantLock implements Lock {
/**
* 仅需要将操作代理到 sync 上即可
* */
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 判断锁是否处于占用状态
* */
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 获取锁
* */
@Override
protected boolean tryAcquire(int arg) {
// 原子交换,将0修改为1,抢占锁
if (compareAndSetState(0, 1)) {
// 告诉其他线程,当前线程抢到了锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(getState() + 1);
}
return false;
}
/**
* 释放锁
* */
@Override
protected boolean tryRelease(int arg) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setState(getState() - 1);
if (getState() == 0) {
setExclusiveOwnerThread(null);
}
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 锁的拿取,代理到 Sync 上
* */
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
/**
* 锁的释放,代理到 Sync 上
* */
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
简历润色
简历上可写:深度理解 AQS 原理和 volatile 关键字,可手写 ReentrantLock 核心实现;
下一章预告
带你玩转 synchronized 关键字;