简介 在上篇 Java 多线程基础(一) 我们提到了一些线程的常用方法,这篇我们具体看看其中一些方法的使用以及方法的区别,让我们在工作中更好的使用。
wait 方法与 notify 方法 在 Object 类中定义了 wait 方法和 notify 方法,wait 方法的作用是让当前线程进入等待状态,将当前线程置入 预执行队列,会在 wait 方法所在代码处停止执行,直到被通知或者被中断,在调用 wait 方法之前,线程必须获取该对象的锁,因此只能在同步方法或者同步代码块中调用 wait 方法,并且该方法会释放当前线程锁持有的锁。notify 方法是唤醒在当前对象上等待的单个线程,如果有多个线程等待,那么线程调度器会挑出一个 wait 的线程,对其发出 notify ,并使它等待获取该对象的对象锁,这意味着,即使收到了通知,线程也不会立即获取到对象锁,必须等待 notify 方法的线程释放锁才可以。和 wait 方法一样,notify 方法也只能在同步方法或者同步代码块中调用。它还有个相似的方法 notifyAll,它的作用是唤醒在当前对象上等待的所有线程。
下面通过一个生产者消费者来说明 wait 方法和 notify 方法的使用:
/**
* @author mghio
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 wait() 和 notify() 方法使用示例
* @since JDK 1.8
*/
public class ThreadWaitAndNotifyDemo {
public static void main(String[] args) {
Producer producer = new Producer();
producer.start();
new Consumer("Consumer One", producer).start();
new Consumer("Consumer Two", producer).start();
new Consumer("Consumer Three", producer).start();
new Consumer("Consumer Four", producer).start();
}
static class Producer extends Thread {
List<String> messageList = new ArrayList<>(2);
@Override
public void run() {
try {
while (true) {
Thread.sleep(2000);
synchronized (messageList) {
String message = String.format("producer message [create time:%s]", LocalDateTime.now());
messageList.add(message);
System.out.println("Producer " + getName() + " producer a msg: " + message);
messageList.notify();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
String getMessage() {
synchronized (messageList) {
if (messageList.size() == 0) {
try {
messageList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return messageList.remove(0);
}
}
}
static class Consumer extends Thread {
private Producer producer;
public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}
@Override
public void run() {
while (true) {
String message = producer.getMessage();
System.out.println("Consumer " + getName() + " get a msg: " + message);
}
}
}
}
输出结果如下:
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:42.319]
Consumer Consumer One get a msg: producer message [create time:2019-12-14T22:45:42.319]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:44.324]
Consumer Consumer Two get a msg: producer message [create time:2019-12-14T22:45:44.324]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:46.325]
Consumer Consumer Three get a msg: producer message [create time:2019-12-14T22:45:46.325]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:48.328]
Consumer Consumer Four get a msg: producer message [create time:2019-12-14T22:45:48.328]
消费者线程循环调用生产者的 getMessage 方法获取消息,如果消息列表 messageList 为空,则调用消息列表的 wait 方法让线程进入等待状态,生产者每隔 2 秒生成消息并放入消息列表 messageList 中,放入成功后调用 notify 方法唤醒一个处于 wait 状态的线程去消费消息,需要注意的是,在调用 wait 和 notify 方法时必须要先获得该对象的锁,上面的示例中是在 synchronized 代码块中调用的。
sleep 方法 与 wait、notify 方法不同,sleep 方法定义在 Thread 类中,从方法名也可以知道,这个方法的作用就是让当前线程休眠,即调用该方法后当前线程会从运行状态(Running)状态进入到阻塞(休眠)状态(Blocked),同时该方法必须指定休眠的时间,当前线程的休眠时间会大于或者等于这个指定的休眠时间。当线程重新被唤醒时,线程会由阻塞状态(Blocked)变成就绪状态(Runnable),然后等待 CPU 的调度执行。sleep 方法的示例代码如下:
/**
* @author mghio
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 sleep() 方法使用示例
* @since JDK 1.8
*/
public class ThreadSleepDemo {
private static Object object = new Object();
public static void main(String[] args) {
MyThread myThreadOne = new MyThread("t1");
MyThread myThreadTwo = new MyThread("t2");
myThreadOne.start();
myThreadTwo.start();
}
static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
synchronized (object) {
try {
for (int i = 0; i < 5; i++) {
System.out.println(String.format("%s: %d", this.getName(), i));
if (i % 2 == 0) {
Thread.sleep(2000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果如下:
t1: 0
t1: 1
t1: 2
t1: 3
t1: 4
t2: 0
t2: 1
t2: 2
t2: 3
t2: 4
我们启动了两个线程 t1 和 t2,两个线程的 run 方法引用了同一个对象 object 的同步锁(synchronized (object)),虽然在第一个线程 t1 中当 i 被 2 整除时会调用 Thread.sleep(2000) 让当前线程休眠 2 s,但是此时线程 t2 也不会得到 cpu 的执行权去执行,因为 t1 线程调用 sleep 方法并没有释放object所持有的同步锁。如果我们注释掉 synchronized (object) 后再次执行该程序,线程 t1 和 t2 是可以交替执行的,注释之后的输出结果如下:
t2: 0
t1: 0
t1: 1
t2: 1
t1: 2
t2: 2
t2: 3
t1: 3
t2: 4
t1: 4
yield 方法 yield 方法定义在 Thread 类中,是线程特有的方法。此方法的主要作用是让步,它会使当前线程从运行状态(Running)变为就绪状态(Runnable),从而让其他具有同样优先级的处于就绪状态的线程获取到 CPU 执行权(PS: CPU 会从众多的处于就绪状态的线程里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到),但是,也并不能保证在当前线程调用 yield 之后,其它哪些具有相同优先级的线程就一定能获得执行权,也有可能是当前线程又进入到运行状态(Running)继续运行。yield 方法的示例代码如下:
/**
* @author maguihai
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 yield() 方法使用示例
* @since JDK 1.8
*/
public class ThreadYieldDemo {
public static void main(String[] args) {
MyThread myThreadOne = new MyThread("t1");
MyThread myThreadTwo = new MyThread("t2");
myThreadOne.start();
myThreadTwo.start();
}
static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(String.format("%s [%d] ---> %d", this.getName(), this.getPriority(), i));
if (i % 2 == 0) {
yield();
}
}
}
}
}
输出结果如下:
t1 [5] ---> 0
t2 [5] ---> 0
t1 [5] ---> 1
t1 [5] ---> 2
t1 [5] ---> 3
t1 [5] ---> 4
t1 [5] ---> 5
t1 [5] ---> 6
t1 [5] ---> 7
t1 [5] ---> 8
t1 [5] ---> 9
t2 [5] ---> 1
t2 [5] ---> 2
t2 [5] ---> 3
t2 [5] ---> 4
t2 [5] ---> 5
t2 [5] ---> 6
t2 [5] ---> 7
t2 [5] ---> 8
t2 [5] ---> 9
从以上输出结果可以看出,线程 t1 中的变量 i 在被 2 整除的时候,并没有切换到线程 t2 去执行,这也验证了我们上文说的,yield 方法虽然可以让线程由运行状态变成就绪状态,但是,它不一定会让其它线程获取 CPU 执行权从而进入到运行状态,即使这个其它线程和当前具有相同的优先级,yield 方法不会释放锁(证明方法只需将上面这个示例的 run 方法里面加上 synchronized (obj) 即可,此时 t2 线程会等到线程 t1 执行完毕后才会执行)。
join 方法 在有些场景中我们需要在子线程去执行一些耗时的任务,但是我们的主线程又必须等待子线程执行完毕之后才能结束,那么此时就可以使用 join 方法了,该方法定义在 Thread 类中,方法的作用是:让主线程等待子线程执行结束之后才能继续执行,下面我们通过一个例子来看看:
/**
* @author maguihai
* @date: 2019-12-15
* @version: 1.0
* @description: 线程 join() 方法使用示例
* @since JDK 1.8
*/
public class ThreadJoinDemo {
public static void main(String[] args) {
try {
MyThread myThread = new MyThread("t1");
myThread.start();
myThread.join();
System.out.println(String.format("%s ---> %s finish", LocalDateTime.now(), Thread.currentThread().getName()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(String.format("%s ---> %s start", LocalDateTime.now(), this.getName()));
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s ---> %s finish", LocalDateTime.now(), this.getName()));
}
}
}
输出结果如下:
2019-12-15T00:22:55.971 ---> t1 start
2019-12-15T00:22:57.984 ---> t1 finish
2019-12-15T00:22:57.985 ---> main finish
在主线程 main 中通过 new MyThread("t1") 新建线程 t1。接着,通过 t1.start() 启动线程 t1,在执行 t1.join()之后, 主线程会进入阻塞状态等待 t1 运行结束。子线程 t1 结束之后,会唤醒主线程,主线程重新获取 CPU 执行权,主线程继续往下运行。在使用了 join 方法之后主线程会等待子线程结束之后才会结束。
总结 以上是线程一些常用的方法介绍和具体使用知识总结。