关于JUC的一些概述(二)

目录

一.读写锁

1.读写锁原理

过程1:t1 w.lock, t2 r.lock(t1线程调用写锁的加锁,t2线程调用读锁的加锁)

过程2:t3 r.lock, t4 w.lock

过程3:t1.unlock

过程4:t2 r.unlock,t3 r.unlock

2.StampedLock

 二.信号量

1.介绍

2.原理

 三.CountdownLatch


接上次关于JUC的一些概述(一),上一节主要介绍了AQS的原理以及ReentrantLock的原理.

这一节主要介绍读写锁(ReentrantReadWriteLock)、信号量(Semaphore)、计数器(CountdownLatch)以及CyclicBarrier

一.读写锁

背景:当我们的对某数据的读操作频率远远大于写操作频率时,我们希望能够读-读并发,这样可以提高性能。然而如果在读、写操作都加入了synchronize,那么读-读操作也会同步等待,这是我们不希望看到的。因此引入了读写锁ReentrantReadWriteLock。

读写锁的作用是,其可以获得两种锁,分别是读锁和写锁。当某个线程获取到读锁时,另一个线程也可以获得这个读锁,实现读-读并发。但是只要操作中有写操作,那么都会堵塞。

即:读-读并发,读-写、写-读、写写都会同步阻塞等待。

注意:

  • 读锁不支持条件变量(即实现await等待)
  • 重入时,升级不支持(即如果某线程获得了读锁,那么不能再获得写锁。反之可以)
  • 读写锁中,读锁是共享模式,写锁的独占模式。

1.读写锁原理

读写锁其实用的是一个同步器Sycn,因此其等待队列以及state状态都是同一个,那前面有提到过,读写锁的state高16位负责读锁的状态,低16位负责写锁的状态。我们用state = x_x代表上锁状态。

过程1:t1 w.lock, t2 r.lock(t1线程调用写锁的加锁,t2线程调用读锁的加锁)

(1)t1成功加上写锁,将state的状态置为0_1

 (2)t2执行读写的加锁r.lock(),此时因为同步器的持有线程为t1,而写-读操作是会造成同步阻塞的,所以t2会加入同步器的等待队列,但需要注意的是,此时等待队列中的Node结点包含一种状态,即Node.SHARED模式和Node.EXCLUSIVE,因为t2加的是读锁,所以t2线程的结点的状态被设置成了Node.SHARED,并进入了阻塞

过程2:t3 r.lock, t4 w.lock

这时,线程t3和线程t4又分别加了读锁和写锁

过程3:t1.unlock

这时,线程t1释放写锁,若释放成功(将状态置0和exclusiveOwnerThread修改null),会执行唤醒流程,会让等待队列中的老二(头节点其实是一个哨兵,Dummy)恢复运行,此时会将t2的下一个线程t3置为等待队列中的新老二,但这个时候还没有结束,而是会查看当前老二的状态是否为共享模式,即Node.SHARED,明显t3加的时读锁,所以其模式也为共享模式,所以同时将线程t3唤醒,并将其从阻塞队列中出队,其下一个结点t4变成新的老二。然后反复这个过程,检查状态,这也是为什么读写锁能够实现读读并发的原因。显然这时候t4状态为独占模式,因此唤醒失败,因此为如下状态:

 注:因为有两个线程被唤醒,其占用了2个读锁,所以状态码是2_0(有点类似重入的感觉)

过程4:t2 r.unlock,t3 r.unlock

此时t2执行r.unlock会将状态码2_0 => 1_0,但此时由于计数还不为0,所以还会线程4还是不会被唤醒,知道t3执行了r.unlock,此时线程t4被唤醒,并开始竞争当前同步器,若竞争成功,则线程t4成为exclusiveOwnerThread的主人。

2.StampedLock

在JDK8以后,为了进一步优化读的性能(因为ReentrantLock对读其实还是加了锁的,只要加了锁,多少都会影响到性能),提出了一个StampedLock类,它的特点是在使用读锁、写锁时都必须配合【戳(Stamped)】使用。

其主要使用了乐观读的方法,在读取数据完毕后需要做一次戳校验,如果校验成功,说明期间并没有写操作,数据可以安全使用(有点类似于CAS),如果没有通过,才会获取读锁,保证数据安全。

具体的一个原理以及代码就不贴了,大家感兴趣可以去搜搜相关资料。

 二.信号量

1.介绍

 信号量Semaphore([ˈsɛməˌfɔr])是用来限制同时访问共享资源的线程上限的,其实AQS模式中的共享模式,举个例子就是停车场的问题(哈哈和,都是这个例子)

一个停车场(共享资源)有多个空位,同时有多个车(线程)想要进行停车,那么当停车场有空位时,车子就可以进行停车(访问共享资源),如果车位满了,则车子就要进行排队(线程阻塞)。其使用如下:

public static void main(String[] args) {
 // 1. 创建 semaphore 对象
     Semaphore semaphore = new Semaphore(3);
     // 2. 10个线程同时运行
     for (int i = 0; i < 10; i++) {
         new Thread(() -> {
         // 3. 获取许可
             try {
                 semaphore.acquire();
             } 
             catch (InterruptedException e) {
                 e.printStackTrace();
             }
             //当有空位才会执行以下操作
             try {
                 log.debug("running...");
                 sleep(1);
                 log.debug("end...");
             } 
             finally {
                 // 4. 释放许可
                 semaphore.release();
             }
         }).start();
     }
 }

2.原理

我们可以看到,其实当我们给予许可数时(停车场空位数),就会给同步器Sycn的state设置了相应的一个数值,假如Semaphore semaphore = new Semaphore(3);则state=3,这时假设有5个线程来获取资源

 其中,线程会通过CAS来竞争,假设线程1、2、4竞争成功,则线程0,3会进入AQS队列park阻塞,同时state置为0

若线程4执行了release释放了permits,则state会+1

 

接下来线程0会被唤醒,参与竞争剩下的许可,如果此时外界没有线程来竞争,或者线程0竞争成功,则如下状态

 

 三.CountdownLatch

1.介绍

与信号量不同的是,CountdownLatch主要用来线程同步协作,等待所有线程完成任务后,才会执行调用await()以后的代码。比如一个场景:学校放假

假设一个学校准备放假,那么学校的大门需要关闭(假设为线程0),此时有六个年级(线程1-6),那么学校的大门需要等到六个年级的学生把他们的门都关好了,出校门才能把学校大门关上。即学校大门(线程0的任务),需要等待六个年级(线程1-6完成任务)都关好门,出校园了。才能执行关门操作,就需要等待线程1-6的执行结束。这时就可以用CountdownLatch创建一个计数为6的计数器latch,然后在线程0调用latch.await()方法,那么线程1-6在执行完自己的任务后,调用latch.countDown()就会使计数器-1,直到计数器为0了,线程0就会恢复运行,执行关校门的动作。

这个场景其实可以想到有一个Thread的join方法,即哪个线程调用了join就要等待这个线程结束才会执行join后面的程序,但其实这个是有缺点的,就是你无法准确的得知有多少个线程需要被等待。有时线程量特别多,可能要写很多的join。

2.原理(略)

四.CyclicBarrier

1.介绍

CyclicBarrier循环栅栏作用与CountdownLatch类似,都是用来进行线程协作,等待线程满足某个计数,需要同步的线程调用await方法进行等待。

但是CyclicBarrier与CountdownLatch的最大区别主要是在于前者是可以重用的,即当计数减为0时,其又可以恢复到原来预设的计数,可以重新将CyclicBarrier对象再次进行计数,而CountdownLatch是不支持这类操作的,需要重新创建一个对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值