1. 程式人生 > >Java多執行緒學習(一)Java多執行緒入門

Java多執行緒學習(一)Java多執行緒入門

Java 併發的基礎知識,可能會在筆試中遇到,技術面試中也可能以併發知識環節提問的第一個問題出現。比如面試官可能會問你:“談談自己對於程序和執行緒的理解,兩者的區別是什麼?”

一 程序和多執行緒簡介

1.1 程序和執行緒

程序和執行緒的對比這一知識點由於過於基礎,所以在面試中很少碰到,但是極有可能會在筆試題中碰到。常見的提問形式是這樣的:“什麼是執行緒和程序?,請簡要描述執行緒與程序的關係、區別及優缺點? ”。

1.2 何為程序?

程序是程式的一次執行過程,是系統執行程式的基本單位,因此程序是動態的。系統執行一個程式即是一個程序從建立,執行到消亡的過程。如下圖所示,在 windows 中通過檢視工作管理員的方式,我們就可以清楚看到 window 當前執行的執行緒(.exe檔案的執行)。

程序

1.3 何為執行緒?

執行緒與程序相似,但執行緒是一個比程序更小的執行單位。一個程序在其執行的過程中可以產生多個執行緒。與程序不同的是同類的多個執行緒共享同一塊記憶體空間和一組系統資源,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程序小得多,也正因為如此,執行緒也被稱為輕量級程序。

1.4 何為多執行緒?

多執行緒就是多個執行緒同時執行或交替執行。單核CPU的話是順序執行,也就是交替執行。多核CPU的話,因為每個CPU有自己的運算器,所以在多個CPU中可以同時執行。

1.5 為什麼多執行緒是必要的?

個人覺得可以用一句話概括:開發高併發系統的基礎,利用好多執行緒機制可以大大提高系統整體的併發能力以及效能。

1.6 為什麼提倡多執行緒而不是多程序?

執行緒就是輕量級程序,是程式執行的最小單位。使用多執行緒而不是用多程序去進行併發程式的設計,是因為執行緒間的切換和排程的成本遠遠小於程序。

二 幾個重要的概念

2.1 同步和非同步

同步和非同步通常用來形容一次方法呼叫。同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者可以繼續後續的操作。

關於非同步目前比較經典以及常用的實現方式就是訊息佇列:在不使用訊息佇列伺服器的時候,使用者的請求資料直接寫入資料庫,在高併發的情況下資料庫壓力劇增,使得響應速度變慢。但是在使用訊息佇列之後,使用者的請求資料傳送給訊息佇列之後立即 返回,再由訊息佇列的消費者程序從訊息佇列中獲取資料,非同步寫入資料庫。由於訊息佇列伺服器處理速度快於資料庫(訊息佇列也比資料庫有更好的伸縮性),因此響應速度得到大幅改善。

2.2 併發(Concurrency)和並行(Parallelism)

併發和並行是兩個非常容易被混淆的概念。它們都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是序列的。而並行是真正意義上的“同時執行”。

多執行緒在單核CPU的話是順序執行,也就是交替執行(併發)。多核CPU的話,因為每個CPU有自己的運算器,所以在多個CPU中可以同時執行(並行)。

2.3 高併發

高併發(High Concurrency)是網際網路分散式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。

高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發使用者數等。

2.4 臨界區

臨界區用來表示一種公共資源或者說是共享資料,可以被多個執行緒使用。但是每一次,只能有一個執行緒使用它,一旦臨界區資源被佔用,其他執行緒要想使用這個資源,就必須等待。在並行程式中,臨界區資源是保護的物件。

2.5 阻塞和非阻塞

非阻塞指在不能立刻得到結果之前,該函式不會阻塞當前執行緒,而會立刻返回,而阻塞與之相反。

三 使用多執行緒常見的三種方式

前兩種實際上很少使用,一般都是用執行緒池的方式比較多一點。

①繼承Thread類

public class MyThread extends Thread {
	@Override
	public void run() {
		super.run();
		System.out.println("MyThread");
	}
}

Run.java

public class Run {

	public static void main(String[] args) {
		MyThread mythread = new MyThread();
		mythread.start();
		System.out.println("執行結束");
	}

}

執行結果:
結果

從上面的執行結果可以看出:執行緒是一個子任務,CPU以不確定的方式,或者說是以隨機的時間來呼叫執行緒中的run方法。

②實現Runnable介面

推薦實現Runnable介面方式開發多執行緒,因為Java單繼承但是可以實現多個介面。

MyRunnable.java

public class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println("MyRunnable");
	}
}

Run.java

public class Run {

	public static void main(String[] args) {
		Runnable runnable=new MyRunnable();
		Thread thread=new Thread(runnable);
		thread.start();
		System.out.println("執行結束!");
	}

}

執行結果:
執行結果

③使用執行緒池

使用執行緒池的方式也是最推薦的一種方式,另外,《阿里巴巴Java開發手冊》在第一章第六節併發處理這一部分也強調到“執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示建立執行緒”。這裡就不給大家演示程式碼了,執行緒池這一節會詳細介紹到這部分內容。

四 例項變數和執行緒安全

執行緒類中的例項變數針對其他執行緒可以有共享和不共享之分。下面通過兩個簡單的例子來說明!

4.1 不共享資料的情況


/**
 * 
 * @author SnailClimb
 * @date 2018年10月30日
 * @Description: 多個執行緒之間不共享變數執行緒安全的情況
 */
public class MyThread extends Thread {

	private int count = 5;

	public MyThread(String name) {
		super();
		this.setName(name);
	}

	@Override
	public void run() {
		super.run();
		while (count > 0) {
			count--;
			System.out.println("由 " + MyThread.currentThread().getName() + " 計算,count=" + count);
		}
	}

	public static void main(String[] args) {
		MyThread a = new MyThread("A");
		MyThread b = new MyThread("B");
		MyThread c = new MyThread("C");
		a.start();
		b.start();
		c.start();
	}
}

執行結果:
執行結果
可以看出每個執行緒都有一個屬於自己的例項變數count,它們之間互不影響。我們再來看看另一種情況。

4.2 共享資料的情況

/**
 * 
 * @author SnailClimb
 * @date 2018年10月30日
 * @Description: 多個執行緒之間共享變數執行緒不安全的情況
 */
public class SharedVariableThread extends Thread {
	private int count = 5;

	@Override
	public void run() {
		super.run();
		count--;
		System.out.println("由 " + SharedVariableThread.currentThread().getName() + " 計算,count=" + count);
	}

	public static void main(String[] args) {

		SharedVariableThread mythread = new SharedVariableThread();
		// 下列執行緒都是通過mythread物件建立的
		Thread a = new Thread(mythread, "A");
		Thread b = new Thread(mythread, "B");
		Thread c = new Thread(mythread, "C");
		Thread d = new Thread(mythread, "D");
		Thread e = new Thread(mythread, "E");
		a.start();
		b.start();
		c.start();
		d.start();
		e.start();
	}
}

執行結果:
執行結果
可以看出這裡已經出現了錯誤,我們想要的是依次遞減的結果。為什麼呢??

因為在大多數jvm中,count–的操作分為如下下三步:

  1. 取得原有count值
  2. 計算i -1
  3. 對i進行賦值

所以多個執行緒同時訪問時出現問題就是難以避免的了。

那麼有沒有什麼解決辦法呢?

答案是:當然有,而且很簡單。給大家提供兩種解決辦法:一種是利用 synchronized 關鍵字(保證任意時刻只能有一個執行緒執行該方法),一種是利用 AtomicInteger 類(JUC 中的 Atomic 原子類)。大家如果之前沒有接觸 Java 多執行緒的話,可能對這兩個概念不太熟悉,不過不要擔心我後面會一一向你介紹到!這裡不能用 volatile 關鍵字,因為 volatile 關鍵字不能保證複合操作的原子性。

五 一些常用方法

5.1 currentThread()

返回對當前正在執行的執行緒物件的引用。

5.2 getId()

返回此執行緒的識別符號

5.3 getName()

返回此執行緒的名稱

5.4 getPriority()

返回此執行緒的優先順序

5.5 isAlive()

測試這個執行緒是否還處於活動狀態。

什麼是活動狀態呢?

活動狀態就是執行緒已經啟動且尚未終止。執行緒處於正在執行或準備執行的狀態。

5.6 sleep(long millis)

使當前正在執行的執行緒以指定的毫秒數“休眠”(暫時停止執行),具體取決於系統定時器和排程程式的精度和準確性。

5.7 interrupt()

中斷這個執行緒。

5.8 interrupted() 和isInterrupted()

interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有將狀態標誌清除為false的功能

isInterrupted(): 測試執行緒Thread對相關是否已經是中斷狀態,但部清楚狀態標誌

5.9 setName(String name)

將此執行緒的名稱更改為等於引數 name 。

5.10 isDaemon()

測試這個執行緒是否是守護執行緒。

5.11 setDaemon(boolean on)

將此執行緒標記為 daemon執行緒或使用者執行緒。

5.12 join()

在很多情況下,主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是 主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了

join()的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行

5.13 yield()

yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU時間。注意:放棄的時間不確定,可能一會就會重新獲得CPU時間片。

5.14 setPriority(int newPriority)

更改此執行緒的優先順序

六 如何停止一個執行緒呢?

stop(),suspend(),resume()(僅用於與suspend()一起使用)這些方法已被棄用,所以我這裡不予講解。

6.1 使用interrupt()方法

我們上面提到了interrupt()方法,先來試一下interrupt()方法能不能停止執行緒

public class MyThread extends Thread {
	@Override
	public void run() {
		super.run();
		for (int i = 0; i < 5000000; i++) {
			System.out.println("i=" + (i + 1));
		}
	}
		public static void main(String[] args) {
		try {
			MyThread thread = new MyThread();
			thread.start();
			Thread.sleep(2000);
			thread.interrupt();
		} catch (InterruptedException e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
	}
}

執行上訴程式碼你會發現,執行緒並不會終止

針對上面程式碼的一個改進:

interrupted()方法判斷執行緒是否停止,如果是停止狀態則break


/**
 * 
 * @author SnailClimb
 * @date 2018年10月30日
 * @Description: 使用interrupt()方法終止執行緒
 */
public class InterruptThread2 extends Thread {
	@Override
	public void run() {
		super.run();
		for (int i = 0; i < 500000; i++) {
			if (this.interrupted()) {
				System.out.println("已經是停止狀態了!我要退出了!");
				break;
			}
			System.out.println("i=" + (i + 1));
		}
		System.out.println("看到這句話說明執行緒並未終止------");
	}

	public static void main(String[] args) {
		try {
			InterruptThread2 thread = new InterruptThread2();
			thread.start();
			Thread.sleep(2000);
			thread.interrupt();
		} catch (InterruptedException e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
	}
}

執行結果:
執行結果
for迴圈雖然停止執行了,但是for迴圈下面的語句還是會執行,說明執行緒並未被停止。

6.2 使用return停止執行緒

public class MyThread extends Thread {

	@Override
	public void run() {
			while (true) {
				if (this.isInterrupted()) {
					System.out.println("ֹͣ停止了!");
					return;
				}
				System.out.println("timer=" + System.currentTimeMillis());
			}
	}
	public static void main(String[] args) throws InterruptedException {
		MyThread t=new MyThread();
		t.start();
		Thread.sleep(2000);
		t.interrupt();
	}

}

執行結果:
執行結果
當然還有其他停止執行緒的方法,後面再做介紹。

七 執行緒的優先順序

每個執行緒都具有各自的優先順序,執行緒的優先順序可以在程式中表明該執行緒的重要性,如果有很多執行緒處於就緒狀態,系統會根據優先順序來決定首先使哪個執行緒進入執行狀態。但這個並不意味著低
優先順序的執行緒得不到執行,而只是它執行的機率比較小,如垃圾回收機制執行緒的優先順序就比較低。所以很多垃圾得不到及時的回收處理。

執行緒優先順序具有繼承特性比如A執行緒啟動B執行緒,則B執行緒的優先順序和A是一樣的。

執行緒優先順序具有隨機性也就是說執行緒優先順序高的不一定每一次都先執行完。

Thread類中包含的成員變數代表了執行緒的某些優先順序。如Thread.MIN_PRIORITY(常數1)Thread.NORM_PRIORITY(常數5),
Thread.MAX_PRIORITY(常數10)。其中每個執行緒的優先順序都在Thread.MIN_PRIORITY(常數1)Thread.MAX_PRIORITY(常數10) 之間,在預設情況下優先順序都是Thread.NORM_PRIORITY(常數5)

學過作業系統這門課程的話,我們可以發現多執行緒優先順序或多或少借鑑了作業系統對程序的管理。

執行緒優先順序具有繼承特性測試程式碼:

MyThread1.java:

public class MyThread1 extends Thread {
	@Override
	public void run() {
		System.out.println("MyThread1 run priority=" + this.getPriority());
		MyThread2 thread2 = new MyThread2();
		thread2.start();
	}
}

MyThread2.java:

public class MyThread2 extends Thread {
	@Override
	public void run() {
		System.out.println("MyThread2 run priority=" + this.getPriority());
	}
}

Run.java:

public class Run {
	public static void main(String[] args) {
		System.out.println("main thread begin priority="
				+ Thread.currentThread().getPriority());
		Thread.currentThread().setPriority(6);
		System.out.println("main thread end   priority="
				+ Thread.currentThread().getPriority());
		MyThread1 thread1 = new MyThread1();
		thread1.start();
	}
}

執行結果:
執行結果

八 Java多執行緒分類

8.1 多執行緒分類

使用者執行緒:執行在前臺,執行具體的任務,如程式的主執行緒、連線網路的子執行緒等都是使用者執行緒

守護執行緒:執行在後臺,為其他前臺執行緒服務.也可以說守護執行緒是JVM中非守護執行緒的 “傭人”

特點:一旦所有使用者執行緒都結束執行,守護執行緒會隨JVM一起結束工作

應用:資料庫連線池中的檢測執行緒,JVM虛擬機器啟動後的檢測執行緒

最常見的守護執行緒:垃圾回收執行緒

8.2 如何設定守護執行緒?

可以通過呼叫Thead類的setDaemon(true)方法設定當前的執行緒為守護執行緒

注意事項:

1.  setDaemon(true)必須在start()方法前執行,否則會丟擲IllegalThreadStateException異常
2. 在守護執行緒中產生的新執行緒也是守護執行緒
3. 不是所有的任務都可以分配給守護執行緒來執行,比如讀寫操作或者計算邏輯

MyThread.java:

public class MyThread extends Thread {
	private int i = 0;

	@Override
	public void run() {
		try {
			while (true) {
				i++;
				System.out.println("i=" + (i));
				Thread.sleep(100);
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

Run.java:

public class Run {
	public static void main(String[] args) {
		try {
			MyThread thread = new MyThread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我離開thread物件也不再列印了,也就是停止了!");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

執行結果:
守護執行緒