JAVA之AQS底层原理

发布于 2021-4-8 19:11
浏览
0收藏

LockSupport


LockSupport是线程等待唤醒机制(wait/notify)的改良版本。LockSupport中的 park() 和 unpark() 的作用分别是阻塞线程和接触阻塞线程。

 

3种让线程等待和唤醒的方法(线程通信)


方式1:使用Object中的wait()方法让线程等待,notify()方法唤醒线程
synchronized + wait + notify

方式1:使用Object中的wait()方法让线程等待,notify()方法唤醒线程

static Object objectLock = new Object(); // 创建锁

public static void main(String[] args) {

    // 创建A线程,进入后打印,并阻塞。
    new Thread(() -> {
        synchronized (objectLock) {
            System.out.println(Thread.currentThread().getName() + " 进来了!");
            objectLock.wait();
            System.out.println(Thread.currentThread().getName() + " 被唤醒!");
        }
    }, "A").start();

    // 创建B线程,用于唤醒
    new Thread(() -> { 
        synchronized (objectLock) {
            objectLock.notify();
            System.out.println(Thread.currentThread().getName() + " 通知!");
        }
    }, "B").start();
}

 

wait、notify的限制:

 

  • 我们发现 wait 和 notify 如果不在一个代码块里面,必须与 synchronized 搭配使用,否则会报错。
  • 如果我们先使用notify、再使用wait,因为wait是后执行了,所以不能被唤醒。


方式2:使用JUC包中Condition的await()方法让线程等待,signal()方法唤醒线程
Lock + await + signal

// 创建Lock对象,得到condition
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

public static void main(String[] args) {

    // 创建A线程,用await方法阻塞
    new Thread(() -> {
        lock.lock();
        try { 
            System.out.println(Thread.currentThread().getName() + " 进来了!");
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println(Thread.currentThread().getName() + " 被唤醒!");
    }, "A").start();

    // 创建B线程,用于唤醒
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 通知!");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }, "B").start();

}

 

await、signal的限制:

 

  1. 和 wait 、notify 的问题一模一样,他们的底层机制是一样的。


方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
park + unpark,每个线程都有一个 “许可证” ,只有 0 和 1,默认为 0。unpark(Thread t) 方法发放许可证,没许可证就不允许放行。

public static void main(String[] args) {

    // 创建A线程,用park()方法阻塞
    Thread a = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 进来了!");

        LockSupport.park();

        System.out.println(Thread.currentThread().getName() + " 被唤醒!");
    }, "A");
    a.start();

    // 创建B线程,用于唤醒
    Thread b = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 通知!");
        // 唤醒指定线程
        LockSupport.unpark(a);
    }, "B");
    b.start();
}

 

LockSupport的优势:

 

  • 既不用synchronized或Lock。
  • 先唤醒,再阻塞,也能够被唤醒。因为线程已经有了“许可证”了,所以park()方法相当于没执行。


park底层调用了unsafe类的park本地方法。

UNSAFE.park(false, 0L);

调用一次 unpark 就加 1,变为 1。调用一次 park 会消费许可证,变回 0。重复调用 unpark 不会积累凭证。

 

AQS理论

 

  • AQS(AbstractQueuedSynchronizer),抽象的队列同步器。ReentrantLock类里,有一个内部类Sync就是继承的AQS类。
  • AQS是用来构建锁 或者 其他同步器组件的重量级基础框架及整个JUC体系的基石。通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。


AQS的作用
和AQS有关的 :ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore......

锁和同步器的关系:

 

  • 锁,面向锁的使用者。
  • 同步器,面向锁的实现者。


如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁的分配。这个机制主要用的是 CLH队列 的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过 CAS 自旋以及 LockSupport.park() 的方式,维护state变量的状态,使并发达到同步的控制效果。

JAVA之AQS底层原理-开源基础软件社区

AQS源码体系


AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队,将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对State值的修改。

 

  • 表示同步状态的int成员变量
    private volatile int state;​

state为 0 就是没人占用,可以去获取资源。大于等于 1,有人占用资源,需要排队。

  • CLH队列
    CLH队列,是一个双向队列。通过自旋等待,state变量判断是否阻塞。

内部类Node作为载体,装的是需要排队的线程。

 

  • 内部类Node
    队列中每一个排队的个体就是一个Node。Node有前结点prev 、 后结点next 、头指针head 、尾指针tail,用于完成双向队列。

Node类里有两个模式:SHARED(表示线程以共享的模式等待锁)、EXCLUSIVE(表示线程以独占的方式等待锁)

类中也有一个int类型的状态变量 waitStatus。意思是等候区其他线程的等待状态。

volatile int waitStatus;

 

从ReentrantLock开始解读AQS

JAVA之AQS底层原理-开源基础软件社区

ReentrantLock类中有个子类Sync继承了AQS类,NonfairSync类和FairSync继承了Sync类。JAVA之AQS底层原理-开源基础软件社区

公平锁和非公平锁实现方法的唯一区别就在于:公平锁在获取同步状态时多了一个限制条件:hasQueuePredecessors()。

 

这个是公平锁加锁时判断等待队列中是否存在有效节点的方法。因为公平锁是先排先得。

 

new一个ReentrantLock类时,不传参数默认是非公平锁(NonfairSync),传入true是公平锁(FairSync)JAVA之AQS底层原理-开源基础软件社区

  • 公平锁:讲究先来先到,线程在获取锁时,如果等待队列中以及有线程在等待,那么当前线程就会进入等待队列中。
  • 非公平锁:不管是否有等待队列,都会去尝试获得锁。


AQS非公平锁就是,一来就先去插队,如果插队失败,才去乖乖的排队。

 

银行办理业务案例

public static void main(String[] args) {

    ReentrantLock lock = new ReentrantLock();

    // 带入一个银行办理业务的案例来模拟我们的AQS如果进行线程的管理和通知唤醒机制

    // 3个线程模拟3个来银行,受理窗口办理业务的顾客

    // A 顾客就是第一个顾客,此时手里窗口没有任何人,A可以直接去办理
    new Thread(() -> {
        lock.lock();
        try {

            System.out.println("A线程 进入");

            // 办理20分钟
            TimeUnit.MINUTES.sleep(20);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }, "A").start();

    // B顾客,由于窗口只有一个(只能一个线程持有锁),B只能等待,进入候客区
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("B线程 进入");
        } finally {
            lock.unlock();
        }
    }, "B").start();

    // C顾客,进入候客区(当A办理完成后,会与B去抢)
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("C线程 进入");
        } finally {
            lock.unlock();
        }
    }, "C").start();

}

 

整个ReentrantLock的加锁过程,可以分为三个阶段:

  1. 尝试加锁
  2. 加锁失败,线程进入AQS队列
  3. 线程进入队列后,进入阻塞状态


lock()上锁


调用的是sync类的lock()方法。

如果是FairSync:

final void lock() {
    acquire(1);
}

 

如果是NonfairSync:

 

/*
    底层是unsafe类,尝试用CAS获得锁,成功后设置这个线程拥有访问权限。否则调用acquire
*/
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
/*
    unsafe类的方法,传入(0,1),如果这个对象的内存偏移量的位置,expect如果为 0,就为改为 1
*/
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
} 

//CAS修改成功后,通过AbstractOwnableSynchronizer类的setExclusiveOwnerThread方法,把exclusiveOwnerThread线程设为当前线程。

 

当第一个顾客发现没人窗口没人后,开始办理业务。state变为1,占用顾客的线程是 currentThread。

 

第一个客户已经占用了窗口,没那么快完成。第二个客户也调用lock()方法,发现窗口被占用,只能去排队:

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//里面的addWaiter方法、tryAcquire方法、acquireQueue方法都是重点

 

AQS的tryAcquire方法
我们进入tryAcquire方法发现没有逻辑代码,直接抛出异常。这就是典型的模板方法设计模式。意思是所有子类必须实现这个方法,不实现父类就抛出异常。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

 

然后发现 ReentrantLock 的 内部类NofairSync 重写了这个方法:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

 

这个方法其实调用的是内部类Sync 的 nonfairTryAcquire 方法。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();    //需要排队的第二位顾客
    int c = getState();        // 获取当前窗口的状态state(0空闲,1占用)
    
    //如果运气非常的好,窗口恰巧空闲了,就CAS改变状态,把窗口的线程设为自己。
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    
    //如果当前线程 等于 正在办理业务的线程 (说明获得了多次锁,是可重入锁的理论)
    else if (current == getExclusiveOwnerThread()) {    
        int nextc = c + acquires;        // nextc为当前状态加 1
        if (nextc < 0) // overflow(溢出)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);                // 设置状态变量 state
        return true;
    }
    return false;
}

 

我们传入的第二位顾客再次发现,有人在办理业务,返回 false。

在acquire方法中 !tryAcquire(arg) 取反为 true ,继续判断下面的方法。

 

  • AQS的addWaiter方法
    acquire传入的是 Node.EXCLUSIVE 参数(结点的模式);结点进入队列。
     private Node addWaiter(Node mode) {
        //构造Node结点(当前线程,模式)
        Node node = new Node(Thread.currentThread(), mode);    
         
        // 获取Node的尾结点,如果为null,说明队列没有结点。
        Node pred = tail;
        
        // 当第三个顾客进入的时候,等候区已经有结点了,执行这个代码块。和enq方法相似,尾插法。
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        
        //如果队列没有结点,调用enq方法准备进入队列
        enq(node);
        return node;
    }​

 

enq 方法(将节点插入队列):

private Node enq(final Node node) {
    //相当于自旋
    for (;;) {
        Node t = tail;        // t 是尾指针
        
        //如果尾指针为null,说明队列无结点,进行初始化
        if (t == null) { 
            /*    第一个结点并不是我们传入的结点,而是系统new了一个结点作为占位符。
                  这个结点Thread=null,waitStatus=0,是傀儡结点又称哨兵结点,用于占位。    */
            if (compareAndSetHead(new Node()))    
                tail = head;
        
        //队列有结点后,继续循环,进入下面这个代码块(尾插法,结点的尾、前、后结点都设置好)    
        } else {
            //传入结点的前一个指针指向尾结点
            node.prev = t;
            //尾指针 指向 传入的节点
            if (compareAndSetTail(t, node)) {
                t.next = node;    // 尾结点的下一个节点是 传入的节点
                return t;        // 返回新插入的尾结点
            }
        }
    }
}

 

  • AQS的acquireQueued方法
    传入的参数是 (addWaiter(Node.EXCLUSIVE), arg)。
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                final Node p = node.predecessor();    //传入结点的上一个结点 
                
                // 如果前结点 == 哨兵结点 && 再看窗口能否抢占,失败就false。
                if (p == head && tryAcquire(arg)) {
                    // 头结点指向当前节点,节点Thread=null,prev=null,即当前节点变成了新的哨兵结点
                    setHead(node);    
                    // 原哨兵结点的next=null,没有连接了,会被GC回收
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                //抢占失败后是否park阻塞
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
                
                /*
                    这时自旋锁,抢占又失败后,继续进入shouldParkAfterFailedAcquire方法,
                    因为第一次循环已经将前结点的waitStatus的值改为-1,所以返回true。    
                    然后进入parkAndCheckInterrupt方法。
                    */
                
                /* 
               锁被释放,其他线程被唤醒后!parkAndCheckInterrupt()返回false,继续自旋!
               B线程的前结点就是哨兵结点,执行tryAcquire方法,因为A线程走了,所以成功抢占!返回true   
                */
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }​

 

shouldParkAfterFailedAcquire 方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;    //查看前结点的waitStatus状态
    
    //SIGNAL值固定为-1
    //如果是SIGNAL状态,即等待中,直接返回true。
    if (ws == Node.SIGNAL)        
        return true;
     
    //waitStatus 大于0说明是 CANCELLED 状态
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
        
    } else {
        //把前结点的waitStatus值改为 -1,用于后续唤醒操作
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
    }
    return false;
}

 

parkAndCheckInterrupt 方法:

private final boolean parkAndCheckInterrupt() {
    //阻塞这个线程!这时可以认为已经坐在等待区了。
    LockSupport.park(this);            
    
    //线程被唤醒后,不被阻塞,这里就返回false
    return Thread.interrupted();    
}

 

此时这个acquireQueued方法还没有结束,会被卡在parkAndCheckInterrupt方法内部,如果这个线程被unpark了。就会继续执行acquireQueued方法的代码。

 

unlock释放锁
调用的是sync类的lock()方法。

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

 

调用AQS的release方法,arg=1.

public final boolean release(int arg) {
    //释放一把锁后,返回true
    if (tryRelease(arg)) {
        // 头结点就是哨兵结点
        Node h = head;        
        // 哨兵的waitStatus为-1,符合条件进入
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);        //
        return true;
    }
    return false;
}

 

tryRelease 方法,也是一个模板方法,ReentrantLock类的Sync重写了这个方法。

 

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

 

protected final boolean tryRelease(int releases) {
    //如果当前State为1,减去1后为0
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
    boolean free = false;
    
    if (c == 0) {
        free = true;    //c=0,说明可以解锁,free变为true
        setExclusiveOwnerThread(null);    //设置当前窗口的占用线程为 null
    }
    setState(c);    //把状态改为相应的值
    return free;
}

 

unparkSuccessor方法,释放锁!

private void unparkSuccessor(Node node) {
    // 传入的是哨兵结点,waitStatus为-1
    int ws = node.waitStatus;
    
    // 又把哨兵结点的waitStatus改为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);    

    // s是哨兵结点的下一个结点。
    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;
    }
    
    // 如果哨兵结点的下一个结点存在,且waitStatus为0,释放锁!
    if (s != null)
        LockSupport.unpark(s.thread);
}

 

此时线程被唤醒,前面acquireQueued里阻塞的其他线程就继续往下执行。

 

 

 

 

 

 

 

标签
已于2021-4-8 19:11:34修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐