多线程-基础-2[Concurrent包下的并发线程]
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写入数据结束....