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

相关文章
|
8天前
|
存储 Java
StampedLock(戳记锁)源码解读与使用
StampedLock(戳记锁)源码解读与使用
|
8天前
|
安全 Java 测试技术
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
ReentrantReadWriteLock(可重入读写锁)源码解读与使用
|
8天前
|
Java
从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁
【5月更文挑战第6天】从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁
10 1
|
8天前
|
安全 Java
ReentrantLock 原理你都知道吗?
通过以上步骤和示例代码,你应该对 ReentrantLock 的工作原理有了清晰的理解。欢迎关注威哥爱编程,一起学习成长。
|
算法 Java
【JUC基础】05. Synchronized和ReentrantLock
前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。
AQS(abstractQueuedSynchronizer)锁实现原理详解
AQS(abstractQueuedSynchronizer)抽象队列同步器。其本身是一个抽象类,提供lock锁的实现。聚合大量的锁机制实现的共用方法。
124 0
|
Java
Java并发编程 - AQS 之 ReentrantLock
Java并发编程 - AQS 之 ReentrantLock
110 0
深入浅出ReentrantLock(可重入锁)(2)
深入浅出ReentrantLock(可重入锁)
98 0
深入浅出ReentrantLock(可重入锁)(2)
|
Java
深入浅出ReentrantLock(可重入锁)
深入浅出ReentrantLock(可重入锁)
159 0
深入浅出ReentrantLock(可重入锁)
|
Java 编译器
深入浅出ReentrantLock(可重入锁)(3)
深入浅出ReentrantLock(可重入锁)
81 0