Java多執行緒學習(五)執行緒間通訊知識點補充

Guide哥發表於2018-03-26

系列文章傳送門:

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

Java多執行緒學習(二)synchronized關鍵字(1)

Java多執行緒學習(二)synchronized關鍵字(2)

Java多執行緒學習(三)volatile關鍵字

Java多執行緒學習(四)等待/通知(wait/notify)機制

Java多執行緒學習(五)執行緒間通訊知識點補充

系列文章將被優先更新於微信公眾號“Java面試通關手冊”,歡迎廣大Java程式設計師和愛好技術的人員關注。

本節思維導圖:

本節思維導圖

思維導圖原始檔+思維導圖軟體關注微信公眾號:“Java面試通關手冊” 回覆關鍵字:“Java多執行緒” 免費領取。

我們通過之前幾章的學習已經知道在執行緒間通訊用到的synchronized關鍵字、volatile關鍵字以及等待/通知(wait/notify)機制。今天我們就來講一下執行緒間通訊的其他知識點:管道輸入/輸出流、Thread.join()的使用、ThreadLocal的使用。

一 管道輸入/輸出流

管道輸入/輸出流和普通檔案的輸入/輸出流或者網路輸入、輸出流不同之處在於管道輸入/輸出流主要用於執行緒之間的資料傳輸,而且傳輸的媒介為記憶體

管道輸入/輸出流主要包括下列兩類的實現:

面向位元組: PipedOutputStream、 PipedInputStream

面向字元: PipedWriter、 PipedReader

1.1 第一個管道輸入/輸出流例項

完整程式碼:github.com/Snailclimb/…

writeMethod方法

	public void writeMethod(PipedOutputStream out) {
		try {
			System.out.println("write :");
			for (int i = 0; i < 300; i++) {
				String outData = "" + (i + 1);
				out.write(outData.getBytes());
				System.out.print(outData);
			}
			System.out.println();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
複製程式碼

readMethod方法

	public void readMethod(PipedInputStream input) {
		try {
			System.out.println("read  :");
			byte[] byteArray = new byte[20];
			int readLength = input.read(byteArray);
			while (readLength != -1) {
				String newData = new String(byteArray, 0, readLength);
				System.out.print(newData);
				readLength = input.read(byteArray);
			}
			System.out.println();
			input.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
複製程式碼

測試方法

	public static void main(String[] args) {

		try {
			WriteData writeData = new WriteData();
			ReadData readData = new ReadData();

			PipedInputStream inputStream = new PipedInputStream();
			PipedOutputStream outputStream = new PipedOutputStream();

			// inputStream.connect(outputStream);
			outputStream.connect(inputStream);

			ThreadRead threadRead = new ThreadRead(readData, inputStream);
			threadRead.start();

			Thread.sleep(2000);

			ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
			threadWrite.start();

		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}
複製程式碼

我們上面定義了兩個方法writeMethodreadMethod,前者用於寫位元組/字元(取決於你用的是PipedOuputStream還是PipedWriter),後者用於讀取位元組/字元(取決於你用的是PipedInputStream還是PipedReader).我們定義了兩個執行緒threadReadthreadWrite ,threadRead執行緒執行readMethod方法,threadWrite執行writeMethod方法。然後 通過outputStream.connect(inputStream)inputStream.connect(outputStream)使兩個管道流產生連結,這樣就可以將資料進行輸入與輸出了。

執行結果:

執行結果

二 Thread.join()的使用

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

Thread類除了提供join()方法之外,還提供了join(long millis)、join(long millis, int nanos)兩個具有超時特性的方法。這兩個超時方法表示,如果執行緒thread在指定的超時時間沒有終止,那麼將會從該超時方法中返回

2.1 join方法使用

不使用join方法的弊端演示:

Test.java

public class Test {

	public static void main(String[] args) throws InterruptedException {

		MyThread threadTest = new MyThread();
		threadTest.start();

		//Thread.sleep(?);//因為不知道子執行緒要花的時間這裡不知道填多少時間
		System.out.println("我想當threadTest物件執行完畢後我再執行");
	}
	static public class MyThread extends Thread {

		@Override
		public void run() {
			System.out.println("我想先執行");
		}

	}
}
複製程式碼

執行結果:

執行結果
可以看到子執行緒中後被執行,這裡的例子只是一個簡單的演示,我們想一下:假如子執行緒執行的結果被主執行緒執行需要怎麼辦? sleep方法? 當然可以,但是子執行緒執行需要的時間是不確定的,所以sleep多長時間當然也就不確定了。這裡就需要使用join方法解決上面的問題。

使用join方法解決上面的問題:

Test.java

public class Test {

	public static void main(String[] args) throws InterruptedException {

		MyThread threadTest = new MyThread();
		threadTest.start();

		//Thread.sleep(?);//因為不知道子執行緒要花的時間這裡不知道填多少時間
		threadTest.join();
		System.out.println("我想當threadTest物件執行完畢後我再執行");
	}
	static public class MyThread extends Thread {

		@Override
		public void run() {
			System.out.println("我想先執行");
		}

	}
}
複製程式碼

上面的程式碼僅僅加上了一句:threadTest.join();。在這裡join方法的作用就是主執行緒需要等待子執行緒執行完成之後再結束

2.2 join(long millis)方法的使用

join(long millis)中的引數就是設定的等待時間。

JoinLongTest.java

public class JoinLongTest {

	public static void main(String[] args) {
		try {
			MyThread threadTest = new MyThread();
			threadTest.start();

			threadTest.join(2000);// 只等2秒
			//Thread.sleep(2000);

			System.out.println("  end timer=" + System.currentTimeMillis());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	static public class MyThread extends Thread {

		@Override
		public void run() {
			try {
				System.out.println("begin Timer=" + System.currentTimeMillis());
				Thread.sleep(10000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}
}
複製程式碼

執行結果:

執行結果

不管是執行threadTest.join(2000)還是Thread.sleep(2000)“end timer=1522036620288”語句的輸出都是間隔兩秒“end timer=1522036620288”語句輸出後該程式還會執行一段時間,因為執行緒中的run方法中有Thread.sleep(10000)語句

另外threadTest.join(2000)Thread.sleep(2000) 和區別在於: Thread.sleep(2000)不會釋放鎖,threadTest.join(2000)會釋放鎖

三 ThreadLocal的使用

變數值的共享可以使用public static變數的形式,所有執行緒都使用一個public static變數。 如果想實現每一個執行緒都有自己的共享變數該如何解決呢? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個執行緒繫結自己的值,可以將ThreadLocal類形象的比喻成存放資料的盒子,盒子中可以儲存每個執行緒的私有資料。

再舉個簡單的例子: 比如有兩個人去寶屋收集寶物,這兩個共用一個袋子的話肯定會產生爭執,但是給他們兩個人每個人分配一個袋子的話就不會出現這樣的問題。如果把這兩個人比作執行緒的話,那麼ThreadLocal就是用來這兩個執行緒競爭的。

ThreadLocal類相關方法:

方法名稱 描述
get() 返回當前執行緒的此執行緒區域性變數的副本中的值。
set(T value) 將當前執行緒的此執行緒區域性變數的副本設定為指定的值
remove() 刪除此執行緒區域性變數的當前執行緒的值。
initialValue() 返回此執行緒區域性變數的當前執行緒的“初始值”

3.1 ThreadLocal類的初試

Test1.java

public class Test1 {
	public static ThreadLocal<String> t1 = new ThreadLocal<String>();

	public static void main(String[] args) {
		if (t1.get() == null) {
			System.out.println("為ThreadLocal類物件放入值:aaa");
			t1.set("aaaֵ");
		}
		System.out.println(t1.get());//aaa
		System.out.println(t1.get());//aaa
	}

}

複製程式碼

從執行結果可以看出,第一次呼叫ThreadLocal物件的get()方法時返回的值是null,通過呼叫set()方法可以為ThreadLocal物件賦值。

如果想要解決get()方法null的問題,可以使用ThreadLocal物件的initialValue方法。如下:

Test2.java


public class Test2 {
	public static ThreadLocalExt t1 = new ThreadLocalExt();

	public static void main(String[] args) {
		if (t1.get() == null) {
			System.out.println("從未放過值");
			t1.set("我的值");
		}
		System.out.println(t1.get());
		System.out.println(t1.get());
	}
	static public class ThreadLocalExt extends ThreadLocal {
		@Override
		protected Object initialValue() {
			return "我是預設值 第一次get不再為null";
		}
	}


}
複製程式碼

3.2 驗證執行緒變數間的隔離性

Test3.java

/**
 *TODO 驗證執行緒變數間的隔離性
 */
public class Test3 {

	public static void main(String[] args) {
		try {
			for (int i = 0; i < 10; i++) {
				System.out.println("       在Main執行緒中取值=" + Tools.tl.get());
				Thread.sleep(100);
			}
			Thread.sleep(5000);
			ThreadA a = new ThreadA();
			a.start();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	static public class Tools {
		public static ThreadLocalExt tl = new ThreadLocalExt();
	}
	static public class ThreadLocalExt extends ThreadLocal {
		@Override
		protected Object initialValue() {
			return new Date().getTime();
		}
	}

	static public class ThreadA extends Thread {

		@Override
		public void run() {
			try {
				for (int i = 0; i < 10; i++) {
					System.out.println("在ThreadA執行緒中取值=" + Tools.tl.get());
					Thread.sleep(100);
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}
}
複製程式碼

從執行結果可以看出子執行緒和父執行緒各自擁有各自的值。

執行結果:

執行結果

3.3 InheritableThreadLocal

ThreadLocal類固然很好,但是子執行緒並不能取到父執行緒的ThreadLocal類的變數,InheritableThreadLocal類就是解決這個問題的

取父執行緒的值:

修改Test3.java的內部類Tools 和ThreadLocalExt類如下:

 static public class Tools {
		public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
	}
	static public class InheritableThreadLocalExt extends InheritableThreadLocal {
		@Override
		protected Object initialValue() {
			return new Date().getTime();
		}
	}
複製程式碼

執行結果:

執行結果
取父執行緒的值並修改:

修改Test3.java的內部類Tools 和InheritableThreadLocalExt類如下:

	static public class Tools {
		public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
	}
	static public class InheritableThreadLocalExt extends InheritableThreadLocal {
		@Override
		protected Object initialValue() {
			return new Date().getTime();
		}

		@Override
		protected Object childValue(Object parentValue) {
			return parentValue + " 我在子執行緒加的~!";
		}
	}
複製程式碼

執行結果:

執行結果
在使用InheritableThreadLocal類需要注意的一點是:如果子執行緒在取得值的同時,主執行緒將InheritableThreadLocal中的值進行更改,那麼子執行緒取到的還是舊值

參考:

《Java多執行緒程式設計核心技術》

《Java併發程式設計的藝術》

相關文章