Java 多線程(二) synchronized關鍵字

1.概要

  • 線程安全:多個線程同時訪問公共對象或者同一個對象時,採用了加鎖的機制,對公共數據進行保護,直到線程對該數據使用完。
  • 非線程安全:多個線程同時訪問公共對象或者同一個對象時,發生數據不一致或者數據污染
  • 髒讀:讀到的數據其實是被更改過的,數據不一致或者數據污染。

2.Synchronized方法與鎖對象

  • synchronized

1.監視器:每個對象都有一個監視器(monitor),它允許每個線程同時互斥和協作,就像每個對象都會有一塊受監控的區域

 (數據結構),當線程執行需要取到監控區域的數據時,首先驗證是否有線程擁有監視器,已有線程擁有監視器則進入監視

 器的monior entry list進行等待Thread.state:BLOCKED,直到釋放退出監控區域且釋放鎖。以這種FIFO的方式等待。

  • 對象鎖:每個對象在堆內存中的頭部都會維持一塊鎖區域,任何線程要同步執行對象數據都會放入監視器且都必須獲鎖。

 一個線程可以允許多次對同一對象上鎖.對於每一個對象來說,java虛擬機維護一個計數器,記錄對象被加了多少次鎖,沒被鎖的

 對象的計數器是0,線程每加鎖一次,計數器就加1,每釋放一次,計數器就減1.當計數器跳到0的時候,鎖就被完全釋放了

synchronized工作機制是這樣的:Java中每個對象都有一把鎖與之相關聯,

鎖控制着對象的synchronized代碼。一個要執行對象的synchronized代碼

的線程必須先獲得那個對象的鎖。

  • synchronized 同步方法

 一個對象只有一把對象鎖

 synchronized獲取的是對象鎖,保證線程順序進入對象方法。

public class test {
	public void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			Thread.sleep(5000);
			
			System.out.println("end");
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 執行結果:可以看出線程是並行運行的。
start:A
start:B
end
end

 在方法加入同步synchronized關鍵字,執行結果:線程同步順序執行

start:A
end
start:B
end
 鎖重入:關鍵字synchronized擁有鎖重入的功能,當一個線程得到對象鎖後在當前線程能夠再次獲得此對象鎖,這說明一個

線程在得到對象鎖後可以無限制的獲得對象鎖。

public class test {
	
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public synchronized void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			methodB();
			
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	
	public synchronized void methodB() throws InterruptedException{
		Thread.sleep(5000);
		System.out.println("end");
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 執行結果:自己可以再次獲取自己的內部對象鎖,當線程獲取到對象鎖後在其內部還可以獲得對象鎖,如果鎖不可重入的話

就很容易造成死鎖,因爲外部對象鎖還未釋放,導致在內部永遠獲取不到對象鎖,線程永遠處於等待。

start:A
end
start:B
end
 出現異常,鎖會自動釋放:當一個線程執行代碼是出現異常時,會自動釋放所持有的對象鎖。


 同步不具有繼承性:當父類方法進行同步,子類重寫該方法,子類方法不具有同步性,需要添加synchronized關鍵字。

  • synchronized同步代碼塊

 用synchronized同步方法是有弊端的,當方法某一條語句執行時間過長,就會導致其他線程需要等待較長時間。所以同步 代碼塊可以相對提高效率。

  • 同步代碼塊的使用

synchronized需要依賴於對象鎖,同步代碼塊是需要一個鎖對象,可以是當前對象(this),一般系統併發量很高不採用當

前對象,而採用任意其他一個對象,不然造成大量線程等待在該對象。

public class test {
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public  void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			synchronized (this) {
				methodB();
			}
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	public synchronized void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 執行結果:線程A、B併發進入方法,但有一個線程等待在同步代碼塊前。
 
start:A
start:B
end
end
 任意對象鎖:在同步代碼塊不取this(當前對象)鎖,而採用任意對象鎖,這樣的好處是當有很多synchronized同步方法時

,如果用this對象鎖,會造成阻塞,而採用任意鎖,其他同步塊則不會造成阻塞。

public class test {
	private Object obj = new Object();
	public void methodA(){
		System.out.println("非synchronized方法");
	}
	
	public  void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			synchronized (this) {
				methodB();
			}
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	public synchronized void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	
	public void methodC(){
		synchronized (obj) {
			try {
				System.out.println("start:"+Thread.currentThread().getName());
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();//執行methodC
	}
}

 執行結果:線程A使用當前this對象鎖同步代碼塊,線程B能夠同步執行OBJ對象鎖進行同步代碼塊

start:A
start:B
end
 同步代碼塊解鎖無限等待問題:同步方法獲得的是當前對象的對象鎖,一單其中一個同步方法陷入死循環,該對象的其他同

步方法都無限等待,所以同步需要同步的部分代碼塊且使用任意對象鎖。

  • synchronized同步靜態方法

 關鍵字synchronized還可以修飾static方法,如果這樣寫,那是對當前的.java文件的class對象進行加鎖。

  • 與實例方法取得不同的對象鎖

 Java程序在運行時,Java運行時系統一直對所有的對象進行所謂的運行時類型標識,即所謂的RTTI。這項信息紀錄了

每個對象所屬的類。虛擬機通常使用運行時類型信息選準正確方法去執行,用來保存這些類型信息的類是Class類。

Class類封裝一個對象和接口運行時的狀態,當裝載類時,Class類型的對象自動創建。

public class test {
	private Object obj = new Object();
	public synchronized void methodA(){
		System.out.println("start not static :"+Thread.currentThread().getName());
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("static end");
	}
	
	public synchronized static void testSynchronized(){
		try {
			System.out.println("start:"+Thread.currentThread().getName());
			methodB();
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	public static  void methodB() throws Exception{
		Thread.sleep(5000);
		System.out.println("end");
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 執行結果:兩個線程獲取的是不同的對象鎖,一個是test.classs對象鎖,一個是test對象鎖
start not static :A
start:B
static end
end 
  • String常量池類型鎖

 在JVM中String有常量池緩存的特性

 什麼事常量池?:

  常量池(constant pool)在編譯期間被指定,並被保存在已編譯的.class文件當中,用於存儲關於類、方法、接口中 的常量,也包括字符串直接量

 String與常量池

  String str1 = new String("abc")

  String str2 = "abc";

 上面是兩種創建字符串的方式,看起來沒有什麼區別,但實則有很大區別

 第一種是用new()來新建對象的,它會在存放於堆中。每調用一次就會創建一個新的對象。

 而第二種是先在棧中創建一個對String類的對象引用變量str2,然後通過符號引用去字符串常量池裏找有沒有"abc",如果沒有

,則將"abc"存放進字符串常量池,並令str2指向”abc”,如果已經有”abc” 則直接令str2指向“abc”。

所以如果String str3 = “abc”;str2 和str3是同一對象,String str4 = “adcd”;str4和str2是不同的對象。

結論:如果使用String對象作爲對象鎖,必須要注意是否對象會改變;這就是String常量池帶來的問題,一般不會用String做

爲對象鎖,而改用其他,比如 new Object()。


  • 死鎖

 由於不同的線程都在等待永遠不能被釋放的鎖,從而導致任務不能繼續執行。在多線程中死鎖是必須避免的,會導致線程的

 假死。

public class test {
	public synchronized void methodA(){
		System.out.println("start:"+Thread.currentThread().getName());
		try {
			Thread.sleep(5000);
			
			while(true){
				
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("static end");
	}
	
	public static void main(String[] args) {
		test t = new test();
		ThreadA threadA = new ThreadA(t);
		threadA.setName("A");
		threadA.start();
		
		ThreadB threadB = new ThreadB(t);
		threadB.setName("B");
		threadB.start();
	}
}
 執行結果:程序會一直等待。

 我們可以通過jstack 命令來查看jvm內線程狀態,來找到死鎖的地方。

  • volatile關鍵字

 volatile關鍵字的主要作用是使變量在多個線程中可見。

 強制從公共的堆棧中取得變量的值,而不是在線程的私有棧中取變量的值。

 解決同步死循環:

public class RunThread implements Runnable{
	private boolean isRun = true;
	
	public void setIsRun(boolean flag){
		this.isRun = flag;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(isRun == true){
			try {
				Thread.sleep(2000);
				System.out.println("當前線程:"+Thread.currentThread().getName());
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("end");
	}
}
public class test {
	public static void main(String[] args) {
		RunThread runthread = new RunThread();
		new Thread(runthread).start();
		System.out.println("我要停止了");
		runthread.setIsRun(false);
		
	}
}

 執行結果:上面代碼運行在64bitJVM,-server模式程序陷入死循環。解決辦法是使用volatile關鍵字,從公共堆棧中取得數

據,而不是線程私有棧中

 volatile非原子性

public class MyThread extends Thread{
	volatile private static int count = 0;
	public void run(){
		addCount();
	}
	
	public void addCount(){
		for(int i = 0;i<100;i++){
			count++;
		}
		System.out.println(count);
	}
}
 主類:
public class test {
	public static void main(String[] args) {
		MyThread [] threadArr = new MyThread[100];
		for(int i=0;i<100;i++){
			threadArr[i] = new MyThread();
		}
		
		for(int i =0;i<100;i++){
			threadArr[i].start();
		}
		
	}
}
 執行結果

8500
8900
9100
9204
9298
9398
9498
9598
9698
9798
9898
9998

 關鍵字volatile主要使用場合是在多線程中可以感知變量值更改了,並且可以獲得最新的值,每次取值都是從共享內存中

 取的數據,而不是從線程的私有內存中取得數據。

 但如果修改實例變量,比如i++,這樣的操作並不是一個原子操作,也是非線程安全的。表達式的步驟是:

 1)從內存中取得i的值

 2)計算i的值

 3)將i的值寫到主存中

 如果在第二步計算值時,其他線程也在修改i的值,就會出現髒讀。解決辦法是使用synchronized,volatile只能保證從

 每次從主存中取得i的值。所以說volatile並不具有原子性。而是強制數據的讀寫影響到主存中去。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章