多執行緒基礎之synchronized和volatile

阿嵬茨的囚徒發表於2019-04-19

多執行緒安全三大特性:

(1)原子性:

指一系列操作要麼一起執行完成,要麼一起不執行。例如i++操作其實並不是原子的,執行緒需要先獲取到i的值然後線上程記憶體中對i的值進行+1再重新整理到主記憶體中,在這個期間可能有別的執行緒對i的值進行了修改,這樣得出的結果就是錯誤的,所以我們需要同步鎖Synchronized(Volatile並不是原子性)。

(2)可見性:

執行緒之間變數相互可見。假設有一個全域性變數i的值為0,同時有執行緒A和執行緒B對一個全域性變數i執行++操作那麼在JMM(JAVA記憶體模型)中由A執行緒獲取全域性變數i的值後線上程記憶體中對i執行++操作(由於執行緒互相之間不可見此時對B執行緒來說i還是0),此時B執行緒同時進行與A執行緒一樣的操作,在最後重新整理到主記憶體中那麼i的值就為1而不是2。這就是執行緒之間互相不可見造成的執行緒不安全。


多執行緒基礎之synchronized和volatile

                                                    執行緒安全情況下




多執行緒基礎之synchronized和volatile

                                                    執行緒不安全情況下

(3)有序性:

即程式執行的順序按照程式碼的先後順序執行。在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。


synchronized:

java中自帶的同步鎖,可修飾方法或用來同步程式碼塊

public class ThreadDemo implements Runnable {
    static int a=100;
	Object obj=new Object();
	@Override
	public void run() {
		test3();
	}
	/**
	 * Synchronzed修飾方法 此時相當於使用Synchronized(this){...}包裹了方法中的所有程式碼
	 */
	public  synchronized void test1() {
		System.out.println();
	}
	
	/**
	 *  synchronized 程式碼塊選擇一個物件作為物件鎖 只有獲取到該物件鎖的執行緒才可以訪問程式碼塊
	 */
	public void test2() {
		synchronized(obj){
			System.out.println();
		}
	}
	
	/**
	 *  synchronized 修飾靜態方法 相當於synchronized(ThreadDemo.class) 以類位元組碼檔案作為物件鎖  所有該類的物件想訪問程式碼都必須獲取同一個鎖
	 */
	public synchronized static void test3() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a--;
			System.out.println(Thread.currentThread().getName()+"----"+a);
		}
	}

} 複製程式碼

1.修飾方法:

等於synchronized(this){...},一個物件例項中同時只能有一條執行緒訪問該方法,如果該物件例項中有其它修飾了synchronized的方法,那麼它們將共用一把鎖。


2.同步程式碼塊:

語法:synchronized(obj){...},一個物件例項中同時只能有一條執行緒訪問該同步程式碼塊,如果該物件例項中有其它使用了這個obj物件鎖修飾的程式碼塊,那麼它們將共用一把鎖。


3.修飾靜態方法:

等於synchronized(xxx.class){...},所有該class的例項物件共用同一把鎖,以class位元組碼檔案作為物件鎖。

程式碼如下:

public class ThreadDemo implements Runnable {
    private static int a=0;

	@Override
	public void run() {
		add();
	}
   
	public static void main(String[] args) {
		Thread t1=new Thread(new ThreadDemo(),"執行緒1");
		Thread t2=new Thread(new ThreadDemo(),"執行緒2");
		t1.start();
		t2.start();
	}
	
	public synchronized static void add() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300); /**非執行緒安全情況下讓執行緒休眠堆積 重新排程 產生竟態條件**/
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a++;
			System.out.println(Thread.currentThread().getName()+":----"+a);
		}
		
	}

}複製程式碼

執行結果:

執行緒1:----1
執行緒1:----2
執行緒1:----3
執行緒1:----4
……
執行緒1:----40
執行緒1:----41
執行緒1:----42
執行緒1:----43
執行緒1:----44
執行緒1:----45
執行緒1:----46
執行緒1:----47
執行緒1:----48
執行緒1:----49
執行緒1:----50
執行緒2:----51
執行緒2:----52
執行緒2:----53
執行緒2:----54
執行緒2:----55
執行緒2:----56
執行緒2:----57
執行緒2:----58
執行緒2:----59
……
執行緒2:----96
執行緒2:----97
執行緒2:----98
執行緒2:----99
執行緒2:----100
複製程式碼

synchronized在使用時需要注意效能問題,應自己衡量好效能與執行緒安全之間的平衡。複製程式碼

volatile:

java自帶的關鍵字,用來修飾變數,被宣告的變數對所有執行緒可見同時禁止重排序,但不保證原子性,假設有多條執行緒同時對變數值進行修改還是會出現執行緒不安全。

修改上述程式碼:

public class ThreadDemo implements Runnable {
    private volatile static int a=0;

	@Override
	public void run() {
		add();
	}
   
	public static void main(String[] args) {
		Thread t1=new Thread(new ThreadDemo(),"執行緒1");
		Thread t2=new Thread(new ThreadDemo(),"執行緒2");
		t1.start();
		t2.start();
	}
	
	public  static void add() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300); /**非執行緒安全情況下讓執行緒休眠堆積 重新排程 產生竟態條件**/
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a++;
			System.out.println(Thread.currentThread().getName()+":----"+a);
		}
		
	}

}複製程式碼

執行結果:

執行緒2:----1
執行緒1:----2
執行緒2:----4
執行緒1:----4
執行緒2:----5
執行緒1:----6
執行緒2:----7
執行緒1:----8
執行緒2:----9
執行緒1:----10
執行緒1:----11
執行緒2:----12
執行緒1:----13
執行緒2:----14
執行緒1:----16
執行緒2:----16
執行緒1:----18
執行緒2:----18
執行緒1:----19
執行緒2:----20
執行緒1:----21
執行緒2:----22
執行緒1:----23
執行緒2:----23
執行緒1:----24
執行緒2:----25
執行緒1:----26
執行緒2:----27
執行緒2:----29
執行緒1:----29
執行緒1:----30
執行緒2:----31
執行緒1:----32
執行緒2:----33
執行緒1:----34
執行緒2:----35
執行緒1:----36
執行緒2:----37
執行緒1:----38
執行緒2:----39
執行緒1:----40
執行緒2:----41
執行緒1:----42
執行緒2:----43
執行緒1:----44
執行緒2:----45
執行緒1:----46
執行緒2:----47
執行緒1:----48
執行緒2:----49
執行緒1:----50
執行緒2:----51
執行緒1:----52
執行緒2:----53
執行緒1:----54
執行緒2:----55
執行緒1:----56
執行緒2:----57
執行緒1:----58
執行緒2:----59
執行緒1:----60
執行緒2:----61
執行緒1:----62
執行緒2:----63
執行緒1:----64
執行緒2:----65
執行緒1:----66
執行緒2:----67
執行緒1:----68
執行緒2:----69
執行緒1:----70
執行緒2:----71
執行緒1:----72
執行緒2:----73
執行緒1:----74
執行緒2:----75
執行緒1:----76
執行緒2:----77
執行緒1:----78
執行緒2:----79
執行緒1:----80
執行緒2:----81
執行緒1:----82
執行緒2:----83
執行緒1:----84
執行緒2:----85
執行緒1:----86
執行緒2:----87
執行緒1:----88
執行緒2:----89
執行緒1:----90
執行緒2:----91
執行緒1:----92
執行緒2:----93
執行緒1:----94
執行緒2:----95
執行緒2:----97
執行緒1:----97
執行緒1:----98
執行緒2:----99複製程式碼

執行緒1和執行緒2兩個執行緒還是會得出相同的值,這就是因為同時有2個執行緒修改了值。


總結:
volatile只保證執行緒之間的可見性和禁止重排序,但是不能保證原子性。synchronized可以保證原子性但是synchronized多了會影響效能,jdk1.6後對synchronized有了優化,如何使用需要看實際情況自己衡量。


相關文章