Java併發編程的藝術(一)——Java併發的基礎知識

上學期學習了計算機組成,跟着老師用C++模擬了一下CPU的流水線以及緩存之後發現正因爲工程師對於性能與效率的極致追求才有了現在先進的各式計算機設備。最近看了java虛擬機的書後也發現了虛擬機優化這塊用到了很多並行架構,比如G1,CMS垃圾回收器就是用到了多線程。在當今多核架構盛行的情況下,如何高效的利用多個CPU協同工作,成爲提高程序運行速率的核心技術點之一。

多核心協同工作在編程界就是所謂的併發編程技術了。

關於併發編程的基礎知識:

1. 上下文切換

任務從保存到再次加載的過程就是一次上下文切換

計算機的CPU其實並不是始終執行一個程序的,實際是通過給每個程序分配CPU時間片,當前程序的時間片一旦執行完,CPU會保存該程序的狀態,轉而去執行其他程序的時間片了,因爲時間片比較短,一般只有幾十毫秒,所以在人看來就像多個程序同時運行一樣。具體的運轉過程如下圖:
在這裏插入圖片描述
CPU在不同的程序中進行切換是需要消耗資源的(耗電,耗存儲空間之類的)那麼如果想要提升性能,很容易想到的方法之一爲減少上下文切換的次數從而減少資源開銷。

如何減少上下文切換:
(1). 無鎖併發編程,比如將數據ID按照Hash取模分段,不同線程處理不同段的數據
(2). CAS算法,compare and swap
(3). 使用最少線程,避免創建不需要的線程

2. 死鎖

爲了控制資源分配,人們發明了鎖,就像衛生間門鎖一樣,當一個人在使用時必須把門鎖起來,這樣後面來的人需要等待,而不是直接闖入進去。
但是一旦出現兩個已經上鎖了的衛生間A,B。A衛生間裏的人拿着B衛生間的門鎖鑰匙,而B衛生間裏的人手上拿着A的門鑰匙,如果是這種情況,A,B將永遠打不開,外面排隊的人只能無奈的等着,這就是所謂的死鎖。

下面寫一個簡易的死鎖:

public class DeadLock{

    private final String A = "A";
    private final String B = "B";

    public void deadLock(){
    
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
            	//線程1中獲取A的資源後再嘗試獲取B的資源
                synchronized (A){
                    try{
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (B){
                        System.out.println("thread A");
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
            	//線程2中先獲取B資源後再嘗試獲取A資源
                synchronized (B){
                    try{
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (A){
                        System.out.println("Thread B");
                    }
                }
            }
        });
        
        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        dl.deadLock();
    }
}

運行程序會發現程序不中斷但不輸出任何結果。
通過jps以及jstack命令可以發現該程序出現了一個死鎖:
在這裏插入圖片描述在這裏插入圖片描述
如何避免死鎖:
(1).避免一個線程同時獲取多個鎖
(2).避免一個線程在鎖內同時佔用多個資源,儘量保證一個鎖佔用一個資源
(3).嘗試使用定時鎖
(4).對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會解鎖失敗

CAS: compare and swap
CAS算法是一個能保證共享變量原子操作而不需要加鎖的算法
具體實現邏輯:在操作期間先比較共享變量的舊值有沒有變化,如果沒有變化則替換成新值,若變化了則不進行交換。

CAS的問題
(1). ABA問題 A變化爲B之後再變成A 對於CAS來說值並沒有變化,但實際上發生過變化,CAS卻檢測不到
解決方案:增加版本號
(2).循環時間長開銷大,如果自旋CAS長時間不成功,會給CPU帶來非常大的執行開銷
(3).只能保證一個共享變量的原子操作
若想對多個共享變量進行操作則需要使用鎖

3. volatile

volatile: 一個字段被聲明爲volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。

volatile修飾的共享變量執行寫操作時彙編語言中會出現Lock指令,Lock指令有以下的作用:

  1. lock前綴指令會引起處理器緩存回寫到內存
  2. 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效

4. synchronized

JVM基於進入和退出Monitor對象來實現方法的同步和代碼塊的同步,這就是synchronized關鍵字的實現方式。鎖的信息是存放在對象頭內的。

鎖的四種狀態:
無鎖狀態,偏向鎖,輕量級鎖,重量級鎖
TIPS: 鎖只能升級不能降級,目的是爲了提高獲得鎖和釋放鎖的效率

偏向鎖:
大多數情況下,鎖不存在多線程競爭,而且總是有同一個線程多次獲得,爲了讓線程獲得鎖的代價更低引入偏向鎖

自旋鎖:指當一個線程獲取鎖的時候,如果鎖已經被其他線程獲取,那該線程將循環等待,然後不斷判斷鎖是否能被成功獲取,直到獲取到鎖纔會退出循環

5. 原子操作的實現

  1. 總線鎖:Lock信號,當出現該信號,其他處理器的請求會被阻塞住,那麼該處理器可以獨佔共享內存
    缺點:總線鎖會把CPU與內存之間的通信鎖住,這使得鎖定期間,其他處理器無法操作其他內存地址數據,會影響性能
  2. 緩存鎖:頻繁使用的內存會存在處理器的L1,L2,L3高速緩存裏,所以原子操作可以直接在處理器內部緩存內進行,不需要聲明總線鎖
    處理器不使用緩存鎖的情況:
    (1)操作的數據不能被緩存在處理器內部,或者操作的數據跨了多個緩存行,處理器會調用總線鎖定
    (2)處理器不支持緩存鎖定

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章