讓人抓頭的Java併發(一) 輕鬆認識多線程

本篇文章做爲Java併發系列的第一篇,並不會介紹相應的api,只是簡單的提到多線程關鍵線程的通訊和同步,後續文章會詳細介紹其中的原理編程

線程簡介

現代操做系統的最小執行單元,也稱爲輕量級線程。一個進程裏能夠建立多個線程,各個線程能夠共享進程資源(內存地址、文件I/O等),又各自擁有獨立的計數器、堆棧和局部變量等屬性,JVM運行時數據區也如此。

Java線程的實現

在HotSpot虛擬機中使用的是一對一線程模型,一個Java線程映射一個內核線程實現。內核線程(Kernel Level Thread)是直接由操做系統內核(Kernel)支持的線程,由內核完成切換,內核經過調度器(Scheduler)對線程調度並將線程的任務映射到各個處理器上。程序使用內核線程的高級接口--輕量級進程(Light Weight Process,一般講的線程),不直接使用內核線程。

由於Java線程採用這種一對一映射內核線程方式實現,因此Java線程調度沒法經過設置優先級控制(線程調度採用搶佔式)api

輕量級進程與內核線程1:1的關係
輕量級進程與內核線程之間1:1的關係

多線程的好處

正確使用多線程老是能給程序帶來顯著的好處,使用多線的緣由主要有如下幾點:
  • 提升cpu利用率 -- 一個線程在一個時刻只能運行在一個cpu核心上,使用多線程將計算分配到多個cpu核心會顯著減小程序處理時間。
  • 更快的響應時間 -- 好比咱們在一個訂單完成以後的通知賣家和發送短信的後續操做,像這種沒有關聯的或者數據一致性不強的操做,用多線程並行會比串行來的快,得到更快的響應。
  • 創建了編程模型 -- 讓開發人員專一於所遇到的問題,而不是多線程化。解決問題以後,稍做修改能方便的映射到多線程模型上。

線程的狀態:

Java定義了6種線程狀態,在任意一個時間點,一個線程只能處於其中的一種狀態。

Java線程生命週期中的六種不一樣狀態
狀態 說明
NEW 初始狀態,線程被構建尚未調用start()方法
RUNNABLE 運行狀態,Java中運行和就緒統稱運行中,可能正在執行,也可能在等待CPU時間片
WAITING 等待狀態,這種狀態的線程不會被分配CPU時間片,須要等待其餘線程顯式喚醒
TIMED_WAITING 超時等待狀態,不一樣於等待狀態的是在必定時間以後它們會由系統自動喚醒
BLOCKED 阻塞狀態,表示線程在等待對象的monitor鎖,試圖經過synchronized去獲取某個鎖
TERMINATED 終止狀態,表示當前線程已執行完畢

一圖勝千言,下圖展現了Java線程狀態間的轉換
Java線程狀態間的轉換

💡小提示: 操做系統中的運行和就緒兩個狀態在Java中合併成運行狀態,LockSupport類的park()方法會使當前線程進入等待狀態,因爲併發包中的ReentrantLock和condition等併發工具使用的是LockSupport的park(),因此阻塞於它們的線程不一樣於阻塞於synchronized的線程是處於阻塞狀態,而是等待狀態;關於這些狀態的轉換你們能夠寫個小demo試一試,利用jstack查看線程狀態。 緩存

線程的建立

  • 繼承Thread類
  • 實現Runnable接口
  • 實現Callable接口
不推薦使用繼承Thread方式實現,Runnable和Callable的區別在於實現後者能夠獲取執行的結果返回值(實現了Future接口的對象)和拋出異常。能夠經過Future接口的get()獲取返回值,get()會阻塞當前線程直到任務完成,Future接口的cancel(boolean)方法能夠取消任務的執行(任務未啓動,若是已啓動boolean爲true將以中斷方式中止任務,false則不會產生影響)。實現有返回值的任務一般使用FutureTask配合線程池,該類實現了Runnable和Futrue接口

推薦使用線程池管理線程,線程池有如下好處bash

  • 提升響應速度,任務到達不須要等待線程建立
  • 下降資源消耗,重複利用已建立的線程下降線程建立和銷燬的消耗
  • 更好的管理線程,線程是稀缺資源,使用線程池能夠進行統一分配監控

💡小提示:儘可能不要使用Executors來建立線程池,由於使用的無界隊列會發生內存溢出,具體緣由後續章節會介紹多線程


線程間的通訊

Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定了一個線程對共享變量的寫入什麼時候對其餘線程可見。Java多線程同時訪問一個對象或者變量時,因爲每一個線程擁有的只是這個變量的拷貝(爲了加快程序的執行),因此在執行過程當中,一個線程看到的變量並不必定是最新的。

💡小提示:線程私有的本地內存是一個緩存、寫緩衝等的抽象概念併發

Java內存模型

Java中的線程通訊方式有如下幾種:工具

  • synchronized -- 一個線程釋放鎖以後會使同步隊列中阻塞狀態的線程從新嘗試獲取鎖
  • volatile -- 一個線程對變量的修改必須同步刷新回主存中,保證了其餘線程對變量的可見性,但並不保證原子性
  • Object的wait()/notify()機制 -- 調用對象的notify()方法會使處於對象等待隊列中的線程從對象的wait()方法返回,從新進入同步隊列嘗試獲取鎖
  • 管道輸入/輸出流 -- 線程之間數據傳輸(單向),媒介爲內存

線程間的通訊方式:共享內存、消息隊列;學習

//等待通知模式的經典範式:
synchronized (object) {
    while (boolean) {
        object.wait();
    }
    doSomeThing();
}

synchronized (object) {
    change boolean;
    object.notify();
}
複製代碼

💡小提示:等待處使用while而不是if是爲了防止錯誤或者提早的通知 ui


線程同步

將操做共享變量的代碼行做爲一個總體,同一時間只容許一個線程執行。目的是爲了防止多個線程訪問一個變量時出現不一致。

Java中實現線程同步的方式:spa

  • synchronized
  • Lock
  • CountDownLatch
  • CyclicBarrier
  • Semaphore
  • 阻塞隊列

除了synchronized其它幾種同步方式的實現都與AQS有關,後續文章中會詳細介紹,下面給你們來個demo感覺下本篇文章的內容。

/**
 * @author XiaMu
 */
public class FutureTaskDemo {

    private static volatile Integer count = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(500);

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 不推薦,爲了方便使用了
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        List<FutureTask<Integer>> resultList = new ArrayList<>(500);
        for (int i = 0; i < 500; i++) {
            FutureTask<Integer> task = new FutureTask<>(() -> {
//                lock.lock();(1)
                int result = count++;
//                lock.unlock();(1)
//                countDownLatch.countDown();(2)
                return result;
            });
            executorService.submit(task);
            resultList.add(task);
        }
//        countDownLatch.await();(2)
        System.out.println("第一處計算結果:" + count);
        // 爲了查看每一個任務的執行結果
        Map<Integer, Integer> resultMap = new HashMap<>(500);
        for (FutureTask<Integer> result : resultList) { // (3)
            Integer sum = result.get();
            if (resultMap.containsKey(sum)) {
                System.out.println(sum + "計算過了");
            } else {
                resultMap.put(sum, 1);
            }
        }
        System.out.println("第二處計算結果:" + count);
        executorService.shutdown();
    }
}
複製代碼

取兩次執行結果:

第一處計算結果:0                        第一處計算結果:270
135計算過了                             492計算過了
163計算過了                             16計算過了
454計算過了                             437計算過了
458計算過了                             445計算過了
463計算過了                             第二處計算結果:496
470計算過了
317計算過了
319計算過了
第二處計算結果:492
複製代碼

💡:毫無疑問,計算結果出現了錯誤,能夠將(1)註釋打開加鎖保證計算正確。第一處計算結果不等於第二處是由於主線程執行到第一處時,子任務並無執行完。若是沒有(3)的for循環中獲取結果的Future.get()阻塞主線程,那麼第二處計算結果打印時子任務可能還沒執行完。註釋(2)打開能夠解決兩次打印不一致問題,由於CountDownLatch會將主線程阻塞直到第500個子任務執行完畢。

💡:使用集合時最好根據狀況指定初始容量,不然在後續的操做中會發生頻繁擴容影響效率



【本篇文章只是講了個大概,若是小夥伴們發現有問題或者疑惑的地方歡迎指出,一塊兒學習進步!】







參考:《Java併發編程的藝術》《碼出高效》

相關文章
相關標籤/搜索