參考文章: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