1. 程式人生 > >多執行緒安全問題詳解

多執行緒安全問題詳解

在上一篇部落格中已經提到了什麼是多執行緒:https://blog.csdn.net/weixin_42647847/article/details/80969240

那多執行緒在java中是如何實現的呢。

一、實現多執行緒的四種方式

1.繼承Thread類,重寫run方法2.實現Runnable介面,實現run方法,實現Runnable介面的實現類的例項物件作為Thread建構函式的target3.通過Callable和FutureTask建立執行緒4.通過執行緒池的方式建立執行緒。程式碼例項:
public class ThreadDemo01 extends Thread{
    public ThreadDemo01(){
        //編寫子類的構造方法,可預設
    }
    public void run(){
        //編寫自己的執行緒程式碼
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args){ 
        ThreadDemo01 threadDemo01 = new ThreadDemo01(); 
        threadDemo01.setName("我是自定義的執行緒1");
        threadDemo01.start();       
        System.out.println(Thread.currentThread().toString());  
    }
}
public class ThreadDemo02 {

    public static void main(String[] args){ 
        System.out.println(Thread.currentThread().getName());
        Thread t1 = new Thread(new MyThread());
        t1.start(); 
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通過實現介面的執行緒實現方式!");
    }   
}
public class ThreadDemo03 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);

        Thread t = new Thread(oneTask);

        System.out.println(Thread.currentThread().getName());

        t.start();

    }

}

class Tickets<Object> implements Callable<Object>{

    //重寫call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通過實現Callable介面通過FutureTask包裝器來實現的執行緒");
        return null;
    }   
}
public class ThreadDemo05{

    private static int POOL_NUM = 10;     //執行緒池數量

    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {  
            RunnableThread thread = new RunnableThread();

            //Thread.sleep(1000);
            executorService.execute(thread);  
        }
        //關閉執行緒池
        executorService.shutdown(); 
    }   

}

class RunnableThread implements Runnable  
{     
    @Override
    public void run()  
    {  
        System.out.println("通過執行緒池方式建立的執行緒:" + Thread.currentThread().getName() + " ");  

    }  
} 
各實現方式的比較:通過Thread和Runable的方式無法獲取到執行緒執行的反回結果,因為Runable中的run方法反回的是void,而通過使用Callable和FutureTask建立執行緒,則可以獲取到執行緒的執行結果,因為Callabel介面中的call方法返回一個物件,而在FutureTask中也提供了相應對執行緒操作的方法

二、執行緒的6種狀態與轉換

NEW狀態,指執行緒剛剛建立,尚未啟動。Runnable狀態,是執行緒正在正常執行, 可能會有某種耗時計算、IO等待、cpu時間片切換等,這個狀態下可能發生的等待,而不是鎖,sleep等。Blocked這個狀態下,是在多個執行緒有同步操作的場景,比如在等待另一個執行緒的synchronized塊的執行釋放,或者可重入的synchronized塊裡呼叫wait方法,也就是這裡是執行緒在等待進入臨界區。
WAITING這個狀態下是指執行緒擁有了某個鎖之後,呼叫了他的wait方法,等待其他執行緒notify或notifyall一遍該執行緒可以繼續下一步操作,這裡要區分BLOCKED和WATING的區別,一個是在臨界點外面等待進入,一個是在臨界點裡面wait等待別人notify,執行緒呼叫了join方法,join了另外的執行緒的時候,也會進入WAITING狀態,等待被他join的執行緒執行結束。TIMED_WAITING這個狀態就是在限的(時間限制)的WAITING,一般出現在呼叫wait(long),join(long)等情況下,另外一個執行緒sleep後也會進入TIMED_WAITING狀態。
TERMINATED 這個狀態下表示 該執行緒的run方法已經執行完畢了,基本上就等於死亡了。三、幾種執行緒間狀態切換會呼叫方的區別Thread.sleep(long millis) 一定是當前執行緒呼叫些方法,當前執行緒進入TIMED_WATING狀態,但不釋放鎖,時間過後執行緒自動甦醒進入RUNNABLE狀態,作用:給其他執行緒執行機會的最佳方式。Thread.yield,一定是當前執行緒 呼叫此方法,當前執行緒 放棄時間片,由執行狀態變為可執行狀態,讓os再次選擇執行緒。作用:讓相同優先順序的執行緒 輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。thread.yield不會導致阻塞。t.join/t.join(long millis),當前執行緒裡呼叫其他執行緒1的join方法,當前執行緒進入WAITING狀態,等待執行緒1執行完畢後,當前執行緒進入RUNNABLE狀態4.boj.wait(),當前執行緒呼叫物件的wait()方法,當前執行緒釋放物件鎖,進入WAITING狀態,依靠notify/notifyAll()喚醒或者wait(long timeout)時間到自動喚醒。5.obj.notify()喚醒在此物件監視器上等待的單個執行緒,選擇任意的等待的進行喚醒。notifyAll()喚醒在些物件監視器上等待的所有執行緒。

四、多執行緒三個核心概念

原子性即一個操作可能包含很多子操作,要麼全部執行,要麼全部不執行。可見性可見是指當多個執行緒併發訪問共享變數時,一個執行緒對共享變數的修改,其他執行緒能夠立即看到,CPU從主記憶體中讀資料的效率相對來說不高,現在主流的計算機中,都有幾級快取。每個執行緒讀取共享變數時,都會將該變數載入進其對應CPU的快取記憶體裡,修改該變數後,CPU會立即更新該快取,但並不一定會立即將其寫回主記憶體(實際上寫回主記憶體的時間不可預期)。此時其它執行緒(尤其是不在同一個CPU上執行的執行緒)訪問該變數時,從主記憶體中讀到的就是舊的資料,而非第一個執行緒更新後的資料。順序性順序性指的是,程式執行的順序按照程式碼的先後順序執行。

五、如何解決多執行緒併發問題

1.保證操作的原子性   a.常見的機制是鎖和程式碼同步,使用鎖,可以保證同一時間只有一個執行緒能拿到鎖,也就保證了同一時間只有一個執行緒能執行鎖之間的程式碼同步方法或者同步程式碼塊。使用非靜態同步方法時,鎖住的是當前例項;使用靜態同步方法時,鎖住的是該類的Class物件;使用靜態程式碼塊時,鎖住的是synchronized關鍵字後面括號內的物件。無論使用鎖還是synchronized,本質都是一樣,通過鎖來實現資源的排它性,從而實際目的碼段同一時間只會被一個執行緒執行,進而保證了目的碼段的原子性。這是一種以犧牲效能為代價的方法。    b.CASJava中提供了對應的原子操作類來實現該操作,並保證原子性,其本質是利用了CPU級別的CAS指令。由於是CPU級別的指令,其開銷比需要作業系統參與的鎖的開銷小。2.保證可見性java提供了volatile關鍵字來保證可見性。當使用volatile修飾某個變數時,它會保證對該變數的修改會立即被更新到記憶體中,並且將其它快取中對該變數的快取設定成無效,因此其它執行緒需要讀取該值時必須從主記憶體中讀取,從而得到最新的值。3.保證順序性通過volatile關鍵字修飾變數和synchronized和鎖來保證順序性。JVM還通過被稱為happens-before原則隱式地保證順序性。兩個操作的執行順序只要可以通過happens-before推匯出來,則JVM會保證其順序性,反之JVM對其順序性不作任何保證,可對其進行任意必要的重新排序以獲取高效率。程式的重排序java程式在執行的過程中,會經過重排序以獲取更高的執行效率,程式經過編譯器和處理器都會對程式指令進行重排序,在單執行緒中對存在控制依賴的操作重排序,不會改變結果,但多執行緒中,可以會改變程式的執行結果,所以在JMM中使用了happens-before原則,來保證操作間的可見性。

happens-before原則(先行發生原則)

  • 程式順序規則:一個執行緒中的每個操作,happens-before於隨後該執行緒中的任意後續操作
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的獲取
  • volatile變數規則:對一個volatile域的寫,happens-before於對這個變數的讀
  • 傳遞性:如果A happens-before B,B happens-before C,那麼A happens-before C
  • start規則:如果執行緒A執行執行緒B的start方法,那麼執行緒A的ThreadB.start()happens-before於執行緒B的任意操作
  • join規則:如果執行緒A執行執行緒B的join方法,那麼執行緒B的任意操作happens-before於執行緒A從TreadB.join()方法成功返回。

五、Synchronized

synchronized關鍵字是程式最常用的保證執行緒安全的手段。
它的作用就是確保執行緒互斥的訪問同步程式碼,保證共享變數的可見性,有效解決程式執行重排問題實現原理:synchronized修飾方法和synchronized修飾程式碼塊,可以鎖物件也可以鎖類,底層實現原理就是通過每個物件在記憶體中的分配都會有一個物件頭來儲存鎖物件monitor,並用mark word欄位來鎖物件的資訊,每個物件存在著與一個monitor與之關聯,而synchorized就是通過獲取這個monitor來獲取鎖的,對於程式碼塊是使用monitorenter和monitorexit指令來標識同步的,而同步方法則是通過ACC-SYNCHRONIZED來標識的。

六、鎖(Lock)

鎖的四種狀態無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,隨著鎖的競爭可以從偏向鎖升級為輕量級鎖,再升級為重量級鎖,偏向鎖:如果一個執行緒獲得了鎖,那就就會進入偏向模式,當這個執行緒再次請求鎖時,無需再做任何同步操作,自動獲得鎖,提高程式效能。輕量級鎖,是在鎖競爭激烈的情況下,當前鎖被其他執行緒獲得,偏向鎖失效了,會進入輕量級鎖,而不是一下子就進入重量級鎖,從而提升程式效率的。自旋鎖,虛擬機器為了避免執行緒直接在作業系統中掛起做的最後嘗試,就在獲取不到鎖時,先進行一定時間空迴圈後,如果得到鎖,就進入臨界區,否則掛起,鎖升級為重量級鎖重量級鎖,是發生在當前鎖狀態為輕量級鎖時,其他級程鎖自旋失敗後,升級為重量級鎖,其他執行緒進入阻塞,效能降低。鎖消除,是發生在程式進行編譯時會對程式進行掃描,如果發現加鎖的程式碼中不存在共享資源競爭,那麼為了提高程式的效率,會自動消除這個沒必要的鎖。鎖粗化將連續的加鎖,解鎖的操作連結到一起,擴充套件成一個範圍更大的鎖,來提高程式效率。java鎖的種類這個得從多個方面去說在獲取鎖的時候有兩種獲取方式公平鎖:指多個執行緒間按照申請順序獲取鎖非公平鎖,指多執行緒獲取鎖的順序不是按照申請順序。從鎖的特性上說,鎖是一種可入重鎖,指的是當一個執行緒在外層方法獲取到鎖時,在進入內層呼叫方法會自動獲取鎖。從實現上鎖有互斥鎖/讀寫鎖(獨享鎖/共享鎖)互斥鎖(獨享鎖,寫鎖)指的是鎖一次只能被一個執行緒所持有 實現有ReentrantLock,和ReadWriteLock中的寫鎖共享鎖(讀鎖)指的是鎖一次可被多個執行緒持有,實現有ReadWriteLock中的讀鎖分段鎖,是在concurrentHashMap實現執行緒安全時所有的一種鎖的設計在鎖的應用方面可以有樂觀鎖,悲觀鎖悲觀鎖指的是悲觀的認為對一個數據的併發操作都是不安全的,採取加鎖的形式處理而樂觀鎖則認為對一個數據的併發操作樂觀的認為是安全的,不加鎖處理。所以在java中大多數都是使用悲觀所程式設計,而樂觀鎖則應用在CAS無鎖程式設計中。