写点什么

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

作者:冰河
  • 2021 年 11 月 25 日
  • 本文字数:5467 字

    阅读完需:约 18 分钟

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

大家好,我是冰河~~


今天跟大家聊聊 AQS 中的 ReentrantLock、ReentrantReadWriteLock、StampedLock 与 Condition。

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;@Slf4jpublic 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;@Slf4jpublic 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;@Slf4jpublic 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);        }    }}
复制代码


<font color="#FF0000">我们可以这样选择使用 synchronozed 锁还是 ReentrantLock 锁:</font>


  • 当只有少量竞争者时,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;@Slf4jpublic 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();    }}
复制代码


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

发布于: 2021 年 11 月 25 日阅读数: 9
用户头像

冰河

关注

公众号:冰河技术 2020.05.29 加入

互联网高级技术专家,《深入理解分布式事务:原理与实战》,《海量数据处理与大数据技术实战》和《MySQL技术大全:开发、优化与运维实战》作者,mykit-data与mykit-transaction-message框架作者。【冰河技术】作者。

评论 (1 条评论)

发布
用户头像
infoQ的写作平台质量真的挺高的,使用markdown来写文章,看着真舒服。
23 小时前
回复
没有更多了
【高并发】浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition