天天看點

4.java并發程式設計基礎

一、線程簡介

1.1、什麼是線程

1.2、為什麼要使用多線程

1.3、線程優先級

1.4、線程的狀态

Java線程在運作的生命周期中可能處于表4-1所示的6種不同的狀态;

4.java并發程式設計基礎

1.5、Daemon線程

Daemon線程是一種支援型線程,因為它主要被用作程式中背景排程以及支援性工作。這意味着,當一個Java虛拟機中不存在非Daemon線程的時候,Java虛拟機将會退出。可以通過調用Thread.setDaemon(true)将線程設定為Daemon線程。

Daemon線程被用作完成支援性工作,但是在Java虛拟機退出時Daemon線程中的finally塊并不一定會執行。

main線程(非Daemon線程)在啟動了線程DaemonRunner之後随着main方法執行完畢而終止,而此時Java虛拟機中已經沒有非Daemon線程,虛拟機需要退出。Java虛拟機中的所有Daemon線程都需要立即終止,是以DaemonRunner立即終止,但是DaemonRunner中的finally塊并沒有執行。

**注意 **在建構Daemon線程時,不能依靠finally塊中的内容來確定執行關閉或清理資源的邏輯

二、啟動和終止線程

2.1、構造線程

2.2、啟動線程

2.3、了解中斷

阻塞庫方法::比如說,一個線程因為等待資源,而無限阻塞,這是時候,可以調用interrupt()方法将線程進行中斷(抛出異常)

不調用阻塞庫方法::擷取中斷标志,然後進行修改,然後進行 自己的邏輯操作;

中斷可以了解為線程的一個辨別位屬性,它表示一個運作中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調用該線程的interrupt()方法對其進行中斷操作。

線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜态方法Thread.interrupted()對目前線程的中斷辨別位進行複位。如果該線程已經處于終結狀态,即使該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會傳回false。

從Java的API中可以看到,許多聲明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在抛出InterruptedException之前,Java虛拟機會先将該線程的中斷辨別位清除,然後抛出InterruptedException,此時調用isInterrupted()方法将會傳回false。

在代碼清單4-7所示的例子中,首先建立了兩個線程,SleepThread和BusyThread,前者不停地睡眠,後者一直運作,然後對這兩個線程分别進行中斷操作,觀察二者的中斷辨別位

SleepThread interrupted is false

BusyThread interrupted is true

           

從結果可以看出,抛出InterruptedException的線程SleepThread,其中斷辨別位被清除了,而一直忙碌運作的線程BusyThread,中斷辨別位沒有被清除。

2.4、過期的suspend()、resume()和stop()

大家對于CD機肯定不會陌生,如果把它播放音樂比作一個線程的運作,那麼對音樂播放做出的暫停、恢複和停止操作對應線上程Thread的API就是suspend()、resume()和stop()。

被 wait()和 notiafy 取代

2.5、安全地終止線程

在4.2.3節中提到的中斷狀态是線程的一個辨別位,而中斷操作是一種簡便的線程間互動方式,而這種互動方式最适合用來取消或停止任務。除了中斷以外,還可以利用一個boolean變量來控制是否需要停止任務并終止該線程。

在代碼清單4-9所示的例子中,建立了一個線程CountThread,它不斷地進行變量累加,而主線程嘗試對其進行中斷操作和停止操作。

public class Shutdown {

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

		Runner one = new Runner();

		Thread countThread = new Thread(one, "CountThread");

		countThread.start();

		// 睡眠1秒,main線程對CountThread進行中斷,使CountThread能夠感覺中斷而結束

		TimeUnit.SECONDS.sleep(1);

		countThread.interrupt();

		Runner two = new Runner();

		countThread = new Thread(two, "CountThread");

		countThread.start();

		// 睡眠1秒,main線程對Runner two進行取消,使CountThread能夠感覺on為false而結束

		TimeUnit.SECONDS.sleep(1);

		two.cancel();

	}

	private static class Runner implements Runnable {

		private long i;

		private volatile boolean on = true;

		@Override

		public void run() {

			while (on && !Thread.currentThread().isInterrupted()){

				i++;

			}

			System.out.println("Count i = " + i);

		}

		public void cancel() {

			on = false;

		}

	}

}
           

示例在執行過程中,main線程通過中斷操作和cancel()方法均可使CountThread得以終止。(利用自定義标志位終止)

這種通過辨別位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地将線程停止,是以這種終止線程的做法顯得更加安全和優雅。

三、線程間通信

3.1、volatile和synchronized關鍵字

Java支援多個線程同時通路一個對象或者對象的成員變量,由于每個線程可以擁有這個變量的拷貝,是以程式在執行過程中,一個線程看到的變量并不一定是最新的。(需要volatile保證可見性)

關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確定多個線程在同一個時刻,隻能有一個線程處于方法或者同步塊中,它保證了線程對變量通路的可見性和排他性。

對于同步塊的實作使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。無論采用哪種方式,其本質是對一個對象的螢幕(monitor)進行擷取,而這個擷取過程是排他的,也就是同一時刻隻能有一個線程擷取到由synchronized所保護對象的螢幕。

任意一個對象都擁有自己的螢幕,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先擷取到該對象的螢幕才能進入同步塊或者同步方法,而沒有擷取到螢幕(執行該方法)的線程将會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀态

4.java并發程式設計基礎

從圖4-2中可以看到,任意線程對Object(Object由synchronized保護)的通路,首先要獲得Object的螢幕。如果擷取失敗,線程進入同步隊列,線程狀态變為BLOCKED。當通路Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對螢幕的擷取

3.2、等待/通知機制

if換while

4.java并發程式設計基礎

3.3、等待/通知的經典範式

從4.3.2節中的WaitNotify示例中可以提煉出等待/通知的經典範式,該範式分為兩部分,分别針對等待方(消費者)和通知方(生産者)。

等待方遵循如下原則。

  • 1)擷取對象的鎖。
  • 2)如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
  • 3)條件滿足則執行對應的邏輯。

對應的僞代碼如下:一定要while,需要滿足第二、三條,是以不能用IF

synchronized(對象) {

	while(條件不滿足) {

		對象.wait();

	}

	對應的處理邏輯

}
           

通知方遵循如下原則。

  • 1)獲得對象的鎖。
  • 2)改變條件。
  • 3)通知所有等待在對象上的線程。

對應的僞代碼如下:

synchronized(對象) {

	改變條件

	對象.notifyAll();

}
           

3.4、管道輸入/輸出流

管道輸入/輸出流和普通的檔案輸入/輸出流或者網絡輸入/輸出流不同之處在于,它主要用于線程之間的資料傳輸,而傳輸的媒介為記憶體。

管道輸入/輸出流主要包括了如下4種具體實作:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前兩種面向位元組,而後兩種面向字元。

在代碼清單4-12所示的例子中,建立了printThread,它用來接受main線程的輸入,任何main線程的輸入均通過PipedWriter寫入,而printThread在另一端通過PipedReader将内容讀出并列印。

public class Piped {

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

		PipedWriter out = new PipedWriter();

		PipedReader in = new PipedReader();

		// 将輸出流和輸入流進行連接配接,否則在使用時會抛出IOException

		out.connect(in);

		Thread printThread = new Thread(new Print(in), "PrintThread");

		printThread.start();

		int receive = 0;

		try {

			while ((receive = System.in.read()) != -1) {

			out.write(receive);

		}

		} finally {

			out.close();

		}

	}

	static class Print implements Runnable {

		private PipedReader in;

		public Print(PipedReader in) {

			this.in = in;

		}

		public void run() {

			int receive = 0;

		try {

			while ((receive = in.read()) != -1) {

				System.out.print((char) receive);

		}

		} catch (IOException ex) {}

		}

	}

}
           

運作該示例,輸入一組字元串,可以看到被printThread進行了原樣輸出。

Repeat my words.

Repeat my words.
           

對于Piped類型的流,必須先要進行綁定,也就是調用connect()方法,如果沒有将輸入/輸出流綁定起來,對于該流的通路将會抛出異常。

3.5、Thread.join()的使用

每個線程終止的前提是前驅線程的終止,每個線程等待前驅線程終止後,才從join()方法傳回,這裡涉及了等待/通知機制。(上面線程執行完才能執行下面的)

3.6、ThreadLocal的使用

四、線程應用執行個體

4.1、等待逾時模式

4.2、一個簡單的資料庫連接配接池示例

4.3、線程池技術及其示例

4.4、一個基于線程池技術的簡單Web伺服器

五、本章小結

繼續閱讀