十、ReentrantLock原理

AQS

AQS是一个多线程编程框架,abstract Queued Synchronizer (抽象队列同步器),可称之为同步器。

在我们很熟悉的地方用到了这些东西,比如:RenntrantLock 可重入锁 ,ThreadPoolExecutor 线程池。

AQS在源码中被广泛应用,尤其是在JUC(java util Concurrent)中。

回顾

java 复制代码
package com.example;

import java.util.concurrent.locks.ReentrantLock;

public class Main {

    ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        Main main = new Main();

        Thread t1 = new Thread(new Runnable(){

            @Override
            public void run() {
                main.printLog();
            }
            
        });

        Thread t2 = new Thread(new Runnable(){

            @Override
            public void run() {
                main.printLog();
            }
            
        });

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

    }

    private void printLog() {
        try {
            lock.lock();

            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "->" + i);
            }

        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }
}

可重入锁的最关键部分是: lock.lock(),就是这个lock方法,

实际上是调用了 sync对象的lock方法。sync对象是一个ReentrantLock类的一个全局变量。

ReentrantLock 类 有两种实现,公平锁和非公平锁,他们的区别就是是否会按照得到锁的顺序来执行代码Sync 是 该类中的一个抽象静态内部类,它定义了 锁的一些关键行为。 而 上面说的全局sync对象,也分为 公平实现非公平实现,分别是:FairSyncNonFairSync.

非公平锁: 加锁的方式为:通过CAS操作来修改state状态,尝试去争夺锁,如果争夺成功,就将当前锁的owner设置为 当前线程对象。否则,就执行acquire(1) 去尝试获取锁。

acquire方法:

它的逻辑主要是3个:

  • tryAcquire 尝试获取锁
  • addWaiter 如果尝试获取锁失败,就将当前线程加入到等待队列
  • 如果在尝试获取锁失败,并且加入队列成功时,那么 就会采用自旋的方式将线程挂起。

tryAcquire 如下:

也就是说,实际的过程必须由Sync的子类去实现。

在非公平 NonFairSync 中,实现如下:

非公平sync获取 锁的 原理为:

  • 判断当前锁的状态,如果是0 无锁状态,就通过CAS(compareAndSetState)来判断能否获取锁,能,就直接变更当前lock的owner为当前线程。
  • 如果当前是有锁的状态,并且是同一个线程,那么就增加重入次数。
  • 上面两种情况都属于获得锁成功,会返回true。
  • 上述条件都不满足,就是获取锁失败,返回 false。

RenentrantLock和AQS的关系

可以看到,我们在使用RentrantLock创建出非公平锁,并执行加锁操作时,大部分逻辑都在AQS框架层实现了。 这种设计模式叫做 模板方法模式。像是Android中,每一个activity的生命周期函数,我们去重写的时候,就是在原有的方法基础上加入新逻辑。这个也属于模板方法模式。

他们的关系就是, RenentrantLock 内部定义一个Sync继承AQS,并实现自己的关键逻辑,而更底层的线程控制核心逻辑,则在 AQS代码中。这样可以很好地解耦业务代码和框架代码,用户只需要接触到 Lock,而对加锁解锁的细节无需关心。

AQS原理

state

表示当前锁的状态

  • 0 无锁状态
  • 非0 则是有锁状态,并且这个数字表示的是同一个线程获得锁的次数,如果一个线程重入5次,那么state就是5,而在释放锁的时候,也要释放5次。直到state为0,其他线程才能获得锁。

它还有一个功能就是实现独占锁,或者 共享锁

独占锁

只有一个线程可以持有,当一个线程去申请锁的时候,先判断state是不是0,如果不是就要等待。

共享锁

允许多个线程持有锁,比如某个操作允许10个线程同时进行,那么超过这个数量的线程就要等待,少于这个数量则可以将state++,然后执行同步语句。

Node

双端队列,并且是等待队列。当多个线程争夺资源时,会进入此队列。这个队列是 AQS实现多线程同步的核心,

上面的代码中,有两个node,一个头 head,一个尾 tail。 Node源码如下:

默认情况下,一个AQS中头node和尾node都是null。

获取锁失败后流程分析

锁存在的意义就是,使得获取到锁的线程执行同步代码。多个线程争夺锁,争夺失败的,就要被阻塞,等待后续的唤醒。

ReentrantLock 让线程等待以及唤醒的操作流程如下:

当调用lock加锁失败时, 会调用 tryAcquire ,addWaiter,acquireQueued ,并且 tryAcquire 在 ReentrantLock被实现。如果tryAcquire返回true,获得锁成功, 才会继续执行同步代码。

下图是 addWaiter源代码:

而当 tryAcquire 返回false时,它会被添加到一个等待队列的末端。但是插入不一定能成功。

  • 当tail为空时,也就是 队列从未被初始化时,此时需要在队列中插入一个空的Node
  • 当 compareAndSetTail失败时,说明插入过程中,有线程修改了此队列,因此需要调用enq方法将当前node插入到队列末端。

经历过 addWaiter之后,线程以Node形式插入到了等待队列的末端。

接下来就是 acquiredQueued方法, 它并不会直接去挂起Node中的线程,在插入节点的过程中,可能之前持有锁的线程已经释放了锁,此时,执行自旋去尝试获取锁。如果自旋操作还是没有获取到锁,那么就将该线程挂起或者阻塞。

获取锁流程总结

  • AQS模板方法中,acquire 通过调用子类的 tryAcquire来尝试获取锁、
  • 如果获取失败,则调用 addWaiter将当前线程构造成Node插入到 同步队列的末尾
  • 在 acquireQueued 方法中以自旋的方式尝试获取锁
  • 如果获取失败,就判断是否需要将当前线程阻塞
  • 如果需要阻塞,则调用 native层的lockSupport来实现阻塞

释放锁的流程

释放锁也是从sync对象开始的。

  • 先执行tryRelease看看是否能够释放
  • 如果能够释放,则调用unparkSuccessor来释放

unparkSuccessor源码如下:

关于CAS

上面的源代码中多次提到了 CAS,compareAndSetXXX。 比如说:compareAndSetState。它最终会调用unsafe中的api进行操作。 CAS全称是 CompareAndSwap ,它是通过硬件实现的 并发安全技术。 底层通过CPU的CAS指令,对缓存加锁和总线加锁的方式来实现多处理器之间的原子操作。

实现的具体过程有3个要素:

  • 内存值V
  • 旧的预期值E
  • 要修改的新值U

当且仅当V和E相同时,才将V修改为E,否则什么都不做。

  • 如果是多处理器,就通过lock前缀的cmpxchg指令实现缓存枷锁或者总线加锁的方式来实现多处理器之间的原子操作
  • 如果是单处理器,直接使用cmpxchg完成原子操作

自定义AQS

本文提到的 ReentrantLock 中的Sync,只是AQS的一种实现,实际上我们可以通过 继承 AQS的方式同样实现我们自己的锁同步机制。 比如:

总结

  • AQS是一套框架,框架内部定义好了大部分同步所需的逻辑
  • 状态指示器state和等待队列 node是AQS的核心
  • 由于AQS有两种不同的实现,所以我们可以用它来实现 独占锁ReentrantLock,或者共享锁 (读写锁)。

如果我们去重写一套同步机制,需要重写的方法如下:

相关推荐
菠菠萝宝6 小时前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
A_one20108 小时前
前端开发常见问题与面试-02
面试·职场和发展
一个 00 后的码农11 小时前
25会计研究生复试面试问题汇总 会计专业知识问题很全! 会计复试全流程攻略 会计考研复试真题汇总
经验分享·考研·面试·面试问题·25考研·考研复试·会计复试
星之卡比*12 小时前
前端面试题---vite和webpack的区别
前端·面试
卷卷的小趴菜学编程13 小时前
c++之多态
c语言·开发语言·c++·面试·visual studio code
JustHappy13 小时前
「CSS暴论💥」CSS“常识”回顾——上(你不会和我一样不记得吧🤡🤡)
css·面试
三天不学习15 小时前
.Net面试宝典【刷题系列】
面试·职场和发展·.net
渗透测试老鸟-九青15 小时前
HW面试经验分享 | 北京蓝中研判岗
网络·经验分享·安全·网络安全·面试·渗透·代码审计
Ciderw16 小时前
MySQL日志undo log、redo log和binlog详解
数据库·c++·redis·后端·mysql·面试·golang
一个 00 后的码农17 小时前
25旅游管理研究生复试面试问题汇总 旅游管理专业知识问题很全! 旅游管理复试全流程攻略 旅游管理考研复试真题汇总
考研·面试·面试问题·考研复试·旅游管理·复试调剂·面试真题