溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java線程安全問題小結(jié)_動力節(jié)點Java學院整理

發(fā)布時間:2020-09-03 14:24:32 來源:腳本之家 閱讀:141 作者:mrr 欄目:編程語言

淺談java內(nèi)存模型

       不同的平臺,內(nèi)存模型是不一樣的,但是jvm的內(nèi)存模型規(guī)范是統(tǒng)一的。其實java的多線程并發(fā)問題最終都會反映在java的內(nèi)存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改??偨Y(jié)java的內(nèi)存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器并不是每次處理數(shù)據(jù)都是取內(nèi)存的。JVM定義了自己的內(nèi)存模型,屏蔽了底層平臺內(nèi)存管理細節(jié),對于java開發(fā)人員,要清楚在jvm內(nèi)存模型的基礎(chǔ)上,如果解決多線程的可見性和有序性。

       那么,何謂可見性? 多個線程之間是不能互相傳遞數(shù)據(jù)通信的,它們之間的溝通只能通過共享變量來進行。Java內(nèi)存模型(JMM)規(guī)定了jvm有主內(nèi)存,主內(nèi)存是多個線程共享的。當new一個對象的時候,也是被分配在主內(nèi)存中,每個線程都有自己的工作內(nèi)存,工作內(nèi)存存儲了主存的某些對象的副本,當然線程的工作內(nèi)存大小是有限制的。當線程操作某個對象時,執(zhí)行順序如下:

 (1) 從主存復制變量到當前工作內(nèi)存 (read and load)

 (2) 執(zhí)行代碼,改變共享變量值 (use and assign)

 (3) 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容 (store and write)

JVM規(guī)范定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作內(nèi)存中都有副本時,如果一個線程修改了這個共享變量,那么其他線程應該能夠看到這個被修改后的值,這就是多線程的可見性問題。

        那么,什么是有序性呢 ?線程在引用變量時不能直接從主內(nèi)存中引用,如果線程工作內(nèi)存中沒有該變量,則會從主內(nèi)存中拷貝一個副本到工作內(nèi)存中,這個過程為read-load,完成后線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現(xiàn)系統(tǒng)決定。

        線程不能直接為主存中中字段賦值,它會將值指定給工作內(nèi)存中的變量副本(assign),完成后這個變量副本會同步到主存儲區(qū)(store-write),至于何時同步過去,根據(jù)JVM實現(xiàn)系統(tǒng)決定.有該字段,則會從主內(nèi)存中將該字段賦值到工作內(nèi)存中,這個過程為read-load,完成后線程會引用該變量副本,當同一線程多次重復對字段賦值時,比如:

Java代碼  

 for(int i=0;i<10;i++) 
 a++; 

線程有可能只對工作內(nèi)存中的副本進行賦值,只到最后一次賦值后才同步到主存儲區(qū),所以assign,store,weite順序可以由JVM實現(xiàn)系統(tǒng)決定。假設(shè)有一個共享變量x,線程a執(zhí)行x=x+1。從上面的描述中可以知道x=x+1并不是一個原子操作,它的執(zhí)行過程如下:

1 從主存中讀取變量x副本到工作內(nèi)存

2 給x加1

3 將x加1后的值寫回主存

如果另外一個線程b執(zhí)行x=x-1,執(zhí)行過程如下:

1 從主存中讀取變量x副本到工作內(nèi)存

2 給x減1

3 將x減1后的值寫回主存

那么顯然,最終的x的值是不可靠的。假設(shè)x現(xiàn)在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發(fā)生:

1:線程a從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10

2:線程b從主存讀取x副本到工作內(nèi)存,工作內(nèi)存中x值為10

3:線程a將工作內(nèi)存中x加1,工作內(nèi)存中x值為11

4:線程a將x提交主存中,主存中x為11

5:線程b將工作內(nèi)存中x值減1,工作內(nèi)存中x值為9

6:線程b將x提交到中主存中,主存中x為9

同樣,x有可能為11,如果x是一個銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執(zhí)行的,并且每個線程執(zhí)行的加1或減1是一個原子操作??纯聪旅娲a:

Java代碼  

public class Account { 
   private int balance; 
   public Account(int balance) { 
     this.balance = balance; 
   } 
   public int getBalance() { 
     return balance; 
   } 
   public void add(int num) { 
     balance = balance + num; 
   } 
   public void withdraw(int num) { 
     balance = balance - num; 
   } 
   public static void main(String[] args) throws InterruptedException { 
     Account account = new Account(1000); 
     Thread a = new Thread(new AddThread(account, 20), "add"); 
     Thread b = new Thread(new WithdrawThread(account, 20), "withdraw"); 
     a.start(); 
     b.start(); 
     a.join(); 
     b.join(); 
     System.out.println(account.getBalance()); 
   } 
   static class AddThread implements Runnable { 
     Account account; 
     int   amount; 
     public AddThread(Account account, int amount) { 
       this.account = account; 
       this.amount = amount; 
     } 
     public void run() { 
       for (int i = 0; i < 200000; i++) { 
         account.add(amount); 
       } 
     } 
   } 
   static class WithdrawThread implements Runnable { 
     Account account; 
     int   amount; 
     public WithdrawThread(Account account, int amount) { 
       this.account = account; 
       this.amount = amount; 
     } 
     public void run() { 
       for (int i = 0; i < 100000; i++) { 
         account.withdraw(amount); 
       } 
     } 
   } 
 } 

第一次執(zhí)行結(jié)果為10200,第二次執(zhí)行結(jié)果為1060,每次執(zhí)行的結(jié)果都是不確定的,因為線程的執(zhí)行順序是不可預見的。這是java同步產(chǎn)生的根源,synchronized關(guān)鍵字保證了多個線程對于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執(zhí)行有序性和內(nèi)存可見性,而volatile關(guān)鍵字之解決多線程的內(nèi)存可見性問題。后面將會詳細介紹。

synchronized關(guān)鍵字

        上面說了,java用synchronized關(guān)鍵字做為多線程并發(fā)環(huán)境的執(zhí)行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成為互斥區(qū)或臨界區(qū),為了保證共享變量的正確性,synchronized標示了臨界區(qū)。典型的用法如下:

Java代碼 

 synchronized(鎖){ 
   臨界區(qū)代碼 
 }

為了保證銀行賬戶的安全,可以操作賬戶的方法如下:

Java代碼  

 public synchronized void add(int num) { 
   balance = balance + num; 
 } 
 public synchronized void withdraw(int num) { 
   balance = balance - num; 
 } 

剛才不是說了synchronized的用法是這樣的嗎:

Java代碼  

 synchronized(鎖){ 
 臨界區(qū)代碼 
 }

那么對于public synchronized void add(int num)這種情況,意味著什么呢?其實這種情況,鎖就是這個方法所在的對象。同理,如果方法是public  static synchronized void add(int num),那么鎖就是這個方法所在的class。

        理論上,每個對象都可以做為鎖,但一個對象做為鎖時,應該被多個線程共享,這樣才顯得有意義,在并發(fā)環(huán)境下,一個沒有共享的對象作為鎖是沒有意義的。假如有這樣的代碼:

Java代碼 

 public class ThreadTest{ 
  public void test(){ 
   Object lock=new Object(); 
   synchronized (lock){ 
     //do something 
   } 
  } 
 } 

lock變量作為一個鎖存在根本沒有意義,因為它根本不是共享對象,每個線程進來都會執(zhí)行Object lock=new Object();每個線程都有自己的lock,根本不存在鎖競爭。

        每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個被線程被喚醒(notify)后,才會進入到就緒隊列,等待cpu的調(diào)度。當一開始線程a第一次執(zhí)行account.add方法時,jvm會檢查鎖對象account的就緒隊列是否已經(jīng)有線程在等待,如果有則表明account的鎖已經(jīng)被占用了,由于是第一次運行,account的就緒隊列為空,所以線程a獲得了鎖,執(zhí)行account.add方法。如果恰好在這個時候,線程b要執(zhí)行account.withdraw方法,因為線程a已經(jīng)獲得了鎖還沒有釋放,所以線程b要進入account的就緒隊列,等到得到鎖后才可以執(zhí)行。

一個線程執(zhí)行臨界區(qū)代碼過程如下:

1 獲得同步鎖

2 清空工作內(nèi)存

3 從主存拷貝變量副本到工作內(nèi)存

4 對這些變量計算

5 將變量從工作內(nèi)存寫回到主存

6 釋放鎖

可見,synchronized既保證了多線程的并發(fā)有序性,又保證了多線程的內(nèi)存可見性。

生產(chǎn)者/消費者模式

        生產(chǎn)者/消費者模式其實是一種很經(jīng)典的線程同步模型,很多時候,并不是光保證多個線程對某共享資源操作的互斥性就夠了,往往多個線程之間都是有協(xié)作的。

        假設(shè)有這樣一種情況,有一個桌子,桌子上面有一個盤子,盤子里只能放一顆雞蛋,A專門往盤子里放雞蛋,如果盤子里有雞蛋,則一直等到盤子里沒雞蛋,B專門從盤子里拿雞蛋,如果盤子里沒雞蛋,則等待直到盤子里有雞蛋。其實盤子就是一個互斥區(qū),每次往盤子放雞蛋應該都是互斥的,A的等待其實就是主動放棄鎖,B等待時還要提醒A放雞蛋。
如何讓線程主動釋放鎖

很簡單,調(diào)用鎖的wait()方法就好。wait方法是從Object來的,所以任意對象都有這個方法??催@個代碼片段:

Java代碼   

 Object lock=new Object();//聲明了一個對象作為鎖 
  synchronized (lock) { 
    balance = balance - num; 
    //這里放棄了同步鎖,好不容易得到,又放棄了 
    lock.wait(); 
 }

如果一個線程獲得了鎖lock,進入了同步塊,執(zhí)行l(wèi)ock.wait(),那么這個線程會進入到lock的阻塞隊列。如果調(diào)用lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。

聲明一個盤子,只能放一個雞蛋 

Java代碼

 import java.util.ArrayList; 
 import java.util.List; 
 public class Plate { 
   List<Object> eggs = new ArrayList<Object>(); 
   public synchronized Object getEgg() { 
     if (eggs.size() == 0) { 
       try { 
         wait(); 
       } catch (InterruptedException e) { 
       } 
     } 
     Object egg = eggs.get(0); 
     eggs.clear();// 清空盤子 
     notify();// 喚醒阻塞隊列的某線程到就緒隊列 
     System.out.println("拿到雞蛋"); 
     return egg; 
   } 
   public synchronized void putEgg(Object egg) { 
     if (eggs.size() > 0) { 
       try { 
         wait(); 
       } catch (InterruptedException e) { 
       } 
     } 
     eggs.add(egg);// 往盤子里放雞蛋 
     notify();// 喚醒阻塞隊列的某線程到就緒隊列 
     System.out.println("放入雞蛋"); 
   } 
   static class AddThread extends Thread{ 
     private Plate plate; 
     private Object egg=new Object(); 
     public AddThread(Plate plate){ 
       this.plate=plate; 
     } 
     public void run(){ 
       for(int i=0;i<5;i++){ 
         plate.putEgg(egg); 
       } 
     } 
   } 
   static class GetThread extends Thread{ 
     private Plate plate; 
     public GetThread(Plate plate){ 
       this.plate=plate; 
     } 
     public void run(){ 
       for(int i=0;i<5;i++){ 
         plate.getEgg(); 
       } 
     } 
   } 
   public static void main(String args[]){ 
     try { 
       Plate plate=new Plate(); 
       Thread add=new Thread(new AddThread(plate)); 
       Thread get=new Thread(new GetThread(plate)); 
       add.start(); 
       get.start(); 
       add.join(); 
       get.join(); 
     } catch (InterruptedException e) { 
       e.printStackTrace(); 
     } 
     System.out.println("測試結(jié)束"); 
   } 
 } 

 執(zhí)行結(jié)果:

Html代碼

 放入雞蛋 
 拿到雞蛋 
 放入雞蛋 
 拿到雞蛋 
 放入雞蛋 
 拿到雞蛋 
 放入雞蛋 
 拿到雞蛋 
 放入雞蛋 
 拿到雞蛋 
 測試結(jié)束 

聲明一個Plate對象為plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設(shè)

1 開始,A調(diào)用plate.putEgg方法,此時eggs.size()為0,因此順利將雞蛋放到盤子,還執(zhí)行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列還沒有線程。

2 又有一個A線程對象調(diào)用plate.putEgg方法,此時eggs.size()不為0,調(diào)用wait()方法,自己進入了鎖對象的阻塞隊列。

3 此時,來了一個B線程對象,調(diào)用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個雞蛋,還執(zhí)行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒后,它進入到就緒隊列,就緒隊列也就它一個,因此馬上得到鎖,開始往盤子里放雞蛋,此時盤子是空的,因此放雞蛋成功。

4 假設(shè)接著來了線程A,就重復2;假設(shè)來料線程B,就重復3。

整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

volatile關(guān)鍵字

       volatile是java提供的一種同步手段,只不過它是輕量級的同步,為什么這么說,因為volatile只能保證多線程的內(nèi)存可見性,不能保證多線程的執(zhí)行有序性。而最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工作內(nèi)存,任何修改都及時寫在主存。因此對于Valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對變量的修改是有序的。什么意思呢?假如有這樣的代碼:

Java代碼  

 public class VolatileTest{ 
  public volatile int a; 
  public void add(int count){ 
    a=a+count; 
  } 
 } 

        當一個VolatileTest對象被多個線程共享,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個線程的執(zhí)行是無序的,因為沒有任何機制來保證多個線程的執(zhí)行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內(nèi)存和主存的同步。所以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

1)對變量的寫操作不依賴于當前值。

2)該變量沒有包含在具有其他變量的不變式中

volatile只保證了可見性,所以Volatile適合直接賦值的場景,如

Java代碼  

public class VolatileTest{ 
  public volatile int a; 
  public void setA(int a){ 
    this.a=a; 
  } 
 }

在沒有volatile聲明時,多線程環(huán)境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作內(nèi)存和同步a到主存的步驟,相當于是一個原子操作。所以簡單來說,volatile適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。

以上所述是小編給大家介紹的Java線程安全問題小結(jié),希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI