目录
一、引入
Watch类:
加入我想创建一个Watch类,在其中定义两个线程,一个线程用来计算时间,一个线程用来输出时间,我们这样写:
package com.thread;
import java.util.Date;
public class Watch {
private static String time;
static class DisplayThread extends Thread{//显示器线程
@Override
public void run() {
System.out.println(time);
}
}
static class TimeThread extends Thread{
@Override
public void run() {
time = new Date().toString();
}
}
public static void main(String[] args) {//时间线程
new TimeThread().start();
new DisplayThread().start();
}
}
但是,由于线程抢占式运行的方式,我们无法保证在显示器线程输出前,时间线程已经为时间赋了值,所以显示器线程的输出结果极有可能是空:
sleep方法:
所以我们改写代码的,调用sleep方法:
package com.thread;
import java.util.Date;
public class Watch {
private static String time;
static class DisplayThread extends Thread{
@Override
public void run() {
if(time==null) {
try {
sleep(13);//当time未被赋值时,令显示器线程阻塞,知道time被赋值
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(time);
}
}
static class TimeThread extends Thread{
@Override
public void run() {
time = new Date().toString();
}
}
public static void main(String[] args) {
new TimeThread().start();
new DisplayThread().start();
}
}
运行结果:
但这里,阻塞时间因电脑而异,所以这种方法也不可取。
我们再改写代码,调用join方法。
join方法:
package com.thread;
import java.util.Date;
public class Watch {
private static String time;
static class DisplayThread extends Thread{
TimeThread timeThread;
DisplayThread(TimeThread timeThread){
this.timeThread = timeThread;
}
@Override
public void run() {
if(time==null) {
try {
timeThread.join();//阻塞显示器线程,运行时间线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(time);
}
}
static class TimeThread extends Thread{
@Override
public void run() {
time = new Date().toString();
}
}
public static void main(String[] args) {
TimeThread timeThread = new TimeThread();
timeThread.start();
new DisplayThread(timeThread).start();
}
}
到目前,join方法是最完善的方法,但本片我想通过这个例子讲一讲线程协作,即如何利用线程协作实现Watch类。
二、线程协作
synchronized关键字只是起到了多个线程“串行”执行临界区中代码的作用,但是哪个线程先执行,哪个线程后执行依无法确定,Object类中的wait()、notify()和notifyAll()三个方法解决了线程间的协作问题,通过这三个方法的“合理”使用可以确定多线程中线程的先后执行顺序:
(1)wait()方法
对象锁(Object或其它类对象)调用了wait()方法会使当前持有该对象锁的线程处于线程等待状态同时该线程释放对对象锁的控制权,直到在其他线程中该对象锁调用notify()方法或notifyAll()方法时等待此对象锁的线程才会被唤醒。
通俗来讲,在详述Java中的线程3——线程数据共享(synchronized对象锁) 我提到过,线程数据共享中,响应代码被锁住,所有线程共用一把钥匙,wait()相当于让当前线程先将钥匙交给其他线程,其他线程争夺,知道某个线程将“钥匙”“还回来”(notify()方法)。
但是,因为同类现成的多个线程执行相同的run方法,所以一旦有一类线程执行了wait()方法,只能由其它线程类来解除,如果在本类解除,如果写在wait()方法之后,则永远无法解除,若写wait()方法之前,则永远会有一个线程对象无法被本类解除,始终保持就绪状态,程序也不会停止,举例:
package com.thread;
class CounterThread extends Thread {
private Object lockObj;
public CounterThread(String threadName, Object lockObj) {
super(threadName);
this.lockObj = lockObj;
}
@Override
public void run() {
int i = 1;
while (true) {
synchronized (lockObj) {
System.out.println(getName() + ":" + i);
try {
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
lockObj.notify();
i++;
}
}
}
}
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new CounterThread("1号计数器",lockObj).start();
new CounterThread("2号计数器",lockObj).start();
}
}
运行:
我们会发现,虽然是死循环,但是程序卡顿在这,不再执行,也没有终止,那是因为19行线程阻塞后,run方法中在此以后的代码不再执行,也不存在notify()解除阻塞一说。
再看一个示例:
package com.thread;
class CounterThread extends Thread {
private Object lockObj;
public CounterThread(String threadName, Object lockObj) {
super(threadName);
this.lockObj = lockObj;
}
@Override
public void run() {
int i = 1;
while (true) {
synchronized (lockObj) {
lockObj.notify();//如果的再wait()方法之前调用,则永远只能解除上一个正在运行的线程,行程永远不会终止。
System.out.println(getName() + ":" + i);
try {
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
}
}
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new CounterThread("1号计数器",lockObj).start();
new CounterThread("2号计数器",lockObj).start();
}
}
(2)notify()方法
notify():对象锁调用notify()方法就会唤醒在此对象锁上等待的单个线程。
notify()方法只能和wait()方法配对使用。单独使用notify()方法没有意义。
(3)notifyAll()
notifyAll():对象锁调用notifyAll()方法就会唤醒在此对象锁上等待的所有线程;调用notifyAll()方法并不会立即激活某个等待线程,它只能撤销等待线程的中断状态,这样它们就能够在当前线程退出同步方法或同步代码块法后与其它线程展开竞争,以争取获得资源对象来执行。
class CounterThread extends Thread {//计数线程
private Object lockObj;
public CounterThread(String threadName, Object lockObj) {
super(threadName);
this.lockObj = lockObj;
}
@Override
public void run() {//计数线程内写一个死循环,在不被影响的情况下,该线程类创建的对象一定会执行该死循环且不会停下
int i = 1;
while (true) {
synchronized (lockObj) {
System.out.println(getName() + ":" + i);
try {
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
}
}
class NotifyAllThread extends Thread {//Notify线程
private Object lockObj;
public NotifyAllThread(Object lockObj) {
this.lockObj = lockObj;
}
@Override
public void run() {
synchronized (lockObj) {
System.out.println("notifyAll方法执行完毕");
lockObj.notifyAll();
}
}
}
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new CounterThread("1号计数器", lockObj).start();
new CounterThread("2号计数器", lockObj).start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new NotifyAllThread(lockObj).start();
}
}
(4)sleep()方法和wait()方法区别
a.sleep()方法被调用后当前线程进入阻塞状态,但是当前线程仍然持有对象锁,在当前线程sleep期间,其它线程无法执行sleep方法所在临界区中的代码:
代码示例:
package com.thread;
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new PrintThread("1号打印机", lockObj).start();
new PrintThread("2号打印机", lockObj).start();
}
}
class PrintThread extends Thread {
private Object lockObj;
public PrintThread(String threadName, Object lockObj) {
super(threadName);
this.lockObj = lockObj;
}
@Override
public void run() {
synchronized (lockObj) {// 当某台打印机执行临界区中的代码,输出线程名后由于调用了sleep方法,从而使得该打印机线程阻塞30秒,在这30秒期间因该打印机线程仍然持有对象锁,从而导致另一台打印机线程只能在30秒后才能执行临界区中的代码。
System.out.println(getName());
try {
sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
第7,8行,输出线程创建了两个对象,由于输出线程run方法中,执行完输出语句后会阻塞,而且限制性的线程对象在阻塞期间会一直占用对象锁,直到30秒后阻塞结束,下一个线程才会运行。不放在两个输出语句中加入输出时间,观察结果:
可以看到,第一次输出和第二次输出间隔了30秒。
b.对象锁调用了wait()方法会使当前持有该对象锁的线程处于线程等待状态同时该线程释放对对象锁的控制权,在当前线程处于线程等待期间,其它线程可以执行wait方法所在临界区中的代码。
再看看调用wait()方法的结果:
package com.thread;
import java.util.Date;
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new PrintThread("1号打印机", lockObj).start();
new PrintThread("2号打印机", lockObj).start();
}
}
class PrintThread extends Thread {
private Object lockObj;
public PrintThread(String threadName, Object lockObj) {
super(threadName);
this.lockObj = lockObj;
}
@Override
public void run() {
synchronized (lockObj) {
System.out.println(getName()+new Date());
try {
lockObj.wait();//第一次输出后,执行wait()方法,因为当前线程会将对象锁释放,所以下一线程直接执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
我们发现,两次输出间隔极小,几乎不到一秒,而且程序不会停止,因为我们没有调用notify()方法将阻塞状态解放。
三、总结
最后回到我们开头的问题,实现一个Watch类,如下编写代码:
package com.thread;
import java.util.Date;
public class Watch {
private static String time;
private static final Object OBJECT = new Object();
public static void main(String[] args) {
TimeThread timeThread = new TimeThread();
new DisplayThread(timeThread).start();
}
static class DisplayThread extends Thread {//显示器线程
TimeThread timeThread;
DisplayThread(TimeThread timeThread) {
this.timeThread = timeThread;
}
@Override
public void run() {
synchronized (OBJECT) {
timeThread.start();
try {
OBJECT.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(time);
}
}
static class TimeThread extends Thread {//时间线程
@Override
public void run() {
synchronized (OBJECT) {
time = new Date().toString();
OBJECT.notify();
}
}
}
}
首先在第7行和第9行定义全局变量time和全局变态常量OBJECT对象,time用来存时间,OBJECT对象用来充当对象锁。
首先,我们知道,有可能显示器线程先抢到CPU控制权导致输出结果为空,一不做二不休,我们干脆先让他先执行;
在第27行显示器线程占有对象锁后再开启时间线程,虽然开启,但时间线程在显示器执行中之前会一直阻塞;
为了让时间不为空所以这是要让时间线程执行,所以在第29行调用wait()方法使显示器线程阻塞并转交对象锁;
此时,从第43行开始,时间线程一次执行到底并解放显示器线程的阻塞状态,输出结果,一定不为空。