Java并发编程之ReentrantLock

    xiaoxiao2022-07-05  156

    什么是ReentrantLock

    ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

    开场白

    其实ReentrantLock并没有我们想的那么神秘,在我们之前的博客已经尝试使用过ReentrantLock,相信大家也有所感悟。ReentrantLock的底层是根据AbstractQueuedSynchronizer实现的,对于AbstractQueuedSynchronizer,可以参考我的这篇博客:Java并发编程之AQS

    公平锁与非公平锁

    在学习ReentrantLock之前,我们有必要先了解一下什么是公平锁,什么是非公平锁。

    公平锁:尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁

    在Java中,我们常用的synchronized实际上就是非公平锁。而对于我们将要讲解的ReentrantLock,它默认情况下是非公平锁,但是可以设置为公平锁。

    ReentrantLock

    可重入锁

    ReentrantLock其实是一种可重入锁,若还不知道什么是可重入锁,可以参考我的这篇博客Java并发编程之Lock

    ReentrantLock的定义

    我们先上ReentrantLock的定义

    public class ReentrantLock implements Lock, java.io.Serializable

    实现了Lock接口

    ReentrantLock的内部实现

    ReentrantLock内部维护了三个内部类,Sync、NonfairSync、FairSync,其中Sync继承了AbstractQueuedSynchronizer抽象类,而NonfairSync、FairSync继承了Sync,下面进行详细分析。

    Sync类源码分析

    先上定义

    abstract static class Sync extends AbstractQueuedSynchronizer

    这其实也是一个继承了AbstractQueuedSynchronizer的抽象静态类

    Sync实现
    abstract static class Sync extends AbstractQueuedSynchronizer { // 序列号 private static final long serialVersionUID = -5179523762034025860L; // 获取锁 abstract void lock(); // 非公平方式获取 final boolean nonfairTryAcquire(int acquires) { // 当前线程 final Thread current = Thread.currentThread(); // 获取状态 int c = getState(); if (c == 0) { // 表示没有线程正在竞争该锁 if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用 // 设置当前线程独占 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; } // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它 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; } // 判断资源是否被当前线程占有 protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); } // 新生一个条件 final ConditionObject newCondition() { return new ConditionObject(); } // Methods relayed from outer class // 返回资源的占用线程 final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } // 返回状态 final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } // 资源是否被占用 final boolean isLocked() { return getState() != 0; } /** * Reconstitutes the instance from a stream (that is, deserializes it). */ // 自定义反序列化逻辑 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }

    NonfairSync

    NonfairSync采用了非公平方式来获得锁

    static final class NonfairSync extends Sync { // 版本号 private static final long serialVersionUID = 7316153563782823691L; // 获得锁 final void lock() { if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用 // 把当前线程设置独占了锁 setExclusiveOwnerThread(Thread.currentThread()); else // 锁已经被占用,或者set失败 // 以独占模式获取对象,忽略中断 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }

    FairSyn

    FairSyn采用了公平方式来获得锁

    static final class FairSync extends Sync { // 版本序列化 private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 以独占模式获取对象,忽略中断 acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ // 尝试公平获取锁 protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取状态 int c = getState(); if (c == 0) { // 状态为0 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功 // 设置当前线程独占 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据 // 下一个状态 int nextc = c + acquires; if (nextc < 0) // 超过了int的表示范围 throw new Error("Maximum lock count exceeded"); // 设置状态 setState(nextc); return true; } return false; } }

    当资源空闲时,它总是会先判断sync队列是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。

    FairSyn与NonfairSync的区别
    FairSyn:只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源NonfairSync:每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部

    ReentrantLock的属性

    private final Sync sync;

    实际上这就是一个同步队列,对ReentrantLock的操作大多转化为对sync的操作

    ReentrantLock的构造方法

    public ReentrantLock() { sync = new NonfairSync(); }

    默认采用非公平策略

    public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

    通过传递参数确定采用公平策略或者是非公平策略

    ReentrantLock的核心函数

    ReentrantLock的操作大多转化为对Sync的操作,实际上就是对AQS的操作。由于我们之前详细讲解过AQS,这里不再赘述。

    谈谈性能

    对于ReentrantLock的实现来说,其实非公平锁的性能远远优于公平锁,为什么呢?原因在于,非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这体现在下面两个方面。

    线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。

    ReentrantLock与synchronized的比较

    ReentrantLock与synchronized的功能与内存语义大致相同

    不同点
    ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性ReentrantLock 的性能比synchronized会好点ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁

    参考:【JUC】JDK1.8源码分析之ReentrantLock(三) Java多线程(九)之ReentrantLock与Condition 从源码角度彻底理解ReentrantLock(重入锁)

    最新回复(0)