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