首頁 > 軟體

Java 天生就是多執行緒

2022-07-01 18:00:59

一、Java 中的執行緒

一個Java 程式從main() 方法開始執行,然後按照既定的程式碼邏輯執行,看似沒有其他執行緒參與,但實際上Java

程式天生就是多執行緒程式,因為執行main() 方法的是一個名稱為main 的執行緒。

public static void main(String[] args) {

// java 虛擬機器器執行緒系統的管理介面
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要獲取同步的monitor 和synchronizer 資訊,僅僅獲取執行緒和執行緒堆疊資訊
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍歷執行緒,僅列印執行緒ID 和執行緒名稱資訊
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("執行緒ID" + threadInfo.getThreadId() + "執行緒名" + threadInfo.getThreadName());
}
}

上面程式碼輸出的結果:

  • $textcolor{red}{Monitor Ctrl-Break}$ 監控 Ctrl-Break 中斷訊號的
  • $textcolor{red}{Signal Dispatcher}$ 分發處理傳送給 JVM 訊號的執行緒
  • $textcolor{red}{ Finalizer }$ 呼叫物件 finalize 方法的執行緒
  • $textcolor{red}{Reference Handler}$ 清除 Reference 的執行緒
  • $textcolor{red}{ main }$ main 執行緒,使用者程式入口

從上面的例子中,我們能發現,在Java中短短的幾行程式碼,就給我們啟動了5個執行緒,當然,不同的版本,啟動的執行緒數量也不一樣,由此我們可以得出:**Java

天生就是多執行緒的**

1、啟動

執行緒的啟動方式有兩種(原始碼中的註釋是這麼寫的)參見程式碼:cn.enjoyedu.ch1.base.NewThread:

  • X extends Thread;,然後 X.start
  • X implements Runnable;然後交給 Thread 執行

範例程式碼:(派生自Thread 類,來實現我們的兩種執行緒啟動方式)

/**
* 擴充套件自Thread 類
*/
private static class UserThread extends Thread{
@Override
public void run() {
System.out.println("UserThread.run");
}
}
/**
* 擴充套件自 Runnable 類
*/
private static class UserRunnable implements Runnable {

@Override
public void run() {
System.out.println("UserRunnable.run");
}
}
public static void main(String[] args) {
UserThread userThread = new UserThread();
userThread.start();
UserRunnable userRunnable = new UserRunnable();
new Thread(userRunnable).start();
}

Thread 和 Runnable 的區別:

  • Thread 是Java 裡對執行緒的唯一抽象。
  • Runnable是Java對任務(業務邏輯)的抽象。
  • Thread可以接受任意一個Runnable的範例並執行。

2、中止

  • 執行緒自然終止:要麼是run 執行完成了,要麼是丟擲了一個未處理的異常導致執行緒提前結束。
  • stop:暫停、恢復和停止操作對應線上程ThreadAPI就是suspend()、resume() 和 stop()。但是這些API都是過期的,不再建議使用。不建議使用的主要原因有:以suspend()方法為例,在呼叫後,執行緒不會釋放已佔有的資源(比如鎖),而是佔有資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop() 方法在終結一個執行緒時,不會保證執行緒的資源正常釋放,通常是沒有給予執行緒完成資源釋放的機會,因此會到導致程式可能工作在不確定的狀態下。整因為suspend()、resume() 和 stop()方法帶來的副作用,這些方法才會被標註為不建議使用的過期方法中。
  • 中斷:安全的中止則是其它執行緒通過呼叫執行緒A的interrupt()方法對其進行中止操作,中斷代表著其它執行緒對A執行緒打了個招呼,“A, 你要中斷了”,不代表執行緒A 會立即停止自己的工作,同樣A執行緒可以不理會這種請求。因為Java 中的執行緒是共同作業式的,不是搶佔式。執行緒通過檢查自身的中斷標誌位是否被置為true來進行響應。
private static class UserThread extends Thread{
public UserThread(String name){
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "interrupt flag = " + isInterrupted());
while (!isInterrupted()){
// while (!Thread.interrupted()){
// while (true){
System.out.println(threadName+ "is running");
System.out.println(threadName+ "inner interrupt flag = "+ isInterrupted());
}
System.out.println(threadName+ "interrupt flag = " + isInterrupted());
}
}
public static void main(String[] args) {
Thread endTread = new UserThread("endTread");
endTread.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中斷執行緒, 其實設定執行緒的標識位
endTread.interrupt();
}

執行上面的程式碼:

我們發現,在使用isInterrupted()進行執行緒中斷的之後,isInterrupted()會返回一個true。

我們一起來看一下isInterrupted() 方法的原始碼:

我們再使用一下靜態的 interrupted()方法,他返回的也是一個bool 值,

先看一下這個方法的原始碼:

從原始碼中我們發現,它返回也是中斷識別符號,但是,它把中斷識別符號給重新賦值成了true。

我們來看一下執行效果:

由此我們可以總結出:**執行緒通過方法 isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法 Thread.interrupted()來進行判斷當前執行緒是否被中斷,不過 Thread.interrupted()會同時將中斷標識位改寫為 false。**

3、阻塞

如果一個執行緒處於阻塞狀態(如執行緒呼叫了 thread.sleep、thread.join、 thread.wait 等),則執行緒在檢查中斷標識時,如果發現中斷標識位true,則會在這些阻塞方法呼叫處丟擲InterruptedException 異常,並且在丟擲異常後會立即將執行緒的中斷標識位清除,即重新設定為true

private static class UserThread extends Thread{
public UserThread(String name){
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "interrupt flag = " + isInterrupted());
while (!isInterrupted()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(threadName+ "inner interrupt flag = "+ isInterrupted());
e.printStackTrace();
}
System.out.println(threadName+ "is running");
}
System.out.println(threadName+ "interrupt flag = " + isInterrupted());
}
}
public static void main(String[] args) {
Thread endTread = new UserThread("endTread");
endTread.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中斷執行緒, 其實設定執行緒的標識位
endTread.interrupt();
}

上面程式碼執行結果:

那麼像這種,我們該怎麼去中中斷操作呢?只需要在??catch?? 中呼叫??interrupt()?? 方法就可以了

程式碼執行結果:

4、深入理解run 和 start

  • Thread類是Java裡對執行緒概念的抽象,可以這樣理解:我們通過??new Thread()?? 其實只是new出一個thread的範例,還沒有和作業系統中真正的執行緒掛起勾來。只有執行了??start()?? 方法後,才實現了真正意義上的啟動執行緒。
  • ??start()?? 方法讓一個執行緒進入就緒佇列等待分配CPU,分到CPU後才呼叫??run()??方法,??start()?? 方法不能重複呼叫,如果重複呼叫,就會丟擲異常。
  • run() 方法是業務邏輯實現的地方,本質上和任意一個類的任意一個成員方法並沒有任何區別,可以重複執行,也可以單獨呼叫。

那麼start() 和 run() 有什麼區別呢?請看程式碼:

private static class UserThread extends Thread{
@Override
public void run() {
int i = 90;
while (i > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am "+Thread.currentThread().getName()+"and now the i="+ i--);
}
}
}
public static void main(String[] args) {
Thread endTread = new UserThread();
endTread.setName("threadRun");
endTread.start();
}

程式碼執行結果:(觀察執行結果,我們可以得出,呼叫start() 方法的時候,執行start() 方法的是子執行緒)

我們修改一下程式碼,呼叫??run()?? 方法

public static void main(String[] args) {
Thread endTread = new UserThread();
endTread.setName("threadRun");
endTread.run();

}

檢視執行結果:(觀察執行結果,我們可以得出,呼叫run() 方法的時候,執行run() 方法的是主執行緒)

5、join 方法

join() 方法是把指定的執行緒加入到當前執行緒,可以將兩個交替執行的執行緒合併為順序執行。

static class Students implements Runnable {
private Thread thread;
public Students(Thread thread) {
this.thread = thread;
}
public Students() {
}
@Override
public void run() {
System.out.println("學生開始排隊打飯。。。。。");
try {
if (thread != null) thread.join();
// 休眠2 秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("學生打飯完成");
}
}
static class Teacher implements Runnable {
@Override
public void run() {
try {
// 休眠2 秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("老師開始打飯、、、、、");
System.out.println(Thread.currentThread().getName() + "老師打飯完成。");
}
}
public static void main(String[] args) throws InterruptedException {
Teacher teacher = new Teacher();
Thread teaThread = new Thread(teacher);
Students students = new Students(teaThread);
Thread stuThread = new Thread(students);
stuThread.start();
teaThread.start();
System.out.println("我開始打飯、、、、、");
stuThread.join();
// 讓主執行緒休眠2 秒
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"我打飯完成");
}

程式碼執行結果如下:

由上程式碼執行結果,我們可以得出:**線上程B中呼叫了執行緒A的??join()?? 方法,只到執行緒A 執行完畢後,才會繼續執行執行緒B的。**

6、執行緒優先順序

在 Java 執行緒中,通過一個整型成員變數 priority 來控制優先順序,優先順序的範 圍從 1~10,線上程構建的時候可以通過??setPriority(int)??方法來修改優先順序,預設 優先順序是 5,優先順序高的執行緒分配時間片的數量要多於優先順序低的執行緒。

設定執行緒優先順序時,針對頻繁阻塞(休眠或者I/O操作)的執行緒需要設定較 高優先順序,而偏重計算(需要較多CPU時間或者偏運算)的執行緒則設定較低的 優先順序,確保處理器不會被獨佔。在不同的 JVM 以及作業系統上,執行緒規劃會 存在差異,有些作業系統甚至會忽略對執行緒優先順序的設定。

7、守護執行緒

Daemon(守護)執行緒是一種支援型執行緒,因為它主要被用作程式中後臺調 度以及支援性工作。這意味著,當一個 Java 虛擬機器器中不存在非 Daemon 執行緒的 時候,Java 虛擬機器器將會退出。可以通過呼叫??Thread.setDaemon(true)??將執行緒設定 為Daemon執行緒。我們一般用不上,比如垃圾回收執行緒就是Daemon執行緒。

Daemon執行緒被用作完成支援性工作,但是在 Java 虛擬機器器退出時Daemon線 程中的??finally?? 塊並不一定會執行。在構建Daemon執行緒時,不能依靠??finally?? 塊中 的內容來確保執行關閉或清理資源的邏輯。

8、synchronized 內建鎖

執行緒開始執行,擁有自己的棧空間,就如同一個指令碼一樣,按照既定的程式碼 一步一步地執行,直到終止。但是,每個執行中的執行緒,如果僅僅是孤立地執行, 那麼沒有一點兒價值,或者說價值很少,如果多個執行緒能夠相互配合完成工作, 包括資料之間的共用,協同處理事情。這將會帶來巨大的價值。

Java支援多個執行緒同時存取一個物件或者物件的成員變數,關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線 程在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數 存取的可見性和排他性,又稱為內建鎖機制。

下面我們看一段程式碼,在main 方法中啟動兩個執行緒,每個執行緒的count 值都是10000,兩個執行緒的和應該就是20000。

public class OnlyMain {
private long count = 0;
private Object object = new Object();
public long getCount() {
return count;
}
public void incCount() {
count++;
}
// 執行緒
private static class Count extends Thread {
private OnlyMain onlyMain;

public Count(OnlyMain onlyMain) {
this.onlyMain = onlyMain;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// count = count++ = 10000
onlyMain.incCount();
}
}
}
public static void main(String[] args) throws InterruptedException {
OnlyMain onlyMain = new OnlyMain();
// 啟動兩個執行緒
Count count1 = new Count(onlyMain);
Count count2 = new Count(onlyMain);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(onlyMain.count);
}
}

程式碼的執行結果是:

經過多次執行,每次執行的結果都不一樣,只有在很少很少的機率的情況下,才會出現正確的20000結果值,這是為什麼呢?

這是因為,兩個執行緒同時對count 成員變數進行存取,才導致輸出結果的錯誤。怎麼解決呢?使用synchronized 內建鎖。

修改上面程式碼中的incCount() 方法,新增一個內鎖:

public synchronized void incCount() {
count++;
}

這樣我們就能保證每次執行的正確結果了:

9、物件鎖和類鎖

物件鎖是用於物件實體方法的鎖,或者一個物件範例上,類鎖 是用於類的靜態方法或一個類的class 上的,我們知道,類的物件範例可以有很多個,但是每個類只有一個class物件,所有不同物件範例的物件鎖是互不干擾的,但是每個類只有一個類鎖。

但是有一點必須要注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的class 物件。類鎖和物件鎖之間也是互不干擾的。

二、總結

到此這篇關於Java 天生就是多執行緒的文章就介紹到這了,更多相關Java 多執行緒內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com