一文搞懂“ReentrantLock用法和底层原理”

ReentrantLock是什么

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
相对于 synchronizedReentrantLock具备如下特点:

  • synchronized 一样,都支持可重入
  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

可重入

  • 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

那么在源码中可重入是怎么实现的呢,可以看下图。

在这里插入图片描述
当检查到持有锁的线程是当前线程且state不为0(即当前线程再次获取锁),此时会把state的值累加,state的值即代表了当前线程锁的重入次数。当state值变为0时,当前线程才将锁释放。

可中断 lockInterruptibly()

synchronizedreentrantlock.lock() 的锁, 是不可被打断的; 也就是说别的线程已经获得了锁, 线程就需要一直等待下去,不能中断,直到获得到锁才运行。

使用reentrantlock.lockInterruptibly(); 可以通过调用阻塞线程的thread.interrupt()方法打断。

代码示例如下

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            try {
                // 如果没有竞争那么此方法就会获取 lock 对象锁
                // 如果有竞争就进入阻塞队列,可以被其它线程用 interruput 方法打断
                System.out.println("t1线程尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("t1线程没有获得锁,被打断...return");
                return;
            }

            try {
                System.out.println("t1线程获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        // t1启动前 主线程先获得了锁
        lock.lock();
        System.out.println("主线程获得锁");
        thread.start();
        Thread.sleep(1000);
        System.out.println("interrupt...打断t1");
        thread.interrupt();
    }

}

结果如下:

主线程获得锁
t1线程尝试获得锁
interrupt...打断t1
t1线程没有获得锁,被打断...return
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at org.example.ReentrantLockTest.lambda$main$0(ReentrantLockTest.java:16)
	at java.lang.Thread.run(Thread.java:750)

设置超时时间

使用 lock.tryLock() 方法会返回获取锁是否成功。如果成功则返回true,反之则返回false

并且tryLock方法可以设置指定等待时间,参数为:tryLock(long timeout, TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位

获取锁的过程中, 如果超过等待时间, 或者被打断, 就直接从阻塞队列移除, 此时获取锁就失败了, 不会一直阻塞着 !

不设置等待时间, 立即失败。

代码示例如下:

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

public class ReentrantLockTest2 {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            System.out.println("t1线程尝试获得锁");
            try {
                // 等待1s
                // 此时肯定获取锁失败, 因为主线程已经获得了锁对象,主线程2s之后才释放锁
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("t1线程等待1s获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("t1线程获取不到锁");
                return;
            }
            try {
                System.out.println("t1线程获得锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        System.out.println("主线程获得锁");
        t1.start();
        // 主线程2s之后才释放锁
        Thread.sleep(2000);
        System.out.println("主线程释放了锁");
        lock.unlock();
    }

}

结果如下:

主线程获得锁
t1线程尝试获得锁
t1线程等待1s获取不到锁
主线程释放了锁

下面看下tryLock(long timeout, TimeUnit unit) 源码解析
在这里插入图片描述
我们继续跟踪到tryAcquireNanos(int arg, long nanosTimeout)方法
在这里插入图片描述
可以看到线程中断时,会抛出异常。

而当tryAcquire(int acquires)方法获取锁失败时,执行doAcquireNanos(int arg, long nanosTimeout)方法。

在这里插入图片描述
而当等待时间超过nanosTimeout时,会执行cancelAcquire(Node node)方法将当前线程移出AQS队列。

公平锁

  • ReentrantLock默认是非公平锁, 可以指定为公平锁new ReentrantLock(true)
  • 在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。一般不设置ReentrantLock为公平的, 会降低并发度。
  • synchronized底层的monitor锁就是不公平的, 和谁先进入阻塞队列是没有关系的。

公平锁
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

ReentrantLock源码里是使用FairSyncNonfairSync实现公平锁和非公平锁的。两者获取锁的方式有什么不同呢?我们可以看下源码。

如下图所示,FairSynctryAcquire(int acquires)方法获取锁时,会先调用hasQueuedPredecessors()方法检查当前线程在AQS队列中是否排在队收;如果是,该线程会获取锁。
在这里插入图片描述
NonfairSync执行nonfairTryAcquire(int acquires)方法获取锁时,不会检查当前线程在AQS队列中的位置,理论上队列中的所有线程都有机会获取锁。
在这里插入图片描述

条件变量 Condition

传统对象等待集合只有一个 waitSet, Lock可以通过newCondition()方法 生成多个等待集合Condition对象。 LockCondition 是一对多的关系

ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒。

使用流程

  1. await 前需要 获得锁
  2. await 执行后,会释放锁,进入 conditionObject (条件变量)中等待
  3. await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  4. 竞争 lock 锁成功后,从 await 后继续执行
  5. signal 方法用来唤醒条件变量(等待室)汇总的某一个等待的线程
  6. signalAll方法, 唤醒条件变量(休息室)中的所有线程

示例代码

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

public class ConditionTest {

    private static ReentrantLock lock = new ReentrantLock();

    // t1线程休息室
    private static Condition waitCondition1 = lock.newCondition();

    // t2线程休息室
    private static Condition waitCondition2 = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            lock.lock();
            System.out.println("t1线程获得锁");
            try {
                System.out.println("t1线程开始等待");
                waitCondition1.await();
                System.out.println("t1线程被唤醒");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
                System.out.println("t1线程执行结束");
            }

        }, "t1").start();

        new Thread(() -> {
            lock.lock();
            System.out.println("t2线程获得锁");
            try {
                System.out.println("t2线程开始等待");
                waitCondition2.await();
                System.out.println("t2线程被唤醒");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
                System.out.println("t2线程执行结束");
            }
        }, "t2").start();

        Thread.sleep(2000);

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("唤醒t1线程开始执行");
                waitCondition1.signal();
            } finally {
                lock.unlock();
            }

        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("唤醒t2线程开始执行");
                waitCondition2.signal();
            } finally {
                lock.unlock();
            }
        }).start();
    }
}

结果如下

t1线程获得锁
t1线程开始等待
t2线程获得锁
t2线程开始等待
唤醒t1线程开始执行
唤醒t2线程开始执行
t1线程被唤醒
t1线程执行结束
t2线程被唤醒
t2线程执行结束

需要注意的是同一时间,被唤醒的线程t1、t2需要重新抢占锁再次执行后续代码。所以线程被唤醒后,同一时间只有一个线程执行后续代码。

什么是AQS

上文讲到的ReentrantLock中许多方法都是AQS实现的,那么什么是AQS呢?

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLockSemaphore,其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS 的核心思想

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
在这里插入图片描述

AQS源码分析

使用方法很简单,线程操纵资源类就行。主要方法有两个lock()unlock()。我们深入代码去理解。我在源码的基础上加注释,希望大家也跟着调试源码。其实非常简单。

AQS 的数据结构

AQS 主要有三大属性分别是 head ,tail, state,其中state 表示同步状态,head为等待队列的头结点,tail 指向队列的尾节点。

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
    
Node的数据结构
class Node{
  //节点等待状态
  volatile int waitStatus;
  // 双向链表当前节点前节点
  volatile Node prev;
  // 下一个节点
  volatile Node next;
  // 当前节点存放的线程
  volatile Thread thread;
  // condition条件等待的下一个节点
  Node nextWaiter;
}

waitStatus 只有特定的几个常量,相应的值解释如下:
在这里插入图片描述

lock源码分析

首先我们看一下lock()方法源代码,直接进入非公平锁的lock方法:

final void lock() {
    //1、判断当前state 状态, 没有锁则当前线程抢占锁
    if (compareAndSetState(0, 1))
        // 独占锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 2、锁被人占了,尝试获取锁,关键方法了
        acquire(1);
}

进入 AQS的acquire() 方法:

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

总-分-总

lock方法主要由tryAquire()尝试获取锁,addWaiter(Node.EXCLUSIVE) 加入等待队列,acquireQueued(node,arg)等待队列尝试获取锁。示意图如下:
在这里插入图片描述

tryAquire()方法源码

既然是非公平锁,那么我们一进来就想着去抢锁,不管三七二一,直接试试能不能抢到,抢不到再进队列。

final boolean nonfairTryAcquire(int acquires) {
    //1、获取当前线程
    final Thread current = Thread.currentThread();
    // 2、获取当前锁的状态,0 表示没有被线程占有,>0 表示锁被别的线程占有
    int c = getState();
    // 3、如果锁没有被线程占有
    if (c == 0) {
         // 3.1、 使用CAS去获取锁,   为什么用case呢,防止在获取c之后 c的状态被修改了,保证原子性
        if (compareAndSetState(0, acquires)) {
            // 3.2、设置独占锁
            setExclusiveOwnerThread(current);
            // 3.3、当前线程获取到锁后,直接返回true
            return true;
        }
    }
    // 4、判断当前占有锁的线程是不是自己
    else if (current == getExclusiveOwnerThread()) {
        // 4.1 可重入锁,加+1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
         // 4.2 设置锁的状态
        setState(nextc);
        return true;
    }
    return false;
}

addWaiter()方法的解析

private Node addWaiter(Node mode),当前线程没有货得锁的情况下,进入CLH队列

private Node addWaiter(Node mode) {
    // 1、初始化当前线程节点,虚拟节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 2、获取尾节点,初始进入节点是null
    Node pred = tail;
    // 3、如果尾节点不为null,怎将当前线程节点放到队列尾部,并返回当前节点
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果尾节点为null(其实是链表没有初始化),怎进入enq方法
    enq(node);
    return node;
}

// 这个方法可以认为是初始化链表
private Node enq(final Node node) {
	// 1、入队 : 为什么要用循环呢?  
    for (;;) {
       // 获取尾节点
        Node t = tail;
        // 2、尾节点为null
        if (t == null) { // Must initialize
            // 2.1 初始话头结点和尾节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        // 3、将当前节点加入链表尾部
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

需要注意的是,初始化的头结点是一个空的node,本身不存储数据,只起到一个dummy结点的作用。
有人想明白为什么enq要用for(;;)吗? 咋一看最多只要循环2次啊! 答疑来了,这是对于单线程来说确实是这样的,但是对于多线程来说,有可能在第2部完成之后就被别的线程先执行入链表了,这时候第3步cas之后发现不成功了,怎么办?只能再一次循环去尝试加入链表,直到成功为止。

acquireQueued()方法详解

addWaiter 方法我们已经将没有获取锁的线程放在了等待链表中,但是这些线程并没有处于等待状态。acquireQueued的作用就是将线程设置为等待状态。

final boolean acquireQueued(final Node node, int arg) {
     // 失败标识
    boolean failed = true;
    try {
        // 中断标识
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前一个节点
            final Node p = node.predecessor();
            // 1、如果前节点是头结点,那么去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 重置头结点
                setHead(node);
                p.next = null; // help GC
                // 获得锁
                failed = false;
                // 返回false,节点获得锁,,,然后现在只有自己一个线程了这个时候就会自己唤醒自己
                // 使用的是acquire中的selfInterrupt(); 
                return interrupted;
            }
            // 2、如果线程没有获得锁,且节点waitStatus=0,shouldParkAfterFailedAcquire并将节点的waitStatus赋值为-1
            // parkAndCheckInterrupt将线程park,进入等待模式,
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

unlock()源码分析

public final boolean release(int arg) {
     // 如果成功释放独占锁,
    if (tryRelease(arg)) {
        Node h = head;
        // 如果头结点不为null,且后续有入队结点
        if (h != null && h.waitStatus != 0)
            //释放当前线程,并激活等待队里的第一个有效节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// 如果释放锁返回true,否者返回false
// 并且释放资源后重新设置state值,state为0释放锁
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;
    }


private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        // 重置头结点的状态waitStatus
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     // 获取头结点的下一个节点
    Node s = node.next;
    // s.waitStatus > 0 为取消状态 ,结点为空且被取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 获取队列里没有cancel的最前面的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果节点s不为null,则获得锁
    if (s != null)
        LockSupport.unpark(s.thread);
}

  • 个人公众号
    个人公众号
  • 个人小游戏
    个人小游戏
ReentrantLock和synchronized都是用于实现并发编程中的同步机制,但它们的底层原理和使用方式有所不同。 1. synchronized的底层原理: synchronized是Java中的关键字,它基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步。在Java对象头中,有一个标志位用于表示对象是否被定。当线程进入synchronized代码块时,它会尝试获取对象的,如果已经被其他线程持有,则该线程会被阻塞,直到被释放。当线程退出synchronized代码块时,它会释放对象的,使其他线程可以获取并执行相应的代码。 2. ReentrantLock底层原理: ReentrantLockJava中的一个类,它使用了一种称为CAS(Compare and Swap)的机制来实现同步。CAS是一种无的同步机制,它利用了CPU的原子指令来实现对共享变量的原子操作。ReentrantLock内部维护了一个同步状态变量,通过CAS操作来获取和释放。当一个线程尝试获取时,如果已经被其他线程持有,则该线程会进入等待状态,直到被释放。与synchronized不同,ReentrantLock提供了更灵活的获取和释放方式,例如可以实现公平和可重入。 总结: - synchronized是Java中的关键字,基于进入和退出监视器对象来实现同步,而ReentrantLock是一个类,使用CAS机制来实现同步。 - synchronized是隐式,不需要手动获取和释放,而ReentrantLock是显式,需要手动调用lock()方法获取,unlock()方法释放。 - ReentrantLock相比synchronized更灵活,可以实现公平和可重入等特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的大鱼人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值