1. 程式人生 > >Java併發程式設計系列-(7) Java執行緒安全

Java併發程式設計系列-(7) Java執行緒安全

7. 執行緒安全

7.1 執行緒安全的定義

如果多執行緒下使用這個類,不過多執行緒如何使用和排程這個類,這個類總是表示出正確的行為,這個類就是執行緒安全的。

類的執行緒安全表現為:

  • 操作的原子性
  • 記憶體的可見性

不做正確的同步,在多個執行緒之間共享狀態的時候,就會出現執行緒不安全。

7.2 如何保證執行緒安全

棧封閉

所有的變數都是在方法內部宣告的,這些變數都處於棧封閉狀態。

比如下面的例子,a和b都是在方法內部定義的,無法被外部執行緒所訪問,當方法結束後,棧記憶體被回收,所以是執行緒安全的。

void fun(){
    int a = 1;
    int b= 2;
    // do something
}

無狀態

沒有任何成員變數的類,就叫無狀態的類,這種類不存在共享的資源,顯然是安全的。

public class StatelessClass {
    
    public int service(int a,int b) {
        return a*b;
    }
}

不可變的類

讓狀態不可變,兩種方式:

  1. 加final關鍵字。對於一個類,所有的成員變數應該是私有的,並且可能的情況下,所有的成員變數應該加上final關鍵字。需要注意如果成員變數又是一個物件時,這個物件所對應的類也要是不可變,才能保證整個類是不可變的。
  2. 根本就不提供任何可供修改成員變數的地方,同時成員變數也不作為方法的返回值。

下面例子中的,成員變數都是final並且也沒有提供給外部修改變數的地方,因此是執行緒安全的。

public class ImmutableFinal {
    
    private final int a;
    private final int b;
    
    public ImmutableFinal(int a, int b) {
        super();
        this.a = a;
        this.b = b;
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }
}

下面的例子中,雖然User成員變數是final的,無法修改引用。但是外部依然可以通過getUser獲取到User的引用之後,修改User物件。

public class ImmutableFinalRef {
    
    private final int a;
    private final int b;
    private final User user;//這裡就不能保證執行緒安全了
    
    public ImmutableFinalRef(int a, int b) {
        super();
        this.a = a;
        this.b = b;
        this.user = new User();
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }
    
    public User getUser() {
        return user;
    }

    public static class User{
        private int age;

        public User(int age) {
            super();
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
        
    }
    
    public static void main(String[] args) {
        ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
        User u = ref.getUser();
        //u.setAge(35);
    }
}

volatile

volitile在ConcurrentHashMap等併發容器中都有使用,用於保證變數的可見性。最適合一個執行緒寫,多個執行緒讀的情景。

加鎖和CAS

加鎖可以顯示地控制執行緒對類的訪問,使用正確可以保證執行緒安全。

CAS操作通過不斷的迴圈對比,試圖對目標物件進行修改,也能保證執行緒安全。廣泛用於JDK併發容器的實現中。

安全的釋出

類中持有的成員變數,特別是物件的引用,如果這個成員物件不是執行緒安全的,通過get等方法釋出出去,會造成這個成員物件本身持有的資料在多執行緒下不正確的修改,從而造成整個類執行緒不安全的問題。

ThreadLocal

這個類能使執行緒中的某個值與儲存值的物件關聯起來。ThreadLocal提供了get與set等訪問介面與方法,這些方法為使用該變數的每個執行緒都存有一份獨立的副本,因此get總是返回由當前執行執行緒在呼叫set時設定的最新值。

當某個執行緒初次呼叫ThreadLocal.get方法時,就會呼叫initialValue來獲取初始值。從概念上講,你可以將ThreadLocal

7.3 死鎖

定義

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

死鎖的根本成因:獲取鎖的順序不一致導致。

可以利用下面的示意圖幫助理解:

死鎖範例

下面的程式中,兩個執行緒分別獲取到了first和second,然後相互等待,產生了死鎖。

public class DeadLockSample extends Thread {
    private String first;
    private String second;
    public DeadLockSample(String name, String first, String second) {
        super(name);
        this.first = first;
        this.second = second;
    }
    public void run() {
        synchronized (first) {
            System.out.println(this.getName() + " obtained: " + first);
            try {
                Thread.sleep(1000L);
                synchronized(second) {
                    System.out.println(this.getName() + " obtained: " + second);
                }
            } catch (InterruptedException e) {
                // Do nothing
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";
        DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
        DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

定位和解決死鎖

Debug時可以使用 jps 或者系統的 ps 命令、工作管理員等工具,確定程序 ID。其次,呼叫 jstack 獲取執行緒棧,jstack your_pid. jstack 本身也會把類似的簡單死鎖抽取出來,直接打印出來。

如果是開發自己的管理工具,需要用更加程式化的方式掃描服務程序、定位死鎖,可以考慮使用 Java 提供的標準管理 API,ThreadMXBean,其直接就提供 findDeadlockedThreads() 方法用於定位,上面的例子中用到了這個方法。

怎麼預防死鎖?

  1. 如果可能的話,儘量避免使用多個鎖,並且只有需要時才持有鎖。

  2. 如果必須使用多個鎖,儘量設計好鎖的獲取順序。如果對於兩個執行緒的情況,可以參考如下的實現:

在實現轉賬的類時,為了防止由於相互轉賬導致的死鎖,下面的實現中,通過對比賬戶的hash值來確定獲取鎖的順序。當兩者的hash值相等時,雖然這種情況非常少見,使用了單獨的鎖,來控制兩個執行緒的訪問順序。

注意System.identityHashCode()是JDK自帶的hash實現,在絕大部分情況下,保證了物件hash值的唯一性。

public class SafeOperate implements ITransfer {
    private static Object tieLock = new Object();//加時賽鎖

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //先鎖hash小的那個
        if(fromHash<toHash) {
            synchronized (from){
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                            +" get"+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }           
        }else if(toHash<fromHash) {
            synchronized (to){
                Thread.sleep(100);
                synchronized (from){
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }           
        }else {//解決hash衝突的方法
            synchronized (tieLock) {
                synchronized (from) {
                    synchronized (to) {
                        from.flyMoney(amount);
                        to.addMoney(amount);                        
                    }
                }
            }
        }
        
    }
}
  1. 使用帶超時的方法,為程式帶來更多可控性。

類似 Object.wait(…) 或者 CountDownLatch.await(…),都支援所謂的 timed_wait,我們完全可以就不假定該鎖一定會獲得,指定超時時間,併為無法得到鎖時準備退出邏輯。

  1. 使用Lock實現(推薦)

併發 Lock 實現,如 ReentrantLock 還支援非阻塞式的獲取鎖操作 tryLock(),這是一個插隊行為(barging),並不在乎等待的公平性,如果執行時物件恰好沒有被獨佔,則直接獲取鎖。

標準的使用流程如下:

while(true) {
   if(A.getLock().tryLock()) {
    try {
        if(B.getLock().tryLock()) {
            try {
              //兩把鎖都拿到了,開始執行業務程式碼
                   break;
            }finally {
              B.getLock().unlock();
            }
       }
    }finally {
        A.getLock().unlock();
    }
  }
  // 非常重要,sleep隨機的時間,以防兩個執行緒謙讓,產生長時間的等待,也就是活鎖
  SleepTools.ms(r.nextInt(10));
}

7.4 活鎖/執行緒飢餓/無鎖

活鎖

活鎖恰恰與死鎖相反,死鎖是大家都拿不到資源都佔用著對方的資源,而活鎖是拿到資源卻又相互釋放不執行。當多執行緒中出現了相互謙讓,都主動將資源釋放給別的執行緒使用,這樣這個資源在多個執行緒之間跳動而又得不到執行,這就是活鎖。

在上面解決死鎖的第四個方案中,為了避免活鎖,採用了隨機休眠的機制。

執行緒飢餓

執行緒執行中有執行緒優先順序,優先順序高的執行緒能夠插隊並優先執行,這樣如果優先順序高的執行緒一直搶佔優先順序低執行緒的資源,導致低優先順序執行緒無法得到執行,這就是飢餓。當然還有一種飢餓的情況,一個執行緒一直佔著一個資源不放而導致其他執行緒得不到執行,與死鎖不同的是飢餓在以後一段時間內還是能夠得到執行的,如那個佔用資源的執行緒結束了並釋放了資源。

無鎖

對於併發控制而言,鎖是一種悲觀的策略,它總是假設每一次的臨界區操作會產生衝突,由此,如果有多個執行緒同時需要訪問臨界區資源,則寧可犧牲資源讓執行緒進行等待。

無鎖是一種樂觀的策略,它假設對資源的訪問是沒有衝突的。既然沒有衝突,自然不需要等待,所以所有的執行緒都可以在不停頓地狀態下持續執行。當遇到衝突,則使用CAS來檢測執行緒衝突,如果發現衝突,則重試直到沒有衝突為止。

CAS演算法的過程是,它包含三個引數CAS(V,E,N),V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才將V的值設定為N,如果V值和E值不同,說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。

7.5 影響效能的因素

  • 上下文切換:一般花費5000-10000個時鐘週期,幾微秒
  • 記憶體同步:加鎖等操作,增加額外的指令執行時間
  • 阻塞:掛起執行緒,包括額外的上下文切換

7.6 鎖效能優化

減少鎖的持有時間

減少鎖的持有時間有助於降低鎖衝突的可能性,進而提升系統的併發能力。

減小鎖粒度

這種技術的典型使用場景就是ConcurrentHashMap。

對於HashMap來說,最重要的兩個方法就是get() 和put(),一種最自然的想法就是對整個HashMap加鎖,必然可以得到一個執行緒安全的物件.但是這樣做,我們就認為加鎖粒度太大.對於ConcurrentHashMap,它內部進一步細分了若干個小的hashMap,稱之為段(SEGMENT).預設的情況下,一個ConcurrentHashMap被進一步細分為16個段

如果需要在ConcurrentHashMap中增加一個新的表項,並不是整個HashMap加鎖,而是首先根據hashcode得到該表項應該被存放到哪個段中,然後對該段加鎖,並完成put()操作.在多執行緒環境中,如果多個執行緒同時進行put()操作,只要被加入的表項不存放在同一個段中,則執行緒間便可以做到真正的並行。

讀寫分離鎖來替換獨佔鎖

在讀多寫少的場合,使用讀寫鎖可以有效提升系統的併發能力

鎖分離

如果將讀寫鎖的思想進一步的延伸,就是鎖分離.讀寫鎖根據讀寫鎖操作功能上的不同,進行了有效的鎖分離.使用類似的思想,也可以對獨佔鎖進行分離.

以LinkedBlockingQueue為例,take函式和put函式分別實現了衝佇列取和往佇列加資料,雖然兩個方法都對佇列進項了修改,但是LinkedBlockingQueue是基於連結串列的所以一個操作的是頭,一個是佇列尾端,從理論情況下將並不衝突

如果使用獨佔鎖則take和put就不能完成真正的併發,所以jdk並沒有才用這種方式取而代之的是兩把不同的鎖分離了put和take的操作

鎖粗化

凡事都有一個度,如果對同一個鎖不停地進行請求,同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於效能的優化。

為此,虛擬機器在遇到一連串連續地對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這個操作叫做鎖的粗化.

7.7 實現執行緒安全的單例模式

懶漢式

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

執行緒安全,並且解決了多例項的問題,但是它並不高效。因為在任何時候只能有一個執行緒呼叫 getInstance() 方法。

雙重檢驗鎖

public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton() {
    }
    public static Singleton getSingleton() {
        if (singleton == null) { // 儘量避免重複進入同步塊
            synchronized (Singleton.class) { // 同步.class,意味著對同步類方法呼叫
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  • volatile 能夠提供可見性,以及保證 getInstance 返回的是初始化完全的物件。
  • 在同步之前進行 null 檢查,以儘量避免進入相對昂貴的同步塊。
  • 直接在 class 級別進行同步,保證執行緒安全的類方法呼叫。

在這段程式碼中,爭論較多的是 volatile 修飾靜態變數,當 Singleton 類本身有多個成員變數時,需要保證初始化過程完成後,才能被 get 到。 在現代 Java 中,記憶體排序模型(JMM)已經非常完善,通過 volatile 的 write 或者 read,能保證所謂的 happen-before,也就是避免常被提到的指令重排。換句話說,構造物件的 store 指令能夠被保證一定在 volatile read 之前。

餓漢式

這種方法非常簡單,因為單例的例項被宣告成 static 和 final 變量了,在第一次載入類到記憶體中時就會初始化,所以建立例項本身是執行緒安全的。

public class Singleton{
    //類載入時就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

靜態內部類(推薦)

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

靜態內部類是在被呼叫時才會被載入,因此它是懶漢式的。


本文由『後端精進之路』原創,首發於部落格 http://teckee.github.io/ , 轉載請註明出處

搜尋『後端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。

相關推薦

Java併發程式設計系列-(7) Java執行安全

7. 執行緒安全 7.1 執行緒安全的定義 如果多執行緒下使用這個類,不過多執行緒如何使用和排程這個類,這個類總是表示出正確的行為,這個類就是執行緒安全的。 類的執行緒安全表現為: 操作的原子性 記憶體的可見性 不做正確的同步,在多個執行緒之間共享狀態的時候,就會出現執行緒不安全。 7.2 如何保證執

原創】Java併發程式設計系列2:執行概念與基礎操作

## 【原創】Java併發程式設計系列2:執行緒概念與基礎操作 > 偉大的理想只有經過忘我的鬥爭和犧牲才能勝利實現。 本篇為【Dali王的技術部落格】Java併發程式設計系列第二篇,講講有關執行緒的那些事兒。主要內容是如下這些: - 執行緒概念 - 執行緒基礎操作 ### 執行緒概念 程序代表了執

Java併發程式設計:什麼是執行安全,以及併發必須知道的幾個概念

廢話 眾所周知,在Java的知識體系中,併發程式設計是非常重要的一環,也是面試的必問題,一個好的Java程式設計師是必須對併發程式設計這塊有所瞭解的。為了追求成為一個好的Java程式設計師,我決定從今天開始死磕Java的併發程式設計,儘量彌補自己在這方面的知識缺陷。 併發必須知道的概念

java併發程式設計(一) 執行安全(1)

最近想了解併發程式設計,二執行緒安全是它的基礎。所以看了下java相關的執行緒安全知識。 執行緒安全的核心是程式碼正確性(一般是輸出的結果); 首先無狀態的物件是執行緒安全的;因為一個無狀態的物件即不包含其他域;也沒有對其他域的引用; (1)原子性    原子性:即程式碼不

Java併發程式設計規則:構建執行安全的共享物件

構建執行緒安全的共享物件,使其在多執行緒環境下能夠提供安全的訪問。編寫正確的併發程式關鍵在於控制共享、可變的狀態進行訪問管理。synchornized關鍵字既可以阻塞程式,也可以維護操作的原子性,它是一個執行緒安全與非執行緒安全的臨界區標識,通過它我們可以控制物件的記憶體可

Java併發程式設計系列-(6) Java執行

6. 執行緒池 6.1 基本概念 在web開發中,伺服器需要接受並處理請求,所以會為一個請求來分配一個執行緒來進行處理。如果每次請求都新建立一個執行緒的話實現起來非常簡便,但是存在一個問題:如果併發的請求數量非常多,但每個執行緒執行的時間很短,這樣就會頻繁的建立和銷燬執行緒,如此一來會大大降低系統的效率。

Java併發程式設計:4種執行池和緩衝佇列BlockingQueue

一. 執行緒池簡介 1. 執行緒池的概念:           執行緒池就是首先建立一些執行緒,它們的集合稱為執行緒池。使用執行緒池可以很好地提高效能,執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個任務傳給執行緒池,執行緒池就會啟動一

java併發程式設計實戰》之 執行安全性

1.執行緒安全性 當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。 無狀態物件一定是執行緒安全的,何為無狀態,就是類中不包含任何域,也不包含各種其

[讀書筆記][Java併發程式設計實戰]第二章 執行安全性

                                          第二章 執行緒安全性 1-什麼是執行緒安全的類? 當多個執行緒訪問某一個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個

(十)java併發程式設計--建立和啟動執行java.lang.Thread 、java.lang.Runnable)

執行緒建立的幾種方式. 建立和啟動一個執行緒 建立一個執行緒. Thread thread = new Thread(); 啟動java執行緒. thread.start(); 這兩個例子並沒有執行執行緒執行體,執行緒將會啟動後然後

學了Java併發程式設計藝術及多執行核心程式設計技術,以及最開始學的程式設計思想那本書,今天做些總結

併發Map分析位碼shift預設值是28,對hash值右移28位,取高四位,獲得segments位置,掩碼mask預設值16-1,作一個與值,不知道有何用處,兩個都是不可修改,初始值和併發度有關,一旦確立下來決定了segments陣列大小,包括segments陣列物件不可修改

Java併發程式設計中的多執行是怎麼實現的?

眾所周知,在Java的知識體系中,併發程式設計是非常重要的一環,也是面試中必問的題,一個好的Java程式設計師是必須對併發程式設計這塊有所瞭解的。 併發必須知道的概念在深入學習併發程式設計之前,我們需要了解幾個基本的概念。同步和非同步同步和非同步用請求返回呼叫的方式來理解相對簡單。 同步:

Java併發程式設計札記-(六)JUC執行池-01概述

前面的例子中總是需要執行緒時就建立,不需要就銷燬它。但頻繁建立和銷燬執行緒是很耗資源的,在併發量較高的情況下頻繁建立和銷燬執行緒會降低系統的效率。執行緒池可以通過重複利用已建立的執行緒降低執行緒建立和銷

Java併發程式設計:如何建立執行、程序?

在前面一篇文章中已經講述了在程序和執行緒的由來,今天就來講一下在Java中如何建立執行緒,讓執行緒去執行一個子任務。下面先講述一下Java中的應用程式和程序相關的概念知識,然後再闡述如何建立執行緒以及如何建立程序。下面是本文的目錄大綱:   一.Java中關於應

Java併發程式設計中四種執行池及自定義執行使用教程

引言 通過前面的文章,我們學習了Executor框架中的核心類ThreadPoolExecutor ,對於執行緒池的核心排程機制有了一定的瞭解,並且成功使用ThreadPoolExecutor 建立了執行緒池。 而在Java中,除了ThreadPoolExecutor ,Executor框

Java併發程式設計的藝術(四)——執行的狀態

執行緒的狀態 初始態:NEW 建立一個Thread物件,但還未呼叫start()啟動執行緒時,執行緒處於初始態。 執行態:RUNNABLE 在Java中,執行態包括就緒態 和 執行態。 就緒態 該狀態下的執行緒已經獲得執行所需的所有資源

java併發程式設計-再談daemon執行

守護執行緒:顧名思義是用來做“守護神”的工作,一直守護著使用者執行緒直到使用者執行緒工作完畢(比如:main執行緒結束)。對於守護執行緒我們需要注意兩點:通過呼叫setDaemon(true)方法將執行

Java併發程式設計的藝術(十)——執行池(1)

執行緒池的作用 減少資源的開銷 減少了每次建立執行緒、銷燬執行緒的開銷。 提高響應速度 每次請求到來時,由於執行緒的建立已經完成,故可以直接執行任務,因此提高了響應速度。 提高執行緒的可管理性 執行緒是一種稀缺資源,若不加以限制,不僅會佔用大量資源

Java併發程式設計---Executors多工執行框架

 一.概念        為了更好地控制多執行緒,JDK提供了一套執行緒框架Executor,幫助開發人員有效地進行執行緒控制.他們都在java.util.concurrent包中,是JDK併發包的核

java併發程式設計實戰》:執行同步輔助類之訊號量(semaphore)

1.訊號量的概念: 訊號量是一種計數器,用來保護一個或者多個共享資源的訪問,它是併發程式設計的一種基礎工具,大多數程式語言都提供了這個機制。 2、訊號量控制執行緒訪問流程: 如果執行緒要訪問一個共享資源,它必須先獲得訊號量。如果訊號量的內部計數器大於0,訊號量將減1,然後