Java線程和多線程(十五)——線程的活性

當開發者在應用中使用了併發來提高性能的同時,開發者也須要注意線程之間有可能會相互阻塞。當整個應用執行的速度比預期要慢的時候,也就是應用沒有按照預期的執行時間執行完畢。在本章中,咱們來須要仔細分析可能會影響應用多線程的活性問題。java

死鎖

死鎖的概念在軟件開發者中已經廣爲熟知了,甚至普通的計算機用戶也會常用這個概念,儘管不是在正確的情況下使用。嚴格來講,死鎖意味着兩個或者更多線程在等待另外一個線程釋放其鎖定的資源,而請求資源的線程自己也鎖定了對方線程所請求的資源。以下:sql

Thread 1: locks resource A, waits for resource B
Thread 2: locks resource B, waits for resource A

爲了更好的理解問題,參考一下以下的代碼:bash

public class Deadlock implements Runnable {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    private final Random random = new Random(System.currentTimeMillis());
    public static void main(String[] args) {
        Thread myThread1 = new Thread(new Deadlock(), "thread-1");
        Thread myThread2 = new Thread(new Deadlock(), "thread-2");
        myThread1.start();
        myThread2.start();
    }

    public void run() {
        for (int i = 0; i < 10000; i++) {
            boolean b = random.nextBoolean();
            if (b) {
                System.out.println("[" + Thread.currentThread().getName() + 
                "] Trying to lock resource 1.");
                synchronized (resource1) {
                    System.out.println("[" + Thread.currentThread(). 
                        getName() + "] Locked resource 1.");
                    System.out.println("[" + Thread.currentThread().  
                        getName() + "] Trying to lock resource 2.");
                    synchronized (resource2) {
                        System.out.println("[" + Thread.  
                        currentThread().getName() + "] Locked resource 2.");
                    }
                }
            } else {
                System.out.println("[" + Thread.currentThread().getName() +  
                "] Trying to lock resource 2.");
                synchronized (resource2) {
                    System.out.println("[" + Thread.currentThread().  
                        getName() + "] Locked resource 2.");
                    System.out.println("[" + Thread.currentThread().  
                        getName() + "] Trying to lock resource 1.");
                    synchronized (resource1) {
                        System.out.println("[" + Thread.  
                        currentThread().getName() + "] Locked resource 1.");
                    }
                }
            }
        }
    }
}

從上面的代碼中能夠看出,兩個線程分別啓動,而且嘗試鎖定2個靜態的資源。但對於死鎖,咱們須要兩個線程的以不一樣順序鎖定資源,所以咱們利用隨機實例選擇線程要首先鎖定的資源。markdown

若是布爾變量btrueresource1會鎖定,而後嘗試去得到resource2的鎖。若是bfalse,線程會優先鎖定resource2,然而嘗試鎖定resource1。程序不用一下子就會碰到死鎖問題,而後就會一直掛住,直到咱們結束了JVM纔會結束:多線程

[thread-1] Trying to lock resource 1. [thread-1] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 1. [thread-2] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 2. [thread-1] Trying to lock resource 1.

在上面的執行中,thread-1持有了resource2的鎖,等待resource1的鎖,而線程thread-2持有了resource1的鎖,等待resource2的鎖。併發

若是咱們將b的值配置true或者false的話,是不會碰到死鎖的,由於執行的順序始終是一致的,那麼thread-1thread-2請求鎖的順序始終是一致的。兩個線程都會以一樣的順序請求鎖,那麼最多會暫時阻塞一個線程,最終都可以順序執行。dom

大概來講,形成死鎖須要以下的一些條件:函數

  • 互斥:必須存在一個資源在某個時刻,僅能由一個線程訪問。
  • 資源持有:當鎖定了一個資源的時候,線程仍然須要去得到另一個資源的鎖。
  • 沒有搶佔策略:當某個線程已經持有了資源一段時間的時候,沒有可以強佔線程鎖定資源的機制。
  • 循環等待:在運行時必須存在兩個或者更多的線程,相互請求對方鎖定的資源。

儘管產生死鎖的條件看起來較多,可是在多線程應用中存在死鎖仍是比較常見的。開發者能夠經過打破死鎖構成的必要條件來避免死鎖的產生,參考以下:post

  • 互斥: 這個需求一般來講是不可避免的,資源不少時候確實只能互斥訪問的。可是並非老是這樣的。當使用DBMS系統的時候,可能使用相似樂觀鎖的方式來代替原來的悲觀鎖的機制(在更新數據的時候鎖定表中的一行)。
  • 還有一種可行的方案,就是對資源持有進行處理,當獲取了某一資源的鎖以後,馬上獲取其餘所必須資源的鎖,若是獲取鎖失敗了,則釋放掉以前全部的互斥資源。固然,這種方式並非老是能夠的,有可能鎖定的資源以前是沒法知道的,或者是廢棄了的資源。
  • 若是鎖不能馬上獲取,防止出現死鎖的一種方式就是給鎖的獲取配置上一個超時時間。在SDK類中的ReentrantLock就提供了相似超時的方法。
  • 從上面的代碼中,咱們能夠發現,若是每一個線程的鎖定資源的順序是相同的,是不會產生死鎖的。而這個過程能夠經過將全部請求鎖的代碼都抽象到一個方法,而後由線程調用來實現。這就能夠有效的避免死鎖。

在一個更高級的應用中,開發者或許須要考慮實現一個檢測死鎖的系統。在這個系統中,來實現一些基於線程的監控,當前程獲取一個鎖,而且嘗試請求別的鎖的時候,都記錄日誌。若是以線程和鎖構成有向圖,開發者是可以檢測到2不一樣的線程持有資源而且同時請求另外的阻塞的資源的。若是開發者能夠檢測,並可以強制阻塞的線程釋放掉已經獲取的資源,就可以自動檢測到死鎖而且自動修復死鎖問題。性能

飢餓

線程調度器會決定哪個處於RUNNABLE狀態的線程會的執行順序。決定通常是基於線程的優先級的;所以,低優先級的線程會得到較少的CPU時間,而高優先級的線程會得到較多的CPU時間。固然,這種調度聽起來較爲合理,可是有的時候也會引發問題。若是老是執行高優先級的線程,那麼低優先級的線程就會沒法得到足夠的時間來執行,處於一種飢餓狀態。所以,建議開發者只在真的十分必要的時候纔去配置線程的優先級。

一個很複雜的線程飢餓的例子就是finalize()方法。Java語言中的這一特性能夠用來進行垃圾回收,可是當開發者查看一下finalizer線程的優先級,就會發現其運行的優先級不是最高的。所以,頗有可能finalize()方法跟其餘方法比起來會執行更久。

另外一個執行時間的問題是,線程以何種順序經過同步代碼塊是沒有定義的。當不少並行線程須要經過封裝的同步代碼塊時,會有的線程等待的時間要比其它線程的時間更久才能進入同步代碼快。理論上,他們可能永遠沒法進入代碼塊。這個問題可使用公平鎖的方案來解決。公平鎖在選擇下個線程的時候會考慮到線程的等待時間。其中一個公平鎖的實現就是java.util.concurrent.locks.ReentrantLock:

若是使用ReentrantLock的以下構造函數:

/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

傳入true,那麼ReentrantLock是一個公平鎖,是會容許線程按掛起順序來依次獲取鎖執行的。這樣能夠削減線程的飢餓,可是,並不能徹底解決飢餓的問題,畢竟線程的調度是由操做系統調度的。因此,ReentrantLock類只考慮等待鎖的線程,調度上是沒法起做用的。舉個例子,儘管使用了公平鎖,可是操做系統會給低優先級的線程很短的執行時間。

相關文章
相關標籤/搜索