Java 併發編程

併發編程的核心是爲了提升電腦資源的利用率,由於現代操做系統都是多核的,能夠同時跑多個線程。那麼是否是線程越多越好? 因爲線程的切換涉及上下文的切換,所謂上下文就是線程運行時須要的資源,系統要分配給它消耗時間。因此爲了減小上下文的切換,咱們有如下幾種方法:java

  • CAS算法
  • 協程,單線程裏實現多任務調度
  • 避免建立不須要的線程所以
協程和線程區別:每一個線程OS會給它分配固定大小的內存(通常2MB)來存儲當前調用或掛起的函數的內部變量,固定大小的棧意味着內存利用率很低或有時面對複雜函數沒法知足要求,協成就實現了可動態伸縮的棧(最小2KB,最大1GB).其二OS線程受操做系統調度,調度時要將當前線程狀態存到內存,將另外一個線程執行指令放到寄存器,這幾步很耗時。Go調度器並不是硬件調度器,而是Go語言內置的一中機制,所以goroutine調度時則不須要切換上下文。

Java併發機制的底層實現原理,java代碼編譯成字節碼後加載到JVM中,JVM執行字節碼最終轉化成彙編命令在CPU上運行,所以Java所使用的併發機制依賴JVM的實現和CPU指令。Java大部分併發容器和框架都依賴於volatile和原子操做的實現原理。算法

  • volatile:被volatile修身的變量在進行寫操做時會多出一行以Lock爲前綴的彙編代碼,Lock前綴的指令在多核處理器下執行兩件事情,1.將當前處理器緩存行(緩存可分配的最小單元)的數據寫入到系統內2.寫回內存的操做使其它處理器地址爲該緩存的內存無效。這兩條保證了所謂的可見性
  • 原子操做的實現:首先看一看處理器是如何實現原子操做的,有兩核CPU1和CPU2,兩個處理器同時對數據i進行操做,CPU採起總線鎖使得一個數據不能同時被多個處理器操做。大概原理就是使用處理器提供的一個LOCK信號,一個處理器在總線上輸出此信號時另外一個處理器的請求被阻塞住。這樣會致使別的處理器不能處理其它內存地址的數據,由於總線鎖開銷比較大出現了緩存鎖,使得CPU1修改緩存行1中數據時若使用了緩存鎖定,那麼CPU2就不能再緩存該緩存。處理器提供了一系列命令支持這兩種機制,如BTS,XADD等,被這些指令操做的內存區域就會加鎖,使其它處理器不能同時訪問。

Java內存模型

Java之間經過共享內存進行通訊,處理器和編譯器爲了提升性能會對指令進行重排序,這在單線程狀況下不會發生異常,可是在多線程下就會形成結果的不一致編程

int a=0;
public int calculate(){
    a=1;  1 
    boolean flag=true;  2
    if(flag){ 
        return a*a;
    }
    return 0;
}

現有兩個線程執行這段代碼,線程A執行時對指令進行了重排序先制行 2 在執行 1,在中間線程B插入了進來此時a=1值還沒被寫入致使返回結果爲0發生錯誤。緩存

處理器遵循as-if-serial語義,即無論如何重排序結果不變,可是多線程狀況下會出現錯誤

爲了不重排序,Java引入了volatile變量,使得語句在操做被volatile修飾的變量時禁止指令重排序。在執行指令時插入內存屏障也就是這個目的,最關鍵的是volatile的讀/寫內存語義以下服務器

  • 寫語義:寫一個volatile變量時會把線程對應本地內存的值刷新到主存中
  • 讀語義:讀一個volatile變量時會把本地內存的值設置爲無效,從主存中讀

volatile的缺陷在於這個動做是不徹底的,所以又提出了CAS機制,CAS會使用處理器提供的機器級別的原子命令(CMPXCHG),原子執行讀-改-寫操做。Java concurrent包中一個通用化的實現模式就是結合二者,步驟以下多線程

  • 聲明共享變量爲volatile
  • 使用CAS實現線程間的同步和通訊,(自旋樂觀鎖,性能大大提高)

Java線程池

線程池的核心做用就是維護固定的幾個線程,有任務來的時候直接使用避免建立/銷燬線程致使的額外開銷。 線程池執行流程以下:架構

提交任務-->核心線程池已滿? 是 提交任務到消息隊列--->隊列已滿? 是 按指定策略執行
                          否 建立線程執行任務                否 加進隊列

瞭解了線程池的原理最重要的就是如何是去使用它,而使用的關鍵就是參數的設置。併發

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

以上是ThreadPoolExecutor的構造函數,咱們逐一看一看各參數的含義框架

  • corePoolSize 一直維護的線程數
  • maximumPoolSize 最大線程數
  • keepAliveTime 多餘線程存活的時間(實際線程數比corePool多的那部分)
  • workQueue 存儲線程的隊列,可選擇ArrayBlockingQueue等
  • threadFactory 建立線程時的用到的工廠,可經過自定義工廠建立更有意義的線程名稱
  • handler 隊列滿時採起的策略 有AbortPolicy(直接拋出異常)/CallerRunsPolicy(只用調用者所在的線程執行)等等

提交線程池有兩個方法,一個是submit這個不須要返回值,一個是submit會返回一個future對象,並經過future的get()方法獲取返回值(該方法會阻塞直到線程完成任務)。函數

合理配置線程池,CPU密集型任務配置少數線程池如N(CPU個數)+1,I/O密集型任務配置多一點的線程池如2N(CPU個數),其次是使用有界隊列即便發現錯誤。

Executor框架

在HotSpot VM的線程模型中,Java線程被一對一的映射成本地操做系統的線程,操做系統會調度線程把它們分配給可用的CPU。在上層Java經過用戶級調度器Executor將任務映射爲幾個線程,在下層操做系統內核將這些線程映射到硬件處理器上面。

Executor的出現將任務與如何執行任務分離開了,避免了每建立一個線程就要執行它。Executor的整個架構有一下幾個要點

  • 實現了Runnable和Callable的對象可提交到Executor運行
  • 可返回Future獲取線程執行後的返回值
  • 內部維護一個線程池(上面介紹的)來處理提交過來的任務

Executor最核心的就是ThreadPoolExecutor,下面介紹如下以及各自使用場景

  • FixedThreadPool 固定線程個數,用於高負載的服務器,知足資源的管理需求
  • SingleThreadPool 單個線程,保證順序的執行任務
  • CachedThreadPool 大小無界的線程池,使用負載比較輕的服務器
  • ScheduledThreadPoolExecutor 後臺週期執行任務
相關文章
相關標籤/搜索