ReentrantLock底层原理学习一

J.U.C 简介

复制代码
Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。我们在接下来的课程中,回去剖析一些经典的比较常用的组件的设计思想

Lock

复制代码
Lock 在 J.U.C 中是最核心的组件,前面我们讲 synchronized 的时候说过,锁最重要的特性就是解决并发安全问题。为什么要以 Lock 作为切入点呢?如果有同学看过 J.U.C 包中的所有组件,一定会发现绝大部分的组件都有用到了 Lock。所以通过 Lock 作为切入点使得在后续的学习过程中会更加轻松。

Lock 简介

复制代码
在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

Lock 的实现

复制代码
Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现
ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

Lock 的类关系图

复制代码
Lock 有很多的锁的实现,但是直观的实现是 ReentrantLock 重入锁

void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放

void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程 可 中 断 , 抛 出java.lang.InterruptedException 异常

boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回 true

boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法

void unlock() // 释放锁

ReentrantLock 重入锁

复制代码
重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized 和 ReentrantLock 都是可重入锁。锁会存在重入的特性,那是因为对于同步锁的理解程度还不够,比如在下面这类的场景中,存在多个加锁的方法的相互调用,其实就是一种重入特性的场景。

重入锁的设计目的

复制代码
比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
java 复制代码
public class ReentrantDemo{
	 public synchronized void demo(){
		 System.out.println("begin:demo");
		 demo2();
	 }
	 public void demo2(){
		 System.out.println("begin:demo1");
			 synchronized (this){
			 }
		 }
		 public static void main(String[] args) {
		 ReentrantDemo rd=new ReentrantDemo();
		 new Thread(rd::demo).start();
	 }
}

ReentrantLock 的使用案例

java 复制代码
public class AtomicDemo {
 private static int count=0;
 static Lock lock=new ReentrantLock();
 public static void inc(){
	 lock.lock();
	 try {
	 	Thread.sleep(1);
	 } catch (InterruptedException e) {
		 e.printStackTrace();
	 }
	 count++;
	 lock.unlock();
 }
 public static void main(String[] args) throws 
InterruptedException {
	 for(int i=0;i<1000;i++){
	 	new Thread(()->{AtomicDemo.inc();}).start();;
	 }
	 	Thread.sleep(3000);
		System.out.println("result:"+count);
	 }
}

ReentrantReadWriteLock

复制代码
我们以前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.
java 复制代码
public class LockDemo {
 static Map<String,Object> cacheMap=new HashMap<>();
 static ReentrantReadWriteLock rwl=new 
ReentrantReadWriteLock();
 static Lock read=rwl.readLock();
 static Lock write=rwl.writeLock();
 public static final Object get(String key) {
	 System.out.println("开始读取数据");
	 read.lock(); //读锁
	 try {
	 return cacheMap.get(key);
	 }finally {
	 	read.unlock();
	 }
 }
	 public static final Object put(String key,Object value){
		 write.lock();
		 System.out.println("开始写数据");
		 try{
		 	return cacheMap.put(key,value);
		 }finally {
		 	write.unlock();
		 }
	 }
}
复制代码
在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。
在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
⚫ 读锁与读锁可以共享
⚫ 读锁与写锁不可以共享(排他)
⚫ 写锁与写锁不可以共享(排他)

ReentrantLock 的实现原理

复制代码
我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?
AQS 是什么 
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。
AQS 的两种功能 从使用层面来说,AQS 的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁,比如前面给大家演示的 ReentrantLock 就是以独占方式实现的互斥锁
共 享 锁 , 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如ReentrantReadWriteLock。
AQS 的内部实现 
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

Node 的组成

释放锁以及添加线程对于队列的变化

当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。

里会涉及到两个变化

  1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己

  2. 通过 CAS 讲 tail 重新指向新的尾部节点head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:

这个过程也是涉及到两个变化

  1. 修改 head 节点指向下一个获得锁的节点

  2. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可

相关推荐
程序员张32 小时前
Maven编译和打包插件
java·spring boot·maven
ybq195133454312 小时前
Redis-主从复制-分布式系统
java·数据库·redis
weixin_472339463 小时前
高效处理大体积Excel文件的Java技术方案解析
java·开发语言·excel
小毛驴8503 小时前
Linux 后台启动java jar 程序 nohup java -jar
java·linux·jar
枯萎穿心攻击4 小时前
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法
开发语言·unity·c#·游戏引擎
DKPT4 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
Eiceblue5 小时前
【免费.NET方案】CSV到PDF与DataTable的快速转换
开发语言·pdf·c#·.net
好奇的菜鸟5 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
m0_555762906 小时前
Matlab 频谱分析 (Spectral Analysis)
开发语言·matlab
DuelCode6 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis