Java多線程對象及變量的並發訪問


目錄:

  1. synchronized總結
  2. 寫一個死鎖
  3. 線程安全的三大特性
  4. java內存模型
  5. synchronized與volatile對比
  6. Atomic原子類
  7. CAS機制(compare and swap)
  8. 樂觀鎖悲觀鎖

1、 synchronized

1.1、方法內的變量為線程安全的

“非線程安全”問題存在於實例變量中,如果一個變量是方法內的變量,那么這個變量是線程安全的,也不會出現“非線程安全”問題。

代碼:

package Thread.thread2;

public class Num {
    //private int num;
     public void addI(String str){
        int num =0;
        if(str.equals("a")){
            num=100;
            System.out.println("a set over");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            num = 200;
            System.out.println("b set over");
        }
        System.out.println("num = "+num);

    }
}
package Thread.thread2;

public class ThreadA extends Thread{
    private Num num;
    public ThreadA(Num num){
        super();
        this.num = num;
    }

    @Override
    public void run() {
        super.run();
        num.addI("a");
    }
}
package Thread.thread2;

public class ThreadB extends Thread{
    private Num num;
    public ThreadB(Num num){
        super();
        this.num = num;
    }

    @Override
    public void run() {
        super.run();
        num.addI("b");
    }
}
package Thread.thread2;

public class TestRun {
    public static void main(String[] args) {
        Num num = new Num();
        ThreadA threadA = new ThreadA(num);
        ThreadB threadB = new ThreadB(num);
        threadA.start();
        threadB.start();
    }
}

運行結果:

a set over
b set over
num = 200
num = 100

解讀:上面的代碼,兩個線程明明訪問的同一方法,a等了b兩秒,講道理a最后應該打印出b修改后的值,但為什么還是100呢?

原因:

  1. 方法內部的變量為方法私有的變量,其生存走起隨着方法的結束而終結。
  2. 每個線程執行的時候都會把局部變量存放在各自棧幀的工作內存中(棧幀進入虛擬機棧),虛擬機棧線程間不共享,故不存在線程安全的問題。

針對第二條原因擴展說明:

Java虛擬機在執行Java程序的時候會把它管理的內存划分為5個不同的區域,其中 【方法區】和【堆】是線程共享的,而【虛擬機棧】、【程序計數器】、【本地方法棧】是線程不共享的,如下圖所示:

1.2、實例變量非線程安全可以通過synchronized來解決

代碼 將上面代碼修改如下:

package Thread.thread2;

public class Num {
     private int num;
     public void addI(String str){
        //int num =0;
        if(str.equals("a")){
            num=100;
            System.out.println("a set over");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            num = 200;
            System.out.println("b set over");
        }
        System.out.println("num = "+num);

    }
}

結果:

a set over
b set over
num = 200
num = 200

這次a也打印出了200,因為這次兩個線程修改的是同一個變量。

對於這樣的問題我們可以用synchronized方法來解決,只需要在共同訪問的方法上加上該關鍵字就可以了,如下:

synchronized public void addI(String str){
    ...
}

1.3、synchronized方法與synchronized代碼塊對比

synchronized方法使用有個弊端,就是如果同步方法很費時間的代碼的話,效率會很慢。我們可以使用同步代碼塊來將需要同步的代碼加鎖,而費時間的代碼讓其異步。

1.4、synchronized方法加的是什么鎖?

答:this鎖

1.5、synchronized加載static方法上的是什么鎖?

答:字節碼鎖,類名.class。一般情況下不用static。JVM編譯的時候是存在方法區的,是垃圾回收機制不會回收的地方。

1.6、其他總結

  • 多個線程訪問一個對象中的方法時候,順序是輪流執行的,並非同步執行的。
  • 多個對象訪問一個同步方法時,運行結果是異步的(就是多個對象產生了多個鎖。),哪個對象的線程先執行帶synchronized關鍵字的方法,哪個對象的線程先執行,然后異步執行。
  • A線程訪問一個synchronized方法或synchronized同步塊,B線程訪問非synchronized方法或非synchronized同步塊時候,B線程可以隨意調用其他的非synchronized方法
  • 兩個線程訪問同一個對象的兩個同步的方法時候,結果是同步執行的,不存在臟讀的現象
  • synchronized擁有鎖重入的功能,也就是在使用synchronized時候,當一個線程得到一個對象鎖時候,再次請求此對象鎖是可以再次得到該對象的鎖的。
  • 可重入鎖也支持在父子類繼承的環境中
  • 出現異常,鎖自動釋放。
  • 如果子類繼承父類的synchronized方法(如:synchronized public void x(){})時候,子類在重寫的時候,如果不標注synchronized的話,子類就會吃掉“synchronized”,就是子類中的x方法是非synchronized,導致運行的時候並不同步。所以在重寫的x(),子類就要就要寫上synchronized,運行就是同步的。
  • 當一個線程訪問一個synchronized同步塊時,另一個線程仍然可以訪問對象中的非synchronized(this)同步代碼塊。
  • synchronized(非this對象):如果一個類中有很多個synchronized方法,這時雖然能出現同步,但會受到阻塞,所以影響運行效率,但如果使用synchronized(非this對象),它與synchronized方法是異步的,不與其他同步方法爭奪this鎖
  • 同步代碼塊放在非同步synchronized方法中進行聲明,線程調用是無序的,但是在使用synchronized(非this對象),可以解決臟讀的現象
  • 多個線程執行synchronized(非對象x)呈同步效果

2、 死鎖

死鎖產生的原因:兩個鎖互相等待而導致。寫個死鎖代碼如下:

package Thread.deadLock;

public class Mythread implements Runnable {
    private Object obj = new Object();
    public boolean flag = true;
    @Override
    public void run() {
       if(flag){
           while (true){
               synchronized (obj){
                   try {
                       Thread.sleep(50);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   synchronized (this){
                       System.out.println("1");
                   }
               }
           }
       }else {
          while (true){
              synchronized (this){
                  try {
                      Thread.sleep(50);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  synchronized (obj){
                      System.out.println("2");
                  }
              }
          }
       }
    }

}
package Thread.deadLock;

public class TestDeadLock {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        new Thread(mythread).start();
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mythread.flag = false;
        new Thread(mythread).start();
    }
}

3、 線程安全的三大特性

原子性、可見性、有序性。

3.1、什么是原子性?

一個操作或者連續的多個操作,要么全部執行要么全部不執行。事物的原子性一樣的道理。而多線程執行的過程運行的時候就會出現不能保證原子性的問題。因此我們需要使用同步和鎖來確保這個特性。

3.2、什么是可見性?

一個線程對共享變量的修改能夠及時的被其他線程看到。共享變量是存放在主內存中的,每個線程訪問共享變量會在自己的工作內存中存一個副本而不是在主內存中直接操作共享變量。

共享變量可見性實現的原理:

  • 把工作內存1更改過得共享變量刷新到主內存中
  • 將主內存中的最新共享變量更新到工作內存2中

Java語言層面實現可見性的方式(不包過jdk1.5后引用的包的高級特性): synchronized volatile

3.4、什么是有序性?

Java代碼有序的執行;但也會有指令重排序的問題。

重排序:代碼執行的順序與書寫的順序不同,指令重排序是編譯器或處理器為了提高性能而做的優化

  • 編譯器優化的重排序(編譯器優化)
  • 指令級並行重排序(多核處理器優化)
  • 內存系統重排序(處理器優化)

as-if-serial:無論怎么重排序都應該與書寫順序執行的結果一致,當然這是單線程下能夠保證;如下代碼:

int num1 = 1; //第一行
int num2 = 2; //第二行
int num = num1 + num2; //第三行 

單線程:無論第一、第二行怎么重排序也不會到了第三行的下邊。

但是多線程下就會有問題了。

4、 Java內存模型

Java內存模型(java memory model,JMM)描述了Java程序中的各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取出變量的這樣的細節。

兩條規則:

  • 線程對共享變量的所有操作都是在自己的工作內存中操作,不能直接在主內存上讀寫。
  • 不同線程之間無法直接訪問其他線程工作內存中的變量,線程間變量值得傳遞需要通過主內存來完成。

5、 synchronized 與volatile對比

注意:緊靠volatile不能保證線程的安全性(原子性)只能保證可見性

  • volatile輕量級只能修飾變量,synchronized重量級還可以修飾方法
  • volatile只能保證數據的可見性,不能用來同步,因為多個線程並發訪問volatile修飾的變量不會阻塞。如何實現可見性的?通過加入內存屏障和禁止指令重排序優化實現的。
  • synchronized不僅保證可見性,而且還能保證原子性,線程會阻塞。原子性不用說,加鎖了保證多操作的同步了。而實現可見性是在線程解鎖前,必須把共享變量的最新值刷新到主內存中;線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取新的值。

6、 AtomicInteger原子類

https://mp.weixin.qq.com/s/f9PYMnpAgS1gAQYPDuCq-w

7、 CAS機制(compare and swap)

https://mp.weixin.qq.com/s/nRnQKhiSUrDKu3mz3vItWg

8、 樂觀鎖悲觀鎖v

https://mp.weixin.qq.com/s/LLGla7tI-W7zWaT3vCviVw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM