一、多線程是什么?為什么要用多線程?
介紹多線程之前要介紹線程,介紹線程則離不開進程。
首先 進程 :是一個正在執行中的程序,每一個進程執行都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元;
線程:就是進程中的一個獨立控制單元,線程在控制着進程的執行。一個進程中至少有一個進程。
多線程:一個進程中不只有一個線程。
為什么要用多線程:
①、為了更好的利用cpu的資源,如果只有一個線程,則第二個任務必須等到第一個任務結束后才能進行,如果使用多線程則在主線程執行任務的同時可以執行其他任務,而不需要等待;
②、進程之間不能共享數據,線程可以;
③、系統創建進程需要為該進程重新分配系統資源,創建線程代價比較小;
④、Java語言內置了多線程功能支持,簡化了java多線程編程。
二、線程的生命周期:
- 新建 :從新建一個線程對象到程序start() 這個線程之間的狀態,都是新建狀態;
- 就緒 :線程對象調用start()方法后,就處於就緒狀態,等到JVM里的線程調度器的調度;
- 運行 :就緒狀態下的線程在獲取CPU資源后就可以執行run(),此時的線程便處於運行狀態,運行狀態的線程可變為就緒、阻塞及死亡三種狀態。
- 等待/阻塞/睡眠 :在一個線程執行了sleep(睡眠)、suspend(掛起)等方法后會失去所占有的資源,從而進入阻塞狀態,在睡眠結束后可重新進入就緒狀態。
- 終止 :run()方法完成后或發生其他終止條件時就會切換到終止狀態。
三、創建線程的方法:具體實現代碼詳解請看點擊后方鏈接:多線程擴展一、創建線程的三種方法詳細對比(http://www.cnblogs.com/yjboke/p/8919090.html)
1、繼承Thread類:
步驟:①、定義類繼承Thread;
2、實現Runnable接口: 接口應該由那些打算通過某一線程執行其實例的類來實現。類必須定義一個稱為run 的無參方法。
實現步驟: ①、定義類實現Runnable接口
②、覆蓋Runnable接口中的run方法
將線程要運行的代碼放在該run方法中。
③、通過Thread類建立線程對象。
④、將Runnable接口的子類對象作為實際參數傳遞給Thread類的構造函數。
自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程執行指定對象的run方法就要先明確run方法所屬對象
⑤、調用Thread類的start方法開啟線程並調用Runnable接口子類的run方法。
3、通過Callable和Future創建線程:
實現步驟:①、創建Callable接口的實現類,並實現call()方法,改方法將作為線程執行體,且具有返回值。
②、創建Callable實現類的實例,使用FutrueTask類進行包裝Callable對象,FutureTask對象封裝了Callable對象的call()方法的返回值
③、使用FutureTask對象作為Thread對象啟動新線程。
④、調用FutureTask對象的get()方法獲取子線程執行結束后的返回值。
四、繼承Thread類和實現Runnable接口、實現Callable接口的區別。
繼承Thread:線程代碼存放在Thread子類run方法中。
優勢:編寫簡單,可直接用this.getname()獲取當前線程,不必使用Thread.currentThread()方法。
劣勢:已經繼承了Thread類,無法再繼承其他類。
實現Runnable:線程代碼存放在接口的子類的run方法中。
優勢:避免了單繼承的局限性、多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。
劣勢:比較復雜、訪問線程必須使用Thread.currentThread()方法、無返回值。
實現Callable:
優勢:有返回值、避免了單繼承的局限性、多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。
劣勢:比較復雜、訪問線程必須使用Thread.currentThread()方法
建議使用實現接口的方式創建多線程。
五、線程狀態管理
1、線程睡眠---sleep:
線程睡眠的原因:線程執行的太快,或需要強制執行到下一個線程。
線程睡眠的方法(兩個):sleep(long millis)在指定的毫秒數內讓正在執行的線程休眠。
sleep(long millis,int nanos)在指定的毫秒數加指定的納秒數內讓正在執行的線程休眠。
線程睡眠的代碼演示:
public class SynTest { public static void main(String[] args) { new Thread(new CountDown(),"倒計時").start(); } } class CountDown implements Runnable{ int time = 10; public void run() { while (true) { if(time>=0){ System.out.println(Thread.currentThread().getName() + ":" + time--); try { Thread.sleep(1000); //睡眠時間為1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
每隔一秒則會打印一次,打印結果為:
倒計時:10 倒計時:9 倒計時:8 倒計時:7 倒計時:6 倒計時:5 倒計時:4 倒計時:3 倒計時:2 倒計時:1 倒計時:0
擴展:Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程序的執行效率。但是不管程序員怎么編寫調度,只能最大限度的影響線程執行的次序,而不能做到精准控制。因為使用sleep方法之后,線程是進入阻塞狀態的,只有當睡眠的時間結束,才會重新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,我們不可能精准的去干涉它,所以如果調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。
2、線程讓步---yield:
該方法和sleep方法類似,也是Thread類提供的一個靜態方法,可以讓正在執行的線程暫停,但是不會進入阻塞狀態,而是直接進入就緒狀態。相當於只是將當前線程暫停一下,然后重新進入就緒的線程池中,讓線程調度器重新調度一次。也會出現某個線程調用yield方法后暫停,但之后調度器又將其調度出來重新進入到運行狀態。
public class SynTest { public static void main(String[] args) { yieldDemo ms = new yieldDemo(); Thread t1 = new Thread(ms,"張三吃完還剩"); Thread t2 = new Thread(ms,"李四吃完還剩"); Thread t3 = new Thread(ms,"王五吃完還剩"); t1.start(); t2.start(); t3.start(); } } class yieldDemo implements Runnable{ int count = 20; public void run() { while (true) { if(count>0){ System.out.println(Thread.currentThread().getName() + count-- + "個瓜"); if(count % 2 == 0){ Thread.yield(); //線程讓步 } } } } }
sleep和yield的區別:
①、sleep方法聲明拋出InterruptedException,調用該方法需要捕獲該異常。yield沒有聲明異常,也無需捕獲。
②、sleep方法暫停當前線程后,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。而yield方法調用后 ,是直接進入就緒狀態。
3、線程合並---join:
當B線程執行到了A線程的.join()方法時,B線程就會等待,等A線程都執行完畢,B線程才會執行。
join可以用來臨時加入線程執行。
以下為代碼演示:
public static void main(String[] args) throws InterruptedException { yieldDemo ms = new yieldDemo(); Thread t1 = new Thread(ms,"張三吃完還剩"); Thread t2 = new Thread(ms,"李四吃完還剩"); Thread t3 = new Thread(ms,"王五吃完還剩"); t1.start(); t1.join(); t2.start(); t3.start(); System.out.println( "主線程"); }
4、停止線程:
原stop方法因有缺陷已經停用了,那么現在改如何停止線程?現在分享一種,就是讓run方法結束。
開啟多線程運行,運行的代碼通常是循環結構,只要控制住循環,就可以讓run方法結束,也就是線程結束。
具體代碼如下:
1 public class StopThread { 2 3 public static void main(String[] args) { 4 int num = 0; 5 StopTh st = new StopTh(); 6 Thread t1 = new Thread(st); 7 Thread t2 = new Thread(st); 8 t1.start(); 9 t2.start(); 10 //設置主線程執行50次,執行結束之后停止線程 11 while (true) { 12 if(num++ == 50){ 13 st.flagChange(); 14 break; 15 } 16 System.out.println(Thread.currentThread().getName() + "..." + num); 17 } 18 } 19 } 20 21 class StopTh implements Runnable{ 22 23 private boolean flag = true; 24 public void run() { 25 while(flag){ 26 System.out.println(Thread.currentThread().getName() + "stop run" ); 27 } 28 } 29 public void flagChange(){ 30 flag = false; 31 } 32 }
特殊情況:當線程處於了凍結狀態,就不會讀取到標記,也就不會結束。當沒有指定方法讓凍結的線程回復到運行狀態時,我們需要對凍結狀態進行清除,也就是強制讓線程恢復到運行狀態中來,這樣可就可以操作標記讓線程結束。
Thread類提供該方法: interrupt();(如果線程在調用Object類的wait()、wait(long)、wait(long,int)方法,或者該類的join()、join(long)、join(long、int)、sleep(long)或sleep(long、int)方法過程中受阻,則其中斷狀態將被清除,還將收到一個InterruptedException。)
5、設置優先級:
每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的也並非沒機會執行。
Thread類中提供了優先級的三個常量,代碼如下:
MAX_PRIORITY =10 MIN_PRIORITY =1 NORM_PRIORITY =5
------------------------------------------------------
ThreadDemo td = new ThreadDemo();
Thread t1 = new Thread(td,"張三");
t1.priority(9); //設置優先級
t1.start(); //設置完畢
六、線程同步與鎖。
為什么要進行線程同步?
java允許多線程並發控制,當多個線程同時操作一個可共享資源變量時(如對其進行增刪改查操作),會導致數據不准確,而且相互之間產生沖突。所以加入同步鎖以避免該線程在沒有完成操作前被其他線程調用,從而保證該變量的唯一性和准確性。
不同步會發生的問題?
在介紹同步方法之前先演示一下當多個線程操作一個共享資源時可能會發生的錯誤,這里用的方法是讓線程在執行時睡眠10毫秒,會導致多個線程去操作同一個資源變量:
1 public class SynTest { 2 public static void main(String[] args) { 3 //定義三個線程, 4 MySyn ms = new MySyn(); 5 Thread t1 = new Thread(ms,"線程1輸出:"); 6 Thread t2 = new Thread(ms,"線程2輸出:"); 7 Thread t3 = new Thread(ms,"線程3輸出:"); 8 t1.start(); 9 t2.start(); 10 t3.start(); 11 } 12 } 13 14 class MySyn implements Runnable{ 15 16 int tick = 10; //共執行10次線程 17 public void run() { 18 while(true){ 19 if(tick>0){ 20 try { 21 Thread.sleep(10); //執行中讓線程睡眠10毫秒, 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 System.out.println(Thread.currentThread().getName() + " " + tick--); 26 } 27 } 28 } 29 }
輸出結果用以下圖片展示,可以看到我勾選的部分都發生了沖突數據:
同步方法1:
同步函數:就是用synchronize關鍵字修飾的方法。因為每個java對象都有一個內置鎖,當用synchronize關鍵字修飾方法時內置鎖會保護整個方法,而在調用該方法之前,要先獲得內置鎖,否則就會處於阻塞狀態。
代碼演示:請將上方代碼的第17行改為以下代碼↓
public synchronized void run() {
同步方法2:
同步代碼塊:就是擁有synchronize關鍵字修飾的語句塊,被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
代碼演示:將上方代碼的run方法改成下方代碼
public void run() { while(true){ synchronized (this) { //同步代碼塊 if(tick>0){ try { Thread.sleep(10); //執行中讓線程睡眠10毫秒, } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + tick--); } } } }
加同步之后的輸出數據為:
線程1輸出: 10 線程2輸出: 9 線程2輸出: 8 線程2輸出: 7 線程2輸出: 6 線程2輸出: 5 線程2輸出: 4 線程3輸出: 3 線程3輸出: 2 線程3輸出: 1
追加問題:如果同步函數被靜態修飾之后,使用的鎖是什么?靜態方法中不能定義this!
靜態內存是:內存中沒有本類對象,但是一定有該類對應的字節碼文件對象。 類名.class 該對象類型是Class。
所以靜態的同步方法使用的鎖是該方法所在類的字節碼文件對象。 類名.class。代碼如下:
public static mySyn(String name){ synchronized (Xxx.class) { Xxx.name = name; } }
--------------------------------以下是總結-----------------------------------
七、死鎖
進程A中包含資源A,進程B中包含資源B,A的下一步需要資源B,B的下一步需要資源A,所以它們就互相等待對方占有的資源釋放,所以也就產生了一個循環等待死鎖。
1 public class DeadLock { 2 3 public static void main(String[] args) { 4 Thread t1 = new Thread(new DeadLockTest(true)); 5 Thread t2 = new Thread(new DeadLockTest(false)); 6 t1.start(); 7 t2.start(); 8 } 9 } 10 11 class DeadLockTest implements Runnable{ 12 13 private boolean flag; 14 static Object obj1 = new Object(); 15 static Object obj2 = new Object(); 16 public DeadLockTest(boolean flag) { 17 this.flag = flag; 18 } 19 public void run(){ 20 if(flag){ 21 synchronized(obj1){ 22 System.out.println("if lock1"); 23 synchronized (obj2) { 24 System.out.println("if lock2"); 25 } 26 } 27 }else{ 28 synchronized (obj2) { 29 System.out.println("else lock2"); 30 synchronized (obj1) { 31 System.out.println("else lock1"); 32 } 33 } 34 } 35 } 36 }
死鎖形成的必要條件總結(都滿足之后就會產生):
①、互斥條件:資源不能被共享,只能被同一個進程使用;
②、請求與保持條件:已經得到資源的進程可以申請新的資源;
③、非剝奪條件:已經分配的資源不能從相應的進程中強制剝奪;
④、循環等待條件:系統中若干進程形成環路,該環路中每個進程都在等待相鄰進程占用的資源。
八、線程間的通信,等待喚醒機制。
九、線程池