java面試(二) 多線程

目錄

 

進程與線程的區別

多線程的實現(Thread、Runnable、callabe+futuretask、線程池四種方法的區別、實現原理、適用場景)

線程的啓動(start()和run()的區別)

線程傳遞參數(1.向run()方法進行傳參;2.獲取子線程的返回值)

Runnable與callable的區別

線程狀態(新建、執行、無限等待、限期等待、阻塞、終止6種狀態變換過程)

sleep()和wait()的區別

Notify()和NotifyAll()

Yield ()

線程的停止

線程之間的轉換

線程不安全的原因

Synchronized關鍵字

Synchronized和ReentrantLock的對比、區別、適用場景

鎖的種類:公平鎖與非公平鎖、樂觀鎖與悲觀鎖、獨享鎖與共享鎖、可重入鎖、分段鎖、鎖的狀態(無鎖、偏向鎖、輕量級鎖、重量級鎖、自旋鎖、適應自旋鎖)相關的知識點和對比。

CAS(Compare and Swap )原理

AQS(AbstractQueuedSynchronized)原理

死鎖含義、成立條件

如何有效避免死鎖

銀行家算法

Volatile關鍵字

線程之間的通信

Java線程池

爲什麼使用線程池

線程池的創建、核心參數

線程池的種類

線程池任務執行方法(submit() 、execute()兩種的區別)

線程池關閉

線程池常用等待隊列


進程與線程的區別

進程是執行着的應用程序,而線程是進程內部的一個執行序列。一個進程可以有多個線程。線程又叫做輕量級進程。

地址空間和其它資源:進程間相互獨立,同一進程的各線程間共享。某進程內的線程在其它進程不可見。

通信:進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。

調度和切換:線程上下文切換比進程上下文切換要快得多。

在多線程OS中,進程不是一個可執行的實體

 

多線程的實現(Thread、Runnable、callabe+futuretask、線程池四種方法的區別、實現原理、適用場景)

1、繼承Thread類創建線程類 (extends)
(1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱爲執行體(線程體)。

(2)創建Thread子類的實例,即創建了線程對象。

(3)調用線程對象的start()方法來啓動該線程。  
 

  
public class FirstThreadTest extends Thread{  
    int i = 0;  
    //重寫run方法,run方法的方法體就是現場執行體  
    public void run()  
    {  
        for(;i<100;i++){  
        log.info(getName()+"  "+i);  
        }  
    }  
 
    public static void main(String[] args)  
    {  
        for(int i = 0;i< 100;i++)  
        {  
            log.info(Thread.currentThread().getName()+"  : "+i);  
            if(i==20)  
            {  
                new FirstThreadTest().start();  
                new FirstThreadTest().start();  
            }  
        }  
    }  
} 

 

2、通過Runnable接口創建線程類
(1)定義runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。

(2)創建 Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象。

(3)調用線程對象的start()方法來啓動該線程。

public class RunnableThreadTest implements Runnable  
{  
  
    private int i;  
    public void run()  
    {  
        for(i = 0;i <100;i++)  
        {  
           log.info(Thread.currentThread().getName()+" "+i);  
        }  
    }  
 
    public static void main(String[] args)  
    {  
        for(int i = 0;i < 100;i++)  
        {  
            log.info(Thread.currentThread().getName()+" "+i);  
            if(i==20)  
            {  
                RunnableThreadTest runner= new RunnableThreadTest();  
                new Thread(runner,"新線程1").start();  
                new Thread(runner,"新線程2").start();  
            }  
        }  
    }   
}  

3、通過Callable和Future創建線程

(1)創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值

public interface Callable
{
  V call() throws Exception;
}

(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方

法的返回值。(FutureTask是一個包裝器,它通過接受Callable來創建,它同時實現了Future和Runnable接口。)

(3)使用FutureTask對象作爲Thread對象的target創建並啓動新線程。

(4)調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值

public class CallableThreadTest implements Callable<Integer>  
{  
  
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            log.info(Thread.currentThread().getName()+" 的循環變量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的線程").start();  
            }  
        }  
        try  
        {  
            log.info("子線程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }  
  
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            log.info(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
  
}  

1、採用實現Runnable、Callable接口的方式創建多線程

      優勢:

       線程類只是實現了Runnable接口或Callable接口,還可以繼承其他類。

       在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。

       劣勢:

     編程稍微複雜,如果要訪問當前線程,則必須使用Thread.currentThread()方法。

2、使用繼承Thread類的方式創建多線程

      優勢:

      編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。

      劣勢:

      線程類已經繼承了Thread類,所以不能再繼承其他父類。

3、Runnable和Callable的區別

     (1) Callable規定(重寫)的方法是call(),Runnable規定(重寫)的方法是run()。

     (2) Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。

     (3) call方法可以拋出異常,run方法不可以。

     (4) 運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的

完成,並檢索計算的結果。通過Future對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果future.get()。

 

線程的啓動(start()和run()的區別)

系統調用線程類的start()方法來啓動一個線程,此時該線程處於就緒狀態,而非運行狀態,也就意味着這個線程可以被JVM來調度執行。在調度過程中,JVM通過調用線程類的run()方法來完成實際的操作,當run()方法結束後,此線程就會終止。

​ 如果直接調用線程類的run()方法,這會被當做一個普通的函數調用,程序中仍然只有主線程這一個線程,也就是說,start()方法能夠異步地調用run()方法,但是直接調用run()方法卻是同步的,因此也就無法達到多線程的目的。

​ 只有通過調用線程類的start()方法才能真正達到多線程的目的。
 

線程傳遞參數(1.向run()方法進行傳參;2.獲取子線程的返回值)

1.通過構造方法傳遞數據 

在創建線程時,必須要建立一個Thread類的或其子類的實例。因此,我們不難想到在調用start方法之前通過線程類的構造方法將數據傳入線程。並將傳入的數據使用類變量保存起來,以便線程使用(其實就是在run方法中使用)。

package mythread;
 
public class MyThread1 extends Thread
{
    private String name;
 
    public MyThread1(String name)
    {
        this.name = name;
    }
    public void run()
    {
        System.out.println("hello " + name);
    }
    public static void main(String[] args)
    {
        Thread thread = new MyThread1("world");
        thread.start();        
    }
}

2、通過變量和方法傳遞數據

    向對象中傳入數據一般有兩次機會,第一次機會是在建立對象時通過構造方法將數據傳入,另外一次機會就是在類中定義一系列的public的方法或變量(也可稱之爲字段)。然後在建立完對象後,通過對象實例逐個賦值。下面的代碼是對MyThread1類的改版,使用了一個setName方法來設置name變量:
 

package mythread;
 
public class MyThread2 implements Runnable
{
    private String name;
 
    public void setName(String name)
    {
        this.name = name;
    }
    public void run()
    {
        System.out.println("hello " + name);
    }
    public static void main(String[] args)
    {
        MyThread2 myThread = new MyThread2();
        myThread.setName("world");
        Thread thread = new Thread(myThread);
        thread.start();
    }
}

3、通過回調函數傳遞數據

    上面討論的兩種向線程中傳遞數據的方法是最常用的。但這兩種方法都是main方法中主動將數據傳入線程類的。這對於線程來說,是被動接收這些數據的。然而,在有些應用中需要在線程運行的過程中動態地獲取數據,如在下面代碼的run方法中產生了3個隨機數,然後通過Work類的process方法求這三個隨機數的和,並通過Data類的value將結果返回。從這個例子可以看出,在返回value之前,必須要得到三個隨機數。也就是說,這個value是無法事先就傳入線程類的。

package mythread;
 
class Data
{
    public int value = 0;
}
class Work
{
    public void process(Data data, Integer numbers)
    {
        for (int n : numbers)
        {
            data.value += n;
        }
    }
}
public class MyThread3 extends Thread
{
    private Work work;
 
    public MyThread3(Work work)
    {
        this.work = work;
    }
    public void run()
    {
        java.util.Random random = new java.util.Random();
        Data data = new Data();
        int n1 = random.nextInt(1000);
        int n2 = random.nextInt(2000);
        int n3 = random.nextInt(3000);
        work.process(data, n1, n2, n3);   // 使用回調函數
        System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
                + String.valueOf(n3) + "=" + data.value);
    }
    public static void main(String[] args)
    {
        Thread thread = new MyThread3(new Work());
        thread.start();
    }
}

 在上面代碼中的process方法被稱爲回調函數。從本質上說,回調函數就是事件函數。在Windows API中常使用回調函數和調用API的程序之間進行數據交互。因此,調用回調函數的過程就是最原始的引發事件的過程。在這個例子中調用了process方法來獲得數據也就相當於在run方法中引發了一個事件。

線程狀態(新建、執行、無限等待、限期等待、阻塞、終止6種狀態變換過程)

 

 

sleep()和wait()的區別

1、這兩個方法來自不同的類分別是Thread和Object
2、最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
3、wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用(使用範圍)
4、sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
5、sleep是Thread類的靜態方法。sleep的作用是讓線程休眠制定的時間,在時間到達時恢復,也就是說sleep將在接到時間到達事件事恢復線程執行。wait是Object的方法,也就是說可以對任意一個對象調用wait方法,調用wait方法將會將調用者的線程掛起,直到其他線程調用同一個對象的notify方法纔會重新激活調用者。

Notify()和NotifyAll()

notifyAll調用後,會將全部線程由等待池移到鎖池,然後參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放後再次參與競爭。而notify只會喚醒一個線程。

Yield ()

ield()的作用是讓步,它能夠讓當前線程從“運行狀態”進入到“就緒狀態”,從而讓其他等待線程獲取執行權,但是不能保證在當前線程調用yield()之後,其他線程就一定能獲得執行權,也有可能是當前線程又回到“運行狀態”繼續運行,注意:這裏我將上面的“具有相同優先級”的線程直接改爲了線程,很多資料都寫的是讓具有相同優先級的線程開始競爭,但其實不是這樣的,優先級低的線程在拿到cpu執行權後也是可以執行,只不過優先級高的線程拿到cpu執行權的概率比較大而已,並不是一定能拿到。
 

線程的停止

  1. 使用退出標誌,使線程正常退出,也就是當 run() 方法完成後線程中止。
  2. 使用 stop() 方法強行終止線程,但是不推薦使用這個方法,該方法已被棄用。
  3. 使用 interrupt 方法中斷線程。

線程之間的轉換

Java線程的狀態

線程不安全的原因

  1. 線程的交叉執行(原子性)
  2. 重排序結合線程的交叉執行(可見性)
  3. 共享變量更新後的值沒有在工作內存中與主內存之間及時更新(可見性)

Synchronized關鍵字

public class Client {
    public static void main(String[] args) {
        testSynchronized();
    }
 
    private static void testSynchronized() {
        new Foo().sayHello();
    }
    static class Foo {
    //修飾代碼塊
    void sayHello() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
    //修飾實例方法
    synchronized void sayGood(){
        System.out.println("good");
    }
    //修飾靜態方法
    static synchronized void sayHi(){
        System.out.println("hi");
    }
  }
}

1 修飾實例方法,作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
2 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖 。也就是給當前類加鎖,會作
用於類的所有對象實例,因爲靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態
資源,不管new了多少個對象,只有一份,所以對該類的所有對象都加了鎖)。所以如果一個線程A調用一個實
例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允
許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態
synchronized 方法佔用的鎖是當前實例對象鎖。
3 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。 和 synchronized 方
法一樣,synchronized(this)代碼塊也是鎖定當前對象的。synchronized 關鍵字加到 static 靜態方法和
synchronized(class)代碼塊上都是是給 Class 類上鎖。這裏再提一下:synchronized關鍵字加到非 static 靜態
方法上是給對象實例上鎖。 

實現原理:
JVM 是通過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的。

具體實現是在編譯之後在同步方法調用前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令。

其本質就是對一個對象監視器( Monitor )進行獲取,而這個獲取過程具有排他性從而達到了同一時刻只能一個線程訪問的目的。

而對於沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit 之後才能嘗試繼續獲取鎖。



 

Synchronized和ReentrantLock的對比、區別、適用場景

相似點:
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個線程獲得了對象鎖,進入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的(操作系統需要在用戶態與內核態之間來回切換,代價很高,不過可以通過對鎖優化進行改善)。

功能區別:
這兩種方式最大區別就是對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成

便利性:很明顯Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來加鎖和釋放鎖,爲了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。

鎖的細粒度和靈活度:很明顯ReenTrantLock優於Synchronized

性能的區別:
在Synchronized優化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑑了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
 

鎖的種類:公平鎖與非公平鎖、樂觀鎖與悲觀鎖、獨享鎖與共享鎖、可重入鎖、分段鎖、鎖的狀態(無鎖、偏向鎖、輕量級鎖、重量級鎖、自旋鎖、適應自旋鎖)相關的知識點和對比。

 

樂觀鎖  每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。

樂觀鎖適用於多讀的應用類型,樂觀鎖在Java中是通過使用無鎖編程來實現,最常採用的是CAS算法

悲觀鎖   總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。

傳統的MySQL關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

  • 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
  • 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。

 

公平鎖   就是很公平,在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個,就佔有鎖,否則就會加入到等待隊列中,以後會按照FIFO的規則從隊列中取到自己。

公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖   上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。

非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖。

 

獨享鎖  是指該鎖一次只能被一個線程所持有。

共享鎖  是指該鎖可被多個線程所持有。

對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。

讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。

獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

 

分段鎖  其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

 

可重入鎖  又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因爲之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖。

  • 可重入鎖的一個優點是可一定程度避免死鎖
  • AQS通過控制status狀態來判斷鎖的狀態,對於非可重入鎖狀態不是0則去阻塞;對於可重入鎖如果是0則執行,非0則判斷當前線程是否是獲取到這個鎖的線程,是的話把status狀態+1,釋放的時候,只有status爲0,纔將鎖釋放。

鎖的狀態   無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖(但是鎖的升級是單向的,也就是說只能從低級到高級,不會出現鎖降級)目的是:提高獲取鎖和釋放鎖的效率.

自旋鎖

所謂自旋,就是指當有另外一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個獲得鎖的線程釋放鎖之後,這個線程就可以馬上獲得鎖的。鎖在原地循環的時候,是會消耗cpu的,就相當於在執行一個啥也沒有的for循環。

本來一個線程把鎖釋放之後,當前線程是能夠獲得鎖的,但是假如這個時候有好幾個線程都在競爭這個鎖的話,那麼有可能當前線程會獲取不到鎖,還得原地等待繼續空循環消耗cup,甚至有可能一直獲取不到鎖。

基於這個問題,我們必須給線程空循環設置一個次數,當線程超過了這個次數,我們就認爲,繼續使用自旋鎖就不適合了,此時鎖會再次膨脹,升級爲重量級鎖。

自適應自旋鎖

所謂自適應自旋鎖就是線程空循環等待的自旋次數並非是固定的,而是會動態着根據實際情況來改變自旋等待的次數

 

CAS(Compare and Swap )原理

CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實現了樂觀鎖。

簡單來說,CAS算法有3個三個操作數:

  • 需要讀寫的內存值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它線程去修改它;


AQS(AbstractQueuedSynchronized)原理

抽象隊列同步器(AbstractQueuedSynchronizer,簡稱AQS)是用來構建鎖或者其他同步組件的基礎框架,它使用一個整型的volatile變量(命名爲state)來維護同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。

(感覺這又是個很深奧的問題,看看別人的吧)

https://www.cnblogs.com/sky-chen/p/11365131.html

 

死鎖含義、成立條件

所謂死鎖是指多個進程因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些進程都將無法向前推進。

死鎖產生的4個必要條件:

互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某 資源僅爲一個進程所佔有。此時若有其他進程請求該資源,則請求進程只能等待。

不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。

請求和保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程佔有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。

循環等待條件:存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。

 

如何有效避免死鎖

  1. 避免多次鎖定。儘量避免同一個線程對多個 Lock 進行鎖定。例如上面的死鎖程序,主線程要對 A、B 兩個對象的 Lock 進行鎖定,副線程也要對 A、B 兩個對象的 Lock 進行鎖定,這就埋下了導致死鎖的隱患。
  2. 具有相同的加鎖順序。如果多個線程需要對多個 Lock 進行鎖定,則應該保證它們以相同的順序請求加鎖。比如上面的死鎖程序,主線程先對 A 對象的 Lock 加鎖,再對 B 對象的 Lock 加鎖;而副線程則先對 B 對象的 Lock 加鎖,再對 A 對象的 Lock 加鎖。這種加鎖順序很容易形成嵌套鎖定,進而導致死鎖。如果讓主線程、副線程按照相同的順序加鎖,就可以避免這個問題。
  3. 使用定時鎖。程序在調用 acquire() 方法加鎖時可指定 timeout 參數,該參數指定超過 timeout 秒後會自動釋放對 Lock 的鎖定,這樣就可以解開死鎖了。
  4. 死鎖檢測。死鎖檢測是一種依靠算法機制來實現的死鎖預防機制,它主要是針對那些不可能實現按序加鎖,也不能使用定時鎖的場景的。

最具有代表性的避免死鎖的算法是dijkstra的銀行家算法

 

銀行家算法

當一個進程申請使用資源的時候,銀行家算法通過先 試探 分配給該進程資源,然後通過安全性算法判斷分配後的系統是否處於安全狀態,若不安全則試探分配作廢,讓該進程繼續等待。

這裏寫圖片描述

 

想看具體算法請點下面鏈接 

https://www.cnblogs.com/home123/p/7509066.html 

Volatile關鍵字

用以聲明變量的值可能隨時會別的線程修改,使用volatile修飾的變量會強制將修改的值立即寫入主存,主存中值的更新會使緩存中的值失效(非volatile變量不具備這樣的特性,非volatile變量的值會被緩存,線程A更新了這個值,線程B讀取這個變量的值時可能讀到的並不是是線程A更新後的值)。volatile會禁止指令重排。

volatile具有可見性、有序性,不具備原子性

  • 原子性:如果你瞭解事務,那這個概念應該好理解。原子性通常指多個操作不存在只執行一部分的情況,如果全部執行完成那沒毛病,如果只執行了一部分,那對不起,你得撤銷(即事務中的回滾)已經執行的部分。
  • 可見性:當多個線程訪問同一個變量x時,線程1修改了變量x的值,線程1、線程2...線程n能夠立即讀取到線程1修改後的值。
  • 有序性:即程序執行時按照代碼書寫的先後順序執行。在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。(本文不對指令重排作介紹,但不代表它不重要,它是理解JAVA併發原理時非常重要的一個概念)。

適用場景

  • 適用於對變量的寫操作不依賴於當前值,對變量的讀取操作不依賴於非volatile變量。
  • 適用於讀多寫少的場景。
  • 可用作狀態標誌。
  • JDK中volatie應用:JDK中ConcurrentHashMap的Entry的value和next被聲明爲volatile,AtomicLong中的value被聲明爲volatile。AtomicLong通過CAS原理(也可以理解爲樂觀鎖)保證了原子性。

 

線程之間的通信

1.同步
就是通過Synchronized關鍵字來進行同步訪問控制,確保誰拿到了相應的鎖才能執行相應的操作

本質上就是共享內存式的通信,這個共享內存在java的內存模型中就是主內存,相當於通過主內存的數據進行線程通信。因Synchronized解鎖時會將工作內存中的數據刷新到主內存中,Synchronized加鎖時會將工作內存中的值清空從主內存讀。多個線程訪問同一變量,誰拿到了鎖誰就去訪問。

2.while輪詢
假設我們添加線程向一個List中存入元素(一直存入),判斷線程判斷如果存入的元素達到了3個,我們就退出存入元素的線程,轉而進入另一個等待它存入三個元素的線程。那麼這個等待的線程我們使用while輪詢list集合中是否達到了三個,如果到了三個我們就進行下一步,沒有就一直輪詢,

問題:

1.我們發現判斷的線程如果沒達到它的要求,cpu執行到它時就一直空轉,白白浪費

2.這個方法還有一個問題,我們沒辦法保證可見性,也就是說假設當加入元素的線程到達了3個,但是此時元素數量3只是處於工作內存中,那麼在它將工作內存中的3刷新到共享內存中的這段時間中可能又加入了新的元素,加入後等待線程取到值,可是這時候已經大於3了,所以程序會出現問題。

3.wait/notify機制
這是一個Object裏的方法,兩個方法的作用就是沉睡和喚醒,當我們的等待線程發現沒有達到想要的條件我們就沉睡它,此時另一個線程來加入元素,當元素數量達到了3 ,我們可以喚醒等待線程,告訴他你的條件達到了,你繼續執行吧

問題:如果說添加元素的線程一下添加了3個,進行了喚醒操作,但是等待線程還沒運行到wait,這時產生了次空喚醒。當等待線程執行到wait之後沉睡,因爲它要依靠添加元素的線程喚醒,但是添加元素的線程已經進行了喚醒,因此會一直沉睡。

4.消息管道:
就是通過一條管道傳輸線程之間通信的消息。

 

進程間通訊的方式有哪些,各有什麼優缺點


1)管道:管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程之間使用。進程的親緣關係通常是指父子進程關係。

2)有名管道(FIFO):有名管道也是半雙工的通信方式,但是允許在沒有親緣關係的進程之間使用,管道是先進先出的通信方式。

3)信號量:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。

4)消息隊列:消息隊列是有消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。

5)信號 ( sinal ) :信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。

6)共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
7)套接字( socket ) :套接字也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同機器間的進程通信。
 

爲什麼使用線程池

1.可以節省創建線程和銷燬線程需要的系統資源;

2.可以提高響應的速度,減少用戶等待時間;

3.通過控制線程池的大小,可以增強系統的可控性.

線程池的創建、核心參數

Java通過Executors提供四種線程池,分別爲:

  • newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
  • newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
  • newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
  • newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

 

參數

  • corePoolSize :核心線程數量
  • maximumPoolSize :線程最大線程數
  • workQueue :工作隊列,存儲等待執行的任務 很重要 會對線程池運行產生重大影響
  • keepAliveTime :空閒線程存活時間
  • unit :keepAliveTime的時間單位
  • threadFactory :線程工廠,用來創建線程
  • rejectHandler :當拒絕處理任務時的策略

ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。

ThreadPoolExecutor.DiscardPolicy:丟棄任務,但是不拋出異常。

ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新提交被拒絕的任務 ThreadPoolExecutor.CallerRunsPolicy:由調用線程(提交任務的線程)處理該任務

線程池的種類

1)   FixedThreadPool

這是一個線程數固定的線程池,

當需要運行的線程數量大體上變化不大時,適合使用這種線程池。它可以一次性支付高昂的創建線程的開銷,之後再使用的時候就不再需要這種開銷。

2)   SingleThreadExecutor

這是一個線程數量爲1的線程池,所有提交的這個線程池的任務都會按照提交的先後順序排隊執行。由於任務之間沒有併發執行,因此提交到線程池種的任務之間不會相互干擾。程序執行的結果更具有確定性。

3)   CachedThreadPool

任務提交到線程池的時候,如果池中沒有空閒的線程,線程池就會爲這個任務創建一個線程,如果有空閒的線程,就會使用已有的空閒線程執行任務。

有一個銷燬機制,如果一個線程60秒之內沒有被使用過,這個線程就會被銷燬,這樣就節省了很多資源。CachedThreadPool是一個比較通用的線程池,它在多數情況下都能表現出優良的性能。以後編碼的時候,遇事不決,用緩存(線程池)。

線程池任務執行方法(submit() 、execute()兩種的區別)

  • execute() 參數 Runnable ;submit() 參數 (Runnable) 或 (Runnable 和 結果 T) 或 (Callable)
  • execute() 沒有返回值;而 submit() 有返回值
  • submit() 的返回值 Future 調用get方法時,可以捕獲處理異常

線程池關閉

可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。
原理都是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。
只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。
當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。
至於應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown方法來關閉線程池,如果任務不一定要執行完,則可以調用shutdownNow方法。

線程池常用等待隊列

工作隊列的默認選項是 SynchronousQueue 在使用ThreadPoolExecutor線程池的時候,需要指定一個實現了BlockingQueue接口的任務等待隊列。

在ThreadPoolExecutor線程池的API文檔中,一共推薦了三種等待隊列,它們是:

SynchronousQueue、LinkedBlockingQueue和ArrayBlockingQueue。

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