百度java的线程技术_自我提升(基础技术篇)——java线程简介
前言:雖然自己平時都在用多線程,也能完成基本的工作需求,但總覺得,還是對線程沒有一個系統(tǒng)的概念,所以,查閱了一些資料,理解那些大神和官方的資料,寫這么一篇關(guān)于線程的文章
本來想廢話一番,講講自己的經(jīng)歷,不過,還是直接上正題吧。
想要系統(tǒng)的認識一個東西,我們還是得一步步來,從基礎(chǔ)出發(fā),這樣才能比較系統(tǒng)的了解一個東西!
線程基礎(chǔ)什么是線程?
幾乎每種操作系統(tǒng)都支持進程的概念 ―― 進程就是在某種程度上相互隔離的、獨立運行的程序。
線程化是允許多個活動共存于一個進程中的工具。大多數(shù)現(xiàn)代的操作系統(tǒng)都支持線程,而且線程的概念以各種形式已存在了好多年。Java 是第一個在語言本身中顯式地包含線程的主流編程語言,它沒有把線程化看作是底層操作系統(tǒng)的工具。
有時候,線程也稱作輕量級進程。就象進程一樣,線程在程序中是獨立的、并發(fā)的執(zhí)行路徑,每個線程有它自己的堆棧、自己的程序計數(shù)器和自己的局部變量。但是,與分隔的進程相比,進程中的線程之間的隔離程度要小。它們共享內(nèi)存、文件句柄和其它每個進程應(yīng)有的狀態(tài)。
進程可以支持多個線程,它們看似同時執(zhí)行,但互相之間并不同步。一個進程中的多個線程共享相同的內(nèi)存地址空間,這就意味著它們可以訪問相同的變量和對象,而且它們從同一堆中分配對象。盡管這讓線程之間共享信息變得更容易,但您必須小心,確保它們不會妨礙同一進程里的其它線程。(鎖的問題)
Java 線程工具和 API 看似簡單。但是,編寫有效使用線程的復(fù)雜程序并不十分容易。因為有多個線程共存在相同的內(nèi)存空間中并共享相同的變量,所以您必須小心,確保您的線程不會互相干擾。每個 Java(Android) 程序都使用線程
每個 Java 程序都至少有一個線程 ― 主線程。當(dāng)一個 Java 程序啟動時,JVM 會創(chuàng)建主線程,并在該線程中調(diào)用程序的main()方法。
JVM 還創(chuàng)建了其它線程,您通常都看不到它們 ― 例如,與垃圾收集、對象終止和其它 JVM 內(nèi)務(wù)處理任務(wù)相關(guān)的線程。其它工具也創(chuàng)建線程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))或 Swing UI 工具箱、servlet 容器、應(yīng)用程序服務(wù)器和 RMI(遠程方法調(diào)用(Remote Method Invocation))。
每一個Android程序也是如此,而Android程序,默認是UI線程。為什么使用線程?
在 Java (Android)程序中使用線程有許多原因:
* 使 UI 響應(yīng)更快
*利用多處理器系統(tǒng)
*簡化建模
* 執(zhí)行異步或后臺處理響應(yīng)更快的 UI:
事件驅(qū)動的 UI 工具箱(如 AWT 和 Swing)有一個事件線程,它處理 UI 事件,如擊鍵或鼠標(biāo)點擊。
AWT 和 Swing 程序把事件偵聽器與 UI 對象連接。當(dāng)特定事件(如單擊了某個按鈕)發(fā)生時,這些偵聽器會得到通知。事件偵聽器是在 AWT 事件線程中調(diào)用的。
如果事件偵聽器要執(zhí)行持續(xù)很久的任務(wù),如檢查一個大文檔中的拼寫,事件線程將忙于運行拼寫檢查器,所以在完成事件偵聽器之前,就不能處理額外的 UI 事件。這就會使程序看來似乎停滯了,讓用戶不知所措。
要避免使 UI 延遲響應(yīng),事件偵聽器應(yīng)該把較長的任務(wù)放到另一個線程中,這樣 AWT 線程在任務(wù)的執(zhí)行過程中就可以繼續(xù)處理 UI 事件(包括取消正在執(zhí)行的長時間運行任務(wù)的請求)。
Android 的話,舉個最簡答的例子,界面顯示和網(wǎng)絡(luò)請求,界面展示在UI線程,網(wǎng)絡(luò)請求在子線程。網(wǎng)絡(luò)請求是耗時操作,UI線程只負責(zé)界面渲染。就可以讓頁面流暢,等網(wǎng)絡(luò)請求結(jié)束后再刷新界面。這樣,就不會因為網(wǎng)絡(luò)請求而阻塞界面的顯示了。利用多處理器系統(tǒng)
多處理器系統(tǒng)比過去更普及了。以前只能在大型數(shù)據(jù)中心和科學(xué)計算設(shè)施中才能找到它們。現(xiàn)在許多低端服務(wù)器系統(tǒng) ― 甚至是一些臺式機系統(tǒng) ― 都有多個處理器。
現(xiàn)代操作系統(tǒng),包括 Linux、Solaris 和 Windows,Mac都可以利用多個處理器并調(diào)度線程在任何可用的處理器上執(zhí)行。
調(diào)度的基本單位通常是線程;如果某個程序只有一個活動的線程,它一次只能在一個處理器上運行。如果某個程序有多個活動線程,那么可以同時調(diào)度多個線程。在精心設(shè)計的程序中,使用多個線程可以提高程序吞吐量和性能。簡化建模
在某些情況下,使用線程可以使程序編寫和維護起來更簡單。考慮一個仿真應(yīng)用程序,您要在其中模擬多個實體之間的交互作用。給每個實體一個自己的線程可以使許多仿真和對應(yīng)用程序的建模大大簡化。
另一個適合使用單獨線程來簡化程序的示例是在一個應(yīng)用程序有多個獨立的事件驅(qū)動的組件的時候。例如,一個應(yīng)用程序可能有這樣一個組件,該組件在某個事件之后用秒數(shù)倒計時,并更新屏幕顯示。與其讓一個主循環(huán)定期檢查時間并更新顯示,不如讓一個線程什么也不做,一直休眠,直到某一段時間后,更新屏幕上的計數(shù)器,這樣更簡單,而且不容易出錯。這樣,主線程就根本無需擔(dān)心計時器。這個比較抽象,嗯,我試著用更簡單的話來解釋:就舉例快遞吧,單線程就是只有一個快遞員,但是有那么多的包裹要配送,這一個快遞員,就需要去想,我該按什么順序送呢?先送哪里后送哪里才最快等等?這樣這個快遞員就像想很多問題(其實就是linux或者cpu或者程序員要想)。現(xiàn)在多線程,就是我有很多快遞員了,我就指派A快遞員負責(zé)A區(qū)域的快遞,B負責(zé)B區(qū)域的,以此類推。我每個快遞員,就只需要負責(zé)自己那塊區(qū)域,這樣既高效又省事。(當(dāng)然,具體要設(shè)計的cpu的線程調(diào)度問題了,就這么理解著吧)那么主線程的快遞呢,那就是vip了,走專門的路線,到指定的地點。異步或后臺處理
服務(wù)器應(yīng)用程序從遠程來源(如套接字)獲取輸入。當(dāng)讀取套接字時,如果當(dāng)前沒有可用數(shù)據(jù),那么對SocketInputStream.read()的調(diào)用將會阻塞,直到有可用數(shù)據(jù)為止。
如果單線程程序要讀取套接字,而套接字另一端的實體并未發(fā)送任何數(shù)據(jù),那么該程序只會永遠等待,而不執(zhí)行其它處理。相反,程序可以輪詢套接字,查看是否有可用數(shù)據(jù),但通常不會使用這種做法,因為會影響性能。
但是,如果您創(chuàng)建了一個線程來讀取套接字,那么當(dāng)這個線程等待套接字中的輸入時,主線程就可以執(zhí)行其它任務(wù)。您甚至可以創(chuàng)建多個線程,這樣就可以同時讀取多個套接字。這樣,當(dāng)有可用數(shù)據(jù)時,您會迅速得到通知(因為正在等待的線程被喚醒),而不必經(jīng)常輪詢以檢查是否有可用數(shù)據(jù)。使用線程等待套接字的代碼也比輪詢更簡單、更不易出錯。是不是感覺,這樣子,多線程好爽,也好簡單呀,但是,簡單卻有風(fēng)險
雖然 Java 線程工具非常易于使用,但當(dāng)您創(chuàng)建多線程程序時,應(yīng)該盡量避免一些風(fēng)險。
當(dāng)多個線程訪問同一數(shù)據(jù)項(如靜態(tài)字段、可全局訪問對象的實例字段或共享集合)時,需要確保它們協(xié)調(diào)了對數(shù)據(jù)的訪問,這樣它們都可以看到數(shù)據(jù)的一致視圖,而且相互不會干擾另一方的更改。為了實現(xiàn)這個目的,Java 語言提供了兩個關(guān)鍵字:synchronized和volatile。(稍后再詳細講這兩兄弟,因為他們太重要了)
當(dāng)從多個線程中訪問變量時,必須確保對該訪問正確地進行了同步。對于簡單變量,將變量聲明成volatile也許就足夠了,但在大多數(shù)情況下,需要使用同步。
如果您將要使用同步來保護對共享變量的訪問,那么必須確保在程序中所有訪問該變量的地方都使用同步。
同時還需要注意:
雖然線程可以大大簡化許多類型的應(yīng)用程序,過度使用線程可能會危及程序的性能及其可維護性。線程消耗了資源。因此,在不降低性能的情況下,可以創(chuàng)建的線程的數(shù)量是有限制的。
尤其在單處理器系統(tǒng)中,使用多個線程不會使主要消耗 CPU 資源的程序運行得更快。還是舉個代碼例子吧,畢竟是程序員嘛
以下示例使用兩個線程,一個用于計時,一個用于執(zhí)行實際工作。主線程使用非常簡單的算法計算素數(shù)。
在它啟動之前,它創(chuàng)建并啟動一個計時器線程,這個線程會休眠十秒鐘,然后設(shè)置一個主線程要檢查的標(biāo)志。十秒鐘之后,主線程將停止。請注意,共享標(biāo)志被聲明成volatile。
小結(jié)
Java 語言包含了內(nèi)置在語言中的功能強大的線程工具。您可以將線程工具用于:
增加 GUI 應(yīng)用程序的響應(yīng)速度
利用多處理器系統(tǒng)
當(dāng)程序有多個獨立實體時,簡化程序邏輯
在不阻塞整個程序的情況下,執(zhí)行阻塞 I/O
當(dāng)使用多個線程時,必須謹慎,遵循在線程之間共享數(shù)據(jù)的規(guī)則,我們將在共享對數(shù)據(jù)的訪問中討論這些規(guī)則。所有這些規(guī)則歸結(jié)為一條基本原則:不要忘了同步。
線程的生命創(chuàng)建線程
在 Java 程序中創(chuàng)建線程有幾種方法。每個 Java 程序至少包含一個線程:主線程。其它線程都是通過Thread構(gòu)造器或?qū)嵗^承類Thread的類來創(chuàng)建的。
Java 線程可以通過直接實例化Thread對象或?qū)嵗^承Thread的對象來創(chuàng)建其它線程。在線程基礎(chǔ)中的示例(其中,我們在十秒鐘之內(nèi)計算盡量多的素數(shù))中,我們通過實例化CalculatePrimes類型的對象(它繼承了Thread),創(chuàng)建了一個線程。
當(dāng)我們討論 Java 程序中的線程時,也許會提到兩個相關(guān)實體:完成工作的實際線程或代表線程的Thread對象。正在運行的線程通常是由操作系統(tǒng)創(chuàng)建的;Thread對象是由 Java VM 創(chuàng)建的,作為控制相關(guān)線程的一種方式。啟動線程
在一個線程對新線程的Thread對象調(diào)用start()方法之前,這個新線程并沒有真正開始執(zhí)行。Thread對象在其線程真正啟動之前就已經(jīng)存在了,而且其線程退出之后仍然存在。這可以讓您控制或獲取關(guān)于已創(chuàng)建的線程的信息,即使線程還沒有啟動或已經(jīng)完成了。
通常在構(gòu)造器中通過start()啟動線程并不是好主意。這樣做,會把部分構(gòu)造的對象暴露給新的線程。如果對象擁有一個線程,那么它應(yīng)該提供一個啟動該線程的start()或init()方法,而不是從構(gòu)造器中啟動它。結(jié)束線程
線程會以以下三種方式之一結(jié)束:
線程到達其run()方法的末尾。
線程拋出一個未捕獲到的Exception或Error。
另一個線程調(diào)用一個棄用的stop()方法。棄用是指這些方法仍然存在,但是您不應(yīng)該在新代碼中使用它們,并且應(yīng)該盡量從現(xiàn)有代碼中除去它們。
通常用interrupt()方法,這個方法是中斷線程的方法
當(dāng) Java 程序中的所有線程都完成時,程序就退出了。加入線程
Thread API 包含了等待另一個線程完成的方法:join()方法。當(dāng)調(diào)用Thread.join()時,調(diào)用線程將阻塞,直到目標(biāo)線程完成為止。
Thread.join()通常由使用線程的程序使用,以將大問題劃分成許多小問題,每個小問題分配一個線程。嗯,例子后面補充,先講概念。調(diào)度
除了何時使用Thread.join()和Object.wait()外,線程調(diào)度和執(zhí)行的計時是不確定的。如果兩個線程同時運行,而且都不等待,您必須假設(shè)在任何兩個指令之間,其它線程都可以運行并修改程序變量。如果線程要訪問其它線程可以看見的變量,如從靜態(tài)字段(全局變量)直接或間接引用的數(shù)據(jù),則必須使用同步以確保數(shù)據(jù)一致性。(這個大家,自己寫一段程序就可以實驗了)休眠
Thread API 包含了一個sleep()方法,它將使當(dāng)前線程進入等待狀態(tài),直到過了一段指定時間,或者直到另一個線程對當(dāng)前線程的Thread對象調(diào)用了Thread.interrupt(),從而中斷了線程。當(dāng)過了指定時間后,線程又將變成可運行的,并且回到調(diào)度程序的可運行線程隊列中。
如果線程是由對Thread.interrupt()的調(diào)用而中斷的,那么休眠的線程會拋出InterruptedException,這樣線程就知道它是由中斷喚醒的,就不必查看計時器是否過期。
Thread.yield()方法就象Thread.sleep()一樣,但它并不引起休眠,而只是暫停當(dāng)前線程片刻,這樣其它線程就可以運行了。在大多數(shù)實現(xiàn)中,當(dāng)較高優(yōu)先級的線程調(diào)用Thread.yield()時,較低優(yōu)先級的線程就不會運行。
CalculatePrimes示例使用了一個后臺線程計算素數(shù),然后休眠十秒鐘。當(dāng)計時器過期后,它就會設(shè)置一個標(biāo)志,表示已經(jīng)過了十秒。守護程序線程
我們提到過當(dāng) Java 程序的所有線程都完成時,該程序就退出,但這并不完全正確。隱藏的系統(tǒng)線程,如垃圾收集線程和由 JVM 創(chuàng)建的其它線程會怎么樣?我們沒有辦法停止這些線程。如果那些線程正在運行,那么 Java 程序怎么退出呢?
這些系統(tǒng)線程稱作守護程序線程。Java 程序?qū)嶋H上是在它的所有非守護程序線程完成后退出的。
任何線程都可以變成守護程序線程。可以通過調(diào)用Thread.setDaemon()方法來指明某個線程是守護程序線程。您也許想要使用守護程序線程作為在程序中創(chuàng)建的后臺線程,如計時器線程或其它延遲的事件線程,只有當(dāng)其它非守護程序線程正在運行時,這些線程才有用。說了這么多,來個例子:用多個線程分解大任務(wù)
本來想截圖的,但是,不知道為什么,上傳不了,就上代碼吧
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);
}
}小結(jié)
就象程序一樣,線程有生命周期:它們啟動、執(zhí)行,然后完成。一個程序或進程也許包含多個線程,而這些線程看來互相單獨地執(zhí)行。
線程是通過實例化Thread對象或?qū)嵗^承Thread的對象來創(chuàng)建的,但在對新的Thread對象調(diào)用start()方法之前,這個線程并沒有開始執(zhí)行。當(dāng)線程運行到其run()方法的末尾或拋出未經(jīng)處理的異常時,它們就結(jié)束了。
sleep()方法可以用于等待一段特定時間;而join()方法可能用于等到另一個線程完成。
共享對數(shù)據(jù)的訪問線程,我之前有舉例,說線程可以看作快遞員,既然是快遞員,就要送包裹,打包包裹,這個包裹,就是我們說的數(shù)據(jù)。而線程應(yīng)用中,核心也是對數(shù)據(jù)的處理和訪問。共享變量
要使多個線程在一個程序中有用,它們必須有某種方法可以互相通信或共享它們的結(jié)果。
讓線程共享其結(jié)果的最簡單方法是使用共享變量。它們還應(yīng)該使用同步來確保值從一個線程正確傳播到另一個線程,以及防止當(dāng)一個線程正在更新一些相關(guān)數(shù)據(jù)項時,另一個線程看到不一致的中間結(jié)果。
之前在計算素數(shù)的示例使用了一個共享布爾變量,用于表示指定的時間段已經(jīng)過去了。這說明了在線程間共享數(shù)據(jù)最簡單的形式是:輪詢共享變量以查看另一個線程是否已經(jīng)完成執(zhí)行某項任務(wù)。存在于同一個內(nèi)存空間中的所有線程
正如前面討論過的,線程與進程有許多共同點,不同的是線程與同一進程中的其它線程共享相同的進程上下文,包括內(nèi)存。這非常便利,但也有重大責(zé)任。只要訪問共享變量(靜態(tài)或?qū)嵗侄?,線程就可以方便地互相交換數(shù)據(jù),但線程還必須確保它們以受控的方式訪問共享變量,以免它們互相干擾對方的更改。
任何線程可以訪問所有其作用域內(nèi)的變量,就象主線程可以訪問該變量一樣。素數(shù)示例使用了一個公用實例字段,叫做finished,用于表示已經(jīng)過了指定的時間。當(dāng)計時器過期時,一個線程會寫這個字段;另一個線程會定期讀取這個字段,以檢查它是否應(yīng)該停止。注:這個字段被聲明成volatile,這對于這個程序的正確運行非常重要。在本章的后面,我們將看到原因。受控訪問的同步
為了確保可以在線程之間以受控方式共享數(shù)據(jù),Java 語言提供了兩個關(guān)鍵字:synchronized和volatile。
Synchronized有兩個重要含義:它確保了一次只有一個線程可以執(zhí)行代碼的受保護部分(互斥,mutual exclusion 或者說 mutex),而且它確保了一個線程更改的數(shù)據(jù)對于其它線程是可見的(更改的可見性)。
如果沒有同步,數(shù)據(jù)很容易就處于不一致狀態(tài)。例如,如果一個線程正在更新兩個相關(guān)值(比如,粒子的位置和速率),而另一個線程正在讀取這兩個值,有可能在第一個線程只寫了一個值,還沒有寫另一個值的時候,調(diào)度第二個線程運行,這樣它就會看到一個舊值和一個新值。同步讓我們可以定義必須原子地運行的代碼塊,這樣對于其他線程而言,它們要么都執(zhí)行,要么都不執(zhí)行。
同步的原子執(zhí)行或互斥方面類似于其它操作環(huán)境中的臨界段的概念。確保共享數(shù)據(jù)更改的可見性
確保共享數(shù)據(jù)更改的可見性
同步可以讓我們確保線程看到一致的內(nèi)存視圖。
處理器可以使用高速緩存加速對內(nèi)存的訪問(或者編譯器可以將值存儲到寄存器中以便進行更快的訪問)。在一些多處理器體系結(jié)構(gòu)上,如果在一個處理器的高速緩存中修改了內(nèi)存位置,沒有必要讓其它處理器看到這一修改,直到刷新了寫入器的高速緩存并且使讀取器的高速緩存無效。
這表示在這樣的系統(tǒng)上,對于同一變量,在兩個不同處理器上執(zhí)行的兩個線程可能會看到兩個不同的值!這聽起來很嚇人,但它卻很常見。它只是表示在訪問其它線程使用或修改的數(shù)據(jù)時,必須遵循某些規(guī)則。
Volatile比同步更簡單,只適合于控制對基本變量(整數(shù)、布爾變量等)的單個實例的訪問。當(dāng)一個變量被聲明成volatile,任何對該變量的寫操作都會繞過高速緩存,直接寫入主內(nèi)存,而任何對該變量的讀取也都繞過高速緩存,直接取自主內(nèi)存。這表示所有線程在任何時候看到的volatile變量值都相同。
如果沒有正確的同步,線程可能會看到舊的變量值,或者引起其它形式的數(shù)據(jù)損壞。用鎖保護的原子代碼塊
Volatile對于確保每個線程看到最新的變量值非常有用,但有時我們需要保護比較大的代碼片段,如涉及更新多個變量的片段。
同步使用監(jiān)控器(monitor)或鎖的概念,以協(xié)調(diào)對特定代碼塊的訪問。
每個 Java 對象都有一個相關(guān)的鎖。同一時間只能有一個線程持有 Java 鎖。當(dāng)線程進入synchronized代碼塊時,線程會阻塞并等待,直到鎖可用,當(dāng)它可用時,就會獲得這個鎖,然后執(zhí)行代碼塊。當(dāng)控制退出受保護的代碼塊時,即到達了代碼塊末尾或者拋出了沒有在synchronized塊中捕獲的異常時,它就會釋放該鎖。
這樣,每次只有一個線程可以執(zhí)行受給定監(jiān)控器保護的代碼塊。從其它線程的角度看,該代碼塊可以看作是原子的,它要么全部執(zhí)行,要么根本不執(zhí)行。Java 鎖定
Java 鎖定合并了一種互斥形式。每次只有一個線程可以持有鎖。鎖用于保護代碼塊或整個方法,必須記住是鎖的身份保護了代碼塊,而不是代碼塊本身,這一點很重要。一個鎖可以保護許多代碼塊或方法。
反之,僅僅因為代碼塊由鎖保護并不表示兩個線程不能同時執(zhí)行該代碼塊。它只表示如果兩個線程正在等待相同的鎖,則它們不能同時執(zhí)行該代碼。
例子:
這個例子中,兩個線程都能夠執(zhí)行set方法,其實,可以這樣想,就是,你是兩個對象,在執(zhí)行各自的方法。當(dāng)然,其實并不是我說的這樣,只是,以現(xiàn)在的知識不知道該如何解釋,希望懂的朋友可以幫忙解釋一下。同步的方法
創(chuàng)建synchronized塊的最簡單方法是將方法聲明成synchronized。這表示在進入方法主體之前,調(diào)用者必須獲得鎖:
public class Point {
public synchronized void setXY(int x, int y) {
this.x = x;
this.y = y;
}
}
對于普通的synchronized方法,這個鎖是一個對象,將針對它調(diào)用方法。對于靜態(tài)synchronized方法,這個鎖是與Class對象相關(guān)的監(jiān)控器,在該對象中聲明了方法。
僅僅因為setXY()被聲明成synchronized并不表示兩個不同的線程不能同時執(zhí)行setXY(),只要它們調(diào)用不同的Point實例的setXY()就可同時執(zhí)行。對于一個Point實例,一次只能有一個線程執(zhí)行setXY(),或Point的任何其它synchronized方法。同步的塊
synchronized塊的語法比synchronized方法稍微復(fù)雜一點,因為還需要顯式地指定鎖要保護哪個塊。Point的以下版本等價于前一頁中顯示的版本:
public class Point {
public void setXY(int x, int y) {
synchronized (this) {
this.x = x;
this.y = y;
}
}
}
使用this引用作為鎖很常見,但這并不是必需的。這表示該代碼塊將與這個類中的synchronized方法使用同一個鎖。
由于同步防止了多個線程同時執(zhí)行一個代碼塊,因此性能上就有問題,即使是在單處理器系統(tǒng)上。最好在盡可能最小的需要保護的代碼塊上使用同步。
訪問局部(基于堆棧的)變量從來不需要受到保護,因為它們只能被自己所屬的線程訪問。大多數(shù)類并沒有同步
因為同步會帶來小小的性能損失,大多數(shù)通用類,如java.util中的 Collection 類,不在內(nèi)部使用同步。這表示在沒有附加同步的情況下,不能在多個線程中使用諸如HashMap這樣的類。
通過每次訪問共享集合中的方法時使用同步,可以在多線程應(yīng)用程序中使用 Collection 類。對于任何給定的集合,每次必須用同一個鎖進行同步。通常可以選擇集合對象本身作為鎖。
下一頁中的示例類SimpleCache顯示了如何使用HashMap以線程安全的方式提供高速緩存。但是,通常適當(dāng)?shù)耐讲⒉恢皇且馕吨矫總€方法。
Collections類提供了一組便利的用于List、Map和Set接口的封裝器。您可以用Collections.synchronizedMap封裝Map,它將確保所有對該映射的訪問都被正確同步。
如果類的文檔沒有說明它是線程安全的,那么您必須假設(shè)它不是。
如以下代碼樣本所示,SimpleCache.java使用HashMap為對象裝入器提供了一個簡單的高速緩存。load()方法知道怎樣按對象的鍵裝入對象。在一次裝入對象之后,該對象就被存儲到高速緩存中,這樣以后的訪問就會從高速緩存中檢索它,而不是每次都全部地裝入它。對共享高速緩存的每個訪問都受到synchronized塊保護。由于它被正確同步,所以多個線程可以同時調(diào)用getObject和clearCache方法,而沒有數(shù)據(jù)損壞的風(fēng)險。
小結(jié)
由于線程執(zhí)行的計時是不確定的,我們需要小心,以控制線程對共享數(shù)據(jù)的訪問。否則,多個并發(fā)線程會互相干擾對方的更改,從而損壞數(shù)據(jù),或者其它線程也許不能及時看到對共享數(shù)據(jù)的更改。
通過使用同步來保護對共享變量的訪問,我們可以確保線程以可預(yù)料的方式與程序變量進行交互。
每個 Java 對象都可以充當(dāng)鎖,synchronized塊可以確保一次只有一個線程執(zhí)行由給定鎖保護的synchronized代碼。
未完待續(xù)。。。。。
繼續(xù)上一次的接著說,本來想一次寫完的,但是,比較懶吧,也想放松一下,周末,就休息了。
今天就補全剩下的一部分吧
同步詳細信息
(這一部分,相對前面要難一點,做好心理準(zhǔn)備,可能需要多看幾次)互斥
在共享對數(shù)據(jù)的訪問中,我們討論了synchronized塊的特征,并在實現(xiàn)典型互斥鎖(即,互斥或臨界段)時說明了它們,其中每次只有一個線程可以執(zhí)行受給定鎖保護的代碼塊。(換句話說,就是互斥,其實就是相互排斥的意思,只有一個線程可以持有,不能存在多個線程都持有,這里的持有可以理解為使用權(quán)限,舉個例子,你打電話時,只能和一個人保持通話。)
互斥是同步所做工作的重要部分,但同步還有其它幾種特征,這些特征對于在多處理器系統(tǒng)上取得正確結(jié)果非常重要。可見性
除了互斥,同步(如volatile)強制某些可見性約束。當(dāng)對象獲取鎖時,它首先使自己的高速緩存無效,這樣就可以保證直接從主內(nèi)存中裝入變量。
(這里,可能有同學(xué)就不理解了,因為,他們不明白,高速緩存和內(nèi)存的區(qū)別,這里,如果想要了解的,自行百度。我就簡要說一下:計算機讀取數(shù)據(jù)的過程,首次讀取,是硬盤讀到內(nèi)存,然后從內(nèi)存讀到cpu,但是,后來人們發(fā)現(xiàn)這樣太慢了,能不能快一點呢?于是,就從cpu中開辟了一個“空間”,這個空間就叫緩存,把一部分數(shù)據(jù)(這里是有條件的,條件就不說了)就放到這個緩存中,然后,再次讀取這部分數(shù)據(jù)時,cpu就直接從緩存讀取了。)
同樣,在對象釋放鎖之前,它會刷新其高速緩存,強制使已做的任何更改都出現(xiàn)在主內(nèi)存中。
這樣,會保證在同一個鎖上同步的兩個線程看到在synchronized塊內(nèi)修改的變量的相同值。(加了synchronized之后,所有線程看到這個塊都是同一個塊。)什么時候必須同步?
要跨線程維護正確的可見性,只要在幾個線程之間共享非 final 變量,就必須使用synchronized(或volatile)以確保一個線程可以看見另一個線程做的更改。
可見性同步的基本規(guī)則是在以下情況中必須同步:
讀取上一次可能是由另一個線程寫入的變量
寫入下一次可能由另一個線程讀取的變量用于一致性的同步
用于一致性的同步
除了用于可見性的同步,從應(yīng)用程序角度看,您還必須用同步來確保一致性得到了維護。當(dāng)修改多個相關(guān)值時,您想要其它線程原子地看到這組更改 ― 要么看到全部更改,要么什么也看不到。這適用于相關(guān)數(shù)據(jù)項(如粒子的位置和速率)和元數(shù)據(jù)項(如鏈表中包含的數(shù)據(jù)值和列表自身中的數(shù)據(jù)項的鏈)。例子不變性和 final 字段(這是我最喜歡的一個東西之一,因為他太好了,不需要我做任何處理)
許多 Java 類,包括String、Integer和BigDecimal,都是不可改變的:一旦構(gòu)造之后,它們的狀態(tài)就永遠不會更改。如果某個類的所有字段都被聲明成final,那么這個類就是不可改變的。(實際上,許多不可改變的類都有非 final 字段,用于高速緩存以前計算的方法結(jié)果,如String.hashCode(),但調(diào)用者看不到這些字段。)
不可改變的類使并發(fā)編程變得非常簡單。因為不能更改它們的字段,所以就不需要擔(dān)心把狀態(tài)的更改從一個線程傳遞到另一個線程。在正確構(gòu)造了對象之后,可以把它看作是常量。
同樣,final 字段對于線程也更友好。因為 final 字段在初始化之后,它們的值就不能更改,所以當(dāng)在線程之間共享 final 字段時,不需要擔(dān)心同步訪問。什么時候不需要同步(這是我非常喜歡的東西,理由參考前一節(jié))
在某些情況中,您不必用同步來將數(shù)據(jù)從一個線程傳遞到另一個,因為 JVM 已經(jīng)隱含地為您執(zhí)行同步。這些情況包括:
由靜態(tài)初始化器(在靜態(tài)字段上或static{}塊中的初始化器)初始化數(shù)據(jù)時
訪問 final 字段時
在創(chuàng)建線程之前創(chuàng)建對象時(這里,稍微講一個道理,先有了對象,不需要管同步,可以這么理解,沒有人來共享這個資源,自然就不涉及到同步了)
線程可以看見它將要處理的對象時死鎖(我最討厭的東西,沒有之一)
只要您擁有多個進程,而且它們要爭用對多個鎖的獨占訪問,那么就有可能發(fā)生死鎖。如果有一組進程或線程,其中每個都在等待一個只有其它進程或線程才可以執(zhí)行的操作,那么就稱它們被死鎖了。
最常見的死鎖形式是當(dāng)線程 1 持有對象 A 上的鎖,而且正在等待與 B 上的鎖,而線程 2 持有對象 B 上的鎖,卻正在等待對象 A 上的鎖。這兩個線程永遠都不會獲得第二個鎖,或者釋放第一個鎖。它們只會永遠等待下去。
要避免死鎖,應(yīng)該確保在獲取多個鎖時,在所有的線程中都以相同的順序獲取鎖。
是不是,很討厭,就像兩個人爭東西一樣,你不給我,我也不給你,然后,要么一直耗著,要么打一架,打贏了就得到鎖,打輸了,就掛了。其實,避免這個問題,從思路上說,就是注意順序,大家排隊領(lǐng)東西。性能考慮事項
關(guān)于同步的性能代價有許多說法 ― 其中有許多是錯的。同步,尤其是爭用的同步,確實有性能問題,但這些問題并沒有象人們普遍懷疑的那么大。
許多人都使用別出心裁但不起作用的技巧以試圖避免必須使用同步,但最終都陷入了麻煩。一個典型的示例是雙重檢查鎖定模式。這種看似無害的結(jié)構(gòu)據(jù)說可以避免公共代碼路徑上的同步,但卻令人費解地失敗了,而且所有試圖修正它的嘗試也失敗了。
在編寫并發(fā)代碼時,除非看到性能問題的確鑿證據(jù),否則不要過多考慮性能。瓶頸往往出現(xiàn)在我們最不會懷疑的地方。投機性地優(yōu)化一個也許最終根本不會成為性能問題的代碼路徑 ― 以程序正確性為代價 ― 是一樁賠本的生意。
所以,簡單來說,在初期,不要考慮什么性能問題。當(dāng)然也因為,在初期,基本遇不到多少線程同步帶來的性能問題。同步準(zhǔn)則
當(dāng)編寫synchronized塊時,有幾個簡單的準(zhǔn)則可以遵循,這些準(zhǔn)則在避免死鎖和性能危險的風(fēng)險方面大有幫助:
使代碼塊保持簡短。Synchronized塊應(yīng)該簡短 ― 在保證相關(guān)數(shù)據(jù)操作的完整性的同時,盡量簡短。把不隨線程變化的預(yù)處理和后處理移出synchronized塊。
不要阻塞。不要在synchronized塊或方法中調(diào)用可能引起阻塞的方法,如InputStream.read()。
在持有鎖的時候,不要對其它對象調(diào)用方法。這聽起來可能有些極端,但它消除了最常見的死鎖源頭。
這一部分沒有小結(jié),因為,東西并沒有講完,這時候總結(jié),為時尚早。等以后,有機會再補全這一個部分吧。
其它線程 API 詳細信息wait()、notify() 和 notifyAll() 方法
除了使用輪詢(它可能消耗大量 CPU 資源,而且具有計時不精確的特征),Object類還包括一些方法,可以讓線程相互通知事件的發(fā)生。
Object類定義了wait()、notify()和notifyAll()方法。要執(zhí)行這些方法,必須擁有相關(guān)對象的鎖。
Wait()會讓調(diào)用線程休眠,直到用Thread.interrupt()中斷它、過了指定的時間、或者另一個線程用notify()或notifyAll()喚醒它。
當(dāng)對某個對象調(diào)用notify()時,如果有任何線程正在通過wait()等待該對象,那么就會喚醒其中一個線程。當(dāng)對某個對象調(diào)用notifyAll()時,會喚醒所有正在等待該對象的線程。
這些方法是更復(fù)雜的鎖定、排隊和并發(fā)性代碼的構(gòu)件。但是,notify()和notifyAll()的使用很復(fù)雜。尤其是,使用notify()來代替notifyAll()是有風(fēng)險的。除非您確實知道正在做什么,否則就使用notifyAll()。
與其使用wait()和notify()來編寫您自己的調(diào)度程序、線程池、隊列和鎖,倒不如使用util.concurrent包,這是一個被廣泛使用的開放源碼工具箱,里面都是有用的并發(fā)性實用程序。JDK 1.5 將包括java.util.concurrent包;它的許多類都派生自util.concurrent。
隨便說一句,如果對線程不是很熟悉,慎用。線程優(yōu)先級
Thread API 讓您可以將執(zhí)行優(yōu)先級與每個線程關(guān)聯(lián)起來。但是,這些優(yōu)先級如何映射到底層操作系統(tǒng)調(diào)度程序取決于實現(xiàn)。在某些實現(xiàn)中,多個 ― 甚至全部 ― 優(yōu)先級可能被映射成相同的底層操作系統(tǒng)優(yōu)先級。
在遇到諸如死鎖、資源匱乏或其它意外的調(diào)度特征問題時,許多人都想要調(diào)整線程優(yōu)先級。但是,通常這樣只會把問題移到別的地方。大多數(shù)程序應(yīng)該完全避免更改線程優(yōu)先級。
所以,不要試圖偷懶,用這種方式,去解決線程調(diào)度的問題,在程序和做人上,當(dāng)前問題,就留個當(dāng)前,不要轉(zhuǎn)移,不然,后果自負。線程組
ThreadGroup類原本旨在用于把線程集合構(gòu)造成組。但是,結(jié)果證明ThreadGroup并沒有那樣有用。您最好只使用Thread中的等價方法。
ThreadGroup確實提供了一個有用的功能部件(Thread中目前還沒有):uncaughtException()方法。線程組中的某個線程由于拋出了未捕獲的異常而退出時,會調(diào)用ThreadGroup.uncaughtException()方法。這就讓您有機會關(guān)閉系統(tǒng)、將一條消息寫到日志文件或者重新啟動失敗的服務(wù)。
最后用了不短不長的時間,整理了這一篇文章,算是對java線程有了一個很基礎(chǔ)的認識。線程這個東西,你單獨用的時候,并沒有什么用,但在整個程序的運行中,他卻先得異常重要。如果,把程序看作是我們的國家或者城市,那么線程就可以看作物流,運算,或者快遞了。這么一說,相信,就算沒接觸過程序的人,也會覺得線程是多么的重要了吧。當(dāng)然,我這里,只是粗淺的聊了聊,希望以后自己完全弄懂線程之后,再來聊聊線程到底是什么,怎么工作,原理是什么吧。
總結(jié)
以上是生活随笔為你收集整理的百度java的线程技术_自我提升(基础技术篇)——java线程简介的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jQuery插件scrollToTop
- 下一篇: linux嵌入式智能家居环境监测系统的设