前言 線程間的通信主要通過共用對欄位的訪問和對象引用欄位的引用,可能會產生兩種錯誤,線程干擾和記憶體一致性錯誤。Java的同步就是防止這些錯誤,但當多個線程訪問同一資源會導致線程執行緩慢,甚至暫停執行。 線程干擾(Thread Interference) 例子 class Counter { priv ...
前言
線程間的通信主要通過共用對欄位的訪問和對象引用欄位的引用,可能會產生兩種錯誤,線程干擾和記憶體一致性錯誤。Java的同步就是防止這些錯誤,但當多個線程訪問同一資源會導致線程執行緩慢,甚至暫停執行。
線程干擾(Thread Interference)
例子
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }View Code
如果現在有兩個線程,線程A執行increment,線程B執行decrement,它們都使用了相同的變數c,這時會發生干擾。即便是執行非常簡單的語句,但簡單語句也可以轉化為Java虛擬機的多個步驟,例如c++可以分解為三個步驟:
1、檢索c的當前值。
2、將檢索值加1。
3、將值存儲回c中。
PS:c—也可以分解為相同步驟,只是第二步是減1。
假如線程A和線程B幾乎同時調用,c的初始值為0,則它們交錯的步驟可能是以下順序:
線程A:檢索c。 線程B:檢索c。 線程A:增加檢索值;結果是1。 線程B:減少檢索值;結果是-1。 線程A:將結果存儲在c中; c現在是1。 線程B:將結果存儲在c中; c現在是-1。
線程A的結果丟失了,被線程B覆蓋了。這種交錯只是一種可能性,也可能是B的結果丟失,也可能不會出錯。也就是因為順序是不可預測的,所以線程干擾的錯誤可能難以發現和修複。
記憶體一致性錯誤
不同線程看到的記憶體中的同一份數據卻有不同的“視圖”,原因很複雜,作為程式員最需要做的是處理好“happens-before”的關係,避免問題。
同步方法
Java提供了兩種基本的同步方法:同步方法和同步語句。同步的行為是依賴鎖來構建的,每一個對象都有與之相關的固定鎖,想要獨占訪問對象的線程必須先獲取對象的鎖(拿不到阻塞線程),完成後釋放鎖,同時與請求同一鎖的線程建立一個happens-before關係。
同步方法的作用:
1、當一個線程正在執行一個對象的同步方法時,所有其他調用該對象的同步方法的線程將被阻塞,直到第一個線程執行完成。
2、當一個同步方法退出後,會自動建立與後續調用的同步方法(相同對象)的一個happens-before關係,保證所有線程對對象狀態的修改可見。
註意:
1、在構造函數中使用synchronized關鍵字是語法錯誤。同步構造函數沒有意義,因為只有創建對象的線程在構建時才能訪問它。
2、如果一個對象對多個線程可見,則通過同步方法完成對該對象變數的所有讀取或寫入操作,不然還是會出現線程干擾和記憶體一致性錯誤。
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }View Code
同步語句
另一種創建同步代碼的方法,與同步方法不同,同步語句必須指定提供內部鎖的對象。同步語句有利於通過細粒度同步來提高併發性。
例如,假設類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++; } } }View Code
volatile關鍵字
原子操作:一個原子操作,要麼發生,要麼不發生。比如 c=0;(非long和double類型) 這個操作是執行了就會發生。再比如:c++;可分割成三個操作步驟,執行時可能會丟失某些步驟,就不是一個原子操作。非原子操作都會存線上程安全問題,同步方法和同步語句可以讓它變成一個原子操作。
volatile變數是一種稍弱的同步機制,所有聲明為volatile的變數(包括long和double),讀取和寫入都是原子的。
volatile變數特性:
1、當一個線程讀取一個volatile變數時,它看到總是最新的值。
2、原子動作不能交錯,使用volatile變數不用擔心線程干擾。
PS:java.util.concurrent包下提供了一些原子類。
參考文獻
https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html