java-通过AQS理解独占锁的实现机制

9 篇文章 0 订阅

详细分析如何利用AQS实现独占锁的获取与释放

独占式可重入非公平锁的获取

独占锁默认就是非公平锁,我们要想了解独占锁的获取是否,首先必须知道同步队列为一个有头尾节点的双向链表
1.通过new ReentrantLock().lock();进入lock的的代码实现区

public void lock() {
    sync.lock();
    }
    ......
    sync = new NonfairSync();

2.再进入sync.lock()查看具体的lock()上锁的实现

final void lock() {
    if (compareAndSetState(0, 1))        
    	setExclusiveOwnerThread(Thread.currentThread());    
    else        
    	acquire(1);
    }

3.首先就进行了一次CAS比较交换操作,尝试获取锁。我们进入AQS查看CAS的实现机制

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this    
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

4.通过底层的本地方式进行比较交换如果比较交换成功,返回true则直接进入AQS将当前锁的持有线程改为当前请求的线程。也就是完成了请求锁的这个线程拿到了锁。

protected final void setExclusiveOwnerThread(Thread thread) {    
exclusiveOwnerThread = thread;
}

5.如果CAS比较交换失败返回false则进入AQS提供的acquire(1)尝试再次获取锁。

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

6.首先进入被ReentrantLock中的sync重写的tryAcquire操作。尝试再一次的获取锁。因为lock时可能有多个线程通过CAS比较操作,但是只能有一个成功,其他的线程就是进入acquire进入tryAcquire再次尝试获取锁。

protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();//拿到当前线程
    int c = getState();//获取锁状态
    if (c == 0) {//当前锁没有被任何线程所持有
        if (compareAndSetState(0, acquires)) {//再次尝试比较交换来获取锁
       //比较交换成功
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //当前线程状态不为0或者比较交换失败
    else if (current == getExclusiveOwnerThread()) {//判断当前线程是不是锁的持有线程
    //如果是则满足可重入操作,nextc计时器+1这里的nextc类似于内建锁的monitor
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//更新状态
        return true;
    }
    return false;
}

7.重新判断锁状态尝试获取锁,判断是否满足可重入状态,则成功返回true条件就是当前锁未被获取比较交换成功,或者当前锁的持有线程是请求锁线程满足可重入则返回true。否则返回false回到acquie函数中。

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

8.如果tryAcquie返回true则if条件为false无需进行acquireQueued操作,直接返回。因为已经获取到锁了嘛。如果tryAcquire返回false怎进入下一个判断acquireQueue,但是我们发现参数就是一个方法,所以要先进入addWaiter操作。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);//将当前线程包装成一个节点利用Node的重载构造,由上可知mode==null
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;//获取尾节点
    if (pred != null) {//如果当前有尾节点则尾插将尾节点和node节点相连接
        node.prev = pred;//node前驱指向尾节点
        if (compareAndSetTail(pred, node)) {//利用如下给出的方法调用本地方法完成比较尾插操作。
            pred.next = node;//尾节点的next指向node节点
            return node;//尾插成功,将当前包装好的线程节点返回给上一次调用
        }
    }
    //走到这说明尾节点为null及没有尾节点或者说尾插失败。
    enq(node);
    return node;
}
Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

9.如果队列有值并且尾插成功则直接返回上一层。否则进入enq方法。

private Node enq(final Node node) {
    for (;;) {
    //哎呦看到没死循环哦,这里的死循环就是自旋啦。
        Node t = tail;
        if (t == null) { // Must initialize
        //走到这说明没有尾节点为null,也就是addwaiter中失败的第一个条件,也可以理解为队列为空一个值都没有则初始化队列
            if (compareAndSetHead(new Node()))//同样调用的native本地方法进行的初始化队列将node直接设置为头节点。所以这里就不打开展示了。
                tail = head;//因为刚初始化的队列也只有node一个节点所以尾节点也指向node
        } else {
        //addwaiter失败的第二个第二个条件队列不为空但是尾插失败。可能是多个线程同时请求尾插,但是肯定只有一个线程成功对吧,所以其他的线程jiu'hui就会到这里来了。死循环反复尝试尾插直到成功为止。
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

10.这里enq肯定会尾插成功返回的,不成功根本不返回的。回到上一侧addwaiter直接return node。由回到上一层。这时候acquireQueued的参数node,arg就全了可以进入了。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//首先设置初始状态
    try {
        boolean interrupted = false;
        for (;;) {
        //又一个死循环哦
            final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了报错即可。
            if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
            //尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
                setHead(node);//将node节点中的值清零,将这个节点设置为头节点
                p.next = null; // help GC
                failed = false;
                return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
            }
            //走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

11.走到这里说明虽然把node节点尾插进入同步队列了,但是不是同步队列中第一个啊,或者说虽然是但是获取锁失败了。则到了shouldParkAfterFailedAcquire(p, node) 方法,判断该节点是否为阻塞状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//获取node前驱节点的状态
    if (ws == Node.SIGNAL)//SIGNAL表示当前节点的后继节点为阻塞状态
       return true;
    if (ws > 0) {
    //说明前驱节点为1,也就是这个节点已经为取消状态,可以取消了。那么就do,一直往前找直到找到一个前驱节点状态不为1,并将其设置为node的前驱节点。
     do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    //因为独占锁的同步队列中不可能出现状态值为-2,-3状态所以走到这里说明找到了一个前驱节点不为1的节点。
     compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//这里同样调用本地方法将该节点的前驱节点状态设置为SIGNAL
    }
    return false;//返回false表示不需要将该节点阻塞
}

12.通过shouldParkAfterFailedAcquire(p, node)将node节点的前驱节点设置为SIGNAL状态,表示该阻塞,我们回看acquireQueued方法。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//首先设置初始状态
    try {
        boolean interrupted = false;
        for (;;) {
        //又一个死循环哦
            final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了报错即可。
            if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
            //尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
                setHead(node);//将node节点中的值清零,将这个节点设置为头节点
                p.next = null; // help GC
                failed = false;
                return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
            }
            //走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

13.shouldParkAfterFailedAcquire我们如果我们是一上来碰见的前驱节点就是SIGNAL则返回的ture,则进行park操作,如果不是一上来碰上的SIGNAL而是我们设置的SIGNAL我们就继续死循环再次判断是不是前驱是不是头节点,因为如果前驱为头节点那么该节点虽然为阻塞状态也该被唤醒了。如果前驱不是头节点会再次进入shouldPark操作。这是就是一上来碰见SIGNAL了就可以进行park操作了。通过这个false避免了将同步队列中的第一个节点状态。返回true进入parkAndCheckInterrupt。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//将当前线程阻塞
    //如果这里外面来了一个中断呢?可是我现在已经是阻塞了,类比sleep操作,都会有一个把中断重置为false的操作。
    return Thread.interrupted();//将重置中断状态,也就是将中断状态设置为false,如果真的有中断则interrupted返回true。
}
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);//设置阻断器
    UNSAFE.park(false, 0L);//调用本地方法来实际阻塞该线程
    setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

14.设置完中断状态返回上一层,执着的尝试获取锁,直到获取到锁为止返回。自旋着一直等到是第一个线程可以获取锁了为止。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//首先设置初始状态
    try {
        boolean interrupted = false;
        for (;;) {
        //又一个死循环哦
            final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了,报错进入finally块。
            if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
            //尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
                setHead(node);//将node节点中的值清零,将这个节点设置为头节点
                p.next = null; // help GC
                failed = false;
                return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
            }
            //走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //进来说明在我将线程的阻塞的过程中有了一个中断信号。
                interrupted = true;//将中断状态这是为true
        }
    } finally {
        if (failed)//如果是正常return进来的failed肯定为false,只有p==null也就是出错的前提下才会进入cancelAcquire。
            cancelAcquire(node);//同步队列出错删除节点处理
    }
}

15.直到称为第一个节点获取到锁成功return返回上一层,如果acquireQueue返回true表示阻塞过。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
static void selfInterrupt() {
    Thread.currentThread().interrupt();//设置中断状态,也就是自己产生一个中断
}
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();
    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();          
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

16.selfInterrupt()的代码很简单,就是“当前线程”自己产生一个中断。但是,为什么需要这么做呢?
我们在parkAndCheckInterrupt()中将线程阻塞并且判断以下现在有没有中断状态,如果遇到了中断状态先无视毕竟我现在是阻塞状态对吧,并把线程有过中断信号这件事返回给acquireQueued。获取到了acquireQueued判断了以下返回了一个中断过的信息,于是将interrputer变量改为true就是中断过啦。并在这个线程获取到锁后将这个是否中断过的信息返回给了acquire。
虽然你获取到了锁了,还拦截了中断避免了阻塞和中断碰撞的异常报错,但是不可否认你被中断的事实啊。所以线程在acquire重新将中断设回了true。这样,我可以在获取锁后,手动继续判断中断信号。
举个应用例子:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test{
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            Thread thread = new Thread(()->{
                try {
                    lock.lock();
                    if(Thread.interrupted()==false) {
                        System.out.println(Thread.currentThread().getName() + "运行呢");
                    }
                }
                catch (Exception e) {

                }finally {
                    System.out.println(Thread.currentThread().getName()+"释放锁哦");
                    lock.unlock();
                }
            });
            thread.start();
            thread.interrupt();
        }
    }
}

运行结果,运行呢这句话是不会被执行的。因为虽然lock获取锁的时候拦截了中断信号,但是最后又给设置回去了。所以外部依然可以在获取着锁的前提下选择中断线程,当然锁还是会释放的哦。
下面给出一个详细的流程图再梳理一下思路吧
在这里插入图片描述

独占式可重入非公平锁的释放

1.lock.unlock()首先调用lock提供的unlock方法。当然这个方法是被ReentrantLock覆写过的哦。

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

2.进入release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3.首先调用ReentrantLock覆写的tryRelease方法尝试释放锁。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//获取当前锁的状态并-1,参数穿的1,这里要和lock对应当时+的多少,现在就得-多少
      if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();//如果当前线程不是持有锁的线程,那咋释放锁啊。抛异常啊
    boolean free = false;
    if (c == 0) {//因为锁的可重入性所以c不一定等于0,也就是该锁的持有线程不一定能改回null。只有当是最后一层加锁退出,c剪为0才可以将锁释放并且返回true,并将持有锁线程设置为null。
        free = true;
        setExclusiveOwnerThread(null);
    }
//修改状态,无论是否释放锁都要这个线程成功释放了一层锁就要修改状态。
    setState(c);
    return free;
}

4.返回上一层,如果try返回的false表示还没有完全释放锁。所以不进入if。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
    //进入表示该线程已经释放锁了
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //如果队列不为空,并且队列头节点的状态为-1,你可能会好奇为啥是-1,!=0不是还会有别的状态嘛。如果是取消状态,都删了,0状态表示后继没有节点,如果有节点尝试获取锁的时候已经将进程设置为-1了。
            unparkSuccessor(h);
        return true;
    }
    return false;
}

5.如果同步队列初始化了,并且还有节点需要被唤醒调用unparkSuccessor()

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;//获取头节点的节点状态
    if (ws < 0)//如果头节点状态为-1,表示后继节点阻塞在同步队列等待被通知呢。
        compareAndSetWaitStatus(node, ws, 0);  //调用本地方法将头节点的状态设置为0
    Node s = node.next;//获取头节点后下一个节点
    if (s == null || s.waitStatus > 0) {//如果这个节点被阻塞着但是状态为1也就是要申请从同步队列中取消。那么肯定不能唤醒这个线程了。取消该线程
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)//从尾往前找,直到找到node的下一个状态不为1的线程,或者找到了null的前一个不为1的节点
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)//说明至少找到了一个状态不为取消状态的节点,调用本地方法将该线程唤醒。
        LockSupport.unpark(s.thread);
}
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

同样配上我自己分析的实际执行步骤

在这里插入图片描述

独占式获取锁释放锁的总结

1.线程获取锁失败,将线程调用addwaiter方法封装成node节点,进行尾插入队操作。在addwaiter中,方法enq完成对同步队列的头节点初始化,以及CAS尾插失败后的重试操作。
2.入队之后,排队获取锁的核心方法aquireQueue,节点排队获取锁是一个自旋过程。当且仅当,当前节点的前驱节点为头节点,并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。
否则不满足条件时,会不断自旋将前驱节点的状态设置为SIGNAL,而后调用LockSupport.part()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为空)

其他的独占锁类比获取释放过程

1.响应中断的获取锁
acquire方法是一个不响应中断的方法。
acquireSharedInterruptibly获取锁相应中断,原理与acquire几乎一样。唯一区别在于parkAndCheckInterrupt()返回true表示线程阻塞时被遇到了中断信号,抛出中断异常后线程退出。
2.超时等待功能获取锁
超时等待获取锁tryAcquireNanos(),该方法在三种情况下会返回,
1.在超时时间内,当前线程成功获取到锁。
2.当前线程在超时时间内被中断。
3.超时时间结束,仍然未获取到锁,线程退出,返回false;
超时获取锁逻辑于可中断获取锁基本一致,唯一区别在于获取锁失败后,增加了一个时间处理,如果当前时间超过截至时间,线程不再等待,直接退出,返回false,否则将线程阻塞,置为等待状态排队获取锁。

重入锁的理解

重入:表示能够对共享资源重复加锁,即当前线程再次获取锁时不会被阻塞。
通过在获取锁过程tryAcquire中当同步状态不为0表示同步状态已经被线程获取。判断获取锁的线程是否为当前请求锁的线程,如果是同步状态计数器+1,返回true成功获取锁。表示持有线程重复进入同步代码块。
通过释放过程先将同步状态剪为0并且判断当前申请释放的线程是否为持有锁的线程,来表示锁成功被释放。

公平锁和非公平锁对比

1.非公平锁在lock中一上来就有一次CAS。而公平锁直接就是tryAcquire
2.非公平锁在tryAcquire中一上来就又一次CAS,而公平锁则是先判断以下同步队列是否为空,为空才进行CAS操作。
同上上述操作,公平锁保证如果同步队列中有节点就尾插,每次被唤醒都是同步队列中等待时间最长的节点。从而避免了线程等待时间过长饿死的情况,抱枕了请求资源顺序的绝对性,但是需要频繁的进行同步队列变量,频繁的方法切换,性能差,效率低。
通过上述操作,非公屏锁因为多了两次CAS自旋,保证了系统更大的吞吐量,但是可能操作线程长时间不被响应出现饥饿现象。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值