龙门阵,多线程之——AQS(同步器)源码解析

AbstractQueuedSynchronizer 是构建 Lock 锁或者其他同步组件的基础框架,使用一个Int变量来表示同步状态,通过内置的FIFO(first in first out)队列来完成共享资源的线程排队工作。—— 《Java并发编程的艺术》

我们了解一个东西,一般都是有很多疑问,然后逐个击破,所以这里我们就带着疑问出发,看看AQS到底是个什么东西呢?


问题1:AQS是个什么呢?AQS结构是怎么样的?

是什么?

AQS是个什么在开篇就提到,它和synchronized关键字一样,都是用来控制多线程情况下的共享资源的可见性问题。避免多个线程同时修改同一个对象,造成结果与预期不一致。synchronized关键字会隐式的获得锁及释放锁,而AQS因为获取锁、释放锁的操作是交给程序员去控制,所以控制性更强。

结构?

1.队列:1个同步队列,n等待队列。同步队列是用来排队争抢共享资源的多个线程的队列(SyncQueue);等待队列则是用来存放得到了锁,而主动放弃锁进入等待状态(waiterQueue)的线程。(类比Object.wait()方法,且等待队列可有 n 个,通过Lock接口的.newCondition()方法创建)。

队列的存放并不是直接放一个队列集合,而是记录了head(等待队列叫firstWaiter),tail(等待队列叫lastWaiter)两个首尾节点,在一般情况下并不需要遍历列表,所以效率很高。

结构如图:


2.state:一个volatile修饰的int型变量,用于标识同步状态。当state>0时,表示对象锁已被某线程成功获取。state=0,为可尝试获取。

3.内部类Node:用final修饰,无法继承、修改。主要作用是包装当前线程为节点,然后放入队列中。

4.内部类ConditionObject: 等待队列。继承自Condition接口,在Lock.newCondition()中返回实例,可创建多个。


问题2:AQS为什么可以做到同步效果?

这个问题我们得想想同步效果是个什么效果?多个线程竞争一个资源,在同一时刻只有一个能取得控制权,而没取到的那些家伙怎么办,去哪里了呢。在同步队列里排着队,蓄势待发,等着抢下一轮的争夺(cpu调度)。

我们可以从源码来追踪看看。找到入口:加锁的方法Lock.lock,所以我们找到它的实现类ReentrentLock。从ReentrentLock.lock()进入,看看它的子类NoFairSync(非公平锁)的实现。

当compareAndSetState方法去获取锁失败时,进入acquire(1),1是自定义的数字,当(state=1)>0则表示获取了锁。compareAndSetState是常说的CAS操作,有什么用呢?就是保证设置状态时操作是原子的。继续看看后续:

其中tryAcquire(arg)是抽象方法,由实现类去实现,表示尝试是否可以获取锁(判断state是否等于0),不行进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。其中addWaiter是将当前线程包装为节点Node放入同步队列尾部,然后调用acquireQueued(final Node node, int arg),循环去获取对象锁,失败就阻塞,被唤醒又争夺,失败又阻塞,直到被中断或拿到锁。源码如下:

那么看看如何阻塞的?

就是它!LockSupport.park(this); LockSupport是concurrent包中提供的基础工具类,用于阻塞线程。而真正阻塞线程的是一个unsafe.park()的方法。

unsafe是用于执行低级别、不安全操作的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。一般我们无法拿到这个类的实例。

所以到这里,就知道为什么AQS可以阻塞线程啦。而等待队列中同样是用LockSupport.park()来阻塞线程。


问题3:阻塞线程什么时候唤醒?唤醒之后会执行什么操作?

我们在使用Lock时,常使用Lock lock = ReentrentLock(可重入锁)。获取锁为lock.lock(),而释放锁时则调用lock.unlock();假设现在有三个线程A、B、C同时在竞争,且A之前已经获取了锁,准备释放。

当A需要释放锁时,调用lock.unlock(),因此我们从unlock()方法着手,看看源码怎么实现唤醒,已经做了哪些操作。

这里tryReleases()为ReentrentLock的实现,因为是可重入的,所以state不一定为1,可以是2,3,4...。当c等于0时,清空线程信息。注意,若不是当前获得锁的线程调用是会抛出IllegalMonitorStateException()的。

如果头结点head不为空,且未被取消(h.waitStatus!=0),则唤醒下一个节点,调用unparkSuccessor(h)。这里B线程、C线程均在队列中等待,若B线程为head,则此处会被唤醒。看一下unparkSuccessor的实现:

从中可以看到如果有下一个节点,则找到后唤醒。

线程唤醒后,回到之前阻塞的地方:ParkAndCheckInterrupt(),检查是否线程被中断。未被中断则进入循环,重新进行锁竞争。若被中断,则调用selfInterrupt();


问题4:ConditionObject 里的队列什么时候唤醒 / 阻塞?

何时阻塞?

lock通过调用方法newCondtion()返回一个condition实例。每个实例中都有一个waiterQueue。当在有需要时(比如阻塞队列满了,得等空了才能放入),我们会创建conditon对象,调用condition.await()方法(类比Object.wait() )来放弃锁,进入等待队列。

何时唤醒?

同Object调用notify()相似,Lock调用signal()来唤醒阻塞在等待队列的线程。

12.png

为了方便理解过程,用一个简单的固定大小的阻塞队列实现来观察:

public class FixSizeBlockingQueue {
    private Lock lock = new ReentrantLock();//默认非公平锁
    private LinkedList msgQueue = new LinkedList<>();//消息队列
    private static final int MAXSIZE = 3;//最大消息队列容量
    private volatile int count;
    private volatile int total;

    //队列满了,则用它阻塞线程
    private Condition fullCod = lock.newCondition();
    //队列空了,用它阻塞线程
    private Condition nullCod = lock.newCondition();

    //添加一条消息
    public boolean add(String msg){
        boolean result = false;
        lock.lock();
        try{
            if(count>=MAXSIZE){
                fullCod.await();//队列满了,阻塞
            }
            result = msgQueue.add(msg);
            if(result){
                count++;
                total++;
                nullCod.signal();//唤醒因队列为空,取不到数据而阻塞的队列
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return result;
    }

    //获取一条消息
    public String get(){
        String result = null;
        lock.lock();
        try{
            if(count==0){
                nullCod.await();//队列空了,阻塞
            }
            result = msgQueue.poll();
            if(result!=null){
                count--;
                fullCod.signal();//唤醒因队列满了而阻塞的线程
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return result;
    }

    public int totalNum(){
        return total;
    }
}

测试类:


class FixSizeBlockingQueueTest {

    static FixSizeBlockingQueue queue;

    @BeforeAll
    public static void init(){
        queue = new FixSizeBlockingQueue();
    }
    @Test
    public void test() throws InterruptedException {
            //2个生产者
            for(int i=0;i<2;i++){
                String name = "creator-"+i;
                Creater creater  = new Creater(name,queue);
                Thread thread = new Thread(creater);
                thread.start();
            }
            //2个消费者
            for(int i=0;i<2;i++){
                String name = "cust-"+i;
                Cunstom cunstom  = new Cunstom(name,queue);
                Thread thread = new Thread(cunstom);
                thread.start();
            }
            Thread.sleep(20000);
    }
    //生产者
    public class Creater implements Runnable{
        private String no;//编号
        private FixSizeBlockingQueue queue;

        public Creater(String no, FixSizeBlockingQueue queue) {
            this.no = no;
            this.queue = queue;
        }
        @Override
        public void run() {
            int i=0;
            while (queue.totalNum()<20){
                String msg = "生产者("+no+")生成了一条消息:msg"+i;
                long begin = System.currentTimeMillis();
                System.out.println(begin+"\r\r"+msg);
                queue.add(msg);
                i++;
                System.out.println("生产者("+no+")生成的消息放入成功,耗时:"+(System.currentTimeMillis()-begin));
            }

        }

    }


    //消费者
    public class Cunstom implements Runnable{
        private String no;//编号
        private FixSizeBlockingQueue queue;
        public Cunstom(String no, FixSizeBlockingQueue queue) {
            this.no = no;
            this.queue = queue;
        }
        @Override
        public void run() {
            while(queue.totalNum()<20){
                long begin = System.currentTimeMillis();
                System.out.println("消费者者("+no+")开始读取消息。");
                String msg = queue.get();
                System.out.println("消费者者("+no+")消费到一条消息,耗时:"+(System.currentTimeMillis()-begin)+" 内容为:"+msg);
            }
        }

    }
}

在这个例子中,可以看到ConditionObject是有两个的,一个fullCod,一个NullCod,线程被谁调用就在谁的等待队列中排队。

最后,同步器中子类Node中的waitState状态变化,如singal,cancelled,condition等,在控制场景中也是变化较多,不过就不去慢慢罗列了,在锁控制的流程上,跟着源码走一遍,相信就了解了。

你可能感兴趣的:(龙门阵,多线程之——AQS(同步器)源码解析)