从ReentrantLock源码分析,理解AQS应用

一、Lock类

Lock简介

在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

Lock的实现

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁应该具有不同实现。
我们看java.util.concurrent.locks包下,就提供了几种锁:
ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数。
ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

Lock的类关系图

在这里插入图片描述

二、ReentrantLock 重入锁

重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized 和 ReentrantLock 都是可重入锁。

重入锁的设计目的

比如下面这个示例:

public class LockDemo {
    
    private static Lock lock = new ReentrantLock();
    
    public void demo01(){
        try {
            lock.lock();
            demo02();
        }finally {
            lock.unlock();
        }
    }

    public void demo02(){
        try {
            lock.lock();
            System.out.println("测试重入锁");
        }finally {
            lock.unlock();
        }
    }
}

假设不允许重入,调用 demo01 方法获得了当前的对象锁,然后在这个方法中再去调用
demo02,demo02 中的存在同一个锁,这个时候当前线程会因为无法获得demo02 的锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。

三、ReentrantLock 的实现原理

从锁的作用我们可以知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。那么在 ReentrantLock 中,也一定会存在这样的需要去解决的问题。但是在多线程竞争重入锁时,竞争失败的线程是存放在哪儿呢,又是如何实现阻塞以及被唤醒的呢?

AQS登场

线程是放在AQS队列里面的,关于AQS队列,前面有博文进行了介绍和源码解读,可参考理解,这里不再赘述。

Sync类

我们平时加锁都是通过lock()方法进行加锁的,我们具体看一下lock方法是怎么实现的,源码如下:

   public void lock() {
        sync.lock();
    }

我们看到是通过sync.lock()方法实现的,那么sync又是什么呢?通过ReentrantLock源码我们可以看到如下:

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
    	// 抽象的lock方法
        abstract void lock();
		...
    }

在ReentrantLock类中有一个Sync抽象内部类,而且这个内部类继承了AbstractQueuedSynchronizer(AQS)。

公平锁和非公平锁(FairSync/NonfairSync )

Sync里面定义了一个抽象的lock()方法,抽象方法肯定有具体的实现,我们可以发现,源码中还有两个内部类继承了Sync,实现了lock()方法,代码如下

	/**
     * Sync object for non-fair locks
     */
     // 非公平锁
    static final class NonfairSync extends Sync {

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        // 非公平锁获取锁方法
		protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
     // 公平锁
    static final class FairSync extends Sync {

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

什么是公平锁、非公平锁?
其实很简单,锁的公平性是相对于获取锁的顺序而言的。公平锁就是意味着先来的线程先得到锁,后来的线程后得到锁,非公平锁就是后来的线程可能先得到锁。
我们从代码来看他们的差异,在FairSync和NonfairSync类中都有一个lock()和tryAcquire()方法,如上面的代码,看一下这个两个方法的实现的差异
在这里插入图片描述
非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会
在这里插入图片描述
公平锁在判断条件多了hasQueuedPredecessors()方法,也就是加入了[同步队列中当前节点是否有前驱节点]的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁
下面我们以非公平锁的实现来解读ReentrantLock源码

加锁过程

lock方法

        final void lock() {
        	// 先通过CAS尝试去加锁,我们可以看到,加锁其实就是把state字段从0改成1
            if (compareAndSetState(0, 1))
            	// CAS加锁成功,把当前线程设置为独占状态
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 如果加锁不成功,就调用AQS中的acquire()方法去竞争锁
                acquire(1);
        }

acquire方法
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续 acquire(1)操作
➢ 大家思考一下,acquire 方法中的 1 的参数是用来做什么呢?
这个方法的主要逻辑是

  1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
  3. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。
public final void acquire(int arg) {
	
        if (!tryAcquire(arg) &&   // 先尝试去竞争锁
        	// 如果没有竞争到锁,就先添加到等待队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果线程被中断了,在这一步进行中断的处理
            // 就是响应parkAndCheckInterrupt返回的interrupted状态
            selfInterrupt();
    }

tryAcquire()方法
这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false
它是重写 AQS 类中的 tryAcquire 方法,并且大家仔细看一下 AQS 中 tryAcquire方法的定义,并没有实现,而是抛出异常。按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成 abstract,让子类来实现呀?大家可以想想为什么

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

nonfairTryAcquire方法

  1. 获取当前线程,判断当前的锁的状态
  2. 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值
  3. 当前线程是属于重入,则增加重入次数
		final boolean nonfairTryAcquire(int acquires) {
			// 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取锁状态
            int c = getState();
            // c==0表示没有加锁
            if (c == 0) {
            	//cas 替换 state 的值,cas 成功表示获取锁成功
                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;
        }

addWaiter方法
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能

  1. 将当前线程封装成 Node
  2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
  3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
	private Node addWaiter(Node mode) {
		// 将当前线程封装成node,并且设置mode为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // 将tail赋值给pred变量
        Node pred = tail;
        // 如果pred不为null,表示当前队列里面有等待线程,下面操作就是把当前这个新的node节点放到队列最后
        if (pred != null) {
        	// 将当前节点的prev指针指向pred
            node.prev = pred;
            // 通过CAS设置tail指向当前的node
            if (compareAndSetTail(pred, node)) {
            	// pred的next指针指向node
                pred.next = node;
                // 返回当前节点
                return node;
            }
        }
        // 如果pred是null,表示当前队列未初始化,则通过enq方法初始化队列,并把node添加到队列,这个方法在AQS文章中分析过,不再多说
        enq(node);
         // 返回当前节点
        return node;
    }

假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter方法结束后,AQS 中的链表结构图
在这里插入图片描述
acquireQueued方法
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁

  1. 获取当前节点的 prev 节点
  2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
  3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
  4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
  5. 最后,通过 cancelAcquire 取消获得锁的操作
	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        	// 设置线程中断状态
            boolean interrupted = false;
            // 自旋
            for (;;) {
            	// 获取当前节点的前一个节点
                final Node p = node.predecessor();
                // 如果前一个节点是head节点,就再去竞争锁,如果tryAcquire返回false,则线程会在当前位置挂起,获得锁后,会继续从这里执行
                if (p == head && tryAcquire(arg)) {
                	// 如果获取到锁,把当前节点设置为head节点,setHead方法里面会把当前节点的prev指针指向null,
                	// private void setHead(Node node) {
				    //    head = node;
				    //    node.thread = null;
				    //    node.prev = null;
				    // }
                    setHead(node);
                    // 把原来的head节点的next指针指向null,到这一步,将相当于把原来的节点从队列中删除了
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // ThreadA 可能还没释放锁,使得ThreadB 在执行 tryAcquire 时会返回false;shouldParkAfterFailedAcquire从方法名称可以看出,意思是获取锁失败应该阻塞;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 如果shouldParkAfterFailedAcquire反回true,就挂起当前线程,并且判断是否被中断,
                    // 如果parkAndCheckInterrupt反回true,表示线程被中断过,则interrupted=true,抛到上层去响应中断
                    interrupted = true;
            }
        } finally {
            if (failed)
            	// 回滚
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire方法
如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire 方法
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。

  1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false.
    返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt挂起当前线程
	// pred  表示当前节点的prev节点
	// node 当前节点
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		// 获取前置节点的状态
        int ws = pred.waitStatus;
        // 如果前置节点的状态是SIGNAL,则把当前线程挂起就好
        if (ws == Node.SIGNAL)
            return true;
         // 获取前置节点的状态>0,表示是Cance状态,此时线程是超时(timeOut)状态,是无效的,永远不会被唤醒了,
         // static final int CANCELLED =  1;
         // static final int SIGNAL    = -1;
         // static final int CONDITION = -2;
         // static final int PROPAGATE = -3;
         //ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
        if (ws > 0) {
        // 前驱节点的ws > 0,说明ws = Cancelled,表示前驱线程被取消,从前驱节点继续往前遍历,从双向列表中移除 CANCELLED 的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { // 这种情况表示前驱节点的 ws = 0 或者 ws = PROPAGATE,不能挂起线程,更改状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt方法
使用 LockSupport.park 挂起当前线程编程 WATING 状态
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。

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

通过 acquireQueued 方法来竞争锁,如果 ThreadA 还在执行中没有释放锁的话,意味着 ThreadB 和 ThreadC 只能挂起了。
在这里插入图片描述

释放锁过程

unlock()方法

	public void unlock() {
		// 调用AQS的release方法
        sync.release(1);
    }

release方法

public final boolean release(int arg) {
		// 尝试释放锁
        if (tryRelease(arg)) {
        	// 释放锁成功,将head节点赋值给h
            Node h = head;
            // 如果h
            if (h != null && h.waitStatus != 0)
            	// 唤醒节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease方法

	protected final boolean tryRelease(int releases) {
			// 获取锁状态,然后减去unlock次数,因为是重入锁,所以state可以是>1的,所得的c表示锁的重入次数
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            // 释放锁成功标志默认设置为false
            boolean free = false;
            // 判断c==0,当为0时,表示释放锁
            if (c == 0) {
            	// 释放锁成功标志设置为true
                free = true;
                // 设置独占线程(占有锁的线程)为null,以方便其他线程获得锁
                setExclusiveOwnerThread(null);
            }
            // 设置锁状态(锁的重入次数)
            setState(c);
            // 返回释放锁成功标识
            return free;
        }

unparkSuccessor方法

// 注意此时参数node是head节点
private void unparkSuccessor(Node node) {
         // 获取当前node的状态,
        int ws = node.waitStatus;
        // ws<0,表示是处于signal状态,是需要被被唤醒的
        if (ws < 0)
        	// 通过CAS尝试更改锁状态
            compareAndSetWaitStatus(node, ws, 0);
        // 
        Node s = node.next;
        // 如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
        // 否则直接跳过
        if (s == null || s.waitStatus > 0) {
        	// 表示waitStatus=0,没有处于等待状态
            s = null;
            // 通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	// unpark是用来唤醒指定线程
            LockSupport.unpark(s.thread);
    }

通过锁的释放,原本的结构就发生了一些变化。head 节点的 waitStatus 变成了 0,ThreadB 被唤醒
在这里插入图片描述

原本挂起的线程会继续执行

通过 ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行,应该从哪里执行大家还有印象吧。 原来被挂起的线程是在 acquireQueued 方法中,所以被唤醒以后继续从这个方法开始执行

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                final Node p = node.predecessor();
                // 如果前一个节点是head节点,就再去竞争锁,如果tryAcquire返回false,则线程会在当前位置挂起,获得锁后,会继续从这里执行
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) 
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。
由于 ThreadB 的 prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这个时候调用 tryAcquire 方法时,可以顺利获取到锁

  1. 把 ThreadB 节点当成 head
  2. 把原 head 节点的 next 节点指向为 nul
    在这里插入图片描述

四、总结

整个加锁和释放锁的过程可以抽象为下面一个流程图
在这里插入图片描述

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:1354518382@qq.com 博客地址:https://blog.csdn.net/qq_35576976/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值