執行緒鎖(四)

童話述說我的結局發表於2020-10-04

前面篇幅講了很多理論及原理性東西,今天想了想,來點現實場景的東西把前面的內容串一串

一. 死鎖產生的原因

1) 系統資源的競爭

通常系統中擁有的不可剝奪資源,其數量不足以滿足多個程式執行的需要,使得程式在 執行過程中,會因爭奪資源而陷入僵局,如磁帶機、印表機等。只有對不可剝奪資源的競爭 才可能產生死鎖,對可剝奪資源的競爭是不會引起死鎖的。

2) 程式推進順序非法

程式在執行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,併發程式 P1、P2分別保持了資源R1、R2,而程式P1申請資源R2,程式P2申請資源R1時,兩者都 會因為所需資源被佔用而阻塞。

訊號量使用不當也會造成死鎖。程式間彼此相互等待對方發來的訊息,結果也會使得這 些程式間無法繼續向前推進。例如,程式A等待程式B發的訊息,程式B又在等待程式A 發的訊息,可以看出程式A和B不是因為競爭同一資源,而是在等待對方的資源導致死鎖。

3) 死鎖產生的必要條件

產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。

 

  • 互斥,共享資源 X 和 Y 只能被一個執行緒佔用;
  • 佔有且等待,執行緒 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
  • 不可搶佔,其他執行緒不能強行搶佔執行緒 T1 佔有的資源;
  • 迴圈等待,執行緒 T1 等待執行緒 T2 佔有的資源,執行緒 T2 等待執行緒 T1 佔有的資源,就是迴圈等待

 

下面就定義來演示一個死鎖場景的例子:

public class Transfer {
    private String accountName;
    private int balance;

    public Transfer(String accountName, int balance) {
        this.accountName = accountName;
        this.balance = balance;
    }

    public void debit(int amount){ //更新轉出方的餘額
        this.balance-=amount;
    }

    public void credit(int amount){ //更新轉入方的餘額
        this.balance+=amount;
    }

    public String getAccountName() {
        return accountName;
    }

    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }
}

  

public class Deadlock  implements  Runnable{

    private Transfer fromAccount; //轉出賬戶
    private Transfer toAccount; //轉入賬戶
    private int amount;  //轉入金額

    public Deadlock(Transfer fromAccount, Transfer toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;

    }
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fromAccount) {
                synchronized (toAccount) {
                    if (fromAccount.getBalance() >= amount) {
                        fromAccount.debit(amount);
                        toAccount.credit(amount);
                    }
                }
            }
            //轉出賬戶的餘額
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //轉入賬戶的餘額
            System.out.println(toAccount.getAccountName() + "-->" + toAccount.getBalance());

        }

    }

    public static void main(String[] args) throws InterruptedException {
        Transfer fromAccount=new Transfer("使用者A",100000);
        Transfer toAccount=new Transfer("使用者B",300000);

        Thread a =new Thread(new Deadlock(fromAccount,toAccount,10));
        Thread b=new Thread(new Deadlock(toAccount,fromAccount,30));

        a.start();
        b.start();
    }
} 

 如下圖,死鎖條件產生了,下面死鎖產生的條件就是互斥產生的,想要破壞死鎖只用破壞死鎖的四大條件的其中一個就可以,下面我們就上面程式碼優化下,破壞佔有且等待這個條件來破壞。

 

 

 

 我們增加一個類,專門用來分配資源

public class Allocator {

    private List<Object> list=new ArrayList<>();
    //保證互斥,只有一個執行緒能進入此方法
    synchronized  boolean apply(Object from,Object to){
        if(list.contains(from)||list.contains(to)){
            return false;
        }
        list.add(from);
        list.add(to);
        return true;
    }

    synchronized void free(Object from,Object to){
        list.remove(from);
        list.remove(to);
    }


}

  修改原有的Deadlock類

public class Deadlock  implements  Runnable{

    private Transfer fromAccount; //轉出賬戶
    private Transfer toAccount; //轉入賬戶
    private int amount;  //轉入金額
    Allocator allocator;

    public Deadlock(Transfer fromAccount, Transfer toAccount, int amount,Allocator allocator) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
        this.allocator=allocator;
    }
    @Override
    public void run() {
        while(true){
//鎖力度調大,在if層面 if (allocator.apply(fromAccount,toAccount)) { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance() >= amount) { fromAccount.debit(amount); toAccount.credit(amount); } } } } //轉出賬戶的餘額 System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance()); //轉入賬戶的餘額 System.out.println(toAccount.getAccountName() + "-->" + toAccount.getBalance()); } } public static void main(String[] args) throws InterruptedException { Transfer fromAccount=new Transfer("使用者A",100000); Transfer toAccount=new Transfer("使用者B",300000); Allocator allocator=new Allocator(); Thread a =new Thread(new Deadlock(fromAccount,toAccount,10,allocator)); Thread b=new Thread(new Deadlock(toAccount,fromAccount,30,allocator)); a.start(); b.start(); } }

 自己執行下會發現死鎖解決了,上面我們是破壞了佔有且等待這個條件解決了死鎖,下面我們就不可搶佔這個條件來解決死鎖問題,我們利用Lock的屬性來破壞,將Deadlock程式碼再修改一次(Lock我想在下個篇幅中具體講)

public class Deadlock1 implements  Runnable{

    private Transfer fromAccount; //轉出賬戶
    private Transfer toAccount; //轉入賬戶
    private int amount;  //轉入金額

    Lock fromLock=new ReentrantLock();
    Lock toLock=new ReentrantLock();

    public Deadlock1(Transfer fromAccount, Transfer toAccount, int amount) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
    }
    @Override
    public void run() {
        while(true){

              if (fromLock.tryLock()) {
                  if (toLock.tryLock()) {
                      if (fromAccount.getBalance() >= amount) {
                          fromAccount.debit(amount);
                          toAccount.credit(amount);
                      }
                  }
              }

            //轉出賬戶的餘額
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //轉入賬戶的餘額
            System.out.println(toAccount.getAccountName() + "-->" + toAccount.getBalance());

        }

    }

    public static void main(String[] args) throws InterruptedException {
        Transfer fromAccount=new Transfer("使用者A",100000);
        Transfer toAccount=new Transfer("使用者B",300000);

        Thread a =new Thread(new Deadlock1(fromAccount,toAccount,10));
        Thread b=new Thread(new Deadlock1(toAccount,fromAccount,30));

        a.start();
        b.start();
    }
}

  

最後我們再迴圈等待條件破壞死鎖

public class Deadlock2  implements  Runnable{

    private Transfer fromAccount; //轉出賬戶
    private Transfer toAccount; //轉入賬戶
    private int amount;  //轉入金額


    public Deadlock2(Transfer fromAccount, Transfer toAccount, int amount ) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;

    }
    @Override
    public void run() {
        Transfer left=null;
        Transfer right=null;
        //控制加鎖順序
        if (fromAccount.hashCode()>toAccount.hashCode()){
            left=toAccount;
            right=fromAccount;
        }
        while(true){
                synchronized (left) {
                    synchronized (right) {
                        if (fromAccount.getBalance() >= amount) {
                            fromAccount.debit(amount);
                            toAccount.credit(amount);
                        }
                    }
                }

            //轉出賬戶的餘額
            System.out.println(fromAccount.getAccountName() + "->" + fromAccount.getBalance());
            //轉入賬戶的餘額
            System.out.println(toAccount.getAccountName() + "-->" + toAccount.getBalance());

        }

    }

    public static void main(String[] args) throws InterruptedException {
        Transfer fromAccount=new Transfer("使用者A",100000);
        Transfer toAccount=new Transfer("使用者B",300000);

        Allocator allocator=new Allocator();
        Thread a =new Thread(new Deadlock(fromAccount,toAccount,10,allocator));
        Thread b=new Thread(new Deadlock(toAccount,fromAccount,30,allocator));

        a.start();
        b.start();
    }
}

二.Thread.join

       它的作用其實就是讓執行緒的執行結果對後續執行緒的訪問可見。在講之前我們先用一個例子說明,下面例子說明了輸出值的不穩定性,因為我們啟動的執行緒可能沒一下啟動,main執行緒先執行完了就會出現這結果

 

 如果我們想要執行緒的結果對主執行緒可見我們可以用join,看下圖結果我們一定有問題,為啥加了join就對主執行緒可見,下面我們就join的原理講解下

 

 我們點選t.join();看下join方法做了什麼事情,看到下面原始碼我們發現一個關健字wait,如果看過我前面寫的文章朋友應該很快想到他的原理就是阻塞,想到了阻塞那就一定有喚醒,那我們想,喚醒阻塞程式碼的地方會在哪裡呢。

//首先原始碼中方法中加了鎖
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }

一個執行緒在run方法執行結束時就代表一個執行緒的終止,執行緒終止會呼叫C的javaThread:: exit方法;在這個方法裡面會有個清理的方法叫ensure_join(this);這個方法會喚醒阻塞方法下的執行緒;

三.ThreadLocal

       ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。說白點就是實際上一種執行緒隔離機制,也是為了保證在多執行緒環境下對於共享變數的訪問的安全性。下面我們就一個例子說明下

    

 

 上面的例子我們發現每個執行緒間存在干擾問題,而且有的執行緒取的值還是相同的,如果我們想做到執行緒問的隔離,那我們就可以用到ThreadLocal,下圖結果我們發現做到執行緒的隔離

 

 下面我們就ThreadLocal是如何實現的,我們進入原始碼

在原始碼的set方法 

 public void set(T value) {
//獲取當前執行緒 Thread t = Thread.currentThread();
//有Map最先想到的是儲存,我們點進去看下 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }

 點進去好像也沒有啥東西

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
//ThreadLocal.ThreadLocalMap threadLocals = null;
}

 那我們就行先暫時跳過,我們走if邏輯,我們第一次進來map是空的所有走createMap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  下面這段程式碼對每個執行緒都會建立一個儲存,這就做到了執行緒間的隔離,而且下面原始碼中用到了hash計算及線性探測,如果我要全寫完的話要化更多的時間,而且跟我要講的執行緒主題偏遠了,後面如果有時間,我再單獨就這塊內容的線性探測深入講解下

 
   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//有興趣可以點進看下Entry;這一步是初始化一個長度 table = new Entry[INITIAL_CAPACITY];
//這裡面i的值其實就是一個下標,這裡裡其實就是用hash計算出下標位置,然後將資料儲存起來 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }

  

 

 

 

相關文章