沉淀再出发:再谈java的多线程机制

Posted 精心出精品

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了沉淀再出发:再谈java的多线程机制相关的知识,希望对你有一定的参考价值。

沉淀再出发:再谈java的多线程机制

一、前言

    自从我们学习了操作系统之后,对于其中的线程和进程就有了非常深刻的理解,但是,我们可能在C,C++语言之中尝试过这些机制,并且做过相应的实验,但是对于java的多线程机制以及其中延伸出来的很多概念和相应的实现方式一直都是模棱两可的,虽然后来在面试的时候可能恶补了一些这方面的知识,但是也只是当时记住了,或者了解了一些,等到以后就会变得越来越淡忘了,比如线程的实现方式有两三种,线程池的概念,线程的基本生命周期等等,以及关于线程之间的多并发引起的资源的抢占和竞争,锁的出现,同步和异步,阻塞等等,这些概念再往下面延伸就到了jvm这种虚拟机的内存管理层面上了,由此又出现了jvm的生存周期,内存组成,函数调用,堆和栈,缓存,volatile共享变量等等机制,至此我们才能很好的理解多线程和并发。

二、java的多线程初探

 2.1、进程和线程的生命周期

   让我们看看网上对多线程生命周期的描述:

    Java线程具有五中基本状态:

1  新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2  就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,
随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
5 1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态; 6 2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态; 7 3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 8 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    这种解释其实和我们在操作系统中学习的是一致的,只不过内部的实现方式有所不同而已,同样的如果实在Linux之中,进程和线程的生命周期有略微有所不同,但是究其根源来说都是这几种步骤,只不过在某种过程之下可能有所细分而已。

    再比如说其他资料上对java的多线程生命周期的划分,我们也可以看到就是把其中的阻塞状态分离出来而已:

    明白了这一点,对于我们继续细分其中的状态背后的意义至关重要。

2.2、多线程状态的实现

2.2.1、start()

    新启一个线程执行其run()方法,一个线程只能start一次。主要是通过调用native start0()来实现。

 1 public synchronized void start() {
 2      //判断是否首次启动
 3         if (threadStatus != 0)
 4             throw new IllegalThreadStateException();
 5 
 6         group.add(this);
 7 
 8         boolean started = false;
 9         try {
10        //启动线程
11             start0();
12             started = true;
13         } finally {
14             try {
15                 if (!started) {
16                     group.threadStartFailed(this);
17                 }
18             } catch (Throwable ignore) {
19                 /* do nothing. If start0 threw a Throwable then
20                   it will be passed up the call stack */
21             }
22         }
23     }
24     private native void start0();

2.2.2、run()

    run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当该线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,如果继承Thread类则必须重写run方法,在run方法中定义具体要执行的任务。start()的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

1 public void run() {
2     if (target != null) {
3         target.run();
4     }
5 }

    target是一个Runnable对象。run()就是直接调用Thread线程的Runnable成员的run()方法,并不会新建一个线程。

2.2.3、sleep()

  sleep方法有两个重载版本:

1  sleep(long millis)     //参数为毫秒
2  sleep(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

    sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。sleep() 定义在Thread.java中。sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。
    我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。

 1 package com.thread.test;
 2 
 3 public class SleepLockTest{ 
 4 
 5  private static Object obj = new Object();
 6 
 7  public static void main(String[] args){ 
 8      ThreadA t1 = new ThreadA("t1"); 
 9      ThreadA t2 = new ThreadA("t2"); 
10      t1.start(); 
11      t2.start();
12  } 
13 
14  static class ThreadA extends Thread{
15      public ThreadA(String name){ 
16          super(name); 
17      } 
18      public void run(){ 
19          // 获取obj对象的同步锁
20          synchronized (obj) {
21              try {
22                  for(int i=0; i <10; i++){ 
23                      System.out.printf("%s: %d\\n", this.getName(), i); 
24                      // i能被4整除时,休眠100毫秒
25                      if (i%4 == 0)
26                          Thread.sleep(100);
27                  }
28              } catch (InterruptedException e) {
29                  e.printStackTrace();
30              }
31          }
32      } 
33  } 
34 }
sleep不会释放同步锁

2.2.4 yield()

    调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

 1 package com.thread.test;
 2 
 3 class ThreadB extends Thread {
 4     public ThreadB(String name) {
 5         super(name);
 6     }
 7 
 8     public synchronized void run() {
 9         for (int i = 0; i < 10; i++) {
10             System.out.printf("%s [%d]:%d\\n", this.getName(), this.getPriority(), i);
11             // i整除4时,调用yield
12             if (i % 4 == 0)
13                 Thread.yield();
14         }
15     }
16 }
17 
18 public class YieldTest {
19     public static void main(String[] args) {
20         ThreadB t1 = new ThreadB("t1");
21         ThreadB t2 = new ThreadB("t2");
22         t1.start();
23         t2.start();
24     }
25 }
yield让步,变为就绪态,可能切换线程

   可以看到这两次的让步效果是不错的。

   wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.yield();但是,t2是不会获取cpu执行权的。因为t1并没有释放“obj所持有的同步锁”

 1 package com.thread.test;
 2 
 3 public class YieldLockTest{ 
 4 
 5  private static Object obj = new Object();
 6 
 7  public static void main(String[] args){ 
 8      ThreadA t1 = new ThreadA("t1"); 
 9      ThreadA t2 = new ThreadA("t2"); 
10      t1.start(); 
11      t2.start();
12  } 
13 
14  static class ThreadA extends Thread{
15      public ThreadA(String name){ 
16          super(name); 
17      } 
18      public void run(){ 
19          // 获取obj对象的同步锁
20          synchronized (obj) {
21              for(int i=0; i <10; i++){ 
22                  System.out.printf("%s [%d]:%d\\n", this.getName(), this.getPriority(), i); 
23                  // i整除4时,调用yield
24                  if (i%4 == 0)
25                      Thread.yield();
26              }
27          }
28      } 
29  } 
30 }
yield不释放同步锁

2.2.5 join()

   thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

   join方法有三个重载版本:

1  join()
2  join(long millis)     //参数为毫秒
3  join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

   join()实际是利用了wait(),只不过它不用等待notify()/notifyAll(),且不受其影响。它结束的条件是:1)等待时间到;2)目标线程已经run完(通过isAlive()来判断)。

 1 public final synchronized void join(long millis) throws InterruptedException {
 2     long base = System.currentTimeMillis();
 3     long now = 0;
 4 
 5     if (millis < 0) {
 6         throw new IllegalArgumentException("timeout value is negative");
 7     }
 8     
 9     //0则需要一直等到目标线程run完
10     if (millis == 0) {
11         while (isAlive()) {
12             wait(0);
13         }
14     } else {
15         //如果目标线程未run完且阻塞时间未到,那么调用线程会一直等待。
16         while (isAlive()) {
17             long delay = millis - now;
18             if (delay <= 0) {
19                 break;
20             }
21             wait(delay);
22             now = System.currentTimeMillis() - base;
23         }
24     }
25 }

    Join方法是通过wait实现的,当main线程调用t.join时候,main线程会获得线程对象t的锁,调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出或者时间到。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。

 1 package com.thread.test;
 2 
 3 import static java.lang.Thread.currentThread;
 4 import static java.lang.Thread.sleep;
 5 
 6 /**
 7  * Created with IntelliJ IDEA.
 8  * User: Blank
 9  * Date: 14-3-28
10  * Time: 下午7:49
11  */
12 public class JoinTest implements Runnable {
13 
14 
15     public static void main(String[] sure) throws InterruptedException {
16         Thread t = new Thread(new JoinTest());
17         long start = System.currentTimeMillis();
18         t.start();
19         t.join(1000);//等待线程t 1000毫秒
20         System.out.println(System.currentTimeMillis()-start);//打印出时间间隔
21         System.out.println("Main finished");//打印主线程结束
22     }
23 
24     @Override
25     public void run() {
26        // synchronized (currentThread()) {
27             for (int i = 1; i <= 5; i++) {
28                 try {
29                     sleep(1000);//睡眠5秒,循环是为了方便输出信息
30                 } catch (InterruptedException e) {
31                     e.printStackTrace();
32                 }
33                 System.out.println("睡眠" + i);
34             }
35             System.out.println("TestJoin finished");//t线程结束
36         }
37     //}
38 }
主线程得到锁之后先执行完

 1 package com.thread.test;
 2 
 3 import static java.lang.Thread.currentThread;
 4 import static java.lang.Thread.sleep;
 5 
 6 /**
 7  * Created with IntelliJ IDEA.
 8  * User: Blank
 9  * Date: 14-3-28
10  * Time: 下午7:49
11  */
12 public class JoinTest implements Runnable {
13 
14 
15     public static void main(String[] sure) throws InterruptedException {
16         Thread t = new Thread(new JoinTest());
17         long start = System.currentTimeMillis();
18         t.start();
19         t.join(1000);//等待线程t 1000毫秒
20         System.out.println(System.currentTimeMillis()-start);//打印出时间间隔
21         System.out.println("Main finished");//打印主线程结束
22     }
23 
24     @Override
25     public void run() {
26         synchronized (currentThread()) {
27             for (int i = 1; i <= 5; i++) {
28                 try {
29                     sleep(1000);//睡眠5秒,循环是为了方便输出信息
30                 } catch (InterruptedException e) {
31                     e.printStackTrace();
32                 }
33                 System.out.println("睡眠" + i);
34             }
35             System.out.println("TestJoin finished");//t线程结束
36         }
37     }
38 }
main得不到锁,最后结束

2.2.6、interrupt()

   此操作会中断等待中的线程,并将线程的中断标志位置位。如果线程在运行态则不会受此影响
   可以通过以下三种方式来判断中断:

1)isInterrupted()
    此方法只会读取线程的中断标志位,并不会重置。
2)interrupted()
   此方法读取线程的中断标志位,并会重置。
3)throw InterruptException
   抛出该异常的同时,会重置中断标志位。

2.2.6.1、终止处于“阻塞状态”的线程

    通常,我们通过“中断”方式终止处于“阻塞状态”的线程。当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:

 1 @Override
 2 public void run() {
 3     try {
 4         while (true) {
 5             // 执行任务...
 6         }
 7     } catch (InterruptedException ie) {  
 8         // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
 9     }
10 }

    在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理。

 1 @Override
 2 public void run() {
 3     while (true) {
 4         try {
 5             // 执行任务...
 6         } catch (InterruptedException ie) {  
 7             // InterruptedException在while(true)循环体内。
 8             // 当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出
 9             break;
10         }
11     }
12 }

    上面的InterruptedException异常的捕获在whle(true)之内。当产生InterruptedException异常时,被catch处理之外,仍然在while(true)循环体内;要退出while(true)循环体,需要额外的执行退出while(true)的操作。

2.2.6.2、 终止处于“运行状态”的线程

    通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。

    通过“中断标记”终止线程:

1 @Override
2 public void run() {
3     while (!isInterrupted()) {
4         // 执行任务...
5     }
6 }

    isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。注意interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。
    通过“额外添加标记”终止处于“运行状态”的线程,线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。注意将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。

 1 private volatile boolean flag= true;
 2 protected void stopTask() {
 3     flag = false;
 4 }
 5 @Override
 6 public void run() {
 7     while (flag) {
 8         // 执行任务...
 9     }
10 }

     综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:

 1 @Override
 2 public void run() {
 3     try {
 4         // 1. isInterrupted()保证,只要中断标记为true就终止线程。
 5         while (!isInterrupted()) {
 6             // 执行任务...
 7         }
 8     } catch (InterruptedException ie) {  
 9         // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
10     }
11 }

正常中断并退出的案例:

 1 package com.thread.test;
 2 
 3 class MyThread extends Thread {
 4     
 5     public MyThread(String name) {
 6         super(name);
 7     }
 8 
 9     @Override
10     public void run() {
11         try {  
12             int i=0;
13             while (!isInterrupted()) {
14                 Thread.sleep(100); // 休眠100ms
15                 i++;
16                 System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  

以上是关于沉淀再出发:再谈java的多线程机制的主要内容,如果未能解决你的问题,请参考以下文章

沉淀再出发:如何在eclipse中查看java的核心代码

沉淀再出发:关于java中的AQS理解

沉淀再出发:java中注解的本质和使用

沉淀再出发:java中的CAS和ABA问题整理

沉淀再出发:jvm的本质

沉淀再出发:spring的架构理解