新聞中心
這篇文章,大部分內(nèi)容,是周五我做的一個關(guān)于如何進行Java多線程編程的Knowledge Sharing的一個整理,我希望能對Java從第一個版本開始,在多線程編程方面的大事件和發(fā)展脈絡(luò)有一個描述,并且提及一些在多線程編程方面常見的問題。對于Java程序員來說,如果從歷史的角度去了解一門語言一個特性的演進,或許能有不同收獲。

成都網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)建站!專注于網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、小程序制作、集團企業(yè)網(wǎng)站設(shè)計等服務(wù)項目。核心團隊均擁有互聯(lián)網(wǎng)行業(yè)多年經(jīng)驗,服務(wù)眾多知名企業(yè)客戶;涵蓋的客戶類型包括:酒店設(shè)計等眾多領(lǐng)域,積累了大量豐富的經(jīng)驗,同時也獲得了客戶的一致表揚!
引言
首先問這樣一個問題,如果提到Java多線程編程,你會想到什么?
- volatile、synchronized關(guān)鍵字?
- 競爭和同步?
- 鎖機制?
- 線程安全問題?
- 線程池和隊列?
好吧,請原諒我在這里賣的關(guān)子,其實這些都對,但是又不足夠全面,如果我們這樣來談?wù)揓ava多線程會不會全面一些:
- 模型:JMM(Java內(nèi)存模型)和JCM(Java并發(fā)模型)
- 使用:JDK中的并發(fā)包
- 實踐:怎樣寫線程安全的代碼
- 除錯:使用工具來分析并發(fā)問題
- ……
可是,這未免太死板了,不是么?
不如換一個思路,我們少談一些很容易查到的語法,不妨從歷史的角度看看Java在多線程編程方面是怎樣進化的,這個過程中,它做了哪些正確的決定,犯了哪些錯誤,未來又會有怎樣的發(fā)展趨勢?
另外,還有一點要說是,我希望通過大量的實例代碼來說明這些事情。Linus說:“Talk is cheap, show me the code.”。下文涉及到的代碼我已經(jīng)上傳,可以在此打包下載。
誕生
Java的基因來自于1990年12月Sun公司的一個內(nèi)部項目,目標(biāo)設(shè)備正是家用電器,但是C++的可移植性和API的易用性都讓程序員反感。旨在解決這樣的問題,于是又了Java的前身Oak語言,但是知道1995年3月,它正式更名為Java,才算Java語言真正的誕生。
JDK 1.0
1996年1月的JDK1.0版本,從一開始就確立了Java最基礎(chǔ)的線程模型,并且,這樣的線程模型再后續(xù)的修修補補中,并未發(fā)生實質(zhì)性的變更,可以說是一個具有傳承性的良好設(shè)計。
搶占式和協(xié)作式是兩種常見的進程/線程調(diào)度方式,操作系統(tǒng)非常適合使用搶占式方式來調(diào)度它的進程,它給不同的進程分配時間片,對于長期無響應(yīng)的進程,它有能力剝奪它的資源,甚至將其強行停止(如果采用協(xié)作式的方式,需要進程自覺、主動地釋放資源,也許就不知道需要等到什么時候了)。Java語言一開始就采用協(xié)作式的方式,并且在后面發(fā)展的過程中,逐步廢棄掉了粗暴的stop/resume/suspend這樣的方法,它們是違背協(xié)作式的不良設(shè)計,轉(zhuǎn)而采用wait/notify/sleep這樣的兩邊線程配合行動的方式。
一種線程間的通信方式是使用中斷:
- public class InterruptCheck extends Thread {
- @Override
- public void run() {
- System.out.println("start");
- while (true)
- if (Thread.currentThread().isInterrupted())
- break;
- System.out.println("while exit");
- }
- public static void main(String[] args) {
- Thread thread = new InterruptCheck();
- thread.start();
- try {
- sleep(2000);
- } catch (InterruptedException e) {
- }
- thread.interrupt();
- }
- }
這是中斷的一種使用方式,看起來就像是一個標(biāo)志位,線程A設(shè)置這個標(biāo)志位,線程B時不時地檢查這個標(biāo)志位。另外還有一種使用中斷通信的方式,如下:
- public class InterruptWait extends Thread {
- public static Object lock = new Object();
- @Override
- public void run() {
- System.out.println("start");
- synchronized (lock) {
- try {
- lock.wait();
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().isInterrupted());
- Thread.currentThread().interrupt(); // set interrupt flag again
- System.out.println(Thread.currentThread().isInterrupted());
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) {
- Thread thread = new InterruptWait();
- thread.start();
- try {
- sleep(2000);
- } catch (InterruptedException e) {
- }
- thread.interrupt();
- }
- }
在這種方式下,如果使用wait方法處于等待中的線程,被另一個線程使用中斷喚醒,于是拋出InterruptedException,同時,中斷標(biāo)志清除,這時候我們通常會在捕獲該異常的地方重新設(shè)置中斷,以便后續(xù)的邏輯通過檢查中斷狀態(tài)來了解該線程是如何結(jié)束的。
在比較穩(wěn)定的JDK 1.0.2版本中,已經(jīng)可以找到Thread和ThreadUsage這樣的類,這也是線程模型中最核心的兩個類。整個版本只包含了這樣幾個包:java.io、 java.util、java.net、java.awt和java.applet,所以說Java從一開始這個非常原始的版本就確立了一個持久的線程模型。
值得一提的是,在這個版本中,原子對象AtomicityXXX已經(jīng)設(shè)計好了,這里給出一個例子,說明i++這種操作時非原子的,而使用原子對象可以保證++操作的原子性:
- import java.util.concurrent.atomic.AtomicInteger;
- public class Atomicity {
- private static volatile int nonAtomicCounter = 0;
- private static volatile AtomicInteger atomicCounter = new AtomicInteger(0);
- private static int times = 0;
- public static void caculate() {
- times++;
- for (int i = 0; i < 1000; i++) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- nonAtomicCounter++;
- atomicCounter.incrementAndGet();
- }
- }).start();
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- }
- }
- public static void main(String[] args) {
- caculate();
- while (nonAtomicCounter == 1000) {
- nonAtomicCounter = 0;
- atomicCounter.set(0);
- caculate();
- }
- System.out.println("Non-atomic counter: " + times + ":"
- + nonAtomicCounter);
- System.out.println("Atomic counter: " + times + ":" + atomicCounter);
- }
- }
上面這個例子你也許需要跑幾次才能看到效果,使用非原子性的++操作,結(jié)果經(jīng)常小于1000。
對于鎖的使用,網(wǎng)上可以找到各種說明,但表述都不夠清晰。請看下面的代碼:
- public class Lock {
- private static Object o = new Object();
- static Lock lock = new Lock();
- // lock on dynamic method
- public synchronized void dynamicMethod() {
- System.out.println("dynamic method");
- sleepSilently(2000);
- }
- // lock on static method
- public static synchronized void staticMethod() {
- System.out.println("static method");
- sleepSilently(2000);
- }
- // lock on this
- public void thisBlock() {
- synchronized (this) {
- System.out.println("this block");
- sleepSilently(2000);
- }
- }
- // lock on an object
- public void objectBlock() {
- synchronized (o) {
- System.out.println("dynamic block");
- sleepSilently(2000);
- }
- }
- // lock on the class
- public static void classBlock() {
- synchronized (Lock.class) {
- System.out.println("static block");
- sleepSilently(2000);
- }
- }
- private static void sleepSilently(long millis) {
- try {
- Thread.sleep(millis);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) {
- // object lock test
- new Thread() {
- @Override
- public void run() {
- lock.dynamicMethod();
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- lock.thisBlock();
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- lock.objectBlock();
- }
- }.start();
- sleepSilently(3000);
- System.out.println();
- // class lock test
- new Thread() {
- @Override
- public void run() {
- lock.staticMethod();
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- lock.classBlock();
- }
- }.start();
- }
- }
上面的例子可以反映對一個鎖競爭的現(xiàn)象,結(jié)合上面的例子,理解下面這兩條,就可以很容易理解synchronized關(guān)鍵字的使用:
- 非靜態(tài)方法使用synchronized修飾,相當(dāng)于synchronized(this)。
- 靜態(tài)方法使用synchronized修飾,相當(dāng)于synchronized(Lock.class)。
#p#
JDK 1.2
1998年年底的JDK1.2版本正式把Java劃分為J2EE/J2SE/J2ME三個不同方向。在這個版本中,Java試圖用Swing修正在 AWT中犯的錯誤,例如使用了太多的同步??上У氖牵琂ava本身決定了AWT還是Swing性能和響應(yīng)都難以令人滿意,這也是Java桌面應(yīng)用難以比及其服務(wù)端應(yīng)用的一個原因,在IBM后來的SWT,也不足以令人滿意,JDK在這方面到JDK 1.2后似乎反省了自己,停下腳步了。值得注意的是,JDK高版本修復(fù)低版本問題的時候,通常遵循這樣的原則:
- 向下兼容。所以往往能看到很多重新設(shè)計的新增的包和類,還能看到deprecated的類和方法,但是它們并不能輕易被刪除。
- 嚴(yán)格遵循JLS(Java Language Specification),并把通過的新JSR(Java Specification Request)補充到JLS中,因此這個文檔本身也是向下兼容的,后面的版本只能進一步說明和特性增強,對于一些最初擴展性比較差的設(shè)計,也會無能為力。這個在下文中關(guān)于ReentrantLock的介紹中也可以看到。
在這個版本中,正式廢除了這樣三個方法:stop()、suspend()和resume()。下面我就來介紹一下,為什么它們要被廢除:
- public class Stop extends Thread {
- @Override
- public void run() {
- try {
- while (true)
- ;
- } catch (Throwable e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) {
- Thread thread = new Stop();
- thread.start();
- try {
- sleep(1000);
- } catch (InterruptedException e) {
- }
- thread.stop(new Exception("stop")); // note the stack trace
- }
- }
從上面的代碼你應(yīng)該可以看出兩件事情:
- 使用stop來終止一個線程是不講道理、極其殘暴的,不論目標(biāo)線程在執(zhí)行任何語句,一律強行終止線程,最終將導(dǎo)致一些殘缺的對象和不可預(yù)期的問題產(chǎn)生。
- 被終止的線程沒有任何異常拋出,你在線程終止后找不到任何被終止時執(zhí)行的代碼行,或者是堆棧信息(上面代碼打印的異常僅僅是main線程執(zhí)行stop語句的異常而已,并非被終止的線程)。
很難想象這樣的設(shè)計出自一個連指針都被廢掉的類型安全的編程語言,對不對?再來看看suspend的使用,有引起死鎖的隱患:
- public class Suspend extends Thread {
- @Override
- public void run() {
- synchronized (this) {
- while (true)
- ;
- }
- }
- public static void main(String[] args) {
- Thread thread = new Suspend();
- thread.start();
- try {
- sleep(1000);
- } catch (InterruptedException e) {
- }
- thread.suspend();
- synchronized (thread) { // dead lock
- System.out.println("got the lock");
- thread.resume();
- }
- }
- }
從上面的代碼可以看出,Suspend線程被掛起時,依然占有鎖,而當(dāng)main線程期望去獲取該線程來喚醒它時,徹底癱瘓了。由于suspend在這里是無期限限制的,這會變成一個徹徹底底的死鎖。
相反,看看這三個方法的改進品和替代品:wait()、notify()和sleep(),它們令線程之間的交互就友好得多:
- public class Wait extends Thread {
- @Override
- public void run() {
- System.out.println("start");
- synchronized (this) { // wait/notify/notifyAll use the same
- // synchronization resource
- try {
- this.wait();
- } catch (InterruptedException e) {
- e.printStackTrace(); // notify won't throw exception
- }
- }
- }
- public static void main(String[] args) {
- Thread thread = new Wait();
- thread.start();
- try {
- sleep(2000);
- } catch (InterruptedException e) {
- }
- synchronized (thread) {
- System.out.println("Wait() will release the lock!");
- thread.notify();
- }
- }
- }
在wait和notify搭配使用的過程中,注意需要把它們鎖定到同一個資源上(例如對象a),即:
- 一個線程中synchronized(a),并在同步塊中執(zhí)行a.wait()
- 另一個線程中synchronized(a),并在同步塊中執(zhí)行a.notify()
再來看一看sleep方法的使用,回答下面兩個問題:
- 和wait比較一下,為什么sleep被設(shè)計為Thread的一個靜態(tài)方法(即只讓當(dāng)前線程sleep)?
- 為什么sleep必須要傳入一個時間參數(shù),而不允許不限期地sleep?
如果我前面說的你都理解了,你應(yīng)該能回答這兩個問題。
- public class Sleep extends Thread {
- @Override
- public void run() {
- System.out.println("start");
- synchronized (this) { // sleep() can use (or not) any synchronization resource
- try {
- /**
- * Do you know:
- * 1. Why sleep() is designed as a static method comparing with
- * wait?
- * 2. Why sleep() must have a timeout parameter?
- */
- this.sleep(10000);
- } catch (InterruptedException e) {
- e.printStackTrace(); // notify won't throw exception
- }
- }
- }
- public static void main(String[] args) {
- Thread thread = new Sleep();
- thread.start();
- try {
- sleep(2000);
- } catch (InterruptedException e) {
- }
- synchronized (thread) {
- System.out.println("Has sleep() released the lock!");
- thread.notify();
- }
- }
- }
在這個JDK版本中,引入線程變量ThreadLocal這個類:
每一個線程都掛載了一個ThreadLocalMap。ThreadLocal這個類的使用很有意思,get方法沒有key傳入,原因就在于這個 key就是當(dāng)前你使用的這個ThreadLocal它自己。ThreadLocal的對象生命周期可以伴隨著整個線程的生命周期。因此,倘若在線程變量里存放持續(xù)增長的對象(最常見是一個不受良好管理的map),很容易導(dǎo)致內(nèi)存泄露。
- public class ThreadLocalUsage extends Thread {
- public User user = new User();
- public User getUser() {
- return user;
- }
- @Override
- public void run() {
- this.user.set("var1");
- while (true) {
- try {
- sleep(1000);
- } catch (InterruptedException e) {
- }
- System.out.println(this.user.get());
- }
- }
- public static void main(String[] args) {
- ThreadLocalUsage thread = new ThreadLocalUsage();
- thread.start();
- try {
- sleep(4000);
- } catch (InterruptedException e) {
- }
- thread.user.set("var2");
- }
- }
- class User {
- private static ThreadLocal
- public void set(Object object) {
- enclosure.set(object);
- }
- public Object get() {
- return enclosure.get();
- }
- }
上面的例子會一直打印var1,而不會打印var2,就是因為不同線程中的ThreadLocal是互相獨立的。
用jstack工具可以找到鎖相關(guān)的信息,如果線程占有鎖,但是由于執(zhí)行到wait方法時處于wait狀態(tài)暫時釋放了鎖,會打印waiting on的信息:
- "Thread-0" prio=6 tid=0x02bc4400 nid=0xef44 in Object.wait() [0x02f0f000]
- java.lang.Thread.State: WAITING (on object monitor)
- at java.lang.Object.wait(Native Method)
- - waiting on <0x22a7c3b8> (a Wait)
- at java.lang.Object.wait(Object.java:485)
- at Wait.run(Wait.java:8)
- - locked <0x22a7c3b8> (a Wait)
如果程序持續(xù)占有某個鎖(例如sleep方法在sleep期間不會釋放鎖),會打印locked的信息:
- "Thread-0" prio=6 tid=0x02baa800 nid=0x1ea4 waiting on condition [0x02f0f000]
- java.lang.Thread.State: TIMED_WAITING (sleeping)
- at java.lang.Thread.sleep(Native Method)
- at Wait.run(Wait.java:8)
- - locked <0x22a7c398> (a Wait)
而如果是線程希望進入某同步塊,而在等待鎖的釋放,會打印waiting to的信息:
- "main" prio=6 tid=0x00847400 nid=0xf984 waiting for monitor entry [0x0092f000]
- java.lang.Thread.State: BLOCKED (on object monitor)
- at Wait.main(Wait.java:23)
- - waiting to lock <0x22a7c398> (a Wait)
#p#
JDK 1.4
在2002年4月發(fā)布的JDK1.4中,正式引入了NIO。JDK在原有標(biāo)準(zhǔn)IO的基礎(chǔ)上,提供了一組多路復(fù)用IO的解決方案。
通過在一個Selector上掛接多個Channel,通過統(tǒng)一的輪詢線程檢測,每當(dāng)有數(shù)據(jù)到達(dá),觸發(fā)監(jiān)聽事件,將事件分發(fā)出去,而不是讓每一個channel長期消耗阻塞一個線程等待數(shù)據(jù)流到達(dá)。所以,只有在對資源爭奪劇烈的高并發(fā)場景下,才能見到NIO的明顯優(yōu)勢。
相較于面向流的傳統(tǒng)方式這種面向塊的訪問方式會丟失一些簡易性和靈活性。下面給出一個NIO接口讀取文件的簡單例子(僅示意用):
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.channels.FileChannel;
- public class NIO {
- public static void nioRead(String file) throws IOException {
- FileInputStream in = new FileInputStream(file);
- FileChannel channel = in.getChannel();
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- channel.read(buffer);
- byte[] b = buffer.array();
- System.out.println(new String(b));
- channel.close();
- }
- }
JDK 5.0
2004年9月起JDK 1.5發(fā)布,并正式更名到5.0。有個笑話說,軟件行業(yè)有句話,叫做“不要用3.0版本以下的軟件”,意思是說版本太小的話往往軟件質(zhì)量不過關(guān)——但是按照這種說法,JDK的原有版本命名方式得要到啥時候才有3.0啊,于是1.4以后通過版本命名方式的改變直接升到5.0了。
JDK 5.0不只是版本號命名方式變更那么簡單,對于多線程編程來說,這里發(fā)生了兩個重大事件,JSR 133和JSR 166的正式發(fā)布。
JSR 133
JSR 133重新明確了Java內(nèi)存模型,事實上,在這之前,常見的內(nèi)存模型包括連續(xù)一致性內(nèi)存模型和先行發(fā)生模型。
對于連續(xù)一致性模型來說,程序執(zhí)行的順序和代碼上顯示的順序是完全一致的。這對于現(xiàn)代多核,并且指令執(zhí)行優(yōu)化的CPU來說,是很難保證的。而且,順序一致性的保證將JVM對代碼的運行期優(yōu)化嚴(yán)重限制住了。
但是JSR 133指定的先行發(fā)生(Happens-before)使得執(zhí)行指令的順序變得靈活:
- 在同一個線程里面,按照代碼執(zhí)行的順序(也就是代碼語義的順序),前一個操作先于后面一個操作發(fā)生
- 對一個monitor對象的解鎖操作先于后續(xù)對同一個monitor對象的鎖操作
- 對volatile字段的寫操作先于后面的對此字段的讀操作
- 對線程的start操作(調(diào)用線程對象的start()方法)先于這個線程的其他任何操作
- 一個線程中所有的操作先于其他任何線程在此線程上調(diào)用 join()方法
- 如果A操作優(yōu)先于B,B操作優(yōu)先于C,那么A操作優(yōu)先于C
而在內(nèi)存分配上,將每個線程各自的工作內(nèi)存(甚至包括)從主存中獨立出來,更是給JVM大量的空間來優(yōu)化線程內(nèi)指令的執(zhí)行。主存中的變量可以被拷貝到線程的工作內(nèi)存中去單獨執(zhí)行,在執(zhí)行結(jié)束后,結(jié)果可以在某個時間刷回主存:
但是,怎樣來保證各個線程之間數(shù)據(jù)的一致性?JLS給的辦法就是,默認(rèn)情況下,不能保證任意時刻的數(shù)據(jù)一致性,但是通過對 synchronized、volatile和final這幾個語義被增強的關(guān)鍵字的使用,可以做到數(shù)據(jù)一致性。要解釋這個問題,不如看一看經(jīng)典的 DCL(Double Check Lock)問題:
- public class DoubleCheckLock {
- private volatile static DoubleCheckLock instance; // Do I need add "volatile" here?
- private final Element element = new Element(); // Should I add "final" here? Is a "final" enough here? Or I should use "volatile"?
- private DoubleCheckLock() {
- }
- public static DoubleCheckLock getInstance() {
- if (null == instance)
- synchronized (DoubleCheckLock.class) {
- if (null == instance)
- instance = new DoubleCheckLock();
- //the writes which initialize instance and the write to the instance field can be reordered without "volatile"
- }
- return instance;
- }
- public Element getElement() {
- return element;
- }
- }
- class Element {
- public String name = new String("abc");
- }
在上面這個例子中,如果不對instance聲明的地方使用volatile關(guān)鍵字,JVM將不能保證getInstance方法獲取到的 instance是一個完整的、正確的instance,而volatile關(guān)鍵字保證了instance的可見性,即能夠保證獲取到當(dāng)時真實的 instance對象。
但是問題沒有那么簡單,對于上例中的element而言,如果沒有volatile和final修飾,element里的name也無法在前文所述的instance返回給外部時
新聞標(biāo)題:Java多線程發(fā)展簡史
標(biāo)題路徑:http://m.fisionsoft.com.cn/article/djseceg.html


咨詢
建站咨詢
