進行多線程編程的時候,需要考慮的是線程間的同步問題。對於共享的資源,需要進行互斥的訪問。在Java中可以使用一些手段來達到線程同步的目的:
1. synchronized
2. ThreadLocal,線程本地變量
3. Java.util.concurrent.Lock
Java中,線程會共享堆上的實例變量以及方法區的類變量,而棧上的數據是私有的,不必進行保護。synchronized方法或synchronized塊將標記一塊監視區域,線程在進入該區域時,需要獲得對象鎖或類鎖,JVM將自動上鎖。synchronized提供了兩種主要特性:
1. 互斥。互斥是指一次只允許一個線程持有某個特定的鎖,因此可使用該特性實現對共享數據的並發訪問,保證一次只有一個線程能夠使用該共享數據。
2.可見性。確保釋放鎖之前對共享數據做出的更改對隨后獲得該鎖的另一個線程是可見的。如果不能保證可見性,也就無法保證數據正確性,這將引發嚴重問題。volitail關鍵字同樣保證了這種可見性。
在這里,我們將探討synchronized使用時的三種情況:
1. 在對象上使用synchronized
2. 在普通成員方法上使用synchronized
3. 在靜態成員方法上使用synchronized
這三種線程同步的表現有何不同?
下面通過三段示例代碼來演示這三種情況。這里模擬線程報數的場景。
情況一:在普通成員函數上使用synchronized
public class MyThread extends Thread { public static void main(String[] args) throws Exception { for (int i = 1; i < 100; i++) { MyThread t = new MyThread(); t.setName("Thread="+i); t.start(); Thread.sleep(100); } } @Override public synchronized void run() { for (int i = 1; i < 10000; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } }
對一個成員函數使用synchronized進行加鎖,所獲取的鎖,是方法所在對象本身的對象鎖。在這里,每個線程都以自身的對象作為對象鎖,要對線程進行同步,要求鎖對象必須唯一,所以這里多個線程間同步失敗。
情況二:在對象上使用synchronized
這里在類中增加一個成員變量lock,在該變量上使用synchronized:
public class MyThread1 extends Thread { private String lock; public MyThread1(String lock) { this.lock = lock; } public static void main(String[] args) throws Exception { String lock = new String("lock"); for (int i = 1; i < 100; i++) { Thread t = new MyThread1(lock); t.setName("Thread=" + i); t.start(); Thread.sleep(100); } } @Override public void run() { synchronized (lock) { for (int i = 1; i < 10000; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
100個線程在創建的時候,都傳遞了同一個lock對象(在main中創建的)去初始化線程類成員lock,因此,這100個線程都在同一個lock對象上進行synchronized同步。因此線程同步成功。
情況三:在靜態成員函數上使用synchronized
public class MyThread2 extends Thread { public static void main(String[] args) throws Exception { for (int i = 1; i < 10; i++) { Thread t = new MyThread2(); t.setName("Thread=" + i); t.start(); Thread.sleep(10); } } public static synchronized void func() { for (int i = 1; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } @Override public void run() { func(); } }
這種情況下,線程獲得的鎖是對象鎖,而對象鎖是唯一的,因此多個進程間也能同步成功。
補充:
1. 慎用字符串常量做同步對象,因為JVM內部會把常量字符串轉換成同一個對象,同理的,基本數據除了Float和Double外,也有緩存對象[-128,127].
2. synchronized方法繼承問題:1. 子類會繼承父類的synchronized方法。2. 如果子類重寫了父類的synchronized方法,必須也加上synchronized關鍵字,否則子類中的方法將變成非同步的。 3. 同一個子類對象中,子類的synchronized方法父類的synchronized方法使用的是同一個臨界區。
(完)