10.1、CountDownLatch :閉鎖,線程遞減鎖
需要指定一個數字,可以同構await()方法產生阻塞,直到數字減爲0時,阻塞自動被解開,可以通過contDown()方法使數字遞減。經常用於監聽某些初始化操作,等待初始化執行完畢後,通知主線程繼續工作。
/**
* ContDownLatch :用於程序資源初始化時
*/
@Slf4j
public class UserCountDownLatch {
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(2);
/*主線程加載*/
new Thread(()->{
log.info("主線程{}加載資源,初始化開始...",Thread.currentThread().getName());
try {
countDownLatch.await(); //主線程阻塞
log.info("主線程{}加載資源,初始化結束,開始執行任務...",Thread.currentThread().getName());
} catch (InterruptedException e) {
log.error("主線程執行異常,原因:{}",e);
}
},"t1").start();
/**
* 模擬資源初始化加載1
*/
new Thread(() -> {
log.info("線程{}初始化數據開始......",Thread.currentThread().getName());
try {
Thread.sleep(5000);
log.info("線程{}初始化數據結束......",Thread.currentThread().getName());
} catch (InterruptedException e) {
log.error("線程{}初始化數據異常......",Thread.currentThread().getName());
}
countDownLatch.countDown();
},"t2").start();
/**
* 模擬資源初始化加載2
*/
new Thread(() -> {
log.info("線程{}初始化數據開始......",Thread.currentThread().getName());
try {
Thread.sleep(3000);
log.info("線程{}初始化數據結束......",Thread.currentThread().getName());
} catch (InterruptedException e) {
log.error("線程{}初始化數據異常......",Thread.currentThread().getName());
}
countDownLatch.countDown();
},"t3").start();
}
}
14:18:51.057 [t3] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 線程t3初始化數據開始......
14:18:51.057 [t1] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 主線程t1加載資源,初始化開始....
14:18:51.057 [t2] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 線程t2初始化數據開始......
14:18:54.061 [t3] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 線程t3初始化數據結束......
14:18:56.061 [t2] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 線程t2初始化數據結束......
14:18:56.061 [t1] INFO com.qiulin.study.thread.day03.UserCountDownLatch - 主線程t1加載資源,初始化結束,開始執行任務........
10.2、CyclicBarrier:柵欄
適用於希望多個線程在某一個節點找齊,當指定數量的線程都到達該節點時再回去同時放行接着執行。
/**
*eg:三個運動員進入賽道,當最後一個到達的時候,立即開始比賽
**/
@Slf4j
public class UseCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
log.info("有3位選手陸續進入比賽場地.........");
ExecutorService executorService = new ThreadPoolExecutor(3,3, 10,TimeUnit.SECONDS,new LinkedBlockingQueue<>());
Arrays.asList("張三","李四","王五").stream().forEach(x->{
executorService.execute(new Runner(cyclicBarrier,x, new Random().nextInt(10000)));
});
executorService.shutdown();
}
/**
* Runner
*/
static class Runner implements Runnable {
private CyclicBarrier cyclicBarrier;
private String name;
private int takeTime;
public Runner(CyclicBarrier cyclicBarrier,String name,int takeTime){
this.cyclicBarrier = cyclicBarrier;
this.name = name;
this.takeTime = takeTime;
}
@Override
public void run() {
try {
log.info("運動員{}走向賽道...",name);
Thread.sleep(takeTime);
cyclicBarrier.await();
log.info("運動員{}到達起點,開始比賽...",name);
Thread.sleep(takeTime);
log.info("運動員{}到達終點,比賽結束,用時{}秒...",name,takeTime);
}catch (Exception e){
log.error("運動員{}受傷,退出比賽...",name);
}
}
}
}
14:22:53.809 [main] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 有3位選手陸續進入比賽場地.........
14:22:53.842 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員李四走向賽道...
14:22:53.842 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員張三走向賽道...
14:22:53.842 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員王五走向賽道...
14:23:02.507 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員張三到達起點,開始比賽...
14:23:02.507 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員王五到達起點,開始比賽...
14:23:02.507 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員李四到達起點,開始比賽...
14:23:04.920 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員張三到達終點,比賽結束,用時2413秒...
14:23:09.576 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員王五到達終點,比賽結束,用時7068秒...
14:23:11.171 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseCyclicBarrier - 運動員李四到達終點,比賽結束,用時8664秒...
10.3、Semaphore:信號量
Semaphonre是計數信號量。Semaphone管理一系列許可證。每個acquire方法阻塞,acquire()方法拿走一個許可證,每個release()釋放一個許可。實際上並沒有許可證這個對象,Semaphore只是維護了一個可獲得許可證的數量。
**
* Semaphore :信號量
* eg:賽道上有4個賽道,每個賽道只允許一個人,有8個運動員需要比賽,當一個運動員跑完時,另一個運動員馬上佔用賽道
*/
@Slf4j
public class UseSemaphore {
public static void main(String[] args) {
//LinkedBlockingQueue:無界隊列,所以maximunPoolSize大小無論設置多少,其實無效
ExecutorService executorService = new ThreadPoolExecutor(4,4, 10, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
Semaphore semaphore = new Semaphore(4);
LongStream.range(1,8).boxed().forEach(x->{
executorService.execute(new Runner(semaphore,"線程"+x,new Random().nextInt(1000)));
});
executorService.shutdown();
}
/**
* 運動員
*/
static class Runner implements Runnable {
private Semaphore semaphore;
private String name;
private int takeTime;
public Runner(Semaphore semaphore,String name,int takeTime){
this.semaphore = semaphore;
this.name = name;
this.takeTime = takeTime;
}
@Override
public void run() {
try {
log.info("運動員{}走向賽道...",name);
Thread.sleep(takeTime);
semaphore.acquire(); //獲取許可
log.info("運動員{}到達起點,開始比賽...",name);
Thread.sleep(takeTime);
semaphore.release(); //釋放許可
log.info("運動員{}到達終點,比賽結束,用時{}秒...",name,takeTime);
}catch (Exception e){
log.error("運動員{}受傷,退出比賽...",name);
}
}
}
}
14:29:58.452 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程1走向賽道...
14:29:58.452 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程4走向賽道...
14:29:58.452 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程2走向賽道...
14:29:58.452 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程3走向賽道...
14:29:58.880 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程1到達起點,開始比賽...
14:29:59.258 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程4到達起點,開始比賽...
14:29:59.304 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程1到達終點,比賽結束,用時424秒...
14:29:59.304 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程5走向賽道...
14:29:59.406 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程3到達起點,開始比賽...
14:29:59.406 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程2到達起點,開始比賽...
14:29:59.882 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程5到達起點,開始比賽...
14:30:00.061 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程4到達終點,比賽結束,用時803秒...
14:30:00.061 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程6走向賽道...
14:30:00.235 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程6到達起點,開始比賽...
14:30:00.357 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程2到達終點,比賽結束,用時951秒...
14:30:00.357 [pool-1-thread-3] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程3到達終點,比賽結束,用時951秒...
14:30:00.357 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程7走向賽道...
14:30:00.409 [pool-1-thread-4] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程6到達終點,比賽結束,用時173秒...
14:30:00.461 [pool-1-thread-1] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程5到達終點,比賽結束,用時578秒...
14:30:00.846 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程7到達起點,開始比賽...
14:30:01.336 [pool-1-thread-2] INFO com.qiulin.study.thread.day03.UseSemaphore - 運動員線程7到達終點,比賽結束,用時489秒..
10.4、ReentrantLock:重入鎖
在需要進行同步的代碼部分加上鎖定,最終一定要釋放,否則會造成該鎖永遠無
法釋放,其他線程永遠進不來。
10.4.1、線程同步
/**
* 重入鎖,線程阻塞
*/
@Slf4j
public class UseReentrantLock {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final UseReentrantLock reentrantLock = new UseReentrantLock();
new Thread(() -> reentrantLock.test1()).start();
new Thread(() -> reentrantLock.test2()).start();
}
public void test1(){
try {
lock.lock();
log.info("當前線程:{}進入.....",Thread.currentThread().getName());
Thread.sleep(2000);
log.info("當前線程:{}退出.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
public void test2(){
try {
lock.lock();
log.info("當前線程:{}進入.....",Thread.currentThread().getName());
Thread.sleep(2000);
log.info("當前線程:{}退出.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
}
21:12:22.212 [Thread-0] INFO com.qiulin.study.thread.day03.UseReentrantLock - 當前線程:Thread-0進入.....
21:12:24.215 [Thread-0] INFO com.qiulin.study.thread.day03.UseReentrantLock - 當前線程:Thread-0退出.....
21:12:24.215 [Thread-1] INFO com.qiulin.study.thread.day03.UseReentrantLock - 當前線程:Thread-1進入.....
21:12:26.216 [Thread-1] INFO com.qiulin.study.thread.day03.UseReentrantLock - 當前線程:Thread-1退出.....
10.4.2、Condition :條件通知
使用Synchronized的時候,如果需要多個線程間進行通信需要Object的wait()和notify()、notifyAll()方法進行配合工作。
在Lock對象中,有一個新的等待通知類-Condition。這個Condition一定是針對具體的某一把鎖的,在只有鎖的基礎上產生Condition。= 喚醒[不釋放鎖,直到finally中鎖釋放,才喚醒]【lock.newConditon()】
@Slf4j
public class UseCondition {
private Lock lock = new ReentrantLock();
/*獲取condition實例*/
private Condition condition = lock.newCondition();
public static void main(String[] args) {
final UseCondition useCondition = new UseCondition();
new Thread(() ->{
useCondition.method1();
},"t1").start();
new Thread(() ->{
useCondition.method2();
},"t2").start();
}
/**
* 阻塞: condition.await(); // object wait()
*/
public void method1(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(2000);
log.info("當前線程{}任務執行完畢,阻塞.....",Thread.currentThread().getName());
condition.await(); // object wait(),阻塞
log.info("當前線程{}被喚醒.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
/**
* 喚醒[不釋放鎖,直到finally中鎖釋放,才喚醒]: condition.signal(); // object notify()
*/
public void method2(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(3000);
log.info("當前線程{}任務執行完畢,發出通知,喚醒阻塞線程......",Thread.currentThread().getName());
condition.signal(); // object notify() 喚醒
Thread.sleep(1000);
log.info("線程{}繼續執行.......",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
}
21:36:51.786 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1,開始執行任務....
21:36:53.788 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1任務執行完畢,阻塞.....
21:36:53.788 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t2,開始執行任務....
21:36:56.789 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t2任務執行完畢,發出通知,喚醒阻塞線程......
21:36:57.790 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 線程t2繼續執行.......
21:36:57.790 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1被喚醒.....
10.4.3、多個Condition時,使用condition.signalAll()喚醒當前condition.await()的方法。
/**
* Condition使用
* 多個Condition的使用
*/
@Slf4j
public class UseCondition {
private final Lock lock = new ReentrantLock();
/*獲取condition實例*/
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
public static void main(String[] args) {
final UseCondition useCondition = new UseCondition();
new Thread(() ->{ useCondition.method1(); },"t1").start();
new Thread(() ->{ useCondition.method2(); },"t2").start();
new Thread(() ->{ useCondition.method3(); },"t3").start();
new Thread(() ->{ useCondition.method4(); },"t4").start();
new Thread(() ->{ useCondition.method5(); },"t5").start();
}
/**
* 阻塞: condition1.await(); // object wait()
*/
public void method1(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(1000);
log.info("當前線程{}任務執行完畢,阻塞.....",Thread.currentThread().getName());
condition1.await(); // object wait(),阻塞
log.info("當前線程{}被喚醒.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
/**
* 阻塞: condition1.await(); // object wait()
*/
public void method2(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(2000);
log.info("當前線程{}任務執行完畢,阻塞.....",Thread.currentThread().getName());
condition1.await(); // object wait(),阻塞
log.info("當前線程{}被喚醒.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
/**
* 阻塞: condition2.await(); // object wait()
*/
public void method3(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(3000);
log.info("當前線程{}任務執行完畢,阻塞.....",Thread.currentThread().getName());
condition2.await(); // object wait(),阻塞
log.info("當前線程{}被喚醒.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
/*
*喚醒[不釋放鎖,直到finally中鎖釋放,才喚醒]:condition1.signalAll();
*/
public void method4(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(4000);
log.info("當前線程{}任務執行完畢,阻塞.....",Thread.currentThread().getName());
condition1.signalAll(); // object notifyAll()喚醒
log.info("當前線程{}被喚醒.....",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
/**
* 喚醒[不釋放鎖,直到finally中鎖釋放,才喚醒]: condition2.signal(); // object notify()
*/
public void method5(){
try {
lock.lock();
log.info("當前線程{},開始執行任務....",Thread.currentThread().getName());
Thread.sleep(5000);
log.info("當前線程{}任務執行完畢,發出通知,喚醒阻塞線程......",Thread.currentThread().getName());
condition2.signal(); // object notify() 喚醒
Thread.sleep(1000);
log.info("線程{}繼續執行.......",Thread.currentThread().getName());
}catch (Exception e){
log.error("線程{},異常原因{}",Thread.currentThread().getName(),e);
}finally {
lock.unlock();
}
}
}
21:52:21.734 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1,開始執行任務....
21:52:22.738 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1任務執行完畢,阻塞.....
21:52:22.738 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t2,開始執行任務....
21:52:24.738 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t2任務執行完畢,阻塞.....
21:52:24.738 [t3] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t3,開始執行任務....
21:52:27.739 [t3] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t3任務執行完畢,阻塞.....
21:52:27.739 [t4] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t4,開始執行任務....
21:52:31.740 [t4] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t4任務執行完畢,阻塞.....
21:52:31.740 [t4] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t4被喚醒.....
21:52:31.740 [t5] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t5,開始執行任務....
21:52:36.741 [t5] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t5任務執行完畢,發出通知,喚醒阻塞線程......
21:52:37.741 [t5] INFO com.qiulin.study.thread.day03.UseCondition - 線程t5繼續執行.......
21:52:37.741 [t1] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t1被喚醒.....
21:52:37.741 [t2] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t2被喚醒.....
21:52:37.741 [t3] INFO com.qiulin.study.thread.day03.UseCondition - 當前線程t3被喚醒.....
10.4.4、ReentrantReadWriteLock:讀寫鎖
讀寫鎖ReentrantReadWriteLock,其核心就是實現讀寫分離的鎖。在高併發場景下,尤其是在讀多寫少的情況下,性能遠高於重入鎖。但是在寫多讀少的情況下,使用ReentrantLock性能高於讀寫鎖。
對於Synchronized、ReentrantLock鎖而言,同一時間內,只能有一個線程訪問被鎖定的代碼塊。而讀寫鎖本質是讀寫分離。即包含兩個鎖(讀鎖,寫鎖)。在讀鎖情境下,多個線程可以併發訪問,但是在寫鎖的時候,只能一個一個順序訪問。即【讀讀共享,寫寫互斥,讀寫互斥】
/**
* 讀寫鎖(讀多寫少的場景下使用)
*/
@Slf4j
public class UseReentrantLockReadAndWrite {
private final ReentrantReadWriteLock lock= new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public static void main(String[] args) {
UseReentrantLockReadAndWrite uselock = new UseReentrantLockReadAndWrite();
new Thread(() ->{uselock.read1();},"t1").start();
new Thread(() ->{uselock.read1();},"t2").start();
new Thread(() ->{uselock.write1();},"t3").start();
new Thread(() ->{uselock.write2();},"t4").start();
}
/**
* 讀 //4s
*/
private void read1(){
try {
readLock.lock();
log.info("當前線程:{}讀取數據開始....",Thread.currentThread().getName());
Thread.sleep(4000);
log.info("當前線程:{}讀取數據結束....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
} finally {
readLock.unlock();
}
}
/**
* 讀 //3s
*/
private void read2(){
try {
readLock.lock();
log.info("當前線程:{}讀取數據開始....",Thread.currentThread().getName());
Thread.sleep(3000);
log.info("當前線程:{}讀取數據結束....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
} finally {
readLock.unlock();
}
}
/**
* 寫 //2s
*/
private void write1(){
try {
writeLock.lock();
log.info("當前線程:{}寫入數據開始....",Thread.currentThread().getName());
Thread.sleep(2000);
log.info("當前線程:{}寫入數據結束....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
} finally {
writeLock.unlock();
}
}
/**
* 寫 //3s
*/
private void write2(){
try {
writeLock.lock();
log.info("當前線程:{}寫入數據開始....",Thread.currentThread().getName());
Thread.sleep(3000);
log.info("當前線程:{}寫入數據結束....",Thread.currentThread().getName());
}catch (Exception e){
log.error("當前線程:{},出現異常,原因:{}",Thread.currentThread().getName(),e);
} finally {
writeLock.unlock();
}
}
}
22:33:22.507 [t2] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t2讀取數據開始....
22:33:22.507 [t1] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t1讀取數據開始....
22:33:26.511 [t2] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t2讀取數據結束....
22:33:26.511 [t1] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t1讀取數據結束....
22:33:26.511 [t3] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t3寫入數據開始....
22:33:28.512 [t3] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t3寫入數據結束....
22:33:28.512 [t4] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t4寫入數據開始....
22:33:31.513 [t4] INFO com.qiulin.study.thread.day03.UseReentrantLockReadAndWrite - 當前線程:t4寫入數據結束....