Java基础--AQS原理



温馨提示:本文3.2W字左右,阅读时间较长,慎入!

1. Lock 谱系图

在这里插入图片描述
jdk对锁的实现的类主要是2个:ReentrantLock(重入锁),ReentrantReadWriteLock(可重入读写锁)

2. Lock 接口

在这里插入图片描述

//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock(); 
//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 
void lockInterruptibly() throws InterruptedException; 
//尝试获取锁,获取锁成功则返回true,否则返回false 
boolean tryLock(); 
//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
//释放锁
void unlock(); 
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

接下来,通过一个小例子,体验锁。
首先是一个并发的问题场景:

public class People {

    private Long sum = 0L;

    private static Long all = 0L;

    public People() {
    }

    public Long getSum() {
        return sum;
    }

    public void setSum(Long sum) {
        this.sum = sum;
    }

    public Long getAll() {
        return all;
    }

    public void setAll(Long all) {
        People.all = all;
    }
}
public class MyLockMain {

    public static void main(String[] args) {
        People people = new People();
        Runnable runnable = () -> {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + thread.getId() + " start ");
            for (int i = 0; i < 10000; i++) {
                people.setAll(people.getAll() + 1);
                people.setSum(people.getSum() + 1);
            }
            System.out.println(thread.getName() + thread.getId() + " end ");
        };
        System.out.println("main thread sum = " + people.getSum() + " , all = " + people.getAll());
        ExecutorService service = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        for (int i = 0; i < 10; i++) {
            service.execute(runnable);
        }
        service.shutdown();
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            System.out.println("main interrupt exception");
        }
        System.out.println("main end sum = " + people.getSum() + " , all = " + people.getAll());
    }

}

我们创建了一个people类,people类有两个属性,这两个属性都有get,set方法。
在主线程中,我们启动10个线程,每个线程都对people的属性进行增加。
我们预期的结果是这两个属性的值都是10W。
执行下看看结果:
在这里插入图片描述
这是一个并发问题,解决这个问题可以通过加锁实现。
基于Lock接口,我们尝试自己实现一个简单的伪锁:

public class MyLock implements Lock {

    private volatile int value;

    @Override
    public void lock() {
        synchronized (this) {
            while (value != 0){
                try {
                    this.wait(); // 重量级锁  CAS自旋
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            value = 1;
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return value == 1;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        synchronized (this) {
            value = 0;
            this.notifyAll();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

在我们自己实现的锁中,以一个int的value的值进行标识。如果value=1标识锁被占用,value=0标识锁空闲。
内部是通过synchronized实现的.
在加锁的地方,我们通过死循环尝试竞争锁,如果竞争失败,则线程挂起,线程阻塞。
在释放锁的地方,我们重置标志,然后唤醒所有挂起的线程,让被挂起的线程竞争锁。
我们试试好不好使。

public class MyLockMain {

    public static void main(String[] args) {
        People people = new People();
        Lock lock = new MyLock();
        Runnable runnable = () -> {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + thread.getId() + " start ");
            for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                    people.setAll(people.getAll() + 1);
                    people.setSum(people.getSum() + 1);
                } finally {
                    lock.unlock();
                }
            }
            System.out.println(thread.getName() + thread.getId() + " end ");
        };
        System.out.println("main thread sum = " + people.getSum() + " , all = " + people.getAll());
        ExecutorService service = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        for (int i = 0; i < 10; i++) {
            service.execute(runnable);
        }
        service.shutdown();
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            System.out.println("main interrupt exception");
        }
        System.out.println("main end sum = " + people.getSum() + " , all = " + people.getAll());
    }

}

在这里插入图片描述
虽然我们实际还是使用synchronized实现的同步,但是,通过这个小例子,可以体验锁是什么。

4. AbstractOwnableSynchronizer

抽象线程持有类,简称AOS.
AOS的代码逻辑很简单:
在这里插入图片描述
AOS是抽象类,有一个内存可见的非持久化的线程属性,以及线程属性对应的get,set方法。
如果锁是独占锁,那么这个类中的线程就是锁持有者的线程。换个角度理解就是,AOS中的线程占有锁。

5. AbstractQueuedSynchronizer

抽象队列同步器,简称AQS。
AQS继承了AOS.
在这里插入图片描述
AQS内部比较复杂,AQS内部有一个Sync的抽象类,Sync抽象类有两个实现,NonfairSync和FairSync。
其中NonfaireSync是非公平获取锁。
FairSync是公平获取锁。
AQS内部还有两个内部类:Node和ConditionObject。
ConditionObject还实现了Condition接口。

5.1 AQS的API

话不多说,我们首先看看AQS的api.
在这里插入图片描述
请注意重点:

  1. AQS是一个框架
  2. 原子int值表示状态
  3. int值只能通过getState(),setState(int)和cas更新
  4. AQS的等待队列是FIFO的(CLH)
  5. AQS无法直接使用,需要子类实现AQS指定的方法

使用:
在这里插入图片描述
我们在使用AQS的框架的时候,只需要让AQS的子类实现这些方法即可。至于等待队列CLH的调度检测等,AQS框架已经帮我们实现了。
这是一个典型的模板方法设计模式的使用例子。

5.2 自定义非重入独占锁

我们根据API的说明,尝试自己实现一个锁。
当然,这个实现的锁,是真正的锁,不是之前使用synchronized的伪锁。

public class MyAQS implements Lock {

    private volatile Sync sync = new Sync();

    private class Sync extends AbstractQueuedSynchronizer{

        @Override
        protected boolean tryAcquire(int arg) {
            // 独占模式获取锁
            assert arg == 1; // arg 必须等于 1
            if(compareAndSetState(0, 1)){ // 修改锁占用状态成功
                setExclusiveOwnerThread(Thread.currentThread()); // 设置锁占用线程
                return true; // 锁获取成功
            }
            return false; // 锁获取失败
        }

        @Override
        protected boolean tryRelease(int arg) {
            // 独占模式释放锁
            assert arg == 1; // arg 必须等于 1
            setExclusiveOwnerThread(null); // 清空占用锁线程
            setState(0); // 设置锁空闲
            return true;
        }
    }

    @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));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

我们实现了Lock接口,在内部私有类继承AQS框架。
我们实现的这个锁是非重入的独占锁。
获取锁主要做了这些操作:
检测传入参数
设置锁占用状态
设置锁持有线程

释放锁主要做了这些操作:
检测传入参数
清空锁持有线程
设置锁空闲

OK,我们创建了自己的锁,那么,这把锁能不能使用呢?
我们尝试使用

    public static void main(String[] args) {
        Lock lock = new MyAQS();
        People people = new People();
        Runnable runnable = () -> {
            lock.lock();
            try{
                for (int i = 0;i < 1000;i++){
                    people.setSum(people.getSum() + 1);
                }
            } finally {
                lock.unlock();
            }
        };
        ExecutorService service = new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));
        for(int i = 0;i < 100;i++){
            service.execute(runnable);
        }
        service.shutdown();
        try{
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException ie){
            System.out.println("ie");
        }
        System.out.println("people sum = " + people.getSum());
    }

不管执行多少次都是10W:
在这里插入图片描述
这个例子只是验证了,我们的锁在并发情况下,是排他锁,或者说是独占锁,也就是说,同一时刻,只有一个线程可以持有锁。
之前我们还说过,MyAQS是非重入锁:

    public static void main(String[] args) {
        Lock lock = new MyAQS();
        Runnable runnable = () -> {
            lock.lock();
            try{
                if(lock.tryLock()){
                    System.out.println("tryLock success");
                    lock.unlock();
                } else{
                    System.out.println("tryLock failed");
                }
            } finally {
                lock.unlock();
            }
        };
        new Thread(runnable).start();
    }

我们在线程内获得锁后,再次尝试获取锁。
在这里插入图片描述
他核心代码就是我们重写AQS的tryAcquire的方法中,使用cas操作将state设置为1.
如果是使用cas操作将state加1,那么就是可重入锁。

可重入锁的释放与获取刚好相反,重入锁的获取是+1,那么释放就是-1.
非可重入锁的获取是设置为1,那么非可重入锁的释放就是设置为0.

5.2 自定义非重入独占锁的缺陷

MyAQS在获取锁上没有问题,主要问题是锁的释放上。
在这里插入图片描述
释放锁没有检测当前状态,如果已经释放了,重复释放锁,是否允许。
在我们非重入锁以及排他锁来看,这样是对的。
释放锁,没有验证,现在持有锁的线程和释放的线程是否是同一个线程。
用大白话来说:
线程A获取了锁,然后A线程在干活。
线程B也来获取锁,然后发现锁被人获取了。
然后线程B调用释放锁的方法,此时锁状态就变了,然后线程B就可以获取锁了。
然后线程B在线程A未释放锁的前提下,获取到了锁。造成的后果就是,线程A与线程B并发执行了。
举个例子:

    public static void main(String[] args) {
        Lock lock = new MyAQS();
        People people = new People();
        Runnable runnable = () -> {
            Runnable runnable1 = () -> {
                for (int i = 0; i < 1000; i++) {
                    people.setSum(people.getSum() + 1);
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ie) {
                    System.out.println("ie");
                }
            };
            if (lock.tryLock()) {
                try {
                    runnable1.run();
                } finally {
                    lock.unlock();
                }
            } else {
                lock.unlock();
                lock.lock();
                try {
                    runnable1.run();
                } finally {
                    lock.unlock();
                }
            }
        };
        ExecutorService service = new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));
        for(int i = 0;i < 100;i++){
            service.execute(runnable);
        }
        service.shutdown();
        try{
            TimeUnit.SECONDS.sleep(110);
        }catch (InterruptedException ie){
            System.out.println("ie");
        }
        System.out.println("people sum = " + people.getSum());
    }

线程的任务是:
线程尝试获取锁,如果获取成功,那么就执行增加people的sum的操作,增加1000次后sleep1秒,执行完成后,在finally中释放锁,任务结束。
如果获取失败,那么调用锁释放操作,然后阻塞获取锁,获取成功后,执行增加操作,执行完成后,在finally中释放锁,任务结束。
这个任务与之前的任务的不同点是,当第一次尝试获取锁失败后,会调用锁释放操作,然后再次获取锁。
接下来看下执行结果:
在这里插入图片描述
说实话,任务里面加完100次后睡眠1秒,然后100个线程,总共需要睡眠100秒(至少),还是很浪费时间的。
结合之前学习的线程的知识,持有锁,然后让出CPU资源,可以使用yeild()替换睡眠操作。
为什么需要睡眠?
指令执行的速度非常快,线程睡眠是故意造成线程竞争锁的目的。
因为我们在锁释放的时候没有验证当前请求的线程是否是线程的持有者,造成不管有没有持有锁,都可以释放锁。就会造成例子中,恶意线程释放其他线程的锁,然后自己使用。
所以,我们在释放的时候,需要验证当前请求的线程是否持有锁:
在这里插入图片描述
还记得吗,AQS继承AOS,而AOS很简单,就是记录哪个线程持有锁(独占模式)
所以,我们判断请求线程与持有线程是否一致,如果一致才释放,否则不释放。
我们使用修改后的锁重试上面的例子:
在这里插入图片描述
使用了yeild快多了。
和我们预期相同。加入持有锁线程判断后,其他线程不能随意释放不属于自己得到的锁了。

5.3 自定义重入独占锁

public class MyAQS1 implements Lock {

    private volatile Sync sync = new Sync();

    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            assert arg == 1;
            int state = getState();
            if ((Thread.currentThread() == getExclusiveOwnerThread() || getExclusiveOwnerThread() == null)
                    && compareAndSetState(state, state + 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            assert arg == 1;
            int state = getState();
            if (Thread.currentThread() == getExclusiveOwnerThread()
                    && compareAndSetState(state, state - 1)) {
                setExclusiveOwnerThread(getState() == 0 ? null : getExclusiveOwnerThread());
                return true;
            }
            return false;
        }
    }

    @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));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

我们定义了一个可重入的独占锁。
核心方法仍然是获取锁和释放锁的方法:
在这里插入图片描述
获取锁:
所处于空闲,或者请求线程已经持有锁
锁状态+1
设置锁持有线程

释放锁:
请求线程持有锁
锁状态-1
设置锁持有线程,如果锁空闲,锁持有线程置空

这就是一个可重入的独占锁
接下来用一个小例子验证:

    public static void main(String[] args) {
        Lock lock = new MyAQS1();
        Runnable runnable = () -> {
            Thread thread = Thread.currentThread();
            lock.lock();
            try {
                Thread.yield();
                System.out.println(thread.getName() + thread.getId() + " 1 get lock");
                lock.lock();
                try {
                    Thread.yield();
                    System.out.println(thread.getName() + thread.getId() + " 2 get lock");
                } finally {
                    lock.unlock();
                    System.out.println(thread.getName() + thread.getId() + " 2 free lock");
                }
            } finally {
                lock.unlock();
                System.out.println(thread.getName() + thread.getId() + " 1 free lock");
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

线程任务中,每次获取到锁,就让出CPU时间。
看下最终的结果:
在这里插入图片描述
因为获取锁的时候,需要验证请求线程是否持有锁,这里实现的独占模式。
获取锁将锁状态加1,这里实现的可重入。
可重入锁需要注意一点,在线程中,获取了多少次锁,就需要释放多少次锁。
要保证线程退出同步的时候,锁状态是0

5.4 自定义门栓锁

前面自定义的都是独占锁,接下来实现一个共享锁。
首先,门栓锁是什么?
想象这样一个场景:全家人出去购物,购物回到房门口之后,发现只有妈妈带有钥匙,于是大家一个排一个的在们口等待。妈妈使用钥匙打开房门后,在一个一个的进入家里。

门栓锁的原理就是这样,当门栓锁还没有收到信号的时候,所有尝试获取信号的线程全部阻塞。当收到信号后,阻塞的线程竞争门栓锁。

public class MyAQS3 {

    private final Sync sync = new Sync();

    private class Sync extends AbstractQueuedSynchronizer{

        boolean isSignalled(){
            return getState() != 0;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            return isSignalled() ? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            setState(1);
            return true;
        }
    }

    public boolean isSignalled(){
        return sync.isSignalled();
    }

    public void signal(){
        sync.releaseShared(1);
    }

    public void await() throws InterruptedException{
        sync.acquireSharedInterruptibly(1);
    }

}
    public static void main(String[] args) {
        MyAQS3 myAQS = new MyAQS3();
        Runnable runnable = () -> {
            myAQS.signal();
            System.out.println("signal");
        };
        Runnable runnable1 = () -> {
            Thread thread = Thread.currentThread();
            try {
                myAQS.await();
                System.out.println("await");
            } catch (InterruptedException e) {
                System.out.println("ie");
            }
        };
        System.out.println("await!!!!!");
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        try{
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException ie){
            System.out.println("ie");
        }
        System.out.println("signal!!!!!!");
        new Thread(runnable).start();
    }

在这里插入图片描述
独占锁和共享锁有一个很大区别:
独占锁在同一时间只有一个线程能获取锁,并开始运行。
而共享锁有一个传播的概念,获取到锁的节点,会通知后面的线程获取锁。
我们将上面的验证逻辑稍微修改:

    public static void main(String[] args) {
        MyAQS3 myAQS = new MyAQS3();
        Runnable runnable = () -> {
            myAQS.signal();
            System.out.println("signal");
        };
        Runnable runnable1 = () -> {
            Thread thread = Thread.currentThread();
            try {
                System.out.println("before await" + Thread.currentThread().getId());
                myAQS.await();
                System.out.println("await" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ie) {
                    System.out.println("ie");
                }
                System.out.println("after yield" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                System.out.println("ie");
            }
        };
        Runnable runnable2 = () -> {
            Thread thread = Thread.currentThread();
            try {
                System.out.println("before await" + Thread.currentThread().getId());
                myAQS.await();
                System.out.println("await" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException ie) {
                    System.out.println("ie");
                }
                System.out.println("after yield" + Thread.currentThread().getId() + " time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                System.out.println("ie");
            }
        };
        System.out.println("await!!!!!");
        new Thread(runnable1).start();
        new Thread(runnable2).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException ie) {
            System.out.println("ie");
        }
        System.out.println("signal!!!!!!");
        new Thread(runnable).start();
    }

第一个线程开始排队,等待门栓;然后第二个线程也开始排队,也等待门栓。
主线程睡眠2秒后,发出信号,门栓已开。
然后第一个线程开始执行,同时第二个线程也开始执行。
第一个线程获取到门栓锁后,睡眠1秒钟。
第二个线程获取到门栓锁后,睡眠2秒钟。
在这里插入图片描述
这个门栓锁是不复位的门栓锁,也就是说当门栓锁收到信号后,后续的线程都可以获取门栓锁了。
门栓锁不是单例。可以为读操作资源增加门栓锁。
当资源准备好后,一直可以读取。

5.5 AQS内部线程存储数据结构–Node

在这里插入图片描述
通过查看源码,发现Node主要是一个双向链表。
这个Node结构,怎么理解呢?
我们首先查看其构造方法:
在这里插入图片描述
发现构造主要传入了三个信息:这个Node对应的thread和Node的模式,以及一个等待的int状态量。
但是通过方法名和传入的类型,对mode这个传入的参数先保持一定的怀疑,貌似不是单纯的模式。
继续往下看:
在这里插入图片描述
从这里我们可以得到两个信息:
1.Node记录了是独占的,还是共享的。
2.int型的状态量至少有1,-1,-2,-3状态。

状态 状态名称 含义
1 取消 已取消(因超时或者中断,取消获取锁)
0 新建 线程节点新建初始化
-1 阻塞 线程节点的的后续线程阻塞,当前节点释放后需要通知后续节点
-2 等待通知 线程在等待通知队列
-3 传播 无条件传播

继续往下:
在这里插入图片描述
从这里我们可以得到这个Node的数据结构是双向链表。
在这里插入图片描述
在Node属性最后,可以看到这一段代码。前面我们知道,SHARED是使用new Node()这个构造器进行初始化的。而new Node()构造器里面什么都没有,也就是说,当nextWaiter是空节点的时候,表是Node是独占模式。
这也是为什么我们看到的第二个构造器传入的Node的参数名字是mode了。
他是由两个属性共同决定的。
在这里插入图片描述
说实话,我看到这里是比较模糊的。到底是个啥?
不着急,稍安勿躁,我们继续往下看。

5.6 AQS内部行为

从5.5可以知道AQS内部数据的存储结构,接下来我们分析下AQS内部的行为
首先AQS是抽象类,那么,AQS要求子类必须实现的接口是
在这里插入图片描述
在这里插入图片描述
在AQS中是没有提供实现的,也就是说,子类如果不实现,会抛出异常。
根据我们前面他体验的,自定义锁的实现,可以知道:
在这里插入图片描述
我们最终的目的是利用AQS实现锁,所以,接下来的方法将会围绕Lock接口的方法进行展开。

5.6.1 tryLock

在这里插入图片描述
tryLock是最简单的方法,直接调用我们Sync里面实现的加锁的操作。

5.6.2 lock–acquire

在这里插入图片描述
锁的加锁操作调用的是AQS的acquire方法,那么,AQS的acquire方法干了什么?
在这里插入图片描述
这是时序图:
在这里插入图片描述
首先会尝试调用获取锁的操作,如果没有获取到锁,那么就会执行&&后面的操作。否则将会继续执行。
从这里可以看出,这不是一个公平锁,因为他在lock的时候会尝试获取锁。如果获取到了锁,那么就会执行。

5.6.2.1 tryAcquire

其中,tryAcquire是AQS的子类必须实现的操作,否则就会报UnsupportedOperationException异常。默认的tryAcquire方法内的操作是new 一个异常抛出。

5.6.2.2 AddWaiter

如果tryAcquire方法返回false,表示获取锁获取失败,然后就会执行addWaiter方法
在这里插入图片描述
在addWaiter中会使用当前线程创建共享的线程节点:
在这里插入图片描述
在addWaiter方法中,首先创建了当前线程的Node节点,然后获取双向队列的尾,如果尾结点不为空,那么将当前节点的前继节点设置为尾节点,然后将新建的节点设置为尾节点,最后将原来的尾节点的后继节点设置为尾节点:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.6.2.3 enq

新增线程节点到双向列表中,然后会执行enq方法(如果尾节点为空,那么表示双向列表为空)
在这里插入图片描述
如果尾节点为空,将会新建一个空node节点,并且将空node节点放到双向列表的头节点,然后头节点和尾节点相等。(第一次循环结束,注意这里是一个死循环)
如果尾节点不为空,就将新建节点的前继节点设置为尾节点,然后更新尾节点,在将原尾节点的后继设置为新建节点。(此时是第二次循环,同时结束循环)
为什么要用死循环?
因为中间调用了CAS操作,那么CAS操作会存在失败的情况,如果CAS调用失败,就会进行死循环的CAS。这里存在自旋的操作,同时,这里也是乐观的,认为CAS失败是小概率,小时间段的操作。
如果长时间CAS失败,这里就会大量自旋,大量的死循环,浪费CPU资源。

5.6.2.4 acquireQueued

acquireQueued方法

final boolean acquireQueued(final Node node, int arg) { // node 是新建的节点,arg是传入的int值
    boolean failed = true; // 设置返回值是true;如果返回值是true代表着需要执行selfInterrupt();方法
    try {
        boolean interrupted = false; // 设置返回值是false;如果返回值是true代表着需要执行selfInterrupt();方法
        for (;;) { // 这里是死循环,自旋
            final Node p = node.predecessor(); // 获取新建节点的前继为当前循环节点
            if (p == head && tryAcquire(arg)) { // 如果当前循环节点是头节点那么尝试获取锁
                setHead(node);// 尝试获取锁成功,更新双向列表中的头结点为新建节点,此时新建节点的前继引用断裂
                p.next = null; // help GC // 原头结点获取到锁,将其从双向列表中移除,
                //因为列表中前继引用已断裂,所以将移除节点的后继引用断裂,此时移除节点已经不再双向列表中了
                // 本次循环结束,移除节点已经是一个可gc的对象了
                failed = false; // 因为新建节点还在双向列表中,双向列表不为空,且本次循环已经有节点获取到锁了,所以不需要取消
                return interrupted;// 返回false,表示不需要执行selfInterrupt方法
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 清理双向列表中取消的线程节点
                parkAndCheckInterrupt()) // 检测是否中断
                interrupted = true; // 返回true;表示需要执行selfInterrupt
        }
    } finally {
        if (failed) // 如果获取锁失败,那么将双向列表中所有的节点都设置为取消
            cancelAcquire(node);
    }
}

5.6.2.5 predecessor

获取前继节点,或者抛出NPE.
在这里插入图片描述

5.6.2.6 shouldParkAfterFailedAcquire

在每次自旋中,如果没有获取锁,那么更新双向列表中可以释放的节点

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 获取前继节点的等待状态
        if (ws == Node.SIGNAL) // 如果前继节点的等待状态是 SIGNAL 也就是-1
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true; // 返回true 将会执行parkAndCheckInterrupt方法
        if (ws > 0) { // ws > 0 那么就表示ws = 1 ,即pred节点是取消节点:CANCELLED
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev; // node 越过前继节点,同时pred前移
                // 如果node的前继节点的等待状态是取消,那么忽略这个节点,知道前继节点不是取消的
                // 这里需要注意,头结点是一个空node,空node的等待状态是没有设置的,但是因为
                // waitStatus是int类型的,默认是0
            } while (pred.waitStatus > 0); // 保证node的前继节点一定不是取消节点
            pred.next = node; // 前继节点的后继节点是node(忽略了取消节点)
        } else { // ws <= 0 ,那么可以是 0,-2,-3
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 将前继节点的状态设置为 -1 SIGNAL,等待通知
        }
        return false;
    }

5.6.2.7 parkAndCheckInterrupt

阻塞线程,并返回线程中断标志
在这里插入图片描述
调用LockSupport.park方法,阻塞线程,然后清除线程的中断标志。
在这里插入图片描述
然后返回线程的中断状态,并重置线程的中断标志。

5.6.2.8 cancelAcquire

如果双向列表的头结点在获取锁的死循环中失败,就会退出死循环:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在predecessor中会抛出异常,只有当新增节点前全部是取消节点或者双向列表为空的时候,新增节点才能访问头结点。
也只有新增节点的前继节点是头结点(忽略取消节点)
就该执行finally中的代码了。
如果头节点获取锁失败,就会执行cancelAcquire方法。
cancelAcquire方法是将尝试获取锁的节点设置为取消状态
在这里插入图片描述
(只有头结点获取锁失败,才会触发,在acquire中,node是新增的节点)
(如果新增节点的前继不是头结点,那么就会就会将前继节点的等待状态设置为SIGNAL)
然后阻塞线程。阻塞线程后设置中断标志为true.最后会返回中断状态。
返回的中断状态会影响是否执行selfInterrupt方法。

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        // 如果node为空,表示node所属的线程现在已经处于阻塞状态
        // 所以,忽略这个阻塞的线程节点,并且终止取消获取锁方法
        if (node == null)
            return;
		// 能进入这个方法,一般就是新增的线程节点中的线程被中断,抛出中断异常,
		// 此时就需要将已经加入到双向列表的线程节点取消
        node.thread = null; // 取消线程节点与线程的关联

        // Skip cancelled predecessors
        Node pred = node.prev; // 得到新增线程节点的前继(忽略取消节点)
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next; // 得到新增线程节点的后继节点

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED; // 新增线程节点设置为取消

        // If we are the tail, remove ourselves.
        // 如果新增线程节点是尾结点,那么就将新增线程节点的前继设置为尾节点
        if (node == tail && compareAndSetTail(node, pred)) {
        	// 如果尾节点更新成功,那么就将尾节点的后继设置为空
            compareAndSetNext(pred, predNext, null);
        } else {
        // 如果新增线程节点不是尾节点
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head && // 新增线程节点的前继不是头节点(双向列表中节点数量大于1)
                ((ws = pred.waitStatus) == Node.SIGNAL || // (新增线程节点的前继节点的等待状态是SIGNAL)
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && // ws < 0,那么在这里,ws一定等于 -1 也就是SIGNAL,
                 //但是因为并发,每次比较之后,值都有可能改变,所以使用ws < 0多判断一次
                pred.thread != null) { // 线程节点所属线程不为空
                Node next = node.next; // 得到新增线程节点的后继线程节点
                if (next != null && next.waitStatus <= 0) // 如果新增线程节点的后继线程节点不为空
                // 且后继线程节点也不是取消的线程节点,那么就将前继节点的后继节点设置为新增线程的后继节点
                    compareAndSetNext(pred, predNext, next);
            } else {
            	// 到了这里,表示新增线程节点是双向列表的头,所以,唤醒新增线程节点中的线程
                unparkSuccessor(node);
            }
            // 新增线程节点的后继设置为自己
            // 我认为隐藏的含义是告诉GC,没有人引用我了,如果内存不足,请回收我吧。
            node.next = node; // help GC
        }
    }

5.6.2.9 unparkSuccessor

唤醒线程节点中的线程

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus; // 获取线程节点的等待状态
        if (ws < 0) // 如果线程节点的等待状态小于 0 ,也就是 -1,-2,-3
            compareAndSetWaitStatus(node, ws, 0); // 那么就将线程节点的等待状态设置为0
            // 0 不仅仅是新建状态,也是线程节点得到锁的标志

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next; // 得到后继线程节点
        if (s == null || s.waitStatus > 0) { // 如果后继线程节点为空或者后继线程节点是取消节点
            s = null; // 忽略取消状态的后继线程节点
            for (Node t = tail; t != null && t != node; t = t.prev) // 遍历双向链表,从后往前遍历
                if (t.waitStatus <= 0) // 找到最前面的非取消状态的线程节点
                    s = t;
        }
        if (s != null) // 如果双向链表中最前面的非取消线程节点不为空
            LockSupport.unpark(s.thread); // 那么唤醒这个线程节点所属的线程
    }

5.6.2.10 selfInterrupt

selfInterrupt方法就是很简单的将线程进行中断。
在这里插入图片描述
为什么要中断呢?
在这里插入图片描述
得到了中断标志,就要在外层进行中断线程:
在这里插入图片描述

5.6.3 unlock-release

首先,unlock中调用的是AQS中的release方法
在这里插入图片描述
这是AQS中release方法的时序图
在这里插入图片描述
比起lock方法,这个可就简单多了。
在这里插入图片描述
release是锁释放的逻辑,代码很少,我们来看下锁是如何释放的
tryRelease是AQS子类必须实现的方法,如果没有实现,将会抛出UnsupportedOperationException异常。

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 尝试释放锁
            Node h = head; // 如果释放成功,那么得到双向列表的头节点
            if (h != null && h.waitStatus != 0) // 如果头结点不为空,且头结点还未获取到锁
                unparkSuccessor(h); // 那么就唤醒头结点(内部会将双向链表从尾到头便利一次,找到最前面的非取消的节点)
            return true;
        }
        return false;
    }

5.6.4 lockInterruptibly-acquireInterruptibly

这个和lock的区别在于,lock是自旋阻塞获取锁,如果获取不到锁,就阻塞了。在5.6.2小节中,我们看了lock的具体过程。
其中有这样一段操作:
在这里插入图片描述
发现线程被中断了,会在外层进行中断线程。也就是说,自旋是不可被打断的,即使我们发出了线程中断信号,线程也只是会在获取到锁之后进行中断。
也就是说,在自旋获取锁的期间,线程是不响应中断信号的。
而acquireInterruptibly方法和lock里面的acquireQueued方法类似,只不过当得到中断信号后,会将异常抛出,将中断异常的捕获和之后的操作交给调用者实现。
acquireInterruptibly的方法:
在这里插入图片描述
如果线程的中断标志已经是中断了,那么就直接抛出异常,快速结束。
否则先尝试获取一次锁,如果获取失败,那么就进入doAcquireInterrupttibly方法。
在这里插入图片描述
doAcquireInterruptibly方法和acquireQueued方法只有红框中的操作不一样,其他的都一模一样。
正式利用了throw抛出异常,使得线程在自旋获取锁的过程中能够响应中断信号。

5.6.5 tryLock(long time, TimeUtil unit)-tryAcquireNanos

尝试获取锁,还有一个有超时时间的方法,在子类里面调用的实际上是AQS的tryAcquireNanos方法
在这里插入图片描述
首先判断线程的中断标志,如果中断标志已中断,那么,直接抛出中断异常,快速结束。
否则先调用tryAcquire尝试获取锁,如果没有获取到锁,那么执行doAcquireNanos。
在这里插入图片描述
这个方法除了红框标记的,其他的和acquireQueued方法相同。所以,我们只看红框中不一样的逻辑。
首先传入了一个long类型的纳秒值。
如果等待的纳秒值小于等于0,直接结束。
否则就会获取当前系统时间的纳秒值,然后计算结束的纳秒值:
结束时间=当前时间+等待时间
在自旋里面,每一次自旋都使用结束时间减去当前系统时间,然后判断结果。
如果差值小于等于0,那么表示时间到了,应该结束。
如果差值大于阈值
在这里插入图片描述
阈值1毫秒
那么阻塞线程。
还会判断线程的中断标志,如果中断标志已中断,那么抛出异常,快速结束。

5.6.6 await-acquireShared

await是共享锁等待信号的方法。
其内部调用了AQS的acquireShared方法:
在这里插入图片描述
也是先进去的时候会尝试获取锁,如果获取到锁变量小于0,那么就会调用doAcquireShared方法。
也就是说,tryAcquireShared如果返回的值 >= 0 那么就获取到了共享锁。
这是acquireShared的时序图:
在这里插入图片描述
如果没有获取到锁,那么就会将新建的节点加入到双向列表中调用addWaiter方法,将新增的线程节点加入到双向列表尾部。
然后进入自选循环。
接着调用predecessor方法,获取新增的线程节点的非取消前继节点。
如果前继节点是头节点(意味着双向列表中前面的线程节点已经获取到了锁,现在新增的线程节点的前继节点是双向列表的头),而新增线程节点的前继也得到了锁,那么就会将前继线程节点移出双向列表,同时将新增的线程节点设置为头结点。
如果设置了新增线程节点为头结点,而且有如下情况的:
a. 共享锁资源大于0(表示头结点也能够获取锁)
b. 头结点为空(其他线程已经让头结点获取到了锁且已经将头节点移出双向列表)
c. 头结点不是取消的线程节点
那么就会得到新增线程节点的后继线程节点。
如果后继节点也是共享锁的线程节点,那么调用doReleaseShared方法:
在这里插入图片描述
这个方法是先获取到头结点,如果头结点不等于空,且双向列表的节点数量不等于1(如果头结点和尾节点重合,那么表示双向列表的节点数量为1,因为双向列表的头节点初始化是一个空节点)那么就获取头结点的等待状态,如果头结点的等待状态是SIGNAL,那么就将头结点的等待状态设置为0,即得到了锁,然后唤醒头结点所属的线程。
如果头结点的等待状态是0,那么设置头结点的等待状态是传播。
这里我是这样理解的:
现在新增的线程节点在双向列表中是第二个节点,此时第一个节点获取到了锁,然后会将头结点移出双向列表,同时将新增的线程节点设置为头结点。
如果新增线程节点有后继节点,对于后继节点来说,如果头结点的等待状态SIGNAL,那么就将头结点设置为已经得到了锁(实际上是头结点之后的节点,头结点是空节点),同时唤醒头结点所属线程;如果头结点已经获取到了锁,那么将头节点的等待状态设置为传播状态。这里应该是自旋进行处理能够获取到锁的线程节点。
剩下的这些方法和acquireQueued方法中的一样处理了。

5.6.7 releaseShared

在这里插入图片描述
这个共享锁释放的方法就很简单,如果tryReleaseShared方法返回false就是释放失败,否则就是释放成功。释放成功会自旋等待双向列表中共享线程节点全部运行。

6. 总结

写了这么多,基本上将AQS的代码阅读了一遍,我们就总结下:

  1. AQS内部存储是一个双端的双向列表
  2. 双向列表的等待状态是1,0,-1,-2,-3
  3. 双向列表总是忽略取消状态的节点
  4. 不管是获取锁还是释放锁都是自旋操作
  5. 线程节点新增入双向列表后,会使线程调度暂时禁用,即暂时阻塞线程
  6. 线程节点获取锁后,将线程调度设置为可用,即唤醒阻塞线程
  7. 独占锁实现tryAcquire和tryRelease方法即可
  8. 共享锁实现tryAcquireShared和tryReleaseShared方法
  9. 获取锁的方法tryAcquire方法返回true表示获取到了锁
  10. 获取锁的方法tryAcquireShared返回的值大于等于0表示获取到了锁
  11. 释放锁的方法tryRelease方法返回true表示释放了锁
  12. 释放锁的方法tryReleaseShared方法返回true表示释放了锁

AQS的调用链很长,设计非常巧妙,看明白AQS的源码非常困难。经常是看着看着就看不懂为什么了,需要阅读很多遍才能明白设计的巧妙。我从头开始阅读源码,到看到这里花费了大概2个周多一点(下班时间阅读,平均每天2小时,期间还有一个周重装了操作系统)。但是读懂AQS的源码很值得,后面再阅读jdk中的锁的时候,就不会很吃力了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章