layout: post
title: "多線程安全-sychronized"
categories: [編程]
tags: [Java,多線程]
published: True
造成線程數(shù)據(jù)錯亂的三要素 ( 同時也是保持線程安全的要素 )
名詞解釋
- 原子性(Synchronized, Lock)即一個操作或者多個操作甩骏,要么執(zhí)行 要么就都不執(zhí)行抄伍,在執(zhí)行過程中不可打斷
- 有序性 (Volatile,Synchronized, Lock) 程序默認(rèn)的執(zhí)行順序
- 可見性 (Volatile集晚,Synchronized,Lock) 當(dāng)變量被修改時,所修改的值會被立即同步到主存中蜓谋,其他程序或者進(jìn)程再讀取時獲取的值是最新的
synchronized的三種應(yīng)用方式
- 修飾實(shí)例方法梦皮,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
public class AccountingSync implements Runnable{
//共享資源(臨界資源)
static int i=0;
/**
* synchronized 修飾實(shí)例方法
* 暫時先不添加
*/
public void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 輸出結(jié)果:
* 1802452
*/
}
i++ 賦值操作并沒有 原子性 在讀取原來的參數(shù)和返回新的參數(shù)的這段時間中桃焕,如果我們第二個線程也做了同樣的操作剑肯,兩個線程看到的參數(shù)是一樣的,同樣執(zhí)行了 +1 的操作观堂,這里就造成了線程安全破壞让网,我們最終輸出的結(jié)果是 1802452
如果我們添加上 synchronized 此時 increase() 方法在一個時間內(nèi) 只能被一個線程讀寫,也就避免臟數(shù)據(jù)的產(chǎn)生师痕。
但是這樣也不是安全的溃睹,如果我們 new 出兩個 AccountingSync 對象去執(zhí)行操作 結(jié)果是怎么樣?
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實(shí)例
Thread t1=new Thread(new AccountingSyncBad());
//new新實(shí)例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含義:當(dāng)前線程A等待thread線程終止之后才能從thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
結(jié)果依舊是產(chǎn)生了臟數(shù)據(jù)七兜,原因是兩個實(shí)例對象鎖并不同相同丸凭,此時如果兩個線程操作數(shù)據(jù)并非共享的,線程安全是有保障的腕铸,遺憾的是如果兩個線程操作的是共享數(shù)據(jù)惜犀,安全將沒有保障,他們本身的鎖只能保持本身
-
修飾靜態(tài)方法狠裹,作用于當(dāng)前類對象加鎖虽界,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
public class AccountingSyncClass implements Runnable{ static int i=0; /** * 作用于靜態(tài)方法,鎖是當(dāng)前class對象,也就是 * AccountingSyncClass類對應(yīng)的class對象 */ public static synchronized void increase(){ i++; } /** * 非靜態(tài),訪問時鎖不一樣不會發(fā)生互斥 */ public synchronized void increase4Obj(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新實(shí)例 Thread t1=new Thread(new AccountingSyncClass()); //new心事了 Thread t2=new Thread(new AccountingSyncClass()); //啟動線程 t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
這個實(shí)例當(dāng)中我們將 synchronized 作用于靜態(tài)方法了,因?yàn)殪o態(tài)方法不屬于任何實(shí)例對象,它是類成員涛菠,所以這把鎖也可以理解為加在了 Class 上 莉御,但是如果我們線程A 調(diào)用了 class 內(nèi)部 static synchronized 方法 線程B 調(diào)用了 class 內(nèi)部 非 static 方法 是可以的,不會發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對象俗冻,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對象鎖
-
修飾代碼塊礁叔,指定加鎖對象,對給定對象加鎖迄薄,進(jìn)入同步代碼庫前要獲得給定對象的鎖琅关。
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其他耗時操作.... //使用同步代碼塊對變量i進(jìn)行同步操作,鎖對象為instance synchronized(instance){ for(int j=0;j<1000000;j++){ i++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
synchronized 是如何實(shí)現(xiàn)的同步
同步代碼塊
內(nèi)部使用 monitorenter 和 monitorexit 指令實(shí)現(xiàn),monitorenter 指令插入到同步代碼塊的開始位置讥蔽,monitorexit 指令插入到同步代碼塊的結(jié)束位置涣易,jvm需要保證每一個monitorenter都有一個 monitorexit 與之對應(yīng)。任何一個對象都有一個 monitor 與之相關(guān)聯(lián)冶伞,當(dāng)它的monitor被持有之后新症,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令前响禽,將會嘗試獲取對象所對應(yīng)的 monitor 所有權(quán)徒爹,即嘗試獲取對象的鎖荚醒;將線程執(zhí)行到 monitorexit 時就會釋放鎖。
(人話:)每個對象都會與一個monitor相關(guān)聯(lián)隆嗅,當(dāng)某個monitor被擁有之后就會被鎖住腌且,當(dāng)線程執(zhí)行到monitorenter指令時,就會去嘗試獲得對應(yīng)的monitor榛瓮。步驟如下:
每個monitor維護(hù)著一個記錄著擁有次數(shù)的計數(shù)器铺董。未被擁有的monitor的計數(shù)器為0,當(dāng)一個線程獲得monitor(執(zhí)行monitorenter)后禀晓,該計數(shù)器自增變?yōu)?1 精续。當(dāng)同一個線程再次獲得該monitor的時候,計數(shù)器再次自增粹懒;當(dāng)不同線程想要獲得該monitor的時候重付,就會被阻塞。
-
當(dāng)同一個線程釋放monitor(執(zhí)行monitorexit指令)的時候凫乖,計數(shù)器再自減确垫。當(dāng)計數(shù)器為0的時候。monitor將被釋放帽芽,其他線程便可以獲得monitor删掀。
同步方法
不過相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符导街。JVM就是根據(jù)該標(biāo)示符來實(shí)現(xiàn)方法的同步的:當(dāng)方法調(diào)用時披泪,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了搬瑰,執(zhí)行線程將先獲取monitor款票,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor泽论。
在jvm字節(jié)碼層面并沒有任何特別的指令來實(shí)現(xiàn)synchronized修飾的方法艾少,而是在class文件中將該方法的access_flags字段中的acc_synchronized標(biāo)志位設(shè)置為1,表示該方法為synchronized方法翼悴。
在java設(shè)計中缚够,每一個對象自打娘胎里出來就帶了一把看不見的鎖,即monitor鎖抄瓦。monitor是線程私有的數(shù)據(jù)結(jié)構(gòu)潮瓶,每一個線程都有一個monitor record列表陶冷,同時還有一個全局可用列表钙姊。每一個被鎖住對象都會和一個monitor關(guān)聯(lián)。monitor中有一個owner字段存放擁有該對象的線程的唯一標(biāo)識埂伦,表示該鎖被這個線程占有煞额。owner:初始時為null,表示當(dāng)前沒有任何線程擁有該monitor,當(dāng)線程成功擁有該鎖后膊毁,owner保存線程唯一標(biāo)識胀莹,當(dāng)鎖被釋放時,owner又變?yōu)閚ull婚温。
總結(jié):
同步方法和同步代碼塊底層都是通過monitor來實(shí)現(xiàn)同步的描焰。兩者的區(qū)別:同步方式是通過方法中的access_flags中設(shè)置ACC_SYNCHRONIZED標(biāo)志來實(shí)現(xiàn);同步代碼塊是通過monitorenter和monitorexit來實(shí)現(xiàn)我們知道了每個對象都與一個monitor相關(guān)聯(lián)栅螟。而monitor可以被線程擁有或釋放荆秦。