欧美亚洲中文,在线国自产视频,欧洲一区在线观看视频,亚洲综合中文字幕在线观看

      1. <dfn id="rfwes"></dfn>
          <object id="rfwes"></object>
        1. 站長資訊網(wǎng)
          最全最豐富的資訊網(wǎng)站

          一起聊聊Java多線程之線程安全問題

          本篇文章給大家?guī)砹岁P(guān)于java的相關(guān)知識,其中主要介紹了關(guān)于多線程的相關(guān)問題,包括了線程安裝、線程加鎖與線程不安全的原因、線程安全的標(biāo)準(zhǔn)類等等內(nèi)容,希望對大家有幫助。

          一起聊聊Java多線程之線程安全問題

          推薦學(xué)習(xí):《java視頻教程》

          本篇文章介紹的內(nèi)容為Java多線程中的線程安全問題,此處的安全問題并不是指的像黑客入侵造成的安全問題,線程安全問題是指因多線程搶占式執(zhí)行而導(dǎo)致程序出現(xiàn)bug的問題。

          1.線程安全概述

          1.1什么是線程安全問題

          首先我們需要明白操作系統(tǒng)中線程的調(diào)度是搶占式執(zhí)行的,或者說是隨機(jī)的,這就造成線程調(diào)度執(zhí)行時線程的執(zhí)行順序是不確定的,有一些代碼執(zhí)行順序不同不影響程序運(yùn)行的結(jié)果,但也有一些代碼執(zhí)行順序發(fā)生改變了重寫的運(yùn)行結(jié)果會受影響,這就造成程序會出現(xiàn)bug,對于多線程并發(fā)時會使程序出現(xiàn)bug的代碼稱作線程不安全的代碼,這就是線程安全問題。

          下面,將介紹一種典型的線程安全問題實(shí)例,整數(shù)自增問題。

          1.2一個存在線程安全問題的程序

          有一天,老師布置了這樣一個問題:使用兩個線程將變量count自增10萬次,每個線程承擔(dān)5萬次的自增任務(wù),變量count的初始值為0。
          這個問題很簡單,最終的結(jié)果我們也能夠口算出來,答案就是10萬。
          小明同學(xué)做事非常迅速,很快就寫出了下面的一段代碼:

          class Counter {     private int count;     public void increase() {         ++this.count;     }     public int getCount() {         return this.count;     }}public class Main11 {     private static final int CNT = 50000;     private static final Counter counter = new Counter();     public static void main(String[] args) throws InterruptedException {          Thread thread1 = new Thread(() -> {             for (int i = 0; i < CNT; i++) {                 counter.increase();             }         });         Thread thread2 = new Thread(() -> {             for (int j = 0; j < CNT; j++) {                 counter.increase();             }         });          thread1.start();         thread2.start();          thread1.join();         thread2.join();          System.out.println(counter.getCount());     }}

          按理來說,結(jié)果應(yīng)該是10萬,我們來看看運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          運(yùn)行的結(jié)果比10萬要小,你可以試著運(yùn)行該程序你會發(fā)現(xiàn)每次運(yùn)行的結(jié)果都不一樣,但絕大部分情況,結(jié)果都會比預(yù)期的值要小,下面我們就來分析分析為什么會這樣。

          2.線程加鎖與線程不安全的原因

          2.1案例分析

          上面我們使用多線程運(yùn)行了一個程序,將一個變量值為0的變量自增10萬次,但是最終實(shí)際結(jié)果比我們預(yù)期結(jié)果要小,原因就是線程調(diào)度的順序是隨機(jī)的,造成線程間自增的指令集交叉,導(dǎo)致運(yùn)行時出現(xiàn)兩次自增但值只自增一次的情況,所以得到的結(jié)果會偏小。

          我們知道一次自增操作可以包含以下幾條指令:

          1. 將內(nèi)存中變量的值加載到寄存器,不妨將該操作記為load。
          2. 在寄存器中執(zhí)行自增操作,不妨將該操作記為add
          3. 將寄存器的值保存至內(nèi)存中,不妨將該操作記為save。

          我們來畫一條時間軸,來總結(jié)一下常見的幾種情況:

          情況1: 線程間指令集,無交叉,運(yùn)行結(jié)果與預(yù)期相同,圖中寄存器A表示線程1所用的寄存器,寄存器B表示線程2所用的寄存器,后續(xù)情況同理。
          一起聊聊Java多線程之線程安全問題
          情況2: 線程間指令集存在交叉,運(yùn)行結(jié)果低于預(yù)期結(jié)果。
          一起聊聊Java多線程之線程安全問題
          情況3: 線程間指令集完全交叉,實(shí)際結(jié)果低于預(yù)期。
          一起聊聊Java多線程之線程安全問題
          根據(jù)上面我們所列舉的情況,發(fā)現(xiàn)線程運(yùn)行時沒有交叉指令的時候運(yùn)行結(jié)果是正常的,但是一旦有了交叉會導(dǎo)致自增操作的結(jié)果會少1,綜上可以得到一個結(jié)論,那就是由于自增操作不是原子性的,多個線程并發(fā)執(zhí)行時很可能會導(dǎo)致執(zhí)行的指令交叉,導(dǎo)致線程安全問題。

          那如何解決上述線程不安全的問題呢?當(dāng)然有,那就是對對象加鎖。

          2.2線程加鎖

          2.2.1什么是加鎖

          為了解決由于“搶占式執(zhí)行”所導(dǎo)致的線程安全問題,我們可以對操作的對象進(jìn)行加鎖,當(dāng)一個線程拿到該對象的鎖后,會將該對象鎖起來,其他線程如果需要執(zhí)行該對象的任務(wù)時,需要等待該線程運(yùn)行完該對象的任務(wù)后才能執(zhí)行。

          舉個例子,假設(shè)要你去銀行的ATM機(jī)存錢或者取款,每臺ATM機(jī)一般都在一間單獨(dú)的小房子里面,這個小房子有一扇門一把鎖,你進(jìn)去使用ATM機(jī)時,門會自動的鎖上,這個時候如果有人要來取款,那它得等你使用完并出來它才能進(jìn)去使用ATM,那么這里的“你”相當(dāng)于線程,ATM相當(dāng)于一個對象,小房子相當(dāng)于一把鎖,其他的人相當(dāng)于其他的線程。
          一起聊聊Java多線程之線程安全問題
          一起聊聊Java多線程之線程安全問題
          在java中最常用的加鎖操作就是使用synchronized關(guān)鍵字進(jìn)行加鎖。

          2.2.2如何加鎖

          synchronized 會起到互斥效果, 某個線程執(zhí)行到某個對象的 synchronized 中時, 其他線程如果也執(zhí)行到同一個對象 synchronized 就會阻塞等待。
          線程進(jìn)入 synchronized 修飾的代碼塊, 相當(dāng)于加鎖,退出 synchronized 修飾的代碼塊, 相當(dāng)于 解鎖

          java中的加鎖操作可以使用synchronized關(guān)鍵字來實(shí)現(xiàn),它的常見使用方式如下:

          方式1: 使用synchronized關(guān)鍵字修飾普通方法,這樣會使方法所在的對象加上一把鎖。
          例如,就以上面自增的程序?yàn)槔?,嘗試使用synchronized關(guān)鍵字進(jìn)行加鎖,如下我對increase方法進(jìn)行了加鎖,實(shí)際上是對某個對象加鎖,此鎖的對象就是this,本質(zhì)上加鎖操作就是修改this對象頭的標(biāo)記位。

          class Counter {     private int count;     synchronized public void increase() {         ++this.count;     }     public int getCount() {         return this.count;     }}

          多線程自增的main方法如下,后面會以相同的栗子介紹synchronized的其他用法,后面就不在列出這段代碼了。

          public class Main11 {     private static final int CNT = 50000;     private static final Counter counter = new Counter();     public static void main(String[] args) throws InterruptedException {          Thread thread1 = new Thread(() -> {             for (int i = 0; i < CNT; i++) {                 counter.increase();             }         });         Thread thread2 = new Thread(() -> {             for (int j = 0; j < CNT; j++) {                 counter.increase();             }         });          thread1.start();         thread2.start();          thread1.join();         thread2.join();          System.out.println(counter.getCount());     }}

          看看運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          方式2: 使用synchronized關(guān)鍵字對代碼段進(jìn)行加鎖,但是需要顯式指定加鎖的對象。
          例如:

          class Counter {     private int count;     public void increase() {         synchronized (this){             ++this.count;         }     }     public int getCount() {         return this.count;     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          方式3: 使用synchronized關(guān)鍵字修飾靜態(tài)方法,相當(dāng)于對當(dāng)前類的類對象進(jìn)行加鎖。

          class Counter {     private static int count;     synchronized public static void increase() {         ++count;     }     public int getCount() {         return this.count;     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          常見的用法差不多就是這些,對于線程加鎖(線程拿鎖),如果兩個線程同時拿一個對象的鎖,就會產(chǎn)生鎖競爭,兩個線程同時拿兩個不同對象的鎖不會產(chǎn)生鎖競爭。
          對于synchronized這個關(guān)鍵字,它的英文意思是同步,但是同步在計算機(jī)中是存在多種意思的,比如在多線程中,這里同步的意思是“互斥”;而在IO或網(wǎng)絡(luò)編程中同步指的是“異步”,與多線程沒有半點(diǎn)的關(guān)系。

          synchronized 的工作過程:

          1. 獲得互斥鎖lock
          2. 從主內(nèi)存拷貝變量的最新副本到工作的內(nèi)存
          3. 執(zhí)行代碼
          4. 將更改后的共享變量的值刷新到主內(nèi)存
          5. 釋放互斥鎖unlock

          synchronized 同步塊對同一條線程來說是可重入的,不會出現(xiàn)自己把自己鎖死的問題,即死鎖問題,關(guān)于死鎖后續(xù)文章再做介紹。

          綜上,synchronized關(guān)鍵字加鎖有如下性質(zhì):互斥性,刷新內(nèi)存性,可重入性。

          synchronized關(guān)鍵字也相當(dāng)于一把監(jiān)視器鎖monitor lock,如果不加鎖,直接使用wait方法(一種線程等待的方法,后面細(xì)說),會拋出非法監(jiān)視器異常,引發(fā)這個異常的原因就是沒有加鎖。

          2.2.3再析案例

          對自增那個代碼上鎖后,我們再來分析一下為什么加上了所就線程安全了,先列代碼:

          class Counter {     private int count;     synchronized public void increase() {         ++this.count;     }     public int getCount() {         return this.count;     }}public class Main11 {     private static final int CNT = 50000;     private static final Counter counter = new Counter();     public static void main(String[] args) throws InterruptedException {          Thread thread1 = new Thread(() -> {             for (int i = 0; i < CNT; i++) {                 counter.increase();             }         });         Thread thread2 = new Thread(() -> {             for (int j = 0; j < CNT; j++) {                 counter.increase();             }         });          thread1.start();         thread2.start();          thread1.join();         thread2.join();          System.out.println(counter.getCount());     }}

          多線程并發(fā)執(zhí)行時,上一次就分析過沒有指令集交叉就不會出現(xiàn)問題,因此這里我們只討論指令交叉后,加鎖操作是如何保證線程安全的,不妨記加鎖為lock,解鎖為unlock,兩個線程運(yùn)行過程如下:
          線程1首先拿到目標(biāo)對象的鎖,對對象進(jìn)行加鎖,處于lock狀態(tài),當(dāng)線程2來執(zhí)行自增操作時會發(fā)生阻塞,直到線程1的自增操作完畢,處于unlock狀態(tài),線程2才會就緒取執(zhí)行線程2的自增操作。
          一起聊聊Java多線程之線程安全問題
          加鎖后線程就是串行執(zhí)行,與單線程其實(shí)沒有很大的區(qū)別,那多線程是不是沒有用了呢?但是對方法加鎖后,線程運(yùn)行該方法才會加鎖,運(yùn)行完該方法就會自動解鎖,況且大部分操作并發(fā)執(zhí)行是不會造成線程安全的,只有少部分的修改操作才會有可能導(dǎo)致線程安全問題,因此整體上多線程運(yùn)行效率還是比單線程高得多。

          2.3線程不安全的原因

          首先,線程不安全根源是線程間的調(diào)度充滿隨機(jī)性,導(dǎo)致原有的邏輯被改變,造成線程不安全,這個問題無法解決,無可奈何。

          多個線程針對同一資源進(jìn)行寫(修改)操作,并且針對資源的修改操作不是原子性的,可能會導(dǎo)致線程不安全問題,類似于數(shù)據(jù)庫的事務(wù)。

          由于編譯器的優(yōu)化,內(nèi)存可見性無法保證,就是當(dāng)線程頻繁地對同一個變量進(jìn)行讀操作時,會直接從寄存器上讀值,不會從內(nèi)存上讀值,這樣內(nèi)存的值修改時,線程就感知不到該變量已經(jīng)修改,會導(dǎo)致線程安全問題(這是編譯器優(yōu)化的結(jié)果,現(xiàn)代的編譯器都有類似的優(yōu)化不止于Java),因?yàn)橄啾扔诩拇嫫?,從?nèi)容中讀取數(shù)據(jù)的效率要小的多,所以編譯器會盡可能地在邏輯不變的情況下對代碼進(jìn)行優(yōu)化,單線程情況下是不會翻車的,但是多線程就不一定了,比如下面一段代碼:

          import java.util.Scanner;public class Main12 {     private static int isQuit;     public static void main(String[] args) {         Thread thread = new Thread(() -> {             while (isQuit == 0) {              }             System.out.println("線程thread執(zhí)行完畢!");         });         thread.start();          Scanner sc = new Scanner(System.in);         System.out.println("請輸入isQuit的值,不為0線程thread停止執(zhí)行!");         isQuit = sc.nextInt();         System.out.println("main線程執(zhí)行完畢!");     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          我們從運(yùn)行結(jié)果可以知道,輸入isQuit后,線程thread沒有停止,這就是編譯器優(yōu)化導(dǎo)致線程感知不到內(nèi)存可見性,從而導(dǎo)致線程不安全。
          我們可以使用volatile關(guān)鍵字保證內(nèi)存可見性。
          我們可以使用volatile關(guān)鍵字修飾isQuit來保證內(nèi)存可見性。

          import java.util.Scanner;public class Main12 {     volatile private static int isQuit;     public static void main(String[] args) {         Thread thread = new Thread(() -> {             while (isQuit == 0) {              }             System.out.println("線程thread執(zhí)行完畢!");         });         thread.start();          Scanner sc = new Scanner(System.in);         System.out.println("請輸入isQuit的值,不為0線程thread停止執(zhí)行!");         isQuit = sc.nextInt();         System.out.println("main線程執(zhí)行完畢!");     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題

          synchronized與volatile關(guān)鍵字的區(qū)別:
          synchronized關(guān)鍵字能保證原子性,但是是否能夠保證內(nèi)存可見性要看情況(上面這個栗子是不行的),而volatile關(guān)鍵字只能保證內(nèi)存可見性不能保證原子性。
          保證內(nèi)存可見性就是禁止編譯器做出如上的優(yōu)化而已。

          import java.util.Scanner;public class Main12 {     private static int isQuit;     //鎖對象     private static final Object lock = new Object();     public static void main(String[] args) {         Thread thread = new Thread(() -> {                 synchronized (lock) {                     while (isQuit == 0) {                      }                     System.out.println("線程thread執(zhí)行完畢!");                 }         });         thread.start();          Scanner sc = new Scanner(System.in);         System.out.println("請輸入isQuit的值,不為0線程thread停止執(zhí)行!");         isQuit = sc.nextInt();         System.out.println("main線程執(zhí)行完畢!");     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題

          編譯器優(yōu)化除了導(dǎo)致內(nèi)存可見性感知不到的問題,還有指令重排序也會導(dǎo)致線程安全問題,指令重排序也是編譯器優(yōu)化之一,就是編譯器會智能地(保證原有邏輯不變的情況下)調(diào)整代碼執(zhí)行順序,從而提高程序運(yùn)行的效率,單線程沒問題,但是多線程可能會翻車,這個原因了解即可。

          3.線程安全的標(biāo)準(zhǔn)類

          Java 標(biāo)準(zhǔn)庫中很多都是線程不安全的。這些類可能會涉及到多線程修改共享數(shù)據(jù), 又沒有任何加鎖措施。例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder。
          但是還有一些是線程安全的,使用了一些鎖機(jī)制來控制,例如,Vector (不推薦使用),HashTable (不推薦使用),ConcurrentHashMap (推薦),StringBuffer。
          還有的雖然沒有加鎖, 但是不涉及 “修改”, 仍然是線程安全的,例如String。

          在線程安全問題中可能你還會遇到JMM模型,在這里補(bǔ)充一下,JMM其實(shí)就是把操作系統(tǒng)中的寄存器,緩存和內(nèi)存重新封裝了一下,其中在JMM中寄存器和緩存稱為工作內(nèi)存,內(nèi)存稱為主內(nèi)存。
          其中緩存分為一級緩存L1,二級緩存L2和三級緩存L3,從L1到L3空間越來越大,最大也比內(nèi)存空間小,最小也比寄存器空間大,訪問速度越來越慢,最慢也比內(nèi)存的訪問速度快,最快也沒有寄存器訪問快。

          4.Object類提供的線程等待方法

          除了Thread類中的能夠?qū)崿F(xiàn)線程等待的方法,如join,sleep,在Object類中也提供了相關(guān)線程等待的方法。

          序號 方法 說明
          1 public final void wait() throws InterruptedException 釋放鎖并使線程進(jìn)入WAITING狀態(tài)
          2 public final native void wait(long timeout) throws InterruptedException; 相比于方法1,多了一個最長等待時間
          3 public final void wait(long timeout, int nanos) throws InterruptedException 相比于方法2,等待的最長時間精度更大
          4 public final native void notify(); 喚醒一個WAITING狀態(tài)的線程,并加鎖,搭配wait方法使用
          5 public final native void notifyAll(); 喚醒所有處于WAITING狀態(tài)的線程,并加鎖(很可能產(chǎn)生鎖競爭),搭配wait方法使用

          上面介紹synchronized關(guān)鍵字的時候,如果不對線程加鎖會產(chǎn)生非法監(jiān)視異常,我們來驗(yàn)證一下:

          public class TestDemo12 {     public static void main(String[] args) throws InterruptedException {         Thread thread = new Thread(() -> {             try {                 Thread.sleep(5000);             } catch (InterruptedException e) {                 e.printStackTrace();             }             System.out.println("執(zhí)行完畢!");         });          thread.start();         System.out.println("wait前");         thread.wait();         System.out.println("wait后");     }}

          看看運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題
          果然拋出了一個IllegalMonitorStateException,因?yàn)?code>wait方法的執(zhí)行步驟為:先釋放鎖,再使線程等待,你現(xiàn)在都沒有加鎖,那如何釋放鎖呢?所以會拋出這個異常,但是執(zhí)行notify是無害的。

          wait方法常常搭配notify方法搭配一起使用,前者能夠釋放鎖,使線程等待,后者能獲取鎖,使線程繼續(xù)執(zhí)行,這套組合拳的流程圖如下:
          一起聊聊Java多線程之線程安全問題

          現(xiàn)在有兩個任務(wù)由兩個線程執(zhí)行,假設(shè)線程2比線程1先執(zhí)行,請寫出一個多線程程序使任務(wù)1在任務(wù)2前面完成,其中線程1執(zhí)行任務(wù)1,線程2執(zhí)行任務(wù)2。
          這個需求可以使用wait/notify來實(shí)現(xiàn)。

          class Task{     public void task(int i) {         System.out.println("任務(wù)" + i + "完成!");     }}public class WiteNotify {     //鎖對象     private static final Object lock = new Object();     public static void main(String[] args) throws InterruptedException {         Thread thread1 = new Thread(() -> {             synchronized (lock) {                 Task task1 = new Task();                 task1.task(1);                 //通知線程2線程1的任務(wù)完成                 System.out.println("notify前");                 lock.notify();                 System.out.println("notify后");             }         });         Thread thread2 = new Thread(() -> {             synchronized (lock) {                 Task task2 = new Task();                 //等待線程1的任務(wù)1執(zhí)行完畢                 System.out.println("wait前");                 try {                     lock.wait();                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 task2.task(2);                 System.out.println("wait后");             }         });         thread2.start();         Thread.sleep(10);         thread1.start();     }}

          運(yùn)行結(jié)果:
          一起聊聊Java多線程之線程安全問題

          推薦學(xué)習(xí):《java視頻教程》

          贊(0)
          分享到: 更多 (0)
          網(wǎng)站地圖   滬ICP備18035694號-2    滬公網(wǎng)安備31011702889846號