voliate Synchronized Lock

參考文章:https://blog.csdn.net/huyiju/article/details/97126274
一、voliate相關

1:java內存模型

1.1:計算機的內存模型
在計算機的內存模型中cpu和內存之間的速度存在數量級所以引入了高速緩存,告訴緩存會導致到底以哪個處理器的緩存爲主,同步到主內存,這個時候有有了緩存一致性協議,來保證緩存一致性。

在這裏插入圖片描述
指令重排:例如一下五行代碼,前四行的在計算機cpu的執行順序不一定是12345,也可以是13245或者34125,但是第五步的順序不會變,這種指令重排不會影響最後的計算結果。

          int a=1;

          a++;

          int b=5;

          b++;

         int c=a+b;

1.2:java內存模型

java的內存模型屏蔽掉了計算機硬件和操作系統的差異,但是沒有限制處理器使用緩存和主內存進行交互,也沒有限制編譯器在執行過程中的指令重排這類優化措施。
在這裏插入圖片描述

2:Voliate關鍵字
voliate關鍵字有兩個作用(可見性和禁止指令重排)

1:可見性:保證在多個線程的情況下,線程一把int a的值修改爲5的時候,其他線程也能立即知道int a=5,實現的方法是線程1把int a=5,會把a=5的是立馬通過緩存同步到主內存,然後其他線程使用a之前會從主內存刷新一次,得到被線程1修改的值爲5.

2:有序性:有序性的意思的在本線程內觀察,所有操作都是有序的(線程內是串行的),但是在另一個線程觀察本線程,所有操作都是無序的(主要是指指令重排現象和工作內存與主內存同步)。voliate和synchronize兩個關鍵字來保證線程操作之間的有序性。

2.1:voliate不能保證線程安全(可見性分析)
預期結果:10個線程同時執行,每一個此線程都把i累加到1000.最後的結果應該是10000.

實際結果:多次運行,大部分結果都不足10000

結論分析:i++,不是原子性操作,線程1把i的值賦值的10,這個時候後線程2根據可見性也得到了10,但是線程1接着執行累加把i的值賦值到250,這個時候線程2依然拿到的值是10是過期數據,入棧++後的值爲11,把11同步到了主內存,觸發了線程1從主內存同步的得到值爲11,這個時候會導致線程一的結果錯誤。

public class VoliteTest {
	//voliate關鍵字保證了此變量在各個線程中是一致的
	public static volatile int id=1;
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		Thread[] ths=new Thread[10];
		for(int i=0;i<10;i++) {
			//10個線程同時執行
			ths[i]=new Thread(new Runnable() {
				@Override
				public void run() {
					// TODO Auto-generated method stub
					add();
				}
			});
			ths[i].start();
			
		}
		
		//累加線程執行完畢
		while (Thread.activeCount()>1) {
			Thread.yield();
		}
		System.out.println("========================");
		System.out.println("ID的值是:"+id);
	}
	
	//該方法可以通過加鎖實現線程安全
	public  static void add() {
		for(int i = 0;i<1000;i++) {
			//id++不是原子性的
			//第一步:假如線程1同步id=5,這個時候要++,其他線程切換了進來
			//第二部:其他比如線程2同步id=5,執行了很多操作,id=100了
			//第三部:線程1執行++,得到了id=6,這個時候的id爲5已經是過期數據了,
			//第四部:線程1將結果6同步到內存共享,其他的線程得到id=6.會導致最後的值不等於10001
		    id++;
		}
	}
 
}

2.2:voliate禁止指令重排
voliate保證cpu能夠保證在這個關鍵字之前和之後的代碼不會指令重排,保證按照書寫順序執行代碼。

之所以能實現指令重排是因爲voliate的可見性決定的,這個關鍵字保證了從緩存到主內存之間的同步,就像內存之間的一道屏障,保證了之前和之後的代碼無法越過這個 內存柵欄。

單例代碼案例:保證線程安全和只有一個實例

public class Danli {
	
	private volatile static Danli danli;
	
	//私有構造,防止通過外部new創建對象
	private Danli() {
		
	}
	
	//懶漢模式  需要的時候get
	//(方法加鎖導致多線程效率低)
	public static synchronized Danli getDanli() {
		if(danli==null) {
			//在此處判斷加鎖,防止線程1判斷爲空後,線程二創建了實例
			//在加鎖方法之內再次判斷一次
			synchronized (Danli.class) {
				if(danli==null) {//再次判斷
					danli=new Danli();//內存屏障,保證寫完之後其他線程讀取
					//1:在工作內存創建,(store存儲)到主內存
					//在此之間可能有其他線程插入
					//2:主內存的danli(write寫入)
                           //這是一個內存柵欄
				}
			}
		}
		return danli;
	}
 
}

二、synchronize關鍵字

2.1:說一下對synchronize關鍵字的理解
synchronize關鍵字解決了多線程的資源同步性,該關鍵字保證了在多線程的條件下,同時是有一個線程能夠獲取到資源。

在jdk的早起版本中,這個關鍵字是重量級鎖,這個關鍵字在編譯之後,在同步塊的前後會形成monitorenter和monitorexit(監視器進入和退出,通過獲取對象鎖計數器加一減一來實現鎖機制)兩個節碼指令,其他線程在競爭的時候會掛起,效率低下,但是隨着jdk的發展,通過對synchronize底層的發展,不必每次線程都掛起。來大大提升了效率

比如採用偏向鎖、輕量級鎖、鎖粗化、鎖自旋、鎖自旋、鎖消除等手段來提升效率

2.2:synchronize在用項目中用到了嗎?
2.2.1:synchronize用法
1:用來修飾靜態方法(給類加了鎖,無論new多少個對象,都會產生競爭,線程1new的對象獲取普通加鎖方法和線程2獲取類的靜態加鎖方法不會產生競爭)

2:用來修飾靜態代碼塊(也是給類加了鎖)

3:用來修飾普通方法(鎖給了對象,只用new的同一對象纔有競爭)

2.2.2:項目中的使用(懶漢單例模式)

package com.thit.connpool;
 
public class LazySimple {
	//voliate修飾,禁止指令重排
	private static volatile LazySimple lazySimple;
	//私有構造防止new創建對象
	private LazySimple() {
		
	}
	//靜態方法外部得到實例
	public static LazySimple getDanli() {
		//首先判斷是否爲空
		if (lazySimple==null) {
			synchronized (LazySimple.class) {
				//再次判斷是否爲空
				if(lazySimple==null) {
					//new 對象非原子性操作
					//1:分配內存空間
					//2:初始化值(構造器)
					//3:將對象指向內存地址
					lazySimple=new LazySimple();
				}
			}
		}
		return lazySimple;
	}
}

代碼分析:

1:其中 volatile修飾lazySimple;是爲了防止指令重排,其中lazySimple=new LazySimple();這段代碼不是原子性操作,實際分爲3步

                //1:分配內存空間
                //2:初始化值
                //3:將引用指向內存地址
                lazySimple=new LazySimple();

由於jvm有指令重排的優化,所以代碼執行順序可能是1>3>2,當線程1執行1>3的時候沒有執行到2初始化賦值的時候,線程2進入執行,這個時候判斷到lazySimplenull,會在創造一個對象,就不是單例了。加了voliate關鍵字修飾,會因爲可見性而防止指令重排,因爲其他的線程想要判斷lazySimplenull的時候,會在1>2>3執行完成,在線程1修改lazySimple的時候,沒寫入之前內存像是有屏障一般。這邊形成了指令重排無法越過的內存屏障。

三、說說synchronize關鍵字和voliate關鍵字
1:voliate修飾變量多線程可見性但是沒有原子性,synchronize修飾方法能保證可見性和原子性。

2:voliate的效率更高一點,線程不會阻塞,但是synchronize線程會阻塞

3:voliate用來解決變量的多線程可見性,但是synchronize用來解決多線程資源訪問問題

四、lock接口
4.1:爲什麼需要Lock接口?

我們知道鎖是來控制多線程併發訪問資源的競爭,在Lock接口出現之前我們使用synchronize關鍵字來實現鎖功能,但是synchronize獲取鎖是隱式的,我們無法控制鎖的獲取,釋放中斷的可操作性性,由於synchronize獲取鎖固話,所以創建了Lock接口來實現更領過的鎖。

Lock和synchronize都是重入鎖

1:嘗試非中斷的獲取鎖,

2:可以實現中斷,與synchronize關鍵字不同,當獲取鎖的線程能夠響應中斷,中斷的時候,異常內拋出,釋放鎖

3:能指定時間獲取鎖,在指定時間內無法獲取鎖的時候返回

4.2:代碼實現

Lock lock=new ReentrantLock();
		lock.lock();//獲取鎖
		try {
			
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();//finally修飾,異常處理,一定要釋放鎖
		}

原文鏈接:https://blog.csdn.net/huyiju/article/details/97646152

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