1. 程式人生 > 實用技巧 >多執行緒——基礎

多執行緒——基礎

1.程式、程序、執行緒

程式:用語言編寫的一組指令的集合,指一段靜態的程式碼

程序:程式的一次執行過程,或是正在執行的一個程式

執行緒:一個程式內部的一條執行路徑

一個程序可以有多個執行緒

2.實現多執行緒的方式

繼承Thread類

public class TestThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("我在看程式碼——" + i);
}
}

public static void main(String[] args) {
TestThread1 thread1 = new TestThread1();
thread1.start();

for (int i = 0; i < 1000; i++) {
System.out.println("我在學習——"+i);
}
}
}

Thread實現了Runnable介面

不建議使用:避免OOP單繼承侷限性

實現Runnable介面

package com.yl.demo;

public class TestThread2 implements Runnable{

private int id;

public TestThread2() {
}

public TestThread2(int id) {
this.id = id;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("我在看程式碼 "+id+" ——" + i);
}
}

public static void main(String[] args) {
TestThread2 t1 = new TestThread2(1);
TestThread2 t2 = new TestThread2(2);
new Thread(t1).start();
new Thread(t2).start();

for (int i = 0; i < 1000; i++) {
System.out.println("我在學習——" + i);
}
}
}

推薦使用:避免單繼承侷限性,靈活方便,方便同一個物件被多個執行緒使用

對比thread和runnable

相同點:都要重寫run方法

不同點:Thread不適合資源共享,Runnable適合,使用Runnable,可以避免java中的單繼承限制,程式碼可以被多個執行緒共享,程式碼和資料獨立;執行緒池只能彷彿實現Runnable或Callable類執行緒,不能直接放入繼承Thread的類

實現Callable介面

  • 需要返回值型別

  • 重寫call方法,需要丟擲異常

  • 建立目標物件

  • 建立執行服務

  • 提交執行

  • 獲取結果

  • 關閉服務

package com.yl.demo;

import java.util.concurrent.*;

public class TestThread3 implements Callable {
private int id;

public TestThread3(int id){
this.id = id;
}

@Override
public Boolean call(){
for (int i = 0; i < 200; i++) {
System.out.println("我在學習" + id + "——" + i);
}
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestThread3 t1 = new TestThread3(1);
TestThread3 t2 = new TestThread3(2);

//建立執行服務
ExecutorService ser = Executors.newFixedThreadPool(1);
//提交執行
Future f1 = ser.submit(t1);
Future f2 = ser.submit(t2);
//獲取結果
boolean rs1 = (boolean) f1.get();
boolean rs2 = (boolean) f2.get();
//關閉服務
ser.shutdownNow();
}
}

好處:可以定義返回值、可以丟擲異常

使用執行緒池

3.對比Thread和Runnable建立方式:

相同點:都要重寫run方法

不同點:Thread不適合資源共享,Runnable適合,使用Runnable,可以避免java中的單繼承限制,程式碼可以被多個執行緒共享,程式碼和資料獨立;執行緒池只能彷彿實現Runnable或Callable類執行緒,不能直接放入繼承Thread的類

4.執行緒狀態

初始(NEW):新建立了一個執行緒物件,但還沒有呼叫start()方法。

執行(RUNNABLE):Java執行緒中將就緒(ready)和執行中(running)兩種狀態籠統的稱為“執行”。 執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的執行緒在獲得CPU時間片後變為執行中狀態(running)。 阻塞(BLOCKED):表示執行緒阻塞於鎖。 等待(WAITING):進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。 終止(TERMINATED):表示該執行緒已經執行完畢。

這6種狀態定義在Thread類的State列舉中,可檢視原始碼進行一一對應。

5.執行緒休眠

  • sleep(時間)指定當前執行緒阻塞的毫秒數

  • sleep存在異常InterruptedException

  • sleep時間達到後執行緒進入就緒狀態

  • sleep可以模擬網路延時倒計時

  • 每一個物件都有鎖,執行緒不會釋放鎖

sleep()和wait()

sleep是執行緒類(Thread)的方法,導致此執行緒暫停執行指定時間,把執行機會給其他執行緒,但是監控狀態依然保持,到時後會自動恢復。呼叫sleep不會釋放物件鎖。 wait是Object類的方法,對此物件呼叫wait方法導致本執行緒放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件發出notify方法(或notifyAll)後本執行緒才進入物件鎖定池準備獲得物件鎖進入執行狀態。wait宣告在同步程式碼塊或同步方法中

6.執行緒禮讓

  • 讓當前正在執行的執行緒暫停,但不阻塞

  • 將執行緒從執行狀態轉為就緒狀態

  • 讓cpu重新排程,禮讓不一定成功,取決於cpu

7.執行緒停止

  • 建議執行緒正常停止——>利用次數,不建議死迴圈

  • 建議使用標誌位——>設定一個標誌位boolean

  • 不要用stop或destroy

8.Join

  • join合併執行緒,待此執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞

  • 可以想象成插隊

9.執行緒優先順序

  • 優先順序範圍1-10

  • 優先順序越高,分配的資源越多

  • setPriority(int xxx)

  • getPriority()

10.守護執行緒

  • 執行緒分為使用者執行緒和守護執行緒

  • 虛擬機器必須保證使用者執行緒執行完畢

  • 虛擬機器不用等待守護執行緒執行完畢

  • 如:後臺記錄操作日誌、監控記憶體、垃圾回收等待

Thread thread = new Thread(god);
thread.setDaemon(true); //預設是false

11.執行緒同步

多個執行緒操作同一個資源

併發:同一個物件被多個執行緒同時操作

加入鎖機制synchronized

關鍵字synchronized可以保證在同一時刻,只有一個執行緒可以執行某個方法或某個程式碼塊,同時synchronized可以保證一個執行緒的變化可見(可見性),即可以代替volatile。

synchronized方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行就獨佔該鎖,直到該方法返回才釋放鎖。

當一個執行緒獲得物件的排他鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可,存在一下問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起

  • 多執行緒競爭下,加鎖釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題

  • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題

Java中每一個物件都可以作為鎖,這是synchronized實現同步的基礎:

  • 方法裡需要修改的內容才需要鎖,鎖太多會浪費資源

  1. 普通同步方法(例項方法),鎖是當前例項物件this ,進入同步程式碼前要獲得當前例項的鎖

  2. 靜態同步方法,鎖是當前類的class物件 ,進入同步程式碼前要獲得當前類物件的鎖

  3. 同步方法塊,鎖是括號裡面的物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

  • 同步方法仍然涉及到同步監視器(鎖),只是不需要我們顯示的宣告

synchronized(同步監視器) {

需要同步執行的程式碼片段

}

同步監視器是java中任意的一個物件,只要保證多個執行緒看到的該物件是”同一個“,即可保證同步塊中的程式碼是併發安全的

可重入實現:

每個鎖關聯一個執行緒持有者和一個計數器。當計數器為0時表示該鎖沒有被任何執行緒持有,那麼任何執行緒都都可能獲得該鎖而呼叫相應方法。當一個執行緒請求成功後,JVM會記下持有鎖的執行緒,並將計數器計為1。此時其他執行緒請求該鎖,則必須等待。而該持有鎖的執行緒如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。當執行緒退出一個synchronized方法/塊時,計數器會遞減,如果計數器為0則釋放該鎖。

Lock

  • 通過顯式定義同步鎖物件來實現同步,同步鎖使用Lock物件充當

  • ReentranLock(可重入鎖)實現了Lock,可以顯示加鎖、釋放鎖

synchronized和Lock的對比

  • Lock是顯示鎖,synchronized是隱式鎖,出了作用域自動釋放

  • Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖

  • 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好,具有更好的擴充套件性(提供更多的子類)

  • 優先使用順序:

    • Lock>同步程式碼塊(已經進入了方法體,分配了相應資源)>同步方法(方法體之外)

  • Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;

  • synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  • Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

執行緒池

提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中,可以避免頻繁建立銷燬、實現重複利用

好處:

  • 提高響應速度

  • 降低資源消耗

  • 便於執行緒管理:

    • corePoolSize:核心池的大小

    • maxPoolSize:最大執行緒數

    • keepAliveTime:執行緒沒有任務時最多保持多長時間後終止

執行緒池相關API: ExecutorService和Executors

  • ExecutorService:真正的執行緒池介面,常見子類ThreadPoolExecutor

  • Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池

package com.yl.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPool {
public static void main(String[] args) {
//1.建立服務,建立執行緒池
//newFixedThreadPool:引數為執行緒池大小
ExecutorService service = Executors.newFixedThreadPool(10);

//2.執行
service.execute(new myThread());
service.execute(new myThread());
service.execute(new myThread());

//3.關閉連線
service.shutdown();
}

}

class myThread implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}