四、深入理解AbstractQueuedSynchronizer

版权声明:不正确的地方还恳请指出,真心感谢! https://blog.csdn.net/m0_38032942/article/details/87891109

在这里插入图片描述
这些是之前的文章,方便边阅读边看看之前学习过的内容:
《一、多线程基本操作与实现》
《二、synchronized底层实现与优化》
《三、生产者消费者模型》

一、LOCK体系简介

JDK1.5之后增加Java.Util.concurrent.locks提供了与内建锁完全不同的实现多线程共享资源访问机制。内建锁使用的还是native方法实现的,底层是monitor指令,而Lock体系的锁全是纯Java实现的一个锁体系。

Lock体系的锁失去了内建锁隐式地加锁与解锁过程,增加了可中断的获取锁以及超时获取锁以及共享。Lock锁的标准使用形式:

Lock lock = new ReentrantLock();
try{
     lock.lock();
}finally{
     lock.unlock();
}

二、Lock常用API

1、void lock(); 获取锁
2、void lockInterruptibly() throws InterruptedException; 获取锁的过程能响应中断(Lock独有)。
3、boolean tryLock(); 获取锁返回true,反之返回false,可响应中断。
4、boolean tryLock(long time,TimeUnit unit); 在三的基础上增加了超时等待机制,规定时间内未获取到锁,线程直接返回(Lock独有
5、void unlock(); 解锁
6、Condition newCondition(); 获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回。
在这里插入图片描述
AbstractQueuedSynchronizer(同步器),Lock体系最核心的存在
同步器作用:用来构建锁与其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列形成同步队列,要使用AQS,推荐使用静态内部类继承AQS,覆写AQS中的protected用来改变同步状态的方法,其他方法主要是实现排队与阻塞机制。状态更新使用getState()、setState()、compareAndSetState()。

子类推荐使用静态内部类来继承AQS实现自己的同步语义。同步器即支持独占锁,也支持共享锁。

class NewBeeLock implements Lock {
    private Sync sync = new Sync();
    static class Sync extends AbstractQueuedSynchronizer{
        @Override
        //规定同步状态为1
        protected boolean tryAcquire(int arg) {
            if(arg != 1){
                throw new RuntimeException("arg不为1");
            }
            //CAS操作把0变成1
            if(compareAndSetState(0,1)){
                //当前线程成功获取到锁
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if(getState() == 0){
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }
    }

    //Lock接口方法------------------------------------------------
    @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,time);
    }

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

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

//------------------------Test----------------
class MyThread implements Runnable{
    private Lock lock = new NewBeeLock();
    @Override
    public void run() {
        try{
            lock.lock();
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(myThread);
            thread.start();
        }
    }
}

在这里插入图片描述
通过我们的自定义Lock的实现类可以看出:Lock面向使用者,定义了使用者与锁交互的接口,隐藏了实现细节,AQS面向锁的实现者,简化了锁的实现方式屏蔽同步状态的管理、线程排队、线程等待与唤醒等底层操作。

三、ASQ提供的模板方法

独占锁:
1、void acquire(int arg):独占式获取同步状态,如果获取失败则将当前线程插入同步队列进行等待
2、void acquireInterruptibly(int arg):在1的基础上增加响应中断
3、void acquireInterruptibly(int arg,long nanosTimeOut):在2的基础上增加超时等待,在规定时间为获取到同步状态返回false
4、boolean tryAcquire(int arg):获取状态成功返回true,否则返回false
5、boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个结点

共享锁:
1、void acquireShared(int arg): 共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态。
2、void acquireSharedInterruptibly(int arg) : 增加了响应中断的功能
3、boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) : 在2的基础上增加了超时等待功能
4、boolean releaseShared(int arg) : 共享锁释放同步状态。

四、同步队列

要想掌握AQS的底层实现,其实也就是对这些模板方法的逻辑进行学习。在学习这些模板方法之前,我们得首先了解下AQS中的同步队列是一种什么样的数据结构,因为同步队列是AQS对同步状态的管理的基石。

AQS中的同步队列是一个尾带有头的双向链表,节点组成为:Node prev,Node next,Thread thread,将线程封装为Node结点后进行入队与出队处理,如下图:
在这里插入图片描述
在这里插入图片描述
节点的属性如下:

  • volatile int waitStatus; // 节点状态
  • volatile Node prev; // 当前节点的前驱节点
  • volatile Node next; // 当前节点的后继节点
  • volatile Thread thread; // 当前节点所包装的线程对象
  • Node nextWaiter; // 等待队列中的下一个节点

节点的状态如下:

  • int INITIAL = 0; // 初始状态
  • int CANCELLED = 1; // 当前节点从同步队列中取消
  • int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程继续运行。
  • int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
  • int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。

五、深入理解AQS

独占锁获取:acquire(int arg)

public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
 }

tryAcquire()方法就是再次使用CAS尝试获取同步状态,如果成功方法直接返回,当前线程置为持有锁线程!若再次尝试失败,调用addWaiter()

addWaiter()将当前线程封装为Node节点之后插入同步队列中:

private Node addWaiter(Node mode) {
 	 //将当前线程以指定模式封装为Node节点(独占式/共享式)
     Node node = new Node(Thread.currentThread(), mode);
     //拿到当前同步队列的尾节点 
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         //CAS操作把当前节点尾插入同步队列,失败的话会调用enq()
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     //当前队列为空,或者CAS尾插失败时调用enq()
     enq(node);
     return node;
 }

当前同步队列为空或者尾插失败:调用enq(),enq()的源码如下:
可以看出当同步队列为空时完成队列初始化操作以及不断CAS自旋将当前节点尾插入同步队列!

private Node enq(final Node node) {
    for (;;) {	//不断自旋
    	//拿到尾节点
        Node t = tail;
        //当前队列为空
        if (t == null) { // Must initialize
        	//完成队列的初始化操作,lazy-load模式
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //不断的将当前节点使用CAS尾插入同步队列中直到成功为止
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued的源码:

final boolean acquireQueued(final Node node, int arg) {
	//设置失败状态,初始化为true
    boolean failed = true;
    try {
    	//设置中断状态,默认为false
        boolean interrupted = false;
        for (;;) {
        	//拿到当前节点的前驱节点
            final Node p = node.predecessor();
            //当前节点的前驱节点为头节点并且再次获取同步状态成功
            if (p == head && tryAcquire(arg)) {
            	//将当前节点置为头节点
                setHead(node);
                //将前驱节点出队列
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
        	//将当前节点置为取消状态,node.waitStutas = 1;
            cancelAcquire(node);
    }
}

由上面这段代码可以看出:节点从同步队列获取同步状态的前提条件:if(p == head && tryAcquire(arg)) ,只有当前驱节点为头结点时,线程才有机会获取同步状态。
在这里插入图片描述
当线程获取同步状态失败的时候,首先调用shouldParkAfterFailedAcquire方法,shouldParkAfterFailedAcquire方法的源码:

private static boolean shouldParkAfterFailedAcquire
							(Node pred, Node node) {
		//获取前驱节点状态
        int ws = pred.waitStatus;
        //SIGNAL是-1,表示当前节点的后继节点处于等待状态
        if (ws == Node.SIGNAL)
        	//表示当前节点阻塞
            return true;
        //前驱节点被取消
        if (ws > 0) {
        	//一直向前找到节点状态不是取消状态的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	//将前驱节点状态置为SIGNAL:-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire(p,node)一直在acquireQueue()方法中自旋直到将前驱节点状态置为SIGNAL,表示此事应该将当前节点阻塞!前驱节点状态置为-1,调用parkAndCheckInterrupt()阻塞当前节点线程:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

整个acquire()方法的流程图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
独占锁的释放:release()
unparkSuccessor():唤醒头结点的下一个结点(唤醒距离头结点最近的一个非空结点 —— 保证公平性),如果它存在的话,此时头结点是当前已经获取到锁的结点

public final boolean release(int arg) {
    if (tryRelease(arg)) {
    	// 获取到当前同步队列的头节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
        	// 唤醒头节点的下一个节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor:唤醒头节点的下一个节点,其实是唤醒距离头结点最近的一个非空节点,这是为了最大限度的保证公平性,在头部的等待的时间是最长的,所以应该先唤醒距离头结点最近的一个非空节点!

private void unparkSuccessor(Node node) {
	// 获取节点状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);


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

总结一下:

  • 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试
  • 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞
  • 释放锁的时候会唤醒后继节点

总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。

猜你喜欢

转载自blog.csdn.net/m0_38032942/article/details/87891109