程序:計算機指令的集合,它以文件的形式存儲在磁盤上,是應用程序執行的藍本。
進程:是一個程序在其自身的地址空間中的一次執行活動。進程是資源申請、調度和獨立運行的單位,因此,它使用系統中的運行資源。而程序不能申請系統資源,不能被系統調度,也不能作為獨立運行的單位,它不占用系統的運行資源。作為藍本的程序可以被多次加載到系統的不同內存區域分別執行,形成不同的進程。基于進程的特點是允許計算機同時運行兩個或更多的程序。
線程:是進程中的一個單一的順序控制流程,一個進程在執行過程中,可以產生多個線程。每個線程也有自己產生、存在和消亡的過程。
線程又稱為輕量級進程,它和進程一樣擁有獨立的執行控制,由操作系統負責調度,區別在于線程沒有獨立的存儲空間,而是和所屬進程中的其它線程共享一個存儲空間。這使得線程間的通信遠較進程簡單,而進程之間的通信則比較困難,另外在資源的占用上,線程比進程要小。
線程(Thread)和進程(Process)的關系很密切,進程和線程是兩個不同的概念,進程的范圍大于線程
通俗的講,進程就是一個程序,線程是這個程序能夠同時做的各件事情。例如:媒體播放器運行時就是一個進程,而媒體播放器同時下載文件和播放歌曲就是兩個線程。
從另一個角度講,每個進程都擁有一組完整的屬于自己的變量,而線程則共享一個進程中的這些數據。
主線程:程序啟動時,一個線程立即運行,該線程稱為程序的主線程。(main()方法),其他線程都是由主線程產生的,主線程通常必須最后完成執行,因此需要執行各種關閉動作。
觀察以下程序:
public class MainThreadDemo {public static void main(String args[]) {Thread t = Thread.currentThread();System.out.println("當前線程名稱是: " + t.getName());t.setName("MyJavaThread");System.out.println("改名后線程名稱是: " + t.getName());System.out.println("輸出當前線程: " + t); }
}
代碼分析:
Thread.currentThread()
是一個靜態方法,返回正在執行的線程對象的引用。getName()
方法可以得到當前引用線程的名稱setName(String s)
可以改變線程的內部名稱- 當將一個線程對象作為輸出時,將輸出
[線程名稱,優先級別,線程組名 ]
- 每個線程都屬于一個線程組,如果沒有設定,則由JVM來設定。
并發編程:在計算機編程中有一個基本概念,就是在同一時刻處理多個任務的思想。許多程序設計問題都要求程序能夠停下正在做的工作,轉而處理某個其他問題,然后再返回主進程。多線程的任務相比傳統的進程而言(只有一個主線程),可以同時有多個地方執行代碼。
把問題切分成多個可獨立運行的部分(任務),從而提高程序的響應能力。在程序中,這些彼此獨立運行的部分稱之為線程,上述概念被稱為“并發”。
Java提供了類 java.lang.Thread
來方便多線程編程,這個類提供了大量的方法方便控制線程.
Thread
類最重要的方法是run()
,它為Thread
類的方法start()
所調用,為了指定我們自己的代碼,需要覆蓋run()
方法,來提供我們線程所要執行的代碼。
創建線程
- 繼承
java.lang.Thread()
類,覆蓋run()
方法。在創建的Thread
類的子類中重寫run()
,加入線程所要執行的代碼。
class mythread extends Thread
{public void run(){}
}
例如:
package Test;class FileTransThread extends Thread{ private String fileName; public FileTransThread(String fileName){ this.fileName = fileName; } public void run(){ System.out.println("傳送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "傳送完畢"); }
}
public class Main { public static void main(String[] args) throws Exception { FileTransThread ft1 = new FileTransThread("文件1"); FileTransThread ft2 = new FileTransThread("文件2"); FileTransThread ft3 = new FileTransThread("文件3"); ft1.start(); System.out.println("1");System.out.println("2");ft2.start();System.out.println("3");System.out.println("4");ft3.start(); System.out.println("5");System.out.println("6");}
}
運行結果:(兩次完全相同的運行,但是結果卻不同,說明線程大概是同步進行的,而且順序是隨機的)
- 繼承
java.lang.Thread()
類方法簡單明了,符合大家的習慣。可是如果這個類已經繼承了一個類,則沒有辦法再繼承java.lang.Thread()
類。這時我們可以實現java.lang.Runnable
接口,并實現run()
方法。然后通過這個類創建線程。詳見實例:
class mythread implements Runnable {public void run( ) {/* 實現該方法*/ }
}
例如:
package Test;class FileTransRunnable implements Runnable
{ private String fileName; public FileTransRunnable(String fileName){ this.fileName = fileName; }public void run(){ System.out.println("傳送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "傳送完畢"); }
}
public class Main
{ public static void main(String[] args) throws Exception { FileTransRunnable ft1 = new FileTransRunnable("文件1"); FileTransRunnable ft2 = new FileTransRunnable("文件2"); FileTransRunnable ft3 = new FileTransRunnable("文件3");Thread f1=new Thread(ft1); //創建線程Thread f2=new Thread(ft2);Thread f3=new Thread(ft3);f1.start(); f2.start();f3.start();}
}
運行對象:
- 對象可以自由地繼承自另一個類。
- 同一個runnable對象可以傳遞給多個線程,可以實現資源共享。
線程的啟動
新建的線程不會自動開始運行,必須通過strat()
方法啟動線程。如果不調用這個方法,線程將不會運行。
線程的實現
線程從創建、啟動到終止的整個過程叫做一個生命周期。線程總共由五個狀態。
- 新建狀態(new):創建之后還沒有調用
start()
。 - 就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的
start()
方法,處于該狀態的線程位于可運行線程池中,成為可運行,等待獲取CPU的使用權。線程有資格運行但調度程序還沒有把他選定為運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。在線程運行之后或者從阻塞、等待或睡眠狀態回來后,也返回到可運行狀態。 - 運行狀態(Running):線程調度程序從可運行池中選擇一個線程作為當前線程時線程所處的狀態,這也是線程進入運行狀態的唯一一種方式。就緒狀態的線程獲取了CPU,執行程序代碼。
- 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
- 等待阻塞:運行的線程執行wait()方法,該線程放入等待池中。
- 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,該線程放入鎖池中。
- 其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
- 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。這個線程對象也許還存在,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。 如果在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
簡單來講:
- 創建狀態:使用new運算符創建一個線程。
- 可運行狀態:使用start()方法啟動一個線程后,系統分配了資源。
- 運行中狀態:執行線程的run()方法。
- 阻塞狀態:運行的線程因某種原因停止繼續運行。
- 死亡狀態:線程結束。
線程的調度
- 線程睡眠:
public static void sleep(long millis) throws InterruptedException
millis參數設定睡眠的時間,以毫秒為單位。當睡眠結束后,就轉為就緒(Runnable)狀態。sleep()方法的平臺移植性較好。 - 線程等待:Object類中的
wait()
方法,導致當前的線程等待,直到其他線程調用此對象的notify()
方法或notifyAll()
喚醒方法。這個兩個喚醒方法是Object類中的方法,行為等價于調用 wait(0) 一樣。 - 線程讓步:
public static void yield()
方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。通過yield()
方法,當前線程把cpu讓給別的線程,而不用進入休眠狀態而等待很長時間。該方法只影響當前正在運行的線程,且沒有任何機制保證它將會被采納。 - 線程加入:
public final void join() throws InterruptedException
方法,在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉為就緒狀態。
另外,join()方法還有帶超時限制的重載版本。 例如t.join(5000);則讓線程等待5000毫秒,如果超過這個時間,則停止等待,變為可運行狀態。 - 線程喚醒:Object類中的
notify()
方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的。類似的方法還有一個notifyAll()
,喚醒在此對象監視器上等待的所有線程。
多線程的互斥和同步
線程的同步
在多線程的程序中,多個線程可能會對同一個資源并發訪問。這種情況下,如果不對共享的資源進行保護,就可能產生問題。
所謂同步(synchronize),就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回,同時其它線程也不能調用這個方法。
通俗地講,一個線程是否能夠搶占CPU,必須考慮另一個線程中的某種條件,而不能隨便讓操作系統按照默認方式分配CPU,如果條件不具備,就應該等待另一個線程運行,直到條件具備。
同步的原理
Java中每個對象都有一個內置鎖,當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
同步的實現
在Java中通過互斥鎖標志Synchronized關鍵字的運用來實現同步。Java中同步有兩種方法:
- 方法級同步
synchronized void method( ) { //同步的方法}
- 實現方法:在要標志為同步的方法前加上synchronized關鍵字。如:public synchronized void call(String msg){ }
- 實現原理:當調用對象的同步方法時,線程取得對象鎖或監視器,如果另一個線程試圖執行同步方法,他就會發現被鎖住了,就會進入掛起狀態,直到對象監視器上的鎖被釋放為止。當鎖住放啊的線程從方法中返回時,只有一個排隊等候的線程可以訪問對象。
- 鎖的作用域:該方法被執行的整個時間。
- 程序塊級同步
synchronized()object
{}
- 臨界區:只希望防止多個線程同時訪問方法內部的部分代碼,而不是防止訪問整個方法,通過這種方式分離出來的代碼段被稱為“臨界區”,即需要進行互斥的代碼段。
- 實現方法:用
synchronized
來指定某個對象,此對象的鎖被用來對花括號內的代碼進行同步控制。如:
synchronized(target){target.call(msg);}
- 實現原理:在進入同步代碼前,必須得到object對象的鎖,如果其他線程已經得到這個鎖,那么就得等到鎖被釋放后才能進入臨界區。
- 鎖的作用域:只在代碼塊運行的時間內
例子:
class TicketRunnable implements Runnable
{ private int ticketNum = 3; // 以3 張票為例 public void run() { while (true) { String tName = Thread.currentThread().getName(); // 將需要獨占 CPU的代碼用 synchronized(this)包圍起來 synchronized (this) { if (ticketNum <= 0) { System.out.println(tName + "無票"); break; } else { try { Thread.sleep(1000);// 程序休眠 1000 毫秒 }catch (Exception ex) {} ticketNum--; // 代碼行1 System.out.println(tName + "賣出一張票,還剩" + ticketNum + "張票'); } } } }
} public class Main
{ public static void main(String[] args){ TicketRunnable tr = new TicketRunnable(); Thread th1 = new Thread(tr, "thread 1"); Thread th2 = new Thread(tr, "thread 2"); th1.start(); th2.start(); }
}
從以上代碼可以看出,該方法的本質是將需要獨占 CPU 的代碼用synchronized(this)包圍起來。如前所述,一個線程進入這段代碼之后,就在 this 上加了一個標記,直到該線程將這段代碼運行完畢,才釋放這個標記。如果其他線程想要搶占 CPU,先要檢查 this 上是否有這個標記。若有,就必須等待。
但是可以看出,該代碼實際上運行較慢,因為一個線程的運行,必須等待另一個線程將同步代碼段運行完畢。因此,從性能上講,線程同步是非常耗費資源的一種操作。我們要盡量控制線程同步的代碼段范圍,理論上說,同步的代碼段范圍越小,段數越少越好,因此在某些情況下,推薦將小的同步代碼段合并為大的同步代碼段。
死鎖
如果出現一種極端情況,一個線程等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候鏈”如果進入封閉狀態,也就是說,最后那個對象等候的是第一個對象,此時,所有線程都會陷入無休止的相互等待狀態,造成死鎖。盡管這種情況并非經常出現,但一旦碰到,程序的調試將變得異常艱難。
發生死鎖必須同時滿足的四個條件:
- 互斥條件。線程中使用的資源中至少要有一個是不能共享的。
- 至少有一個線程它必須持有一個資源且正在等待獲取一個當前被別的線程持有的資源。
- 資源不能被線程搶占。
- 必須有循環等待,這時,一個線程等待其他線程所持有的資源,后者又在等待另一個線程所持有的資源。
死鎖的實例:(待更,腦袋不轉了)