java中線程(Thread)的知識很重要,沒有它,我們項目中的很多功能都無法實現。跟線程有關的是進程,日常生活中我們聽的比較多的是進程,通常我們的電腦卡了,我們就會說要殺進程。進程跟線程是不同的概念,兩者有區別也有聯系。進程,通俗的講就是我們電腦中運行中的程序,程序的概念是靜態的,進程是動態的概念。像我們電腦運行的視頻播放器,音樂播放器都是進程。線程,是運行在進程中的順序控制流,只能使用分配給進程的資源和環境。一個進程中至少會有一個線程。
了解線程的相關概念后,我們現在來將如何實現線程。線程的實現方式有兩種。一種是通過繼承Thread類,并重寫run()方法實現;另一種是通過實現Runnable接口并實現其run()方法。下面通過例子來分析兩種實現的區別。
1、通過繼承Thread類
1 package thread; 2 /** 3 * 4 * @author CIACs 5 *線程的生成通過繼承Thread類實現 6 */ 7 public class ThreadTest { 8 public static void main(String[] args) { 9 MyThread1 t1 = new MyThread1();10 t1.start();11 MyThread2 t2 = new MyThread2();12 t2.start();13 }14 15 }16 17 class MyThread1 extends Thread18 {19 20 @Override21 public void run() {22 for(int i=0;i<50;i++)23 {24 System.out.控制臺輸出結果:

在這里我們可以看到兩個線程會交叉執行,并不是一個先執行完后,另一個再執行。這就是說當線程啟動后我們是不能控制執行順序的。(當然是在還沒用synchronized、wait()、notify()的時候)
2、通過實現Runnable接口
1 package thread; 2 /** 3 * 4 * @author CIACs 5 *線程通過實現Runnable接口生成 6 */ 7 public class ThreadTest2 { 8 public static void main(String[] args) { 9 Thread1 t1 = new Thread1();10 new Thread(t1).start();11 Thread2 t2 = new Thread2();12 new Thread(t2).start();13 }14 15 }16 17 class Thread1 implements Runnable18 {19 @Override20 public void run() {21 for(int i=0;i<50;i++)22 {23 System.out.println("Thread1 running "+i);24 }25 26 }27 }28 29 class Thread2 implements Runnable30 {31 @Override32 public void run() {33 for(int i=0;i<50;i++)34 {35 System.out.println("Thread2 running "+i);36 }37 38 }39 }控制臺輸出結果:

在這里我只上傳了部分結果的截圖,我們所要的在這部分截圖中就可以看出了。
在編寫程序時我們把希望線程執行的代碼放到run()方法中,然后通過調用start()方法來啟動線程,start()方法會為線程的執行準備好資源,之后再去調用run()方法,當某個類繼承了Thread類后,該類就是線程類。線程的消亡不能通過調用stop()方法,而是讓run()方法自然結束。
每個線程有其優先級,最高為10(MAX_PRIORITY),最低為1(MIN_PRIORITY),設置優先級是為了在多線程環境中便于系統對線程的調度,同等情況下,優先級高的會先比優先級低的執行。當然操作系統也不是完全按照優先級高低執行的,否則有可能優先級低的會一直處于等待狀態,操作系統有自己的調度算法(這里就先不展開討論了)。當運行中的線程調用了yield()方法,就會讓出cpu的占用;調用sleep()方法會使線程進入睡眠狀態,此時其他線程也就可以占用cpu資源了;有另一個更高優先級的線程出現也會導致運行中的線程讓出cpu資源。有些調度算法是分配固定的時間片給線程執行,在這段時間內可以占用cpu,一旦用完就必須讓出cpu資源。
線程的生命周期有如下四個
1、創建狀態,當用new創建一個新的線程對象時,該線程處于創建狀態,但此時系統沒有分配資源給它。
2、可運行狀態,執行線程的start()方法后系統將會分配線程運行所需的系統資源,并調用線程體run()方法,此時線程處于可運行狀態。
3、不可運行狀態,當線程調用了sleep()方法,或者wait()方法時,線程處于不可運行狀態,線程的輸入輸出阻塞時也會導致線程不可運行。
4、消亡狀態,當線程的run()方法執行結束后,就進入消亡狀態。
下圖是普通線程的狀態轉換圖:

當使用了synchronized關鍵字時,線程的狀態轉換圖會有點不同,如下圖

控制多線程同步,使用wait()和notify()的線程狀態轉換圖如下:

對于單核cpu來說,某一時刻只能有一個線程在執行,但在宏觀上我們會看到多個進程在執行,這就是微觀串行,宏觀上并行。現在單核的電腦基本上已經沒有了。多核的電腦就可以實現微觀并行。多線程編程就是為了最大限度的利用cpu資源。例如當某一個線程和外設打交道時,此時它不需要用到cpu資源,但它仍然占著cpu,其他的線程就不能利用,多線程編程就可以解決該問題。多線程是多任務處理的一種特殊形式。但是多線程可能會造成沖突,兩個或多個線程要同時訪問一個共同資源。
下面以銀行取款為例,有一個賬號,里面存1000元,然后創建兩個線程模擬從銀行柜臺和ATM同時取700元。這里我們的銀行卡不能透支。
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class BankAccount { 8 private int MyMoney = 1000; 9 10 public void getMoney(int money)11 {12 if(MyMoney<=0||(MyMoney-money)<0)13 {14 System.out.println("余額不足");15 }16 else17 {18 try {19 Thread.sleep((long)(Math.random()*1000));20 } catch (InterruptedException e) {21 e.printStackTrace();22 }23 System.out.println("取"+money);24 MyMoney = MyMoney-money;25 }26 System.out.println("賬戶剩余的錢"+MyMoney);27 }28 } 1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Thread1 extends Thread{ 8 private BankAccount MyBank; 9 public Thread1(BankAccount ba)10 {11 this.MyBank = ba;12 }13 @Override14 public void run() {15 16 MyBank.getMoney(700);17 }18 19 } 1 package thread; 2 3 public class Client { 4 public static void main(String[] args) { 5 BankAccount MyBank = new BankAccount(); 6 Thread1 t1 = new Thread1(MyBank); 7 Thread1 t2 = new Thread1(MyBank); 8 t1.start(); 9 t2.start();10 11 }12 }控制臺輸出結果:

我們取了1400,賬戶余額未-400,很明顯銀行是不允許我們這樣做的。這就說,可能當一個線程進入到取錢代碼部分時先進行了睡眠,另一個也進來了,且也進入睡眠。當其中一個醒來時,取完錢后,另一個也醒來繼續取錢。我們如何解決這個問題呢?這時我們就要用synchronized關鍵字來解決。只需在取錢的方法處加上synchronized關鍵字修飾就可以解決該問題。
加上后控制臺輸出結果:

synchronized關鍵字是同步的意思,當synchronized修飾一個方法時,該方法叫做同步方法。Java中的每個對象都有一個鎖(lock)或者叫監視器(monitor),當我們使用synchronized關鍵字修飾一個對象的方法時,就會把這個對象上鎖,當該對象上鎖時,其他的線程就無法再訪問該對象的方法,直到線程結束或拋出異常,該對象的鎖就會釋放掉??此圃诜椒ㄇ凹由蟬ynchronized關鍵字修飾好像完美的解決了多線程訪問同一資源的沖突問題,但是由于synchronized是粗粒度的,也就是使用了該關鍵字會把方法所對應的類也會鎖上,該類的任何其他方法都不會被其他線程所訪問,這樣就導致了資源利用率降低。為了解決這個方法,我們還是要利用synchronized關鍵字。此時synchronized關鍵字鎖的是我們創建的任何一個對象。synchronized塊代碼如下
1 private obj = new Object();2 synchronized(obj)3 {4 //線程要執行的代碼5 }此時我們鎖的是我們創建的一個任一對象,synchronized塊外面的方法是沒有被鎖的,也就是說其他線程可以訪問synchronized塊外面的方法。
wait()跟notify()方法可以實現線程的等待和喚醒操作,這兩個方法都是定義在Object類中的,而且是final的,因此會被所有的java類所繼承,且無法重寫。調用這兩個方法要求線程已經獲得了對象的鎖,因此會把這兩個方法放在synchronized方法或塊中,且是成對出現的。執行wait()時,線程會釋放對象的鎖,還有一個方法也會使線程暫停執行,那就是sleep()方法,不過該方法不會釋放對象的鎖。
生產者,消費者問題在每一本學習線程的書上都會有提到,大概就是說生產者生產了商品就通知消費者消費商品,當消費者消費了商品酒通知生產者生產商品,這里涉及到了線程間的通信。我們用輸出0和1來模擬生產者消費者問題。當number為1時就要減一,當number為0時就要加一。要解決該問題就要用wait()跟notify()方法結合synchronized的使用。
Number類
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Number 8 { 9 private int number = 1;10 public synchronized void AddNumber()11 {12 if(number > 0)13 {14 try15 {16 //當不符合條件時進入等待狀態,讓出cpu資源17 wait();18 } catch (InterruptedException e)19 {20 e.printStackTrace();21 }22 }23 //執行加操作24 number++;25 System.out.println(number);26 //喚醒減操作的線程27 notify();28 }29 30 public synchronized void SubNumber()31 {32 if(number == 0)33 {34 try35 {36 //等于0時進入等待狀態37 wait();38 } catch (InterruptedException e)39 {40 e.printStackTrace();41 }42 }43 //執行減操作44 number--;45 System.out.println(number);46 //喚醒加操作的線程47 notify();48 }49 }AddThread類
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class AddThread extends Thread 8 { 9 //設置線程執行的標志,初始為true10 boolean flag = true;11 private Number number;12 public AddThread(Number number)13 {14 this.number = number;15 }16 //改變判斷標志,使線程停止17 public void setFlag()18 {19 this.flag = false;20 }21 22 @Override23 public void run()24 {25 while(flag)26 {27 try28 {29 Thread.sleep((long)(Math.random()*1000));30 this.number.AddNumber();31 } catch (InterruptedException e)32 {33 e.printStackTrace();34 }35 }36 }37 }SubThread類
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class SubThread extends Thread 8 { 9 //設置線程執行的標志,初始為true10 boolean flag = true;11 private Number number;12 public SubThread(Number number)13 {14 this.number = number;15 }16 //改變標志的值,使線程停止17 public void setFlag()18 {19 this.flag = false;20 }21 22 @Override23 public void run()24 {25 while(flag)26 {27 try28 {29 Thread.sleep((long)(Math.random()*1000));30 this.number.SubNumber();31 } catch (InterruptedException e)32 {33 e.printStackTrace();34 }35 }36 }37 }客戶端
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Client 8 { 9 public static void main(String[] args)10 {11 Number num = new Number();12 AddThread t1 = new AddThread(num);13 SubThread t2 = new SubThread(num);14 t2.start();15 t1.start();16 try17 {18 //過了三秒后使其中一個線程停止,一個線程停止后,另一個也不能執行,因為另一個線程處于等待狀態。19 Thread.sleep((long)(Math.random()*3000));20 t1.setFlag();21 22 } catch (InterruptedException e)23 {24 e.printStackTrace();25 }26 27 }28 }控制臺輸出結果:

在這個例子中我利用flag標志來控制線程的停止和執行,我們不會用stop()方法去控制線程的停止。
在線程的使用中我們還要注意的是如果線程中有控制的是局部變量則每個線程對局部變量的改變互不影響,如果是成員變量則會影響到其他線程。使用好線程,我們能提高做事的效率。
新聞熱點
疑難解答