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

详细分析如何利用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自旋,保证了系统更大的吞吐量,但是可能操作线程长时间不被响应出现饥饿现象。

已标记关键词 清除标记
课程简介: 历经半个多月的时间,Debug亲自撸的 “企业员工角色权限管理平台” 终于完成了。正如字面意思,本课程讲解的是一个真正意义上的、企业级的项目实战,主要介绍了企业级应用系统中后端应用权限的管理,其中主要涵盖了六大核心业务模块、十几张数据库表。 其中的核心业务模块主要包括用户模块、部门模块、岗位模块、角色模块、菜单模块和系统日志模块;与此同时,Debug还亲自撸了额外的附属模块,包括字典管理模块、商品分类模块以及考勤管理模块等等,主要是为了更好地巩固相应的技术栈以及企业应用系统业务模块的开发流程! 核心技术栈列表: 值得介绍的是,本课程在技术栈层面涵盖了前端和后端的大部分常用技术,包括Spring Boot、Spring MVC、Mybatis、Mybatis-Plus、Shiro(身份认证与资源授权跟会话等等)、Spring AOP、防止XSS攻击、防止SQL注入攻击、过滤器Filter、验证码Kaptcha、热部署插件Devtools、POI、Vue、LayUI、ElementUI、JQuery、HTML、Bootstrap、Freemarker、一键打包部署运行工具Wagon等等,如下图所示: 课程内容与收益: 总的来说,本课程是一门具有很强实践性质的“项目实战”课程,即“企业应用员工角色权限管理平台”,主要介绍了当前企业级应用系统中员工、部门、岗位、角色、权限、菜单以及其他实体模块的管理;其中,还重点讲解了如何基于Shiro的资源授权实现员工-角色-操作权限、员工-角色-数据权限的管理;在课程的最后,还介绍了如何实现一键打包上传部署运行项目等等。如下图所示为本权限管理平台的数据库设计图: 以下为项目整体的运行效果截图: 值得一提的是,在本课程中,Debug也向各位小伙伴介绍了如何在企业级应用系统业务模块的开发中,前端到后端再到数据库,最后再到服务器的上线部署运行等流程,如下图所示:
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页