了解了在 Java 中如何启动一个线程,并且学习了 Thread 类的 API 以及线程的状态和锁的概念后,接下来我们学习一下线程之间的等待和通知,也就是wait/notify。我们首先思考一个问题,为什么要有wait/notify?
线程和线程之间并不是完全封闭的,可以通过共享变量进行线程间通信。但是多个线程访问同一共享变量时,如果没有同步机制,那么共享变量的值可能会出问题,并不是真正的结果,或者说不是准确的值,出现了运算错误,而我们的wait/notify就是为了解决这一问题的而出现的。
wait/notify 概念
当前线程启动后,调用对象的wait()方法,当前线程释放对象锁,从运行态进入阻塞态,然后进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒,再继续进入运行态,继续执行。
还有notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
线程wait
值得注意的是这两个方法不在Thread对象中,而是在Object中,也就是说所有的java类继承了这两个方法,而java类继承这两个方法的原因是java同步操作的需要,说着这里我们必须对线程同步有所了解。
线程同步
什么是线程同步呢?我们先看看什么是异步,异步其实就是指多个线程同时执行。但在多个线程同时执行的过程中,可能会访问共享资源,此时我们希望确保多个线程在同一时间只能有一个线程访问,此时就称之为线程同步,最简单的就是加锁来实现。
使用实例:
两位老师给学生留下抄单词的作业,最多留下十份作业,两个学生从十分作业中各自拿出一份抄写,当作业不够十份后,老师继续留作业,指导够十份为止,然后等待,不够十份继续留作业。学生一直抄写作业,当把老师留下的作业抄写完毕,等待,然后老师留作业后继续抄写。
public class Student extends Thread {
private String name;
private LinkedList<Task> tasks;
public Student(String name, LinkedList<Task> tasks) {
//调用Thread构造方法,设置threadName
super(name);
this.name = name;
this.tasks = tasks;
}
public void copyWord() throws InterruptedException {
String threadName = Thread.currentThread().getName();
while (true) {
Task task = null;
synchronized (tasks) {
if (tasks.size() > 0) {
task = tasks.removeFirst();
sleep(100);
tasks.notifyAll();
} else {
System.out.println(threadName+"开始等待");
tasks.wait();
System.out.println("学生线程 "+threadName + "线程-" + name + "等待结束");
}
}
if (task != null) {
for (int i = 1; i <= task.getLeftCopyCount(); i++) {
System.out.println(threadName + "线程-" + name + "抄写" + task.getWordToCopy() + "。已经抄写了" + i + "次");
}
}
}
}
//重写run方法,完成任务。
@Override
public void run() {
try {
copyWord();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Task {
private int leftCopyCount;
public int getLeftCopyCount() {
return leftCopyCount;
}
public Task(int leftCopyCount, String wordToCopy) {
this.leftCopyCount = leftCopyCount;
this.wordToCopy = wordToCopy;
}
public void setLeftCopyCount(int leftCopyCount) {
this.leftCopyCount = leftCopyCount;
}
public String getWordToCopy() {
return wordToCopy;
}
public void setWordToCopy(String wordToCopy) {
this.wordToCopy = wordToCopy;
}
private String wordToCopy;
}
public class Teacher extends Thread {
private String name;
private List<String> punishWords = Arrays.asList("internationalization", "hedgehog", "penicillin", "oasis", "nirvana", "miserable");
private LinkedList<Task> tasks;
private int MAX = 10;
public Teacher(String name, LinkedList<Task> tasks) {
//调用Thread构造方法,设置threadName
super(name);
this.name = name;
this.tasks = tasks;
}
public void arrangePunishment() throws InterruptedException {
String threadName = Thread.currentThread().getName();
while (true) {
synchronized (tasks) {
if (tasks.size() < MAX) {
Task task = new Task(new Random().nextInt(3) + 1, getPunishedWord());
tasks.addLast(task);
System.out.println(threadName + "留了作业,抄写" + task.getWordToCopy() + " " + task.getLeftCopyCount() + "次");
tasks.notifyAll();
} else {
System.out.println(threadName+"开始等待");
tasks.wait();
System.out.println("teacher线程 " + threadName + "线程-" + name + "等待结束");
}
}
}
}
//重写run方法,完成任务。
@Override
public void run() {
try {
arrangePunishment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private String getPunishedWord() {
return punishWords.get(new Random().nextInt(punishWords.size()));
}
}
public class WaitNotifyClient {
public static void main(String[] args) {
LinkedList<Task> tasks = new LinkedList<>();
Student xiaoming = new Student("小明", tasks);
xiaoming.start();
Student xiaowang = new Student("小王", tasks);
xiaowang.start();
Teacher lilaoshi = new Teacher("李老师", tasks);
lilaoshi.start();
Teacher zhanglaoshi = new Teacher("张老师", tasks);
zhanglaoshi.start();
}
}
运行结果如下:
使用中的注意事项:
1.wait是阻塞当前进程,必须获得锁才能wait,一般配置synchronized关键字使用。反之,如果线程能执行wait,notify/notify等方法,说明线程一定是获得了锁的。
2.当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。