1. 程式人生 > >【Java程式設計的思想】理解synchronized

【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