1. 程式人生 > 實用技巧 >不會吧,你連Java 多執行緒執行緒安全都還沒搞明白,難怪你面試總不過

不會吧,你連Java 多執行緒執行緒安全都還沒搞明白,難怪你面試總不過

什麼是執行緒安全?

當一個執行緒在同一時刻共享同一個全域性變數或靜態變數時,可能會受到其他執行緒的干擾,導致資料有問題,這種現象就叫執行緒安全問題。

為什麼有執行緒安全問題?

當多個執行緒同時共享,同一個全域性變數或靜態變數,做寫的操作時,可能會發生資料衝突問題,也就是執行緒安全問題,但是做讀操作時不會發生資料衝突問題。

執行緒安全解決辦法?

1、如何解決多執行緒之間執行緒安全問題?

答:使用多執行緒之間同步synchronized或使用鎖(lock)

2、為什麼使用執行緒同步或使用鎖能解決執行緒安全問題呢?

答:將可能會發生資料衝突問題(執行緒不安全問題),只能讓當前一個執行緒進行執行。程式碼執行完成後釋放鎖,讓後才能讓其他執行緒進行執行。這樣的話就可以解決執行緒不安全問題。

3、什麼是多執行緒之間同步?

答:當多個執行緒共享同一個資源,不會受到其他執行緒的干擾。

同步程式碼塊

1、什麼是同步程式碼塊?

答:就是將可能會發生執行緒安全問題的程式碼,用synchronized給包括起來。

synchronized(同一個資料) {
 // 可能會發生執行緒衝突問題
}

  

synchronized(物件) {//這個物件可以為任意物件 
    // 需要被同步的程式碼 
} 

  

物件如同鎖,持有鎖的執行緒可以在同步中執行。
沒持有鎖的執行緒即使獲取CPU的執行權,也進不去。

同步的前提:

必須有兩個或者兩個以上的執行緒
必須是多個執行緒使用同一個鎖
必須保證同步中只能有一個執行緒在執行

同步的好處:
解決了多執行緒的安全問題

同步的弊端:
多個執行緒需要判斷鎖,較為消耗資源、搶鎖的資源。

同步函式

1、什麼是同步函式?

答:在方法上修飾 synchronized 稱為同步函式

public synchronized void sale() {
    if (trainCount > 0) { 
        try {
            Thread.sleep(40);
        } catch (Exception e) {
            
        }
        System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
        trainCount--;
        }
    }

  

同步函式使用的是 this 鎖

證明方式:一個執行緒使用同步程式碼塊(this明鎖),另一個執行緒使用同步函式。如果兩個執行緒搶票不能實現同步,那麼會出現資料錯誤。

package cn.icloudit;

class ThreadTrain2 implements Runnable {
    private int count = 100;
    public boolean flag = true;
    private static Object oj = new Object();

    @Override
    public void run() {
        if (flag) {
            while (count > 0) {
                synchronized (this) {
                    if (count > 0) {
                        try {
                            Thread.sleep(50);
                        } catch (Exception e) {
                            // TODO: handle exception
                        }
                        System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                        count--;
                    }
                }

            }

        } else {
            while (count > 0) {
                sale();
            }
        }

    }

    public synchronized void sale() {
        // 前提 多執行緒進行使用、多個執行緒只能拿到一把鎖。
        // 保證只能讓一個執行緒 在執行 缺點效率降低
        // synchronized (oj) {
        if (count > 0) {
            try {
                Thread.sleep(50);
            } catch (Exception e) {
                // TODO: handle exception
            }
            System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
            count--;
        }
        // }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        ThreadTrain2 threadTrain1 = new ThreadTrain2();
        Thread t1 = new Thread(threadTrain1, "①號視窗");
        Thread t2 = new Thread(threadTrain1, "②號視窗");
        t1.start();
        Thread.sleep(40);
        threadTrain1.flag = false;
        t2.start();
    }
}

  

靜態同步函式

1、什麼是靜態同步函式?

答:方法上加上 static 關鍵字,使用 synchronized 關鍵字修飾,或者使用類 .class 檔案。

靜態的同步函式使用的鎖是 該函式所屬位元組碼檔案物件
可以用 getClass方法獲取,也可以用當前 類名.class 表示。

synchronized (ThreadTrain.class) {
    System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
    trainCount--;
    try {
        Thread.sleep(100);
    } catch (Exception e) {
    }   
}

  

總結:
synchronized 修飾方法使用鎖是當前 this 鎖。
synchronized 修飾靜態方法使用鎖是當前類的 位元組碼檔案。

多執行緒死鎖

什麼是多執行緒死鎖?

答:同步中巢狀同步,導致鎖無法釋放

package cn.icloudit;

class ThreadTrain6 implements Runnable {
    // 這是貨票總票數,多個執行緒會同時共享資源
    private int trainCount = 100;
    public boolean flag = true;
    private Object mutex = new Object();

    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (mutex) {
                    // 鎖(同步程式碼塊)在什麼時候釋放? 程式碼執行完, 自動釋放鎖.
                    // 如果flag為true 先拿到 obj鎖,在拿到this 鎖、 才能執行。
                    // 如果flag為false先拿到this,在拿到obj鎖,才能執行。
                    // 死鎖解決辦法:不要在同步中巢狀同步。
                    sale();
                }
            }
        } else {
            while (true) {
                sale();
            }
        }
    }
    
    public synchronized void sale() {
        synchronized (mutex) {
            if (trainCount > 0) {
                try {
                    Thread.sleep(40);
                } catch (Exception e) {

                }
                System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
                trainCount--;
            }
        }
    }
}

public class DeadlockThread {

    public static void main(String[] args) throws InterruptedException {
        ThreadTrain6 threadTrain = new ThreadTrain6(); // 定義 一個例項
        Thread thread1 = new Thread(threadTrain, "一號視窗");
        Thread thread2 = new Thread(threadTrain, "二號視窗");
        thread1.start();
        Thread.sleep(40);
        threadTrain.flag = false;
        thread2.start();
    }

}

  

多執行緒的三大特性

原子性

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

一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

我們操作資料也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行程式碼在Java中是不具備原子性的,則多執行緒執行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。

原子性其實就是保證資料一致、執行緒安全一部分。

可見性

當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

若兩個執行緒在不同的cpu,那麼執行緒1改變了i的值還沒重新整理到主存,執行緒2又使用了i,那麼這個i值肯定還是之前的,執行緒1對變數的修改執行緒沒看到這就是可見性問題。

有序性

程式執行的順序按照程式碼的先後順序執行。

一般來說處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。如下:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
則因為重排序,他還可能執行順序為 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因為這打破了依賴關係。

顯然重排序對單執行緒執行是不會有任何問題,而多執行緒就不一定了,所以我們在多執行緒程式設計時就得考慮這個問題了。

Java記憶體模型

共享記憶體模型指的就是Java記憶體模型(簡稱JMM),JMM決定一個執行緒對共享變數的寫入時,能對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。

總結:什麼是Java記憶體模型:java記憶體模型簡稱jmm,定義了一個執行緒對另一個執行緒可見。共享變數存放在主記憶體中,每個執行緒都有自己的本地記憶體,當多個執行緒同時訪問一個數據的時候,可能本地記憶體沒有及時重新整理到主記憶體,所以就會發生執行緒安全問題。

Volatile 關鍵字

什麼是 Volatile 關鍵字?
答:Volatile 關鍵字的作用是變數在多個執行緒之間可見。

class ThreadVolatileDemo extends Thread {
    public    boolean flag = true;
    @Override
    public void run() {
        System.out.println("開始執行子執行緒....");
        while (flag) {
        }
        System.out.println("執行緒停止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }

}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設定成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);

    }
}

  

已經將結果設定為fasle為什麼?還一直在執行呢。

原因:執行緒之間是不可見的,讀取的是副本,沒有及時讀取到主記憶體結果。

解決辦法:使用Volatile關鍵字將解決執行緒之間可見性, 強制執行緒每次讀取該值的時候都去“主記憶體”中取值

Volatile非原子性

public class VolatileNoAtomic extends Thread {
    private static volatile int count;

    // private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount() {
        for (int i = 0; i < 1000; i++) {
            count++;
            // count.incrementAndGet();
        }
        System.out.println(count);
    }

    public void run() {
        addCount();
    }

    public static void main(String[] args) {

        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }

        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }

}

  

結果發現 資料不同步,因為Volatile不用具備原子性。

AtomicInteger原子類

AtomicInteger是一個提供原子操作的Integer類,通過執行緒安全的方式操作加減。

public class VolatileNoAtomic extends Thread {
    static int count = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //等同於i++
            atomicInteger.incrementAndGet();
        }
        System.out.println(count);
    }

    public static void main(String[] args) {
        // 初始化10個執行緒
        VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
        for (int i = 0; i < 10; i++) {
            // 建立
            volatileNoAtomic[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < volatileNoAtomic.length; i++) {
            volatileNoAtomic[i].start();
        }
    }

}

  

volatile與synchronized區別

僅靠volatile不能保證執行緒的安全性。(原子性)

volatile輕量級,只能修飾變數。synchronized重量級,還可修飾方法;
volatile只能保證資料的可見性,不能用來同步,因為多個執行緒併發訪問volatile修飾的變數不會阻塞。
synchronized 不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的執行緒才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個執行緒爭搶synchronized鎖物件時,會出現阻塞。

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!