天天看點

Java基礎常見面試題——鎖

1.synchronized鎖實作原理? (Lock)和 (synchronized)兩種鎖差別?

synchronized可以保證方法或者代碼塊在運作時,同一時刻隻有一個方法可以進入到臨界區,同時它還可以保證共享變量的記憶體可見性。

synchronized是用java的monitor機制來實作的,就是synchronized代碼塊或者方法進入及退出的時候會生成monitorenter跟monitorexit兩條指令。線程執行到monitorenter時會嘗試擷取對象所對應的monitor所有權,即嘗試擷取的對象的鎖;monitorexit即為釋放鎖。

Synchronized是java語言中的一個重量級的操作,因為java線程是映射到作業系統的原生線程上的,阻塞或者喚醒一條線程,都需要作業系統來幫忙完成,需要從使用者态切換到核心态,轉換需要消耗很多處理時間,可能比使用者代碼執行的時間還長。虛拟機對此作了一些優化,比如 自旋鎖,避免頻繁進入切換到核心态中。

ReentrantLock重入鎖

ReentrantLock和 Synchronized類似,一個表現為API 層面上的互斥(lock 和 unlock 方法),一個表現為原生文法層面上的互斥。ReentrantLock 比 Synchronized增加了一些進階功能。

①等待中斷:持有鎖的線程長期不釋放鎖(執行時間長的同步塊)的時候,正在等待的線程可以選擇放棄等待,做其他事情。

②實作公平鎖:ReentrantLock 預設是非公平的,通過構造參數可設定為公平鎖,Synchronized是非公平的。 公平:按照申請鎖的時間,先來先得,有序。

③鎖可以綁定多個條件。

jdk1.6對鎖的實作引入了大量的優化,如自旋鎖、适應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

2.什麼是死鎖(deadlock)?為什麼會死鎖?死鎖出現後如何消除?

所謂死鎖是指多個程序因競争資源而造成的一種僵局(互相等待),若無外力作用,這些程序都将無法向前推進。比如:程序A占有對象1的鎖,程序B占有對象2的鎖,程序A需要對象2的鎖才能繼續執行,是以程序A會等待程序B釋放對象2的鎖,而程序B需要對象1的鎖才能繼續執行,同樣會等待程序A釋放對象1的鎖,由于這兩個程序都不釋放已占有的鎖,是以導緻他們進入無限等待中

産生死鎖的原因主要是:

(1) 因為系統資源不足。

(2) 程序運作推進的順序不合适。

(3) 資源配置設定不當等。

如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因争奪有限的資源而陷入死鎖。其次,程序運作推進順序與速度不同,也可能産生死鎖。産生死鎖的四個必要條件:

(1) 互斥條件:一個資源每次隻能被一個程序使用。

(2) 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。

(3) 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。

(4) 循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系。

這四個條件是死鎖的必要條件,隻要系統發生死鎖,這些條件必然成立,而隻要上述條件之一不滿足,就不會發生死鎖。

死鎖的解除與預防:

了解了死鎖的原因,尤其是産生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。是以,在系統設計、程序排程等方面注意如何不讓這四個必要條件成立,如何确定資源的合理配置設定算法,避免程序永久占據系統資源。此外,也要防止程序在處于等待狀态的情況下占用資源。是以,對資源的配置設定要給予合理的規劃。

消除死鎖的3種方式:

1.最簡單、最常用的方式就是系統重新開機(代價大,前面的計算廢棄);

2.撤銷程序,剝奪資源(一次性撤銷和剝奪全部或逐漸撤銷與剝奪);

3.程序回退政策(讓程序回退到為死鎖的某一時刻或狀态)。

3.場景題:現在有三個線程,同時start,用什麼方法可以保證線程執行的順序,線程一執行完線程二執行,線程二執行完線程三執行?

一個簡單的辦法:指定擷取鎖的順序,并強制線程按照指定的順序擷取鎖。是以,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖。簡單來說,就是确定前一線程已經執行完畢,才可以執行下一線程。

法1:調用Thread.join(),确定Thread線程執行完;

法2:CountDownLatch,建立線程類的時候,将上一個計數器和本線程計數器傳入。運作前執行上一個計數器.await(前一線程為0才可以執行),再執行本計數器.countDown(本線程計數器減少)。

例題:如下程式輸出為?

public class TestSync2 implements Runnable {
    int b = 100;          
    synchronized void m1() throws InterruptedException {
        b = 1000;
        Thread.sleep(500); //6     導緻下面輸出語句執行在後面
        System.out.println("b=" + b);
    }
    synchronized void m2() throws InterruptedException {
        Thread.sleep(250); //5
        b = 2000;
    }
    public static void main(String[] args) throws InterruptedException {
        TestSync2 tt = new TestSync2();
        Thread t = new Thread(tt);  //1
        t.start(); //2
        tt.m2(); //3
        System.out.println("main thread b=" + tt.b); //4
}
    @Override
    public void run() {
        try {
            m1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出:main thread b=2000  或  main thread b=1000
b=1000                 b=1000
           

分析:java 都是從main方法執行的,上面說了有2個線程,但是這裡就算修改線程優先級也沒用,優先級是在2個程式都還沒有執行的時候才有先後,現在這個代碼一執行,主線程main已經執行了。當執行1步驟的時候(Thread t = new Thread(tt); //1)線程是new狀态,還沒有開始工作。當執行2步驟的時候(t.start(); //2)當調用start方法,這個線程才正真被啟動,進入runnable狀态,runnable狀态表示可以執行,一切準備就緒了,但是并不表示一定在cpu上面執行,有沒有真正執行取決服務cpu的排程。在這裡當執行3步驟必定是先獲得鎖(由于start需要調用native方法,并且在用完成之後在一切準備就緒了,但是并不表示一定在cpu上面執行,有沒有真正執行取決服務cpu的排程,之後才會調用run方法,執行m1方法)。這裡其實2個synchronized方法裡面的Thread.sheep其實要不要是無所謂的,估計是就為混淆增加難度。3步驟執行的時候其實很快子線程也準備好了,但是由于synchronized的存在,并且是作用同一對象,是以子線程就隻有必須等待了。由于main方法裡面執行順序是順序執行的,是以必須是步驟3執行完成之後才可以到4步驟,而由于3步驟執行完成,子線程就可以執行m1了。這裡就存在一個多線程誰先擷取到問題,如果4步驟先擷取那麼main thread b=2000,如果子線程m1擷取到可能就b已經指派成1000或者還沒有來得及指派4步驟就輸出了,可能結果就是main thread b=1000或者main thread b=2000,在這裡如果把6步驟去掉那麼b=執行在前和main thread b=在前就不确定了。但是由于6步驟存在,是以不管怎麼都是main thread b=在前面,那麼等于1000還是2000看情況,之後b=1000是一定固定的。