1. 程式人生 > >Java並發編程有多難?這幾個核心技術你掌握了嗎?

Java並發編程有多難?這幾個核心技術你掌握了嗎?

周期 回收 dex 而後 語言 交互 例子 implement 資源

本文主要內容索引

1、Java線程

2、線程模型

3、Java線程池

4、Future(各種Future)

5、Fork/Join框架

6、volatile

7、CAS(原子操作)

8、AQS(並發同步框架)

9、synchronized(同步鎖)

10、並發隊列(阻塞隊列)

本文僅分析java並發編程中的若幹核心問題,對於上面沒有提到但是又和java並發編程有密切關系的技術將會不斷添加進來完善文章,本文將長期更新,不斷叠代。本文試圖從一個更高的視覺來總結Java語言中的並發編程內容,希望閱讀完本文之後,可以收獲一些內容,至少應該知道在java中做並發編程實踐的時候應該註意什麽,應該關註什麽,如何保證線程安全,以及如何選擇合適的工具來滿足需求。當然,更深層次的內容就會涉及到jvm層面的知識,包括底層對java內存的管理,對線程的管理等較為核心的問題,當然,本文的定位在於抽象與總結,更為具體而深入的內容就需要自己去實踐,考慮到可能篇幅過長、重復描述某些內容,以及自身技術深度等原因,本文將在深度和廣度上做一些權衡,某些內容會做一些深入的分析,而有些內容會一帶而過,點到為止,總之,本文就當是對學習java並發編程內容的一個總結,以及給哪些希望快速了解java並發編程內容的讀者拋磚引玉,不足之處還望指正。

一、Java線程

一般來說,在java中實現高並發是基於多線程編程的,所謂並發,也就是多個線程同時工作,來處理我們的業務,在機器普遍多核心的今天,並發編程的意義極為重大,因為我們有多個cpu供線程使用,如果我們的應用依然只使用單線程模式來工作的話,對極度浪費機器資源的。所以,學習java並發知識的首要問題是:如何創建一個線程,並且讓這個線程做一些事情?這是java並發編程內容的起點,下面將分別介紹多個創建線程,並且讓線程做一些事情的方法。

繼承Thread類

繼承Thread類,然後重寫run方法,這是第一種創建線程的方法。run方法裏面就是我們要做的事情,可以在run方法裏面寫我們想要在新的線程裏面運行的任務,下面是一個小例子,我們繼承了Thread類,並且在run方法裏面打印出了當然線程的名字,然後sleep1秒中之後就退出了:

/*** Created by hujian06 on 2017/10/31.** the demo of thread*/public class ThreadDemo { public static void main(String ... args) { AThread aThread = new AThread(); //start the thread aThread.start(); }}class AThread extends Thread { @Override public void run() { System.out.println("Current Thread Name:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}

如果我們想要啟動這個線程,只需要像上面代碼中那樣,調用Thread類的start方法就可以了。

實現Runnable接口

啟動一個線程的第二種方法是實現Runnable接口,然後實現其run方法,將你想要在新線程裏面執行的業務代碼寫在run方法裏面,下面的例子展示了這種方法啟動線程的示例,實現的功能和上面的第一種示例是一樣的:

/*** Created by hujian06 on 2017/10/31.** the demo of Runnable*/public class ARunnableaDemo { public static void main(String ... args) { ARunnanle aRunnanle = new ARunnanle(); Thread thread = new Thread(aRunnanle); thread.start(); }}class ARunnanle implements Runnable { @Override public void run() { System.out.println("Current Thread Name:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}

在啟動線程的時候,依然還是使用了Thread這個類,只是我們在構造函數中將我們實現的Runnable對象傳遞進去了,所以在我們執行Thread類的start方法的時候,實際執行的內容是我們的Runnable的run方法。

使用FutureTask

啟動一個新的線程的第三種方法是使用FutureTask,下面來看一下FutureTask的類圖,就可以明白為什麽可以使用FutureTask來啟動一個新的線程了:

技術分享

技術分享

技術分享

技術分享

技術分享

技術分享

FutureTask的類圖

從FutureTask的類圖中可以看出,FutureTask實現了Runnable接口和Future接口,所以它兼備Runnable和Future兩種特性,下面先來看看如何使用FutureTask來啟動一個新的線程:

import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;/*** Created by hujian06 on 2017/10/31.** the demo of FutureTask*/public class FutureTaskDemo { public static void main(String ... args) { ACallAble callAble = new ACallAble(); FutureTask<String> futureTask = new FutureTask<>(callAble); Thread thread = new Thread(futureTask); thread.start(); do { }while (!futureTask.isDone()); try { String result = futureTask.get(); System.out.println("Result:" + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }}class ACallAble implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "Thread-Name:" + Thread.currentThread().getName(); }}

可以看到,使用FutureTask來啟動一個線程之後,我們可以監控這個線程是否完成,上面的示例中主線程會一直等待這個新創建的線程直到它返回,其實只要是Future提供的接口,我們在FutureTask中都可以使用,這極大的方便了我們,Future在並發編程中的意義極為重要,Future代表一個未來會發生的東西,它是一種暗示,一種占位符,它示意我們它可能不會立即得到結果,因為它的任務還在運行,但是我們可以得到一個對這個線程的監控對象,我們可以對線程的執行做一些判斷,甚至是控制,比如,如果我們覺得我們等了太久,並且我們覺得沒有必要再等待下去的時候,就可以將這個Task取消,還有一點需要提到的是,Future代表它可能正在運行,也可能已經返回,當然Future更多的暗示你可以在等待這個結果的同時可以使用其他的線程做一些其他的事情,當你真的需要這個結果的時候再來獲取就可以了,這就是並發,理解這一點非常重要。

本小節通過介紹三種創建並啟動一個新線程的方法,為進行並發編程開了一個頭,目前,我們還只是在能創建多個線程,然後讓多個線程做不同個的事情的階段,當然,這是學習並發編程最為基礎的,無論如何,現在,我們可以讓我們的應用運行多個線程了,下面的文章將會基於這個假設(一個應用開啟了多個線程)討論一些並發編程中值得關註的內容。關於本小節更為詳細的內容,可以參考文章Java CompletableFuture中的部分內容。

二、線程模型

我們現在可以啟動多個線程,但是好像並沒有形成一種類似於模型的東西,非常混亂,並且到目前為止我們的多個線程依然只是各自做各自的事情,互不相幹,多個線程之間並沒有交互(通信),這是最簡單的模型,也是最基礎的模型,本小節試圖介紹線程模型,一種指導我們的代碼組織的思想,線程模型確定了我們需要處理那些多線程的問題,在一個系統中,多個線程之間沒有通信是不太可能的,更為一般的情況是,多個線程共享一些資源,然後相互競爭來獲取資源權限,多個線程相互配合,來提高系統的處理能力。正因為多個線程之間會有通信交互,所以本文接下來的討論才有了意義,如果我們的系統裏面有幾百個線程在工作,但是這些線程互不相幹,那麽這樣的系統要麽實現的功能非常單一,要麽毫無意義(當然不是絕對的,比如Netty的線程模型)。

繼續來討論線程模型,上面說到線程模型是一種指導代碼組織的思想,這是我自己的理解,不同的線程模型需要我們使用不同的代碼組織,好的線程模型可以提高系統的並發度,並且可以使得系統的復雜度降低,這裏需要提一下Netty 4的線程模型,Netty 4的線程模型使得我們可以很容易的理解Netty的事件處理機制,這種優秀的設計基於Reactor線程模型,Reactor線程模型分為單線程模型、多線程模型以及主從多線程模型,Netty的線程模型類似於Reactor主從多線程模型。

當然線程模型是一種更高級別的並發編程內容,它是一種編程指導思想,尤其在我們進行底層框架設計的時候特別需要註意線程模型,因為一旦線程模型設計不合理,可能會導致後面框架代碼過於復雜,並且可能因為線程同步等問題造成問題不可控,最終導致系統運行失控。類似於Netty的線程模型是一種好的線程模型,下面展示了這種模型:

Netty線程模型

簡單來說,Netty為每個新建立的Channel分配一個NioEventLoop,而每個NioEventLoop內部僅使用一個線程,這就避免了多線程並發的同步問題,因為為每個Channel處理的線程僅有一個,所以不需要使用鎖等線程同步手段來做線程同步,在我們的系統設計的時候應該借鑒這種線程模型的設計思路,可以避免我們走很多彎路。關於線程池以及Netty線程池這部分的內容,可以參考文章Netty線程模型及EventLoop詳解。Java線程池池化技術是一種非常有用的技術,對於線程來說,創建一個線程的代價是很高的,如果我們在創建了一個線程,並且讓這個線程做一個任務之後就回收的話,那麽下次要使用線程來執行我們的任務的時候又需要創建一個新的線程,是否可以在創建完成一個線程之後一直緩沖,直到系統關閉的時候再進行回收呢?java線程池就是這樣的組件,使用線程池,就沒必要頻繁創建線程,線程池會為我們管理線程,當我們需要一個新的線程來執行我們的任務的時候,就向線程池申請,而線程池會從池子裏面找到一個空閑的線程返回給請求者,如果池子裏面沒有可用的線程,那麽線程池會根據一些參數指標來創建一個新的線程,或者將我們的任務提交到任務隊列中去,等待一個空閑的線程來執行這個任務。細節內容在下文中進行分析,目前我們只需要明白,線程池裏面有很多線程,這些線程會一直到系統關系才會被回收,否則一直會處於處理任務或者等待處理任務的狀態。首先,如何使用線程池呢?下面的代碼展示了如何使用java線程池的例子:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.ThreadFactory;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/*** Created by hujian06 on 2017/10/31.** the demo of Executors*/public class ExecutorsDemo { public static void main(String ... args) { int cpuCoreCount = Runtime.getRuntime().availableProcessors(); AThreadFactory threadFactory = new AThreadFactory(); ARunnanle runnanle = new ARunnanle(); ExecutorService fixedThreadPool= Executors.newFixedThreadPool(cpuCoreCount, threadFactory); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(threadFactory); ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(cpuCoreCount, threadFactory); ScheduledExecutorService singleThreadExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); fixedThreadPool.submit(runnanle); cachedThreadPool.submit(runnanle); newScheduledThreadPool.scheduleAtFixedRate(runnanle, 0, 1, TimeUnit.SECONDS); singleThreadExecutor.scheduleWithFixedDelay(runnanle, 0, 100, TimeUnit.MILLISECONDS); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } fixedThreadPool.shutdownNow(); cachedThreadPool.shutdownNow(); newScheduledThreadPool.shutdownNow(); singleThreadExecutor.shutdownNow(); }}class ARunnable implements Runnable { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Current Thread Name:" + Thread.currentThread().getName()); }}/*** the thread factory*/class AThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread("aThread-" + threadNumber.incrementAndGet()); }}

更為豐富的應用應該自己去探索,結合自身的需求來借助線程池來實現,下面來分析一下Java線程池實現中幾個較為重要的內容。

ThreadPoolExecutor和ScheduledThreadPoolExecutor

ThreadPoolExecutor和ScheduledThreadPoolExecutor是java實現線程池的核心類,不同類型的線程池其實就是在使用不同的構造函數,以及不同的參數來構造出ThreadPoolExecutor或者ScheduledThreadPoolExecutor,所以,學習java線程池的重點也在於學習這兩個核心類。前者適用於構造一般的線程池,而後者繼承了前者,並且很多內容是通用的,但是ScheduledThreadPoolExecutor增加了schedule功能,也就是說,ScheduledThreadPoolExecutor使用於構造具有調度功能的線程池,在需要周期性調度執行的場景下就可以使用ScheduledThreadPoolExecutor。關於ThreadPoolExecutor與ScheduledThreadPoolExecutor較為詳細深入的分析可以參考下面的文章:

Java線程池詳解(二)

Java線程池詳解(一)

Java調度線程池ScheduleExecutorService

Java調度線程池ScheduleExecutorService(續)

Java並發編程有多難?這幾個核心技術你掌握了嗎?