Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 關於Android多線程的相關知識點詳解

關於Android多線程的相關知識點詳解

編輯:關於Android編程

線程同步 什麼是線程同步? 當使用多個線程來訪問同一個數據時,非常容易出現線程安全問題(比如多個線程都在操作同一數據導致數據不一致),所以我們用同步機制來解決這些問題。
實現同步機制有兩個方法: 1。同步代碼塊: synchronized(同一個數據){} 同一個數據:就是N條線程同時訪問一個數據。 2。 同步方法: public synchronized 數據返回類型 方法名(){} 就是使用 synchronized 來修飾某個方法,則該方法稱為同步方法。對於同步方法而言,無需顯示指定同步監視器,同步方法的同步監視器是 this 也就是該對象的本身(這裡指的對象本身有點含糊,其實就是調用該同步方法的對象)通過使用同步方法,可非常方便的將某類變成線程安全的類,具有如下特征: 1,該類的對象可以被多個線程安全的訪問。 2,每個線程調用該對象的任意方法之後,都將得到正確的結果。 3,每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。 注:synchronized關鍵字可以修飾方法,也可以修飾代碼塊,但不能修飾構造器,屬性等。
實現同步機制注意以下幾點:   安全性高,性能低,在多線程用。性能高,安全性低,在單線程用。 1,不要對線程安全類的所有方法都進行同步,只對那些會改變共享資源方法的進行同步。 2,如果可變類有兩種運行環境,當線程環境和多線程環境則應該為該可變類提供兩種版本:線程安全版本和線程不安全版本(沒有同步方法和同步塊)。在單線程中環境中,使用線程不安全版本以保證性能,在多線程中使用線程安全版本.
線程通訊: 為什麼要使用線程通訊? 當使用synchronized 來修飾某個共享資源時(分同步代碼塊和同步方法兩種情況),當某個線程獲得共享資源的鎖後就可以執行相應的代碼段,直到該線程運行完該代碼段後才釋放對該共享資源的鎖,讓其他線程有機會執行對該共享資源的修改。當某個線程占有某個共享資源的鎖時,如果另外一個線程也想獲得這把鎖運行就需要使用wait() 和notify()/notifyAll()方法來進行線程通訊了。 Java.lang.object 裡的三個方法wait() notify()  notifyAll() wait方法導致當前線程等待,直到其他線程調用同步監視器的notify方法或notifyAll方法來喚醒該線程。 wait(mills)方法 都是等待指定時間後自動蘇醒,調用wait方法的當前線程會釋放該同步監視器的鎖定,可以不用notify或notifyAll方法把它喚醒。
notify() 喚醒在同步監視器上等待的單個線程,如果所有線程都在同步監視器上等待,則會選擇喚醒其中一個線程,選擇是任意性的,只有當前線程放棄對該同步監視器的鎖定後,也就是使用wait方法後,才可以執行被喚醒的線程。
notifyAll()方法 喚醒在同步監視器上等待的所有的線程。只用當前線程放棄對該同步監視器的鎖定後,才可以執行被喚醒的線程。 ————————————————-另外的總結2——————————————–

線程的同步 原子操作:根據Java規范,對於基本類型的賦值或者返回值操作,是原子操作。但這裡的基本數據類型不包括long和double, 因為JVM看到的基本存儲單位是32位,而long 和double都要用64位來表示。所以無法在一個時鐘周期內完成。
自增操作(++)不是原子操作,因為它涉及到一次讀和一次寫。
原子操作:由一組相關的操作完成,這些操作可能會操縱與其它的線程共享的資源,為了保證得到正確的運算結果,一個線程在執行原子操作其間,應該采取其他的措施使得其他的線程不能操縱共享資源。
同步代碼塊:為了保證每個線程能夠正常執行原子操作,Java引入了同步機制,具體的做法是在代表原子操作的程序代碼前加上synchronized標記,這樣的代碼被稱為同步代碼塊。
同步鎖:每個JAVA對象都有且只有一個同步鎖,在任何時刻,最多只允許一個線程擁有這把鎖。
當一個線程試圖訪問帶有synchronized(this)標記的代碼塊時,必須獲得 this關鍵字引用的對象的鎖,在以下的兩種情況下,本線程有著不同的命運。 1、 假如這個鎖已經被其它的線程占用,JVM就會把這個線程放到本對象的鎖池中。本線程進入阻塞狀態。鎖池中可能有很多的線程,等到其他的線程釋放了鎖,JVM就會從鎖池中隨機取出一個線程,使這個線程擁有鎖,並且轉到就緒狀態。 2、 假如這個鎖沒有被其他線程占用,本線程會獲得這把鎖,開始執行同步代碼塊。 (一般情況下在執行同步代碼塊時不會釋放同步鎖,但也有特殊情況會釋放對象鎖 如在執行同步代碼塊時,遇到異常而導致線程終止,鎖會被釋放;在執行代碼塊時,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池中)
線程同步的特征: 1、 如果一個同步代碼塊和非同步代碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因為當一個線程執行一個對象的同步代碼塊時,其他的線程仍然可以執行對象的非同步代碼塊。(所謂的線程之間保持同步,是指不同的線程在執行同一個對象的同步代碼塊時,因為要獲得對象的同步鎖而互相牽制) 2、 每個對象都有唯一的同步鎖 3、 在靜態方法前面可以使用synchronized修飾符。 4、 當一個線程開始執行同步代碼塊時,並不意味著必須以不間斷的方式運行,進入同步代碼塊的線程可以執行Thread.sleep()或執行Thread.yield()方法,此時它並不釋放對象鎖,只是把運行的機會讓給其他的線程。 5、 Synchronized聲明不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。
線程安全的類: 1、 這個類的對象可以同時被多個線程安全的訪問。 2、 每個線程都能正常的執行原子操作,得到正確的結果。 3、 在每個線程的原子操作都完成後,對象處於邏輯上合理的狀態。
釋放對象的鎖: 1、 執行完同步代碼塊就會釋放對象的鎖 2、 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放 3、 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池。
死鎖 當一個線程等待由另一個線程持有的鎖,而後者正在等待已被第一個線程持有的鎖時,就會發生死鎖。JVM不監測也不試圖避免這種情況,因此保證不發生死鎖就成了程序員的責任。
如何避免死鎖 一個通用的經驗法則是:當幾個線程都要訪問共享資源A、B、C 時,保證每個線程都按照同樣的順序去訪問他們。
線程通信 Java.lang.Object類中提供了兩個用於線程通信的方法 1、 wait():執行了該方法的線程釋放對象的鎖,JVM會把該線程放到對象的等待池中。該線程等待其它線程喚醒 2、 notify():執行該方法的線程喚醒在對象的等待池中等待的一個線程,JVM從對象的等待池中隨機選擇一個線程,把它轉到對象的鎖池中。

—————————————————-線程同步的總結3————————————— 我們可以在計算機上運行各種計算機軟件程序。每一個運行的程序可能包括多個獨立運行的線程(Thread)。 線程(Thread)是一份獨立運行的程序,有自己專用的運行棧。線程有可能和其他線程共享一些資源,比如,內存,文件,數據庫等。 當多個線程同時讀寫同一份共享資源的時候,可能會引起沖突。這時候,我們需要引入線程“同步”機制,即各位線程之間要有個先來後到,不能一窩蜂擠上去搶作一團。 同步這個詞是從英文synchronize(使同時發生)翻譯過來的。我也不明白為什麼要用這個很容易引起誤解的詞。既然大家都這麼用,咱們也就只好這麼將就。 線程同步的真實意思和字面意思恰好相反。線程同步的真實意思,其實是“排隊”:幾個線程之間要排隊,一個一個對共享資源進行操作,而不是同時進行操作。
因此,關於線程同步,需要牢牢記住的第一點是:線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程“同步”執行。這可真是個無聊的繞口令。 關於線程同步,需要牢牢記住的第二點是 “共享”這兩個字。只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那麼就根本沒有同步的必要。 關於線程同步,需要牢牢記住的第三點是,只有“變量”才需要同步訪問。如果共享的資源是固定不變的,那麼就相當於“常量”,線程同時讀取常量也不需要同步。至少一個線程修改共享資源,這樣的情況下,線程之間就需要同步。 關於線程同步,需要牢牢記住的第四點是:多個線程訪問共享資源的代碼有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。
為了加深理解,下面舉幾個例子。 有兩個采購員,他們的工作內容是相同的,都是遵循如下的步驟: (1)到市場上去,尋找並購買有潛力的樣品。 (2)回到公司,寫報告。 這兩個人的工作內容雖然一樣,他們都需要購買樣品,他們可能買到同樣種類的樣品,但是他們絕對不會購買到同一件樣品,他們之間沒有任何共享資源。所以,他們可以各自進行自己的工作,互不干擾。 這兩個采購員就相當於兩個線程;兩個采購員遵循相同的工作步驟,相當於這兩個線程執行同一段代碼。
下面給這兩個采購員增加一個工作步驟。采購員需要根據公司的“布告欄”上面公布的信息,安排自己的工作計劃。 這兩個采購員有可能同時走到布告欄的前面,同時觀看布告欄上的信息。這一點問題都沒有。因為布告欄是只讀的,這兩個采購員誰都不會去修改布告欄上寫的信息。
下面增加一個角色。一個辦公室行政人員這個時候,也走到了布告欄前面,准備修改布告欄上的信息。 如果行政人員先到達布告欄,並且正在修改布告欄的內容。兩個采購員這個時候,恰好也到了。這兩個采購員就必須等待行政人員完成修改之後,才能觀看修改後的信息。 如果行政人員到達的時候,兩個采購員已經在觀看布告欄了。那麼行政人員需要等待兩個采購員把當前信息記錄下來之後,才能夠寫上新的信息。 上述這兩種情況,行政人員和采購員對布告欄的訪問就需要進行同步。因為其中一個線程(行政人員)修改了共享資源(布告欄)。而且我們可以看到,行政人員的工作流程和采購員的工作流程(執行代碼)完全不同,但是由於他們訪問了同一份可變共享資源(布告欄),所以他們之間需要同步。
同步鎖
前面講了為什麼要線程同步,下面我們就來看如何才能線程同步。 線程同步的基本實現思路還是比較容易理解的。我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個線程獲取了這把鑰匙,才有權利訪問該共享資源。 生活中,我們也可能會遇到這樣的例子。一些超市的外面提供了一些自動儲物箱。每個儲物箱都有一把鎖,一把鑰匙。人們可以使用那些帶有鑰匙的儲物箱,把東西放到儲物箱裡面,把儲物箱鎖上,然後把鑰匙拿走。這樣,該儲物箱就被鎖住了,其他人不能再訪問這個儲物箱。(當然,真實的儲物箱鑰匙是可以被人拿走復制的,所以不要把貴重物品放在超市的儲物箱裡面。於是很多超市都采用了電子密碼鎖。) 線程同步鎖這個模型看起來很直觀。但是,還有一個嚴峻的問題沒有解決,這個同步鎖應該加在哪裡? 當然是加在共享資源上了。反應快的讀者一定會搶先回答。 沒錯,如果可能,我們當然盡量把同步鎖加在共享資源上。一些比較完善的共享資源,比如,文件系統,數據庫系統等,自身都提供了比較完善的同步鎖機制。我們不用另外給這些資源加鎖,這些資源自己就有鎖。 但是,大部分情況下,我們在代碼中訪問的共享資源都是比較簡單的共享對象。這些對象裡面沒有地方讓我們加鎖。 讀者可能會提出建議:為什麼不在每一個對象內部都增加一個新的區域,專門用來加鎖呢?這種設計理論上當然也是可行的。問題在於,線程同步的情況並不是很普遍。如果因為這小概率事件,在所有對象內部都開辟一塊鎖空間,將會帶來極大的空間浪費。得不償失。 於是,現代的編程語言的設計思路都是把同步鎖加在代碼段上。確切的說,是把同步鎖加在“訪問共享資源的代碼段”上。這一點一定要記住,同步鎖是加在代碼段上的。 同步鎖加在代碼段上,就很好地解決了上述的空間浪費問題。但是卻增加了模型的復雜度,也增加了我們的理解難度。 現在我們就來仔細分析“同步鎖加在代碼段上”的線程同步模型。 首先,我們已經解決了同步鎖加在哪裡的問題。我們已經確定,同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。 其次,我們要解決的問題是,我們應該在代碼段上加什麼樣的鎖。這個問題是重點中的重點。這是我們尤其要注意的問題:訪問同一份共享資源的不同代碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那麼根本就起不到同步的作用,沒有任何意義。 這就是說,同步鎖本身也一定是多個線程之間的共享對象。
Java語言的synchronized關鍵字
為了加深理解,舉幾個代碼段同步的例子。 不同語言的同步鎖模型都是一樣的。只是表達方式有些不同。這裡我們以當前最流行的Java語言為例。Java語言裡面用synchronized關鍵字給代碼段加鎖。整個語法形式表現為 synchronized(同步鎖) {   // 訪問共享資源,需要同步的代碼段 }
這裡尤其要注意的就是,同步鎖本身一定要是共享的對象。
… f1() {
Object lock1 = new Object(); // 產生一個同步鎖
synchronized(lock1){   // 代碼段 A // 訪問共享資源 resource1 // 需要同步 } }
上面這段代碼沒有任何意義。因為那個同步鎖是在函數體內部產生的。每個線程調用這段代碼的時候,都會產生一個新的同步鎖。那麼多個線程之間,使用的是不同的同步鎖。根本達不到同步的目的。 同步代碼一定要寫成如下的形式,才有意義。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 A // 訪問共享資源 resource1 // 需要同步 }
你不一定要把同步鎖聲明為static或者public,但是你一定要保證相關的同步代碼之間,一定要使用同一個同步鎖。 講到這裡,你一定會好奇,這個同步鎖到底是個什麼東西。為什麼隨便聲明一個Object對象,就可以作為同步鎖? 在Java裡面,同步鎖的概念就是這樣的。任何一個Object Reference都可以作為同步鎖。我們可以把Object Reference理解為對象在內存分配系統中的內存地址。因此,要保證同步代碼段之間使用的是同一個同步鎖,我們就要保證這些同步代碼段的synchronized關鍵字使用的是同一個Object Reference,同一個內存地址。這也是為什麼我在前面的代碼中聲明lock1的時候,使用了final關鍵字,這就是為了保證lock1的Object Reference在整個系統運行過程中都保持不變。 一些求知欲強的讀者可能想要繼續深入了解synchronzied(同步鎖)的實際運行機制。Java虛擬機規范中(你可以在google用“JVM Spec”等關鍵字進行搜索),有對synchronized關鍵字的詳細解釋。synchronized會編譯成 monitor enter, … monitor exit之類的指令對。Monitor就是實際上的同步鎖。每一個Object Reference在概念上都對應一個monitor。 這些實現細節問題,並不是理解同步鎖模型的關鍵。我們繼續看幾個例子,加深對同步鎖模型的理解。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 A // 訪問共享資源 resource1 // 需要同步 } }
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 B // 訪問共享資源 resource1 // 需要同步 } }
上述的代碼中,代碼段A和代碼段B就是同步的。因為它們使用的是同一個同步鎖lock1。 如果有10個線程同時執行代碼段A,同時還有20個線程同時執行代碼段B,那麼這30個線程之間都是要進行同步的。 這30個線程都要競爭一個同步鎖lock1。同一時刻,只有一個線程能夠獲得lock1的所有權,只有一個線程可以執行代碼段A或者代碼段B。其他競爭失敗的線程只能暫停運行,進入到該同步鎖的就緒(Ready)隊列。 每一個同步鎖下面都掛了幾個線程隊列,包括就緒(Ready)隊列,待召(Waiting)隊列等。比如,lock1對應的就緒隊列就可以叫做lock1 – ready queue。每個隊列裡面都可能有多個暫停運行的線程。 注意,競爭同步鎖失敗的線程進入的是該同步鎖的就緒(Ready)隊列,而不是後面要講述的待召隊列(Waiting Queue,也可以翻譯為等待隊列)。就緒隊列裡面的線程總是時刻准備著競爭同步鎖,時刻准備著運行。而待召隊列裡面的線程則只能一直等待,直到等到某個信號的通知之後,才能夠轉移到就緒隊列中,准備運行。 成功獲取同步鎖的線程,執行完同步代碼段之後,會釋放同步鎖。該同步鎖的就緒隊列中的其他線程就繼續下一輪同步鎖的競爭。成功者就可以繼續運行,失敗者還是要乖乖地待在就緒隊列中。 因此,線程同步是非常耗費資源的一種操作。我們要盡量控制線程同步的代碼段范圍。同步的代碼段范圍越小越好。我們用一個名詞“同步粒度”來表示同步代碼段的范圍。 同步粒度 在Java語言裡面,我們可以直接把synchronized關鍵字直接加在函數的定義上。 比如。 … synchronized … f1() {   // f1 代碼段 }
這段代碼就等價於 … f1() {   synchronized(this){ // 同步鎖就是對象本身     // f1 代碼段   } }
同樣的原則適用於靜態(static)函數 比如。 … static synchronized … f1() {   // f1 代碼段 }
這段代碼就等價於 …static … f1() {   synchronized(Class.forName(…)){ // 同步鎖是類定義本身     // f1 代碼段   } }
但是,我們要盡量避免這種直接把synchronized加在函數定義上的偷懶做法。因為我們要控制同步粒度。同步的代碼段越小越好。synchronized控制的范圍越小越好。 我們不僅要在縮小同步代碼段的長度上下功夫,我們同時還要注意細分同步鎖。 比如,下面的代碼
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 A // 訪問共享資源 resource1 // 需要同步 } }
… f2() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 B // 訪問共享資源 resource1 // 需要同步 } }
… f3() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 C // 訪問共享資源 resource2 // 需要同步 } }
… f4() {
synchronized(lock1){ // lock1 是公用同步鎖   // 代碼段 D // 訪問共享資源 resource2 // 需要同步 } }
上述的4段同步代碼,使用同一個同步鎖lock1。所有調用4段代碼中任何一段代碼的線程,都需要競爭同一個同步鎖lock1。 我們仔細分析一下,發現這是沒有必要的。 因為f1()的代碼段A和f2()的代碼段B訪問的共享資源是resource1,f3()的代碼段C和f4()的代碼段D訪問的共享資源是resource2,它們沒有必要都競爭同一個同步鎖lock1。我們可以增加一個同步鎖lock2。f3()和f4()的代碼可以修改為: public static final Object lock2 = new Object();
… f3() {
synchronized(lock2){ // lock2 是公用同步鎖   // 代碼段 C // 訪問共享資源 resource2 // 需要同步 } }
… f4() {
synchronized(lock2){ // lock2 是公用同步鎖   // 代碼段 D // 訪問共享資源 resource2 // 需要同步 } }
這樣,f1()和f2()就會競爭lock1,而f3()和f4()就會競爭lock2。這樣,分開來分別競爭兩個鎖,就可以大大較少同步鎖競爭的概率,從而減少系統的開銷。
信號量
同步鎖模型只是最簡單的同步模型。同一時刻,只有一個線程能夠運行同步代碼。 有的時候,我們希望處理更加復雜的同步模型,比如生產者/消費者模型、讀寫同步模型等。這種情況下,同步鎖模型就不夠用了。我們需要一個新的模型。這就是我們要講述的信號量模型。 信號量模型的工作方式如下:線程在運行的過程中,可以主動停下來,等待某個信號量的通知;這時候,該線程就進入到該信號量的待召(Waiting)隊列當中;等到通知之後,再繼續運行。 很多語言裡面,同步鎖都由專門的對象表示,對象名通常叫Monitor。 同樣,在很多語言中,信號量通常也有專門的對象名來表示,比如,Mutex,Semphore。 信號量模型要比同步鎖模型復雜許多。一些系統中,信號量甚至可以跨進程進行同步。另外一些信號量甚至還有計數功能,能夠控制同時運行的線程數。 我們沒有必要考慮那麼復雜的模型。所有那些復雜的模型,都是最基本的模型衍生出來的。只要掌握了最基本的信號量模型——“等待/通知”模型,復雜模型也就迎刃而解了。 我們還是以Java語言為例。Java語言裡面的同步鎖和信號量概念都非常模糊,沒有專門的對象名詞來表示同步鎖和信號量,只有兩個同步鎖相關的關鍵字——volatile和synchronized。 這種模糊雖然導致概念不清,但同時也避免了Monitor、Mutex、Semphore等名詞帶來的種種誤解。我們不必執著於名詞之爭,可以專注於理解實際的運行原理。 在Java語言裡面,任何一個Object Reference都可以作為同步鎖。同樣的道理,任何一個Object Reference也可以作為信號量。 Object對象的wait()方法就是等待通知,Object對象的notify()方法就是發出通知。 具體調用方法為 (1)等待某個信號量的通知 public static final Object signal = new Object();
… f1() { synchronized(singal) { // 首先我們要獲取這個信號量。這個信號量同時也是一個同步鎖
// 只有成功獲取了signal這個信號量兼同步鎖之後,我們才可能進入這段代碼     signal.wait(); // 這裡要放棄信號量。本線程要進入signal信號量的待召(Waiting)隊列
// 可憐。辛辛苦苦爭取到手的信號量,就這麼被放棄了
// 等到通知之後,從待召(Waiting)隊列轉到就緒(Ready)隊列裡面 // 轉到了就緒隊列中,離CPU核心近了一步,就有機會繼續執行下面的代碼了。 // 仍然需要把signal同步鎖競爭到手,才能夠真正繼續執行下面的代碼。命苦啊。     … } }
需要注意的是,上述代碼中的signal.wait()的意思。signal.wait()很容易導致誤解。signal.wait()的意思並不是說,signal開始wait,而是說,運行這段代碼的當前線程開始wait這個signal對象,即進入signal對象的待召(Waiting)隊列。
(2)發出某個信號量的通知 … f2() { synchronized(singal) { // 首先,我們同樣要獲取這個信號量。同時也是一個同步鎖。
// 只有成功獲取了signal這個信號量兼同步鎖之後,我們才可能進入這段代碼 signal.notify(); // 這裡,我們通知signal的待召隊列中的某個線程。
// 如果某個線程等到了這個通知,那個線程就會轉到就緒隊列中 // 但是本線程仍然繼續擁有signal這個同步鎖,本線程仍然繼續執行 // 嘿嘿,雖然本線程好心通知其他線程, // 但是,本線程可沒有那麼高風亮節,放棄到手的同步鎖 // 本線程繼續執行下面的代碼     … } }
需要注意的是,signal.notify()的意思。signal.notify()並不是通知signal這個對象本身。而是通知正在等待signal信號量的其他線程。
以上就是Object的wait()和notify()的基本用法。 實際上,wait()還可以定義等待時間,當線程在某信號量的待召隊列中,等到足夠長的時間,就會等無可等,無需再等,自己就從待召隊列轉移到就緒隊列中了。 另外,還有一個notifyAll()方法,表示通知待召隊列裡面的所有線程。 這些細節問題,並不對大局產生影響。 —————————————————————————————-線程同步的總結4————————————————————————- 總的說來,synchronized關鍵字可以作為函數的修飾符,也可作為函數內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。 在進一步闡述之前,我們需要明確幾點: A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。 B.每個對象只有一個鎖(lock)與之相關聯。 C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。 接著來討論synchronized用到不同地方對代碼產生的影響: 假設P1、P2是同一個類的不同對象,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以調用它們。 1.  把synchronized當作函數修飾符時,示例代碼如下: Public synchronized void methodAAA() { //…. } 這也就是同步方法,那這時synchronized鎖定的是哪個對象呢?它鎖定的是調用這個同步方法對象。也就是說,當一個對象P1在不同的線程中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個對象所屬的Class所產生的另一對象P2卻可以任意調用這個被加了synchronized關鍵字的方法。 上邊的示例代碼等同於如下代碼: public void methodAAA() { synchronized (this)      //  (1) {        //….. } } (1)處的this指的是什麼呢?它指的就是調用這個方法的對象,如P1。可見同步方法實質是將synchronized作用於object reference。――那個拿到了P1對象鎖的線程,才可以調用P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程序也可能在這種情形下擺脫同步機制的控制,造成數據混亂:( 2.同步塊,示例代碼如下:             public void method3(SomeObject so)               {                      synchronized(so) {        //….. } } 這時,鎖就是so這個對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。當有一個明確的對象作為鎖時,就可以這樣寫程序,但當沒有明確的對象作為鎖,只是想讓一段代碼同步時,可以創建一個特殊的instance變量(它得是一個對象)來充當鎖: class Foo implements Runnable {        private byte[] lock = new byte[0];  // 特殊的instance變量     Public void methodA() {        synchronized(lock) { //… } } //….. } 注:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯後的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。 3.將synchronized作用於static 函數,示例代碼如下:       Class Foo { public synchronized static void methodAAA()   // 同步的static 函數 { //…. } public void methodBBB() {        synchronized(Foo.class)   //  class literal(類名稱字面常量) }        }    代碼中的methodBBB()方法是把class literal作為鎖的情況,它和同步的static函數產生的效果是一樣的,取得的鎖很特別,是當前調用這個方法的對象所屬的類(Class,而不再是由這個Class產生的某個具體對象了)。 記得在《Effective Java》一書中看到過將 Foo.class和 P1.getClass()用於作同步鎖還不一樣,不能用P1.getClass()來達到鎖這個Class的目的。P1指的是由Foo類產生的對象。 可以推斷:如果一個類中定義了一個synchronized的static函數A,也定義了一個synchronized 的instance函數B,那麼這個類的同一對象Obj在多線程中分別訪問A和B兩個方法時,不會構成同步,因為它們的鎖都不一樣。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class。 小結如下: 搞清楚synchronized鎖定的是哪個對象,就能幫助我們設計更安全的多線程程序。 還有一些技巧可以讓我們對共享資源的同步訪問更加安全: 1.  定義private 的instance變量+它的 get方法,而不要定義public/protected的instance變量。如果將變量定義為public,對象在外界可以繞過同步方法的控制而直接取得它,並改動它。這也是JavaBean的標准實現方式之一。 2.  如果instance變量是一個對象,如數組或ArrayList什麼的,那上述方法仍然不安全,因為當外界對象通過get方法拿到這個instance對象的引用後,又將其指向另一個對象,那麼這個private變量也就變了,豈不是很危險。 這個時候就需要將get方法也加上synchronized同步,並且,只返回這個private對象的clone()――這樣,調用端得到的就是對象副本的引用了。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved