自我提升(基礎技術篇)——java線程簡介

前言:雖然自己平時都在用多線程,也能完成基本的工作需求,但總覺得,還是對線程沒有一個系統的概念,所以,查閱了一些資料,理解那些大神和官方的資料,寫這么一篇關于線程的文章

本來想廢話一番,講講自己的經歷,不過,還是直接上正題吧。

想要系統的認識一個東西,我們還是得一步步來,從基礎出發,這樣才能比較系統的了解一個東西!

線程基礎

什么是線程?

幾乎每種操作系統都支持進程的概念 ―― 進程就是在某種程度上相互隔離的、獨立運行的程序。

線程化是允許多個活動共存于一個進程中的工具。大多數現代的操作系統都支持線程,而且線程的概念以各種形式已存在了好多年。Java 是第一個在語言本身中顯式地包含線程的主流編程語言,它沒有把線程化看作是底層操作系統的工具。

有時候,線程也稱作輕量級進程。就象進程一樣,線程在程序中是獨立的、并發的執行路徑,每個線程有它自己的堆棧、自己的程序計數器和自己的局部變量。但是,與分隔的進程相比,進程中的線程之間的隔離程度要小。它們共享內存、文件句柄和其它每個進程應有的狀態

進程可以支持多個線程,它們看似同時執行,但互相之間并不同步。一個進程中的多個線程共享相同的內存地址空間,這就意味著它們可以訪問相同的變量和對象,而且它們從同一堆中分配對象。盡管這讓線程之間共享信息變得更容易,但您必須小心,確保它們不會妨礙同一進程里的其它線程。(鎖的問題)

Java 線程工具和 API 看似簡單。但是,編寫有效使用線程的復雜程序并不十分容易。因為有多個線程共存在相同的內存空間中并共享相同的變量,所以您必須小心,確保您的線程不會互相干擾

每個 Java(Android) 程序都使用線程

每個 Java 程序都至少有一個線程 ― 主線程。當一個 Java 程序啟動時,JVM 會創建主線程,并在該線程中調用程序的main()方法。

JVM 還創建了其它線程,您通常都看不到它們 ― 例如,與垃圾收集、對象終止和其它 JVM 內務處理任務相關的線程。其它工具也創建線程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))或 Swing UI 工具箱、servlet 容器、應用程序服務器和 RMI(遠程方法調用(Remote Method Invocation))。

每一個Android程序也是如此,而Android程序,默認是UI線程。

為什么使用線程?

在 Java (Android)程序中使用線程有許多原因:

* 使 UI 響應更快

* 利用多處理器系統

* 簡化建模

* 執行異步或后臺處理

響應更快的 UI:

事件驅動的 UI 工具箱(如 AWT 和 Swing)有一個事件線程,它處理 UI 事件,如擊鍵或鼠標點擊。

AWT 和 Swing 程序把事件偵聽器與 UI 對象連接。當特定事件(如單擊了某個按鈕)發生時,這些偵聽器會得到通知。事件偵聽器是在 AWT 事件線程中調用的。

如果事件偵聽器要執行持續很久的任務,如檢查一個大文檔中的拼寫,事件線程將忙于運行拼寫檢查器,所以在完成事件偵聽器之前,就不能處理額外的 UI 事件。這就會使程序看來似乎停滯了,讓用戶不知所措。

要避免使 UI 延遲響應,事件偵聽器應該把較長的任務放到另一個線程中,這樣 AWT 線程在任務的執行過程中就可以繼續處理 UI 事件(包括取消正在執行的長時間運行任務的請求)。

Android 的話,舉個最簡答的例子,界面顯示和網絡請求,界面展示在UI線程,網絡請求在子線程。網絡請求是耗時操作,UI線程只負責界面渲染。就可以讓頁面流暢,等網絡請求結束后再刷新界面。這樣,就不會因為網絡請求而阻塞界面的顯示了。

利用多處理器系統

多處理器系統比過去更普及了。以前只能在大型數據中心和科學計算設施中才能找到它們。現在許多低端服務器系統 ― 甚至是一些臺式機系統 ― 都有多個處理器。

現代操作系統,包括 Linux、Solaris 和 Windows,Mac都可以利用多個處理器并調度線程在任何可用的處理器上執行。

調度的基本單位通常是線程;如果某個程序只有一個活動的線程,它一次只能在一個處理器上運行。如果某個程序有多個活動線程,那么可以同時調度多個線程。在精心設計的程序中,使用多個線程可以提高程序吞吐量和性能。

簡化建模

在某些情況下,使用線程可以使程序編寫和維護起來更簡單。考慮一個仿真應用程序,您要在其中模擬多個實體之間的交互作用。給每個實體一個自己的線程可以使許多仿真和對應用程序的建模大大簡化。

另一個適合使用單獨線程來簡化程序的示例是在一個應用程序有多個獨立的事件驅動的組件的時候。例如,一個應用程序可能有這樣一個組件,該組件在某個事件之后用秒數倒計時,并更新屏幕顯示。與其讓一個主循環定期檢查時間并更新顯示,不如讓一個線程什么也不做,一直休眠,直到某一段時間后,更新屏幕上的計數器,這樣更簡單,而且不容易出錯。這樣,主線程就根本無需擔心計時器。

這個比較抽象,嗯,我試著用更簡單的話來解釋:就舉例快遞吧,單線程就是只有一個快遞員,但是有那么多的包裹要配送,這一個快遞員,就需要去想,我該按什么順序送呢?先送哪里后送哪里才最快等等?這樣這個快遞員就像想很多問題(其實就是linux或者cpu或者程序員要想)。現在多線程,就是我有很多快遞員了,我就指派A快遞員負責A區域的快遞,B負責B區域的,以此類推。我每個快遞員,就只需要負責自己那塊區域,這樣既高效又省事。(當然,具體要設計的cpu的線程調度問題了,就這么理解著吧)那么主線程的快遞呢,那就是vip了,走專門的路線,到指定的地點。

異步或后臺處理

服務器應用程序從遠程來源(如套接字)獲取輸入。當讀取套接字時,如果當前沒有可用數據,那么對SocketInputStream.read()的調用將會阻塞,直到有可用數據為止。

如果單線程程序要讀取套接字,而套接字另一端的實體并未發送任何數據,那么該程序只會永遠等待,而不執行其它處理。相反,程序可以輪詢套接字,查看是否有可用數據,但通常不會使用這種做法,因為會影響性能。

但是,如果您創建了一個線程來讀取套接字,那么當這個線程等待套接字中的輸入時,主線程就可以執行其它任務。您甚至可以創建多個線程,這樣就可以同時讀取多個套接字。這樣,當有可用數據時,您會迅速得到通知(因為正在等待的線程被喚醒),而不必經常輪詢以檢查是否有可用數據。使用線程等待套接字的代碼也比輪詢更簡單、更不易出錯。

是不是感覺,這樣子,多線程好爽,也好簡單呀,但是,簡單卻有風險

雖然 Java 線程工具非常易于使用,但當您創建多線程程序時,應該盡量避免一些風險。

當多個線程訪問同一數據項(如靜態字段、可全局訪問對象的實例字段或共享集合)時,需要確保它們協調了對數據的訪問,這樣它們都可以看到數據的一致視圖,而且相互不會干擾另一方的更改。為了實現這個目的,Java 語言提供了兩個關鍵字:synchronizedvolatile。(稍后再詳細講這兩兄弟,因為他們太重要了)

當從多個線程中訪問變量時,必須確保對該訪問正確地進行了同步對于簡單變量,將變量聲明成volatile也許就足夠了,但在大多數情況下,需要使用同步。

如果您將要使用同步來保護對共享變量的訪問,那么必須確保在程序中所有訪問該變量的地方都使用同步

同時還需要注意:

雖然線程可以大大簡化許多類型的應用程序,過度使用線程可能會危及程序的性能及其可維護性。線程消耗了資源。因此,在不降低性能的情況下,可以創建的線程的數量是有限制的。

尤其在單處理器系統中,使用多個線程會使主要消耗 CPU 資源的程序運行得更快。

還是舉個代碼例子吧,畢竟是程序員嘛

以下示例使用兩個線程,一個用于計時,一個用于執行實際工作。主線程使用非常簡單的算法計算素數

在它啟動之前,它創建并啟動一個計時器線程,這個線程會休眠十秒鐘,然后設置一個主線程要檢查的標志。十秒鐘之后,主線程將停止。請注意,共享標志被聲明成volatile。


小結

Java 語言包含了內置在語言中的功能強大的線程工具。您可以將線程工具用于:

增加 GUI 應用程序的響應速度

利用多處理器系統

當程序有多個獨立實體時,簡化程序邏輯

在不阻塞整個程序的情況下,執行阻塞 I/O

當使用多個線程時,必須謹慎,遵循在線程之間共享數據的規則,我們將在共享對數據的訪問中討論這些規則。所有這些規則歸結為一條基本原則:不要忘了同步

線程的生命

創建線程

在 Java 程序中創建線程有幾種方法。每個 Java 程序至少包含一個線程:主線程。其它線程都是通過Thread構造器或實例化繼承類Thread的類來創建的。

Java 線程可以通過直接實例化Thread對象或實例化繼承Thread的對象來創建其它線程。在線程基礎中的示例(其中,我們在十秒鐘之內計算盡量多的素數)中,我們通過實例化CalculatePrimes類型的對象(它繼承了Thread),創建了一個線程。

當我們討論 Java 程序中的線程時,也許會提到兩個相關實體:完成工作的實際線程或代表線程的Thread對象。正在運行的線程通常是由操作系統創建的;Thread對象是由 Java VM 創建的,作為控制相關線程的一種方式。

啟動線程

在一個線程對新線程的Thread對象調用start()方法之前,這個新線程并沒有真正開始執行。Thread對象在其線程真正啟動之前就已經存在了,而且其線程退出之后仍然存在。這可以讓您控制或獲取關于已創建的線程的信息,即使線程還沒有啟動或已經完成了。

通常在構造器中通過start()啟動線程并不是好主意。這樣做,會把部分構造的對象暴露給新的線程。如果對象擁有一個線程,那么它應該提供一個啟動該線程的start()或init()方法,而不是從構造器中啟動它。

結束線程

線程會以以下三種方式之一結束:

線程到達其run()方法的末尾。

線程拋出一個未捕獲到的Exception或Error。

另一個線程調用一個棄用的stop()方法。棄用是指這些方法仍然存在,但是您不應該在新代碼中使用它們,并且應該盡量從現有代碼中除去它們。

通常用interrupt()方法,這個方法是中斷線程的方法

當 Java 程序中的所有線程都完成時,程序就退出了。

加入線程

Thread API 包含了等待另一個線程完成的方法:join()方法。當調用Thread.join()時,調用線程將阻塞,直到目標線程完成為止。

Thread.join()通常由使用線程的程序使用,以將大問題劃分成許多小問題,每個小問題分配一個線程。嗯,例子后面補充,先講概念。

調度

除了何時使用Thread.join()Object.wait()外,線程調度和執行的計時是不確定的。如果兩個線程同時運行,而且都不等待,您必須假設在任何兩個指令之間,其它線程都可以運行并修改程序變量。如果線程要訪問其它線程可以看見的變量,如從靜態字段(全局變量)直接或間接引用的數據,則必須使用同步以確保數據一致性。(這個大家,自己寫一段程序就可以實驗了)

休眠

Thread API 包含了一個sleep()方法,它將使當前線程進入等待狀態,直到過了一段指定時間,或者直到另一個線程對當前線程的Thread對象調用了Thread.interrupt(),從而中斷了線程。當過了指定時間后,線程又將變成可運行的,并且回到調度程序的可運行線程隊列中。

如果線程是由對Thread.interrupt()的調用而中斷的,那么休眠的線程會拋出InterruptedException,這樣線程就知道它是由中斷喚醒的,就不必查看計時器是否過期。

Thread.yield()方法就象Thread.sleep()一樣,但它并不引起休眠,而只是暫停當前線程片刻,這樣其它線程就可以運行了。在大多數實現中,當較高優先級的線程調用Thread.yield()時,較低優先級的線程就不會運行。

CalculatePrimes示例使用了一個后臺線程計算素數,然后休眠十秒鐘。當計時器過期后,它就會設置一個標志,表示已經過了十秒。

守護程序線程

我們提到過當 Java 程序的所有線程都完成時,該程序就退出,但這并不完全正確。隱藏的系統線程,如垃圾收集線程和由 JVM 創建的其它線程會怎么樣?我們沒有辦法停止這些線程。如果那些線程正在運行,那么 Java 程序怎么退出呢?

這些系統線程稱作守護程序線程Java 程序實際上是在它的所有非守護程序線程完成后退出的

任何線程都可以變成守護程序線程。可以通過調用Thread.setDaemon()方法來指明某個線程是守護程序線程。您也許想要使用守護程序線程作為在程序中創建的后臺線程,如計時器線程或其它延遲的事件線程,只有當其它非守護程序線程正在運行時,這些線程才有用。

說了這么多,來個例子:用多個線程分解大任務

本來想截圖的,但是,不知道為什么,上傳不了,就上代碼吧

public class TenThreads {

private static class WorkerThread extends Thread {

int max = Integer.MIN_VALUE;

int[] ourArray;

public WorkerThread(int[] ourArray) {

this.ourArray = ourArray;

}

// Find the maximum value in our particular piece of the array

public void run() {

for (int i = 0; i < ourArray.length; i++)

max = Math.max(max, ourArray[i]);

}

public int getMax() {

return max;

}

}

public static void main(String[] args) {

WorkerThread[] threads = new WorkerThread[10];

int[][] bigMatrix = getBigHairyMatrix();

int max = Integer.MIN_VALUE;

// Give each thread a slice of the matrix to work with

for (int i=0; i < 10; i++) {

threads[i] = new WorkerThread(bigMatrix[i]);

threads[i].start();

}

// Wait for each thread to finish

try {

for (int i=0; i < 10; i++) {

threads[i].join();

max = Math.max(max, threads[i].getMax());

}

}

catch (InterruptedException e) {

// fall through

}

System.out.println("Maximum value was " + max);

}

}

小結

就象程序一樣,線程有生命周期:它們啟動執行,然后完成。一個程序或進程也許包含多個線程,而這些線程看來互相單獨地執行。

線程是通過實例化Thread對象或實例化繼承Thread的對象來創建的,但在對新的Thread對象調用start()方法之前,這個線程并沒有開始執行。當線程運行到其run()方法的末尾或拋出未經處理的異常時,它們就結束了。

sleep()方法可以用于等待一段特定時間;而join()方法可能用于等到另一個線程完成。

共享對數據的訪問

線程,我之前有舉例,說線程可以看作快遞員,既然是快遞員,就要送包裹,打包包裹,這個包裹,就是我們說的數據。而線程應用中,核心也是對數據的處理和訪問。

共享變量

要使多個線程在一個程序中有用,它們必須有某種方法可以互相通信共享它們的結果。

讓線程共享其結果的最簡單方法是使用共享變量。它們還應該使用同步來確保值從一個線程正確傳播到另一個線程,以及防止當一個線程正在更新一些相關數據項時,另一個線程看到不一致的中間結果。

之前在計算素數的示例使用了一個共享布爾變量,用于表示指定的時間段已經過去了。這說明了在線程間共享數據最簡單的形式是:輪詢共享變量以查看另一個線程是否已經完成執行某項任務。

存在于同一個內存空間中的所有線程

正如前面討論過的,線程與進程有許多共同點,不同的是線程與同一進程中的其它線程共享相同的進程上下文,包括內存。這非常便利,但也有重大責任。只要訪問共享變量(靜態或實例字段),線程就可以方便地互相交換數據,但線程還必須確保它們以受控的方式訪問共享變量,以免它們互相干擾對方的更改。

任何線程可以訪問所有其作用域內的變量,就象主線程可以訪問該變量一樣。素數示例使用了一個公用實例字段,叫做finished,用于表示已經過了指定的時間。當計時器過期時,一個線程會寫這個字段;另一個線程會定期讀取這個字段,以檢查它是否應該停止。注:這個字段被聲明成volatile,這對于這個程序的正確運行非常重要。在本章的后面,我們將看到原因。

受控訪問的同步

為了確保可以在線程之間以受控方式共享數據,Java 語言提供了兩個關鍵字:synchronizedvolatile

Synchronized有兩個重要含義:它確保了一次只有一個線程可以執行代碼的受保護部分(互斥,mutual exclusion 或者說 mutex),而且它確保了一個線程更改的數據對于其它線程是可見的(更改的可見性)。

如果沒有同步,數據很容易就處于不一致狀態。例如,如果一個線程正在更新兩個相關值(比如,粒子的位置和速率),而另一個線程正在讀取這兩個值,有可能在第一個線程只寫了一個值,還沒有寫另一個值的時候,調度第二個線程運行,這樣它就會看到一個舊值和一個新值。同步讓我們可以定義必須原子地運行的代碼塊,這樣對于其他線程而言,它們要么都執行,要么都不執行。

同步的原子執行互斥方面類似于其它操作環境中的臨界段的概念。

確保共享數據更改的可見性

確保共享數據更改的可見性

同步可以讓我們確保線程看到一致的內存視圖。

處理器可以使用高速緩存加速對內存的訪問(或者編譯器可以將值存儲到寄存器中以便進行更快的訪問)。在一些多處理器體系結構上,如果在一個處理器的高速緩存中修改了內存位置,沒有必要讓其它處理器看到這一修改,直到刷新了寫入器的高速緩存并且使讀取器的高速緩存無效。

這表示在這樣的系統上,對于同一變量,在兩個不同處理器上執行的兩個線程可能會看到兩個不同的值!這聽起來很嚇人,但它很常見。它只是表示在訪問其它線程使用或修改的數據時,必須遵循某些規則。

Volatile比同步更簡單,只適合于控制對基本變量(整數、布爾變量等)的單個實例的訪問。當一個變量被聲明成volatile,任何對該變量的寫操作都會繞過高速緩存,直接寫入主內存,而任何對該變量的讀取也都繞過高速緩存,直接取自主內存。這表示所有線程在任何時候看到的volatile變量值都相同。

如果沒有正確的同步,線程可能會看到舊的變量值,或者引起其它形式的數據損壞。

用鎖保護的原子代碼塊

Volatile對于確保每個線程看到最新的變量值非常有用,但有時我們需要保護比較大的代碼片段,如涉及更新多個變量的片段。

同步使用監控器(monitor)或鎖的概念,以協調對特定代碼塊的訪問。

每個 Java 對象都有一個相關的鎖。同一時間只能有一個線程持有 Java 鎖。當線程進入synchronized代碼塊時,線程會阻塞并等待,直到鎖可用,當它可用時,就會獲得這個鎖,然后執行代碼塊。當控制退出受保護的代碼塊時,即到達了代碼塊末尾或者拋出了沒有在synchronized塊中捕獲的異常時,它就會釋放該鎖。

這樣,每次只有一個線程可以執行受給定監控器保護的代碼塊。從其它線程的角度看,該代碼塊可以看作是原子的,它要么全部執行,要么根本不執行。

Java 鎖定

Java 鎖定合并了一種互斥形式。每次只有一個線程可以持有鎖。鎖用于保護代碼塊或整個方法,必須記住是鎖的身份保護了代碼塊,而不是代碼塊本身,這一點很重要。一個鎖可以保護許多代碼塊或方法。

反之,僅僅因為代碼塊由鎖保護并不表示兩個線程不能同時執行該代碼塊。它只表示如果兩個線程正在等待相同的鎖,則它們不能同時執行該代碼。

例子:

這個例子中,兩個線程都能夠執行set方法,其實,可以這樣想,就是,你是兩個對象,在執行各自的方法。當然,其實并不是我說的這樣,只是,以現在的知識不知道該如何解釋,希望懂的朋友可以幫忙解釋一下。

同步的方法

創建synchronized塊的最簡單方法是將方法聲明成synchronized。這表示在進入方法主體之前,調用者必須獲得鎖:

public class Point {

public synchronized void setXY(int x, int y) {

this.x = x;

this.y = y;

}

}

對于普通的synchronized方法,這個鎖是一個對象,將針對它調用方法。對于靜態synchronized方法,這個鎖是與Class對象相關的監控器,在該對象中聲明了方法。

僅僅因為setXY()被聲明成synchronized并不表示兩個不同的線程不能同時執行setXY(),只要它們調用不同的Point實例的setXY()就可同時執行。對于一個Point實例,一次只能有一個線程執行setXY(),或Point的任何其它synchronized方法。

同步的塊

synchronized塊的語法比synchronized方法稍微復雜一點,因為還需要顯式地指定鎖要保護哪個塊。Point的以下版本等價于前一頁中顯示的版本:

public class Point {

public void setXY(int x, int y) {

synchronized (this) {

this.x = x;

this.y = y;

? ? ? ? ? ? ? ? ? ? ? ? ? ? ?}

? ? ? ? ? ? ? ? }

}

使用this引用作為鎖很常見,但這并不是必需的。這表示該代碼塊將與這個類中的synchronized方法使用同一個鎖。

由于同步防止了多個線程同時執行一個代碼塊,因此性能上就有問題,即使是在單處理器系統上。最好在盡可能最小的需要保護的代碼塊上使用同步。

訪問局部(基于堆棧的)變量從來不需要受到保護,因為它們只能被自己所屬的線程訪問。

大多數類并沒有同步

因為同步會帶來小小的性能損失,大多數通用類,如java.util中的 Collection 類,不在內部使用同步。這表示在沒有附加同步的情況下,不能在多個線程中使用諸如HashMap這樣的類。

通過每次訪問共享集合中的方法時使用同步,可以在多線程應用程序中使用 Collection 類。對于任何給定的集合,每次必須用同一個鎖進行同步。通常可以選擇集合對象本身作為鎖。

下一頁中的示例類SimpleCache顯示了如何使用HashMap以線程安全的方式提供高速緩存。但是,通常適當的同步并不只是意味著同步每個方法。

Collections類提供了一組便利的用于List、Map和Set接口的封裝器。您可以用Collections.synchronizedMap封裝Map,它將確保所有對該映射的訪問都被正確同步。

如果類的文檔沒有說明它是線程安全的,那么您必須假設它不是。

如以下代碼樣本所示,SimpleCache.java使用HashMap為對象裝入器提供了一個簡單的高速緩存。load()方法知道怎樣按對象的鍵裝入對象。在一次裝入對象之后,該對象就被存儲到高速緩存中,這樣以后的訪問就會從高速緩存中檢索它,而不是每次都全部地裝入它。對共享高速緩存的每個訪問都受到synchronized塊保護。由于它被正確同步,所以多個線程可以同時調用getObject和clearCache方法,而沒有數據損壞的風險。


小結

由于線程執行的計時是不確定的,我們需要小心,以控制線程對共享數據的訪問。否則,多個并發線程會互相干擾對方的更改,從而損壞數據,或者其它線程也許不能及時看到對共享數據的更改。

通過使用同步來保護對共享變量的訪問,我們可以確保線程以可預料的方式與程序變量進行交互。

每個 Java 對象都可以充當鎖,synchronized塊可以確保一次只有一個線程執行由給定鎖保護的synchronized代碼。


未完待續。。。。。

繼續上一次的接著說,本來想一次寫完的,但是,比較懶吧,也想放松一下,周末,就休息了。

今天就補全剩下的一部分吧


同步詳細信息

(這一部分,相對前面要難一點,做好心理準備,可能需要多看幾次)

互斥

共享對數據的訪問中,我們討論了synchronized塊的特征,并在實現典型互斥鎖(即,互斥或臨界段)時說明了它們,其中每次只有一個線程可以執行受給定鎖保護的代碼塊。(換句話說,就是互斥,其實就是相互排斥的意思,只有一個線程可以持有,不能存在多個線程都持有,這里的持有可以理解為使用權限,舉個例子,你打電話時,只能和一個人保持通話。)

互斥是同步所做工作的重要部分,但同步還有其它幾種特征,這些特征對于在多處理器系統上取得正確結果非常重要。

可見性

除了互斥,同步(如volatile)強制某些可見性約束。當對象獲取鎖時,它首先使自己的高速緩存無效,這樣就可以保證直接從主內存中裝入變量。

(這里,可能有同學就不理解了,因為,他們不明白,高速緩存和內存的區別,這里,如果想要了解的,自行百度。我就簡要說一下:計算機讀取數據的過程,首次讀取,是硬盤讀到內存,然后從內存讀到cpu,但是,后來人們發現這樣太慢了,能不能快一點呢?于是,就從cpu中開辟了一個“空間”,這個空間就叫緩存,把一部分數據(這里是有條件的,條件就不說了)就放到這個緩存中,然后,再次讀取這部分數據時,cpu就直接從緩存讀取了。)

同樣,在對象釋放鎖之前,它會刷新其高速緩存,強制使已做的任何更改都出現在主內存中。

這樣,會保證在同一個鎖上同步的兩個線程看到在synchronized塊內修改的變量的相同值。(加了synchronized之后,所有線程看到這個塊都是同一個塊。)

什么時候必須同步?

要跨線程維護正確的可見性,只要在幾個線程之間共享非 final 變量,就必須使用synchronized(或volatile)以確保一個線程可以看見另一個線程做的更改。

可見性同步的基本規則是在以下情況中必須同步:

讀取上一次可能是由另一個線程寫入的變量

寫入下一次可能由另一個線程讀取的變量

用于一致性的同步

用于一致性的同步

除了用于可見性的同步,從應用程序角度看,您還必須用同步來確保一致性得到了維護。當修改多個相關值時,您想要其它線程原子地看到這組更改 ― 要么看到全部更改,要么什么也看不到。這適用于相關數據項(如粒子的位置和速率)和元數據項(如鏈表中包含的數據值和列表自身中的數據項的鏈)。例子

不變性和 final 字段(這是我最喜歡的一個東西之一,因為他太好了,不需要我做任何處理)

許多 Java 類,包括String、Integer和BigDecimal,都是不可改變的:一旦構造之后,它們的狀態就永遠不會更改。如果某個類的所有字段都被聲明成final,那么這個類就是不可改變的。(實際上,許多不可改變的類都有非 final 字段,用于高速緩存以前計算的方法結果,如String.hashCode(),但調用者看不到這些字段。)

不可改變的類使并發編程變得非常簡單。因為不能更改它們的字段,所以就不需要擔心把狀態的更改從一個線程傳遞到另一個線程。在正確構造了對象之后,可以把它看作是常量。

同樣,final 字段對于線程也更友好。因為 final 字段在初始化之后,它們的值就不能更改,所以當在線程之間共享 final 字段時,不需要擔心同步訪問。

什么時候不需要同步(這是我非常喜歡的東西,理由參考前一節)

在某些情況中,您不必用同步來將數據從一個線程傳遞到另一個,因為 JVM 已經隱含地為您執行同步。這些情況包括:

由靜態初始化器(在靜態字段上或static{}塊中的初始化器)初始化數據時

訪問 final 字段時

在創建線程之前創建對象時(這里,稍微講一個道理,先有了對象,不需要管同步,可以這么理解,沒有人來共享這個資源,自然就不涉及到同步了)

線程可以看見它將要處理的對象時

死鎖(我最討厭的東西,沒有之一)

只要您擁有多個進程,而且它們要爭用對多個鎖的獨占訪問,那么就有可能發生死鎖。如果有一組進程或線程,其中每個都在等待一個只有其它進程或線程才可以執行的操作,那么就稱它們被死鎖了。

最常見的死鎖形式是當線程 1 持有對象 A 上的鎖,而且正在等待與 B 上的鎖,而線程 2 持有對象 B 上的鎖,卻正在等待對象 A 上的鎖。這兩個線程永遠都不會獲得第二個鎖,或者釋放第一個鎖。它們只會永遠等待下去。

要避免死鎖,應該確保在獲取多個鎖時,在所有的線程中都以相同的順序獲取鎖。

是不是,很討厭,就像兩個人爭東西一樣,你不給我,我也不給你,然后,要么一直耗著,要么打一架,打贏了就得到鎖,打輸了,就掛了。其實,避免這個問題,從思路上說,就是注意順序,大家排隊領東西。

性能考慮事項

關于同步的性能代價有許多說法 ― 其中有許多是錯的。同步,尤其是爭用的同步,確實有性能問題,但這些問題并沒有象人們普遍懷疑的那么大。

許多人都使用別出心裁但不起作用的技巧以試圖避免必須使用同步,但最終都陷入了麻煩。一個典型的示例是雙重檢查鎖定模式。這種看似無害的結構據說可以避免公共代碼路徑上的同步,但卻令人費解地失敗了,而且所有試圖修正它的嘗試也失敗了。

在編寫并發代碼時,除非看到性能問題的確鑿證據,否則不要過多考慮性能。瓶頸往往出現在我們最不會懷疑的地方。投機性地優化一個也許最終根本不會成為性能問題的代碼路徑 ― 以程序正確性為代價 ― 是一樁賠本的生意。

所以,簡單來說,在初期,不要考慮什么性能問題。當然也因為,在初期,基本遇不到多少線程同步帶來的性能問題。

同步準則

當編寫synchronized塊時,有幾個簡單的準則可以遵循,這些準則在避免死鎖和性能危險的風險方面大有幫助:

使代碼塊保持簡短。Synchronized塊應該簡短 ― 在保證相關數據操作的完整性的同時,盡量簡短。把不隨線程變化的預處理和后處理移出synchronized塊。

不要阻塞。不要在synchronized塊或方法中調用可能引起阻塞的方法,如InputStream.read()。

在持有鎖的時候,不要對其它對象調用方法。這聽起來可能有些極端,但它消除了最常見的死鎖源頭。

這一部分沒有小結,因為,東西并沒有講完,這時候總結,為時尚早。等以后,有機會再補全這一個部分吧。

其它線程 API 詳細信息

wait()、notify() 和 notifyAll() 方法

除了使用輪詢(它可能消耗大量 CPU 資源,而且具有計時不精確的特征),Object類還包括一些方法,可以讓線程相互通知事件的發生。

Object類定義了wait()、notify()和notifyAll()方法。要執行這些方法,必須擁有相關對象的鎖。

Wait()會讓調用線程休眠,直到用Thread.interrupt()中斷它、過了指定的時間、或者另一個線程用notify()或notifyAll()喚醒它。

當對某個對象調用notify()時,如果有任何線程正在通過wait()等待該對象,那么就會喚醒其中一個線程。當對某個對象調用notifyAll()時,會喚醒所有正在等待該對象的線程。

這些方法是更復雜的鎖定、排隊和并發性代碼的構件。但是,notify()和notifyAll()的使用很復雜。尤其是,使用notify()來代替notifyAll()是有風險的。除非您確實知道正在做什么,否則就使用notifyAll()。

與其使用wait()和notify()來編寫您自己的調度程序、線程池、隊列和鎖,倒不如使用util.concurrent包,這是一個被廣泛使用的開放源碼工具箱,里面都是有用的并發性實用程序。JDK 1.5 將包括java.util.concurrent包;它的許多類都派生自util.concurrent。

隨便說一句,如果對線程不是很熟悉,慎用。

線程優先級

Thread API 讓您可以將執行優先級與每個線程關聯起來。但是,這些優先級如何映射到底層操作系統調度程序取決于實現。在某些實現中,多個 ― 甚至全部 ― 優先級可能被映射成相同的底層操作系統優先級。

在遇到諸如死鎖、資源匱乏或其它意外的調度特征問題時,許多人都想要調整線程優先級。但是,通常這樣只會把問題移到別的地方。大多數程序應該完全避免更改線程優先級。

所以,不要試圖偷懶,用這種方式,去解決線程調度的問題,在程序和做人上,當前問題,就留個當前,不要轉移,不然,后果自負。

線程組

ThreadGroup類原本旨在用于把線程集合構造成組。但是,結果證明ThreadGroup并沒有那樣有用。您最好只使用Thread中的等價方法。

ThreadGroup確實提供了一個有用的功能部件(Thread中目前還沒有):uncaughtException()方法。線程組中的某個線程由于拋出了未捕獲的異常而退出時,會調用ThreadGroup.uncaughtException()方法。這就讓您有機會關閉系統、將一條消息寫到日志文件或者重新啟動失敗的服務。

最后

用了不短不長的時間,整理了這一篇文章,算是對java線程有了一個很基礎的認識。線程這個東西,你單獨用的時候,并沒有什么用,但在整個程序的運行中,他卻先得異常重要。如果,把程序看作是我們的國家或者城市,那么線程就可以看作物流,運算,或者快遞了。這么一說,相信,就算沒接觸過程序的人,也會覺得線程是多么的重要了吧。當然,我這里,只是粗淺的聊了聊,希望以后自己完全弄懂線程之后,再來聊聊線程到底是什么,怎么工作,原理是什么吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,764評論 12 45
  • 本文出自 Eddy Wiki ,轉載請注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,195評論 0 14
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,975評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,476評論 1 15
  • 之前我們講了文件的修改,本次我們將對文件的刪除和誤刪恢復操作進行介紹。 我在本地工作區中新增了幾個文件,我們通過l...
    夜航星osmo閱讀 200評論 0 0