5.1 显式锁
Java内置锁的功能相对单一,不具备一些比较高级的锁功能,比如:
- 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃,不至于无限等下去。
- 可中断抢锁:在抢锁时,外部线程给抢锁线程发一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
多个等待队列:为锁维持多个等待队列,以便提高锁的效率。比如在生产者-消费者模式实现中,生产者和消费者共用一把锁,该锁上维持两个等待队列,即一个生产者队列和一个消费者队列。
5.1.1 显式锁Lock接口
5.1.2 可重入锁ReentrantLock
可重入的含义: 表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。比如,同一线程在外层函数获得锁后,在内层函数能再次获取该锁,甚至多次抢占到同一把锁。
下面是一段对可重入锁进行两次抢占和释放的伪代码,具体如下:
lock.lock(); // 第一次获取锁lock.lock(); // 第二次获取锁,重新进入try {// 临界区代码块} finally {lock.unlock(); // 释放锁lock.unlock(); // 第二次释放锁}
- 独占的含义:在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。
一个简单地使用ReentrantLock进行同步累加的演示案例如下:
package com.crazymakercircle.demo.lock;// 省略importpublic class LockTest{@org.junit.Testpublic void testReentrantLock(){// 每个线程的执行轮数final int TURNS = 1000;// 线程数final int THREADS = 10;//线程池,用于多线程模拟测试ExecutorService pool = Executors.newFixedThreadPool(THREADS);//创建一个可重入、独占的锁对象Lock lock = new ReentrantLock();// 倒数闩CountDownLatch countDownLatch = new CountDownLatch(THREADS);long start = System.currentTimeMillis();//10个线程并发执行for (int i = 0; i < THREADS; i++){pool.submit(() ->{try{//累加 1000 次for (int j = 0; j < TURNS; j++){//传入锁,执行一次累加IncrementData.lockAndFastIncrease(lock);}Print.tco("本线程累加完成");} catch (Exception e){e.printStackTrace();}//线程执行完成,倒数闩减少一次countDownLatch.countDown();});}
出于“分离变与不变”的设计原则,这里将临界区使用锁的代码进行了抽取和封装,形成一个可以复用的独立类——IncrementData累加类,具体代码如下:
package com.crazymakercircle.demo.lock;// 省略import//封装锁的使用代码public class IncrementData{public static int sum = 0;public static void lockAndFastIncrease(Lock lock){lock.lock(); //step1:抢占锁try{//step2:执行临界区代码sum++;} finally{lock.unlock(); //step3:释放锁}}// 省略其他代码}
5.1.3 使用显式锁的模板代码
- 使用lock()方法抢锁的模板代码
```java
//创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock
Lock lock = new SomeLock();
lock.lock(); //step1:抢占锁
try {
} finally {//step2:抢锁成功,执行临界区代码
}lock.unlock(); //step3:释放锁
2. 调用tryLock()方法非阻塞抢锁的模板代码调用tryLock()方法非阻塞抢占锁,大致的模板代码如下:```java//创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLockLock lock = new SomeLock();if (lock.tryLock()) { //step1:尝试抢占锁try {//step2:抢锁成功,执行临界区代码} finally {lock.unlock(); //step3:释放锁}}else{//step4:抢锁失败,执行后备动作}
调用tryLock(long time,TimeUnit unit)方法抢锁的模板代码 ```java //创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock Lock lock = new SomeLock(); //抢锁时阻塞一段时间,如1秒 if (lock.tryLock(1, TimeUnit.SECONDS)) { //step1:限时阻塞抢占
try {//step2:抢锁成功,执行临界区代码} finally {lock.unlock(); //step3:释放锁}
} else {
//限时抢锁失败,执行后备操作
}
<a name="JhvDl"></a>## 5.1.4 基于显式锁进行“等待-通知”方式的线程间通信与Object对象的wait、notify两类方法相类似,基于Lock显式锁,JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition1. Condition接口的主要方法```javapublic interface Condition{//方法1:等待。此方法在功能上与 Object.wait()语义等效//使当前线程加入 await() 等待队列中,并释放当前锁//当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁void await() throws InterruptedException;//方法2:通知。此方法在功能上与Object.notify()语义等效// 唤醒一个在 await()等待队列中的线程void signal();//方法3:通知全部。唤醒 await()等待队列中所有的线程//此方法与object.notifyAll()语义上等效void signalAll();//方法4:限时等待。此方法与await()语义等效//不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待//线程等待超时返回false,其他情况返回trueboolean await(long time, TimeUnit unit) throws InterruptedException;}
显式锁Condition演示案例 ```java package com.crazymakercircle.demo.lock; // 省略import public class ReentrantCommunicationTest {
// 创建一个显式锁static Lock lock = new ReentrantLock();// 获取一个显式锁绑定的Condition对象static private Condition condition = lock.newCondition();// 等待线程的异步目标任务static class WaitTarget implements Runnable{public void run(){lock.lock(); // ①抢锁try{Print.tcfo("我是等待方");condition.await(); // ② 开始等待,并且释放锁Print.tco("收到通知,等待方继续执行");} catch (InterruptedException e){e.printStackTrace();} finally{lock.unlock();//释放锁}}}//通知线程的异步目标任务static class NotifyTarget implements Runnable{public void run(){lock.lock(); //③抢锁try{Print.tcfo("我是通知方");condition.signal(); // ④发送通知Print.tco("发出通知了,但是线程还没有立马释放锁");} finally{lock.unlock(); //⑤释放锁之后,等待线程才能获得锁}}}public static void main(String[] args) throws InterruptedException{//创建等待线程Thread waitThread = new Thread(new WaitTarget(), "WaitThread");//启动等待线程waitThread.start();sleepSeconds(1); //稍等一下//创建通知线程Thread notifyThread = new Thread(new NotifyTarget(), "NotifyThread");//启动通知线程notifyThread.start();}
}
<a name="pILQn"></a>## 5.1.5 LockSupportLockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法。1. LockSupport的常用方法```java// 无限期阻塞当前线程public static void park();// 唤醒某个被阻塞的线程public static void unpark(Thread thread);// 阻塞当前线程,有超时时间的限制public static void parkNanos(long nanos);// 阻塞当前线程,直到某个时间public static void parkUntil(long deadline);// 无限期阻塞当前线程,带blocker对象,用于给诊断工具确定线程受阻塞的原因public static void park(Object blocker);// 限时阻塞当前线程,带blocker对象public static void parkNanos(Object blocker, long nanos);// 获取被阻塞线程的blocker对象,用于分析阻塞的原因public static Object getBlocker(Thread t);
LockSupport的演示实例 ```java package com.crazymakercircle.demo.lock; // 省略import public class LockSupportDemo {
public static class ChangeObjectThread extends Thread{public ChangeObjectThread(String name){super(name);}@Overridepublic void run(){Print.tco("即将进入无限时阻塞");//阻塞当前线程LockSupport.park();if (Thread.currentThread().isInterrupted()){Print.tco("被中断了,但仍然会继续执行");} else{Print.tco("被重新唤醒了");}}}//LockSupport 测试用例@org.junit.Testpublic void testLockSupport(){ChangeObjectThread t1 = new ChangeObjectThread("线程一");ChangeObjectThread t2 = new ChangeObjectThread("线程二");//启动线程一t1.start();sleepSeconds(1);//启动线程二t2.start();sleepSeconds(1);//中断线程一t1.interrupt();//唤醒线程二LockSupport.unpark(t2);}
}
3. LockSupport.park()和Thread.sleep()的区别3. LockSupport.park()与Object.wait()的区别下面的演示代码演示在LockSupport.park()执行之前,通过执行LockSupport.unPark()唤醒一个线程,具体如下:```javapackage com.crazymakercircle.demo.lock;// 省略importpublic class LockSupportDemo{@org.junit.Testpublic void testLockSupport2(){Thread t1 = new Thread(() ->{try{Thread.sleep(1000); //使sleep阻塞当前线程,时长为1秒} catch (InterruptedException e){e.printStackTrace();}Print.tco("即将进入无限时阻塞");//使用LockSupport.park()阻塞当前线程LockSupport.park();Print.tco("被重新唤醒了");}, "演示线程"); //通过匿名对象创建一个线程t1.start();//唤醒一次没有使用 LockSupport.park()阻塞的线程LockSupport.unpark(t1);//再唤醒一次没有调用 LockSupport.park()阻塞的线程LockSupport.unpark(t1);sleepSeconds(2);//中断线程一//第三唤醒调用 LockSupport.park()阻塞的线程LockSupport.unpark(t1);}// 省略其他}
5.1.6 显式锁的分类
显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。
- 可重入锁和不可重入锁
- 悲观锁和乐观锁
- 公平锁和非公平锁
- 可中断锁和不可中断锁
-
5.2 悲观锁和乐观锁
独占锁其实就是一种悲观锁,Java的synchronized是悲观锁。悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区。虽然悲观锁的逻辑非常简单,但是存在不少问题。
5.2.1 悲观锁存在的问题
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。
5.2.2 通过CAS实现乐观锁
5.2.3 不可重入的自旋锁
作为演示,这里先实现一个简单版本的自旋锁——不可重入的自旋锁,具体的代码如下:
package com.crazymakercircle.demo.lock.custom;// 省略importpublic class SpinLock implements Lock{/**当前锁的拥有者* 使用Thread 作为同步状态*/private AtomicReference<Thread> owner = new AtomicReference<>();/*** 抢占锁*/@Overridepublic void lock(){Thread t = Thread.currentThread();//自旋while (owner.compareAndSet(null, t)){// DO nothingThread.yield();//让出当前剩余的CPU时间片}}/*** 释放锁*/@Overridepublic void unlock(){Thread t = Thread.currentThread();//只有拥有者才能释放锁if (t == owner.get()){// 设置拥有者为空,这里不需要 compareAndSet操作// 因为已经通过owner做过线程检查owner.set(null);}}// 省略其他代码}
5.2.4 可重入的自旋锁
为了实现可重入锁,这里引入一个计数器,用来记录一个线程获取锁的次数。一个简单的可重入的自旋锁的代码大致如下:
package com.crazymakercircle.demo.lock.custom;// 省略importpublic class ReentrantSpinLock implements Lock{/**当前锁的拥有者* 使用拥有者 Thread 作为同步状态,而不是使用一个简单的整数作为同步状态*/private AtomicReference<Thread> owner = new AtomicReference<>();/*** 记录一个线程重复获取锁的次数* 此变量为同一个线程在操作,没有必要加上volatile保障可见性和有序性*/private int count = 0;/*** 抢占锁*/@Overridepublic void lock(){Thread t = Thread.currentThread();// 如果是重入,增加重入次数后返回if (t == owner.get()){++count;return;}//自旋while (owner.compareAndSet(null, t)){// DO nothingThread.yield();//让出当前剩余的CPU时间片}}/*** 释放锁*/@Overridepublic void unlock(){Thread t = Thread.currentThread();//只有拥有者才能释放锁if (t == owner.get()){if (count > 0){// 如果重入的次数大于0, 减少重入次数后返回--count;} else{// 设置拥有者为空// 这里不需要 compareAndSet, 因为已经通过owner做过线程检查owner.set(null);}}}// 省略其他代码}
自旋锁的问题:
在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。
5.2.5 CAS可能导致“总线风暴”
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final class Unsafe {//Unsafe中的CAS操作public final native boolean compareAndSwapInt(Object o, //操作对象long offset, //字段偏移int expected, //预期值int x); //待更新的值// 省略不相干代码}
5.2.6 CLH自旋锁
实现CLH锁的一个学习版本 ```java package com.crazymakercircle.demo.lock.custom; // 省略import public class CLHLock implements Lock {
/*** 当前节点的线程本地变量*/private static ThreadLocal<Node> curNodeLocal = new ThreadLocal();/*** CLHLock队列的尾部指针,使用AtomicReference,方便进行CAS操作*/private AtomicReference<Node> tail = new AtomicReference<>(null);public CLHLock(){//设置尾部节点tail.getAndSet(Node.EMPTY);}//加锁操作:将节点添加到等待队列的尾部@Overridepublic void lock(){Node curNode = new Node(true, null);Node preNode = tail.get();//CAS自旋:将当前节点插入队列的尾部while (!tail.compareAndSet(preNode, curNode)){preNode = tail.get();}//设置前驱节点curNode.setPrevNode(preNode);// 自旋,监听前驱节点的locked变量,直到其值为false// 若前驱节点的locked状态为true,则表示前一个线程还在抢占或者占有锁while (curNode.getPrevNode().isLocked()){//让出CPU时间片,提高性能Thread.yield();}// 能执行到这里,说明当前线程获取到了锁// Print.tcfo("获取到了锁!!!");//将当前节点缓存在线程本地变量中,释放锁会用到curNodeLocal.set(curNode);}//释放锁@Overridepublic void unlock(){Node curNode = curNodeLocal.get();curNode.setLocked(false);curNode.setPrevNode(null); //help for GCcurNodeLocal.set(null); //方便下一次抢锁}//虚拟等待队列的节点@Datastatic class Node{public Node(boolean locked, Node prevNode){this.locked = locked;this.prevNode = prevNode;}// true:当前线程正在抢占锁,或者已经占有锁// false:当前线程已经释放锁,下一个线程可以占有锁了volatile boolean locked;// 前一个节点,需要监听其locked字段Node prevNode;// 空节点public static final Node EMPTY = new Node(false, null);}// 省略其他代码
}
3. CLH锁的原理分析3. 举例说明:CLH锁的抢占过程3. 举例说明:CLH锁的释放过程3. CLH锁的优缺点<a name="esjQ7"></a># 5.3 公平锁与非公平锁<a name="nhlQE"></a>## 5.3.1 非公平锁实战使用ReentrantLock锁作为非公平锁的实战用例,具体代码如下:```javapackage com.crazymakercircle.basic.demo.lock;// 省略importpublic class LockTest{/*** 非公平锁测试用例*/@org.junit.Testpublic void testNotFairLock() throws InterruptedException{//创建可重入锁,默认的非公平锁Lock lock = new ReentrantLock(false);//创建Runnable可执行实例Runnable r = () -> IncrementData.lockAndIncrease(lock);//创建4个线程Thread[] tArray = new Thread[4];for (int i = 0; i < 4; i++){tArray[i] = new Thread(r, "线程" + i);}//启动4个线程for (int i = 0; i < 4; i++){tArray[i].start();}Thread.sleep(Integer.MAX_VALUE);}// 省略其他代码}
5.3.2 公平锁实战
什么是公平锁呢?公平锁是指多个线程按照申请锁的顺序来获取锁,抢锁成功的次序体现为FIFO(先进先出)顺序。虽然ReentrantLock锁默认是非公平锁,但可以通过构造器指定该锁为公平锁,具体的代码如下:
//可重入、公平锁对象Lock lock = new ReentrantLock(true);
下面是一个简单的公平锁实战案例。此实战案例并没有使用ReentrantLock锁,而是使用前面自定义的CLHLock锁进行演示,具体的实战代码如下:
package com.crazymakercircle.basic.demo.lock;// 省略importpublic class LockTest{/*** 公平锁测试用例*/@org.junit.Testpublic void testFairLock() throws InterruptedException{//创建为公平锁的类型Lock lock = new CLHLock();//创建Runnable可执行实例Runnable r = () -> IncrementData.lockAndIncrease(lock);//创建4个线程Thread[] tArray = new Thread[4];for (int i = 0; i < 4; i++){tArray[i] = new Thread(r, "线程" + i);}//启动4个线程for (int i = 0; i < 4; i++){tArray[i].start();}Thread.sleep(Integer.MAX_VALUE);}// 省略其他代码}
5.4 可中断锁与不可中断锁
可中断锁是指抢占过程可以被中断的锁,JUC的显式锁(如ReentrantLock)是一个可中断锁。不可中断锁是指抢占过程不可以被中断的锁,如Java的synchronized内置锁就是一个不可中断锁。
5.4.1 锁的可中断抢占
- lockInterruptibly()
- tryLock(long timeout,TimeUnit unit)
```java
package com.crazymakercircle.demo.lock;
// 省略import
public class IncrementData
{
}public static int sum = 0;//演示方法:可中断抢锁public static void lockInterruptiblyAndIncrease(Lock lock){Print.synTco(" 开始抢占锁");try{lock.lockInterruptibly();} catch (InterruptedException e){Print.synTco("抢占被中断,抢锁失败");// e.printStackTrace();return;}try{Print.synTco("抢到了锁,同步执行1秒");sleepMilliSeconds(1000);sum++;if (Thread.currentThread().isInterrupted()){Print.synTco("同步执行被中断");}} catch (Exception e){e.printStackTrace();} finally{lock.unlock();}}// 省略其他代码
以上代码的测试用例具体如下:```javapackage com.crazymakercircle.basic.demo.lock;// 省略importpublic class LockTest{//测试用例:抢锁过程可中断@org.junit.Testpublic void testInterruptLock() throws InterruptedException{//创建可重入锁,默认的非公平锁Lock lock = new ReentrantLock();//创建Runnable可执行任务实例Runnable r = () -> IncrementData.lockInterruptiblyAndIncrease(lock);Thread t1 = new Thread(r, "thread-1"); //创建第1条线程Thread t2 = new Thread(r, "thread-2"); //创建第2条线程t1.start(); //启动第1个线程t2.start(); //启动第2个线程sleepMilliSeconds(100);Print.synTco( "等待100毫秒,中断两个线程");t1.interrupt(); //启动第1个线程t2.interrupt(); //启动第2个线程Thread.sleep(Integer.MAX_VALUE);}// 省略其他代码}
5.4.2 死锁的监测与中断
JDK 8中包含的ThreadMXBean接口提供了多种监视线程的方法,其中包括两个死锁监测的方法,具体如下:
- findDeadlockedThreads
- findMonitorDeadlockedThreads
ThreadMXBean的实例可以通过JVM管理工厂ManagementFactory去获取,具体的获取代码如下:
//获取ThreadMXBean的实例public static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
在这里举一个死锁监测与中断的案例。首先定义一段需要抢占两把锁才能进入的临界区代码,具体如下:
package com.crazymakercircle.demo.lock;// 省略importpublic class TwoLockDemo{//演示代码:使用两把锁,通过可以中断的方式抢锁public static void useTowlockInterruptiblyLock(Lock lock1, Lock lock2) {String lock1Name =lock1.toString().replace("java.util.concurrent.locks.", "");String lock2Name =lock2.toString().replace("java.util.concurrent.locks.", "");Print.synTco(" 开始抢第一把锁, 为:" + lock1Name);try{lock1.lockInterruptibly();} catch (InterruptedException e){Print.synTco(" 被中断,抢第一把锁失败, 为:" + lock1Name);//e.printStackTrace();return;}try{Print.synTco(" 抢到了第一把锁, 为:" + lock1Name);Print.synTco(" 开始抢第二把锁, 为:" + lock2Name);try{lock2.lockInterruptibly();} catch (InterruptedException e){Print.synTco(" 被中断,抢第二把锁失败,为:" + lock2Name);//e.printStackTrace();return;}try{Print.synTco(" 抢到了第二把锁:" + lock2Name);Print.synTco("do something ");//等待1000mssleepMilliSeconds(1000);} catch (Exception e){e.printStackTrace();} finally{lock2.unlock();Print.synTco(" 释放了第二把锁, 为:" + lock2Name);}} catch (Exception e){e.printStackTrace();} finally{lock1.unlock();Print.synTco(" 释放了第一把锁, 锁为:" + lock1Name);}}
以上代码的测试用例如下:
package com.crazymakercircle.basic.demo.lock;// 省略importpublic class LockTest{//获取ThreadMXBeanpublic static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();//测试用例:抢占两把锁,造成死锁,然后进行死锁监测和部分中断@org.junit.Testpublic void testDeadLock() throws InterruptedException{//创建可重入锁,默认的非公平锁Lock lock1 = new ReentrantLock();Lock lock2 = new ReentrantLock();//Runnable异步执行目标实例1: 先抢占lock1,再抢占lock2Runnable r1 = () ->TwoLockDemo.useTowlockInterruptiblyLock(lock1, lock2);//Runnable异步执行目标实例2: 先抢占lock2,再抢占 lock1Runnable r2 = () ->TwoLockDemo.useTowlockInterruptiblyLock(lock2, lock1);Thread t1 = new Thread(r1, "thread-1"); //创建第1个线程Thread t2 = new Thread(r2, "thread-2"); //创建第2个线程t1.start(); //启动第1个线程t2.start(); //启动第2个线程//等待一段时间再执行死锁监测Thread.sleep(2000);Print.tcfo("等待2秒,开始死锁监测和处理");//获取到所有死锁线程的idlong[] deadlockedThreads = mbean.findDeadlockedThreads();if (deadlockedThreads.length > 0){Print.tcfo("发生了死锁,输出死锁线程的信息");//遍历数组获取所有的死锁线程idfor (long pid : deadlockedThreads){//此方法用于获取不带有堆栈跟踪信息的线程数据//hreadInfo threadInfo = mbean.getThreadInfo(pid);//此方法用于获取带有堆栈跟踪信息的线程数据ThreadInfo threadInfo = mbean.getThreadInfo(pid, Integer.MAX_VALUE);Print.tcfo(threadInfo);}Print.tcfo("中断一个死锁线程,这里是线程:" + t1.getName());t1.interrupt(); //中断一个死锁线程}}// 省略其他代码}
5.5 共享锁与独占锁
5.5.1 独占锁
5.5.2 共享锁Semaphore
共享锁使用示例
package com.crazymakercircle.demo.lock;// 省略importpublic class SemaphoreTest{@org.junit.Testpublic void testShareLock() throws InterruptedException{// 排队总人数(请求总数)final int USER_TOTAL = 10;// 可同时受理业务的窗口数量(同时并发执行的线程数)final int PERMIT_TOTAL = 2;// 线程池,用于多线程模拟测试final CountDownLatch countDownLatch =new CountDownLatch(USER_TOTAL);// 创建信号量,含有两个许可final Semaphore semaphore = new Semaphore(PERMIT_TOTAL);AtomicInteger index = new AtomicInteger(0);// 创建Runnable可执行实例Runnable r = () ->{try{//阻塞开始获取许可semaphore.acquire(1);//获取了一个许可Print.tco( DateUtil.getNowTime()+ ", 受理处理中...,服务号: " + index.incrementAndGet());//模拟业务操作: 处理排队业务Thread.sleep(1000);//释放一个信号semaphore.release(1);} catch (Exception e){e.printStackTrace();}countDownLatch.countDown();};//创建10个线程Thread[] tArray = new Thread[USER_TOTAL];for (int i = 0; i < USER_TOTAL; i++){tArray[i] = new Thread(r, "线程" + i);}//启动10个线程for (int i = 0; i < USER_TOTAL; i++){tArray[i].start();}countDownLatch.await();}}
5.5.3 共享锁CountDownLatch
package com.crazymakercircle.visiable;// 省略importclass Driver{private static final int N = 100; // 乘客数public static void main(String[] args) throws InterruptedException{ //step1:创建倒数闩,设置倒数的总数CountDownLatch doneSignal = new CountDownLatch(N);//取得CPU密集型线程池Executor e = ThreadUtil.getCpuIntenseTargetThreadPool();for (int i = 1; i <= N; ++i) // 启动报数任务e.execute(new Person(doneSignal, i));doneSignal.await(); //step2:等待报数完成,倒数闩计数值为0Print.tcfo("人到齐,开车"); }static class Person implements Runnable{private final CountDownLatch doneSignal;private final int i;Person(CountDownLatch doneSignal, int i){this.doneSignal = doneSignal;this.i = i;}public void run(){try{//报数Print.tcfo("第" + i + "个人已到");doneSignal.countDown(); //step3:倒数闩减少1} catch (Exception ex){}}}}
5.6 读写锁
JUC包中的读写锁接口为ReadWriteLock,主要有两个方法,具体如下:
public interface ReadWriteLock {/*** 返回读锁*/Lock readLock();/*** 返回写锁*/Lock writeLock();}
5.6.1 读写锁ReentrantReadWriteLock
接着进行代码演示,读锁是共享锁,写锁是排他锁:
package com.crazymakercircle.demo.lock;// 省略importpublic 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()
5.6.2 锁的升级与降级
锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。具体的演示代码如下:
package com.crazymakercircle.demo.lock;// 省略importpublic class ReadWriteLockTest2{//创建一个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);Print.tco( "尝试降级写锁为读锁");//写锁降级为读锁(成功)READ_LOCK.lock();Print.tco( "写锁降级为读锁成功");return put;} catch (Exception e){e.printStackTrace();} finally{READ_LOCK.unlock();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);Print.tco( "尝试升级读锁为写锁");//读锁升级为写锁(失败)WRITE_LOCK.lock();Print.tco("读锁升级为写锁成功");return value;} catch (InterruptedException e){e.printStackTrace();} finally{WRITE_LOCK.unlock();READ_LOCK.unlock();}return null;}public static void main(String[] args){//创建Runnable可执行实例Runnable writeTarget = () -> put("key", "value");Runnable readTarget = () -> get("key");//创建1个写线程,并启动new Thread(writeTarget, "写线程").start();//创建1个读线程new Thread(readTarget, "读线程").start();}}
5.6.3 StampedLock
StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。
- 悲观读锁的获取与释放 ```java //获取普通读锁(悲观读锁),返回long类型的印戳值 public long readLock()
//释放普通读锁(悲观读锁),以取锁时的印戳值作为参数 public void unlockRead(long stamp)
2. 写锁的获取与释放```java//获取写锁,返回long类型的印戳值public long writeLock()//释放写锁,以获取写锁时的印戳值作为参数public void unlockWrite(long stamp)
- 乐观读的印戳获取与有效性判断 ```java //获取乐观读,返回long类型的印戳值,返回0表示当前锁处于写锁模式,不能乐观读 public long tryOptimisticRead()
//判断乐观读的印戳值是否有效,以tryOptimisticRead返回的印戳值作为参数 public long tryOptimisticRead(long stamp)
StampedLock的演示案例```javapackage com.crazymakercircle.demo.lock;// 省略importpublic class StampedLockTest{//创建一个Map,代表共享数据final static Map<String, String> MAP = new HashMap<String, String>();//创建一个印戳锁final static StampedLock STAMPED_LOCK = new StampedLock();//对共享数据的写操作public static Object put(String key, String value){long stamp = STAMPED_LOCK.writeLock(); //尝试获取写锁的印戳try{Print.tco(getNowTime() + " 抢占了WRITE_LOCK,开始执行write操作");Thread.sleep(1000);String put = MAP.put(key, value);return put;} catch (Exception e){e.printStackTrace();} finally{Print.tco(getNowTime() + " 释放了WRITE_LOCK");STAMPED_LOCK.unlockWrite(stamp); //释放写锁}return null;}//对共享数据的悲观读操作public static Object pessimisticRead(String key){Print.tco(getNowTime() + "LOCK进入过写模式,只能悲观读");//进入了写锁模式,只能获取悲观读锁long stamp = STAMPED_LOCK.readLock(); //尝试获取读锁的印戳try{//成功获取到读锁,并重新获取最新的变量值Print.tco(getNowTime() + " 抢占了READ_LOCK");String value = MAP.get(key);return value;} finally{Print.tco(getNowTime() + " 释放了READ_LOCK");STAMPED_LOCK.unlockRead(stamp); //释放读锁}}//对共享数据的乐观读操作public static Object optimisticRead(String key){String value = null;//尝试进行乐观读long stamp = STAMPED_LOCK.tryOptimisticRead();if (0 != stamp){Print.tco(getNowTime() + "乐观读的印戳值,获取成功");sleepSeconds(1); //模拟耗费时间1秒value = MAP.get(key);} else // 0 == stamp 表示当前为写锁模式{Print.tco(getNowTime() + "乐观读的印戳值,获取失败");//LOCK已经进入写模式,使用悲观读方法return pessimisticRead(key);}//乐观读操作已经间隔了一段时间,期间可能发生写入//所以,需要验证乐观读的印戳值是否有效,即判断LOCK是否进入过写模式if (!STAMPED_LOCK.validate(stamp)){//乐观读的印戳值无效,表明写锁被占用过
