ReentrantLock

ReentrantLock是可重入的互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码。具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法)。

ReentrantLock 和 Synchronized的对比

ReentrantLockSynchronized
锁实现机制依赖AQS监视器模式
灵活性支持响应中断、超时、尝试获取锁不灵活
释放形式必须显示调用unlock()释放锁自动释放监视器
锁类型公平锁&非公平锁非公平锁
条件队列可关联多个条件队列关联一个条件队列
可重入性可重入可重入

ReentrantLock结构组成

在这里插入图片描述

ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync和FairSync是基于Sync扩展的子类,即ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。

锁实现

公平锁与非公平锁

获取锁失败的线程,会进入队列阻塞,占有锁的线程在释放锁时会唤醒队列线程去获取锁,但是此时可能还有新的并发线程来获取锁.
这时就会形成新线程和队列线程竞争锁的局面。

  • 如果不会因为队列线程已经排了很久的队就把锁让给队列线程,这就是非公平锁,
  • 如果为了保证公平一定会让队列线程竞争成功,这就是公平锁,公平锁的性能会差一点

排它锁(x锁):若事务T对数据D加X锁,则其它任何事务都不能再对D加任何类型的锁,直至T释放D上的X锁;一般要求在修改数据前要向该数据加排它锁,所以排它锁又称为写锁。

共享锁(s锁):若事务T对数据D加S锁,则其它事务只能对D加S锁,而不能加x锁,直至T释放D上的S锁;一般要求在读取数据前要向该数据加共享锁,所以共享锁又称为读锁。

ReentrantLock默认为非公平锁,要使用公平锁在构造函数中传入true即可
在这里插入图片描述

加锁操作
源码

在这里插入图片描述

  1. 当我们调用ReentrantLock提供的加锁方法lock(图中B处)时,会调用其内部类Sync的抽象方法lock(图中C处)
  2. Sync.lock()的具体实现是FairSync(公平锁)、NonfairSync(非公平锁)两个子类的lock()方法(图中D1、D2处)
  3. D1、D2处都会调用Sync的父类AbstractQueuedSynchronizer(AQS).acquire()方法(图中E处),tryAcquire()尝试加锁,如果加锁失败会通过addWaiter()方法把当前线程加入队列,然后通过acquireQueued()阻塞
  4. tryAcquire()(图中F处)由子类FairSync、NonfairSync重写(图中G1、G2处,G2调用H2),加锁核心逻辑在G1、H2
非公平锁加锁流程

在这里插入图片描述

核心代码(H2处):

  1. 首先判断当前状态,若 c==0 说明没有线程占用该锁,并在占用锁成功之后将锁指向当前线程;
  2. 如果 c != 0 说明有线程正拥有了该锁,而且若占用该锁就是当前线程(锁重入),则将 state 加 1。
    这段的代码只是简单地++acquires,并修改status值,是因为当前并没有锁竞争,获取锁的本身就是当前线程,所以直接通过setStatus修改state就可以了,不用CAS。
公平锁加锁流程

在这里插入图片描述

核心代码(G1处):
公平锁加锁流程与非公平锁唯一的区别就是在设置state前多了一步操作:判断当前线程是不是队列中被唤醒的线程,如果是就执行CAS,否则获取资源失败

解锁操作
解锁流程

在这里插入图片描述

源码

ReentrantLock.unlock()方法直接调用其内部类sync.release()方法,这个方法是继承自其父类AbstractQueuedSynchronizer

    // java.util.concurrent.locks.ReentrantLock
    public void unlock() {
        sync.release(1);
    }
    
    // java.util.concurrent.locks.AbstractQueuedSynchronizer,子类Sync直接继承
    public final boolean release(int arg) {
        if (tryRelease(arg)) { 
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    // java.util.concurrent.locks.ReentrantLock.Sync
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;                  // 减少重入次数 
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();  // 当前线程不是持有锁的线程,抛出异常
        boolean free = false;
        // 如果持有线程全部释放,将当前独占锁持有线程设置为null,并更新state
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

AbstractQueuedSynchronizer(AQS)

AQS特点

ReentrantLock底层基于AbstractQueuedSynchronizer(AQS)实现,AQS抽象类定义了一套多线程访问共享资源的同步模板,是一个依赖状态(state)的同步器。
解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,简单的说,AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。对应的设计模式就是模版模式
一般通过定义内部类Sync继承AQS,将同步器所有调用都映射到Sync对应的方法;

AQS基于链表实现的双向同步队列,在CLH的基础上进行了变种,CLH是单向队列,其主要特点是自旋检查前驱节点的locked状态。
而AQS同步队列是双向队列,每个节点也有状态waitStatus,而其并不是一直对前驱节点的状态自旋判断,而是自旋一段时间后阻塞让出cpu时间片(上下文切换),等待前驱节点主动唤醒后继节点。

AQS同步队列的head节点是一个空节点,没有记录线程node.thread=null,其后继节点才是实质性的有线程的节点,当最后一个有线程的节点出队列后,不需要想着清空队列,同时下次有新节点入队列也不需要重新实例化队列。
所以当队列为空head=tail=null,第一个线程节点入队列时,需要先初始化

队列节点部分属性:

static final class Node {

        // 共享锁标识
        static final Node SHARED = new Node();
        // 独占锁标识
        static final Node EXCLUSIVE = null;

        // 线程等待状态,初始为0
        volatile int waitStatus;

        // 表示当前结点已取消调度。当超时或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
        static final int CANCELLED =  1;
        // 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。
        static final int SIGNAL    = -1;
        // 表示结点等待在 Condition 上,当其他线程调用了 Condition 的 signal() 方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
        static final int CONDITION = -2;
        // 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
        static final int PROPAGATE = -3;

        // 前驱结点
        volatile Node prev;

        // 后置结点
        volatile Node next;

        // 持有的线程对象
        volatile Thread thread;
        
        Node nextWaiter;
      
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
    }

加锁

当用户加锁时会执行 AbstractQueuedSynchronizer.acquire(int arg)方法,代码如下:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && //尝试获取锁
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE(null) 为独占锁模式, Node.SHARED 为共享锁,所以ReentrantLock为独占锁
            selfInterrupt();
    }
  1. tryAcquire(arg)方法去尝试获取锁,它调用的是其子类的重写方法(G1、G2处)
  2. 若尝试获取锁失败,则调用 AQS.addWaiter(Node.EXLUSIVE) 方法将当前线程放入到队列;
  3. AQS.acquireQueued()方法把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞;

正常入队流程和队列初始化示意:

在这里插入图片描述

addWaiter方法负责把当前无法获得锁的线程包装为一个Node节点添加到队尾:

    private Node addWaiter(Node mode) {            
        // 1.将当前线程构建成Node类型
        Node node = new Node(Thread.currentThread(), mode);
        
        Node pred = tail; 
        if (pred != null) {                         // 2.tail不为null,说明队列中已经有在排队的线程,进行入队操作
            // 3.入队操作
            node.prev = pred;                       // 3.1将新节点前节点设为tail节点
            if (compareAndSetTail(pred, node)) {    // 3.2将新节点通过CAS设置为tail节点,若设置失败跳到步骤7进入enq(),自旋直至成功入队
                pred.next = node;                   // 3.3将原tail节点的next指向新节点(也就是新的tail节点)
                return node;                        // 3.4返回入队成功的节点,传入acquireQueued()进行阻塞
            }
        }
        enq(node);                                  // 4.tail为null,说明队列还未初始化,直接进入enq()进行队列初始化和入队
        return node;
    }

    private Node enq(final Node node) {
        for (;;) { //自旋操作
            Node t = tail;
            if (t == null) {                        // 5.再次检查tail,为null说明队列还未初始化,如果不为null说明在并发情况下已被其他线程抢先初始化,直接进入步骤7
                if (compareAndSetHead(new Node()))  // 6.初始化队列,设置队列head和tail,如果成功然后进入下一轮循环将新节点入队列(步骤7),失败进入下一轮循环回到步骤5重新尝试初始化
                    tail = head;                    //   head和tail都指向同一个空node,随着队列元素越来越多,tail跟着往后移动,head一直指向这个空节点,不代表任何线程
            } else {                                
                // 7.入队操作,与步骤3相同
                // 进入这一步的除了本线程上一轮步骤6初始化成功了或是在并发情况下已被其他线程抢先初始化的情况,还有步骤3.2时失败会到这里
                node.prev = t;                     
                if (compareAndSetTail(t, node)) {   // 失败继续自旋回到步骤5
                    t.next = node;
                    return t;
                }
            }
        }
    }

即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。

    /**
     * acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞
     */
    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)) {  // 2、如果p是头结点,说明当前节点在真实数据队列的首部,就尝试是否能获得锁,如果重试成功能则无需阻塞
                    setHead(node);                   // 获取锁成功,将当前node设置为头节点
                    p.next = null;                   // 便于GC垃圾回收
                    failed = false;
                    return interrupted;             // return false 
                }
                // 1、说明p不为头结点或者没有拿到锁(可能是非公平锁模式下被其他线程抢占了锁)
                // 这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。
                if (shouldParkAfterFailedAcquire(p, node) &&  
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果上述都完成或者有异常抛出的时候,会判断是否是失败了,如果是,那么当前节点就去取消获取锁。
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法中有一个自旋逻辑,如果2处(p == head && tryAcquire(arg))条件不满足,1处的parkAndCheckInterrupt会把当前线程阻塞。

当前线程移动到队首并被其他线程唤醒时,这个线程会继续自旋,执行到2处,会再次去尝试获取锁,直至成功。

    /**
     * setHead方法是把当前节点置为虚节点,但并没有修改waitStatus
     */
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

    /**
     * 靠前驱节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)  // 1、前置节点的waitStatus是SIGNAL(-1)状态的,那就直接park住了
            return true;
        if (ws > 0) {
            // 2、前置节点的waitStatus > 0,表示当前节点是CANCEL状态,那么从前往后的遍历剔除状态是CANCEL的节点,返回false。
            // acquireQueued方法的无限循环将递归调用该方法,直至1处返回true,导致线程阻塞
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 3、如果waitStatus等于0或者小于-1,那么就直接把前置节点的waitStatus改为SIGNAL(-1)状态,返回false。进入acquireQueued自旋,直至1处返回true,导致线程阻塞
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
解锁
    public final boolean release(int arg) {
        if (tryRelease(arg)) {       // tryRelease如果返回true,说明该锁没有被任何线程持有
            Node h = head;
            if (h != null && h.waitStatus != 0)  // 头结点不为空并且头结点的waitStatus不为0(非初始化节点情况),解除线程挂起状态
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

h == null 说明队列还未初始化
h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

    private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;  // 获取头结点waitStatus
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;   // 获取当前节点的下一个节点
        // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
        if (s != null)
            LockSupport.unpark(s.thread);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值