前言
學(xué)習(xí)Java并發(fā)編程,首先要搞清楚一些基礎(chǔ)概念與理論,這有助于進(jìn)一步的理解并發(fā)編程和寫出正確有效的并發(fā)代碼。本文是作者自己對(duì)Java并發(fā)編程的一些基礎(chǔ)概念與理論的理解與總結(jié),不對(duì)之處,望指出,共勉。
進(jìn)程與線程
簡(jiǎn)而言之,進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的基本單位,而線程是操作系統(tǒng)進(jìn)行調(diào)度的基本單位,所有線程共享進(jìn)程所擁有的資源。
推薦閱讀:進(jìn)程和線程之由來(lái)
為什么需要并發(fā)
CPU的處理速度越來(lái)越快,核心越來(lái)越多,但I(xiàn)O的速度相對(duì)CPU來(lái)說(shuō)非常的緩慢(就像自行車與火箭)。CPU通過(guò)IO獲取數(shù)據(jù)進(jìn)行計(jì)算時(shí)經(jīng)常需要等待,導(dǎo)致利用率很低,不能發(fā)揮自身速度的優(yōu)勢(shì)(就像一名程序員,一天可以完成一百個(gè)需求,公司卻只給其配備一名產(chǎn)品經(jīng)理,每天提一個(gè)需求)。而并發(fā)則可以更好的利用CPU,CPU同時(shí)為多個(gè)線程提供計(jì)算,當(dāng)其中一個(gè)線程因IO等待時(shí)迅速切換至其他進(jìn)程或線程(就像公司為這名程序員配備多名產(chǎn)品經(jīng)理,不停的提需求,程序員不停的切換項(xiàng)目編寫代碼,充分壓榨其勞動(dòng)力)。
從另一種角度來(lái)說(shuō),并發(fā)其實(shí)是一種解耦合的策略,它幫助我們把做什么(目標(biāo))和什么時(shí)候做(時(shí)機(jī))分開。這樣做可以明顯改進(jìn)應(yīng)用程序的吞吐量(獲得更多的CPU調(diào)度時(shí)間)和結(jié)構(gòu)(程序有多個(gè)部分在協(xié)同工作)。
并發(fā)編程的優(yōu)勢(shì)
- 充分利用CPU(核心)的計(jì)算能力
- 提高程序的吞吐量,改善性能
并發(fā)編程的劣勢(shì)
- 并發(fā)在CPU有很多空閑時(shí)間時(shí)能明顯改進(jìn)程序的性能,但當(dāng)線程數(shù)量較多的時(shí)候,線程間頻繁的調(diào)度切換反而會(huì)讓系統(tǒng)的性能下降
- 編寫正確的并發(fā)程序是非常復(fù)雜的,即使對(duì)于很簡(jiǎn)單的問(wèn)題
- 測(cè)試并發(fā)程序是困難的,并發(fā)程序中的缺陷通常不易重現(xiàn)也不容易被發(fā)現(xiàn)
Java內(nèi)存模型
Java內(nèi)存模型(Java Memory Model,JMM) 是對(duì)Java并發(fā)編程中線程與內(nèi)存的關(guān)系的定義,即線程間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory) 中,每個(gè)線程都有一個(gè)私有的本地工作內(nèi)存(Local Memory),線程的本地內(nèi)存中存儲(chǔ)了該線程使用到的共享變量的副本(從主內(nèi)存復(fù)制而來(lái)),線程對(duì)該變量的所有讀/寫操作都必須在自己的本地內(nèi)存中進(jìn)行,不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方本地內(nèi)存中的變量,線程間變量值的傳遞需要通過(guò)與主內(nèi)存同步來(lái)完成。理解Java內(nèi)存模型,對(duì)于編寫正確的Java并發(fā)程序來(lái)說(shuō)至關(guān)重要。
all threads share the main memory. each thread uses a local working memory. refreshing local memory to/from main memory must comply to JMM rules.
推薦閱讀:The Java Language Specification 17.4. Memory Model 、 淺析JVM(二)運(yùn)行時(shí)數(shù)據(jù)區(qū) 、深入理解Java內(nèi)存模型
Java并發(fā)編程需要考慮的問(wèn)題
- 共享性
由Java內(nèi)存模型得知,共享變量是所有線程共享的,如果多個(gè)線程對(duì)共享變量同時(shí)進(jìn)行讀/寫操作程序可能會(huì)達(dá)不到預(yù)期的結(jié)果。當(dāng)然,如果每個(gè)線程操作的始終是各自本地工作內(nèi)存中的變量則不存在共享性問(wèn)題,比如通過(guò)方法參數(shù)傳入、使用局部變量、創(chuàng)建新的實(shí)例。有過(guò)Java Web開發(fā)經(jīng)驗(yàn)的人都知道,Servlet就是以單實(shí)例多線程的方式工作,和每個(gè)請(qǐng)求相關(guān)的數(shù)據(jù)都是通過(guò)Servlet的service
方法(或者是doGet
或doPost
方法)的參數(shù)傳入的。只要Servlet中的代碼只使用局部變量,Servlet就不會(huì)導(dǎo)致同步問(wèn)題。Spring MVC的控制器也是這么做的,從請(qǐng)求中獲得的對(duì)象都是以方法的參數(shù)傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)實(shí)例。
- 互斥性
互斥性指的是同一時(shí)間只允許一個(gè)線程對(duì)共享變量進(jìn)行操作,以保證線程安全,具有唯一性和排它性。在Java 中通常用鎖來(lái)保證共享變量的互斥性,為了提高效率通常允許多個(gè)線程同時(shí)對(duì)共享變量進(jìn)行讀操作,但同一時(shí)間內(nèi)只允許一個(gè)線程對(duì)其進(jìn)行寫操作,所以鎖又分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。對(duì)于使用不變模式(被 final
修飾)的“變量”,則無(wú)需關(guān)心互斥性,因?yàn)槠渲辉试S線程對(duì)其進(jìn)行讀操作。(不變模式也是Java并發(fā)編程時(shí)可以考慮的一種設(shè)計(jì)。讓對(duì)象的狀態(tài)是不變的,如果希望修改對(duì)象的狀態(tài),就會(huì)創(chuàng)建對(duì)象的副本并將改變寫入副本而不改變?cè)瓉?lái)的對(duì)象,這樣就不會(huì)出現(xiàn)狀態(tài)不一致的情況,因此不變對(duì)象是線程安全的。Java中我們使用頻率極高的String
類就采用了這樣的設(shè)計(jì))
- 原子性
原子性指的是對(duì)共享變量的操作是一個(gè)獨(dú)立的、不可分割的整體。換句話說(shuō),就是一次操作,是一個(gè)連續(xù)不可中斷的過(guò)程,共享變量的值不會(huì)執(zhí)行到一半的時(shí)候被其他線程所修改。比如,我們經(jīng)常使用的整數(shù)i++
的操作,其實(shí)需要分成三個(gè)步驟:(1)讀取整數(shù)i
的值;(2)對(duì)i
進(jìn)行加一操作;(3)將結(jié)果寫回主內(nèi)存。在多線程下該操作便會(huì)出現(xiàn)原子性問(wèn)題,不能獲得預(yù)期的值。
- 可見性
可見性指的是當(dāng)一個(gè)線程對(duì)共享變量進(jìn)行更改后,其他線程對(duì)更改后的值是可見的(立即對(duì)主內(nèi)存進(jìn)行同步)。Java提供了volatile
關(guān)鍵字來(lái)保證可見性。當(dāng)一個(gè)共享變量被volatile
修飾時(shí),它會(huì)保證線程工作內(nèi)存中修改的值會(huì)立即被更新到主內(nèi)存中,其他線程也會(huì)將主內(nèi)存中的新值同步至工作內(nèi)存,需要注意的是volatile
并不能保證原子性。
如上圖所示,如果可見性得到保證,那么當(dāng)線程1將X的值更改為2時(shí),線程2內(nèi)的X值也將同步為2,否則線程2內(nèi)的X值仍為1。
- 有序性
為了提高性能,編譯器和處理器可能會(huì)對(duì)指令做重排序,重排序通常可以分為下面三種
- 編譯級(jí)別的重排序,比如編譯器的優(yōu)化
- 指令級(jí)重排序,比如CPU指令執(zhí)行的重排序
- 內(nèi)存系統(tǒng)的重排序,比如緩存和讀寫緩沖區(qū)導(dǎo)致的重排序
有序性指的就是在多線程并發(fā)的情況下,代碼實(shí)際執(zhí)行的順序、結(jié)果和單線程是一樣的,不會(huì)因?yàn)橹嘏判虻膯?wèn)題導(dǎo)致結(jié)果不可預(yù)知。
<small>注:水平有限,可能理解的不夠透徹,有興趣的可以看看 Doug Lea:Synchronization and the Java Memory Model,我想沒(méi)有誰(shuí)比他理解的更透徹了。</small>
Java中創(chuàng)建線程的方式
- 方式一,繼承
java.lang.Thread
類
public static void method1() {
class Task extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Started");
}
}
new Task().start();
}
- 方式二,實(shí)現(xiàn)
java.lang.Runnable
接口(推薦)
public static void method2() {
class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Started");
}
}
new Thread(new Task()).start();
}
- 方式三,實(shí)現(xiàn)
java.util.concurrent.Callable
接口(推薦)
public static void method3() {
class Task implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + " Started");
//求和
return 1 + 1;
}
}
ExecutorService es = Executors.newFixedThreadPool(1);
Future future = es.submit(new Task());
try {
System.out.println("Calculate Completed Sum:" + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
es.shutdown();
}