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