初識多線程及其原理-筆記

什麼狀況下應該使用多線程?java

  • 經過並行計算提升程序執行性能
  • 須要等待網絡、I/O響應致使耗費大量的執行時間,
    • 能夠採用異步線程的方式來減小阻塞

tomcat7 之前的io模型編程

  • 客戶端阻塞
  • 線程級別阻塞 BIO

如何應用多線程?數組

  • 在Java中,有多種方式來實現多線程。
  • 繼承Thread類、實現Runnable接口、
  • 使用ExecutorService、Callable、Future實現帶返回結果的多線程。

繼承Thread類建立線程緩存

  • Thread類本質上是實現了Runnable接口的一個實例,表明一個線程的實例。
  • Thread類本質上是實現了Runnable接口的一個實例,表明一個線程的實例。
  • 啓動線程的惟一方法就是經過Thread類的start()實例方法。
  • start()方法是一個native方法,它會啓動一個新線程,並執行run()方法。

實現Runnable接口建立線程tomcat

  • 若是本身的類已經extends另外一個類,就沒法直接extends Thread,
  • 此時,能夠實現一個Runnable接口

實現Callable接口經過FutureTask包裝器來建立Thread線程安全

  • 有的時候,咱們可能須要讓一步執行的線程在執行完成之後,
  • 提供一個返回值給到當前的主線程,主線程須要依賴這個值進行後續的邏輯處理,
  • 那麼這個時候,就須要用到帶返回值的線程了。
public class CallableDemo implements Callable<String> {

	@Override
	public String call() throws Exception {
		int a = 1;
		int b = 2;
		System.out.println(a + b);
		return "執行結果:" + (a + b);
	}

	public static void main(String[] args) throws ExecutionException, InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(1);
		CallableDemo callableDemo = new CallableDemo();
		Future<String> future = executorService.submit(callableDemo);
		System.out.println(future.get());
		executorService.shutdown();
	}
}

Java 併發編程基礎網絡

線程的狀態多線程

  • 線程一共有6種狀態(
    • NEW、
    • RUNNABLE、
    • BLOCKED、
    • WAITING、
    • TIME_WAITING、
    • TERMINATED)
  • NEW:初始狀態,線程被構建,可是尚未調用start方法
  • RUNNABLED:運行狀態,JAVA線程把操做系統中的就緒和運行兩種狀態統一稱爲「運行中」
  • BLOCKED:阻塞狀態,表示線程進入等待狀態,也就是線程由於某種緣由放棄了CPU使用權,阻塞也分爲幾種狀況
    • 等待阻塞:運行的線程執行wait方法,jvm會把當前線程放入到等待隊列
    • 同步阻塞:運行的線程在獲取對象的同步鎖時,
      • 若該同步鎖被其餘線程鎖佔用了,那麼jvm會把當前的線程放入到鎖池中
    • 其餘阻塞:運行的線程執行Thread.sleep或者t.join方法,
      • 或者發出了I/O請求時,JVM會把當前線程設置爲阻塞狀態,
      • 當sleep結束、join線程終止、io處理完畢則線程恢復
  • TIME_WAITING:超時等待狀態,超時之後自動返回
  • TERMINATED:終止狀態,表示當前線程執行完畢

經過相應命令顯示線程狀態併發

  • 打開終端或者命令提示符,鍵入「jps」,能夠得到相應進程的pid
  • 根據上一步驟得到的pid,繼續輸入jstack pid
    • jstack是java虛擬機自帶的一種堆棧跟蹤工具。
    • jstack用於打印出給定的java進程ID或core file或遠程調試服務的Java堆棧信息

線程的中止異步

  • stop、suspend、resume過時不建議使用
    • stop方法在結束一個線程時並不會保證線程的資源正常釋放,
    • 所以會致使程序可能出現一些不肯定的狀態。

要優雅的去中斷一個線程,在線程中提供了一個interrupt方法

  • 當其餘線程經過調用當前線程的interrupt方法,
  • 表示向當前線程打個招呼,告訴他能夠中斷線程的執行了,至於何時中斷,取決於當前線程本身。
  • 能夠經過isInterrupted()來判斷是否被中斷
  • 這種經過標識位或者中斷操做的方式可以使線程在終止時有機會去清理資源,而不是武斷地將線程中止,
    • 所以這種終止線程的作法顯得更加安全和優雅

Thread.interrupted

  • 注意區別,這個是復位方法,與interrupt中斷方法對應
public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                boolean ii = Thread.currentThread().isInterrupted();
                if (ii) {
                    System.out.println("before:" + ii);
                    Thread.interrupted();//對線程進行復位,中斷標識爲false 
                    System.out.println("after:" + Thread.currentThread().isInterrupted());
                }
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();//設置中斷標識,中斷標識爲true 
    }
}

其餘的線程復位

  • 還有一種被動復位的場景,就是對拋出InterruptedException異常的方法,
  • 在InterruptedException拋出以前,JVM會先把線程的中斷標識位清除,
  • 而後纔會拋出InterruptedException,這個時候若是調用isInterrupted方法,將會返回false
public class InterruptDemo {
   public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    //拋出該異常,會將復位標識設置爲false 
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        //設置復位標識爲true 
        TimeUnit.SECONDS.sleep(1);
        System.out.println(thread.isInterrupted());//false
    }
}

首先咱們來看看線程執行interrupt之後的源碼是作了什麼?

  • 其實就是經過unpark去喚醒當前線程,而且設置一個標識位爲true。
  • 並無所謂的中斷線程的操做,因此實際上,線程復位能夠用來實現多個線程之間的通訊。

線程的中止方法之2

  • 定義一個volatile修飾的成員變量,來控制線程的終止
  • 這其實是應用了volatile可以實現多線程之間共享變量的可見性這一特色來實現的。
public class VolatileDemo {
        private volatile static boolean stop = false;

        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                int i = 0;
                while (!stop) {
                    i++;
                }
            });
            thread.start();
            System.out.println("begin start thread");
            Thread.sleep(1000);
            stop = true;
        }
    }

線程的安全性問題

  • 咱們從原理層面去了解線程爲何會存在安全性問題,而且咱們應該怎麼去解決這類的問題。
  • 線程安全問題能夠總結爲: 可見性原子性有序性這幾個問題,
  • 咱們搞懂了這幾個問題而且知道怎麼解決,那麼多線程安全性問題也就不是問題了

CPU高速緩存

  • 線程是CPU調度的最小單元,線程涉及的目的最終仍然是更充分的利用計算機處理的效能,
  • 可是絕大部分的運算任務不能只依靠處理器「計算」就能完成,
    • 處理器還須要與內存交互,好比讀取運算數據、存儲運算結果,這個I/O操做是很難消除的。
  • 而因爲計算機的存儲設備與處理器的運算速度差距很是大,
    • 因此現代計算機系統都會增長一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存和處理器之間的緩衝:
    • 將運算須要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中。

  • 高速緩存從下到上越接近CPU速度越快,同時容量也越小。
  • 如今大部分的處理器都有二級或者三級緩存,從下到上依次爲 L3 cache, L2 cache, L1 cache.
  • 緩存又能夠分爲指令緩存和數據緩存,指令緩存用來緩存程序的代碼數據緩存用來緩存程序的數據

L1 Cache,

  • 一級緩存,本地core(cpu核心)的緩存,
  • 分紅32K的數據緩存L1d和32k指令緩存L1i,
  • 訪問L1須要3cycles,耗時大約1ns;

L2 Cache,

  • 二級緩存,本地core(cpu核心)的緩存,
  • 被設計爲L1緩存與共享的L3緩存之間的緩衝,大小爲256K,
  • 訪問L2須要12cycles,耗時大約3ns;

L3 Cache,

  • 三級緩存,在同插槽的全部core(cpu核心)共享L3緩存,分爲多個2M的段,
  • 訪問L3須要38cycles,耗時大約12ns;

緩存一致性問題

  • CPU-0讀取主存的數據,緩存到CPU-0的高速緩存中,
  • CPU-1也作了一樣的事情,而CPU-1把count的值修改爲了2,而且同步到CPU-1的高速緩存,
  • 可是這個修改之後的值並無寫入到主存中,
  • CPU-0訪問該字節,因爲緩存沒有更新,因此仍然是以前的值,就會致使數據不一致的問題

引起這個問題的緣由是

  • 由於多核心CPU狀況下存在指令並行執行,
  • 而各個CPU核心之間的數據不共享從而致使緩存一致性問題,
  • 爲了解決這個問題,CPU生產廠商提供了相應的解決方案

總線鎖

  • 當一個CPU對其緩存中的數據進行操做的時候,往總線中發送一個Lock信號。
  • 其餘處理器的請求將會被阻塞,那麼該處理器能夠獨佔共享內存。
  • 總線鎖至關於把CPU和內存之間的通訊鎖住了
    • 因此這種方式會致使CPU的性能降低,
    • 因此P6系列之後的處理器,出現了另一種方式,就是緩存鎖。

緩存鎖

  • 若是緩存在處理器緩存行中的內存區域在LOCK操做期間被鎖定,
  • 當它執行鎖操做回寫內存時,處理不在總線上聲明LOCK信號,而是修改內部的緩存地址
  • 而後經過緩存一致性機制來保證操做的原子性,
    • 由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域的數據,
    • 當其餘處理器回寫已經被鎖定的緩存行的數據時會致使該緩存行無效。

因此若是聲明瞭CPU的鎖機制,會生成一個LOCK指令,會產生兩個做用:

  • Lock前綴指令會引發處理器緩存回寫到內存,在P6之後的處理器中,LOCK信號通常不鎖總線,而是鎖緩存
  •  一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效

緩存一致性協議

  • 處理器上有一套完整的協議,來保證Cache的一致性,比較經典的應該就是:MESI
  • MESI協議的方法是在CPU緩存中保存一個標記位,
    • 這個標記爲有四種狀態:
      • M(Modified) 修改緩存,當前CPU緩存已經被修改,表示已經和內存中的數據不一致了
      • I(Invalid) 失效緩存,說明CPU的緩存已經不能使用了
      • E(Exclusive) 獨佔緩存,當前cpu的緩存和內存中數據保持一直,並且其餘處理器沒有緩存該數據
      • S(Shared) 共享緩存,數據和內存中數據一致,而且該數據存在多個cpu緩存中
  • 每一個Core(cpu核心)的Cache控制器不只知道本身的讀寫操做,也監聽其它Cache的讀寫操做,嗅探(snooping)協議

CPU的讀取會遵循幾個原則:

  • 若是緩存的狀態是I,那麼就從內存中讀取,不然直接從緩存讀取
  • 若是緩存處於M或者E的CPU 嗅探到其餘CPU有讀的操做,就把本身的緩存寫入到內存,並把本身的狀態設置爲S
  • 只有緩存狀態是M或E的時候,CPU才能夠修改緩存中的數據,修改後,緩存狀態變爲M

CPU的優化執行

  • 除了增長高速緩存之外,
  • 爲了更充分利用處理器內內部的運算單元,處理器可能會對輸入的代碼進行亂序執行優化,
  • 處理器會在計算以後將亂序執行的結果充足,保證該結果與順序執行的結果一致,
  • 但並不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致,這個是處理器的優化執行
  • 還有一個就是編程語言的編譯器也會有相似的優化,好比作指令重排來提高性能

併發編程的問題

  • 前面說的和硬件有關的概念你可能聽得有點蒙,還不知道他到底和軟件有啥關係
    • 其實原子性、可見性、有序性問題,是咱們抽象出來的概念,
    • 他們的核心本質就是剛剛提到的緩存一致性問題處理器優化問題  致使指令重排序問題
  • 好比緩存一致性就致使可見性問題、
  • 處理器的亂序執行會致使原子性問題、
  • 指令重排會致使有序性問題。
  • 爲了解決這些問題,因此在JVM中引入了JMM的概念

內存模型(JMM

  • 內存模型定義了共享內存系統中多線程程序讀寫操做行爲的規範,
    • 來屏蔽各類硬件和操做系統的內存訪問差別,
    • 來實現Java程序在各個平臺下都能達到一致的內存訪問效果。
  • Java內存模型的主要目標是定義程序中各個變量的訪問規則,
    • 也就是在虛擬機中將變量存儲到內存以及從內存中取出變量
    • 這裏的變量,指的是共享變量,也就是實例對象、靜態字段、數組對象等存儲在堆內存中的變量
    • 而對於局部變量這類的,屬於線程私有,不會被共享
  • 經過這些規則來規範對內存的讀寫操做,從而保證指令執行的正確性。
    • 它與處理器有關、與緩存有關、與併發有關、與編譯器也有關。
    • 他解決了CP;多級緩存、處理器優化、指令重排等致使的內存訪問問題,保證了併發場景下的可見性、原子性和有序性。
    • 內存模型解決併發問題主要採用兩種方式:限制處理器優化使用內存屏障

Java內存模型定義了線程和內存的交互方式,

  • 在JMM抽象模型中,分爲主內存、工做內存。
  • 主內存是全部線程共享的,工做內存是每一個線程獨有的。
  • 線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存中的變量。
  • 而且不一樣的線程之間沒法訪問對方工做內存中的變量,
  • 線程間的變量值的傳遞都須要經過主內存來完成,他們三者的交互關係以下:

因此,總的來講,JMM是一種規範,

  • 目的是解決因爲多線程經過共享內存進行通訊時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。
  • 目的是保證併發編程場景中的原子性、可見性和有序性
相關文章
相關標籤/搜索