由于AQS是采用了模板方法模式,所以先了解学习设计模式中的模板方法模式。
官方解释模板方法模式:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
其实就是由父类提供一个框架方法(模板方法),在框架方法中调用具体的流程方法,而具体的流程方法则是由实现它的子类进行实现。
例如:抽象父类
public abstract class SendCustom {
/**
* 流程方法,由子类去覆盖
*/
public abstract void to();
public abstract void from();
public abstract void content();
public void date(){
System.out.println("发送时间:"+new Date());
}
public abstract void send();
/**
* 框架方法
*/
public void sendMessage(){
to();
from();
content();
date();
send();
}
}
实现的子类:
public class SendCustomImpl extends SendCustom {
@Override
public void to() {
System.out.println("发给张三");
}
@Override
public void from() {
System.out.println("我是李四");
}
@Override
public void content() {
System.out.println("好久不见,你还好吗!");
}
@Override
public void send() {
System.out.println("可以发送了");
}
public static void main(String[] args) {
SendCustomImpl obj = new SendCustomImpl();
obj.sendMessage();
}
}
在AQS中的(框架)模板方法:
- 独占式获取锁:acquire()、acquireInterruptibly()、tryAcquireNanos()
- 共享式获取锁:acquireShared()、acquireSharedInterruptibly()、tryAcquireShredNanos()
- 独占式释放锁:release()
- 共享式释放锁:releaseShared()
需要子类覆盖的方法,例如使用了AQS的ReentrantLock、ReentrantReadWriteLock……或者是我们自己实现一个同步器锁时,都要重写下面这些方法。
- 独占式获取锁:tryAcquire()
- 共享式获取锁:tryAcquireShared()
- 独占式释放锁:tryRelease()
- 共享式释放锁:tryReleaseShared()
- 判断同步器是否是独占模式:isHeldExclusively()
AQS 的全称为(AbstractQueuedSynchronizer
),这个类在 java.util.concurrent.locks 包下面。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7)等等皆是基于 AQS 的。
当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
模仿ReentrantLock通过AQS定义一个自己的同步器。先明白一个状态变量state
。当状态值为0时,线程可以获取锁。
state
这个变量是锁的同步状态:
- 获取当前同步状态:getState()
- 设置当前同步状态:setState()
- 使用cas设置状态,保证状态设置的原子性:compareAndSetState()
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
由于我们自己实现的是一个独占锁,所以以下分析以独占式锁为主。
public class SelfLock implements Lock {
//用内部类的方式实现AQS,重写流程方法
private static class Sync extends AbstractQueuedSynchronizer{
//定义一个线程调度器
Condition newCondition(){
return new ConditionObject();
}
//尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
//使用了cas,判断当前的state状态是不是0,是0就拥有锁,并改状态state值为1
if (compareAndSetState(0,1)){
//给当前线程上一把独占锁(排他锁)
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState()==0){
throw new RuntimeException("没有线程占用锁异常!");
}
//当前独占线程为null
setExclusiveOwnerThread(null);
//设置状态为0
setState(0);
/*
问题:在获取锁时,判断当前锁状态使用的是compareAndSetState(0,1)保证了原子性,这里释放锁为什么不使用原子性,直接setState(0)?
因为这是实现独占锁,独占锁每次只能只有一个线程可以获取到锁,所以在释放的时候不存在多线程竞争,不用考虑原子性
*/
return true;
}
@Override
protected boolean isHeldExclusively() {
//当前排他锁的状态
//return getState()==0;
//目前占有锁的线程是不是当前线程
return getExclusiveOwnerThread()==Thread.currentThread();
}
}
private final Sync sync=new 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.tryAcquireSharedNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);//调用父类的模板(框架)方法
}
//获取调度器
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
测试:
public class SelfLockTest {
public void test(){
// final Lock lock=new ReentrantLock(); //使用自己实现的锁和ReentrantLock效果一样
final Lock lock=new SelfLock();
class Worker extends Thread {
@Override
public void run() {
while (true){
lock.lock();
try {
//睡一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//业务代码---输出名称
System.out.println(Thread.currentThread().getName());
//睡一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
//睡2秒
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//启动10个线程
for (int i = 0; i <10; i++) {
Worker worker=new Worker();
worker.setDaemon(true);
worker.start();
}
//主线程没隔1秒换行
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
}
}
public static void main(String[] args) {
SelfLockTest test=new SelfLockTest();
test.test();
}
}
问题:在测试中,我们创建了10个线程,去争抢自己实现的AQS独占式锁,效果和ReentrantLock一样,说明我们定义的锁是没有问题的。那么AQS内部如何安排这10个线程的,当有一个线程拿到锁时,其他9个线程是如何安排的?内部是如何实现的?
下面分享一下AQS的内部结构就大概明白这个原理了!!!
他的内部维护了一个双向队列,节点用来装每个线程。这个队列按照专业说法叫:
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;//前驱
volatile Node next;//后驱
volatile Thread thread;//节点存储的内的线程
Node nextWaiter;//等待队列 Link to next node waiting on condition
}
SIGNAL:(signal)后续节点等待状态,当前节点通知后面节点去运行
CANCELLED:(cancelled)线程等待或者被中断了,需要从队列中移走
CONDITION:当前节点处于等待duil
PROPAGATE:共享,表示状态要往后面的节点传播
0: 表示初识状态
当一个线程获取锁失败时,java就会把当前线程打包成一个Node节点,这些节点通过前驱和后驱指针链接在一起构成一个同步对列。
例如上面的例子中:10个线程争抢独占式锁,每次都只有一个线程获取到锁,剩下的线程就会打包成Node构成同步对列,当第一个获取到锁的线程释放锁后,会依次唤醒对列的线程去拿锁,这个同步队列可以理解为拿锁失败的线程链表。
节点在同步对列的增加和移出过程(线程从等待到获取到锁的过程)如图所示:
注意:这里在设置尾节点时采用了cas、而设置头结点却没有,是因为:在争夺锁的线程是不确定的可能有多个,但是获取独占锁的只有一个线程,那么就有多个线程获取锁失败,会链接到队列尾节点上,这里使用cas保证了不会发生错误。而首节点只有一个是确定的。
先明白condition接口是为了Lock锁的调度的接口,每个lock锁都应该实现这个接口进行线程间的调度。
condition中也是Node节点的链表。
每通过锁new一个Condition对象,就会连接在这个锁的同步器同步队列的的nextWaiter
指针中,也就是说同步队列只有一个,而等待队列可能有多个,不过通常建议是一个同步队列,只new一个Condition对象,也就是一个等待队列。
如果一个condition能调用await()
方法,说明已经拥有锁了,那么就会让拥有锁的线程Node移动到等待队列中去。
而signal()
方法唤醒时,会发出通知,让节点回到同步队列中去竞争锁,而此时锁被那个线程占用是不确定的,所以会连接在同步队列的尾部。
Condition不建议使用signalAll()
方法的原因其实就是避免这个操作,因为要把所有位于此Condition对象的等待队列中的节点,统统转移到同步队列尾部去。
而synchronized中则建议使用notifyAll()
, 它的内部其实也是维护了一个同步队列,但是等待队列只有一个,所以线程们调用wait()
进入这个唯一的等待队列中后,是不能确保哪一个线程节点处于等待队列的头结点的,所以在notify()
唤醒时就可能不是想要的那个线程,所以要使用notifyAll()
唤醒所有线程节点,由线程自己去判定条件是否满足。
直接上图了,结合上面的分析,慢慢看图中的解析慢慢读源码吧。
ReentrantLock的可重入原理在于他每次获取锁时都进行加操作,释放锁时进行了减操作,在做cas判断锁状态时,保证了重入多少次就释放多少次。
这也就是非公平锁比公平锁效率高的原因:因为它在获取锁的时候,直接判断当前锁有没有被别的线程占用,没有就直接获取了,不管队列中有没有线程在等待。公平锁则要老老实实的等待,前驱节点的线程执行完,再去获取锁。
LockSupport工具类提供了一些方法,方便阻塞、唤醒、构建同步组件的基础工具。park()开头的方法就是阻塞。
以下内容来自慕课网
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
ReentrantLock(boolean fair)
构造方法来制定是否是公平的。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 性能已不是选择标准
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。