面试必备系列JUC(7)-- AQS和reentrantlock详解


前言

冲虚道长是武当派的掌门,武功高强,精通武当太极剑法,无人能敌。在任我行最佩服的三个半人中,冲虚道长属那半个。任我行武功高强,性情高傲,能得到他的认可也是不容易的事,把冲虚道长列为半个,可见任我行对冲虚道长也有不服,但是却又不得不认可他。


一、AQS的江湖地位

1.1 什么是AQS?

令狐冲:道长,今日我前来是为了相求AQS相关的知识点,最近面试常会遇到,不知道长可否倾囊相授?

冲虚道长:令狐少侠,AQS的全称是AbstractQueuedSynchronizer,并且java中大部分的同步类比如:Lock、Semaphore、ReentrantLock等都是基于AQS来实现的。AQS 是一个抽象的同步框架,提供了原子性管理同步状态,基于阻塞队列模型实现阻塞和唤醒等待线程的功能。

令狐冲:那道长能不能从深入的给我讲讲呢?

冲虚道长:AQS只是一个同步工具,也是实现线程同步的核心组件,利用它可以实现共享锁和独占锁; AQS它本身并没有业务功能,是一个基础组件支撑层,我们可以调用这个组件工具去实现一些锁功能,从使用层面来说, AQS的同步功能分为两种:
独占:只有一个线程得到锁,其他的线程只能阻塞;例如ReentrantLock 的实现;
共享:允许多个线程同时获取锁,并发访问共享资源;例如ReentrantReadWriteLock- 读写锁,读与读操作共享;

令狐冲:那为啥要有AQS呢?

冲虚道长:那你先回答下锁的基本原理是啥。

令狐冲:这个难不倒我,锁的基本原理是,基于将多线程并行任务通过某一机制实现线程的串行化运行,从而达到线程安全的目的。

冲虚道长:要想更好的理解AQS,可以把它和Synchronized类比,在基于OS实现的synchronized中,我们知道有偏向锁/轻量级锁/自旋锁/乐观锁cas的优化。重量级锁阶段,通过线程的阻塞及唤醒来达到线程竞争和同步的目的。那么在ReentrantLock等锁中,也一定会存在某一种机制去解决这个问题。

冲虚道长:我再问你,当多个线程竞争锁的时候,只有一个线程成功,其他的线程怎么办呢?竞争失败的线程是如何实现阻塞以及被唤醒的呢?

令狐冲:这。。。这些都是使用AQS解决的吗?

冲虚道长:是的,你是不是想问AQS具体怎么解决的?我先给你抛出几个问题,你思考下:
问题一 : 锁是什么?就是一个标记位。锁用什么存储,类似于对象头中的锁标记位;
问题二: 如何解决线程的互斥和同步通信呢?类似Object中的wait()和notify() ;
问题三: 在多线程竞争重入锁的时候, 竞争失败的线程是如何管理呢? 下次我将它唤醒从那里开始呢?所以 一定要有一个数据结构来承载它;

拿不到锁的线程必须要做点什么,只有两种选择?
继续抢:例如利用CAS机制自旋,或者控制次数的自适应自旋锁;
park() 阻塞自己,暂时放弃cpu的执行权;

1.2 AQS基本原理

令狐冲:那在AQS中如何解决这个难题的?

冲虚道长:你看下:
锁标记: 实现了互斥共享; private volatile int state;
线程之间的协作:使用 lockSupport: park()和unpark()来阻塞和唤醒;
阻塞线程的管理:使用双向链表数据结构实现了线程的管理和排队,使用Node封装了线程;
可重入:记录CurrentThreadid + state计数;
阻塞的条件:Condition,获取锁的线程需要等待其他线程的处理结果数据;
公平与非公平:看队列里是否有线程排队;
CAS机制: 保证所有的更新操作的原子性;

冲虚道长:在具体看看方法类:

public abstract class AbstractQueuedSynchronizer 
  extends AbstractOwnableSynchronizer implements java.io.Serializable {
  	// CLH 变体队列头、尾节点
    private transient volatile Node head;
  	private transient volatile Node tail;
  	// AQS 同步状态
   	private volatile int state;
  	// CAS 方式更新 state
  	protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

令狐冲:道长稍微给讲讲呗,我这理解能力不行呀~

冲虚道长:首先,如果被请求的共享资源未被占用,将当前请求资源的线程设置为独占线程,并将共享资源设置为锁定状态。

冲虚道长:并且,AQS 使用一个 Volatile 修饰的 int 类型的成员变量 State 来表示同步状态,修改同步状态成功即为获得锁,Volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性。

冲虚道长:如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。

令狐冲:那道长,啥是CLH队列呀?这个得讲讲呀~

冲虚道长:CLH全称是Craig、Landin and Hagersten 队列,是单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱节点释放了锁就结束自旋。

令狐冲:道长,我总结了你刚刚说的内容,你看看对不对:

  1. CLH 队列是一个单向链表,保持 FIFO 先进先出的队列特性
  2. 通过 tail 尾节点(原子引用)来构建队列,总是指向最后一个节点
  3. 未获得锁节点会进行自旋,而不是切换线程状态
  4. 并发高时性能较差,因为未获得锁节点不断轮训前驱节点的状态来查看是否获得锁

冲虚道长:总结的很好呀,AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

冲虚道长:相比于 CLH 队列而言,AQS 中的 CLH 变体等待队列拥有以下特性:

  1. AQS 中队列是个双向链表,也是 FIFO 先进先出的特性
  2. 通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
  3. Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
  4. 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好。

二、ReentrantLock

2.1 概述

令狐冲:道长,那聊聊Reentrantlock北,底层的AQS我已经懂了,你再整体给我唠唠~

冲虚道长:ReentrantLock是重入锁,表示支持同一个线程锁的重新获取,也就是说,如果当前线程 t1 通过调用lock方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。Synchronized和ReentrantLock都是可重入锁,防止死锁。

冲虚道长:看下这个图,了解下ReentrantLock类的层次结构:

2.2 核心数据结构和思想

令狐冲:道长,那你说说reentrantlock的数据结构呗~

冲虚道长:看看下边:

冲虚道长:从类图中可以看到:

  1. ReentrantLock里面有一个Sync类型的sync对象,Sync是ReentrantLock的一个内部类,该内部类继承了AQS。AQS就是Lock维护的那个等待队列。

  2. Node是AQS的一个内部类,等待队列的每个元素都是一个Node类型的对象。

  3. ConditonObject也是AQS维护的一个内部类,该类维护了条件队列。

冲虚道长:再来看看ReentrantLock维护了一个等待队列(或者叫做同步队列)和若干个条件队列(取决于程序中创建了几个条件对象)。

2.3 可重入锁的理解

令狐冲:道长,之前我了解到reentrantlock是可重入锁,你能给我讲讲吗?

冲虚道长:那你先说说你对可重入锁的理解吧~

令狐冲:可重入锁的设计是为例避免死锁的发生,对于同一个线程而言,当获取锁后想要再次进入获取,此时不会被阻塞,而是通过 计数器来实现了可重入,进入的时候+1,退出的时候-1;如果已经获取了锁,再次进入的时候就不要再获取锁了,否则等待一个正在使用的锁就会出现死锁;

冲虚道长:我记得你之前学过synchronized,那你先说说它的可重入原理。

令狐冲:Synchronized会在对象头里专门记录锁的信息,包括 锁的线程, 锁的计数器, 锁的状态。线程在获取锁的时候会检查锁的计数器是不是0,为0说明锁未占用,计数器加1,并记下锁的线程;

令狐冲:当再次有线程来请求同步的时候,先看看是不是当前持有锁的线程,如果是就直接访问,计数器+1;如果不是,就会阻塞。当退出同步块的时候,计数器-1,变成0时,释放锁;例如下面实例递归调用,当线程再次想获取this锁的时候,如果不能重入则会出现死锁。

//递归调用如果锁不可重入,就出现了死锁;
public class ReentrantLockTest {
   
    public synchronized void reentrantKing(){ //获取了this锁
        System.out.println("i am king enter");
    }
 
    public synchronized void reentrantZZ(){
        System.out.println("i am zz enter");
        synchronized (this){                  //需要获取同一把this锁,可重入防治死锁
            reentrantKing();
        }
    }
 
    public static void main(String[] args) {
        test.reentrantZZ();       
}

冲虚道长:你总结的非常到位,而Reentrantlock则是使用AQS的state变量实现重入锁。

冲虚道长:**state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入 锁的实现来说,表示一个同步状态。**它有两个含义的表示,当 state=0 时,表示无锁状态;当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增, 比如重入 3次,那么 state=3。而在释放锁的时候,同样需要释放3次直到state=0其他线程才有资格获得锁。

令狐冲:哦哦,那我就懂了,那reentranlock咋使用呢?

冲虚道长:这个很简单,你看下:

 
    Lock lock = new ReentrantLock(); //创建锁
    lock.lock();                     //获取锁
    try{
        //被lock()保护起来的代码块
    }cache(Exception e){
 
    }finally {
        lock.uulock();               //解锁,finally防止异常
    }

令狐冲:那公平锁和非公平锁怎么设置的?

冲虚道长:公平锁与非公平锁在创建的时候可以进行设置

//todo 创建的时候,可以设置公平锁与非公平锁,注意这个地方
private Lock lock = new ReentrantLock(true);
//根据是否是公平锁调用相应的实现
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

冲虚道长:那你说说公平锁和非公平锁的区别吧

令狐冲:公平锁,不允许插队,严格按照FIFO的顺序;非公平锁,允许插队,不管队列中是否有线程阻塞等待,上去直接就cas竞争锁;效率高,节约了挂起和唤醒的状态 ,但是容易引起线程饥渴。

冲虚道长:那我问个关键点,为什么非公平锁的效率高呀?

令狐冲:这~~,反正别人是这样说的。。

冲虚道长:这个很关键的,你仔细记住!当出现锁冲突的时候,线程的 挂起-唤醒切换时间是无法避免的,而非公平锁可以尽量的避免切换,充分利用了挂起和恢复的时间,进而充分利用了cpu;

冲虚道长:例如线程a正在运行,线程b在排队,线程a运行完了线程c刚好到达抢到锁,线程b继续排队,线程c就避免了排队,线程c节约了一次阻塞和唤醒的开销。

令狐冲:这个是写在源码里的吗?

冲虚道长:源码区别其实就一行代码 :就是通过 hasQueuedPreDecessors()判断 当前节点在同步队列中是否存在前驱节点,是否有线程排队。

令狐冲:这下我是真的明白了,感谢道长传授经验。下次我们武当山再见!

常考问题

  1. 你知道AQS吗,说说?
  2. 了解reeentranlock吗?是重入锁吗?原理知道吗?
  3. 说说synchronized和reentrantlock的区别?
  4. reentrantlock的效率如何?原理知道吗?

===================================================
字节内推:
字节内推〉字节校招开启。简历砸过来!!!!!!!
200多个岗位,地点:北京 上海 广州 杭州 成都 深圳。。
有问题可以直接在公众号中回复,必回答!!!

字节内推码:B1RHWFK
官网校招简历投递通道:https://jobs.toutiao.com/campus/m/position?referral_code=B1RHWFK

===================================================
微信公众号:猿侠令狐冲
公众号定期更新文章!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值