死鎖

在這裡插入圖片描述

概念

死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。

產生條件

雖然程序在執行過程中,可能發生死鎖,但死鎖的發生也必須具備一定的條件,死鎖的發生必須具備以下四個必要條件。

  1. 互斥條件:指程序對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程序佔用。如果此時還有其它程序請求資源,則請求者只能等待,直至佔有資源的程序用畢釋放。
  2. 請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序佔有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。
  3. 不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
  4. 環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鏈,即程序集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

例子

package com.mmall.concurrency.example.deadLock;

import lombok.extern.slf4j.Slf4j;

/**
 * 一個簡單的死鎖類
 * 當DeadLock類的物件flag==1時(td1),先鎖定o1,睡眠500毫秒
 * 而td1在睡眠的時候另一個flag==0的物件(td2)執行緒啟動,先鎖定o2,睡眠500毫秒
 * td1睡眠結束後需要鎖定o2才能繼續執行,而此時o2已被td2鎖定;
 * td2睡眠結束後需要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
 * td1、td2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。
 */

@Slf4j
public class DeadLock implements Runnable {
    public int flag = 1;
    //靜態物件是類的所有物件共享的
    private static Object o1 = new Object(), o2 = new Object();

    @Override
    public void run() {
        log.info("flag:{}", flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    log.info("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    log.info("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都處於可執行狀態,但JVM執行緒排程先執行哪個執行緒是不確定的。
        //td2的run()可能在td1的run()之前執行
        new Thread(td1).start();
        new Thread(td2).start();
    }
}
/*
14:43:34.169 [Thread-1] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:0
14:43:34.169 [Thread-0] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:1
 */

併發最佳實踐

  1. 使用本地變數
    應該儘量使用本地變數,而不是建立一個類或者例項的變數。

  2. 使用不可變類
    String、Integer等。不可變類可以降低程式碼中的需要的同步數量

  3. 最小化鎖的作用域範圍:S=1/(1-a+a/n)
    a:平行計算部分所佔比例
    n:並行處理結點個數
    S:加速比
    當1-a等於0時,沒有序列只有並行,最大加速比 S=n
    當a=0時,只有序列沒有並行,最小加速比 S = 1
    當n→∞時,極限加速比 s→ 1/(1-a)
    例如,若序列程式碼佔整個程式碼的25%,則並行處理的總體效能不可能超過4。該公式稱為:“阿姆達爾定律"或"安達爾定理”。

  4. 使用執行緒池的Executor,而不是直接new Thread 執行
    建立一個執行緒的代價是昂貴的,如果要建立一個可伸縮的Java應用,那麼你需要使用執行緒池。

  5. 寧可使用同步也不要使用執行緒的wait和notify
    從Java1.5以後,增加了許多同步工具,如:CountDownLatch、CyclicBarrier、Semaphore等,應該優先使用這些同步工具。

  6. 使用BlockingQueue實現生產-消費模式
    阻塞佇列不僅可以處理單個生產、單個消費,也可以處理多個生產和消費。

  7. 使用併發集合而不是加了鎖的同步集合
    Java提供了下面幾種併發集合框架:

    ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentLinkedQueue 、ConcurrentLinkedDeque

  8. 使用Semaphone建立有界的訪問
    為了建立穩定可靠的系統,對於資料庫、檔案系統和socket等資源必須要做有機的訪問,Semaphone可以限制這些資源開銷的選擇,Semaphone可以以最低的代價阻塞執行緒等待,可以通過Semaphone來控制同時訪問指定資源的執行緒數。

  9. 寧可使用同步程式碼塊,也不實用同步的方法
    主要針對synchronized關鍵字。使用synchronized關鍵字同步程式碼塊只會鎖定一個物件,而不會將整個方法鎖定。如果更改共同的變數或類的欄位,首先應該選擇的是原子型變數,然後使用volatile。如果需要互斥鎖,可以考慮使用ReentrantLock。

  10. 避免使用靜態變數
    靜態變數在併發執行環境下會製造很多問題,如果必須使用靜態變數,那麼優先是它成為final變數,如果用來儲存集合collection,那麼可以考慮使用只讀集合,否則一定要做特別多的同步處理和併發處理操作。

Spring與執行緒安全

Spring作為一個IOC容器幫助我們管理了許多bean,但Spring並沒有保證它們的執行緒安全,而是將這個任務交給了開發者,需要開發者來編寫執行緒安全的程式碼。

  • Spring bean : singleton , prototype
    Spring在建立Bean時需要為其設定作用域,以singleton為例,它是Spring的預設作用域即單例模式,它的生命週期和Spring容器是一致的,只在第一次注入時會被建立,另一個prototype,每次注入都會建立。
  • 無狀態的物件
    無狀態的物件很適合以singleton,它不用擔心多個執行緒的操作而導致自身狀態被破壞,因此可以說每個無狀態的物件都是執行緒安全的。我們實際使用的DAO、DTO、Entity等等各種業務物件就是無狀態物件,它們只是負責執行某些操作或者傳遞資料,其自身不攜帶狀態。

可以通過將bean的作用域都設定為prototype來保證執行緒安全嗎?理論上可以,但是這種頻繁建立物件的方式會極大地影響應用的效能。