ReentrantLock(可重入锁)源码解读与使用

简介: ReentrantLock(可重入锁)源码解读与使用

1156ed4a1998461ab8c14cae78551792.png

1. 前言

昨天鸽了一天,今天我们继续从源码层面来学习ReentrantLock这个可重入锁。

ReentrantLock是一种独占式的可重入锁,位于java.util.concurrent.locks中,是Lock接口的默认实现类,底部的同步特性基于AQS实现,和synchronized关键字类似,但更灵活、功能更强大、也是目前实战中使用频率非常高的同步类。

2. 锁的分类

在学习ReentrantLock之前,我们来认识下Java中几种不同锁的定义,方便我们后续理解文章中涉及到的锁。

2.1 乐观锁和悲观锁

  • 乐观锁:乐观锁就是每次拿数据时都假设别人不会修改数据,不会上锁,在更新数据的时候会进行判断数据有没有被修改,如果被修改,就会自动重试
  • 悲观锁:悲观锁就是每次拿数据都会认为会有人修改数据,每次操作都会上锁,堵塞其他线程

2.2. 独占锁与共享锁

  • 独占锁:同一时间段,一把锁只能被一个线程获取,比如synchronized关键字就是独占锁。
  • 共享锁:同一时间段,一把锁可以被多个线程获取,比如Semaphore(信号量),CountDownLatch(倒计时器)都是共享锁

2.3. 公平锁与非公平锁

  • 公平锁:按照申请锁的时间先后来获取锁,这种锁往往性能稍差,因为要保证申请时间上的顺序性;
  • 非公平锁:后续新来获取锁的线程会先插队,先尝试获取锁,没有获取到锁再加入队列中阻塞等待。

2.4. 可重入锁和不可重入锁

所谓可重入锁就是一个线程在获取到了一个对象锁后,线程内部再次获取该锁,依旧可以获得,即便持有的锁还没释放,仍然可以获得,不可重入锁这种情况下会发生死锁!

可重入锁在使用时需要注意的是:由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功,接下来在文章我们也会讲解到这点

2.5. 可中断锁与不可中断锁

  • 可中断锁:在获取锁的过程中可以中断获取,不需要非得等到获取锁后再去执行其他逻辑;
  • 不可中断锁:一旦线程申请了锁,就必须等待获取锁后方能执行其他的逻辑处理。

3. 源码解读

3.1. Sync

ReentrantLock 在内部通过构造器来实现公平锁与非公平锁的设置,默认为非公平锁,同样可以通过传参设置为公平锁。底层实现其实是通过FairSync、NonfariSync这个两个内部类,源码如下:

//无参构造,默认为非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

这两个内部类都继承了内部的Sync同步类,Sync内部类继承了AQS,源码如下,可根据注解阅读,前面几篇文章各种同步工具类已经讲过了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();
            // 判断state是否为0
            if (c == 0) {
                // 尝试使用CAS操作获取锁
                if (compareAndSetState(0, acquires)) {
                    // 设置当前线程为锁占用线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果之前已经获取过锁,让state+1即可,实现可重入性质
            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;
        }
 
        // 实现AQS的钩子函数,定义释放锁的逻辑
        protected final boolean tryRelease(int releases) {
            // state - 1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // state降到0,代表当前线程真正释放了锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
 
    }

3.2. FairSync 公平模式

我们接着来看看FairSync和NonfairSync的源码,他们的区别主要在于第一次获取锁时是否会插队

    static final class FairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
 
            // 如果state=0,进入后续判断
            if (c == 0) {
                // 判断在等待队列中,当前线程的前面是否存在线程,如果存在那么我们就不需要获取锁,    
                // 按顺序来,实现公平获取锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程已经占有锁,让state+1即可,不用重新获取
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

这里在通过CAS操作修改state时,也就是获取锁时,首先调用!hasQueuedPredecessors(),来判断当前线程前面是否有正在排队的线程,也就是按顺序来,如果有,就不会获取到锁,从而实现了公平获取锁的性质。

hasQueuedPredecessors():

如果当前线程之前有一个排队的线程,则为 true,如果当前线程位于队列的顶部或队列为空,则为 false。


其后通过current == getExclusiveOwnerThread()当前线程是否是判断持有锁的线程,如果是,就直接让state+1,不需要再次获取锁,实现锁的可重入性。

3.3. FairSync 非公平模式

FairSync 非公平模式的实现特别简单,源码如下

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
 
        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // 直接尝试获取锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 没获取到锁,再通过acquire方法阻塞获取锁
                acquire(1);
        }
 
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

调用lock方法会直接通过Cas操作compareAndSetState(0, 1)获取锁,如果没获取成功就调用acquire(1)去获取锁,acquire是AQS内实现的方法,他会调用钩子函数tryAcquire(在Sync中实现了)获取锁,如果获取失败就会加入CLH队列等待。这样就实现了非公平的特性,在获取锁时先插队尝试直接获取锁,没有获取到才加入CLH队列中等待。

4. 基本使用

我们接下来通过一个案例代码,来使用一下非公平锁模式下的ReentrantLock的使用

public class Test {
    //初始化一个静态lock对象
    private static final ReentrantLock lock = new ReentrantLock();
    //初始化计算量值
    private static int count;
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i <1000 ; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("result:"+count);
    }
}

输出结果为:2000,thread1和thread2分别做了加1000次的操作,由于通过ReentrantLock对修改操作上锁了,故最终结果是正常的。

5. 总结

ReentrantLock是一种独占式的可重入锁,位于JUC包下,是Lock接口的默认实现类。有三个特性:支持可重入,支持公平与非公平特性,提供堵塞锁和非阻塞锁两种获取方法(lock和trylock)


其中内部锁的竞争是基于AQS实现的,当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,这样就实现了锁的可重入。当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,唤醒等待队列中的线程,使其重新竞争锁。


ReentrantLock公平与非公平的特性,主要体现在竞争锁的时候,是否需要判断AQS队列中是否存在等待中的线程。公平锁需要判断,如果有就加入队列,而非公平锁不需要判断,会先尝试获取锁,没有获取到再加入队列等待。

77863e739daa48d69685cd2046bf1bc7.png

相关文章
|
Java 调度
多线程之线程池的七个参数
多线程之线程池的七个参数
336 0
|
存储 Java
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
|
缓存 Java 容器
Spring AOP 源码解析
基于Spring Boot 的AOP启动的源码解析,分析在Spring Boot 容器启动时,AOP的过程
3279 2
Spring AOP 源码解析
|
Kubernetes 关系型数据库 MySQL
docker部署Discuz论坛
docker部署Discuz论坛
docker部署Discuz论坛
|
前端开发 搜索推荐 API
webpack和vite devServer的进阶用法:配置proxy修改请求和响应
在前端日常开发中我们一般都是配置本地开发服务器的proxy来解决跨域问题,查看官网文档或者通过搜索引擎搜出来的都是比较基础的用法。
2484 0
|
2月前
|
安全 Java 开发者
Java集合框架:详解Deque接口的栈操作方法全集
理解和掌握这些方法对于实现像浏览器后退功能这样的栈操作来说至关重要,它们能够帮助开发者编写既高效又稳定的应用程序。此外,在多线程环境中想保证线程安全,可以考虑使用ConcurrentLinkedDeque,它是Deque的线程安全版本,尽管它并未直接实现栈操作的方法,但是Deque的接口方法可以相对应地使用。
126 12
|
存储 安全 算法
深入探索Java中的MarkWord与锁优化机制——无锁、偏向锁、自旋锁、重量级锁
深入探索Java中的MarkWord与锁优化机制——无锁、偏向锁、自旋锁、重量级锁
472 1
|
7月前
|
人工智能 监控 算法
卷不过AI就驯服它!AI训练师速成攻略
这是一篇关于AI训练师职业的全面指南。文章从“驯服AI”的理念出发,将AI训练师比作“幼儿园老师”,详细描述了该职业的工作内容、入行技能要求、成长路径及工作日常。新手可以从基础的数据标注做起,逐步学习Python、数学知识和工具使用,通过三年计划实现职业进阶。文中还分享了摸鱼技巧、崩溃与高光时刻,以及避坑建议和未来转型方向。无论是在电商公司给商品打标签,还是训练医疗AI辅助诊断,这个职业都充满挑战与机遇。最后鼓励大家主动拥抱变化,把AI变成自己的左膀右臂,而非竞争对手。
1257 1
|
存储 负载均衡 NoSQL
一文让你搞懂 zookeeper
一文让你搞懂 zookeeper
15237 15
|
NoSQL Java MongoDB
如何在Spring Boot应用中集成MongoDB数据库
如何在Spring Boot应用中集成MongoDB数据库