并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition

2021-08-17 15:58:13 浏览数 (1)

文章目录

  • J.U.C脑图
  • ReentrantLock概述
  • ReentrantLock 常用方法
  • synchronized 和 ReentrantLock的比较
  • ReentrantLock示例
  • 读写锁ReentrantReadWriteLock
    • 例子
  • StampedLock
    • 示例
  • Condition
    • 示例
  • 代码

J.U.C脑图


ReentrantLock概述

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁,而不会造成自己阻塞自己。

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞

ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方 法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。


除此之外,该锁的还支持获取锁时的公平和非公平性选择。实际上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。看使用场景。

公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。


在Java里一共有两类锁, 一类是synchornized同步锁,还有一种是JUC里提供的锁Lock,Lock是个接口,其核心实现类就是ReentrantLock。

ReentrantLock实现 ,主要是采用自旋锁,循环调用CAS操作来实现加锁,避免了使线程进入内核态的阻塞状态

ReentrantLock独有的功能

  • 可指定是公平锁还是非公平锁,所谓公平锁就是先等待的线程先获得锁
  • 提供了一个Condition类,可以分组唤醒需要唤醒的线程
  • 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()

ReentrantLock 常用方法

代码语言:javascript复制
void  lock()   //加锁 
void  unlock()  //释放锁
boolean isHeldByCurrentThread();   // 当前线程是否保持锁定
boolean isLocked()  // 是否存在任意线程持有锁资源
void lockInterruptbly()  // 如果当前线程未被中断,则获取锁定;如果已中断,则抛出异常(InterruptedException)
int getHoldCount()   // 查询当前线程保持此锁定的个数,即调用lock()方法的次数
int getQueueLength()   // 返回正等待获取此锁定的预估线程数
int getWaitQueueLength(Condition condition)  // 返回与此锁定相关的约定condition的线程预估数
boolean hasQueuedThread(Thread thread)  // 当前线程是否在等待获取锁资源
boolean hasQueuedThreads()  // 是否有线程在等待获取锁资源
boolean hasWaiters(Condition condition)  // 是否存在指定Condition的线程正在等待锁资源
boolean isFair()   // 是否使用的是公平锁

synchronized 和 ReentrantLock的比较

synchornized

ReentrantLock

可重入性

可重入(都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁)

可重入

锁的实现

JVM实现,操作系统级别

JDK实现

性能

在引入偏向锁、轻量级锁(自旋锁)后性能大大提升,官方建议无特殊要求时尽量使用synchornized,并且新版本的一些jdk源码都由之前的ReentrantLock改成了synchornized

与优化后的synchornized相差不大

功能区别

方便简洁,由编译器负责加锁和释放锁 ,不会产生死锁

需手工操作锁的加锁和释放,忘记释放会产生死锁

锁粒度

粗粒度,不灵活

细粒度,可灵活控制

可否指定公平锁

不可以

可以

可否放弃锁

不可以

可以

顺便说下自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

那该如何选择呢?

如果需要实现ReenTrantLock的三个独有功能时,就选择使用ReenTrantLock, 通常情况下synchronized就能够满足了,而且使用起来简单,由JVM管理,不会产生死锁。


ReentrantLock示例

我们把使用synchronized来确保线程安全的例子,使用ReentrantLock来实现下

多次运行: 线程安全


读写锁ReentrantReadWriteLock

可重入锁ReentrantLock是排他锁,这些锁在同一时刻只允许一个线程进行访问

而读写锁ReentrantReadWriteLock在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。即ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问

在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写,使得并发性相比一般的排他锁有了很大提升。


例子

假设现在有一个类,里面有一个map集合,希望在对其读写的时候能够进行一些线程安全的保护,这时我们就可以使用到ReentrantReadWriteLock


StampedLock

StampedLock是Java8引入的一种新的锁机制,是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略。如果有大量的读线程,它也有可能引起写线程的饥饿。而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。

示例

运行结果: 线程安全


Condition

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

获取一个Condition必须通过Lock的newCondition()方法。


示例

Condition是一个多线程间协调通信的工具类,使得某个或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。

Condition可以非常灵活的操作线程的唤醒,下面是一个线程等待与唤醒的例子,其中用1、2、3、4序号标出了日志输出顺序

输出:


代码

https://github.com/yangshangwei/ConcurrencyMaster

0 人点赞