線程安全危險

線程安全的問題是微妙且出乎意料的,因為在沒有進行充分同步的情況下,多線程中的各個操作的順序是不可預測的,也就是容易造成線程安全問題。

簡單舉個例子:

下面的程序在單線程中運行是沒有問題的。

import net.jcip.annotations.NotThreadSafe;

@NotThreadSafe
class UnSafeSequence {
    private int value;
    
    public int getNext() {
        return value++;
    }
}

但是在多線程環境中,它的value值計算就會出現問題。

首先,它共享了value這個變量,其次來說value++是一個組合的操作(注意,它並非是原子性)

它分成3個獨立的操作:

  1. 讀取這個值
  2. 使之加1
  3. 再寫入新值

以下圖示很好表示線程操作順序:

  • 當線程A讀取到value值是9的時候,同時線程B也執行此方法並且也是讀取value值是9
  • 兩個線程的操作都對value加上1
  • 將計算結果寫入value上,但卻是寫入到value的結果是10
  • 也就是說:兩個線程的互相操作,正確結果應該返回11,而它返回了9,這就是線程不安全

圖表中描述了不同線程之間的交替操作。在這些圖表中,時間由左至右發展,每一行表現一個不同線程的活動。 這些交替的圖表通常用來描述最壞的情況,目的是表現特定順序下產生錯誤僭越帶來的危險。

事實上,最壞的情況可能比圖中表現的更糟糕,因為存在重排序的可能。

UnsafeSequence闡明了一種常見的並發危險:競爭條件(race condition)。

因為線程共享相同的內存地址空間,且並發地運行,它們可能訪問或修改其他線程正在使用的變量。 好處是當數據共享相對於其他的線程間通訊機制都更加簡單,但是這其中也存在著巨大的風險:當數據意外改變時,線程可能會出現混亂。

為了使多線程程序的行為可預見性,訪問共享資源必須經過合理的協調,才不會造成線程相互干擾。

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

@ThreadSafe
class Sequence {
    @GuardedBy("this") private int value;

    public synchronized int getNext() {
        return value++;
    }
}

活躍度危險

在並發開發代碼時,對線程安全的關注是至關重要的:安全不能妥協。安全的重要性不僅僅存在多線程程序中,單線程的程序也必須注意保護安全性和正確性。 然而,線程的引入和使用造成另一形式的活躍度危險(liveness failure),這不會出現在單線程的程序中。

當一個活動進入某種永遠無法再繼續執行的狀態時,活躍度失敗就發生了。

就比如Servlet,一個Servlet對象可以處理多個請求,所以可以說Servlet是支持多線程的程序。

一樣舉個例子:

import javax.servlet.*;
import java.io.IOException;

class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        count++;
    }
}

依照上面說過的,上面這個類是線程不安全的。最簡單的方式:在service方法上加上內置鎖(synchronized),就可以實現線程安全。

import javax.servlet.*;
import java.io.IOException;

class UnsafeCountingServlet extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    @Override
    public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        count++;
    }
}

雖然實現了線程安全了,但是這會帶來很嚴重的性能問題:

  • 每個請求都得等待上一個請求的service方法處理了以後才可以執行操作。

也就是我們只是完成一個小小功能,使用多線程目的是想要提高效率,卻沒有把握得當,帶來嚴重的性能問題。

更嚴重的話,還會帶來各種活躍度危險,包括:

  • 死鎖(deadlock)
  • 飢餓(starvation)
  • 活鎖(livelock)

性能危險

在設計良好的應用程序中使用線程,能夠獲得純粹的性能收益,但是線程仍然會給運行時帶來一定成程度的開銷。

上下文切換(context switches)————當調度程序臨時掛起當前運行的線程時,另一個線程開始運行————這在多個 線程組成的應用程序中是很頻繁的,並且帶來巨大的系統開銷:保存和恢復線程執行的上下文,離開執行現場,並且CPU 的時間會花費在對線程的調度而不是在運行上。

性能問題涉及很多方面,包括服務時間、響應性、吞吐量、資源消耗或者可伸縮性的不良表現。