zl程序教程

您现在的位置是:首页 >  Java

当前栏目

【高并发】浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition

2023-02-26 10:18:43 时间

大家好,我是冰河~~

今天,我们一起来学习下Java并发编程中的AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition。好啦,开始进入今天的主题。

(福利推荐:阿里云、腾讯云、华为云服务器最新限时优惠活动,云服务器1核2G仅88元/年、2核4G仅698元/3年,点击这里立即抢购>>>

ReentrantLock

概述

Java中主要分为两类锁,一类是synchronized修饰的锁,另外一类就是J.U.C中提供的锁。J.U.C中提供的核心锁就是ReentrantLock。

ReentrantLock(可重入锁)与synchronized区别:

(1)可重入性
二者都是同一个线程进入1次,锁的计数器就自增1,需要等到锁的计数器下降为0时,才能释放锁。

(2)锁的实现
synchronized是基于JVM实现的,而ReentrantLock是JDK实现的。

(3)性能的区别
synchronized优化之前性能比ReentrantLock差很多,但是自从synchronized引入了偏向锁,轻量级锁也就是自旋锁后,性能就差不多了。

(4)功能区别

  • 便利性

synchronized使用起来比较方便,并且由编译器保证加锁和释放锁;ReentrantLock需要手工声明加锁和释放锁,最好是在finally代码块中声明释放锁。

  • 锁的灵活度和细粒度

在这点上ReentrantLock会优于synchronized。

ReentrantLock独有的功能
  • ReentrantLock可指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • 提供了一个Condition类,可以分组唤醒需要唤醒的线程。而synchronized只能随机唤醒一个线程,或者唤醒全部的线程
  • 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()。ReentrantLock实现是一种自旋锁,通过循环调用CAS操作来实现加锁,性能上比较好是因为避免了使线程进入内核态的阻塞状态。

synchronized能做的事情ReentrantLock都能做,而ReentrantLock有些能做的事情,synchronized不能做。

在性能上,ReentrantLock不会比synchronized差。

synchronized的优势
  • 不用手动释放锁,JVM自动处理,如果出现异常,JVM也会自动释放锁。
  • JVM用synchronized进行管理锁定请求和释放时,JVM在生成线程转储时能够锁定信息,这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。而ReentrantLock只是普通的类,JVM不知道具体哪个线程拥有lock对象。
  • synchronized可以在所有JVM版本中工作,ReentrantLock在某些1.5之前版本的JVM中可能不支持。
ReentrantLock中的部分方法说明
  • boolean tryLock():仅在调用时锁定未被另一个线程保持的情况下才获取锁定。
  • boolean tryLock(long, TimeUnit): 如果锁定在给定的等待时间内没有被另一个线程保持,且当前线程没有被中断,则获取这个锁定。
  • void lockInterruptibly():如果当前线程没有被中断,就获取锁定;如果被中断,就抛出异常。
  • boolean isLocked():查询此锁定是否由任意线程保持。
  • boolean isHeldByCurrentThread(): 查询当前线程是否保持锁定状态。
  • boolean isFair():判断是否是公平锁。
  • boolean hasQueuedThread(Thread):查询指定线程是否在等待获取此锁定。
  • boolean hasQueuedThreads():查询是否有线程正在等待获取此锁定。
  • boolean getHoldCount():查询当前线程保持锁定的个数。

代码示例

示例代码如下:

package io.binghe.concurrency.example.lock;   import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class LockExample {     //请求总数     public static int clientTotal = 5000;     //同时并发执行的线程数     public static int threadTotal = 200;     public static int count = 0;     private static final Lock lock = new ReentrantLock();     public static void main(String[] args) throws InterruptedException {         ExecutorService executorService = Executors.newCachedThreadPool();         final Semaphore semaphore = new Semaphore(threadTotal);         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);         for(int i = 0; i < clientTotal; i++){             executorService.execute(() -> {                 try{                     semaphore.acquire();                     add();                     semaphore.release();                 }catch (Exception e){                     log.error("exception", e);                 }                 countDownLatch.countDown();             });         }         countDownLatch.await();         executorService.shutdown();         log.info("count:{}", count);     }     private static void add(){         lock.lock();         try{             count ++;         }finally {             lock.unlock();         }     } }

ReentrantReadWriteLock

概述

在没有任何读写锁的时候,才可以取得写锁。如果一直有读锁存在,则无法执行写锁,这就会导致写锁饥饿。

代码示例

示例代码如下:

package io.binghe.concurrency.example.lock;   import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @Slf4j public class LockExample {       private final Map<String, Data> map = new TreeMap<>();     private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();     private final Lock readLock = lock.readLock();     private final Lock writeLock = lock.writeLock();       public Data get(String key){         readLock.lock();         try{             return map.get(key);         }finally {             readLock.unlock();         }     }       public Set<String> getAllKeys(){         readLock.lock();         try{             return map.keySet();         }finally {             readLock.unlock();         }     }       public Data put(String key, Data value){         writeLock.lock();         try{             return map.put(key, value);         }finally {             writeLock.unlock();         }     }       class Data{       } }

StampedLock

概述

控制锁三种模式:写、读、乐观读。

StampedLock的状态由版本和模式两个部分组成,锁获取方法返回的是一个数字作为票据,用相应的锁状态来表示并控制相关的访问,数字0表示没有写锁被授权访问。

在读锁上分为悲观锁和乐观锁,乐观读就是在读操作很多,写操作很少的情况下,可以乐观的认为写入和读取同时发生的几率很小。因此,不悲观的使用完全的读取锁定。程序可以查看读取资料之后,是否遭到写入进行了变更,再采取后续的措施,这样的改进可以大幅度提升程序的吞吐量。

总之,在读线程越来越多的场景下,StampedLock大幅度提升了程序的吞吐量。

StampedLock源码中的案例如下,这里加上了注释。

class Point {     private double x, y;     private final StampedLock sl = new StampedLock();       void move(double deltaX, double deltaY) { // an exclusively locked method         long stamp = sl.writeLock();         try {             x += deltaX;             y += deltaY;         } finally {             sl.unlockWrite(stamp);         }     }       //下面看看乐观读锁案例     double distanceFromOrigin() { // A read-only method         long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁         double currentX = x, currentY = y;  //将两个字段读入本地局部变量         if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?             stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁             try {                 currentX = x; // 将两个字段读入本地局部变量                 currentY = y; // 将两个字段读入本地局部变量             } finally {                 sl.unlockRead(stamp);             }         }         return Math.sqrt(currentX * currentX + currentY * currentY);     }       //下面是悲观读锁案例     void moveIfAtOrigin(double newX, double newY) { // upgrade         // Could instead start with optimistic, not read mode         long stamp = sl.readLock();         try {             while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合                 long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁                 if (ws != 0L) { //这是确认转为写锁是否成功                     stamp = ws; //如果成功 替换票据                     x = newX; //进行状态改变                     y = newY;  //进行状态改变                     break;                 } else { //如果不能成功转换为写锁                     sl.unlockRead(stamp);  //我们显式释放读锁                     stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试                 }             }         } finally {             sl.unlock(stamp); //释放读锁或写锁         }     } }

代码示例

示例代码如下:

package io.binghe.concurrency.example.lock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.StampedLock; @Slf4j public class LockExample {     //请求总数     public static int clientTotal = 5000;     //同时并发执行的线程数     public static int threadTotal = 200;       public static int count = 0;       private static final StampedLock lock = new StampedLock();       public static void main(String[] args) throws InterruptedException {         ExecutorService executorService = Executors.newCachedThreadPool();         final Semaphore semaphore = new Semaphore(threadTotal);         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);         for(int i = 0; i < clientTotal; i++){             executorService.execute(() -> {                 try{                     semaphore.acquire();                     add();                     semaphore.release();                 }catch (Exception e){                     log.error("exception", e);                 }                 countDownLatch.countDown();             });         }         countDownLatch.await();         executorService.shutdown();         log.info("count:{}", count);     }       private static void add(){     //加锁时返回一个long类型的票据         long stamp = lock.writeLock();         try{             count ++;         }finally {         //释放锁的时候带上加锁时返回的票据             lock.unlock(stamp);         }     } }

我们可以这样选择使用synchronozed锁还是ReentrantLock锁:

  • 当只有少量竞争者时,synchronized是一个很好的通用锁实现
  • 竞争者不少,但是线程的增长趋势是可预估的,此时,ReentrantLock是一个很好的通用锁实现
  • synchronized不会引发死锁,其他的锁使用不当可能会引发死锁。

Condition

概述

Condition是一个多线程间协调通信的工具类,Condition除了实现wait和notify的功能以外,它的好处在于一个lock可以创建多个Condition,可以选择性的通知wait的线程

特点:

  • Condition 的前提是Lock,由AQS中newCondition()方法 创建Condition的对象
  • Condition await方法表示线程从AQS中移除,并释放线程获取的锁,并进入Condition等待队列中等待,等待被signal
  • Condition signal方法表示唤醒对应Condition等待队列中的线程节点,并加入AQS中,准备去获取锁。

代码示例

示例代码如下

package io.binghe.concurrency.example.lock;   import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class LockExample {     public static void main(String[] args) {         ReentrantLock reentrantLock = new ReentrantLock();         Condition condition = reentrantLock.newCondition();           new Thread(() -> {             try {                 reentrantLock.lock();                 log.info("wait signal"); // 1                 condition.await();             } catch (InterruptedException e) {                 e.printStackTrace();             }             log.info("get signal"); // 4             reentrantLock.unlock();         }).start();           new Thread(() -> {             reentrantLock.lock();             log.info("get lock"); // 2             try {                 Thread.sleep(3000);             } catch (InterruptedException e) {                 e.printStackTrace();             }             condition.signalAll();             log.info("send signal ~ "); // 3             reentrantLock.unlock();         }).start();     } }

推荐阅读

《实践出真知:全网最强秒杀系统架构解密,不是所有的秒杀都是秒杀!!》

《注意:线程的执行顺序与你想的并不一样!!》

《深入解析Callable接口 》

《两种异步模型与深度解析Future接口!》

《解密SimpleDateFormat类的线程安全问题和六种解决方案!》

《不得不说的线程池与ThreadPoolExecutor类浅析》

《深度解析线程池中那些重要的顶层接口和抽象类》

《从源码角度分析创建线程池究竟有哪些方式》

《通过源码深度解析ThreadPoolExecutor类是如何保证线程池正确运行的》

《通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程》

《通过源码深度分析线程池中Worker线程的执行流程》

《从源码角度深度解析线程池是如何实现优雅退出的》

《ScheduledThreadPoolExecutor与Timer的区别和简单示例》

《深度解析ScheduledThreadPoolExecutor类的源代码》

《由InterruptedException异常引发的思考》

《浅谈AQS中的CountDownLatch、Semaphore与CyclicBarrier》

好了,今天就到这儿吧,我是冰河,我们下期见~~

【高并发】浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition


本站部分内容转载自网络,版权属于原作者所有,如有异议请联系QQ153890879修改或删除,谢谢!
转载请注明原文链接:【高并发】浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition

你还在原价购买阿里云、腾讯云、华为云、天翼云产品?那就亏大啦!现在申请成为四大品牌云厂商VIP用户,可以3折优惠价购买云服务器等云产品,并且可享四大云服务商产品终身VIP优惠价,还等什么?赶紧点击下面对应链接免费申请VIP客户吧:

1、点击这里立即申请成为腾讯云VIP客户

2、点击这里立即注册成为天翼云VIP客户

3、点击这里立即申请成为华为云VIP客户

4、点击这里立享阿里云产品终身VIP优惠价

喜欢 (0)
[[email protected]]
分享 (0)