Java高併發程序設計(二)-多線程基礎

一,什麼是線程

要解釋線程,就必須明白什麼是進程。

    進程是指運行中的應用程序,每個進程都有自己獨立的地址空間(內存空間),比如用戶點擊桌面的IE瀏覽器,就啓動了一個進程,操作系統就會爲該進程分配獨立的地址空間。當用戶再次點擊左面的IE瀏覽器,又啓動了一個進程,操作系統將爲新的進程分配新的獨立的地址空間。目前操作系統都支持多進程。

要點:用戶每啓動一個進程,操作系統就會爲該進程分配一個獨立的內存空間。

 

線程(線程是進程內的執行單元):

    是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以併發執行。線程有就緒、阻塞和運行三種基本狀態。

命令:

列出進程:jps

列出線程:jstack pid
二,線程的生命週期
Java中的線程的生命週期大體可分爲5種狀態。
1. 新建(NEW):新創建了一個線程對象。
2. 可運行(RUNNABLE):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
3. 運行(RUNNING):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。
4. 阻塞(BLOCKED):阻塞狀態是指線程因爲某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,纔有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:
(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。
(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。
(三). 其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。
5. 死亡(DEAD):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生

線程生命週期狀態圖

初始狀態:
  1. 實現Runnable接口和繼承Thread可以得到一個線程類,new一個實例出來,線程就進入了初始狀態
可運行狀態:
  1. 可運行狀態只是說你資格運行,調度程序沒有挑選到你,你就永遠是可運行狀態。
  2. 調用線程的start()方法,此線程進入可運行狀態。
  3. 當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖,這些線程也將進入可運行狀態。
  4. 當前線程時間片用完了,調用當前線程的yield()方法,當前線程進入可運行狀態。
  5. 鎖池裏的線程拿到對象鎖後,進入可運行狀態。
運行狀態:
  1. 線程調度程序從可運行池中選擇一個線程作爲當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。
死亡狀態:
  1. 當線程的run()方法完成時,或者主線程的main()方法完成時,我們就認爲它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。
  2. 在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
阻塞狀態:
  1. 當前線程T調用Thread.sleep()方法,當前線程進入阻塞狀態。
  2. 運行在當前線程裏的其它線程t2調用join()方法,當前線程進入阻塞狀態。
  3. 等待用戶輸入的時候,當前線程進入阻塞狀態。
等待隊列(本是Object裏的方法,但影響了線程)):
  1. 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) 代碼段內。
  2. 與等待隊列相關的步驟和圖
  • 線程1獲取對象A的鎖,正在使用對象A。
  • 線程1調用對象A的wait()方法。
  • 線程1釋放對象A的鎖,並馬上進入等待隊列。
  • 鎖池裏面的對象爭搶對象A的鎖。
  • 線程5獲得對象A的鎖,進入synchronized塊,使用對象A。
  • 線程5調用對象A的notifyAll()方法,喚醒所有線程,所有線程進入鎖池。||||| 線程5調用對象A的notify()方法,喚醒一個線程,不知道會喚醒誰,被喚醒的那個線程進入鎖池。
  • notifyAll()方法所在synchronized結束,線程5釋放對象A的鎖。
  • 鎖池裏面的線程爭搶對象鎖,但線程1什麼時候能搶到就不知道了。||||| 原本鎖池+第6步被喚醒的線程一起爭搶對象鎖。
鎖池狀態:
  1. 當前線程想調用對象A的同步方法時,發現對象A的鎖被別的線程佔有,此時當前線程進入鎖池狀態。簡言之,鎖池裏面放的都是想爭奪對象鎖的線程。
  2. 當一個線程1被另外一個線程2喚醒時,1線程進入鎖池狀態,去爭奪對象鎖。
  3. 鎖池是在同步的環境下才有的概念,一個對象對應一個鎖池。
三,線程的基本操作
(1)創建線程
主要有兩種方式:

1)繼承Thread類創建線程

2)實現Runnable接口創建線程

通過繼承Thread類來創建並啓動多線程的一般步驟如下

1】d定義Thread類的子類,並重寫該類的run()方法,該方法的方法體就是線程需要完成的任務,run()方法也稱爲線程執行體。

2】創建Thread子類的實例,也就是創建了線程對象

3】啓動線程,即調用線程的start()方法

代碼實例

public class MyThread extends Thread{//繼承Thread類

  public void run(){

  //重寫run方法

  }

}

public class Main {

  public static void main(String[] args){

    new MyThread().start();//創建並啓動線程

  }

}

通過實現Runnable接口創建並啓動線程一般步驟如下:

1】定義Runnable接口的實現類,一樣要重寫run()方法,這個run()方法和Thread中的run()方法一樣是線程的執行體

2】創建Runnable實現類的實例,並用這個實例作爲Thread的target來創建Thread對象,這個Thread對象纔是真正的線程對象

3】第三部依然是通過調用線程對象的start()方法來啓動線程

代碼實例:

public class MyThread2 implements Runnable {//實現Runnable接口

  public void run(){

  //重寫run方法

  }

}

public class Main {

  public static void main(String[] args){

    //創建並啓動線程

    MyThread2 myThread=new MyThread2();

    Thread thread=new Thread(myThread);

    thread().start();

    //或者    new Thread(new MyThread2()).start();

  }

}


(2)終止線程

Thread.stop()  不推薦使用,它會立即釋放所有monitor(對象或鎖),會導致多線程的數據的不一致性,例如寫進程突然終止,讀進程可能會讀到剛纔還沒有寫完的“殘破”的數據。

(3)中斷線程

public void Thread.interrupt() // 中斷線程

public boolean Thread.isInterrupted() // 判斷是否被中斷

public static boolean Thread.interrupted() // 判斷是否被中斷,並清除當前中斷狀態

直接打斷,並不能夠把該線程中斷,因爲一直在循環中:

public void run(){

    while(true){

         Thread.yield();    

    }

}

t1.interrupt();


優雅的方法,如果發現有人“打招呼”,就會跳出循環:

public void run(){

    while(true){

        if(Thread.currentThread().isInterrputed()){

             System.out.println("interrupted");

             break;

        }

       Thread.yield();

 }}

設置睡眠,如果拋出異常後會清除中斷標記位 ,需要設置中斷狀態

public static native void sleep(long millis) throws InterruptedException

public void run(){

while(true){

if(Thread.currentThread().isInterrupted()){

System.out.println("Interruted!");

break;

}

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

System.out.println("Interruted When Sleep");

//設置中斷狀態,拋出異常後會清除中斷標記位

Thread.currentThread().interrupt();

}

Thread.yield();

}

}


(4)掛起(suspend)和繼續執行(resume)線程

– suspend()不會釋放鎖

– 如果加鎖發生在resume()之前 ,則死鎖發生


(5)等待線程結束(join)和謙讓(yeild) 
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedExceptio
yeild:釋放當前佔用的CPU,並再來競爭(給你機會跟我搶)              
                         
四,守護線程
在後臺默默地完成一些系統性的服務,比如垃圾回收線程、 JIT線程就可以理解爲守護線程
當一個Java應用內,只有守護線程時,Java虛擬機就會自然退出
Thread t=new DaemonT();
t.setDaemon(true);  //設置爲守護線程
t.start();

五,線程優先級
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
例子:
Thread high=new HightPriority();
LowPriority low=new LowPriority();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
高優先級的線程更容易再競爭中獲勝

六,基本的線程同步操作
synchronized
– 指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖。
– 直接作用於實例方法:相當於對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
– 直接作用於靜態方法:相當於對當前類加鎖,進入同步代碼前要獲得當前類的鎖。

等待和喚醒


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章