需求
一次想跑多个线程,但是需求是,某个线程第一个执行,其执行完所有操作之后,后续线程再跑,又指定某一个线程必须等待其余线程执行完毕之后,它在执行。
模拟需求
1.创建三种不同需求线程,以满足第一执行线程,最后执行线程,普通线程。(只有一个线程类,也是可以实现,这边为了方便打出日志,简化操作)
2.创建程序入口,初始化各线程参数
实现的思路
1.利用java线程控制的wait、notifyAll用于实现某个线程第一个执行的需求。
2.利用CountDownLatch用于实现某一个线程必须等待其余线程执行完毕之后,它在执行的需求。
代码示例
主程序代码:功能就是创建一个固定大小为6的线程池,用于执行所有的线程。不做任何限制的情况下,第一次会跑6个线程。一个线程运行完毕,会自动加入一个新的线程进行执行,直至所有线程执行完毕。
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Mian {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(6);
List<Runnable> taskList = new ArrayList<Runnable>();
CountDownLatch countDownLatch = new CountDownLatch(10);
Object lock = new Object();
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new First(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Last(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.add(new Common(countDownLatch, lock));
taskList.forEach(t -> {
executorService.execute(t);
});
executorService.shutdown();
}
}
第一个执行线程代码:首先打了对应的提示,为了模拟正常的运行,采用for循环的方式占用cpu,比sleep更符合实际操作场景,同时也做了个简单的记时操作,用于验证是否其他线程处于等待。计算完毕之后,countDownLatch的记数减一,最后再把阻塞在lock对象上的所有线程唤醒。注意点在于执行唤醒操作时,确保想要阻塞的线程已经全部阻塞了,否则执行了唤醒操作后,还有线程才执行阻塞操作,这类线程就无法被唤醒了。
package thread;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
public class First implements Runnable {
private CountDownLatch countDownLatch;
private Object lock;
public First(CountDownLatch countDownLatch, Object lock) {
this.countDownLatch = countDownLatch;
this.setLock(lock);
}
public First() {
}
@Override
public void run() {
System.out.println("进入第一个线程");
System.out.println("进入第一个线程数值为:" + countDownLatch.getCount());
System.out.println(
"进入第一个线程开始时间" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 1000; j++) {
for (int k = 0; k < 1000; k++) {
for (int k2 = 0; k2 < 26000; k2++) {
}
}
}
}
System.out.println(
"进入第一个线程结束时间" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
countDownLatch.countDown();
System.out.println("结束进入第一个线程,此时线程记数值为:" + countDownLatch.getCount());
synchronized (lock) {
lock.notifyAll();
}
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public Object getLock() {
return lock;
}
public void setLock(Object lock) {
this.lock = lock;
}
}
普通大众角色代码:纯粹是为了模拟需求需要的线程。代码功能,先获取到countDownLatch记数,如果是初始值,表示一个线程都还没有执行完毕,就阻塞线程,否则就继续执行。这儿有个注意点:要想使用wait方法,必须先上锁,并且上锁的对象与线程所在阻塞对象要一致(如下图一),否则会抛出java.lang.IllegalMonitorStateException异常。
图一.png
package thread;
import java.util.concurrent.CountDownLatch;
public class Common implements Runnable {
private CountDownLatch countDownLatch;
private Object lock;
public Common(CountDownLatch countDownLatch, Object lock) {
this.setCountDownLatch(countDownLatch);
this.setLock(lock);
}
public Common(CountDownLatch countDownLatch) {
this.setCountDownLatch(countDownLatch);
}
public Common() {
}
@Override
public void run() {
synchronized (lock) {
long num = countDownLatch.getCount();
if (num == 10) {
try {
System.out.println("普通线程进入阻塞");
lock.wait();
System.out.println("阻塞的线程被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized (lock) {
System.out.println("进入普通线程了");
countDownLatch.countDown();
System.out.println("目前的线程记数值为:" + countDownLatch.getCount());
}
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public Object getLock() {
return lock;
}
public void setLock(Object lock) {
this.lock = lock;
}
}
最后一个执行线程:先获取countDownLatch记数,如果是第一个线程就阻塞,否则就往下执行;执行countDownLatch.await();输出相关信息
package thread;
import java.util.concurrent.CountDownLatch;
public class Last implements Runnable {
private CountDownLatch countDownLatch;
private Object lock;
public Last(CountDownLatch countDownLatch, Object lock) {
this.countDownLatch = countDownLatch;
this.setLock(lock);
}
public Last() {
}
@Override
public void run() {
synchronized (lock) {
long num = countDownLatch.getCount();
if (num == 10) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
synchronized (countDownLatch) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("进入最后一个线程,此时线程记数值为:" + countDownLatch.getCount());
System.out.println("结束最后一个线程");
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public Object getLock() {
return lock;
}
public void setLock(Object lock) {
this.lock = lock;
}
}
代码运行结果
图二.png
这个结果看起很漂亮,但是实际上这个不太符合业务场景。尤其是大众代码,正常情况下应该是并发运行。看上图代码发现: w.png
在阻塞唤醒之后,马上又进入锁代码,可以想象基本是单线程运行了。我之所以加锁,是因为countDownLatch.getCount()是不加锁的,我不加锁获取这个值就会是乱的,因为一个线程执行了countDown,还没有执行getCount。另一个线程可能又执行了countDown,导致获取到的值是不连续了。正常场景下,各线程执行本身互不影响,更多的是并发操作,提高效率。
效率
针对最开始的需求,我要是把线程池固定大小设置为1,第一个执行线程放在数组第一个,最后一个线程放最后一个,感觉还是可以实现需求,只不过是全程单线程执行任务。搞这么麻烦就是为了提升效率。所以同一个需求会有很多种实现,就是效率各不相同。
效率的验证
1.增加整个程序运行完毕时间。(这个不是说在主程序里面代码块前后加个输出时间就ok?因为线程的运行,不影响主线程,所以直接加肯定不对。正确的做法,进入主程序加一个时间为开始时间,最后一个线程加一个时间为结束时间)
2.增加大众线程运算时间。(直接一个输出,时间差基本可以忽略)
验证代码贴图
主程序加计时.png
增加大众线程运算时间.png
最后一个线程加计时.png
运行结果.png
从我实时看输出,也确如直接看代码分析一样,说是多线程实际还是单线程运行,因为基本属于全程加锁。也可以看到整个运行时间是52秒。输出效果看起还是整齐。
去掉大众代码中的锁,因为大众代码各自运行是互不影响的。(countDownLatch.countDown()本身自带锁)
改版大众代码.png
改版结果图.png
可以看到时间是8秒。效率应提升接近7倍。
懵逼???
从这个输出来看感觉有实现上面的需求吗?为啥最后一个线程不是最后输出呢?(线程记数值为啥不对,已经在上面‘为啥加锁’中说明了)
解惑
再瞧大众代码.png
先执行的是countDownLatch.countDown();然后执行计算操作,最后执行输出操作。大家也知道,唤醒最后一个线程的条件是线程记数等于0就可以唤醒了。这么写的确是有问题。所以一定要注意,countDownLatch.countDown()操作一定是在线程所有要做的事情做完再执行。否则就不是某一个线程必须等待前面线程执行完毕后执行。所以效率的统计也是有点问题,改哈大众代码,再看一遍
image.png
正确下的结果.png
改了一哈控制台字体,不然放不下。这下就可以看到最后一个线程是最后输出的(不是偶然,无论多少次都是最后输出,唯一会变化的是线程记数)
再来一发.png
记数没变就没变吧。最后一个线程还是最后输出了。两次时间都是7秒。效率提升是毋庸置疑的。
到此就算完成功能演示。
死锁
还是咱们的大众代码,改一哈如下图
改大众代码锁对象.png
刚刚已经说了这儿除非各线程互有影响或者其他什么原因,理论上是不应该加锁。
刚开始我加的锁对象是lock。改为countDownLatch,再次运行代码就会发现问题。运行结果如图:
死锁结果.png
不要以为我是中途代码运行时截图,实际无论你等多久程序都不会出结果,一直在等待。因为已经产生死锁。我这边就不弄工具去监测死锁了。(实际这个目前我还真不会。。。)产生死锁的原因
这个是因为 最后一个线程代码.png锁的对象是countDownLatch
从刚刚分析来看,大众代码 image.png 不需要锁。
普通大众代码锁对象依然是countDownLatch。但是countDownLatch.await();本身自带锁,进入阻塞之后不会释放countDownLatch锁对象。而普通大众代码又因为获取不到countDownLatch锁对象,所以进入不了countDownLatch.countDown();那么就导致普通对象无法使线程减一,最后一个线程也无法执行。所剩下的线程都无法继续执行。造成死锁。而我一开始锁的lock对象就没事。
造成死锁的原因就是滥用锁。
最后一个线程 image.pngcountDownLatch.await();自带锁也不需要加锁。两个地方都不加锁,自然就不会出现死锁了。
死锁的延伸
刚刚产生死锁的结论是countDownLatch.await();本身自带锁,进入阻塞之后不会释放countDownLatch锁对象。这个结论还需要验证。
验证多重加锁,最里面的锁对象进入阻塞,是否是释放外层锁对象。
主验证代码
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Mian2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(6);
List<Runnable> taskList = new ArrayList<Runnable>();
Object lock = new Object();
Object lock2 = new Object();
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.add(new CommonTest(lock2, lock));
taskList.forEach(t -> {
executorService.execute(t);
});
executorService.shutdown();
}
}
验证线程代码
package thread;
public class CommonTest implements Runnable {
private Object lock2;
private Object lock;
public CommonTest(Object lock2, Object lock) {
this.setLock2(lock2);
this.setLock(lock);
}
public CommonTest() {
}
@Override
public void run() {
System.out.println("进入线程");
synchronized (lock) {
System.out.println("进入一重锁");
synchronized (lock2) {
try {
System.out.println("进入二重锁");
lock2.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public Object getLock() {
return lock;
}
public void setLock(Object lock) {
this.lock = lock;
}
public Object getLock2() {
return lock2;
}
public void setLock2(Object lock2) {
this.lock2 = lock2;
}
}
输出结果
验证结果.png
可以看到其他线程压根就进入不到一重锁,证明了多重锁的情况下,内部阻塞只会释放第一层锁。
我们去掉一层看输出:
去掉一层锁.png
结果.png
所有线程都获取到了一重锁。也证明了上述结论的正确性。
结语
并发操作本身就比较复杂,当时发现死锁,我也是想了许久才发现多重锁的问题。最后,本文如有不正确之处,请评论指出。
网友评论