JUC-ReentrantLock,ReentrantReadWriteLock,StampedLock

2024-01-29 01:36

本文主要是介绍JUC-ReentrantLock,ReentrantReadWriteLock,StampedLock,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 概述

前面介绍过了synchronized关键字作用的锁升级过程
无锁->偏向锁->轻量锁->重锁
下面再介绍实现Lock接口的锁的升级过程
无锁->独占锁(ReentrantLock,Synchronized)->读写锁(ReentranReadWriteLock)->邮戳锁(StampedLock)
并准备了一些问题,回顾一下自己对知识的掌握程度。

  1. 你知道Java里面有哪些锁?
  2. 你说你用过读写锁,锁饥饿问题是什么?有没有比读写锁更快的锁?
  3. StampedLock知道吗?(邮戳锁/票据锁)
  4. ReentrantReadWriteLock有锁降级机制,你知道吗?

2. ReentrantLock

ReentantLock是可重入的独占锁。默认是非公平锁。
可重入:当一个线程持有锁后,在内部可以继续获取锁。
独占:是一种悲观锁,当一个线程持有锁的时候,其他线程会阻塞。
公平和非公平:在公平的机制下,线程会依次排队,放到等待队列中。排队获取锁。在非公平的机制下,新来的线程通过CAS获取锁,获取不到,才会进入等待队列。

2.1 ReentrantLock使用代码演示

public static void main(String[] args){new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");}finally {lock.unlock();}}finally {// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。lock.unlock();// 正常情况,加锁几次就要解锁几次}},"t1").start();new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");}finally {lock.unlock();}},"t2").start();}

2.2 ReentrantLock和Synchronized比较

  1. ReentrantLock是对象,synchronzied是关键字
  2. 两者都是独占锁。(悲观锁)
  3. ReentrantLock加锁后需要手动解锁try{//do something}finally{Lock.unlock();}。synchronized关键字超出同步块自动解锁。
  4. ReentrantLock更灵活,可以控制是否是公平锁。synchronized只能是非公平锁。

使用场景的区别:

2.2.1 synchronized

写冲突比较多,线程强冲突的场景。
自旋的概率比较大,会导致浪费CPU性能。

2.2.2 ReentrantLock

synchronized锁升级是不可逆的,进入重量级锁后性能会下降。
ReentrantReadWriteLock(注意不是ReentrantLock)可以使用读写锁,增加性能。

3. ReentrantReadWriteLock

可重入读写锁。上面的可重入锁在两个线程同时读的过程中会竞争。可重入读写锁可以允许多个线程同时读取同一个资源。只允许读读共存,读写,写写之间都是互斥的。适用于读读不互斥的场景。

3.1 具有锁降级的性质

锁降级可以理解为一种操作。具体操作为写锁持有后,在准备释放写锁的之前,当前线程继续持有读锁,然后释放写锁。

    writeLock.lock();//do something// 锁降级:释放写锁之前先只有读锁。readLock.lock(); // A 降级开始writeLock.unlock();// 注意执行完这一步,其他阻塞队列的头部的读线程才能进入。// 锁降级完成....readLock.unlock();

这种方式的好处是:在耗时长的事务中,锁降级能够使让读操作更快进行执行不会被写操作给抢占,且后面的读操作不会被打断。
锁降级的代码演示:


import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDownTest {private Logger logger = LoggerFactory.getLogger(LockDownTest.class);ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();/***	这里仅仅是想知道锁重入的情况,是不是这个时候加入的锁会到等待队列里面排队。*/public void queryData() {try {Thread.sleep(500);readLock.lock();logger.info("主线程通过可重入读锁,查询数据完成.");} catch (InterruptedException e) {e.printStackTrace();} finally {readLock.unlock();}}public void test3() throws Exception {// 开始锁降级writeLock.lock();logger.info("主线程抢到写锁...");// 这里的休眠是为了让下面线程能在预想的情况下加入等待队列.Thread.sleep(500);// 这里就是假设等待队列里面排在前面的是读锁线程processReadLock(1); processReadLock(2); Thread.sleep(500);processWriteLock(4);Thread.sleep(500);processReadLock(3); Thread.sleep(500);// 开始锁降级readLock.lock(); // A 降级开始// 锁降级完成writeLock.unlock();// 注意必须读锁锁住,写锁释放操作完才降级完成,其他读线程才能进入。logger.info("主线程释放写锁(写锁降级为读锁,允许其他读操作进入)");logger.info("sleep 10s 验证等待队列中的读操作是否能执行..");TimeUnit.SECONDS.sleep(10);// 睡3s验证其他的读操作能进行logger.info("sleep 10s 结束");queryData();// 还是主线程去获取读锁。验证可重入锁。readLock.unlock(); // A 降级结束logger.info("主线程读锁释放");
//        logger.info("过程结束..");}private void processWriteLock(int threadIndex) {new Thread(() -> {logger.info("线程" + threadIndex + " 写锁开始竞争,阻塞中.");writeLock.lock();logger.info("线程" + threadIndex + " 写锁执行中..");writeLock.unlock();logger.info("线程" + threadIndex + " 写锁释放..");}).start();}private void processReadLock(int threadIndex) {new Thread(() -> {logger.info("线程" + threadIndex + " 读锁开始竞争,阻塞中.");readLock.lock();logger.info("线程" + threadIndex + " 读锁执行中..");readLock.unlock();logger.info("线程" + threadIndex + " 读锁释放..");}).start();}public static void main(String[] args) throws Exception {LockDownTest readWriteLockTest = new LockDownTest();readWriteLockTest.test3();}
}

注意这是公平锁的情况,结果说明:
在这里插入图片描述

3.2 可重入读写锁缺点(引入邮戳锁)

ReentrantReadWriteLock实现了读写分离。默认是非公平锁,每个线程是随机获取锁的。可能会导致锁饥饿的问题。
使用公平锁策略一定程度上能缓解这个问题,但是公平锁是牺牲系统的吞吐量为代价的。
引入StampedLock类的乐观锁。

4. StampedLock

StampedLock邮戳锁。这种锁是一种乐观锁,允许线程在读过程中进行写操作。让读多写少的时候,写线程有机会获取写锁。减少了线程饥饿的问题。吞吐量(单位时间系统能处理的请求量)大大提高。
在读线程操作临界资源的时候,允许写操作进行资源修改,那么读取到的数据是错误的怎么办?
为了保证读线程读取数据的正确性。读取的时候是乐观读,乐观读tryOptimisticRead不能保证读取的数据是正确性的,所以将数据读取到局部变量中,再通过lock.validate(stamp)校验是否被写线程修改过,若修改过则需要上悲观锁,重新读取数据到局部变量。

4.1 代码示例

使用代码示例:

 //乐观读,读的过程中也允许获取写锁介入public void tryOptimisticRead(){long stamp = stampedLock.tryOptimisticRead();int result = number;//故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断System.out.println("4秒前stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));for (int i = 0; i < 4; i++) {try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName()+"\t"+"正在读取... "+i+" 秒" +"后stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));}if(!stampedLock.validate(stamp)){System.out.println("有人修改过------有写操作");// 数据校验失败,升级为悲观读stamp = stampedLock.readLock();try{System.out.println("从乐观读 升级为 悲观读");result = number;System.out.println("重新悲观读后result:"+result);}finally {stampedLock.unlockRead(stamp);}}System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);}

4.2 使用场景和注意事项

StampedLock适用于读多写少的高并发场景。通过乐观读很好的解决了写线程饥饿的问题。
值得注意的是:
StampedLock不是可重入锁

5. 无锁-独占锁-读写锁-邮戳锁总结

在这里插入图片描述

  1. 从无锁到独占锁:无锁状态下数据在多线程环境下不安全因此需要锁
  2. 独占锁到可重入读写锁:独占锁在「读读」的时候线程存在竞争关系,实际很多场景中是允许多个线程同时读的。
  3. 可重入读写锁到邮戳锁:可重入读写锁会导致读多写少情况下的线程饥饿问题。引入了邮戳锁,允许读的过程中进行写。但是要采取乐观读的方式,进行数据的校验。如果数据校验失败,从乐观读变为悲观读。(乐观读的过程中允许写,悲观读的过程中不允许写操作)

这篇关于JUC-ReentrantLock,ReentrantReadWriteLock,StampedLock的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/655386

相关文章

【编程底层思考】详解Java的JUC多线程并发编程底层组件AQS的作用及原理

Java中的AbstractQueuedSynchronizer(简称AQS)是位于java.util.concurrent.locks包中的一个核心组件,用于构建锁和其他同步器。AQS为实现依赖于FIFO(先进先出)等待队列的阻塞锁和相关同步器提供了一套高效、可扩展的框架。 一、AQS的作用 统一同步状态管理:AQS提供了一个int类型的成员变量state,用于表示同步状态。子类可以根据自己

Java并发:互斥锁,读写锁,Condition,StampedLock

3,Lock与Condition 3.1,互斥锁 3.1.1,可重入锁 锁的可重入性(Reentrant Locking)是指在同一个线程中,已经获取锁的线程可以再次获取该锁而不会导致死锁。这种特性允许线程在持有锁的情况下,可以递归地调用自身的同步方法或代码块,而不会因为再次尝试获取相同的锁而被阻塞。显然,通常的锁都要设计成可重入的。否则就会发生死锁。 synchronized关键字,就是

JAVA并发编程JUC包之CAS原理

在JDK 1.5之后,java api中提供了java.util.concurrent包,简称JUC包。这个包定义了很多我们非常熟悉的工具类,比如原子类AtomicXX,线程池executors、信号量semaphore、阻塞队列、同步器等。日常并发编程要用的熟面孔基本都在这里。        首先,Atomic包,原子操作类,提供了用法简单、性能高效、最重要是线程安全的更新一个变量。支持

【硬刚Java并发】JUC基础(13):简介

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。

【硬刚Java并发】JUC基础(十二):ForkJoinPool 分支/合并框架

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 1  Fork/Join 框架 Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。 2 Fork/Join 框架与线程池的区别 采用 “工作窃取”模式(work-st

【硬刚Java并发】JUC基础(十一):线程调度

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 1 ScheduledExecutorService 一个 ExecutorService,可安排在给定的延迟后运行或定期执行的命令。 package com.atguigu.juc;import java.util.Random;import java.util.concurrent.

【硬刚Java并发】JUC基础(十):线程池

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 线程池 第四种获取线程的方法:线程池,一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。 线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑

【硬刚Java并发】JUC基础(九):线程八锁

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 1 线程八锁 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法锁的是当前对象this,被锁定后,其它的线程都不能

【硬刚Java并发】JUC基础(八):ReadWriteLock 读写锁

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 读-写锁 ReadWriteLock ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。。 ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作

【硬刚Java并发】JUC基础(七):Condition 控制线程通信

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的Java并发部分补充。 Condition Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的