Java併發程式設計中經常會用到synchronized、volatile和lock,三者都可以解決併發問題,這裡做一個總結。
1、volatile
volatile保證了共享變數的可見性,也就是說,執行緒A修改了共享變數的值時,執行緒B能夠讀到該修改的值。但是,對任意單個volatile變數的讀/寫具有原子性,但類似於i++這種複合操作不具有原子性。因此,volatile最適用一個執行緒寫,多個執行緒讀的場合。
2、synchronized
synchronized是Java中的關鍵字,可以用來修飾變數、方法或程式碼塊,保證同一時刻最多隻有一個執行緒執行這段程式碼。
修飾普通方法(例項方法):
synchronized修飾普通方法時鎖的是當前例項物件 ,進入同步程式碼前要獲得當前例項物件的鎖。執行緒A和執行緒B呼叫同一例項物件的synchronized方法時才能保證執行緒安全,若呼叫不同物件的synchronized方法不會出現互斥的問題。對比如下兩段程式碼:
public class TestSync implements Runnable{ //共享資源 static int i=0;
/** * synchronized 修飾例項方法 */public synchronized void addI(){ i++;}public void run() { for(int j=0;j<10000;j++){ addI(); }}public static void main(String[] args) throws InterruptedException { TestSync instance=new TestSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i);//輸出20000}複製程式碼
}
public class TestSync implements Runnable{ //共享資源 static int i=0;
/** * synchronized 修飾例項方法 */public synchronized void addI(){ i++;}public void run() { for(int j=0;j<10000;j++){ addI(); }}public static void main(String[] args) throws InterruptedException { TestSync instance1=new TestSync(); TestSync instance2=new TestSync(); Thread t1=new Thread(instance1); Thread t2=new Thread(instance2); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i);//輸出可能比20000小}複製程式碼
}
第二段程式碼對不同的例項物件加鎖,也就是t1和t2使用不同的鎖,操作的又是共享變數,因此,執行緒安全無法保證。解決這種問題的方法是將synchronized作用於靜態的addI方法,這樣的話,物件鎖就是當前類的Class物件,由於無論建立多少個例項物件,但類物件只有一個,在這樣的情況下物件鎖就是唯一的。
修飾靜態方法:
當synchronized作用於靜態方法時,鎖的是當前類的Class物件鎖,由於靜態成員不屬於任何一個例項物件,是類成員,因此透過Class物件鎖可以控制靜態成員的併發操作。執行緒A訪問static synchronized方法,執行緒B訪問非static synchronized方法,A和B不互斥,因為使用不同的鎖。
public class TestSync implements Runnable{ //共享資源 static int i=0;
/** * synchronized 修飾例項方法 */public static synchronized void addI(){ i++;}public void run() { for(int j=0;j<10000;j++){ addI(); }}public static void main(String[] args) throws InterruptedException { TestSync instance1=new TestSync(); TestSync instance2=new TestSync(); Thread t1=new Thread(instance1); Thread t2=new Thread(instance2); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i);//輸出20000}複製程式碼
}
修飾程式碼塊:
synchronized除了修飾方法(普通方法、靜態方法)外,還可以修飾程式碼塊。如果一個方法的方法體較大,而需要同步的程式碼只是一小部分時就可以用該種使用方式。
public class TestSync implements Runnable{ static String instanceStr=new String(); static int i=0; @Override public void run() { //使用同步程式碼塊對變數i進行同步操作,鎖物件為instance synchronized(instanceStr){ for(int j=0;j<10000;j++){ i++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new TestSync()); Thread t2=new Thread(new TestSync()); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i);//20000,如果instanceStr不是static則不能保證執行緒安全,同上 }}複製程式碼
除此之外,synchronized還可以對this或Class物件加鎖,保證同步的條件同上。
public void run() { //this加鎖 synchronized(this){ for(int j=0;j<10000;j++){ i++; } } } public void run() { //Class物件加鎖 synchronized(TestSync.class){ for(int j=0;j<10000;j++){ i++; } } }複製程式碼
3、lock
Lock是一個類,透過這個類可以實現同步訪問,先來看一下Lock中的方法,如下:
public interface Lock { /** * 獲取鎖,鎖被佔用則等待 */ void lock(); /** * 獲取鎖時,如果執行緒處於等待,則該執行緒能夠響應中斷而去處理其他事情 */ void lockInterruptibly() throws InterruptedException; /** * 嘗試獲取鎖,如果鎖被佔用則返回false,否則返回true */ boolean tryLock(); /** * 較tryLock多一個等待時間,等待時間到了仍不能獲得鎖則返回false */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * 釋放鎖 */ void unlock();}複製程式碼
常見用法:
Lock lock = new ReentrantLock();//ReentrantLock是Lock的唯一實現類lock.lock();try{ }catch(Exception ex){}finally{ lock.unlock(); }Lock lock = new ReentrantLock();if(lock.tryLock()) { try{ }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //如果不能獲取鎖,則處理其他事情}複製程式碼
Reetrantlock
Reetrantlock是Lock的實現類,它表示可重入鎖。ReentrantLock雖然沒能像synchronized關鍵字一樣支援隱式的重進入,但是在呼叫lock()方法時,已經獲取到鎖的執行緒,能夠再次呼叫lock()方法獲取鎖而不被阻塞。
ReadWriteLock
Reetrantlock屬於排他鎖,這些鎖在同一時刻只允許一個執行緒進行訪問,ReadWriteLock是讀寫鎖,讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,透過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。
volatile與synchronized的區別
(1)volatile主要應用在多個執行緒對例項變數更改的場合,透過重新整理主記憶體共享變數的值從而使得各個執行緒可以獲得最新的值;synchronized則是鎖定當前變數,透過加鎖方式保證變數的可見性。
(2)volatile僅能修飾變數;synchronized則可以使用在變數、方法和類上。
(3)volatile不會造成執行緒的阻塞;多個執行緒爭搶synchronized鎖物件時,會出現阻塞。
(4)volatile僅能實現變數的修改可見性,不能保證原子性。
(5)volatile標記的變數不會被編譯器最佳化,可以禁止進行指令重排;synchronized標記的變數可以被編譯器最佳化。
synchronized與lock的區別
(1)synchronized在執行完同步程式碼或發生異常時,能自動釋放鎖;而Lock則需要在finally程式碼塊中主動透過unLock()去釋放鎖;
(2)Lock可以讓等待鎖的執行緒響應中斷,Lock提供了更靈活的獲取鎖的方式,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
(3)透過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
(4)Lock可以提高多個執行緒進行讀操作的效率。如果競爭資源不激烈,兩者的效能是差不多的,而當有大量執行緒同時競爭時,此時Lock的效能要佳。所以說,在具體使用時要根據情況適當選擇。