1. 程式人生 > >Java多執行緒學習筆記2

Java多執行緒學習筆記2

本文是我學習Java多執行緒以及高併發知識的第一本書的學習筆記, 書名是<<Java多執行緒程式設計核心技術>>,作者是大佬企業高階專案經理 高洪巖前輩,在此向他致敬。我將配合開發文件以及本書和其他的部落格 奉獻著的文章來學習,同時做一些簡單的總結。有些基礎的東西我就不 細分析了,建議以前接觸過其他語言多執行緒或者沒有系統學習過多執行緒 的開發者來看。另外需要注意的是,部落格中給出的一些英文文件我就不 翻譯了,要不太浪費時間了,這個是我想提高自己的英文閱讀水平和 文件檢視能力,想要積攢內功的人可以用有谷歌翻譯自己看文件細讀。 (中文文件建議只參考,畢竟你懂得...) 詳細程式碼見:https://github.com/youaresherlock/multithreadingforjavanotes

本節內容:

資料同步、非執行緒安全情況以及處理方法、Java記憶體模型、Thread.currentThread()抽象方法使用

  • 例項變數與執行緒安全

自定義執行緒類中的例項變數針對其他執行緒可以有共享和不共享之分,這在多個 執行緒之間互動時是很重要的一個技術點。

資料不共享情況:

package chapter01.section02.thread_1_2_3.project_1_t3;

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("由 " + this.currentThread().getName() 
					+ " 計算, count=" + count);
		}
	}
}

package chapter01.section02.thread_1_2_3.project_1_t3;

public class Run {
	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();
	}
}

/*
result:
由 C 計算, count=4
由 A 計算, count=4
由 A 計算, count=3
由 A 計算, count=2
由 A 計算, count=1
由 A 計算, count=0
由 B 計算, count=4
由 C 計算, count=3
由 B 計算, count=3
由 B 計算, count=2
由 C 計算, count=2
由 B 計算, count=1
由 C 計算, count=1
由 B 計算, count=0
由 C 計算, count=0
 */

可以看到一共建立了三個執行緒,每個執行緒都有各自的count變數,自己減少自己的 count變數的值,這樣的情況就是變數不共享,這裡不存在多個執行緒訪問同一個例項 變數的情況

共享資料的情況 共享資料的情況就是多個執行緒可以訪問同一個變數

package chapter01.section02.thread_1_2_3.project_2_t4;

public class MyThread extends Thread {
	private int count = 5;
	
	@Override
	public void run() {
		super.run();
			count--;
			System.out.println("由 " + this.currentThread().getName() 
					+ " 計算, count=" + count);
	}
}


package chapter01.section02.thread_1_2_3.project_2_t4;

public class Run {
	public static void main(String args[]) {
		MyThread myThread = new 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();
	}
}

/*
result:
由 A 計算, count=3
由 B 計算, count=3
由 D 計算, count=2
由 C 計算, count=2
由 E 計算, count=1
*/

可以看到AB以及DC打印出來的值相同,說明AB、DC同時對count進行處理,產生了"非執行緒 安全"問題。我們想要的是5 4 3 2 1這樣遞減的形式

  • 非執行緒安全: 

是指多個執行緒對同一個物件中的同一個例項變數進行操作時會出現值被更改、值不同步 的情況,進而影響程式的執行流程

這裡就要談到一點Java記憶體模型的東西了

  • 主記憶體和工作記憶體

Java記憶體模型規定了所有變數都儲存在主記憶體中(此處主記憶體與物理計算機的主記憶體名字一樣 ,可以類比,但此處僅是虛擬機器記憶體的一部分),這裡的變數包括例項欄位,靜態欄位和構成 陣列物件的元素,但不包括區域性變數和方法引數,因為後者是執行緒私用的。每個執行緒還有自己 的工作記憶體(可與處理器的快取記憶體類比),執行緒的工作記憶體中儲存了被該執行緒使用到的變數 的主記憶體副本拷貝,執行緒對變數的所有操作(讀寫等)都必須在工作記憶體中,不能直接讀寫主 記憶體中的變數。不同的執行緒之間也不能直接訪問對方工作記憶體中的變數,執行緒間的變數值的傳遞 必須通過主記憶體來完成。 

java記憶體模型是圍繞著如何處理原子性,可見性,有序性來建立的。

原子性:     原子性指多個操作的組合要麼一起執行完,要麼全部不執行,這個很好理解,一個執行緒 在執行一組操作的中途,不能被另一個執行緒插一腳,不然會造成資料錯誤,最經典就是 a++;操 作,++ 操作符不是原子的,所以需要使用同步工具保證其原子性。 可見性:     根據java記憶體模型的結構,各個執行緒都會從主記憶體備份一個變數的工作記憶體放在自己的 工作記憶體作為快取,可以提高效率,這樣就造成了可見性問題,即一個執行緒修改了一個數據,如 果一沒有立即同步回主記憶體,二沒有讓其他使用這個資料的執行緒及時從主記憶體同步,則其他執行緒 的資料是錯誤的。 有序性:     編譯器和處理器為了獲得更高的效率,會對指令進行重排序,實際生成的位元組碼指令順 序或者處理器指令順序並非是程式原始碼中的順序,這個在單執行緒的情況下問題不大,因為編譯 器和處理器會保證結果正確,但是多執行緒的環境下,因為執行緒之間很多時候需要協調,如果指令 進行重排,會影響協調結果錯亂。

在某些JVM中,i--的操作要分成如下3步: 1) 取得原有i值 2) 計算 i - 1 3) 對i進行賦值 因此這是出現數據錯誤的原因,如果有多個執行緒同時訪問,那麼一定會出現非執行緒安全問題。

我們使多個執行緒之間進行同步,按照順序排隊的方式進行減1操作:

package chapter01.section02.thread_1_2_3.project_2_t4;

public class MyThread extends Thread {
	private int count = 5;
	
//	@Override
//	public void run() {
//		super.run();
//			count--;
//			System.out.println("由 " + this.currentThread().getName() 
//					+ " 計算, count=" + count);
//	}
	
	@Override
	synchronized public void run() {
		super.run();
			count--;
			System.out.println("由 " + this.currentThread().getName() 
					+ " 計算, count=" + count);
	}
}


package chapter01.section02.thread_1_2_3.project_2_t4;

public class Run {
	public static void main(String args[]) {
		MyThread myThread = new 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();
	}
}

/*
新增同步操作之後的結果:
由 A 計算, count=4
由 E 計算, count=3
由 D 計算, count=2
由 C 計算, count=1
由 B 計算, count=0
*/

通過在run方法前加入sychronized關鍵字,使多個執行緒在執行run方法時,以排隊的方式進行處理 。當一個執行緒呼叫run前,先判斷run方法有沒有被上鎖,如果上鎖,說明有其他執行緒正在呼叫run方 法,必須等其他執行緒對run方法呼叫結束後才可以執行run方法。synchronized可以在任意物件及方 法上加鎖,而加鎖的這段程式碼成為"互斥區"或"臨界區"

舉一個"非執行緒安全的環境"案例,並且解決它

ALogin.java
package chapter01.section02.thread_1_2_3.project_2_t4.project_3_t4threadsafe;

public class ALogin extends Thread {

	@Override
	public void run() {
		LoginServlet.doPost("a", "aa");
	}
}


BLogin.java
package chapter01.section02.thread_1_2_3.project_2_t4.project_3_t4threadsafe;

public class BLogin extends Thread {
	
	@Override
	public void run() {
		LoginServlet.doPost("b", "bb");
	}
}


LoginServlet.java
package chapter01.section02.thread_1_2_3.project_2_t4.project_3_t4threadsafe;

public class LoginServlet {
	
	private static String usernameRef;
	private static String passwordRef;
	
	public static void doPost(String username, String password) {
		//synchronized public static void doPost(String username, String password){
		try {
			usernameRef = username;
			if(username.equals("a")) {
				Thread.sleep(5000);
			}
			passwordRef = password;
			
			System.out.println("username=" + usernameRef + " password="
					+ passwordRef);
			
		} catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

//測試類
Run.java
package chapter01.section02.thread_1_2_3.project_2_t4.project_3_t4threadsafe;

public class Run {

	public static void main(String[] args) {
		ALogin a = new ALogin();
		a.start();
		BLogin b = new BLogin();
		b.start();
		
	}	
}

/*
result:
username=b password=bb
username=b password=aa

我們可以看到出現了"非執行緒安全問題", 可以使用syncronized關鍵字
result:
username=a password=aa
username=b password=bb
 */

println()方法與i++聯合使用時"有可能"出現另一種異常情況 在這裡對Thread()構造方法做出澄清,它會自動設定執行緒的名字 public Thread​() Allocates a new Thread object. This constructor has the same effect as Thread (null, null, gname), where gname is a newly generated name. Automatically  generated names are of the form "Thread-"+n, where n is an integer, 也就是說執行緒建立物件好了名字也就自己設定好了,但是你可以重新setName()

程式碼示例如下:

package chapter01.section02.thread_1_2_4.project_1_smaeNum;

public class MyThread extends Thread {
	private int i = 5;
	
	@Override
	public void run() {
//	synchronized public void run() {
		System.out.println("i=" + (i--) + " threadName="
				+ Thread.currentThread().getName());
	}
}


package chapter01.section02.thread_1_2_4.project_1_smaeNum;

public class Run {
	
	
	public static void main(String[] args) {
		MyThread run = new MyThread();
		
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		Thread t3 = new Thread(run);
		Thread t4 = new Thread(run);
		Thread t5 = new Thread(run);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
	
}

/*
result:
i=4 threadName=Thread-3
i=1 threadName=Thread-4
i=2 threadName=Thread-5
i=3 threadName=Thread-2
i=5 threadName=Thread-1
 */

可以看到出現了非執行緒安全的問題,雖然println()方法在內部是同步的,但i-- 操作卻是在進入println()之前發生的,為了防止這種情況的發生,繼續使用同步 方法

currentThread()方法 currentThread()方法可返回程式碼段正在被哪個執行緒呼叫的資訊。 public static Thread currentThread​() Returns a reference to the currently executing thread object.

看下面的案例:

package chapter01.section03.project_1_t6;

public class Run1 {
	public static void main(String args[]) {
		System.out.println(Thread.currentThread().getName());
	}
}

/*
result:
main
*/

結果說明: main方法被名為main的執行緒呼叫(它是使用者執行緒)

繼續實驗:

package chapter01.section03.project_1_t6;

public class MyThread extends Thread {
	
	public MyThread() {
		System.out.println("構造方法的列印: " + Thread.currentThread().getName());
	}
	
	@Override
	public void run() {
		System.out.println("run方法的列印: " + Thread.currentThread().getName());
	}
}


package chapter01.section03.project_1_t6;

public class Run2 {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.start();
//		myThread.run(); //結果是由main主執行緒執行  結果是 run方法的列印: main
	}
}


/*

result:
構造方法的列印: main
run方法的列印: Thread-0
 */

從執行結果可以發現,MyThread.java類的建構函式是被main執行緒呼叫的,而run 方法是被名稱為Thread-0的執行緒呼叫的,run方法是自動調動的方法  

再來測試一個比較複雜的情況:

package chapter01.section03.project_2_currentThreadExt;

public class CountOperate extends Thread{
	
	public CountOperate(){
		System.out.println("CountOperate--begin");
		System.out.println("Thread.currentThread().getName()="
				+ Thread.currentThread().getName());
		System.out.println("this.getName()=" + this.getName());
		System.out.println("CountOperate---end");
	}

	@Override
	public void run() {
		System.out.println("run---begin");
		System.out.println("Thread.currentThread().getName()="
				+ Thread.currentThread().getName());
		System.out.println("this.getName()=" + this.getName());
		System.out.println("run--end");
	}
}

package chapter01.section03.project_2_currentThreadExt;

public class Run {
	public static void main(String[] args) {
		CountOperate c = new CountOperate();
		Thread t1 = new Thread(c);
		t1.setName("A");
		t1.start();
	}
}

/*
result:
CountOperate--begin
Thread.currentThread().getName()=main 建構函式在主執行緒main中執行
this.getName()=Thread-0 c物件自動命名為Thread-n即Thread-0
CountOperate---end
run---begin
Thread.currentThread().getName()=A t1執行緒物件名字為A,當前程式碼段正在被A執行緒呼叫
this.getName()=Thread-0
run--end
*/

這裡需要說明的是:c的name初始值預設為Thread-0開始,t1的name初始值預設為Thread-1開始