🍤 并发操作合集系列 目录
🍕 并发操作合集系列 源代码
这篇文章中会详细介绍那些切换线程状态的常用方法(wait,notify,sleep,join,yield)。
线程的状态和常用方法 还是这张图。本章中介绍的方法与线程状态的关系如下图所示
wait() 和 notify() 方法 首先,这两个方法是做什么的:
Object.wait() – 挂起一个线程
Object.notify() – 唤醒一个线程
wait()使当前线程阻塞,前提是 必须先获得锁,所以通常在synchronized 同步代码块里使用 wait(),notify/notifyAll() 方法。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。此时的等待状态和sleep造成的等待状态稍有不同,下一章会对此进行解释。
当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码。如果中途遇到wait() ,将再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,此时被唤醒的等待线状态的线程将会变为阻塞状态(BLOCKED)位于对象的等待锁定池,直到notify释放锁并获取该锁 。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程 。
另外wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。
notify 和 notifyAll的区别 : notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。
示例:发送者 - 接收者同步问题 假如有这样的需求:
发送者会向接收者发送数据包
在发送者完成发送之前,接收者无法处理数据包
同样,发送者不得尝试发送另一个数据包,直到接收者已处理完上一个数据包
创建一个Data类来实现这些数据包的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Data { private String packet; private boolean transfer = true ; public synchronized void send (String packet) { while (!transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted" , e); } } transfer = false ; this .packet = packet; notifyAll(); } public synchronized String receive () { while (transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted" , e); } } transfer = true ; notifyAll(); return packet; } }
那么为什么要用while而不是if包住wait()?
因为这里的notifyAll()唤醒的是所有处于等待状态的线程,也就是说被唤醒的线程可能并不符合条件判断,所以应该用while循环再次进行判断,如果不满足将继续调用wait。
下面是Sender类和Receiver类,它们将对Data类进行同步操作:
Sender:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Sender implements Runnable { private Data data; public void run () { String packets[] = { "First packet" , "Second packet" , "Third packet" , "Fourth packet" , "End" }; for (String packet : packets) { data.send(packet); try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000 , 5000 )); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted" , e); } } } }
Receiver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Receiver implements Runnable { private Data load; public void run () { for (String receivedMessage = load.receive(); !"End" .equals(receivedMessage); receivedMessage = load.receive()) { System.out.println(receivedMessage); try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000 , 5000 )); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted" , e); } } } }
写一个客户端程序来启动Sender和Receiver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { Data data = new Data(); Thread sender = new Thread(new Sender(data)); Thread receiver = new Thread(new Receiver(data)); sender.start(); receiver.start(); } ------------------------------------------------- 输出: First packet Second packet Third packet Fourth packet
wait()和sleep()方法 我们首先要知道sleep()方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。也就是说,在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class Test { public static void main (String[] args) throws InterruptedException { new Thread(new WaitTread()).start(); Thread.sleep(1000 ); new Thread(new SleepThread()).start(); } } class WaitTread implements Runnable { @Override public void run () { synchronized (Test.class) { System.out.println("进入WaitTread 当前时间 : " + LocalTime.now()); try { Test.class.wait(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("WaitTread结束了 当前时间 : " + LocalTime.now()); } } } class SleepThread implements Runnable { @Override public void run () { synchronized (Test.class) { System.out.println("进入SleepThread 当前时间 : " + LocalTime.now()); try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("SleepThread结束了 当前时间 : " + LocalTime.now()); } } }
上面的例子中,sleep和wait会分别暂停5秒,线程启动之间间隔1秒。
修改WaitTread和SleepThread的启动顺序,输出结果会截然不同:
1 2 3 4 5 6 7 8 9 10 new Thread(new SleepThread()).start();Thread.sleep(1000 ); new Thread(new WaitTread()).start();------------------------------------------------- 输出 进入SleepThread 当前时间 : 21 :59 :49.747 SleepThread结束了 当前时间 : 21 :59 :54.747 进入WaitTread 当前时间 : 21 :59 :54.748 WaitTread结束了 当前时间 : 21 :59 :59.748
很显然,由于sleep不释放锁,所以直到sleep线程执行结束后wait线程才获取到锁。wait线程在被启动的4秒后才开始执行。
如果把调用顺序反过来会如何:
1 2 3 4 5 6 7 8 9 10 new Thread(new WaitTread()).start();Thread.sleep(1000 ); new Thread(new SleepThread()).start();------------------------------------------------- 输出 进入WaitTread 当前时间 : 21 :53 :56.563 进入SleepThread 当前时间 : 21 :53 :57.492 SleepThread结束了 当前时间 : 21 :54 :02.492 WaitTread结束了 当前时间 : 21 :54 :02.493
在WaitTread调用wait()之后SleepThread获取到了锁,说明wait过程中会释放锁。值得注意的是,WaitTread从开始到结束花了整整6秒时间,而我们设置的wait的时限是5秒,是和SleepThread几乎同时结束的。原因也很简单,wait经过5秒后唤醒,此时该线程处于等待锁定池,而此时的锁还在sleep线程手中,因此直到sleep线程执行完毕释放锁之后,wait线程才能继续执行。
join()方法 join()方法的作用 thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
它的重载:
t.join() : 调用join方法,等待线程t执行完毕
t.join(1000) : 等待 t 线程,等待时间是1000毫秒。
使用join时需要注意的几点 调用join时需要注意以下几点:
只能用于已启动的线程 如果线程被生成了,但还未被起动,调用它的 join() 方法是没有作用的,将直接继续向下执行。在源码中用isAlive()来进行判断。
join是基于wait实现的 通过wait(timeout)来让当前线程变为等待状态,这里的timeout是目标线程的执行时间。从而实现让当前线程成为等待状态,直到目标线程执行完毕。
调用join时,必须先获取目标线程的锁 既然基于wait,那么join方法肯定是在synchronized中,这是因为wait必须是在有锁的地方才能使用。从源码上来看也确实如此。也就是说在调用join时我们必须获得目标线程t的锁,否则会一直被阻塞。
详细可以参考JDK源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public final synchronized void join (long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0 ; if (millis < 0 ) { throw new IllegalArgumentException("timeout value is negative" ); } if (millis == 0 ) { while (isAlive()) { wait(0 ); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0 ) { break ; } wait(delay); now = System.currentTimeMillis() - base; } } }
示例代码 为了理解join,我们来写一个示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class TestJoin implements Runnable { public static void main (String[] args) throws InterruptedException { Thread t = new Thread(new TestJoin()); long start = System.currentTimeMillis(); t.start(); Thread.sleep(1000 ); t.join(2000 ); System.out.println("t.join()调用结束,耗时(ms):" + (System.currentTimeMillis() - start)); System.out.println(" Main 结束了" ); } @Override public void run () { for (int i = 0 ; i < 5 ; i++) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sleeping" + i); } System.out.println("TestJoin 结束了" ); } } ----------------------------------------------------- 输出 sleeping0 sleeping1 t.join()调用结束,耗时(ms):3000 Main 结束了 sleeping2 sleeping3 sleeping4 TestJoin 结束了
可以看出mian线程在调用join()后变为等待状态,让给线程两秒后继续执行main线程。
如果不启动线程呢?把start()注释掉
1 2 3 4 5 6 7 Thread.sleep(1000 ); t.join(2000 ); ------------------------------------------------------- 输出 t.join()调用结束,耗时(ms):1000 Main 结束了
果然join是不起作用的。
我们再通过synchronized块让Main线程获取不到锁,从而测试”调用join时,必须先获取目标线程的锁”。
把run()改成以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void run () { synchronized (Thread.currentThread()) { for (int i = 0 ; i < 5 ; i++) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("sleeping" + i); } System.out.println("TestJoin 结束了" ); } } ------------------------------------------------------- 输出 sleeping0 sleeping1 sleeping2 sleeping3 sleeping4 TestJoin 结束了 t.join()调用结束,耗时(ms):5002 Main 结束了
main线程因为获取不到锁,所以一直处于阻塞状态,直到线程t执行完毕。
yield()方法 yield()的作用 yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。所以,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()的使用场景很少,如果不是有特别的需求,尽量避免使用。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ThreadYield { public static void main (String[] args) { Runnable r = () -> { int counter = 0 ; while (counter < 2 ) { System.out.println(Thread.currentThread().getName()); counter++; Thread.yield(); } }; new Thread(r).start(); new Thread(r).start(); } }
运行结果可能是
1 2 3 4 Thread-0 Thread-1 Thread-1 Thread-0
也可能是
1 2 3 4 Thread-0 Thread-0 Thread-1 Thread-1
Reference Java多线程学习之wait、notify/notifyAll 详解
wait and notify() Methods in Java
java中的sleep()和wait()的区别
Difference Between Wait and Sleep in Java
Java中join()方法的理解
java中yield()方法如何使用
Brief Introduction to Java Thread.yield()