線程安全相關問題總結

1. 什麼是線程安全性?

當多個線程訪問某個類,不管運行時環境採用何種調度方式或者這些線程如何交替執行,並且在主調代碼中不需
要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類爲線程安全的。----《併發編程實戰》
什麼是線程不安全?
多線程併發訪問時,得不到正確的結果。

2. 從字節碼角度剖析線程不安全操作

javac -encoding UTF-8 UnsafeThread.java 編譯成.class
編譯命令:
javac -encoding UTF-8 UnSafeThread.java
將class文件反編譯爲字節碼:
javap -c UnSafeThread.class
javap -c UnsafeThread.class 進行反編譯,得到相應的字節碼指令
0: getstatic #2       獲取指定類的靜態域,並將其押入棧頂 

3: iconst_1          將int型1押入棧頂

 4: iadd             將棧頂兩個int型相加,將結果押入棧頂

 5: putstatic #2      爲指定類靜態域賦值

 8: return

例子中,產生線程不安全問題的原因: num++ 不是原子性操作,被拆分成好幾個步驟,在多線程併發執行的
情況下,因爲cpu調度,多線程快遞切換,有可能兩個同一時刻都讀取了同一個num值,之後對它進行+1操
作,導致線程安全性。

3. 原子性操作

3.1什麼是原子性操作

一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

3.2例子:

​ A想要從自己的帳戶中轉1000塊錢到B的帳戶裏。那個從A開始轉帳,到轉帳結束的這一個過程,稱之爲一個事務。在這個事務裏,要做如下操作: 從A的帳戶中減去1000塊錢。如果A的帳戶原來有3000塊錢,現在就變成2000塊錢了。

​ 在B的帳戶里加1000塊錢。如果B的帳戶如果原來有2000塊錢,現在則變成3000塊錢了。如果在A的帳
戶已經減去了1000塊錢的時候,忽然發生了意外,比如停電什麼的,導致轉帳事務意外終止了,而此時B的帳
戶裏 還沒有增加1000塊錢。那麼,我們稱這個操作失敗了,要進行回滾。回滾就是回到事務開始之前的狀
態,也就是回到A的帳戶還沒減1000塊的狀態,B的帳戶的原來的狀態。此時A的帳戶仍然有3000塊,B的帳
戶仍然有 2000塊。

​ 通俗點講:操作要成功一起成功、要失敗大家一起失敗如何把非原子性操作變成原子性

​ volatile關鍵字僅僅保證可見性,並不保證原子性 synchronize關機字,使得操作具有原子性

4. 深入理解synchronized

內置鎖

<font color="#000066"> 每個java對象都可以用做一個實現同步的鎖,這些鎖稱爲內置鎖</font> 線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。

互斥鎖

<font color="#000066">內置鎖是一個互斥鎖,這就是意味着最多隻有一個線程能夠獲得該鎖,</font>當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,直到線程B釋放這個鎖,如果B線程不釋放這個鎖,那麼A線程將永遠等待下去。
修飾普通方法:鎖住對象的實例
修飾靜態方法:鎖住整個類
修飾代碼塊: 鎖住一個對象 synchronized (lock) 即synchronized後面括號裏的內容

public class SynDemo {
    /**
     * 不要用synchronized修飾靜態方法 因爲他是鎖住的整個類
     */
//    public  synchronized  void  out() throws InterruptedException {
//        Thread.sleep(5000L);
//        System.out.println(Thread.currentThread().getName());
//    }


    private Object lock = new Object();

    public    void  out() throws InterruptedException {
        synchronized(lock){
            Thread.sleep(1000L);
            System.out.println(Thread.currentThread().getName());
        }
    }


    public static void main(String[] args) {

        SynDemo synDemo = new SynDemo();
        SynDemo synDemo2 = new SynDemo();

        new Thread(()->{
            try {
                synDemo.out();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
            try {
                synDemo2.out();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

5. volatile關鍵字及其使用場景

能且僅能修飾變量
保證該變量的可見性,volatile關鍵字僅僅保證可見性,並不保證原子性
<font color="#000066">禁止指令重排序</font>
A、B兩個線程同時讀取volatile關鍵字修飾的對象,A讀取之後,修改了變量的值,修改後的值,對B線程來說,
是可見
使用場景

  • 1:作爲線程開關
  • 2:單例,修飾對象實例,禁止指令重排序
  /**
   * volatile
   * 1:作爲線程開關 2:單例,修飾對象實例,禁止指令重排序
   */
  public class VolatileDemo implements Runnable {
  
      private static volatile boolean flag = true;
  
      @Override
      public void run() {
          while (flag){
              System.out.println(Thread.currentThread().getName());
          }
      }
  
      public static void main(String[] args) throws InterruptedException {
  
          Thread thread = new Thread(new VolatileDemo());
          thread.start();
          Thread.sleep(1000L);
          flag=false;
  
      }
  }
  

6. 單例與線程安全

餓漢式--本身線程安全
在類加載的時候,就已經進行實例化,無論之後用不用到。如果該類比較佔內存,之後又沒用到,就白白浪費
了資源。

/**
 * 餓漢式單例
 * 本身就是線程安全的
 */
public class HungerSingleton {

    private static HungerSingleton ourInstance = new HungerSingleton();

    public static HungerSingleton getInstance() {
        return ourInstance;
    }

    private HungerSingleton() {
    }

    public static void main(String[] args) {

        HungerSingleton hungerSingleton = new HungerSingleton();

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

            new Thread(() -> {
                System.out.println(HungerSingleton.getInstance());
            }).start();


        }

    }

}

懶漢式 -- 最簡單的寫法是非線程安全的
在需要的時候再實例化

/**
 * 懶漢式單例 線程安全的寫法
 */
public class LazySingleton {

    //這裏一定要使用volatile,可以禁止指令重排序
    private static volatile LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static  LazySingleton getInstance() {
        //判斷實例是否爲空,爲空才實例化
        if (lazySingleton == null) {
            //模擬實例化耗時操作
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }

        // 否則直接返回
        return lazySingleton;

    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(LazySingleton.getInstance());
            }).start();
        }
    }
}

7.如何避免線程安全性問題

線程安全性問題成因

  • 多線程環境
  • 多個線程操作同一共享資源
  • 對該共享資源進行了非原子性操作
7.1如何避免

打破成因中三點任意一點

1:多線程環境--將多線程改單線程(必要的代碼,加鎖訪問)

2:多個線程操作同一共享資源--不共享資源(ThreadLocal、不共享、操作無狀態化、不可變)

3:對該共享資源進行了非原子性操作-- 將非原子性操作改成原子性操作(加鎖、使用JDK自帶的原子性操作的類、JUC提供的相應的併發工具類)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章