同步
線程主要通過共享對字段和引用對象的引用字段的訪問來進(jìn)行通信,這種通信形式非常有效,但可能產(chǎn)生兩種錯誤:線程干擾和內(nèi)存一致性錯誤,防止這些錯誤所需的工具是同步。
但是,同步可能會引入線程競爭,當(dāng)兩個或多個線程同時嘗試訪問同一資源并導(dǎo)致Java運行時更慢地執(zhí)行一個或多個線程,甚至?xí)和K鼈儓?zhí)行,饑餓和活鎖是線程競爭的形式。
本節(jié)包括以下主題:
線程干擾描述了當(dāng)多個線程訪問共享數(shù)據(jù)時如何引入錯誤。
內(nèi)存一致性錯誤描述了由共享內(nèi)存的不一致視圖導(dǎo)致的錯誤。
同步方法描述了一種簡單的語法,可以有效地防止線程干擾和內(nèi)存一致性錯誤。
隱式鎖和同步描述了一種更通用的同步語法,并描述了同步是如何基于隱式鎖的。
原子訪問討論的是不能被其他線程干擾的操作的一般概念。
線程干擾考慮一個名為Counter的簡單類:
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
Counter的設(shè)計為每次increment的調(diào)用都會將c加1,每次decrement的調(diào)用都會從c中減去1,但是,如果從多個線程引用Counter對象,則線程之間的干擾可能會妨礙這種情況按預(yù)期發(fā)生。
當(dāng)兩個操作在不同的線程中運行但作用于相同的數(shù)據(jù)時,會發(fā)生干擾,這意味著這兩個操作由多個步驟組成,并且步驟序列交疊。
對于Counter實例的操作似乎不可能進(jìn)行交錯,因為對c的兩個操作都是單個簡單的語句,但是,即使是簡單的語句也可以由虛擬機(jī)轉(zhuǎn)換為多個步驟,我們不會檢查虛擬機(jī)采取的具體步驟 — 只需知道單個表達(dá)式c++可以分解為三個步驟:
檢索c的當(dāng)前值。
將檢索的值增加1。
將增加的值存儲在c中。
表達(dá)式c--可以以相同的方式分解,除了第二步是遞減而不是遞增。
假設(shè)在大約同一時間,線程A調(diào)用increment,線程B調(diào)用decrement,如果c的初始值為0,則它??們的交錯操作可能遵循以下順序:
線程A:檢索c。
線程B:檢索c。
線程A:遞增檢索值,結(jié)果是1。
線程B:遞減檢索值,結(jié)果是-1。
線程A:將結(jié)果存儲在c中,c現(xiàn)在是1。
線程B:將結(jié)果存儲在c中,c現(xiàn)在是-1。
線程A的結(jié)果丟失,被線程B覆蓋,這種特殊的交錯只是一種可能性,在不同的情況下,可能是線程B的結(jié)果丟失,或者根本沒有錯誤,因為它們是不可預(yù)測的,所以難以檢測和修復(fù)線程干擾錯誤。
內(nèi)存一致性錯誤當(dāng)不同的線程具有應(yīng)該是相同數(shù)據(jù)的不一致視圖時,會發(fā)生內(nèi)存一致性錯誤,內(nèi)存一致性錯誤的原因很復(fù)雜,超出了本教程的范圍,幸運的是,程序員不需要詳細(xì)了解這些原因,所需要的只是避免它們的策略。
避免內(nèi)存一致性錯誤的關(guān)鍵是理解先發(fā)生關(guān)系,這種關(guān)系只是保證一個特定語句的內(nèi)存寫入對另一個特定語句可見,要了解這一點,請考慮以下示例,假設(shè)定義并初始化了一個簡單的int字段:
int counter = 0;
counter字段在兩個線程A和B之間共享,假設(shè)線程A遞增counter:
counter++;
然后,不久之后,線程B打印出counter:
System.out.println(counter);
如果兩個語句已在同一個線程中執(zhí)行,則可以安全地假設(shè)打印出的值為“1”,但如果兩個語句在不同的線程中執(zhí)行,則打印出的值可能為“0”,因為無法保證線程A對counter的更改對線程B可見 — 除非程序員在這兩條語句之間建立了先發(fā)生關(guān)系。
有幾種操作可以創(chuàng)建先發(fā)生關(guān)系,其中之一是同步,我們將在下面的部分中看到。
我們已經(jīng)看到了兩種創(chuàng)建先發(fā)生關(guān)系的操作。
當(dāng)一個語句調(diào)用Thread.start時,與該語句具有一個先發(fā)生關(guān)系的每個語句也與新線程執(zhí)行的每個語句都有一個先發(fā)生關(guān)系,導(dǎo)致創(chuàng)建新線程的代碼的效果對新線程可見。
當(dāng)一個線程終止并導(dǎo)致另一個線程中的Thread.join返回時,已終止的線程執(zhí)行的所有語句與成功join后的所有語句都有一個先發(fā)生關(guān)系,線程中代碼的效果現(xiàn)在對執(zhí)行join的線程可見。
有關(guān)創(chuàng)建先發(fā)生關(guān)系的操作列表,請參閱java.util.concurrent包的Summary頁面。
同步方法Java編程語言提供了兩種基本的同步語法:同步方法和同步語句,下兩節(jié)將介紹兩個同步語句中較為復(fù)雜的語句,本節(jié)介紹同步方法。
要使方法同步,只需將synchronized關(guān)鍵字添加到其聲明:
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
如果count是SynchronizedCounter的一個實例,那么使這些方法同步有兩個效果:
首先,不可能對同一對象上的兩個同步方法的調(diào)用進(jìn)行交錯,當(dāng)一個線程正在為對象執(zhí)行同步方法時,調(diào)用同一對象的同步方法的所有其他線程阻塞(暫停執(zhí)行),直到第一個線程使用完對象為止。
其次,當(dāng)一個同步方法退出時,它會自動與同一個對象的同步方法的任何后續(xù)調(diào)用建立一個先發(fā)生關(guān)系,這可以保證對象狀態(tài)的更改對所有線程都可見。
請注意,構(gòu)造函數(shù)無法同步 — 將synchronized關(guān)鍵字與構(gòu)造函數(shù)一起使用是一種語法錯誤,同步構(gòu)造函數(shù)沒有意義,因為只有創(chuàng)建對象的線程在構(gòu)造時才能訪問它。
構(gòu)造將在線程之間共享的對象時,要非常小心對對象的引用不會過早“泄漏”,例如,假設(shè)你要維護(hù)一個包含每個類實例的名為instances的List,你可能想要將以下行添加到你的構(gòu)造函數(shù)中:instances.add(this);但是其他線程可以在構(gòu)造對象完成之前使用instances來訪問對象。
同步方法支持一種簡單的策略來防止線程干擾和內(nèi)存一致性錯誤:如果一個對象對多個線程可見,則對該對象的變量所有讀取或?qū)懭攵际峭ㄟ^synchronized方法完成的(一個重要的例外:一旦構(gòu)造了對象,就可以通過非同步方法安全地讀取構(gòu)造對象后無法修改的final字段),這種策略很有效,但可能會帶來活性問題,我們將在本課后面看到。
固有鎖和同步同步是圍繞稱為固有鎖或監(jiān)控鎖的內(nèi)部實體構(gòu)建的(API規(guī)范通常將此實體簡稱為“監(jiān)視器”。),固有鎖在同步的兩個方面都起作用:強(qiáng)制執(zhí)行對對象狀態(tài)的獨占訪問,并建立對可見性至關(guān)重要的先發(fā)生關(guān)系。
每個對象都有一個與之關(guān)聯(lián)的固有鎖,按照約定,需要對對象字段進(jìn)行獨占和一致訪問的線程必須在訪問對象之前獲取對象的固有鎖,然后在完成它們時釋放固有鎖。線程在獲取鎖和釋放鎖期間被稱為擁有固有鎖,只要一個線程擁有固有鎖,沒有其他線程可以獲得相同的鎖,另一個線程在嘗試獲取鎖時將阻塞。
當(dāng)線程釋放固有鎖時,在該操作與同一鎖的任何后續(xù)獲取之間建立先發(fā)生關(guān)系。
同步方法中的鎖當(dāng)線程調(diào)用同步方法時,它會自動獲取該方法對象的固有鎖,并在方法返回時釋放它,即使返回是由未捕獲的異常引起的,也會發(fā)生鎖定釋放。
你可能想知道調(diào)用靜態(tài)同步方法時會發(fā)生什么,因為靜態(tài)方法與類相關(guān)聯(lián),而不是與對象相關(guān)聯(lián),在這種情況下,線程獲取與類關(guān)聯(lián)的Class對象的固有鎖,因此,對類的靜態(tài)字段的訪問由一個鎖控制,該鎖與該類的任何實例的鎖不同。
同步語句創(chuàng)建同步代碼的另一種方法是使用同步語句,與同步方法不同,同步語句必須指定提供固有鎖的對象:
public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }
在此示例中,addName方法需要同步更改lastName和nameCount,但還需要避免同步調(diào)用其他對象的方法(從同步代碼中調(diào)用其他對象的方法可能會產(chǎn)生有關(guān)活性一節(jié)中描述的問題),如果沒有同步語句,則必須有一個多帶帶的、不同步的方法,其唯一目的是調(diào)用nameList.add。
同步語句對于通過細(xì)粒度同步提高并發(fā)性也很有用,例如,假設(shè)類MsLunch有兩個實例字段,c1和c2,它們從不一起使用,必須同步這些字段的所有更新,但是沒有理由阻礙c1的更新與c2的更新交錯 — 并且這樣做會通過創(chuàng)建不必要的阻塞來減少并發(fā)性。我們創(chuàng)建兩個對象只是為了提供鎖,而不是使用同步方法或使用與此相關(guān)聯(lián)的鎖。
public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; } } public void inc2() { synchronized(lock2) { c2++; } } }
謹(jǐn)慎使用這種用法,你必須絕對確保對受影響字段的交錯訪問是安全的。
可重入同步回想一下,線程無法獲取另一個線程擁有的鎖,但是一個線程可以獲得它已經(jīng)擁有的鎖,允許線程多次獲取同一個鎖可使可重入同步。這描述了一種情況,其中同步代碼直接或間接地調(diào)用也包含同步代碼的方法,并且兩組代碼使用相同的鎖,在沒有可重入同步的情況下,同步代碼必須采取許多額外的預(yù)防措施,以避免線程導(dǎo)致自身阻塞。
原子訪問在編程中,原子操作是一次有效地同時發(fā)生的操作,原子操作不能停在中間:它要么完全發(fā)生,要么根本不發(fā)生,在操作完成之前,原子操作的副作用在完成之前是不可見的。
我們已經(jīng)看到增量表達(dá)式(如c++),沒有描述原子操作,即使非常簡單的表達(dá)式也可以定義可以分解為其他操作的復(fù)雜操作,但是,你可以指定為原子操作:
對于引用變量和大多數(shù)原始變量(除long和double之外的所有類型),讀取和寫入都是原子的。
對于聲明為volatile的所有變量(包括long和double),讀取和寫入都是原子的。
原子操作不能交錯,因此可以使用它們而不用擔(dān)心線程干擾,但是,這并不能消除所有同步原子操作的需要,因為仍然可能存在內(nèi)存一致性錯誤。使用volatile變量可以降低內(nèi)存一致性錯誤的風(fēng)險,因為對volatile變量的任何寫入都會建立與之后讀取相同變量的先發(fā)生關(guān)系,這意味著對volatile變量的更改始終對其他線程可見。更重要的是,它還意味著當(dāng)線程讀取volatile變量時,它不僅會看到volatile的最新更改,還會看到導(dǎo)致更改的代碼的副作用。
使用簡單的原子變量訪問比通過同步代碼訪問這些變量更有效,但程序員需要更加小心以避免內(nèi)存一致性錯誤,額外的功夫是否值得取決于應(yīng)用程序的大小和復(fù)雜性。
java.util.concurrent包中的某些類提供了不依賴于同步的原子方法,我們將在高級并發(fā)對象一節(jié)中討論它們。
上一篇:Thread對象 下一篇:并發(fā)活性文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/73013.html
摘要:在接下來的分鐘,你將會學(xué)會如何通過同步關(guān)鍵字,鎖和信號量來同步訪問共享可變變量。所以在使用樂觀鎖時,你需要每次在訪問任何共享可變變量之后都要檢查鎖,來確保讀鎖仍然有效。 原文:Java 8 Concurrency Tutorial: Synchronization and Locks譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 歡迎閱讀我的Java8并發(fā)教程的第二部分。這份指南將...
原子變量 java.util.concurrent.atomic包定義了支持單個變量的原子操作的類,所有類都有g(shù)et和set方法,類似于對volatile變量的讀寫操作,也就是說,set與在同一個變量上任何后續(xù)的get具有先發(fā)生關(guān)系,compareAndSet原子方法也具有這些內(nèi)存一致性特性,適用于整數(shù)原子變量的簡單原子算法也是如此。 要查看如何使用此包,讓我們返回我們最初用于演示線程干擾的Cou...
高級并發(fā)對象 到目前為止,本課程重點關(guān)注從一開始就是Java平臺一部分的低級別API,這些API適用于非常基礎(chǔ)的任務(wù),但更高級的任務(wù)需要更高級別的構(gòu)建塊,對于充分利用當(dāng)今多處理器和多核系統(tǒng)的大規(guī)模并發(fā)應(yīng)用程序尤其如此。 在本節(jié)中,我們將介紹Java平臺5.0版中引入的一些高級并發(fā)功能,大多數(shù)這些功能都在新的java.util.concurrent包中實現(xiàn),Java集合框架中還有新的并發(fā)數(shù)據(jù)結(jié)構(gòu)。 ...
摘要:并發(fā)教程原子變量和原文譯者飛龍協(xié)議歡迎閱讀我的多線程編程系列教程的第三部分。如果你能夠在多線程中同時且安全地執(zhí)行某個操作,而不需要關(guān)鍵字或上一章中的鎖,那么這個操作就是原子的。當(dāng)多線程的更新比讀取更頻繁時,這個類通常比原子數(shù)值類性能更好。 Java 8 并發(fā)教程:原子變量和 ConcurrentMap 原文:Java 8 Concurrency Tutorial: Synchroni...
并發(fā)活性 并發(fā)應(yīng)用程序及時執(zhí)行的能力被稱為其活性,本節(jié)描述了最常見的活性問題,死鎖,并繼續(xù)簡要描述其他兩個活性問題,饑餓和活鎖。 死鎖 死鎖描述了兩個或多個線程永遠(yuǎn)被阻塞,等待彼此的情況,這是一個例子。 Alphonse和Gaston是朋友,是禮貌的忠實信徒,禮貌的一個嚴(yán)格規(guī)則是,當(dāng)你向朋友鞠躬時,你必須一直鞠躬,直到你的朋友有機(jī)會還禮,不幸的是,這條規(guī)則沒有考慮到兩個朋友可能同時互相鞠躬的可能性...
閱讀 1565·2021-10-25 09:44
閱讀 2926·2021-09-04 16:48
閱讀 1543·2019-08-30 15:44
閱讀 2475·2019-08-30 15:44
閱讀 1731·2019-08-30 15:44
閱讀 2816·2019-08-30 14:14
閱讀 2964·2019-08-30 13:00
閱讀 2143·2019-08-30 11:09