JavaSE學習筆記(12) 線程 多線程 併發與並行 併發 :指兩個或多個事件在 同一個時間段內 發生。 並行 :指兩個或多個事件在 同一時刻 發生(同時發生)。 在操作系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時運行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微 ...
JavaSE學習筆記(12)---線程
多線程
併發與並行
- 併發:指兩個或多個事件在同一個時間段內發生。
- 並行:指兩個或多個事件在同一時刻發生(同時發生)。
在操作系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時運行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。
而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程式越多,能大大的提高電腦運行的效率。
註意:單核處理器的電腦肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同理,線程也是一樣的,從巨集觀角度上理解線程是並行運行的,但是從微觀角度上分析卻是串列運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之為線程調度。
線程與進程
進程:是指一個記憶體中運行的應用程式,每個進程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個進程;進程也是程式的一次執行過程,是系統運行程式的基本單位;系統運行一個程式即是一個進程從創建、運行到消亡的過程。
線程:線程是進程中的一個執行單元,負責當前進程中程式的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。
簡而言之:一個程式運行後至少有一個進程,一個進程中可以包含多個線程
線程調度:
分時調度
所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。
搶占式調度
優先讓優先順序高的線程使用 CPU,如果線程的優先順序相同,那麼會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。
設置線程的優先順序
搶占式調度詳解
大部分操作系統都支持多進程併發運行,現在的操作系統幾乎都支持同時運行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos視窗等軟體。此時,這些程式是在同時運行,”感覺這些軟體好像在同一時刻運行著“。
實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
其實,多線程程式並不能提高程式的運行速度,但能夠提高程式運行效率,讓CPU的使用率更高。
創建線程類
第一種: 通過繼承Thread類實現多線程
Java使用java.lang.Thread
類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的代碼。Java使用線程執行體來代表這段程式流。Java中通過繼承Thread類來創建並啟動多線程的步驟如下:
- 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
- 創建Thread子類的實例,即創建了線程對象
- 調用線程對象的start()方法來啟動該線程
代碼如下:
測試類:
public class Demo01 {
public static void main(String[] args) {
//創建自定義線程對象
MyThread mt = new MyThread("新的線程!");
//開啟新線程
mt.start();
//在主方法中執行for迴圈
for (int i = 0; i < 10; i++) {
System.out.println("main線程!"+i);
}
}
}
自定義線程類:
public class MyThread extends Thread {
//定義指定線程名稱的構造方法
public MyThread(String name) {
//調用父類的String參數的構造方法,指定線程的名稱
super(name);
}
/**
* 重寫run方法,完成該線程執行的邏輯
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
第二種: 通過Runnable介面實現多線程
在開發中,我們應用更多的是通過Runnable介面實現多線程。這種方式剋服了11.2.1節中實現線程類的缺點,即在實現Runnable介面的同時還可以繼承某個類。所以實現Runnable介面的方式要通用一些。
通過Runnable介面實現多線程
public class TestThread2 implements Runnable {//自定義類實現Runnable介面;
//run()方法里是線程體;
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
//創建線程對象,把實現了Runnable介面的對象作為參數傳入;
Thread thread1 = new Thread(new TestThread2());
thread1.start();//啟動線程;
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}
線程狀態
一個線程對象在它的生命周期內,需要經歷5個狀態。
▪ 新生狀態(New)
用new關鍵字建立一個線程對象後,該線程對象就處於新生狀態。處於新生狀態的線程有自己的記憶體空間,通過調用start方法進入就緒狀態。
▪ 就緒狀態(Runnable)
處於就緒狀態的線程已經具備了運行條件,但是還沒有被分配到CPU,處於“線程就緒隊列”,等待系統為其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會進入執行狀態。一旦獲得CPU,線程就進入運行狀態並自動調用自己的run方法。有4中原因會導致線程進入就緒狀態:
\1. 新建線程:調用start()方法,進入就緒狀態;
\2. 阻塞線程:阻塞解除,進入就緒狀態;
\3. 運行線程:調用yield()方法,直接進入就緒狀態;
\4. 運行線程:JVM將CPU資源從本線程切換到其他線程。
▪ 運行狀態(Running)
在運行狀態的線程執行自己run方法中的代碼,直到調用其他方法而終止或等待某資源而阻塞或完成任務而死亡。如果在給定的時間片內沒有執行結束,就會被系統給換下來回到就緒狀態。也可能由於某些“導致阻塞的事件”而進入阻塞狀態。
▪ 阻塞狀態(Blocked)
阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒)。有4種原因會導致阻塞:
\1. 執行sleep(int millsecond)方法,使當前線程休眠,進入阻塞狀態。當指定的時間到了後,線程進入就緒狀態。
\2. 執行wait()方法,使當前線程進入阻塞狀態。當使用nofity()方法喚醒這個線程後,它進入就緒狀態。
\3. 線程運行時,某個操作進入阻塞狀態,比如執行IO流操作(read()/write()方法本身就是阻塞的方法)。只有當引起該操作阻塞的原因消失後,線程進入就緒狀態。
\4. join()線程聯合: 當某個線程等待另一個線程執行結束後,才能繼續執行時,使用join()方法。
▪ 死亡狀態(Terminated)
死亡狀態是線程生命周期中的最後一個階段。線程死亡的原因有兩個。一個是正常運行的線程完成了它run()方法內的全部工作; 另一個是線程被強制終止,如通過執行stop()或destroy()方法來終止一個線程(註:stop()/destroy()方法已經被JDK廢棄,不推薦使用)。
當一個線程進入死亡狀態以後,就不能再回到其它狀態了。
終止線程我們一般不使用JDK提供的stop()/destroy()方法(它們本身也被JDK廢棄了)。通常的做法是提供一個boolean型的終止變數,當這個變數置為false,則終止線程的運行。
終止線程的典型方法(重要)
public class TestThreadCiycle implements Runnable {
String name;
boolean live = true;// 標記變數,表示線程是否可中止;
public TestThreadCiycle(String name) {
super();
this.name = name;
}
public void run() {
int i = 0;
//當live的值是true時,繼續線程體;false則結束迴圈,繼而終止線程體;
while (live) {
System.out.println(name + (i++));
}
}
public void terminate() {
live = false;
}
public static void main(String[] args) {
TestThreadCiycle ttc = new TestThreadCiycle("線程A:");
Thread t1 = new Thread(ttc);// 新生狀態
t1.start();// 就緒狀態
for (int i = 0; i < 100; i++) {
System.out.println("主線程" + i);
}
ttc.terminate();
System.out.println("ttc stop!");
}
}
執行結果如圖所示:
運行效果圖(因為是多線程,故每次運行結果不一定一致)
暫停線程執行sleep/yield
暫停線程執行常用的方法有sleep()和yield()方法,這兩個方法的區別是:
sleep()方法:可以讓正在運行的線程進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。
yield()方法:可以讓正在運行的線程直接進入就緒狀態,讓出CPU的使用權。
暫停線程的方法-sleep()
public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用繼承方式實現多線程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(2000);//調用線程的sleep()方法;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行結果如下所示(註:以下圖示只是部分結果,運行時可以感受到每條結果輸出之前的延遲,是Thread.sleep(2000)語句在起作用):
圖11-6示例11-4運行效果圖
暫停線程的方法-yield()
public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用繼承方式實現多線程
class StateThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
Thread.yield();//調用線程的yield()方法;
}
}
}
執行結果如圖所示(註:以下圖示只是部分結果,可以引起線程切換,但運行時沒有明顯延遲):
運行效果圖
線程的聯合join()
線程A在運行期間,可以調用線程B的join()方法,讓線程B和線程A聯合。這樣,線程A就必須等待線程B執行完畢後,才能繼續執行。如下麵示例中,“爸爸線程”要抽煙,於是聯合了“兒子線程”去買煙,必須等待“兒子線程”買煙完畢,“爸爸線程”才能繼續抽煙。
public class TestThreadState {
public static void main(String[] args) {
System.out.println("爸爸和兒子買煙故事");
Thread father = new Thread(new FatherThread());
father.start();
}
}
class FatherThread implements Runnable {
public void run() {
System.out.println("爸爸想抽煙,發現煙抽完了");
System.out.println("爸爸讓兒子去買包紅塔山");
Thread son = new Thread(new SonThread());
son.start();
System.out.println("爸爸等兒子買煙回來");
try {
son.join();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("爸爸出門去找兒子跑哪去了");
// 結束JVM。如果是0則表示正常結束;如果是非0則表示非正常結束
System.exit(1);
}
System.out.println("爸爸高興的接過煙開始抽,並把零錢給了兒子");
}
}
class SonThread implements Runnable {
public void run() {
System.out.println("兒子出門去買煙");
System.out.println("兒子買煙需要10分鐘");
try {
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "分鐘");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("兒子買煙回來了");
}
}
線程的優先順序
處於就緒狀態的線程,會進入“就緒隊列”等待JVM來挑選。
線程的優先順序用數字表示,範圍從1到10,一個線程的預設優先順序是5。
使用下列方法獲得或設置線程對象的優先順序。
int getPriority();
void setPriority(int newPriority);
註意:優先順序低只是意味著獲得調度的概率低。並不是絕對先調用優先順序高的線程後調用優先順序低的線程。
什麼是線程同步
▪ 同步問題的提出
現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題。 比如:教室里,只有一臺電腦,多個人都想使用。天然的解決辦法就是,在電腦旁邊,大家排隊。前一人使用完後,後一人再使用。
▪ 線程同步的概念
處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改這個對象。 這時候,我們就需要用到“線程同步”。 線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程再使用。
由於同一進程的多個線程共用同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問造成的這種問題。
由於我們可以通過 private 關鍵字來保證數據對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。
▪ synchronized 方法
通過在方法聲明中加入 synchronized關鍵字來聲明,語法如下:
public synchronized void accessVal(int newVal);
synchronized 方法控制對“對象的類成員變數”的訪問:每個對象對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的對象的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。
▪ synchronized塊
synchronized 方法的缺陷:若將一個大的方法聲明為synchronized 將會大大影響效率。
Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。 塊可以讓我們精確地控制到具體的“成員變數”,縮小同步的範圍,提高效率。
synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊,語法如下:
synchronized(syncObject)
{
//允許訪問控制的代碼
}
多線程操作同一個對象(使用線程同步)
public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100, "高");
Drawing draw1 = new Drawing(80, a1);
Drawing draw2 = new Drawing(80, a1);
draw1.start(); // 你取錢
draw2.start(); // 你老婆取錢
}
}
/*
* 簡單表示銀行賬戶
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模擬提款操作
*
* @author Administrator
*
*/
class Drawing extends Thread {
int drawingNum; // 取多少錢
Account account; // 要取錢的賬戶
int expenseTotal; // 總共取的錢數
public Drawing(int drawingNum, Account account) {
super();
this.drawingNum = drawingNum;
this.account = account;
}
@Override
public void run() {
draw();
}
void draw() {
synchronized (account) {
if (account.money - drawingNum < 0) {
System.out.println(this.getName() + "取款,餘額不足!");
return;
}
try {
Thread.sleep(1000); // 判斷完後阻塞。其他線程開始運行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingNum;
expenseTotal += drawingNum;
}
System.out.println(this.getName() + "--賬戶餘額:" + account.money);
System.out.println(this.getName() + "--總共取了:" + expenseTotal);
}
}
synchronized (account)” 意味著線程需要獲得account對象的“鎖”才有資格運行同步塊中的代碼。 Account對象的“鎖”也稱為“互斥鎖”,在同一時刻只能被一個線程使用。A線程擁有鎖,則可以調用“同步塊”中的代碼;B線程沒有鎖,則進入account對象的“鎖池隊列”等待,直到A線程使用完畢釋放了account對象的鎖,B線程得到鎖才可以開始調用“同步塊”中的代碼。