线程安全 -- AQS同步锁(ReentrantLock实现)

前言

AQS是JUC包中可扩展的一个同步器工具,可以通过继承AQS去实现公平锁、非公平锁,等待队列以及同步队列。由于等待队列是在锁下进行入队,所以无需考虑线程安全

单个线程或者说少量线程交替并发执行下,lock锁很可能与同步队列并无关系,除非是IO操作,正常程序执行的时间都非常短。只有并发量非常大或者共享资源执行时间比较长,才会有线程进入同步队列,进行OS操作系统层面的阻塞和唤醒操作

类图

//todo

源码分析

首先我们需要理解,锁的机制,AQS是基于自旋+CAS操作来保证数据的一致性,里面有一个Node内部类用来存储线程节点,其中等待队列就是基于Node的单链表,同步队列是基于Node的双链表

static final class Node {
        static final Node SHARED = new Node();  // 共享节点,CountDownLatch、Semaphore
        static final Node EXCLUSIVE = null; // 排他节点

        static final int CANCELLED =  1; // 线程取消
        static final int SIGNAL    = -1; // 线程待唤醒
        static final int CONDITION = -2; // 线程阻塞等待
        static final int PROPAGATE = -3; // 共享锁状态传播
        volatile int waitStatus; // 对应上述四种+初始化0的等待状态

        volatile Node prev; // 前置节点
        volatile Node next; // 后置节点
        volatile Thread thread; // 链表节点存储的线程
        
        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

一、ReentrantLock

有个Sync静态抽象内部类继承了AQS

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        // 重写公平和非公平锁实现
        abstract void lock();

        // 尝试获取非公平锁
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

		// 锁的释放
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases; // state代表的是重入次数
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) { // 代表锁已经完全释放,将同步队列中的线程设置为null
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

默认是非公平锁,所谓的公平和非公平在于,当释放锁的临界点,是从同步队列中按序抢锁(先到先得),还是随机抢占(谁快谁来)

1、公平锁

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); // 获取重入次数
            if (c == 0) {
                if (!hasQueuedPredecessors() &&     // 判断当前线程是否为头节点
                    compareAndSetState(0, acquires)) { // 判断当前是否无锁状态
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 若锁已被当前线程占有,则进行重入操作,因为已处于锁状态,故无需考虑线程安全问题
            else if (current == getExclusiveOwnerThread()) { 
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

公平锁首先会进入到AQS中acquire方法,这里会先判断tryAcquire尝试获取锁是否成功,成功则结束。后续进来的线程则开始排队抢占…

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

若抢占失败则进入到同步队列中,把当前线程设置为互斥状态。
首先判断尾节点是否为空,若尾节点不为空,则通过CAS将当前线程节点插入到尾节点;

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); // 后续唤醒的时候需要得到被唤醒线程
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { // 抢占进入同步队列,参与抢占锁的一员
            pred.next = node;
            return node;
        }
    }
    enq(node);  // 没抢占进入到同步队列,或节点未初始化,则进行初始化和自旋抢占入队操作
    return 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;
	        if (compareAndSetTail(t, node)) {
	            t.next = node;
	            return t;
	        }
	    }
	}
}

进入同步队列之后,开始自旋去获取当前节点的前置节点(动态变化的)
若前置节点为头节点,则尝试去抢占锁,抢占成功,则从同步队列中移除;抢占失败则尝试去park阻塞
若前置节点非头节点,同样也会去尝试park阻塞

final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
       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)
           cancelAcquire(node);
   }
}

在第一次抢占锁失败后,会尝试将waitStatus从初始化状态0改成SIGNAL状态,修改失败则会继续自旋尝试去获取锁,获取锁失败再去尝试修改waitStatus状态

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

waitStatus状态成功修改为SIGNAL状态之后,就会对当前线程进行阻塞操作

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

至此就完成了AQS同步锁的抢占流程,接着再思考下锁如何进行释放🤔
首先猜想:
1、通过互斥变量state CAS操作进行出锁操作,考虑到Lock的可重入性,每次state-1
2、抢占到锁的线程肯定需要释放,把当前锁对象的线程set为null,接着就是去判断头节点是否为Signal状态,再去唤醒头节点去进行新一轮的锁抢占

大概就是这样了,接着再瞅瞅源码。可以看到对于公平锁和非公平锁释放锁的逻辑都是一样的

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空,代表同步队列中有节点;
        // 头节点等待状态不为0,代表非初始化自旋状态。若是自旋状态直接CAS抢占就完事了
        if (h != null && h.waitStatus != 0) 
            unparkSuccessor(h);
        return true;
    }
    return false;
}

重点看下tryRelease,正如猜想的一样,首先对于state进行-1表示出锁一次的操作,当state减少到0的时候,代表锁已经完全释放了,这时候我们就需要对当前锁对象中的线程清空,并返回free = true,表示当前锁对象自由了。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不是获取锁对象的线程,抛异常
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

接着就是唤醒头节点的环节了,waitStatus < 0表示头节点非Cancel取消状态(在之前if判断中已经判断waitStatus !=0),尝试将状态CAS修改成初始化状态,然后重新去参与自旋抢占锁;
若头节点的下一个节点为空或者waitStatus > 0,则从后往前去唤醒同步队列中第一个可执行的节点;
最后当这个可执行的节点不为空,唤醒它!

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

2、非公平锁

相对于公平锁,非公平锁多了两次抢占的机会,线程刚开始进来的时候,就会进行一次CAS比较,成功则抢占

final void lock() {
	// 进来直接先CAS,true则抢占到锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

第二次抢占:这里和公平锁唯一的不同就是没有了!hasQueuedPredecessors()判断当前线程节点是否为同步队列的头节点或者当前同步队列中没有元素,也就是不管当前线程是否在同步队列中,或是非同步队列头部节点都会尝试抢占一次。成功则把当前锁对象线程设置为当前线程,失败则老老实实进入同步队列中自旋去抢占

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

二、Condition

待输出

总结

在ReentrantLock中包含了4个CAS操作:
1、setState
2、setWaitStatus
3、setHead
4、setTail

AQS是JUC并发基础中很重要的一个模块,代码很简单,但是里面包含了很多思想值得学习和借鉴。包括state全局互斥变量,for(;😉 + CAS的自旋避免OS层面的上下文切换,从尾部开始像头部遍历保证线程安全,waitStatus的几种状态流转来实现业务之间的切换

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值