一般来说,每个线程自己完成自己的任务就可以了,但有时候,线程的处理会依赖另一个线程的数据,所以就需要线程间通信,来达到同步信息的效果。
下面通过几个例子,配合线程通信的方法来描述一下对他们的理解。
关键字(方法)
Thread.join()
,Object.wait()
,Object.notify()
,CountdownLatch
, CyclicBarrier
。
介绍
Thread.join()
private static void demoJoin() {
final Thread A = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A");
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
try{
A.join();
}catch(InterruptedException e){
}
System.out.println("B");
}
});
Thread C = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("C");
}
});
C.start();
B.start();
A.start();
}
结果:CAB,ABC
join的意思是在当前线程执行的过程中,把CPU让给另一个线程来执行。(可以控制线程依次执行)
- A:大哥;B:小弟;C:路人
- 兄弟一场,只要有我B的地方,肯定让A兄先发财。
- 大哥要是已经发财,小弟我就自己努力了
- 路人?他爱在哪在哪,反正小弟永远遵守上面两条
Object.wait(),Object.notify()
B是A的小外甥,A是B的长辈,季节到了,上来一批海货(梭子蟹),小可爱非常的馋可是又非常懂事,中间的谦让就不说了,最后长辈不吃或者吃了一个之后就给小可爱吃了,小可爱吃够了长辈才吃,下面我们用代码模拟一下这个传统美德。
private static void demoWait() {
final String lock = "螃蟹";//两人共享一盘梭子蟹
final Thread A = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("A----螃蟹1");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A----螃蟹2");
System.out.println("A----螃蟹3");
}
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("B----螃蟹1");
System.out.println("B----螃蟹2");
System.out.println("B----螃蟹3");
lock.notify();
}
}
});
A.start();
B.start();
}
结果:
A----螃蟹1
B----螃蟹1
B----螃蟹2
B----螃蟹3
A----螃蟹2
A----螃蟹3
如果不加锁,在执行一个线程的时候另一个会抢占。
wait()
方法表示当前线程让出执行权,notify()
表示唤醒wait()
状态的线程。
CountdownLatch
战争爆发了,D接到命令去保护ABC三个连队撤离。
有一个方案,ABC依次撤离,D最后(ABC依次撤离),实现方式在D中joinC,依次类推。但是那个时候谁先来谁先走呗,这样做效率太慢了,作为D来讲,我只要保证三个部队过去就行,你们仨爱咋办咋办,面向对象对不对。好的,那我掰指头算一下,走完仨就撤。
要模拟这个场景,看来我们需要一个计数器,ABC并行。
private static void protectedArmy() {
int armyNum = 3;
final CountDownLatch countDownLatch = new CountDownLatch(armyNum);
for (char army = 'A'; army <= 'C'; army++) {
final String name = String.valueOf(army);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(name + "部队正在撤离");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(name + "部队已经撤离");
countDownLatch.countDown();
}
}).start();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("D 正在断后(等待其他部队撤离)");
try {
countDownLatch.await();
System.out.println("ABC撤离完毕,D可以撤离了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
结果:
D部队正在断后(等待其他部队撤离)
A部队正在撤离
B部队正在撤离
C部队正在撤离
A部队已经撤离
B部队已经撤离
C部队已经撤离
ABC撤离完毕,D可以撤离了
具体实现过程是:
创建一个计数器,设置总的需要等待的线程数,然后再等待线程中调用countDownLatch.await()
方法,进入等待状态,直到计数值变成 0;在被等待线程中调用其他线程中调用countDownLatch.countDown()
方法,执行一次会将等待数目减去1,当变成0的时候,等待线程不再wait,继续执行之后的代码。
CyclicBarrier
有个赛车比赛,3名选手参赛,人齐才能开始,代码模拟一下。
private static void runABCWhenAllReady() {
int runner = 3;
final CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);
final Random random = new Random();
for (char runnerName = 'A'; runnerName <= 'C'; runnerName++) {
final String rN = String.valueOf(runnerName);
new Thread(new Runnable() {
@Override
public void run() {
long prepareTime = random.nextInt(10000) + 100;
System.out.println("裁判等了" + prepareTime + "毫秒," + rN + "选手到了");
try {
Thread.sleep(prepareTime);
} catch (Exception e) {
e.printStackTrace();
}
try {
System.out.println(rN + "选手准备好了,在等其他人");
cyclicBarrier.await(); // 当前选手准备好,等待其他人
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(rN + "比赛开始"); // 人齐开始比赛
}
}).start();
}
}
结果:
裁判等了133毫秒,A选手到了
裁判等了1642毫秒,B选手到了
裁判等了8062毫秒,C选手到了
A选手准备好了,在等其他人
B选手准备好了,在等其他人
C选手准备好了,在等其他人
C比赛开始
A比赛开始
B比赛开始
实现过程:
先创建一个公共 CyclicBarrier 对象,设置同时等待的线程数,这些线程同时开始自己做准备,自身准备完毕后,需要等待别人准备完毕,这时调用cyclicBarrier.await()
; 即可开始等待别人;
当指定的同时等待的线程数都调用了cyclicBarrier.await()
时,意味着这些线程都准备完毕好,然后这些线程同时继续执行。
CyclicBarrier与CountDownLatch比较
- CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行;
- CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
- CountDownLatch:一次性的;CyclicBarrier:可以重复使用。
- CountDownLatch基于AQS;CyclicBarrier基于锁和Condition。本质上都是依赖于volatile和CAS实现的
Callable
如果想在线程结束之后给你结果,可能就要用到Callable和FutureTask。
class Ticket implements Callable {
public static void main(String[] args) {
Ticket ticket = new Ticket();
FutureTask ft = new FutureTask<>(ticket);
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 2) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i+10;
}
}
结果:
main 的循环变量i的值0
main 的循环变量i的值1
main 的循环变量i的值2
有返回值的线程 0
有返回值的线程 1
有返回值的线程 2
有返回值的线程 3
子线程的返回值:14
call方法里并没有打印,但是循环确实运行了。
过程:
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行 体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该 FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,调用 get()方法会阻塞线程。
两个线程如何交替运行
两个线程交替打印数字,你打印一个我打印一个,我用锁写了一个实现方法。
class Ticket implements Runnable {
Object x = "90";
static int total = 10;
static Ticket t1 = new Ticket();
public static void main(String[] args) {
printMethod();
}
static void printMethod() {
Thread A = new Thread(t1);
Thread B = new Thread(t1);
A.setName("A");
B.setName("B");
A.start();
B.start();
}
@Override
public void run() {
synchronized (x) {
for (; total < 20; total++) {
System.out.println(Thread.currentThread().getName() + "------" + total);
x.notify();
try {
x.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果:
A------10
B------10
A------11
B------12
A------13
B------14
A------15
B------16
A------17
B------18
A------19