Java多线程:sleep()、yield()和join()方法浅析

细心的同学可能发现在多线程环境下常见的方法中,wait、notify 和 notifyAll 这三个方法位于 Object 类中,而 sleep、yield 和 join 三个方法却位于 Thread 类中,这么布置的原因是什么呢?前面我们学习了 wait、notify 和 notifyAll 三个方法,现在我们来看后面三个 Thread 类中的方法,看看学习完这三个方法后你是否能回答之前的问题。

1、sleep

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在以前的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

 1 public class SleepTest {
 2     public synchronized void sleepMethod(){
 3         System.out.println("Sleep start-----");
 4         try {
 5             Thread.sleep(1000);
 6         } catch (InterruptedException e) {
 7             e.printStackTrace();
 8         }
 9         System.out.println("Sleep end-----");
10     }
11 
12     public synchronized void waitMethod(){
13         System.out.println("Wait start-----");
14         try {
15              wait(1000);
16         } catch (InterruptedException e) {
17              e.printStackTrace();
18         }
19         System.out.println("Wait end-----");
20     }
21 
22     public static void main(String[] args) {
23         final SleepTest test1 = new SleepTest();
24 
25         for(int i = 0;i<3;i++){
26             new Thread(new Runnable() {
27                 @Override
28                 public void run() {
29                     test1.sleepMethod();
30                 }
31             }).start();
32         }
33 
34         try {
35             Thread.sleep(4000);//暂停十秒,等上面程序执行完成
36         } catch (InterruptedException e) {
37             e.printStackTrace();
38         }
39         System.out.println("-----分割线-----");
40 
41         final SleepTest test2 = new SleepTest();
42 
43         for(int i = 0;i<3;i++){
44             new Thread(new Runnable() {
45                 @Override
46                 public void run() {
47                     test2.waitMethod();
48                 }
49             }).start();
50         }
51     }
52 }

执行结果为

Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
-----分割线-----
Wait start-----
Wait start-----
Wait start-----
Wait end-----
Wait end-----
Wait end-----

这个结果的区别很明显,通过sleep方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的。而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。

2、yield

之前的文章中我们简单的了解了一下线程优先级的概念,现在再来复习一下。

  1. 记住当线程的优先级没有指定时,所有线程都携带普通优先级(NORM_PRIORITY)。
  2. 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
  3. 记住优先级最高的线程在执行时被给予优先,但是不是绝对的(有可能有一个优先级低的线程已经等了很久,虽然有比它高的优先级但程序有可能会先执行它),并且不能保证线程在启动时就进入运行状态。
  4. 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
  5. 由调度程序决定哪一个线程被执行。
  6. setPriority()用来设定线程的优先级。
  7. 记住在线程开始方法被调用之前,线程的优先级应该被设定。
  8. 你可以使用常量,如MIN_PRIORITY(1),MAX_PRIORITY(10),NORM_PRIORITY(5)来设定优先级

了解完线程优先级的概念后,我们再来看一下 yield 方法。yield 从英文字面意思翻译为:屈服、让步,而yield方法的作用正是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。

而文档也对这个方法进行了说明,总结如下:

  • yield是一个静态的原生(native)方法
  • yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
  • yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
  • 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状

我们还是通过一个例子来演示其使用:

 1 public class YieldExample
 2 {
 3    public static void main(String[] args)
 4    {
 5       Thread producer = new Producer();
 6       Thread consumer = new Consumer();
 7   
 8       producer.setPriority(Thread.MIN_PRIORITY); //Min Priority
 9       consumer.setPriority(Thread.MAX_PRIORITY); //Max Priority
10   
11       producer.start();
12       consumer.start();
13    }
14 }
15   
16 class Producer extends Thread
17 {
18    public void run()
19    {
20       for (int i = 0; i < 5; i++)
21       {
22          System.out.println("I am Producer : Produced Item " + i);
23          Thread.yield();
24       }
25    }
26 }
27   
28 class Consumer extends Thread
29 {
30    public void run()
31    {
32       for (int i = 0; i < 5; i++)
33       {
34          System.out.println("I am Consumer : Consumed Item " + i);
35          Thread.yield();
36       }
37    }
38 }
上述程序在没有调用yield()方法情况下的输出:
I am Consumer : Consumed Item 0
 I am Consumer : Consumed Item 1
 I am Consumer : Consumed Item 2
 I am Consumer : Consumed Item 3
 I am Consumer : Consumed Item 4
 I am Producer : Produced Item 0
 I am Producer : Produced Item 1
 I am Producer : Produced Item 2
 I am Producer : Produced Item 3
 I am Producer : Produced Item 4
上述程序在调用yield()方法情况下的输出:
I am Producer : Produced Item 0
 I am Consumer : Consumed Item 0
 I am Producer : Produced Item 1
 I am Consumer : Consumed Item 1
 I am Producer : Produced Item 2
 I am Consumer : Consumed Item 2
 I am Producer : Produced Item 3
 I am Consumer : Consumed Item 3
 I am Producer : Produced Item 4
 I am Consumer : Consumed Item 4

可以看到上述例子在调用yield方法时会实现两个线程的交替执行。不过请注意:这种交替并不一定能得到保证,源码中也对这个问题进行说明:

  •   调度器可能会忽略该方法。
  •   使用的时候要仔细分析和测试,确保能达到预期的效果。
  •   很少有场景要用到该方法,主要使用的地方是调试和测试。  

3、 join

按照帮助文档中的描述,join方法把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。这么快比较抽象,我们还是通过例子来说明

比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。

 1 // 主线程
 2 public class A extends Thread {
 3     public void run() {
 4         B b = new B();
 5         b.start();
 6         b.join();
 7         ...
 8     }
 9 }
10 // 子线程
11 public class B extends Thread {
12     public void run() {
13         ...
14     }
15 }

上面的有两个类A(主线程类)和B(子线程类)。因为B是在A中创建并启动的,所以,A是主线程类,B是子线程类。
在A主线程中,通过new B()新建“子线程b”。接着通过b.start()启动“子线程b”,并且调用b.join()。在调用b.join()之后,A主线程会一直等待,直到“子线程b”运行完毕;在“子线程b”运行完毕之后,A主线程才能接着运行。 这也就是我们所说的“join()的作用。

join 一共有三个重载版本的方法:

 

我们还可以从源码中加深对 join 的印象

 1 public final void join() throws InterruptedException {
 2     join(0);
 3 }
 4 
 5 public final synchronized void join(long millis)
 6 throws InterruptedException {
 7     long base = System.currentTimeMillis();
 8     long now = 0;
 9 
10     if (millis < 0) {
11         throw new IllegalArgumentException("timeout value is negative");
12     }
13 
14     if (millis == 0) {
15         while (isAlive()) {
16             wait(0);
17         }
18     } else {
19         while (isAlive()) {
20             long delay = millis - now;
21             if (delay <= 0) {
22                 break;
23             }
24             wait(delay);
25             now = System.currentTimeMillis() - base;
26         }
27     }
28 }

很显然,当调用 join 的线程会一直判断自己的状态,如果还没执行完则让主线程等待(因为我们是在主线程中调用子线程的 join 方法,而 wait 方法表示让当前线程等待,当前在主线程中所以是主线程等待)。

好了,介绍完 join 的一些概念,我们通过例子再来看看 join 的应用场景

 1 public class JoinTest implements Runnable{  
 2       
 3     public static int a = 0;  
 4   
 5     public void run() {  
 6         for (int k = 0; k < 5; k++) {  
 7             a = a + 1;  
 8         }  
 9     }  
10   
11     public static void main(String[] args) throws Exception {  
12         Runnable r = new JoinTest();  
13         Thread t = new Thread(r);  
14         t.start();        
15         System.out.println(a);  
16     }         
17 } 

这种情况下请问程序的输出结果是5吗?

答案是:有可能。其实你很难遇到输出5的时候,通常情况下都不是5。当然这也和机器有严重的关系。为什么呢?我的理解是当主线程 main方法执行System.out.println(a);这条语句时,线程还没有真正开始运行,或许正在为它分配资源准备运行。因为为线程分配资源需要时间,而main方法执行完t.start()方法后继续往下执行System.out.println(a);,这个时候得到的结果是a还没有被改变的值0。

怎样才能让输出结果为5?其实很简单,join() 方法提供了这种功能。join() 方法,它能够使调用该方法的线程在此之前执行完毕。

我们只需要在14、15行代码之间调用 join 方法

14 t.start();
15 t.join();        
16 System.out.println(a);

这个时候打印结果将一直是5。

再来看一个针对带参 join 的例子

 1 public class JoinTest { 
 2 
 3     public static void main(String[] args) throws Exception {  
 4            Thread t = new Thread() { 
 5             @Override
 6             public void run() {
 7                 try {
 8                     System.out.println("Begin sleep");
 9                     Thread.sleep(1000);
10                     System.out.println("End sleep");
11                 } catch(InterruptedException e) {
12                     e.printStackTrace();
13                 }
14             }
15         };
16         t.start();
17         try {
18             t.join(1500); //主线程等待1500ms
19             System.out.println("Join finish");
20         } catch(InterruptedException e) {
21             e.printStackTrace();
22         }
23     }         
24 } 

打印结果:

Begin sleep
End sleep
joinFinish

如果将第九行子线程睡眠时间改为2000ms,结果则变为:

Begin sleep
joinFinish
End sleep

也就是说,主线程只等待 1500ms,不管你子线程是否执行结束。

 1 class CustomThread1 extends Thread {
 2     public CustomThread1() {
 3         super("CustomThread1");
 4     }  
 5     public void run() {    
 6         String threadName = Thread.currentThread().getName();    
 7         System.out.println(threadName + " start.");    
 8         try {    
 9             for (int i = 0; i < 5; i++) {    
10                 System.out.println(threadName + " loop at " + i);    
11                 Thread.sleep(1000);    
12             }    
13             System.out.println(threadName + " end.");    
14         } catch (Exception e) {    
15             System.out.println("Exception from " + threadName + ".run");    
16         }    
17     }    
18 }    
19   
20 class CustomThread extends Thread {    
21     CustomThread1 t1;    
22     public CustomThread(CustomThread1 t1) {            
23         super("CustomThread");
24         this.t1 = t1;    
25     }    
26     public void run() {    
27         String threadName = Thread.currentThread().getName();    
28         System.out.println(threadName + " start.");    
29         try {    
30             t1.join();    
31             System.out.println(threadName + " end.");    
32         } catch (Exception e) {    
33             System.out.println("Exception from " + threadName + ".run");    
34         }    
35     }    
36 }    
37   
38 public class JoinTestDemo {
39   
40     public static void main(String[] args) {    
41         String threadName = Thread.currentThread().getName();    
42         System.out.println(threadName + " start.");    
43         CustomThread1 t1 = new CustomThread1();    
44         CustomThread t = new CustomThread(t1);    
45         try {    
46             t1.start();    
47             Thread.sleep(2000);    
48             t.start();    
49             t.join(); //在第二次测试中,将此处注释掉
50         } catch (Exception e) {    
51             System.out.println("Exception from main");    
52         }    
53         System.out.println(threadName + " end!");    
54     }    
55 }   
main start.         //main方法所在的线程起动,但没有马上结束,因为调用t.join();,所以要等到t结束了,此线程才能向下执行。
CustomThread1 start.
CustomThread1 loop at 0
CustomThread1 loop at 1
CustomThread start.    //线程CustomThread起动,但没有马上结束,因为调用t1.join();,所以要等到t1结束了,此线程才能向下执行。
CustomThread1 loop at 2
CustomThread1 loop at 3
CustomThread1 loop at 4
CustomThread1 end.
CustomThread end.     // 线程CustomThread在t1.join();阻塞处起动,向下继续执行的结果
main end!         //线程CustomThread结束,此线程在t.join();阻塞处起动,向下继续执行的结果

结果说明:

main 线程先启动打印 main start,接着 main 线程要睡两秒,所以 t1 线程获得 cpu 资源,执行睡 1 秒,打印 1 句话,所以打印两句话。接着 main 线程结束休眠,t1 进入休眠,main 线程继续执行,启动 t 线程,但 t 线程不一定会立刻执行,因为它还没得到 cpu 资源,但 main 线程继续执行下去的时候遇到了 t.join(),这时 main 线程停住,等待 t 线程执行完毕。t 线程执行,打印 CustomThread start,接着遇到 t1.join(),于是转而等待 t1 执行完,所以 t1 线程重新得到 cpu 资源将循环执行完,打印3句话。t1 执行完后,t 线程也跟着结束,main 线程随着 t 线程的结束也跟着结束。

如果我们将49行的 t.join() 注释掉后,结果如下:

main start.
CustomThread1 start.
CustomThread1 loop at 0
CustomThread1 loop at 1
main end!   // Thread.sleep(2000);结束,虽然在线程CustomThread执行了t1.join();,但这并不会影响到其他线程(这里main方法所在的线程)。
CustomThread start.
CustomThread1 loop at 2
CustomThread1 loop at 3
CustomThread1 loop at 4
CustomThread1 end.
CustomThread end.

main 线程调用t.join时,必须能够拿到线程t对象的锁,如果拿不到它是无法wait的 (参考源码),刚开的例子t.join(1000)不是说明了main线程等待1 秒,如果在它等待之前,其他线程获取了t对象的锁,它等待时间可不就是1毫秒了 。

 1 public class JoinTestDemo {  
 2     public static void main(String[] args) {  
 3         Thread t = new Thread() {
 4             public void run() {  
 5                 try {  
 6                     System.out.println("Begin sleep");  
 7                     Thread.sleep(2000);  
 8                     System.out.println("End sleep");  
 9                 } catch (InterruptedException e) {  
10                     e.printStackTrace();  
11                 }  
12             }  
13         };  
14         new ThreadTest(t).start();  
15         t.start();  
16         try {  
17             t.join();  
18             System.out.println("joinFinish");  
19         } catch (InterruptedException e) {  
20             e.printStackTrace();           
21         }  
22     }  
23 }
24 class ThreadTest extends Thread {   
25     Thread thread;   
26     public ThreadTest(Thread thread) {  
27         this.thread = thread;  
28     }  
29 
30     @Override  
31     public void run() {  
32         synchronized (thread) {  
33             System.out.println("getObjectLock");  
34             try {  
35                 Thread.sleep(9000);  
36             } catch (InterruptedException ex) {  
37                 ex.printStackTrace();  
38             }  
39             System.out.println("ReleaseObjectLock");  
40         }  
41     }  
42 }  

打印结果

getObjectLock
Begin sleep
End sleep
ReleaseObjectLock
joinFinish

在main方法中 通过new  ThreadTest(t).start()实例化 ThreadTest 线程对象, 它通过 synchronized  (thread) ,获取线程对象t的锁,并Sleep(9000)后释放,这就意味着,即使main方法t.join(1000)等待一秒钟,它必须等待ThreadTest 线程释放t锁后才能进入wait方法中,它实际等待时间是9000+1000ms。

但是注意,这个结果不是绝对的,执行9000ms还是9000+1000ms,取决于join执行到还是ThreadTest的run先执行到,如果join先执行到,为9000ms,如果ThreadTest先执行到则是9000+1000ms。

现在我们再来回答文章开头所提的问题:

一个很重要的原因是,Java提供的锁是对象级别的而不是线程级别的,每个对象都是一把锁。对于那些想使用同步但又不想成为线程类的类来讲,从Object类继承这些方法显然比自己手动继承Thread类好得多,毕竟Object类是每一个类的父类,况且一般不建议随便继承,应该慎用。而 sleep、yield 和 join 方法明确是提供给线程类使用的,是专门针对线程操作的方法,所以当然要放在 Thread 类中。

参考资料:

http://uule.iteye.com/blog/1101994

http://www.cnblogs.com/skywang12345/p/java_threads_category.html

原文地址:https://www.cnblogs.com/2015110615L/p/6742375.html