對Java多線程的一些理解

OS中的進程、線程

  • 進程:即處於執行期的程序,且包含其餘資源,如打開的文件、掛起的信號、內核內部數據、處理器狀態、內核地址空間、一個或多個執行的線程、數據段。
  • 線程:進程中的活動對象,內核調度的對象不是進程而是線程;傳統Unix系統一個進程只包含一個線程。

線程在Linux中的實現

從Linux內核的角度來講,並無線程這個概念。Linux把全部的線程都當作進程來實現,內核沒有爲線程準備特別的調度算法和特別的數據結構。線程僅僅被視爲一個與其餘進程共享某些資源的進程。因此,在內核看來,它就是一個普通的進程。linux

在Windows或Solaris等操做系統的實現中,它們都提供了專門支持線程的機制(lightweight processes)。git

寫時拷貝

傳統的fork()系統調用直接把全部資源複製給新建立的進程,效率十分低下,由於拷貝的數據也許並不須要。github

Linux的fork()使用寫時拷貝實現。內核此時並不複製整個進程地址空間,而是讓父進程和子進程共享一個拷貝。算法

只有在須要寫入的時候,數據纔會被複制,在此以前,只是以只讀方式共享。這種優化能夠避免拷貝大量根本就不會被使用的數據(地址空間經常包含幾十M的數據)。segmentfault

所以,Linux建立進程和線程的區別就是共享的地址空間、文件系統資源、文件描述符、信號處理程序等這些不一樣。網絡

如下是StackOverflow上的一個答案:數據結構

alt text

即,在Linux下,進程使用fork()建立,線程使用pthread_create()建立;fork()pthread_create()都是經過clone()函數實現,只是傳遞的參數不一樣,即共享的資源不一樣。(Linux是經過NPTL實現POSIX Thread規範,即經過輕量級進程實現POSIX Thread,使以前在Unix上的庫、軟件能夠平穩的遷移到Linux上)多線程

Java線程如何映射到OS線程

JVM在linux平臺上建立線程,須要使用pthread 接口。pthread是POSIX標準的一部分它定義了建立和管理線程的C語言接口。Linux提供了pthread的實現:併發

pthread_t tid;
if (pthread_create(&tid, &attr, thread_entry_point, arg_to_entrypoint))
{
      fprintf(stderr, "Error creating thread\n");
      return;
}
  • tid是新建立線程的ID
  • attr是咱們須要設置的線程屬性
  • thread_entry_point是會被新建立線程調用的函數指針
  • arg_to_entrypoint是會被傳遞給thread_entry_point的參數

thread_entry_point所指向的函數就是Thread對象的run方法。框架

無返回值線程和帶返回值的線程

  • 無返回值:一種是直接繼承Thread,另外一種是實現Runnable接口
  • 帶返回值:經過Callable和Future實現

帶返回值的線程是咱們在實踐中更經常使用的。

競態條件

當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。

最多見的競態條件類型就是「先檢查後執行」(Check-Then-Act)操做,即經過一個可能失效的觀測結果來決定下一步的動做。

使用「」先檢查後執行「的一種常見狀況就是延遲初始化:

public class LazyInitRace {
    private ExpensiveObject instance = null;
    
    public ExpensiveObject getInstance() {
        if (instance == null) {
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

不要這麼作。

Executor框架

使用裸線程的缺點

prod環境中,爲每一個任務分配一個線程的方法存在嚴重的缺陷,尤爲是當須要建立大量的線程時:

  • 線程生命週期的開銷很是高:線程的建立與銷燬並非沒有代價的。
  • 資源消耗:會消耗內存和CPU,大量的線程競爭CPU資源將產生性能開銷。若是你已經擁有足夠多的線程使全部CPU處於忙碌狀態,那麼建立更多的線程反而會下降性能。
  • 穩定性:可建立的線程的數量上存在限制,包括JVM的啓動參數、操做系統對線程的限制,若是超出這些限制,極可能會拋出OutOfMemoryError異常。

Executor基本原理

Executor基於生產者-消費者模式,提交任務的操做至關於生產者,執行任務的線程則至關於消費者。

線程池的構造函數以下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

線程池大小

  • corePoolSize:核心線程數,當線程池的線程數小於corePoolSize,直接建立新的線程
  • 線程數大於corePoolSize可是小於maximumPoolSize:若是任務隊列還未滿, 則會將此任務插入到任務隊列末尾;若是此時任務隊列已滿, 則會建立新的線程來執行此任務。
  • 線程數等於maximumPoolSize:若是任務隊列還未滿, 則會將此任務插入到任務隊列末尾;若是此時任務隊列已滿, 則會由RejectedExecutionHandler處理。

keep-alive

  • keepAliveTime:當咱們的線程池中的線程數大於corePoolSize時, 若是此時有線程處於空閒(Idle)狀態超過指定的時間(keepAliveTime), 那麼線程池會將此線程銷燬。

工做隊列

工做隊列(WorkQueue)是一個BlockingQueue, 它是用於存放那些已經提交的, 可是尚未空餘線程來執行的任務。

常見的工做隊列有一下幾種:

  • 直接切換(Direct handoffs)
  • 無界隊列(Unbounded queues)
  • 有界隊列(Bounded queues)

在生產環境中,禁止使用無界隊列,由於當隊列中堆積的任務太多時,會消耗大量內存,最後OOM;一般都是設定固定大小的有界隊列,當線程池已滿,隊列也滿的狀況下,直接將新提交的任務拒絕,拋RejectedExecutionException 出來,本質上這是對服務自身的一種保護機制,當服務已經沒有資源來處理新提交的任務,因直接將其拒絕。

Java原生線程池在生產環境中的問題

在服務化的背景下,咱們的框架通常都會集成全鏈路追蹤的功能,用來串聯整個調用鏈,主要是記錄TraceIdSpanIdTraceIdSpanId通常都記錄在ThreadLocal中,對業務方來講是透明的。

當在同一個線程中同步RPC調用的時候,不會存在問題;但若是咱們使用線程池作客戶端異步調用時,就會致使Trace信息的丟失,根本緣由是Trace信息沒法從主線程的ThreadLocal傳遞到線程池的ThreadLocal中。

對於這個痛點,阿里開源的transmittable-thread-local解決了這個問題,實現其實不難,能夠閱讀一下源碼:

https://github.com/alibaba/transmittable-thread-local

性能與伸縮性

對性能的思考

提高性能意味着用更少的資源作更多的事情。「資源」的含義很廣,例如CPU時鐘週期、內存、網絡帶寬、磁盤空間等其餘資源。當操做性能因爲某種特定的資源而受到限制時,咱們一般將該操做稱爲資源密集型的操做,例如,CPU密集型、IO密集型等。

使用多線程理論上能夠提高服務的總體性能,但與單線程相比,使用多線程會引入額外的性能開銷。包括:線程之間的協調(例如加鎖、觸發信號以及內存同步),增長的上下文切換,線程的建立和銷燬,以及線程的調度等。若是過分地使用線程,其性能可能甚至比實現相同功能的串行程序更差。

從性能監視的角度來看,CPU須要儘量保持忙碌狀態。若是程序是計算密集型的,那麼能夠經過增長處理器來提高性能。但若是程序沒法使CPU保持忙碌狀態,那增長更多的處理器也是無濟於事的。

可伸縮性

可伸縮性是指:當增長計算資源時(例如CPU、內存、存儲容量、IO帶寬),程序的吞吐量或者處理能力能響應的增長。

咱們熟悉的三層模型,即程序中的表現層、業務邏輯層和持久層是彼此獨立,而且可能由不一樣的服務來處理,這很好地說明了提升伸縮性一般會形成性能損失。若是把表現層、業務邏輯層和持久層都融合到某個單體應用中,在負載不高的時候,其性能確定要高於將應用程序分爲多層的性能。這種單體應用避免了在不一樣層次之間傳遞任務時存在的網絡延遲,減小了不少開銷。

然而、當單體應用達到自身處理能力的極限時,會遇到一個嚴重問題:提高它的處理能力很是困難,即沒法水平擴展。

Amdahl定律

大多數併發程序都是由一系列的並行工做和串行工做組成的。Amdahl定律描述的是:在增長計算資源的狀況下,程序在理論上可以實現最高加速比,這個值取決於程序中可並行組件串行組件所佔的比重。假定F是必須被串行執行的部分,那麼根據Amdahl定律,在包含N個處理器的機器上,最高的加速比爲:

alt text

當N趨近於無窮大時,最大的加速比趨近於1/F。所以,若是程序中有50%的計算須要串行執行,那麼最高的加速比只能是2。

上下文切換

線程調度會致使上下文切換,而上下文切換是會產生開銷的。如果CPU密集型程序產生大量的線程切換,將會下降系統的吞吐量。

UNIX系統的vmstat命令可以報告上下文切換次數以及在內核中執行時間的所佔比例等信息。若是內核佔用率較高(超過10%),那麼一般表示調度活動發生得很頻繁,這極可能是由I/O或者鎖競爭致使的阻塞引發的。

>> vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 3235932 238256 3202776    0    0     0    11    7    4  1  0 99  0  0
 
 cs:每秒上下文切換次數
 sy:內核系統進程執行時間百分比
 us:用戶進程執行時間百分比

以上。

原文連接

https://segmentfault.com/a/11...

相關文章
相關標籤/搜索