Java併發編程——基礎概念

1、內存模型

        我們以一個最簡單的例子開始

int i = 5;
i = i + 1;

        i=i+1這條語句,雖然看起來只有一步,但是從微觀的角度可以將它分解爲以下幾步

        (1)從內存中讀取i=5,並複製到cpu緩存中

        (2)將cpu緩存中i的值+1,現在cpu緩存中i=6,而內存中i=5

        (3)將cpu緩存中的i刷新到內存中,此時i=6

        簡單一句話就是,賦值會先刷到緩存再刷到內存,取值會直接從內存取。

2、導致併發問題的三個原因,以及JVM提供的解決方案

2.1、可見性問題

2.1.1、問題描述

        由上面內存模型的例子,我們可以引出一個問題,如果我們兩個線程執行i=i+1這個操作,期望的最終結果i=7,但是如果兩個線程同時執行了(1)呢?我們知道多核CPU中的緩存是獨立的、不共享的。兩個線程會同時將i=5刷到自己的緩存中,並分別執行i=i+1,再刷回內存,結果是i=6,這就是緩存一致性問題,也就是線程間緩存不可見導致的可見性問題

2.1.2、解決方案------volatile+Happens-Before原則

        volatile是“易變的”的意思,修飾在屬性(常量/成員變量)上表示通知JVM該屬性可能不太穩定,需要立即將值刷入主存,並禁用cpu緩存,禁用了緩存,自然就不存在緩存一致性問題了。

        接下來介紹一下Happens-Before原則:

        Happens-Before的意思是,如果A Happens-Before B,則A的操作結果對B可見,它一共有8個原則:

(1)程序次序規則:

        一個線程內一段代碼的執行結果是有序的。即兩行代碼先後執行,先執行的代碼產生的結果對後執行的代碼可見。

(2)管程鎖定規則:

        對一個鎖的解鎖Happens-Before於後續對這個鎖的加鎖。即上一輪的加鎖解鎖產生的結果對下一輪加鎖解鎖中的操作可見。

(3)volatile變量規則:

        對一個volatile變量的寫操作Happens-Before於後續對這個變量的讀操作。

(4)線程啓動規則:

        主線程啓動的操作Happens-Before於子線程。即主線程在啓動子線程前的操作對子線程可見。

(5)線程終止規則:

        子線程終止前的操作Happens-Before於主線程。即子線程的操作對主線程可見。

(6)線程中斷規則:

         調用interrupt方法Happens-Before於檢測到中斷事件。即對一個線程執行interrupt方法的結果,對被中斷線程檢測到中斷狀態Thread.isInterrupted之前可見

(7)傳遞規則:

        A Happens-Before B,B Happens-Before C,則 A Happens-Before C.

(8)對象終結規則:

        一個對象的初始化操作Happens-Before於銷燬操作。

2.2、有序性問題

2.2.1、問題描述

        雙重鎖實現單例模式:

public class Singleton {
    private Singleton() { }

    private static Singleton instance;

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

        這段代碼中new Singleton()的執行順序本來應該是這樣的:

        JVM開一塊內存空間--->在這塊內存空間上初始化Singleton對象--->把內存空間地址賦值給instance

        但其實編譯器在編譯過程中會對指令進行重排序,執行順序有可能是這樣的:

        JVM開一塊內存空間--->把內存空間地址賦值給instance--->在這塊內存空間上初始化Singleton對象

        雖然兩種執行順序在單線程中結果是正常的,但是後者在多線程中會出現問題:

        假如A線程執行到了new Singleton(),A把空間地址賦值給instance,這時候B線程進來了,instance==null就是false,但是實際上對象還沒初始化,就會造成調用方法空指針

2.2.2、解決方案------volatile+Happens-Before原則

        volatile還有禁用編譯優化的功能

2.3、原子性問題

2.3.1、問題描述

        最經典的取錢問題,假如一個賬戶只有500元,A和B兩個人同時對這個賬戶進行取500元的操作,分別對應一個進程裏兩個不同的線程,而賬戶裏的錢對應共享資源

        A取錢:檢測賬戶裏有500元---->取出500元

        B取錢,檢測賬戶裏有500元---->取出500元

        如果A和B同時執行檢測賬戶餘額的操作,就會同時執行取錢的操作,此時賬戶餘額就會出現-500的情況

        原子性:我們把一個或多個操作在CPU中執行的過程中不被中斷的特性叫原子性,可以認爲原子性問題就是由多個線程(A、B兩個人)同時對共享資源(賬戶)進行操作(取錢)導致的。

        取錢過程分爲檢測餘額和取錢兩步,這兩步是不可分割的,但是由於A和B是兩個不同的線程,就有可能在檢測餘額的時候出現“線程切換”的操作,破壞了取錢這個過程應具有的原子性

2.3.2、解決方案------synchronized關鍵字

       首先synchronized是一種互斥鎖,即同一時刻只能有一個線程訪問臨界資源

        synchronized是同步的意思,用來告訴JVM多個線程必須同步執行該段代碼。

        synchronized可以修飾成員方法和靜態方法,分別加對象鎖和類鎖;還可以修飾方法內的方法塊,需要自己指定加鎖類型,注意對同一個變量的讀寫操作一定要加同一種鎖

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019-08-04 14:55
 */
public class Test {

    //修飾類方法
    private static synchronized void test1(){
        //修飾代碼塊,加類鎖
        synchronized (Test.class){
            
        }
    }

    //修飾成員方法
    private synchronized void test2(){        
        //修飾代碼塊,加對象鎖
        synchronized (this){
            
        } 
    }

}

    2.3.2.1、粗粒度鎖

        粗粒度鎖,即用同一把鎖保護多個不同的臨界資源,優點就是實現容易,缺點就是所有操作串行,性能低:

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 09:44
 */
public class Account {
    
    //保護鎖
    private final Object lock = new Object();
    
    //賬戶餘額
    private Integer balance;

    //賬戶信息
    private String userInfo;

    //存款
    private void addBalance(Integer amt) {
        synchronized (lock) {
            balance += amt;
        }
    }

    //取款
    private void subBalance(Integer amt){
        synchronized (lock){
            if(balance > amt){
                balance -= amt;
            }
        }
    }

    //設置賬戶信息
    private void setUserInfo(String newUserInfo){
        synchronized (lock){
            userInfo = newUserInfo;
        }
    }

    //查看賬戶信息
    private String getUserInfo(){
        synchronized (lock){
            return userInfo;
        }
    }

}

    2.3.2.2、細粒度鎖

        使用不同的鎖保護不同的臨界資源,叫細粒度鎖,優點就是對不同臨界資源的操作並行化,性能高,缺點就是容易產生死鎖:

package com.lcy.thread.part41;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 09:44
 */
public class Account {

    //賬戶鎖
    private final Object balLock = new Object();
    
    //用戶信息鎖
    private final Object infoLock = new Object();

    //賬戶餘額
    private Integer balance;

    //賬戶信息
    private String userInfo;

    //存款
    private void addBalance(Integer amt) {
        synchronized (balLock) {
            balance += amt;
        }
    }

    //取款
    private void subBalance(Integer amt){
        synchronized (balLock){
            if(balance > amt){
                balance -= amt;
            }
        }
    }

    //設置賬戶信息
    private void setUserInfo(String newUserInfo){
        synchronized (infoLock){
            userInfo = newUserInfo;
        }
    }

    //查看賬戶信息
    private String getUserInfo(){
        synchronized (infoLock){
            return userInfo;
        }
    }

}

3、併發編程中應該注意的三個問題

3.1、安全性問題

        就是保證程序運行結果的正確性,上面導致併發問題的三個原因,就都是安全性問題。

        導致安全性問題的原因就是數據競爭,也就是存在多個線程對同一數據進行讀寫的情況,這時你就要注意安全性問題。

3.2、活躍性問題

3.2.1、死鎖

    3.2.1.1、問題描述

        假如現實生活中有這樣一個場景,現在有兩個人都要炒菜,卻只有一口鍋和一把鏟子,A先拿了鍋,B先拿了鏟子,A在等B用完鏟子才能炒菜,而B也在等A用完鍋才能炒菜,A和B會一直等待下去,就發生了死鎖。

        一組相互競爭資源的線程,由於相互等待對方釋放自己執行下一步所需的臨界資源,導致永久阻塞的現象稱爲死鎖。

    3.2.1.2、解決方案------預防死鎖,破壞死鎖條件

    3.2.1.2.1、佔用且等待條件

        線程已經取得了一個共享資源,在對下一個被佔用的共享資源發出申請時被阻塞,此時該線程並不釋放已經取得的共享資源

        破壞佔用且等待條件很簡單,就是一次性申請所有資源。對應到做飯的情景就是,鍋和鏟子都放在廚房,只有一個人能進廚房

        

package com.lcy.thread.part41;

import java.util.ArrayList;
import java.util.List;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/3 17:21
 */
public class Kitchen {

    private List<Object> kitchen = new ArrayList<>();

    private Object pot = new Object();

    private Object spatula = new Object();

    synchronized List<Object> getTool(){

        if(kitchen.contains(pot) || kitchen.contains(spatula)){
            return null;
        }
        kitchen.add(pot);
        kitchen.add(spatula);
        return kitchen;
    }

    synchronized void backTool(Object pot,Object spatula){

        kitchen.remove(pot);
        kitchen.remove(spatula);

    }


}

    3.2.1.2.2、不可搶佔條件

        線程取得的共享資源不能被其他線程釋放

        破壞不可搶佔條件需要用到java併發包裏的相關類,我們到時候再說。對應到做飯的情景就是,一個人拿了鍋再去拿鏟子,如果鏟子拿不到鍋也不要了。

    3.2.1.2.3、循環等待條件

        當前線程會佔用下一個線程的至少一種資源

        破壞循環等待條件可以把資源排序,規定線程從小到大申請資源。對應到做飯的情景就是,規定做飯必須先拿鏟子再拿鍋。

3.2.2、活鎖

    3.2.2.1、問題描述

        現實生活中可能會有這樣的情況,兩個人面對面走,快要撞上的時候,同時互相謙讓走到另一條道上,結果還是過不去,如此反覆。。。

        線程之間並沒有阻塞,但就是無法滿足繼續執行下去的條件,一直循環嘗試->失敗->嘗試->失敗的操作,這種情況稱爲活鎖。

    3.2.2.2、解決方案

        失敗之後設置一個隨機等待時間再嘗試

3.2.3、飢餓

    3.2.3.1、問題描述

        大家都知道,過馬路要等紅綠燈,如果有一天有個路口的紅綠燈突然壞了,一邊一直是綠燈,另一邊一直是紅燈,等紅燈的人和車就要一直等一直等(如果都遵紀守法的話)。

        線程因執行優先級較低或永久等待,無法得到cpu的運行時間塊,而無法繼續執行下去的狀態稱爲飢餓

    3.2.3.2、解決方案

        保證資源分配的公平性,使用基於先來後到原則的公平鎖,在java併發包裏有相關類,我們後面會講到

4、線程的生命週期

4.1、線程的生命週期

(1)初始化狀態(NEW)

        new Thread();JVM僅僅爲其分配內存

(2)可運行狀態(RUNNABLE)

        Thread.start();表示線程已經可以運行了,但是什麼時候運行取決於JVM線程調度器的調度

(3)運行狀態(RUNNING)

        線程獲得CPU時間塊,執行方法體

(4)阻塞狀態(BLOCKED)

        線程在等待獲取共享資源的鎖的時候

(5)無時間限制等待狀態(WAITING)

        在線程獲取鎖的時候,主動調用wait()等方法,會進入等待被喚醒的狀態,並釋放對應的鎖

(6)有時間限制的等待狀態(TIMED_WAITING)

        在線程獲取鎖的時候,主動調用wait(long millis)等方法,會進入等待被喚醒的狀態,並釋放對應的鎖,如果一定時間內沒有被喚醒,則自己主動甦醒。

(7)終止狀態(TERMINATED)

        線程執行完畢或異常終止

4.2、wait()、notify()、notifyAll()的使用方法

    我們以最簡單的生產者消費者模型爲背景來介紹,下面的例子主要就是兩個線程對一個數的增減

生產者Producer,搶到鎖後,數小於等於0就執行+5然後喚醒等待線程,sychronized結束纔會釋放鎖;大於0就不執行:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:29
 */
public class Producer implements Runnable {

    private Test test;

    public Producer(Test test) {

        this.test = test;

    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            synchronized (test) {
                if (test.i <= 0) {
                    test.i += 5;
                    System.out.println(threadName +"生產...剩餘" + test.i);

                    System.out.println(threadName +"去喚醒...");
                    test.notify();
                }
            }
        }
    }
}

消費者Consumer,搶到鎖後,小於等於0就釋放鎖等待被喚醒;大於0就執行-1:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:29
 */
public class Consumer implements Runnable {

    private Test test;

    public Consumer(Test test) {
        this.test = test;
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            synchronized (test) {
                if (test.i <= 0) {
                    System.out.println(threadName +"等待...");
                    try {
                        test.wait();
                        System.out.println(threadName +"醒了...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                test.i--;
                System.out.println(threadName +"消費...剩餘" + test.i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

測試類:

package com.lcy.thread.part08;

/**
 * 功能描述:
 *
 * @author liuchaoyong
 * @version 1.0
 * @date 2019/9/10 09:35
 */
public class Test {

    public int i = 5;

    public static void main(String[] args) {

        Test test = new Test();

        Thread thread = new Thread(new Consumer(test));
        Thread thread1 = new Thread(new Producer(test));
        thread.start();
        thread1.start();

    }

}

        其實要注意的只有一點,就是調用某個對象的wait()、notifyAll()的線程,一定要獲取到該對象的鎖纔可以,否則會報java.lang.IllegalMonitorStateException異常

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