摘要:共享資源臨界資源修飾實(shí)例方法輸出結(jié)果上述代碼與前面不同的是我們同時(shí)創(chuàng)建了兩個新實(shí)例,然后啟動兩個不同的線程對共享變量進(jìn)行操作,但很遺憾操作結(jié)果是而不是期望結(jié)果。
線程安全是并發(fā)編程中的重要關(guān)注點(diǎn),應(yīng)該注意到的是,造成線程安全問題的主要誘因有兩點(diǎn)
一是存在共享數(shù)據(jù)(也稱臨界資源)
二是存在多條線程共同操作共享數(shù)據(jù)
因此為了解決這個問題,我們可能需要這樣一個方案,當(dāng)存在多個線程操作共享數(shù)據(jù)時(shí),需要保證同一時(shí)刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式有個高尚的名稱叫互斥鎖,即能達(dá)到互斥訪問目的的鎖,也就是說當(dāng)一個共享數(shù)據(jù)被當(dāng)前正在訪問的線程加上互斥鎖后,在同一個時(shí)刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。
在 Java 中,關(guān)鍵字 synchronized 可以保證在同一個時(shí)刻,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點(diǎn)確實(shí)也是很重要的。
synchronized關(guān)鍵字最主要有以下3種應(yīng)用方式,下面分別介紹
修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前實(shí)例 的鎖
修飾靜態(tài)方法,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前類對象 的鎖
修飾代碼塊,指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得 給定對象 的鎖。
synchronized作用于實(shí)例方法所謂的實(shí)例對象鎖就是用synchronized修飾實(shí)例對象中的實(shí)例方法,注意是實(shí)例方法不包括靜態(tài)方法
package sychronized; import static net.mindview.util.Print.*; import java.util.concurrent.*; public class AccountingSync2 implements Runnable { //共享資源(臨界資源) static int i = 0; /** * synchronized 修飾實(shí)例方法 */ synchronized void getI() { if (i % 1000000 == 0) { print(i); } } public synchronized void increase() { i++; getI(); } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } print(i); } public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); AccountingSync2 accountingSync2 = new AccountingSync2(); exec.execute(accountingSync2); exec.execute(accountingSync2); exec.shutdown(); } } 最后的結(jié)果為: 1000000 1519541 2000000 2000000
上述代碼中,我們開啟兩個線程操作同一個共享資源即變量i,由于i++操作并不具備原子性,該操作是先讀取值,然后寫回一個新值,相當(dāng)于原來的值加上1,分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那么第二個線程就會與第一個線程一起看到同一個值,并執(zhí)行相同值的加1操作,這也就造成了線程安全失敗,因此對于increase方法必須使用synchronized修飾,以便保證線程安全。
此時(shí)注意到synchronized修飾的是實(shí)例方法increase,在這樣的情況下,當(dāng)前線程的鎖便是實(shí)例對象instance,注意Java中的線程同步鎖可以是任意對象。
當(dāng)一個線程正在訪問一個對象的 synchronized 實(shí)例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當(dāng)一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實(shí)例方法,但是其他線程還是可以訪問該實(shí)例對象的其他非synchronized方法,但是一個 synchronized 方法可以調(diào)用另一個需要獲得同樣鎖的synchronized方法,因?yàn)橐呀?jīng)獲取了鎖。
如果是一個線程 A 需要訪問實(shí)例對象 obj1 的 synchronized 方法 f1(當(dāng)前對象鎖是obj1),另一個線程 B 需要訪問實(shí)例對象 obj2 的 synchronized 方法f2(當(dāng)前對象鎖是obj2),這樣是允許的,因?yàn)閮蓚€實(shí)例對象鎖并不同相同,此時(shí)如果兩個線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了。
package sychronized; import static net.mindview.util.Print.*; import java.util.concurrent.*; public class AccountingSync2 implements Runnable { //共享資源(臨界資源) static int i = 0; /** * synchronized 修飾實(shí)例方法 */ synchronized void getI() { if (i % 1000000 == 0) { print(i); } } public synchronized void increase() { i++; getI(); } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } print(i); } public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); AccountingSync2 accountingSync2 = new AccountingSync2(); exec.execute(accountingSync2); exec.execute(new AccountingSync2()); exec.shutdown(); } } #輸出結(jié)果: 1000000 1249050 1329218
上述代碼與前面不同的是我們同時(shí)創(chuàng)建了兩個新實(shí)例AccountingSync2,然后啟動兩個不同的線程對共享變量i進(jìn)行操作,但很遺憾操作結(jié)果是1329218而不是期望結(jié)果2000000。
雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的實(shí)例對象,這也就意味著存在著兩個不同的實(shí)例對象鎖,因此兩個進(jìn)程都會進(jìn)入各自持有對象的對象鎖,也就是說兩個線程使用的是不同的鎖,因此線程安全是無法保證的。
解決這種困境的的方式是將synchronized作用于靜態(tài)的increase方法,這樣的話,對象鎖就當(dāng)前類對象,由于無論創(chuàng)建多少個實(shí)例對象,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。下面我們看看如何使用將synchronized作用于靜態(tài)的increase方法。
synchronized作用于靜態(tài)方法當(dāng)synchronized作用于靜態(tài)方法時(shí),其鎖就是當(dāng)前類的class對象鎖。由于靜態(tài)成員不專屬于任何一個實(shí)例對象,是類成員,因此通過class對象鎖可以控制靜態(tài)成員的并發(fā)操作。需要注意的是如果一個線程A調(diào)用一個實(shí)例對象的非static synchronized 方法,而線程B需要調(diào)用這個實(shí)例對象所屬類的靜態(tài) synchronized方法,是允許的,不會發(fā)生互斥現(xiàn)象,因?yàn)?strong>訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的 class 對象,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對象鎖。
package sychronized; import static net.mindview.util.Print.*; import java.util.concurrent.*; class OtherTask implements Runnable{ AccountingSyncClass accounting = new AccountingSyncClass(); @Override public void run(){ for (int j = 0; j < 1000000; j++) { accounting.increaseForObject(); } print(accounting.getI()); } } public class AccountingSyncClass implements Runnable { //共享資源(臨界資源) private static int i = 0; /** * synchronized 修飾實(shí)例方法 */ public synchronized void increaseForObject() { i++; } public synchronized static void increase() { i++; } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } print(i); } public int getI(){ return i; } public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new AccountingSyncClass()); exec.execute(new AccountingSyncClass()); exec.execute(new OtherTask()); // 1 exec.shutdown(); } } 輸出結(jié)果為: 1459696 2692181 2754098 注釋掉代碼中的 1 那一行代碼的輸出結(jié)果為: 1468495 2000000
由于synchronized關(guān)鍵字修飾的是靜態(tài)increase方法,與修飾實(shí)例方法不同的是,其鎖對象是當(dāng)前類的class對象。
注意代碼中的increase4Obj方法是實(shí)例方法,其對象鎖是當(dāng)前實(shí)例對象,如果別的線程調(diào)用該方法,將不會產(chǎn)生互斥現(xiàn)象,畢竟鎖對象不同,但我們應(yīng)該意識到這種情況下可能會發(fā)現(xiàn)線程安全問題(操作了共享靜態(tài)變量i)。
因此在設(shè)計(jì)同步代碼的時(shí)候一定要仔細(xì)思考到底該用 多大的同步粒度 和 該對什么對象 使用同步操作。
synchronized同步代碼塊除了使用關(guān)鍵字修飾實(shí)例方法和靜態(tài)方法外,還可以使用同步代碼塊,在某些情況下,我們編寫的方法體可能比較大,同時(shí)存在一些比較耗時(shí)的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進(jìn)行同步操作,可能會得不償失,此時(shí)我們可以使用同步代碼塊的方式對需要同步的代碼進(jìn)行包裹,這樣就無需對整個方法進(jìn)行同步操作了。
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其他耗時(shí)操作.... //使用同步代碼塊對變量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í)例對象instance,即當(dāng)前實(shí)例對象就是鎖對象,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時(shí)就會要求當(dāng)前線程持有instance實(shí)例對象鎖。
如果當(dāng)前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執(zhí)行i++操作。
當(dāng)然除了instance作為對象外,我們還可以使用this對象(代表當(dāng)前實(shí)例)或者當(dāng)前類的class對象作為鎖。
//this,當(dāng)前實(shí)例對象鎖 synchronized(this){ for(int j=0;j<1000000;j++){ i++; } } //class對象鎖 synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; } }同步方法最好運(yùn)用在 共享資源 內(nèi)部而不是使用它的外部
package sychronized; import static net.mindview.util.Print.*; import java.util.concurrent.*; class OtherTask implements Runnable{ AccountingSyncClass accounting = new AccountingSyncClass(); @Override public void run(){ for (int j = 0; j < 1000000; j++) { accounting.increaseForObject(); } print(accounting.getAdapterInteger()); } } class AdapterInteger { private int i = 0; public synchronized void increase(){ ++i; } public synchronized int getI(){ return i; } } public class AccountingSyncClass implements Runnable { //共享資源(臨界資源) private static AdapterInteger adapterInteger = new AdapterInteger(); /** * synchronized 修飾實(shí)例方法 */ public synchronized void increaseForObject() { adapterInteger.increase(); } public synchronized static void increase() { adapterInteger.increase(); } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } print(getAdapterInteger()); } public static int getAdapterInteger() { return adapterInteger.getI(); } public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new AccountingSyncClass()); exec.execute(new AccountingSyncClass()); exec.execute(new OtherTask()); exec.shutdown(); } } #輸出結(jié)果為 1183139 2688189 3000000
這樣三個線程中的任務(wù)的鎖都是我們的共享變量 adapterInteger 對象的鎖,這樣就可以完成真正的同步,不管哪個線程都是獲得了 adapterInteger 對象的鎖才能運(yùn)行相應(yīng)的代碼。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/70656.html
摘要:本文對多線程基礎(chǔ)知識進(jìn)行梳理,主要包括多線程的基本使用,對象及變量的并發(fā)訪問,線程間通信,的使用,定時(shí)器,單例模式,以及線程狀態(tài)與線程組。源碼采用構(gòu)建,多線程這部分源碼位于模塊中。通知可能等待該對象的對象鎖的其他線程。 本文對多線程基礎(chǔ)知識進(jìn)行梳理,主要包括多線程的基本使用,對象及變量的并發(fā)訪問,線程間通信,lock的使用,定時(shí)器,單例模式,以及線程狀態(tài)與線程組。 寫在前面 花了一周時(shí)...
摘要:為什么叫重入鎖呢,我們把它拆開來看就明了了。釋放鎖,每次鎖持有者數(shù)量遞減,直到為止。 相信大家在工作或者面試過程中經(jīng)常聽到重入鎖這個概念,或者與關(guān)鍵字 synchrozied 的對比,棧長面試了這么多人,80%的面試者都沒有答對或沒有答到點(diǎn)上,或者把雙重效驗(yàn)鎖搞混了,哭笑不得。。 那么你對重入鎖了解有多少呢?今天,棧長幫大家撕開重入鎖的面紗,來見識下重入鎖的真實(shí)容顏。。 什么是重入鎖 ...
摘要:先上一張線程狀態(tài)轉(zhuǎn)換圖做如下說明代碼中共有除之外的種狀態(tài),為了表示線程正在執(zhí)行,特加了這種狀態(tài)。但是由于和以及用于協(xié)調(diào)對共享資源的存取,所以必須在塊中使用。所以即便狀態(tài)的線程被喚醒了,也需要再次獲得鎖,所以喚醒后進(jìn)入狀態(tài)。 1.先上一張線程狀態(tài)轉(zhuǎn)換圖 showImg(https://segmentfault.com/img/bVJQ8i?w=791&h=424); 做如下說明:代碼中共...
摘要:之前我們簡單的討論了一下關(guān)于中的同步還有一些鎖優(yōu)化的問題,今天我們就來簡單的聊一聊關(guān)于中的死鎖問題。這里顯示兩個線程的狀態(tài)現(xiàn)在是處于阻塞狀態(tài),然后都在等待鎖的獲取,我們再繼續(xù)往下看。 之前我們簡單的討論了一下關(guān)于Java中的同步還有一些鎖優(yōu)化的問題,今天我們就來簡單的聊一聊關(guān)于Java中的死鎖問題。 這個問題我們在開發(fā)的時(shí)候,或多或少都能遇到,對業(yè)務(wù)邏輯沒有正確的梳理,又或者是在多線程...
閱讀 1396·2023-04-25 18:34
閱讀 3459·2021-11-19 09:40
閱讀 2836·2021-11-17 09:33
閱讀 2950·2021-11-12 10:36
閱讀 2837·2021-09-26 09:55
閱讀 2663·2021-08-05 10:03
閱讀 2527·2019-08-30 15:54
閱讀 2873·2019-08-30 15:54