Java中的锁(深入理解AQS)

一、队列同步器 AbstractQueuedSynchronizer

队列同步器 AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO 队列来完成资源获取线程的排队工作。

1. Lock接口

Java SE 5 之后,并发包中新增了 Lock 接口(以及相关实现类),用来实现锁功能,它提供了与 synchronized 关键字类似的同步功能,只是在使用时需要显式地获取锁和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与同步的可操作性可中断的获取锁以及超时获取锁等多种synchronized 关键字不具备的功能

Lock 接口提供的synchronized 关键字不具备的主要特性

  • 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  • 能被中断的获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会抛出,同时锁会被释放
  • 超时获取锁:在指定截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

2. AQS的使用

AQS的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法,这样就大大降低了实现一个可靠自定义同步组件的门槛

package lockTest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @Description: AQS
 * @Author: Aiguodala
 * @CreateDate: 2021/4/19 10:51
 */

public class AQSTest {

    private static final MyLock LOCK = new MyLock();
    public static void main(String[] args) {
        new Thread(() -> {
            LOCK.lock();
            try {
                System.out.println("第一个线程获取锁");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("第一个线程释放锁");
                LOCK.unlock();
            }
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            LOCK.lock();
            try {
                System.out.println("第二个线程获取锁");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("第二个线程释放锁");
                LOCK.unlock();
            }
        }).start();




    }
}
class MyLock implements Lock {

    /**
     * 静态内部类,自定义同步器
     */
    private static class Sync extends AbstractQueuedSynchronizer {

        /**
         * 是否处于占用状态
         * @return
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        /**
         * 当状态为 0 的时候获取锁
         * @param arg
         * @return
         */
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 释放锁,将状态置为 0
         * @param arg
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            // setState 放在最后是因为 state 变量是volatile 修饰的,加入写屏障可以保证可见性
            setState(0);
            return true;
        }

        /**
         * 每个Condition 都包含了一个Condition队列
         * @return
         */
        Condition newCondition () {
            return new ConditionObject();
        }
    }

    /*********** 以下方法只需要将操作代理到 Sync 对象上 ***********/

    /**
     * 创建锁对象
     */
    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.tryAcquireNanos(1,unit.toNanos(time));
    }

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

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

3. AQS 实现原理

3.1 同步队列

同步器依赖内部的同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态的等信息构造成一个节点( Node )并将其加入到同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

Node 主要字段

        /**
         * 等待状态
         */
        volatile int waitStatus;

        /**
         * 前驱节点,当节点被加入到等待队列时被添加
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 获取同步状态的线程
         */
        volatile Thread thread;

        /**
         * 等待队列的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,
         * 也就是说节点类型(独占或者共享)和等待队列中的后继节点共用一个字段
         */
        Node nextWaiter;
  • waitStatus 的状态有:
        /**
         * 由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
         * 节点变成该状态将不会变化
         */
        static final int CANCELLED =  1;
        
        /**
         * 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消
         * 将会通知后继节点,使得后继节点得以运行
         */
        static final int SIGNAL    = -1;
        
        /**
         * 节点在等待队列中,节点线程等待在Condition上
         * 当其他线程对Condition调用了signal() 方法后,该节点会从等待队列转移到同步队列中
         */
        static final int CONDITION = -2;
        
        /**
         * 表示下一次共享式同步状态会被无条件传播下去
         */
        static final int PROPAGATE = -3;

队列结构

在这里插入图片描述

  • 同步器包含两个节点类型的引用,一个指向头结点,一个指向尾结点
  • 一个线程成功获取锁后,其他线程无法获得锁而进入同步队列,这个操作必须保证线程安全,利用CAS加入尾结点
  • 首节点完成任务后释放锁并且唤醒后继节点,因为只有一个线程能够获得锁,所以不需要使用CAS

3.2 独占式同步状态获取和释放

① acquire( int arg)

获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • 首先调用自己重写的tryAcquire(arg) 方法,保证线程安全的获取状态并且修改为同步状态
  • 如果失败就构造节点(独占式Node.EXCLUSIVE) 通过addWaiter 加入到队列尾部
  • 最后调用acquireQueued () 方法使得节点以死循环方式获取同步住哪个台,获取不到则阻塞节点保存的线程。

addWaiter (Node node)

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 备份tail节点引用
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 利用CAS 设置尾结点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 不成功进入enq
        enq(node);
        return node;
    }

enq(node)

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // 只有通过CAS成功添加节点才会退出
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环,不断自旋,获取同步状态才可以退出
            for (;;) {
            	// 获取node 的前驱节点
                final Node p = node.predecessor();
                // 只有前驱节点是头结点,才能尝试获取同步状态
                if (p == head && tryAcquire(arg)) {
                	// 将当前节点设置为头结点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire 将前驱节点状态置为 -1 用于标记谁来唤醒该被阻塞地节点
                // 如果 shouldParkAfterFailedAcquire 返回true 则进入parkAndCheckInterrupt 节点被阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
② release(int arg)

当前线程获取同步状态并执行相应逻辑之后,需要释放同步状态,使得后续节点能够继续获取同步状态。

    public final boolean release(int arg) {
    	// 调用自己重写的tryRelease(arg) 方法
        if (tryRelease(arg)) {
            Node h = head;
            // 唤醒处于等待的线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

3.3 共享式同步状态获取和释放

共享式获取与独占式获取最重要的区别在于同一时刻是否能有多个线程同时获取到同步状态。
例如:读文件是共享式,而写文件是独占式

① acquireShared(int arg)
    public final void acquireShared(int arg) {
    	// 调用自定义的tryAcquireShared(arg) 方法,返回值大于0 则获取同步状态
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }


    private void doAcquireShared(int arg) {
    	// 构造SHARED 共享式节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
            	// 获得前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是头结点,则尝试获取同步状态
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
② acquireShared(int arg)
    public final boolean releaseShared(int arg) {
    	// 调用自己重写的tryReleaseShared(arg) 方法尝试释放同步状态
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                	// 必须确保同步状态的线程安全释放,因为释放同步操作的线程
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

3.4 独占式超时获取同步状态

通过调用同步器的doAcquireNanos(int arg, long nanosTimeout) 方法,可以超时获取同步状态,提供了synchronized所不具备的特性

实现逻辑和独占式获取同步状态大体相同,只是在获取失败时会判断是否超时

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

4. LockSupport.park()

park 方法是一个很重要的方法,例如,在 AQS 中对线程的阻塞,例如在线程池中对线程的阻塞,包括阻塞队列等等,都会使用 LockSupport.park() 或者 unpark 来进行阻塞和解除。

    public static void park() {
        UNSAFE.park(false, 0L);
    }
public native void park(boolean var1, long var2);

park 方法通过 UNSAFE 调用,是一个本地方法,会阻塞当前调用的线程,在如下四种情况发生的时候才会返回:

  • 别的线程调用 unpark 方法,将被阻塞的线程唤醒,unpark 方法可以传入一个参数,也就是需要唤醒的线程,并且可以先 unpark 再 park。
  • 线程被中断。
  • park 方法指定阻塞的时间到期。
  • 发生异常现象,和线程中断类似。

4.1 实现原理

park 方法在不同的操作系统有不同的实现方式,就拿 Linux 的来做例子。

每个 Java 对象在操作系统会对应一个 Parker 对象,其中包含一个 _counter 属性,也就是类似于信号量,来对是否阻塞进行标识。

class Parker : public os::PlatformParker {
private:
    volatile int _counter ;
    //...
public:
    void park(bool isAbsolute, jlong time);
    void unpark();
    //...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    //...
}

对于 park 方法,不难看出,就是使用自旋 for(;😉 和 cmpxchg(CAS的指令) 来置换状态。

可以通过 _mutex 来进行状态的变更,如果大于 0,则可以不阻塞执行,如果小于 0,则需要被阻塞并放入 _cond 中。

void os::PlatformEvent::park() {
    int v ;
    for (;;) {
        v = _Event ;
        if (Atomic::cmpxchg (v- 1 , &_Event, v) == v) break ;
    }
    guarantee (v >= 0 , "invariant" ) ;
    if (v == 0 ) {
        // Do this the hard way by blocking ...
        int status = pthread_mutex_lock(_mutex);
        assert_status(status == 0 , status, "mutex_lock" );
        guarantee (_nParked == 0 , "invariant" ) ;
        ++ _nParked ;
        while (_Event < 0 ) {
            status = pthread_cond_wait(_cond, _mutex);
            // for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
            // Treat this the same as if the wait was interrupted
            if (status == ETIME) { status = EINTR; }
            assert_status(status == 0 || status == EINTR, status, "cond_wait" );
        }
        -- _nParked ;

        // In theory we could move the ST of 0 into _Event past the unlock(),
        // but then we'd need a MEMBAR after the ST.
        _Event = 0 ;
        status = pthread_mutex_unlock(_mutex);
        assert_status(status == 0 , status, "mutex_unlock" );
    }
    guarantee (_Event >= 0 , "invariant" ) ;
}

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值