题目一:两个线程顺序打印1~100
题解:实现Runnable接口,利用wait()和notify()来交替打印
public class PrintThread implements Runnable {
private int count = 0;
@Override
public void run() {
synchronized (this) {
while (count <= 100) {
//唤醒其他需要this锁的进程来竞争锁,当前进程等到临界区代码执行完毕才释放锁
this.notify();//代码一处
System.out.println(Thread.currentThread().getName() + "当前数字是:" + count++);
//休息一段时间,放大差异
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//立即阻塞当前线程
try {
this.wait();//代码二处
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试代码
public class PrintTest {
public static void main(String[] args){
PrintThread printThread = new PrintThread(); //代码三处
new Thread(printThread, "Thread-A").start();
new Thread(printThread, "Thread-B").start();
}
}
做的过程中发现有趣的点:这个循环打印0~100的也可以使用继承Thread来实现,只需要将PrintThread类后面的Implements Runnable换成Extends Thread即可,原因:因为Thread其实也是继承Runnable的,这里只是将Thread当作Runnable用(代码三处),不过不建议这么用。
分析:
先说两个注意的点:
(1)调用了notify()或者notigyAll()并不是立马就释放锁的,需要等待当前线程把临界区的代码执行完才释放。
(2)调用wait()之后当前线程会立即阻塞,JVM会在等待wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。
-
为什么实现Runnable接口,而不是继承Thread类?
答:因为这道题让我们两个线程顺序打印0~100。Thread不适合资源共享,而Runnable很适合资源共享。提一下为什么Runnable适合资源共享,因为实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以理解为任务是通过线程驱动从而执行的。
-
Runnable相比与Thread具有的优势
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
-
等待通知机制的定义
先来看一下等待 / 通知的定义(摘抄自《Java并发编程的艺术》):等待 / 通知的相关方法是任何一个Java对象都具有的。等待 / 通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另外一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
-
代码一处的this.notify()调用后,会等待synchronized(this){}的所有代码执行完毕之后才释放,并不是立马释放的
-
代码二处的this.wait()
当前线程调用了this.wait()方法之后,阻塞的是当前线程,然后唤醒等待this锁的线程
题目二:三个线程交替打印10次ABC
题解:使用同步块和wait()、notify()的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能进行打印操作。一个对象是prev,即前个对象的锁,另外一个是自身对象的锁self,为了控制打印顺序,必须要先持有prev锁的情况下才能进行打印
public class PrintABC {
public static class Printer implements Runnable {
private String name;
private Object prev;
private Object self;
public Printer(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
//循环10次
while (count > 0){
//先获取prev锁
synchronized (prev) {
//接着获取self锁
synchronized (self) {
//打印
System.out.println(name);
count--;
//唤醒其他需要用到self锁的线程来竞争
//注意此时self锁还未释放
self.notify();
}
//现在才释放self锁,因为需要等到临界区代码执行完毕
//立即释放prev锁,并当前线程阻塞,等待其他线程唤醒
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
Printer pa = new Printer("A", c, a);
Printer pb = new Printer("B", a, b);
Printer pc = new Printer("C", b, c);
new Thread(pa).start();
Thread.sleep(10);//保证初始ABC的启动顺序
new Thread(pb).start();
Thread.sleep(10);
new Thread(pc).start();
Thread.sleep(10);
}
}
更进一步理解等待 / 通知机制,以线程A举例
- prev.wait()会释放锁,释放谁的锁?因为是prev.wait(),所以按照定义,释放的是prev锁即c锁,阻塞当前线程即线程A
- self.notify()是释放哪个锁,唤醒哪个线程?因为是self对象,在线程A中是a,所以释放的是a锁,持有a锁的还有线程B,所以唤醒的是线程B
- 怎么唤醒线程A?是使用a.nofity()还是c.notify()?因为前面使用的是c.wait()阻塞当前线程,所以使用c.notify()来唤醒,即线程C里面的方法
这样一来就可以保证ABC顺序打印,除了这种方法之外,这道题还可以使用Lock锁方法来实现。