CountDownLatch
这是什么?先从一段代码开始:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i+1;
new Thread(()->{
System.out.println("线程:"+ finalI +"开始执行");
}).start();
}
}
你说这段代码中,这个10个线程是按照顺序执行的吗?看运行结果:
OK,看到结果之后你就需要明白如下道理:
⚠️在主线程中,for循环会创建并启动10个线程,但是需要注意的是,并不是线程创建且启动之后就会立马执行线程中的任务然后再去创建启动下一个线程,而是所有的线程创建启动之后会加入一个线程规划器中由操作系统去决定哪个线程先执行任务,这个分配是随机的,也就是线程执行是乱序的,不是顺序的!
然后我们再看:
这里的主线程任务可不是一定在线程执行完之后才会执行:
那如果我们有这样的一个场景,就是需要所有的线程任务执行完毕之后才能去执行主线程任务,那这个该怎么办呢?
这就需要用到我们的CountDownLatch,怎么用呢?很简单,看代码:
然后我们看下代码:
达到我们想要的结果了,是不是很简单?
Cyclicbarrier
对于CountDownLatch来说,大概就是只有计数为0才会触发某个时间,也就是一个倒计时的效果,而这个Cyclicbarrier恰好是和CountDownLatch相反的,它是需要从0开始计数,达到某个值才去触发某个时间,通常叫做回环栅栏,看下面的例子就能秒懂:
看下运行结果:
这个还是很好理解的!
semaphore
咋一看,这个看起来很陌生,是什么呢?一般叫做信号量,通过一个示例来快速了解!
这个例子没什么难度吧,我们开启5个线程来模拟小孩玩手机的现象,我们运行看下:
这说明5个小孩都抢到手机,都有手机玩,那这里的前提就是,最少得有5个手机,不然5个人不够抢啊,但是现在加入我就只有两个手机咋办?
是不是就得看他们自己了,谁抢到谁玩,因为只有2个手机,但是有5个人,那么肯定是2个人抢到了在玩,而另外3个人要等待,一旦其中的一个人玩手机时间到放下手机后,其余的三个人又会去抢,直到最后,每个人都玩手机了!
那面对这样的一个情景,想一下我们该怎么做?5个人去抢2个手机,一旦2个手机被抢到,其余的人只能等待,说的专业点,这样是不是并发线程数被控制了,这里控制了只有2个线程可以同时去执行,其他的必须等待这两个线程执行完毕才可以执行,那看看该怎么做:
这里大家可以类比小孩子玩手机的举例说明!
这里需要注意的是,我们是通过semaphore来控制线程并发数,其中使用semaphore.acquire();来记录进来的线程,执行一次相当于加1,一旦达到设定的线程并发数就会禁止其他线程进入,任务执行完毕使用semaphore.release();来减1,相当于释放资源,其他线程可以进入执行!
所以啊,任务执行逻辑是放在他们两者之间的:
ReadWriteLock
这是啥,有了Lock为什么还要有个ReadWriteLock?我们看下之前Lock加锁的形式:
我们在写资源的操作上加上了ReentrantLock,这个时候可以避免多个线程对该资源进行写操作,同一时间只允许一个线程来进行写操作,但是此时有什么问题呢?
看我们的读资源操作,此时是可以有多个线程进行读操作的,这其实也不是我们想要的,为什么?
因为如果此时有一个线程正在进行写操作,我是不希望其他线程进行读操作的,因为这个线程很可能读到旧数据,由于多线程执行是乱序的,所以很有可能数据还没有真正写入就被另一个线程读取了,虽然我们的map使用了volatile关键字,不过volatile也就是保证线程缓存中的资源实时同步到共享资源,但是如果我这个线程就没有开始真正的写,那线程本地缓存中就没有最新的数据,那你其他线程读取的自然不是最新的数据了!
你可能想到了给读操作也加上锁,像这样:
但是加上锁之后还是允许一个线程来进行读取资源,问题并没有解决啊,而且有的时候我还需要在没有线程进行写操作的时候,可以有多个线程进行读操作来增加效率,显然,上述做法是不满足要求的!
这个时候就需要我们的ReadWriteLock了,表面意思理解是读写锁,它主要可以保证数据的一致性,有如下特性:
-
支持读-读共存
-
不支持读-写共存
-
更不支持写-写共存
很明显的一点,只要有写操作存在,就不允许其他线程进行读或者写!
我们先来看下之前资源类被多线程执行的情况,先看不加锁的情况,同时对代码稍作修改:
然后分别创建5个线程进行写操作,5个线程进行读操作:
然后我们运行程序看看:
可以看到如此一来执行的结果很乱,在线程1执行写操作的时候还没有完成就没线程3给打断了,我们希望的是数据一致性得到保障,线程1的写操作完全成功了才允许其他线程继续写,而且假如我们模拟线程写数据的耗时操作:
我们在运行看看:
这就是因为,我数据还没有真正的写入成功就被其他线程读取,那结果肯定有误,怎们办,我们看下加锁的情况:
先对写操作加锁,看下运行情况:
这里一定要注意思考,不然很容易晕……好吧,为了大家更好的理解,我把程序改写一下,你会理解的更加透彻,首先修改资源类:
主要是这些改变,使得打印信息在加锁情况下更加直观,然后是我们的操作逻辑:
增加线程名称命名,然后我们再看程序运行结果:
这样看是不是就直观了很多,同一个时间内只能有一个线程进行读写操作,而且在线程进行写操作的时候,其他线程是无法进行读写操作的,但是这里也有个问题,就是如果我没有写操作在进行的时候,我想进行读操作,但是因为加锁,我只能一个线程进行读取,其实此时数据没有被其他线程进行写操作,我完全可以多个线程进行同时读取,这样效率更高,但是很显然通过Lock的形式是无法满足的!
这个时候就到我们的ReadWriteLock闪亮登场了!
这里需要再回顾下它的主要特性:
-
支持读-读共存
-
不支持读-写共存
-
更不支持写-写共存
对于读-写和写-写我们发现使用Lock也是可以实现的,但是它无法完成读-读共存的情况,我们看看下:
我们在读操作中模拟耗时操作,然后我们只运行多线程执行读操作的演示,结果如下:
实际运行就会发现,会花费5秒来执行完程序,因为每个线程执行读操作需要1秒,因为加锁是单线程执行,所以需要五秒,我们看下使用ReadWriteLock的情况:
这里一定要注意ReadWriteLock的用法,同Lock一样,它是个接口,需要使用其子类ReentrantReadWriteLock,同时为了区分读写锁,搞了一下形式:
//写锁
lock.writeLock()
//读锁
lock.readLock()
然后我们看使用了ReentrantReadWriteLock之后的读操作情况:
可以看到,即使你加锁了,但是对于读操作,是允许并发执行的,相当于之前是需要5秒,使用ReentrantReadWriteLock之后只需要1秒,效率大大提高!
所以对于ReentrantReadWriteLock来说记住其三个特性:
-
支持读-读共存
-
不支持读-写共存
-
更不支持写-写共存