第9次文章:執行緒

  • 2019 年 10 月 8 日
  • 筆記

終於加班兒趕完了這周的總結,拖延症是要改改了

一、概念:程式 執行緒 進程

程式:屬於我們所寫的指令集,是一個靜態概念

進程:作業系統調度我們的程式,是一個動態概念

執行緒:在進程內開闢多條執行路徑,每一個執行緒都可以類似於一個小的進程

二、創建執行緒

模擬多執行緒,首先需要創建執行緒。創建執行緒的方法主要有以下幾種方法:

方法一:繼承Thread + run()

啟動:創建子類對象 +對象.start()

下面我們模擬一下龜兔賽跑進行分析:

1)首先建立對應的兔子和烏龜類

public class Rabbit extends Thread{  @Override  public void run() {  for (int i=0; i<100; i++) {  System.out.println("兔子跑了"+i+"步");  }  }  }  class Torrit extends Thread{  @Override  public void run() {  for (int i=0; i<100; i++) {  System.out.println("烏龜跑了"+i+"步");  }  }  }

2)下一步我們創建對應的子類對象,並使用對象.start()進行啟動對象的執行緒

public class RabbitApp {  public static void main(String[] args) {  Rabbit rab = new Rabbit();  Torrit tor = new Torrit();          rab.start();//啟動兔子執行緒  tor.start();//啟動烏龜執行緒  for (int i=0; i<100; i++) {  System.out.println("main=====>"+i);  }  }  }

分析:在這個main方法中,我們開啟了3條執行緒,分別對應的是兔子對象rabbit,烏龜對象torrit,以及程式的主執行緒main方法。在啟動對象的執行緒的時候,一定不能調用我們重寫的run()方法!!!如果調用run()方法,就屬於普通的方法調用,那麼整個cpu會根據程式中的順序,依次執行每一個執行緒,並且會等待上一個執行緒全部執行完之後,才會執行下一段執行緒。那麼我們模擬的就不是多執行緒了,而是單執行緒。所以我們在啟動的時候需要調用父類對象Thread的start方法。這時,電腦的cpu會按照時間片的分配,同時進行3條執行緒。這才是我們模擬的多執行緒。

方法二:實現Runnable + run()

主要步驟有以下幾步:

1、類 :實現Runable介面 + 重寫run()

2、啟動多執行緒,使用靜態代理

1)創建真實角色

2)創建代理角色 Thread +引用

3)代理角色.start()

結合以下程式碼進行分析:

1)創建一個類對象,並且該類對象需要實現Runnable介面,並且重寫run()方法

public class Programer implements Runnable{  @Override  public void run() {  for (int i=0;i<100;i++) {  System.out.println("一邊敲helloworld......"+i);  }  }  }

2)啟動多執行緒,使用靜態代理

public class ProgramerApp {  public static void main(String[] args) {  //1)創建真實角色  Programer programer = new Programer();  //2)創建代理角色 + 真實角色引用  Thread thread =new Thread(programer);  //3)調用.start() 啟動執行緒  thread.start();  for (int i=0;i<100;i++) {  System.out.println("一邊聊qq....."+i);  }  }  }

分析:

(1)將第二種創建多執行緒的方法和第一種創建多執行緒的方法進行對比之後,可以發現第二種方法具有明顯的優勢。在java中,具有單繼承多實現的特點,在創建類的時候,我們只能繼承一個父類對象,但是在實現介面的時候,我們可以實現多個介面。所以我們使用實現介面的方法來進行多執行緒的創建,可以給我們未來繼承的其他父類預留位置。

(2)與此同時,方法二具有資源共享的特點。這個特點我們再結合下面一段模擬「12306」搶票軟體的運行方式進行講解。

public class Web12306 implements Runnable {  private int num = 10;  @Override  public void run() {  while(true) {  if(num<=0) {  break;  }  System.out.println(Thread.currentThread().getName()+"搶到了第--"+num--+"--張票");  }  }  public static void main(String[] args) {  //創建真實對象  Web12306 web = new Web12306();  //創建代理對象  Thread t1 = new Thread(web,"路人甲");  Thread t2 = new Thread(web,"路人乙");  Thread t3 = new Thread(web,"路人丙");  //啟動執行緒  t1.start();  t2.start();  t3.start();  }  }

查看結果:

路人乙搶到了第--9--張票  路人丙搶到了第--8--張票  路人丙搶到了第--6--張票  路人丙搶到了第--5--張票  路人丙搶到了第--4--張票  路人甲搶到了第--10--張票  路人丙搶到了第--3--張票  路人乙搶到了第--7--張票  路人丙搶到了第--1--張票  路人甲搶到了第--2--張票

分析:如以上場景所示,我們將需要發售的10張車票都放入run()方法中,在外部使用3個執行緒進行模擬搶票場景。「路人甲」,「路人乙」,「路人丙「,分別代表3個執行緒,同時啟動,然後運行run裡面的內容,將10張票進行了資源的共享。

所以綜上所述:

在創建的多執行緒的時候,強烈推薦使用方法二,實現介面的方法,具有「避免單繼承的局限性」和「便於資源共享」的優點。

三、執行緒狀態

新生狀態、就緒狀態、運行狀態、阻塞狀態、死亡狀態

1、停止執行緒

第一種:自然終止,執行緒體正常執行完畢

第二種:外部干涉:

1)、執行緒類中 定義 執行緒體使用的標識

2)、執行緒體中使用該標識

3)、對外提供該標識的設置方法

4)、外部根據該條件調用該方法即可

第一種不必過多敘述,下面結合程式碼詳解第二種。

public class StopDemo01 {  public static void main(String[] args) {  Study s = new Study();  new Thread(s).start();//使用匿名類  for(int i = 0;i <= 10 ;i++) {  if(5 == i) {//4)、根據外部條件進行調用stop方法  s.stop();  }  System.out.println("main----->"+i);  }  }  }  class Study implements Runnable {  //1)、執行緒類中 定義 執行緒體使用的標識  private boolean flag = true;  @Override  public void run() {  //2)、執行緒體中使用該標識  while(flag) {  System.out.println("study thread.....");  }  }  //3)、對外提供該標識的設置方法  public void stop() {  this.flag = false;  }  }

分析:在我們重寫run()方法的時候,我們使用了一個自己定義的boolean標識符flag,並且提供相應的重設方法。然後在外部進行按需調用,短暫的停止本執行緒。

2、阻塞狀態

(1)join:合併執行緒,主要是將多條執行緒合併為一條執行緒,然後按照順序依次執行

public class JoinDemo01 extends Thread {  @Override  public void run() {  for (int i =0 ; i<100 ; i++) {  System.out.println("join...."+i);  }  }  public static void main(String[] args) throws InterruptedException {  JoinDemo01 join = new JoinDemo01();//新生  Thread t = new Thread(join);//新生狀態  t.start();//就緒狀態  //cpu調度 進入運行  for (int i=0 ;i<100;i++) {  if (50 == i) {  t.join();//此時 cpu阻塞了main方法  必須將t執行完之後,才能夠重新開始執行main執行緒  }  System.out.println("main....."+i);  }  }  }

分析:在上述程式碼塊中,我們首先是啟動join執行緒,該執行緒和main方法同時在cpu進行運行,當我們計數計到50的時候,我們調用了父類Thread中的join方法,然後將join執行緒對象合併到main執行緒中,所以此時電腦中只剩下了一條執行緒,main方法中未執行完的任務被排在了執行緒對像join後面,等到join全部執行完之後,電腦再重新執行main方法中的剩餘部分。

(2)yield:暫停自己的執行緒,將執行緒讓給其他執行緒,是一個static方法

public class YieldDemo01 extends Thread{  @Override  public void run() {  for(int i=0;i<100;i++) {  System.out.println("Yield....."+i);  }  }  public static void main(String[] args) {  YieldDemo01 yield = new YieldDemo01();//新生狀態  Thread t = new Thread(yield);//新生代理  t.start();//就緒狀態  for(int i =0;i<100;i++) {  if(0 == i%20) {  Thread.yield();//暫停main函數  }  System.out.println("mian----->"+i);  }  }  }

分析:在上述程式碼中,我們將Thread.yield()放在了main方法中,所以當外部條件滿足的時候,程式將進入if語句中,執行Thread.yield()語句,然後暫停main方法的執行緒。

(3)sleep:休眠,此時執行緒並不不釋放鎖,本執行緒也不會被其他執行緒訪問

我們模擬一個與時間相關的倒計時來進行說明:

public class SleepDemo01 {  public static void main(String[] args) throws InterruptedException  {  Date endTime = new Date(System.currentTimeMillis()+10*1000);  long end = endTime.getTime();  while(true) {  //輸出  System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(endTime));  //構建下一秒的時間  endTime = new Date(endTime.getTime() - 1000);  //等待一秒  Thread.sleep(1000);//暫停1秒  //10秒以內繼續  否則就退出  if(end-10*1000 > endTime.getTime()) {  break;  }  }  }  }

我們查看一下結果:

2019-03-10 21:49:42  2019-03-10 21:49:41  2019-03-10 21:49:40  2019-03-10 21:49:39  2019-03-10 21:49:38  2019-03-10 21:49:37  2019-03-10 21:49:36  2019-03-10 21:49:35  2019-03-10 21:49:34  2019-03-10 21:49:33  2019-03-10 21:49:32

分析:最後的結果是,每間隔一秒鐘,列印當前時間的10以內的時間。如果沒有使用Thread.sleep()方法,那麼在控制台上列印時間的時候,將會在一瞬間完成,列印出上面的結果。加入sleep方法之後,每次列印時間間隔1秒。