【Java程式設計的思想】理解synchronized
用法和基本原理
synchronized可以用於修飾類的例項方法、靜態方法和程式碼塊
例項方法
在介紹併發基礎知識的時候,有一部分是關於競態條件的,當多個執行緒訪問和操作同一個物件時,由於語句不是原子操作,所以得到了不正確的結果。這個地方就可以用synchronized進行處理
public class Counter {
private int count;
public synchronized void incr() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Counter是一個簡單的計數器,incr方法和getCount方法都用synchronized進行了修飾。
加了synchronized後,方法內的程式碼就變成了原子操作,當多個執行緒併發更新同一個Counter物件的時候,也不會出現問題。
public class CounterThread extends Thread {
Counter counter;
public CounterThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter.incr();
}
}
public static void main(String[] args) throws InterruptedException {
int num = 1000;
Thread[] threads = new Thread[num];
Counter counter = new Counter();
for (int i = 0; i < threads.length; i++) {
threads[i] = new CounterThread(counter);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println(counter.getCount());
}
}
這是改造後的程式碼,多個執行緒進行計數,得到的結果就是預期的了。
看上去,synchronized使得同時只能有一個執行緒執行例項方法,實際上多個執行緒是可以同時執行同一個synchronized例項方法的,只要它們訪問的物件是不同的即可
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new CounterThread(counter1);
Thread t2 = new CounterThread(counter2);
這裡的t1和t2兩個執行緒是可以同時執行Counter的incr方法的,因為它們訪問的是不同的Counter物件。
所以,synchronized例項方法保護的是同一個物件的方法呼叫,確保同時只有一個執行緒執行。 物件有一個鎖和一個等待佇列,鎖只能被一個執行緒持有,其他試圖獲取同樣鎖的執行緒需要等待。
synchronized保護的是物件而非程式碼,只要訪問的是同一個物件的synchronized方法,即使是不同的程式碼,也會被同步順序訪問。比如,對於Counter中的兩個例項方法getCount和incr,對於同一個Counter物件,一個執行緒執行getCount,另外一個執行incr,它們是不能同時執行的。
靜態方法
synchronized對於靜態方法,保護的是類物件。
public class StaticCounter {
private static int count = 0;
public static synchronized void incr() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
程式碼塊
public class Counter {
private int count;
public void incr() {
synchronized(this) {
count++;
}
}
public int getCount() {
synchronized(this) {
return count;
}
}
}
synchronized括號裡面的就是保護的物件,可以是任意物件,任意物件都有一個鎖和等待佇列,也就是說:任何物件都可以作為鎖物件。
進一步理解synchronized
可重入性
對於同一個執行執行緒,它在獲取了synchronized鎖之後,再呼叫其他需要同樣鎖的程式碼時,是可以直接呼叫的。
可重入是通過記錄鎖的持有執行緒和持有數量來實現的: 當呼叫被synchronized保護的程式碼時,檢查物件是否已被鎖,如果是,再檢查是否被當前執行緒鎖定,如果是,增加持有數量;如果不是被當前執行緒鎖定,才加入等待佇列。當釋放鎖時,減少持有數量,當數量變為0時才釋放整個鎖。
記憶體可見性
在併發基礎知識中有提到記憶體可見性的問題,多個執行緒可以共享訪問和操作相同的變數,但一個執行緒對一個共享變數的修改,另一個執行緒不一定馬上就能看到,甚至永遠都看不到。
synchronized可以保證記憶體可見性,在釋放鎖時,所有寫入都會寫回記憶體,而獲得鎖後,都會從記憶體中讀取最新資料。
死鎖
使用synchronized或者其他鎖,要注意死鎖。
所謂死鎖就是:有a、b兩個執行緒,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A。a和b進入了互相等待的狀態
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {
Thread aThread = new Thread(){
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A sleep over");
synchronized (lockB) {
System.out.println("A 持有了B鎖");
}
}
}
};
aThread.start();
}
private static void startThreadB() {
Thread bThread = new Thread(){
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B sleep over");
synchronized (lockA) {
System.out.println("B 持有了A鎖");
}
}
}
};
bThread.start();
}
public static void main(String[] args) {
startThreadA();
startThreadB();
}
}
執行後aThread和bThread陷入了互相等待。
如何解決?
1. 應該避免在持有一個鎖的同時去申請另一個鎖,如果確實需要多個鎖,所有程式碼都應該按照相同的順序去申請鎖。
2. 使用顯示鎖
同步容器
Collections中有一些方法,可以返回執行緒安全的同步容器
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它們是給所有容器方法都加上synchronized來實現安全的。
這裡的執行緒安全針對的是容器物件,指的是當多個執行緒併發訪問同一個容器物件時,不需要額外的同步操作。
加了synchronized,所有方法呼叫變成了原子操作。是不是就絕對安全了呢? 不是的,至少需要考慮以下情況
複合操作
先了解一下什麼是複合操作
public class EnhancedMap<K,V> {
Map<K,V> map;
public EnhancedMap(Map<K,V> map) {
this.map = Collections.synchronizedMap(map);
}
public V putIfAbsent(K key, V value) {
V old = map.get(key);
if(old != null) {
return old;
}
return map.put(key, value);
}
}
EnhancedMap是一個裝飾類,將Map物件轉換成同步容器物件,增加了一個putIfAbsent方法,該方法只有在原Map中沒有對應鍵的時候才新增value。
map的每個方法都是安全的,但是這個複合方法putIfAbsent是安全的嗎? 答案是否定的。 這是一個檢查然後再更新的複合操作,在多執行緒的情況下,可能多個執行緒都執行完了檢查這一步,都發現Map中沒有對應的鍵,然後就會呼叫put,這就破壞了安全性了。
偽同步
如果給上面的putIfAbsent方法加上synchronized就安全了嗎?答案是不一定的
public synchronized V putIfAbsent(K key, V value) {
V old = map.get(key);
if(old != null) {
return old;
}
return map.put(key, value);
}
像上面這種寫法,就還是無法同步的,因為同步的物件錯誤了。putIfAbsent同步使用的是EnhancedMap物件,而put使用的是map物件。這樣如果一個執行緒呼叫put,一個呼叫putIfAbsent,那麼依然不是安全的。
正確的做法是兩者用同一個鎖
public V putIfAbsent(K key, V value) {
synchronized (map) {
V old = map.get(key);
if(old != null) {
return old;
}
return map.put(key, value);
}
}
迭代
對於同步容器物件,雖然單個操作是安全的,但迭代並不是。
private static void startModifyThread(List<String> list) {
Thread modifyThread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
list.add(i + "");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
modifyThread.start();
}
private static void startIteratorThread(List<String> list) {
Thread iteratorThread = new Thread(){
@Override
public void run() {
while(true) {
for(String str: list) {
System.out.println(str);
}
}
}
};
iteratorThread.start();
}
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<String>());
startModifyThread(list);
startIteratorThread(list);
}
使用強化for迴圈迭代的時候,是不允許進行結構性變化的。同步容器並沒有解決這個問題,如果要這麼做,需要在遍歷的時候給整個容器物件加鎖
Thread iteratorThread = new Thread(){
@Override
public void run() {
while(true) {
synchronized(list) {
for(String str: list) {
System.out.println(str);
}
}
}
}
};
併發容器
同步容器效能是比較低的,所以可以使用專門的併發容器類
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet