注:本笔记是阅读《Java高并发核心编程卷2》整理的笔记!
显示锁
使用Java内置锁时,不需要通过Java代码显式地对同步对象的监视器进行抢占和释放,这些工作由JVM底层完成,而且任何一个Java对象都能作为一个内置锁使用,所以, Java的对象锁使用起来非常方便。但是, Java内置锁的功能相对单一,不具备一些比较高级的锁功能,比如:
- 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃。
- 可中断抢锁:在抢锁时,外部线程给抢锁线程发出一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
- 多个等待队列:生产者和消费者共用一把锁,该锁上维持两个等待队列。
除了以上功能问题之外, Java对象锁还存在性能问题。在竞争稍微激烈的情况下, Java对象锁会膨胀为重量级锁(基于操作系统的Mutex Lock实现),而重量级锁的线程阻塞和唤醒操作需要进程在内核态和用户态之间来回切换,导致其性能非常低。所以,迫切需要提供一种新的锁来提升争用激烈场景下锁的性能。 Java显式锁就是为了解决这些Java对象锁的功能问题、性能问题而生的。 Lock接口位于java.util.concurrent.locks包中,是JUC显式锁的一个抽象类。
可重入锁 ReentrantLock
ReentrantLock是JUC包提供的显式锁的一个基础实现类, ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,但是拥有了限时抢占、可中断抢占等一些高级锁特性。此外, ReentrantLock基于内置的抽象队列同步器(Abstract Queued Synchronized, AQS)实现,在争用激烈场景下,能表现出表内置锁更佳的性能。 ReentrantLock是一个可重入的独占(或互斥)锁 。
使用 lock()方法抢锁的模板代码
使用lock()方法进行阻塞式的锁抢占,其模板代码如下:
//创建所对象, ReentrantLock 为Lock的某个实现类
Lock lock = new ReentrantLock();
lock.lock(); //step1:抢占锁
try {
//step2:抢锁成功,执行临界区代码
} finally {
lock.unlock(); //step3:释放锁
}
以上抢锁模板代码有以下几个需要注意的要点:
- 释放锁操作lock.unlock()必须在try-catch结构的finally块中执行,避免锁因为异常得不到释放。
- 抢占锁操作lock.lock()必须在try语句块之外,而不是放在try块之内。lock()方法没有声明抛出异常,所以可以不包含到try块中。lock()方法并不是一定能够抢占锁成功,如果没有抢占成功,当然也就不需要释放锁 。
- 在抢占锁操作lock.lock()和try语句之间不要插入任何代码,避免抛出异常而无法执行释放锁操作lock.unlock(),导致锁无法被释放。
调用 tryLock()方法非阻塞抢锁的模板代码
lock()是阻塞式抢占,在没有抢到锁的情况下, 当前线程会阻塞。如果不希望线程阻塞,可以使用tryLock()方法抢占锁。 tryLock()是非阻塞抢占,在没有抢到锁的情况下,当前线程会立即返回,不会被阻塞。
//创建所对象, ReentrantLock为Lock的某个实现类
Lock lock = new ReentrantLock();
if (lock.tryLock()) { //step1:尝试抢占锁
try {
//step2:抢锁成功,执行临界区代码
} finally {
lock.unlock(); //step3:释放锁
}
}
else{
//step4:抢锁失败,执行后备动作
}
这种处理方式在实际开发中使用不多,但是其重载版本tryLock(long time, TimeUnit unit)方法在限时阻塞抢锁的场景中非常有用。
调用 tryLock(long time, TimeUnit unit)方法抢锁的模板代码
tryLock(long time, TimeUnit unit)方法用于限时抢锁,该方法在抢锁时会进行一段时间的阻塞等待,其time参数代表最大的阻塞时长,其unit参数为时长的单位(如秒)。
//创建所对象, SomeLock为Lock的某个实现类,如ReentrantLock
Lock lock = new SomeLock();
//抢锁时阻塞一段时间,如1秒
if (lock.tryLock(1, TimeUnit.SECONDS)) { //step1:限时阻塞抢占
try {
//step2:抢锁成功,执行临界区代码
} finally {
lock.unlock(); //step3:释放锁
}
}
else{
//限时抢锁失败,执行后备动作
}
总结
- lock()方法用于阻塞抢锁,抢不到锁时线程会一直阻塞。
- tryLock()方法用于尝试抢锁,该方法有返回值,如果成功则返回true,如果失败(即锁已被其他线程获取)则返回false。此方法无论如何都会立即返回。
- tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过这个方法在抢不到锁时会阻塞一段时间。如果在阻塞期间获取到锁立即返回true,超时则返回false。
基于显式锁进行“等待-通知”方式的线程间通信
“等待-通知”方式的线程间通信机制,具体来说是指一个线程A调用了同步对象的wait()方法进入等待状态,而另一个线程B调用了同步对象的notify()或者notifyAll()方法去唤醒等待线程;当线程A收到线程B的唤醒通知后,就可以重新开始执行。 与Object对象的wait()、 notify()两类方法类似,基于Lock显式锁JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口—java.util.concurrent.locks.Condition。
public interface Condition
{
//方法1:等待。此方法在功能上与 Object.wait()语义等效
//使当前线程加入 await() 等待队列中,并释放当前锁
//当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁
void await() throws InterruptedException;
//方法2:限时等待。此方法与await()语义等效
//不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待
//线程等待超时返回false,其他情况返回true
boolean await(long time, TimeUnit unit) throws InterruptedException;
//方法3:通知。此方法在功能上与Object.notify()语义等效
// 唤醒一个在await()等待队列中的线程
void signal();
//方法4: 通知全部。唤醒await()等待队列中所有的线程
//此方法与object.notifyAll()语义上等效
void signalAll();
}
Condition的“等待-通知”方法和Object的“等待-通知”方法的语义等效关系为:
- Condition类的await()方法和Object类的wait()方法等效。
- Condition类的signal()方法和Object类的notify()方法等效。
- Condition类的signalAll()方法和Object类的notifyAll()方法等效。
// 创建一个显式锁
static Lock lock = new ReentrantLock();
//获取一个显式锁绑定的Condition对象
static private Condition condition = lock.newCondition();
//========在第一个线程中,执行下面代码========
lock.lock(); // ①抢锁
try{
Print.tcfo("我是等待方");
condition.await(); // ②开始等待,并且释放锁
Print.tco("收到通知,等待方继续执行");
} catch (InterruptedException e){
e.printStackTrace();
} finally{
lock.unlock(); //释放锁
}
//========在第二个线程中,执行下面代码========
lock.lock(); //③抢锁
try{
Print.tcfo("我是通知方");
condition.signal(); // ④发送通知,记得立马释放锁,否则被通知的的线程拿不到锁
Print.tco("发出通知了,但是线程还没有立马释放锁");
} finally{
lock.unlock(); //⑤释放锁之后,等待线程才能获得所
}
使用ReentrantLock(重入锁)作为显式锁的实现类,然后通过该显式锁去获取一个Condition实例。在调用await()方法前,等待线程必须获得显式锁(如语句①), await()方法会让当前线程加入到Condition对象等待队列中。在调用signal()方法前,通知线程也必须获得相应显式锁(如语句③)。在语句④调用signal()方法后, JUC会从Condition对象等待队列中唤醒一个线程。当等待线程被唤醒后,将会重新尝试获得与Condition对象绑定的显式锁,一旦抢占成功将继续执行。
由于Lock有公平锁和非公平锁之分,而Condition是与Lock绑定的,所以就有与Lock一样的公平特性:如果是公平锁,等待线程为按照FIFO(先进先出)顺序从Condition对象的等待队列中唤醒;如果是非公平锁,那么后续的唤醒次序就不保证FIFO顺序了。
LockSupport
LockSupport是JUC提供的一线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法,常用三个方法:
// 无限期阻塞当前线程
public static void park();
// 唤醒某个被阻塞的线程
public static void unpark(Thread thread);
// 阻塞当前线程,有超时时间的限制
public static void parkNanos(long nanos);
LockSupport.park()和 Thread.sleep()的区别
- Thread.sleep()没法从外部唤醒,只能自己醒过来;而被LockSupport.park()方法阻塞的线程可以通过调用LockSupport.unpark()方法去唤醒。
- 当一个线程被park()阻塞时,调用Thread.interrupt()方法会设置了线程的中断标志,被阻塞线程都会响应线程的中断信号,唤醒线程的执行。 而Thread.sleep(),wait,join方法的阻塞别打断时会抛出InterruptedException异常,并将打断标记置为false。
- 共同的就是都不会释放所持有的锁。
LockSupport.park()与 Object.wait()的区别
- Object.wait()方法需要在synchronized块中执行,而LockSupport.park()可以在任意地方执行。
- 当被阻塞线程中断时, Object.wait()方法抛出了中断异常,调用者需要捕获;当被阻塞线程中断时, LockSupport.park()不会抛出异常,调用时不需要处理中断异常。
显示锁的分类
可重入锁与不可重入锁
可重入锁也被称为递归锁,指的是一个线程可以多次抢占同一个锁。例如,线程A在进入外层函数抢占了一个Lock显式锁之后,当线程A继续进入内层函数时,如果遇到有抢占同一个Lock显式锁的代码,线程A依然可以抢到该Lock显式锁。 JUC的ReentrantLock类是可重入锁的一个标准实现类。
悲观锁和乐观锁
悲观锁就是悲观思想,每次去入临界区操作数据的时候都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。总体来说,悲观锁适用于写多读少的场景,遇到高并发写的可能性高。 Java的Synchronized重量级锁是一种悲观锁。
乐观锁是一种乐观思想,每次去拿数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复读-比较-写的操作。总体来说,乐观锁适用于读多写少的场景,遇到高并发写的可能性低。 Java中的乐观锁基本都是通过CAS自旋操作实现的。 CAS是一种更新原子操作,比较当前值跟传入值是否一样,是则更新,不是则失败。在争用激烈的场景下, CAS自旋会出现大量的空自旋,会导致乐观锁性能大大降低。Java的Synchronized轻量级锁是一种乐观锁。ReentrantLock 都是乐观锁,JUC的显式锁都是基于AQS实现的,而AQS通过对队列的使用很大程度上减少了锁的争用,极大地减少了空的CAS自旋。
悲观锁存在的问题:
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
CAS实现乐观锁
CAS操作可以非常清晰地分为两个步骤:
- 检测内存位置V的值是否为开始读取的V作为预期原值A。
- 如果是,说明没人修改V,将位置V更新为新值B;否则不要更改该位置,进入自旋。
非重入CAS自旋锁的实现原理 :
public class SpinLock implements Lock {
/**
* 当前锁的拥有者
* 使用Thread 作为同步状态
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 抢占锁
*/
@Override
public void lock() {
Thread t = Thread.currentThread();
//自旋,如果owner不为null则自旋,为null说明没有占用
while (!owner.compareAndSet(null, t)) { //为空则设为t
// DO nothing
Thread.yield();//让出当前剩余的CPU时间片
}
}
//释放锁
@Override
public void unlock() {
Thread t = Thread.currentThread();
//只有拥有者才能释放锁
if (t == owner.get()) {
// 设置拥有者为空,这里不需要 compareAndSet操作
// 因为已经通过owner做过线程检查
owner.set(null); //设为null
}
}
// 省略其他代码
}
以上是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁没有被释放之前,如果又一次重新获取该锁owner不为空。 为了实现可重入锁,这里引入一个计数器,用来记录一个线程获取锁的次数。一个简单的可重入的自旋锁的代码大致如下:
public class ReentrantSpinLock implements Lock {
/**
* 当前锁的拥有者
* 使用拥有者Thread作为同步状态,而不是使用一个简单的整数作为同步状态
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 记录一个线程重复获取锁的次数
* 此变量为同一个线程在操作,没有必要加上volatile保障可见性和有序性
*/
private int count = 0;
/**
* 抢占锁
*/
@Override
public void lock() {
Thread t = Thread.currentThread();
// 如果是重入,增加重入次数后返回
if (t == owner.get()) {
++count;
return;
}
//自旋
while (owner.compareAndSet(null, t)) {
// DO nothing
Thread.yield(); //让出当前剩余的CPU时间片
}
}
/**
* 释放锁
*/
@Override
public void unlock() {
Thread t = Thread.currentThread();
//只有拥有者才能释放锁
if (t == owner.get()) {
if (count > 0) {
// 如果重入的次数大于0,减少重入次数后返回
--count;
} else {
// 设置拥有者为空
//这里不需要compareAndSet,因为已经通过owner做过线程检查
owner.set(null);
}
}
}
// 省略其他代码
}
自旋锁的特点: 线程获取锁的时候,如果锁被其他线程持有,当前线程将循环等待,直到获取到锁。线程抢锁期间状态不会改变,一直是运行状态(RUNNABLE),在操作系统层面线程处于用户态。自旋锁的问题: 在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。总线风暴当然与CPU的架构和设计有关,并不是所有的CPU都会产生总线风暴。JUC基于CAS实现的轻量级锁如何避免总线风暴呢?答案是:使用队列对抢锁线性进行排队,最大程度上减少了CAS操作数量。
CLH自旋锁
CLH锁其实就是一种是基于队列(具体为单向链表)排队的自旋锁,抢锁线程在队列尾部加入一个节点,然后仅在前驱节点上做普通自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁,当前节点抢锁成功。 由于CLH锁只有在节点入队时进行一下CAS的操作,在节点在加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下, CLH锁能大大减少的CAS操作的数量,以避免CPU的总线风暴。
请注意:CAS自旋使用到了原子操作与禁止指令重排机制,而这些操作会要求缓存一致性。如果缓存一致性流量过大会导致总线风暴。而普通自旋仅仅是判断前面节点是否是Empty状态—未锁。
- CLHLock的尾指针tail总是指向最后一个线程的节点。
- CLHLock队列中的抢锁线程一直进行普通自旋,循环判断前一线程的locked状态,如果是true,那么说明前一线程处于自旋等待状态或正在执行临界区代码,所以自己需要自旋等待。
释放过程:
线程B执行抢到锁并且完成临界区代码的执行后,开始unlock(释放)操作,设置其nodeB的前驱引用为null,锁状态locked为false,具体如图5-6所示。线程B释放锁之后, nodeA对象已经没有任何的强引用,可以被GC回收了。
公平锁和非公平锁
公平锁是指不同的线程抢占锁的机会是公平的、平等的,从抢占时间上来说,先对锁进行抢占的线程一定被先满足,抢锁成功的次序体现为FIFO(先进先出)顺序。简单来说,公平锁就是保障了各个线程获取锁都是按照顺序来的,先到的线程先获取锁。非公平锁是指不同的线程抢占锁的机会是非公平的、不平等的,从抢占时间上来说,先对锁进行抢占的线程不一定被先满足,抢锁成功的次序不会体现为FIFO(先进先出)顺序。默认情况下, ReentrantLock实例是非公平锁,但是,如果在实例构造时传入了参数true,所得到的锁就是公平锁。非公平锁的优点在于吞吐量比公平锁大,其缺点是有可能会导致线程优先级反转或者线程饥饿现象。不公平锁的含义是阻塞队列内公平,队列外非公平。
基本使用
构造方法:ReentrantLock lock = new ReentrantLock(true)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock 默认是不公平的:
public ReentrantLock() {
sync = new NonfairSync();
}
说明:公平锁一般没有必要,会降低并发度
可中断锁与不可中断锁
什么是可中断锁? 如果某一线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁。什么是不可中断锁? 一旦这个锁被其他线程占有,如果自己还想抢占,自己只能选择等待或者阻塞,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么自己只能永远等下去,并且没有办法终止等待或阻塞。
简单来说,在抢锁过程中能通过某些方法去终止抢占过程,这就是可中断锁,否则就是不可中断锁。 Java的synchronized内置锁就是一个不可中断锁,而JUC的显式锁(如ReentrantLock)是一个可中断锁。
lockInterruptibly()
public void lockInterruptibly()
:获得可打断的锁,thread.interrupt();可用来打断。如果抢占过程收到由Thread.interrupt()方法发出的线程中断信号, lockInterruptibly()方法会抛出InterruptedException。
- 如果没有竞争此方法就会获取 lock 对象锁
- 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
System.out.println("尝试获取锁");
lock.lockInterruptibly(); // 拿不到锁,在此等待,可被中断
} catch (InterruptedException e) {
System.out.println("没有获取到锁,被打断,直接返回");
return;
}
try {
System.out.println("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock(); //锁被拿了
t1.start();
Thread.sleep(2000);
System.out.println("主线程进行打断锁");
t1.interrupt();
}
tryLock()
tryLock(long timeout, TimeUnit unit)
阻塞式“限时抢占”(在timeout时间内)锁抢占过程中会处理Thread.interrupt()中断信号,如果线程被中断,就会终止抢占并抛出InterruptedException异常。timeout之外自动抢锁终止。
独占锁和共享锁
独占锁指的是每次只有一个线程能持有的锁。独占锁是一种悲观保守的加锁策略。JUC的ReentrantLock类是一个标准的独占锁实现类。共享锁允许多个线程同时获取锁,容许线程并发进入临界区。与独占锁不同,共享锁是一种乐观锁,允许多个执行读操作的线程同时访问共享资源。
JUC的ReentrantReadWriteLock(读写锁)类是一个共享锁实现类。使用该读写锁时,读操作可以有很多线程一起读,但是写操作只能有一个线程去写,而且在写入的时候,别的线程也不能进行读的操作。因为多个读操作并没有线程安全问题,所以在读的地方使用读锁,在写的地方使用写锁,可以提高程序执行效率。
JUC中的共享锁包括Semaphore(信号量)、 ReadLock(读写锁)中的读锁、 CountDownLatch倒数闩。
共享锁 Semaphore ()
synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行
Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁
构造方法:
public Semaphore(int permits)
:permits 表示许可线程的数量(state)public Semaphore(int permits, boolean fair)
:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程
常用API:
public void acquire()
:表示获取许可public void release()
:表示释放许可,acquire() 和 release() 方法之间的代码为同步代码
public static void main(String[] args) {
// 1.创建Semaphore对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 3. 获取许可
semaphore.acquire();
sout(Thread.currentThread().getName() + " running...");
Thread.sleep(1000);
sout(Thread.currentThread().getName() + " end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源。假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞
倒数栓 CountDown
CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成。CountDownLatch可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。
构造器:
public CountDownLatch(int count)
:初始化唤醒需要的 down 几步
常用API:
public void await()
:让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待public void countDown()
:计数器进行减 1(down 1)
应用:同步等待多个 Rest 远程调用结束
// LOL 10人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService service = Executors.newFixedThreadPool(10);
String[] all = new String[10];
Random random = new Random();
for (int j = 0; j < 10; j++) {
int finalJ = j;//常量
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
Thread.sleep(random.nextInt(100)); //随机休眠
all[finalJ] = i + "%";
System.out.print("\r" + Arrays.toString(all)); // \r代表覆盖
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始");
service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始
CyclicBarrier
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,非常像但是要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。
读写锁
读写锁的内部包含了两把锁:一把是为读(操作)锁,是一种共享锁;另一把写(操作)锁,是一种独占锁。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;进一步来说,如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。
JUC包中的读写锁接口为ReadWriteLock,主要有两个方法,具体如下:
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
通过ReadWriteLock接口能获取其内部的两把锁:一把ReadLock,负责读操作;另一把是WriteLock,负责写操作。 JUC中ReadWriteLock接口实现类为ReentrantReadWriteLock。 其读锁是可以多线程共享的共享锁,而其写锁是排他锁,在被占时候不允许其他线程再抢占操作。
public ReentrantReadWriteLock.ReadLock readLock()`:返回读锁
public ReentrantReadWriteLock.WriteLock writeLock()`:返回写锁
public void lock()`:加锁
public void unlock()`:解锁
public boolean tryLock()`:尝试获取锁
public class ReadWriteLockTest{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个读写锁
final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
//获取读锁
final static Lock READ_LOCK = LOCK.readLock();
//获取写锁
final static Lock WRITE_LOCK = LOCK.writeLock();
//对共享数据的写操作
public static Object put(String key, String value){
WRITE_LOCK.lock(); //抢写锁
try{
Print.tco(DateUtil.getNowTime()+" 抢占了WRITE_LOCK,开始执行write操作");
Thread.sleep(1000);
String put = MAP.put(key, value); //写入共享数据
return put;
} catch (Exception e){
e.printStackTrace();
} finally{
WRITE_LOCK.unlock(); //释放写锁
}
return null;
}
//对共享数据的读操作
public static Object get(String key){
READ_LOCK.lock(); //抢占读锁
try{
Print.tco(DateUtil.getNowTime()+" 抢占了READ_LOCK,开始执行read操作");
Thread.sleep(1000);
String value = MAP.get(key); //读取共享数据
return value;
} catch (InterruptedException e){
e.printStackTrace();
} finally{
READ_LOCK.unlock(); //释放读锁
}
return null;
}
//入口方法
public static void main(String[] args){
//创建Runnable异步可执行目标实例
Runnable writeTarget = () -> put("key", "value");
Runnable readTarget = () -> get("key");
//创建4个读线程
for (int i = 0; i < 4; i++){
new Thread(readTarget, "读线程" + i).start();
}
//创建2个写线程,并启动
for (int i = 0; i < 2; i++){
new Thread(writeTarget, "写线程" + i).start();
}
}
}
锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。 ReentrantReadWriteLock不支持读锁的升级,主要是避免死锁,例如两个线程A和B都占了读锁并且都需要升级成写锁, A升级要求B释放读锁, B升级要求A释放读锁,二者就会由于互相等待形成死锁。 与ReentrantLock相比, ReentrantReadWriteLock更适合于读多写少的场景,可以提高并发读的效率;而ReentrantLock更适合于读写比例相差不大或写比读多的场景。