Java:Java学习笔记之ReentrantLock的简单理解和使用

ReentrantLock

1、相关知识

轻松学习java可重入锁(ReentrantLock)的实现原理

1.1 公平锁和非公平锁

1)公平锁: 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
    在这里插入图片描述

2) 非公平锁: 多个线程不会按照申请顺序获取锁,多线程会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。因此是不公平的。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
    在这里插入图片描述

1.2 可重入锁

可重入锁的含义就是某个线程持有某个锁的时候,当该线程再次获取这个锁的时候,不会造成死锁。

  • 简单来说就是:拥有该锁的同时再次获取该锁,不会造成死锁。

什么时候会出现上述这种情况呢?例如:

  • 一个类中有多个同步方法(A,B)
  • 一个线程要执行A方法,那么就要获取这个类对象锁,此时已经获取成功了
  • 在执行A方法的过程中又要去调用B方法,由于B方法也是该类的同步方法,因此该线程又要去尝试获取该类对象锁。

这就是拥有该锁再获取该锁的情况
在这里插入图片描述

public class Widget {
    
    
    public synchronized void doSomething() {
    
    
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
    
    
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

1.3、CAS算法

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

  • java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

举例说明:

阿里二面,面试官:说说 Java CAS 原理?

CAS机制当中使用了3个基本操作数:内存地址V旧的预期值A计算后要修改后的新值B

  • (1)初始状态:在内存地址V中存储着变量值为 1
    在这里插入图片描述
  • (2)线程1想要把内存地址为 V 的变量值增加1。这个时候对线程1来说,旧的预期值A=1,要修改的新值B=2。
    在这里插入图片描述
  • (3)在线程1要提交更新之前,线程2捷足先登了,已经把内存地址V中的变量值率先更新成了2。
    在这里插入图片描述
  • (4)线程1开始提交更新,首先将预期值A内存地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
    在这里插入图片描述
  • (5)线程1重新获取内存地址 V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=2,B=3。这个重新尝试的过程被称为自旋。如果多次失败会有多次自旋。
    在这里插入图片描述
  • (6)线程 1 再次提交更新,这一次没有其他线程改变地址 V 的值。线程1进行Compare,发现预期值 A 和内存地址V的实际值是相等的,进行 Swap 操作,将内存地址 V 的实际值修改为 B。
    在这里插入图片描述
  • 总结:更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 中的实际值相同时,才会将内存地址 V 对应的值修改为B,这整个操作就是CAS。

CAS 基本原理:

  • CAS 主要包括两个操作:CompareSwap,有人可能要问了:两个操作能保证是原子性吗?可以的。

CAS 是一种系统原语,原语属于操作系统用语,原语由若干指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,由操作系统硬件来保证。

回到 Java 语言,JDK 是在 1.5 版本后才引入 CAS 操作,在sun.misc.Unsafe这个类中定义了 CAS 相关的方法。

public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

2、背景、定义和特征

2.1、背景

问题的产生:

  • Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据 的增删改查),将会导致数据不准确,相互之间产生冲突。

如下例:假设有一个卖票系统,一共有100张票,有4个窗口同时卖。

在这里插入图片描述
在这里插入图片描述
输出部分结果:
在这里插入图片描述
显然上述结果是不合理的,对于同一张票进行了多次售出。

这就是多线程情况下, 出现了数据“脏读”情况。

  • 即多个线程访问余票num时,当一个线程获得余票的数 量,要在此基础上进行-1的操作之前,其他线程可能已经卖出多张票,导致获得的
    num不是最新的,然后-1后更新的数据就会有误。这就需要线程同步的实现了。

问题的解决:

因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证 了该变量的唯一性和准确性。

2.2、定义

ReentrantLock,一个可重入互斥锁,它具有与使用synchronized方法和语句所访 问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

2.3、特征

具有以下特征:

  • 1、互斥性:同时只有一个线程可以获取到该锁,此时其他线程请求获取锁,会被阻塞,然后被放到该锁内部维护的一个 AQS 阻塞队列中。
  • 2、可重入性:维护 state 变量,初始为 0,当一个线程获取到锁时,state 使用 CAS 更新为 1,本线程再次申请获取锁,会对state 进行 CAS 递增,重复获取次数即 state,最多为 2147483647 。试图超出此限制会从锁定方法抛出 Error。
  • 3、公平/非公平性:在初始化时,可以通过构造器传参,指定是否为公平锁,还是非公平锁。当设置为 true时,为公平锁,线程争用锁时,会倾向于等待时间最长的线程。

3、基本结构

在这里插入图片描述
基本结构如图所示,ReentrantLock 类实现了接口 Lock,在接口 Lock 中定义了使用锁时的方法,方法及含义如下:

public interface Lock {
    
    
    
    // 获取锁,如果没有获取到,会阻塞。
    void lock();

    // 获取锁,如果没有获取到,会阻塞。响应中断。
    void lockInterruptibly() throws InterruptedException;

    // 尝试获取锁,如果获取到,返回 true,没有获取到 返回 false
    boolean tryLock();

    // 尝试获取锁,没有有获取到,会等待指定时间,响应中断。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 释放锁
    void unlock();
	
	//返回当前线程的Condition ,可多次调用
    Condition newCondition();
}

4、基本使用

class X {
    
    
    private final ReentrantLock lock = new ReentrantLock();
    // ...

    public void m() {
    
    
        lock.lock();  // block until condition holds
        try {
    
    
        // ... method body
        } finally {
    
    
        lock.unlock()
        }
}
}

4.1、解决背景问题

具体的解决方式如下:
在这里插入图片描述

4.2、重入锁 使用

当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。 具体概念就是:自己可以再次获取自己的内部锁。

Java里面内置锁(synchronized)Lock(ReentrantLock)都是可重入的。

在这里插入图片描述
上面便是ReentrantLock重入锁特性,即调用method1()方法时,已经获得了锁, 此时内部调用method2()方法时, 由于本身已经具有该锁,所以可以再次获取。
在这里插入图片描述
上面便是synchronized的重入锁特性,即调用method1()方法时,已经获得了锁, 此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。

4.3、公平锁使用

ReentrantLock便是一种公平锁,通过在构造方法中传入true就是公平锁,传入 false,就是非公平锁

在这里插入图片描述
以下是使用公平锁实现的效果:
在这里插入图片描述
实验结果:
在这里插入图片描述
这是截取的部分执行结果,分析结果可看出两个线程是交替执行的,几乎不会出现 同一个线程连续执行多次。

4.4、可中断使用

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。

而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

public class ReentrantLockTest {
    
    
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
        thread.start();
        thread1.start();
        thread.interrupt();//是第一个线程中断
    }

    static class ThreadDemo implements Runnable {
    
    
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
    
    
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
    
    
            try {
    
    
                firstLock.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}

在这里插入图片描述
构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。

如果没有外界中断,该程序将处于死锁状态永远无法停止。

我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

4.5、可限时使用

使用lock.tryLock(long timeout, TimeUnit unit)来实现可限时锁,参数为时间和单位

  • 无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
public class ReentrantLockTest {
    
    
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
        thread.start();
        thread1.start();
    }

    static class ThreadDemo implements Runnable {
    
    
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
    
    
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
    
    
            try {
    
    
                while(!lock1.tryLock()){
    
    
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                while(!lock2.tryLock()){
    
    
                    lock1.unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}

在这里插入图片描述
线程通过调用tryLock()方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。

第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。

线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。

4.6、Condition实现等待/通知

在这里插入图片描述

  • 1、Condition接口在使用前必须先调用ReentrantLocklock()方法获得锁。
  • 2、调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Conditionsignal()方法唤醒线程。
public class ConditionTest {
    
    

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
    
    

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主线程等待通知");
        try {
    
    
            condition.await();
        } finally {
    
    
            lock.unlock();
        }
        System.out.println("主线程恢复运行");
    }
    static class SignalThread implements Runnable {
    
    

        @Override
        public void run() {
    
    
            lock.lock();
            try {
    
    
                condition.signal();
                System.out.println("子线程通知");
            } finally {
    
    
                lock.unlock();
            }
        }
    }
}

在这里插入图片描述

5、源码分析

在这里插入图片描述
在这里插入图片描述

ReentrantLock 也只是实现了 Lock 接口,并实现了这些方法,那 ReentrantLockAQS 到底有什么关系呢?

这就需要看内部具体如何实现的了。

通过上面类图可以看出,在 ReentrantLock 中含有两个内部类,分别是 NonfairSyncFairSync 而它俩又实现了 抽象类 Sync,抽象类 Sync 继承了 AbstractQueuedSynchronizerAQS。具体代码如下:

public class ReentrantLock implements Lock, java.io.Serializable {
    
    

    private final Sync sync;

    // 锁的同步控制基础类。 子类具体到公平和非公平的版本。 使用AQS状态来表示持有该锁的数量。
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
     
        // 省略 ...
    }

    static final class NonfairSync extends Sync {
    
     
        // 非公平锁逻辑 省略 ...
    }

    static final class FairSync extends Sync {
    
     
        // 公平锁逻辑 省略 ...
    }
    // 默认非公平锁
    public ReentrantLock() {
    
    
        sync = new NonfairSync();
    }
    // 根据传参指定公平锁还是非公平锁,true 公平锁,false 非公平锁
    public ReentrantLock(boolean fair) {
    
    
        sync = fair ? new FairSync() : new NonfairSync();
    }
}


ReentrantLock有两个构造方法,无参构造方法默认是创建非公平锁,而传入true为参数的构造方法创建的是公平锁。

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

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

5.1、AQS

从ReentrantLock的实现看AQS的原理及应用

AQS核心思想:

  • 如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;
  • 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
  • AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配
    在这里插入图片描述

AQSAbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列条件队列

  • 同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁
  • 条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
    在这里插入图片描述

在同步队列中的中模式,分别是独占模式共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁共享锁

AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,

  • ReentrantLock就是通过重写了AQStryAcquiretryRelease方法实现的lockunlock

5.2、非公平锁的实现原理

当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock(),创建的就是非公平锁

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

//或者传入false参数 创建的也是非公平锁
public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

5.2.1、lock方法获取锁:tryAcquire

在这里插入图片描述
在这里插入图片描述

final void lock() {
    
    
    //CAS操作设置state的值
    if (compareAndSetState(0, 1))
        //设置成功 直接将锁的所有者设置为当前线程 流程结束
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //设置失败 则进行后续的加入同步队列准备
        acquire(1);
}

public final void acquire(int arg) {
    
    
    //调用子类重写的tryAcquire方法 如果tryAcquire方法返回false 那么线程就会进入同步队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//子类重写的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    
    
    //调用nonfairTryAcquire方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    
    
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果状态state=0,即在这段时间内 锁的所有者把锁释放了 那么这里state就为0
    if (c == 0) {
    
    
        //使用CAS操作设置state的值
        if (compareAndSetState(0, acquires)) {
    
    
            //操作成功 则将锁的所有者设置成当前线程 且返回true,也就是当前线程不会进入同步
            //队列。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态state不等于0,也就是有线程正在占用锁,那么先检查一下这个线程是不是自己
    else if (current == getExclusiveOwnerThread()) {
    
    
        //如果线程就是自己了,那么直接将state+1,返回true,不需要再获取锁 因为锁就在自己
        //身上了。
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果state不等于0,且锁的所有者又不是自己,那么线程就会进入到同步队列。
    return false;
}

5.2.2、锁的释放:tryRelease

在这里插入图片描述
在这里插入图片描述

public void unlock() {
    
    
    sync.release(1);
}

public final boolean release(int arg) {
    
    
    //子类重写的tryRelease方法,需要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其
    //它线程进行尝试获取锁。
    if (tryRelease(arg)) {
    
    
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
    
protected final boolean tryRelease(int releases) {
    
    
    //状态的state减去releases
    int c = getState() - releases;
    //判断锁的所有者是不是该线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        //如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果该线程释放锁之后 状态state=0,即锁没有重入,那么直接将将锁的所有者设置成null
    //并且返回true,即代表可以唤醒其他线程去获取锁了。如果该线程释放锁之后state不等于0,
    //那么代表锁重入了,返回false,代表锁还未正在释放,不用去唤醒其他线程。
    if (c == 0) {
    
    
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

5.3、公平锁的实现原理

在这里插入图片描述
可以看出在公平锁(FairSync)中多了一个判断条件
!hasQueuedPredecessors()

hasQueuedPredecessors 方法在 AQS 中,如果有当前线程前面的线程排队返回true,如果当前线程是在队列的头部或队列为空,返回false。

代码如下:

public final boolean hasQueuedPredecessors() {
    
    

    Node t = tail; 
    Node h = head;
    Node s;

    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

5.3.1、lock方法获取锁:tryAcquire

在这里插入图片描述
在这里插入图片描述

final void lock() {
    
    
    acquire(1);
}

public final void acquire(int arg) {
    
    
    //同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
    //保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    
    
    final Thread current = Thread.currentThread();
    int c = getState();
    //判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
    if (c == 0) {
    
    
        //调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
        //线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
        //这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
    return false;
}

5.3.2、锁的释放:tryRelease

公平锁的释放和非公平锁的释放一样,这里就不重复。
公平锁和非公平锁的公平性是在获取锁的时候体现出来的,释放的时候都是一样释放的。

5.4、lockInterruptibly可中断方式获取锁

ReentrantLock相对于Synchronized拥有一些更方便的特性,比如可以中断的方式去获取锁。

public void lockInterruptibly() throws InterruptedException {
    
    
    sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    
    
    //如果当前线程已经中断了,那么抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果当前线程仍然未成功获取锁,则调用doAcquireInterruptibly方法,这个方法和
    //acquireQueued方法没什么区别,就是线程在等待状态的过程中,如果线程被中断,线程会
    //抛出异常。
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

5.5、tryLock超时等待方式获取锁

在这里插入图片描述
在这里插入图片描述

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    
    
    //如果当前线程已经中断了  则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //再尝试获取一次 如果不成功则调用doAcquireNanos方法进行超时等待获取锁
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    
    
    if (nanosTimeout <= 0L)
        return false;
    //计算超时的时间 即当前虚拟机的时间+设置的超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    //调用addWaiter将当前线程封装成独占模式的节点 并且加入到同步队列尾部
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
    
    
        for (;;) {
    
    
            final Node p = node.predecessor();
            //如果当前节点的前驱节点为头结点 则让当前节点去尝试获取锁。
            if (p == head && tryAcquire(arg)) {
    
    
                //当前节点获取锁成功 则将当前节点设置为头结点,然后返回true。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            //如果当前节点的前驱节点不是头结点 或者 当前节点获取锁失败,
            //则再次判断当前线程是否已经超时。
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            //调用shouldParkAfterFailedAcquire方法,告诉当前节点的前驱节点 我要进入
            //等待状态了,到我了记得喊我,即做好进入等待状态前的准备。
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                //调用LockSupport.parkNanos方法,将当前线程设置成超时等待的状态。
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

5.6、ReentrantLock的等待/通知机制

在这里插入图片描述
在这里插入图片描述
线程执行condition.signal()方法,将节点1从条件队列中转移到同步队列。

在这里插入图片描述
因为只有在同步队列中的线程才能去获取锁,所以通过Condition对象的waitsignal方法能实现等待/通知机制。

代码示例:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
    
    
    lock.lock();
    try {
    
    
        System.out.println("线程获取锁----" + Thread.currentThread().getName());
        condition.await(); //调用await()方法 会释放锁,和Object.wait()效果一样。
        System.out.println("线程被唤醒----" + Thread.currentThread().getName());
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    } finally {
    
    
        lock.unlock();
        System.out.println("线程释放锁----" + Thread.currentThread().getName());
    }
}

public void signal() {
    
    
    try {
    
    
        Thread.sleep(1000);  //休眠1秒钟 等等一个线程先执行
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    lock.lock();
    try {
    
    
        System.out.println("另外一个线程获取到锁----" + Thread.currentThread().getName());
        condition.signal();
        System.out.println("唤醒线程----" + Thread.currentThread().getName());
    } finally {
    
    
        lock.unlock();
        System.out.println("另外一个线程释放锁----" + Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    
    
    Test t = new Test();
    Thread t1 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            t.await();
        }
    });

    Thread t2 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            t.signal();
        }
    });

    t1.start();
    t2.start();
}

运行输出:

线程获取锁----Thread-0
另外一个线程获取到锁----Thread-1
唤醒线程----Thread-1
另外一个线程释放锁----Thread-1
线程被唤醒----Thread-0
线程释放锁----Thread-0

在这里插入图片描述

6、常见问题

6.1、ReentrantLock和Synchronized对比

在这里插入图片描述
在这里插入图片描述

7、总结

1、ReentrantLock底层通过大量的CAS操作和AQS队列去维护state变量的状态去实现线程安全的功能。

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之 后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个 线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队 首的话,就会排到队尾,由队首的线程获取到锁。

2、ReentrantLock锁流程就是先通过CAS操作尝试修改state状态获取锁,如果获取失败就判断当前占用锁的是不是自身,如果是的话就进行重入。如果不是就进入AQS队列等待。

3、ReentrantLock默认是非公平锁,也可以设置为公平锁使用,那么就要维护AQS队列

4、ReentrantLock是可重入锁

参考

1、轻松学习java可重入锁(ReentrantLock)的实现原理
2、深入理解ReentrantLock的实现原理
3、不能再被问住了!ReentrantLock 源码、画图一起看一看!
4、从ReentrantLock的实现看AQS的原理及应用
5、ReentrantLock(重入锁)功能详解和应用演示

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/122615690
今日推荐