“多執行緒”重點概念整理

weixin_33850890發表於2018-12-09

一、volatile關鍵字記憶體可見性

當程式執行時,JVM會為每一個執行任務的執行緒分配一個獨立的快取空間,用於提高效率。這個執行緒會先從主記憶體中拿到變數到自己的快取中,然後將改變後的值提交到主記憶體,這是兩個操作,如果在第一個操作後,第二個執行緒闖入,這個時候第二個執行緒從主記憶體中讀取的變數就是未改變之前的變數,那麼兩個執行緒最後拿到的值便是不一樣的,產生衝突。

產生此種問題是因為這個變數並不是都在主記憶體中操作的,要解決這個問題,便是在“共享變數” 定義的時候在之前新增一個 volatile 關鍵字。

圖示:


14358449-e238a1df1f1d0fc9.PNG
捕獲.PNG

二、原子變數-CAS演算法

volatile關鍵字保證記憶體可見性,也就是說可以保證變數都在主記憶體中進行,但是不能保證原子性。

(一)那麼什麼是原子性問題?

舉例:i++操作
i++操作實際上是“讀-改-寫”三個操作:

int temp = i;
i=i+1;
temp = i;

當我們執行程式:

int i = 10;
i=i++;  
System.out.println(i); //這個時候輸出列印的應該是 10 

原因在於:此時 i++操作返回的是底層“讀”的時候的 i ,而不是“寫” 完後的 i
: 這就是原子性問題

(二)原子變數(解決原子性問題)

為了解決這個問題,jdk 1.5之後,在java.util.concurrent.atomic包下提供了常用的資料型別的原子變數。

它的底層實現是:
1、首先用volatile保證記憶體的可見性
2、然後用CAS演算法保證資料原子性,CAS演算法是硬體對於併發操作共享資料的支援,CAS包括三個操作:
①記憶體值(從主存中讀取值)、
②預估值(讀取舊值)、
③更新值(如果滿足條件就替換主記憶體中的值)
只有當記憶體值==預估值的時候,記憶體值才能夠等於預估值,否則將不作任何操作。
demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 註釋:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException {
        AtomicDemo atomicDemo = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}
class AtomicDemo implements Runnable {

    //用volatile保證記憶體可見性:無法保證原子性
    //private volatile int number = 0;

    //使用原子變數解決原子性問題
    private AtomicInteger number = new AtomicInteger();

    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getAdd());
    }

    public int getAdd(){
        //使用原子變數提供的相關API進行對原子變數資料的操作,API文件裡面有詳細介紹
        //這裡使用“從主記憶體中獲取和自增”一起的方法返回number自增的值。
        return number.getAndIncrement();
        //return number++;
    }
}

三、ConcurrentHashMap鎖分段機制

Java 5.0 在 java.util.concurrent 包中提供了多種併發容器類來改進同步容器的效能。
ConcurrentHashMap 同步容器類是Java 5 增加的一個執行緒安全的雜湊表。對與多執行緒的操作,介於 HashMap 與 Hashtable 之間。內部採用“鎖分段”機制替代 Hashtable 的獨佔鎖。進而提高效能。
此包還提供了設計用於多執行緒上下文中的 Collection 實現:
ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當期望許多執行緒訪問一個給定 collection 時,ConcurrentHashMap 通常優於同步的 HashMap,ConcurrentSkipListMap 通常優於同步的 TreeMap。當期望的讀數和遍歷遠遠大於列表的更新數時,CopyOnWriteArrayList 優於同步的 ArrayList。

JDK1.8以後ConcurrentHashMap由鎖的分段機制變為CAS。
CopyOnWriteArrayList "寫入並複製" 是個複合操作,當每次寫入時,都會複製。新增操作比較多時效率較低。併發迭代操作多時,可以提高效率。

這裡推薦一篇部落格詳細介紹了ConcurrentHashMap:
https://blog.csdn.net/yansong_8686/article/details/50664351

四、CountDownLacth 閉鎖

閉鎖是一個同步工具類,它是用來保證一組執行緒全部執行完成才能進行下一步操作的工具。就比如一組多執行緒,我們想要獲取他們全部執行的時間,尋常操作時無法完成的,因為獲取時間存在於主執行緒,隨時可能拿到cpu的使用權利,所以這個工具類,就可以實現要全部執行緒執行完,才執行下一步。

閉鎖狀態包含一個計數器,該計數器被初始化為一個正數,表示需要等待的事件數量。countDown方法遞減計數器,表示已經有一個事件已經發生了。而await方法等待計數器達到0,這表示所有需要等待的事件都已經發生。如果計數器的值非0,那麼await會一直阻塞直到計數器為0,或者等待中的執行緒中斷或者超時。

當然採用join方式也可以完成,但是效率是遠遠不及的。

demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 註釋:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException {
        //閉鎖工具類,設需要等待事件數完成的數量為 5
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        DownLatchDemo downLatchDemo = new DownLatchDemo(countDownLatch);

        //開始時間
        long start = System.currentTimeMillis();

        for (int i = 0; i < 5; i++) {
            new Thread(downLatchDemo).start();
        }
        //等待上面的5個執行緒執行完成,也就是事件數為 0 後才執行await之後main執行緒的程式碼
        countDownLatch.await();

        //結束時間
        long end = System.currentTimeMillis();
        System.out.println("執行時間為:"+(end - start));


    }
}
class DownLatchDemo implements Runnable {
    private CountDownLatch latch;
    DownLatchDemo(CountDownLatch latch){
        this.latch = latch;
    }
    public void run() {
        synchronized (this){
            try{
                //執行一個耗時的操作
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            }finally {
                //每一次執行緒的呼叫,使閉鎖操作事件數減 1
                latch.countDown();
            }
        }
    }
}

五、建立實現執行緒的方式

建立實現執行緒的方式有四種:
1、繼承Thread介面
2、實現Runable介面
3、實現Callable介面
4、執行緒池

對於Callable介面:

1、需要實現Callable介面,這裡可以實現泛型介面Callable<?>
2、重寫call方法,call方法可以返回泛型介面裡面的資料型別資料
3、然後在啟動這個執行緒的時候需要FutureTask類的支援,當然這個類就是獲取執行緒返回值的類

demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 註釋:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException, ExecutionException {
        CallableDemo callableDemo = new CallableDemo();

        //要啟動實現Callable 介面的執行緒類 需要 FutureTask 類的支援,用於接收運算結果
        FutureTask futureTask1 = new FutureTask(callableDemo);

        //啟動執行緒
        new Thread(futureTask1).start();

        //獲取執行緒返回值
        System.out.println(Thread.currentThread().getName()+"得到返回值:"+futureTask1.get());
    }
}
//實現Callable泛型介面,也可不實現泛型,call返回的將是一個Object型別的資料
class CallableDemo implements Callable<Integer> {

    private volatile int number;

    //重寫call方法
    public synchronized Integer call() throws Exception {
        number = number +1;
        System.out.println(Thread.currentThread().getName()+"為:"+number);
        return number;
    }
}

六、java中實現同步的兩種方式: syschronized 和 lock

syschronized 實現同步的方式分為:同步方法 和 同步程式碼塊
lock 是一個介面 ,通過:

private Lock lock = new ReentrantLock();
//然後lock呼叫:  
lock.lock();
//....同步程式碼 
lock.unlock();  
//獲得鎖和釋放鎖

還可以:

private Condition condition = lock.newCondition();

呼叫:

condition.await();
condition.signal(); 
condition.signalAll();

等待和喚醒單個或所有執行緒,與wait 和notify、notifyAll不同的是:可以實現多路分用,也就是說將多個執行緒拆分等待,可以喚醒某一個確定同步執行緒。

但是syschronized 可以自動的獲取和釋放鎖,而lock則需要顯示的獲取和釋放,釋放鎖lock.unlock(); 必須放在try ... finally 的finally裡面執行。

當執行緒競爭較激烈的話,Lock 效能優於 syschronized 。兩者取決於業務的需求。

七、讀寫鎖ReadWriteLock

保證 :讀讀、讀寫不是互斥的,寫寫是互斥(事件不能同時發生)的。

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 註釋:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException, ExecutionException {
        final DemoClass demoClass = new DemoClass();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    demoClass.get();
                }
            }).start();
        }

        new Thread(new Runnable() {
            public void run() {
                double number = Math.random()*100;
                demoClass.set((int)number);
            }
        }).start();
    }
}

/**
 * 讀寫鎖例項
 */
class DemoClass  {

    private int number;
    //建立讀寫鎖例項
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //讀
    public void get() {
        lock.readLock().lock();
        try{
            System.out.println("讀--操作:number = "+ number);
        }finally {
            lock.readLock().unlock();
        }
    }
    //寫
    public void set(int number){
        lock.writeLock().lock();
        try{
            this.number = number;
            System.out.println("寫++操作:number = "+ number);
        }finally {
            lock.writeLock().unlock();
        }
    }
}

八、執行緒池

1、為什麼要用執行緒池?
當我們想要多次啟動同一執行緒時,每一次啟動都有建立和銷燬操作,這樣對於高併發的情況是不利的,所以就有了執行緒池的概念。

2、什麼是執行緒?
執行緒池底層是實現一個對列,這個對列裡面存放著多個執行緒,這樣就不要每次建立都要銷燬,影響效率。

3、執行緒池核心介面:Executor (位於Java.util.concurrent包下)

4、執行緒池體系結構
java.util.concurrent.Excutor :負責執行緒的使用與排程的介面
|------ExecutorService 子介面:執行緒池主要介面
|-------------ThreadPoolExecutor :執行緒池的實現類
|-------------ScheduledExecutorService:子介面,負責執行緒的排程
|--------------------ScheduledThreadPoolExecutor:繼承ThreadPoolExecutor ,實現 ScheduledExecutorService

5、工具類:Executors
|----ExecutorService newFixedThreadPool(); 建立固定大小的執行緒池
|----ExecutorService newCachedThreadPool(); 快取執行緒池,執行緒池數量不確定,可以根據需要自動的更改數量
|----ExecutorService newSingleThreadPoolExecutor(); 建立固定大小的執行緒,可以延遲或定時的執行任務。

6、執行緒池的使用:

        //實現Runable的執行緒類
        final DemoClass demoClass = new DemoClass();
        //建立固定大小為5的執行緒池
        final ExecutorService pool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            //為執行緒池中的執行緒分配任務
            pool.submit(new Thread(demoClass));
        }
        //關閉執行緒池
        pool.shutdown();

九、執行緒排程(這裡摘自百度百科)

計算機通常只有一個CPU,在任意時刻只能執行一條機器指令,每個執行緒只有獲得CPU的使用權才能執行指令。所謂多執行緒的併發執行,其實是指從巨集觀上看,各個執行緒輪流獲得CPU的使用權,分別執行各自的任務。在執行池中,會有多個處於就緒狀態的執行緒在等待CPU,JAVA虛擬機器的一項任務就是負責執行緒的排程,執行緒排程是指按照特定機制為多個執行緒分配CPU的使用權。

有兩種排程模型:分時排程模型和搶佔式排程模型。

分時排程模型是指讓所有的執行緒輪流獲得cpu的使用權,並且平均分配每個執行緒佔用的CPU的時間片這個也比較好理解。

java虛擬機器採用搶佔式排程模型,是指優先讓可執行池中優先順序高的執行緒佔用CPU,如果可執行池中的執行緒優先順序相同,那麼就隨機選擇一個執行緒,使其佔用CPU。處於執行狀態的執行緒會一直執行,直至它不得不放棄CPU。

一個執行緒會因為以下原因而放棄CPU。

1 java虛擬機器讓當前執行緒暫時放棄CPU,轉到就緒狀態,使其它執行緒獲得執行機會。
2 當前執行緒因為某些原因而進入阻塞狀態
3 執行緒結束執行

相關文章