前言
學(xué)習(xí)Java并發(fā)編程,首先要搞清楚一些基礎(chǔ)概念與理論,這有助于進一步的理解并發(fā)編程和寫出正確有效的并發(fā)代碼。本文是作者自己對Java并發(fā)編程的一些基礎(chǔ)概念與理論的理解與總結(jié),不對之處狭姨,望指出,共勉苏遥。
進程與線程
簡而言之饼拍,進程是操作系統(tǒng)進行資源分配的基本單位,而線程是操作系統(tǒng)進行調(diào)度的基本單位田炭,所有線程共享進程所擁有的資源师抄。
推薦閱讀:進程和線程之由來
為什么需要并發(fā)
CPU的處理速度越來越快,核心越來越多教硫,但IO的速度相對CPU來說非常的緩慢(就像自行車與火箭)叨吮。CPU通過IO獲取數(shù)據(jù)進行計算時經(jīng)常需要等待,導(dǎo)致利用率很低瞬矩,不能發(fā)揮自身速度的優(yōu)勢(就像一名程序員茶鉴,一天可以完成一百個需求,公司卻只給其配備一名產(chǎn)品經(jīng)理景用,每天提一個需求)涵叮。而并發(fā)則可以更好的利用CPU,CPU同時為多個線程提供計算伞插,當(dāng)其中一個線程因IO等待時迅速切換至其他進程或線程(就像公司為這名程序員配備多名產(chǎn)品經(jīng)理割粮,不停的提需求,程序員不停的切換項目編寫代碼媚污,充分壓榨其勞動力)舀瓢。
從另一種角度來說,并發(fā)其實是一種解耦合的策略耗美,它幫助我們把做什么(目標)和什么時候做(時機)分開京髓。這樣做可以明顯改進應(yīng)用程序的吞吐量(獲得更多的CPU調(diào)度時間)和結(jié)構(gòu)(程序有多個部分在協(xié)同工作)航缀。
并發(fā)編程的優(yōu)勢
- 充分利用CPU(核心)的計算能力
- 提高程序的吞吐量,改善性能
并發(fā)編程的劣勢
- 并發(fā)在CPU有很多空閑時間時能明顯改進程序的性能朵锣,但當(dāng)線程數(shù)量較多的時候,線程間頻繁的調(diào)度切換反而會讓系統(tǒng)的性能下降
- 編寫正確的并發(fā)程序是非常復(fù)雜的甸私,即使對于很簡單的問題
- 測試并發(fā)程序是困難的诚些,并發(fā)程序中的缺陷通常不易重現(xiàn)也不容易被發(fā)現(xiàn)
Java內(nèi)存模型
Java內(nèi)存模型(Java Memory Model,JMM) 是對Java并發(fā)編程中線程與內(nèi)存的關(guān)系的定義皇型,即線程間的共享變量存儲在主內(nèi)存(Main Memory) 中诬烹,每個線程都有一個私有的本地工作內(nèi)存(Local Memory),線程的本地內(nèi)存中存儲了該線程使用到的共享變量的副本(從主內(nèi)存復(fù)制而來)弃鸦,線程對該變量的所有讀/寫操作都必須在自己的本地內(nèi)存中進行绞吁,不同的線程之間也無法直接訪問對方本地內(nèi)存中的變量,線程間變量值的傳遞需要通過與主內(nèi)存同步來完成唬格。理解Java內(nèi)存模型家破,對于編寫正確的Java并發(fā)程序來說至關(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(二)運行時數(shù)據(jù)區(qū) 汰聋、深入理解Java內(nèi)存模型
Java并發(fā)編程需要考慮的問題
- 共享性
由Java內(nèi)存模型得知,共享變量是所有線程共享的喊积,如果多個線程對共享變量同時進行讀/寫操作程序可能會達不到預(yù)期的結(jié)果烹困。當(dāng)然,如果每個線程操作的始終是各自本地工作內(nèi)存中的變量則不存在共享性問題乾吻,比如通過方法參數(shù)傳入髓梅、使用局部變量、創(chuàng)建新的實例绎签。有過Java Web開發(fā)經(jīng)驗的人都知道枯饿,Servlet就是以單實例多線程的方式工作,和每個請求相關(guān)的數(shù)據(jù)都是通過Servlet的service
方法(或者是doGet
或doPost
方法)的參數(shù)傳入的诡必。只要Servlet中的代碼只使用局部變量鸭你,Servlet就不會導(dǎo)致同步問題。Spring MVC的控制器也是這么做的擒权,從請求中獲得的對象都是以方法的參數(shù)傳入而不是作為類的成員袱巨,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個請求對應(yīng)一個實例碳抄。
- 互斥性
互斥性指的是同一時間只允許一個線程對共享變量進行操作愉老,以保證線程安全,具有唯一性和排它性剖效。在Java 中通常用鎖來保證共享變量的互斥性嫉入,為了提高效率通常允許多個線程同時對共享變量進行讀操作焰盗,但同一時間內(nèi)只允許一個線程對其進行寫操作,所以鎖又分為共享鎖和排它鎖咒林,也叫做讀鎖和寫鎖熬拒。對于使用不變模式(被 final
修飾)的“變量”,則無需關(guān)心互斥性垫竞,因為其只允許線程對其進行讀操作澎粟。(不變模式也是Java并發(fā)編程時可以考慮的一種設(shè)計。讓對象的狀態(tài)是不變的欢瞪,如果希望修改對象的狀態(tài)活烙,就會創(chuàng)建對象的副本并將改變寫入副本而不改變原來的對象,這樣就不會出現(xiàn)狀態(tài)不一致的情況遣鼓,因此不變對象是線程安全的啸盏。Java中我們使用頻率極高的String
類就采用了這樣的設(shè)計)
- 原子性
原子性指的是對共享變量的操作是一個獨立的、不可分割的整體骑祟。換句話說回懦,就是一次操作,是一個連續(xù)不可中斷的過程次企,共享變量的值不會執(zhí)行到一半的時候被其他線程所修改粉怕。比如,我們經(jīng)常使用的整數(shù)i++
的操作抒巢,其實需要分成三個步驟:(1)讀取整數(shù)i
的值贫贝;(2)對i
進行加一操作;(3)將結(jié)果寫回主內(nèi)存蛉谜。在多線程下該操作便會出現(xiàn)原子性問題稚晚,不能獲得預(yù)期的值。
- 可見性
可見性指的是當(dāng)一個線程對共享變量進行更改后型诚,其他線程對更改后的值是可見的(立即對主內(nèi)存進行同步)客燕。Java提供了volatile
關(guān)鍵字來保證可見性。當(dāng)一個共享變量被volatile
修飾時狰贯,它會保證線程工作內(nèi)存中修改的值會立即被更新到主內(nèi)存中也搓,其他線程也會將主內(nèi)存中的新值同步至工作內(nèi)存,需要注意的是volatile
并不能保證原子性涵紊。
如上圖所示傍妒,如果可見性得到保證,那么當(dāng)線程1將X的值更改為2時摸柄,線程2內(nèi)的X值也將同步為2颤练,否則線程2內(nèi)的X值仍為1。
- 有序性
為了提高性能驱负,編譯器和處理器可能會對指令做重排序嗦玖,重排序通郴脊停可以分為下面三種
- 編譯級別的重排序,比如編譯器的優(yōu)化
- 指令級重排序宇挫,比如CPU指令執(zhí)行的重排序
- 內(nèi)存系統(tǒng)的重排序苛吱,比如緩存和讀寫緩沖區(qū)導(dǎo)致的重排序
有序性指的就是在多線程并發(fā)的情況下,代碼實際執(zhí)行的順序器瘪、結(jié)果和單線程是一樣的翠储,不會因為重排序的問題導(dǎo)致結(jié)果不可預(yù)知。
<small>注:水平有限娱局,可能理解的不夠透徹彰亥,有興趣的可以看看 Doug Lea:Synchronization and the Java Memory Model咧七,我想沒有誰比他理解的更透徹了衰齐。</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();
}
- 方式二继阻,實現(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();
}
- 方式三耻涛,實現(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();
}