這篇文章,大部分內(nèi)容,是周五我做的一個(gè)關(guān)于如何進(jìn)行Java多線程編程的Knowledge Sharing的一個(gè)整理,我希望能對Java從第一個(gè)版本開始,在多線程編程方面的大事件和發(fā)展脈絡(luò)有一個(gè)描述,并且提及一些在多線程編程方面常見的問題。對于Java程序員來說,如果從歷史的角度去了解一門語言一個(gè)特性的演進(jìn),或許能有不同收獲。
引言
首先問這樣一個(gè)問題,如果提到Java多線程編程,你會想到什么?
- volatile、synchronized關(guān)鍵字?
- 競爭和同步?
- 鎖機(jī)制?
- 線程安全問題?
- 線程池和隊(duì)列?
好吧,請?jiān)徫以谶@里賣的關(guān)子,其實(shí)這些都對,但是又不足夠全面,如果我們這樣來談?wù)揓ava多線程會不會全面一些:
- 模型:JMM(Java內(nèi)存模型)和JCM(Java并發(fā)模型)
- 使用:JDK中的并發(fā)包
- 實(shí)踐:怎樣寫線程安全的代碼
- 除錯(cuò):使用工具來分析并發(fā)問題
- ……
可是,這未免太死板了,不是么?
不如換一個(gè)思路,我們少談一些很容易查到的語法,不妨從歷史的角度看看Java在多線程編程方面是怎樣進(jìn)化的,這個(gè)過程中,它做了哪些正確的決定,犯了哪些錯(cuò)誤,未來又會有怎樣的發(fā)展趨勢?
另外,還有一點(diǎn)要說是,我希望通過大量的實(shí)例代碼來說明這些事情。Linus說:“Talk is cheap, show me the code.”。下文涉及到的代碼我已經(jīng)上傳,可以在此打包下載。
誕生
Java的基因來自于1990年12月Sun公司的一個(gè)內(nèi)部項(xiàng)目,目標(biāo)設(shè)備正是家用電器,但是C++的可移植性和API的易用性都讓程序員反感。旨在解決這樣的問題,于是又了Java的前身Oak語言,但是知道1995年3月,它正式更名為Java,才算Java語言真正的誕生。
JDK 1.0
1996年1月的JDK1.0版本,從一開始就確立了Java最基礎(chǔ)的線程模型,并且,這樣的線程模型再后續(xù)的修修補(bǔ)補(bǔ)中,并未發(fā)生實(shí)質(zhì)性的變更,可以說是一個(gè)具有傳承性的良好設(shè)計(jì)。
搶占式和協(xié)作式是兩種常見的進(jìn)程/線程調(diào)度方式,操作系統(tǒng)非常適合使用搶占式方式來調(diào)度它的進(jìn)程,它給不同的進(jìn)程分配時(shí)間片,對于長期無響應(yīng)的進(jìn)程,它有能力剝奪它的資源,甚至將其強(qiáng)行停止(如果采用協(xié)作式的方式,需要進(jìn)程自覺、主動地釋放資源,也許就不知道需要等到什么時(shí)候了)。Java語言一開始就采用協(xié)作式的方式,并且在后面發(fā)展的過程中,逐步廢棄掉了粗暴的stop/resume/suspend這樣的方法,它們是違背協(xié)作式的不良設(shè)計(jì),轉(zhuǎn)而采用wait/notify/sleep這樣的兩邊線程配合行動的方式。
一種線程間的通信方式是使用中斷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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();
}
}
|
這是中斷的一種使用方式,看起來就像是一個(gè)標(biāo)志位,線程A設(shè)置這個(gè)標(biāo)志位,線程B時(shí)不時(shí)地檢查這個(gè)標(biāo)志位。另外還有一種使用中斷通信的方式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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方法處于等待中的線程,被另一個(gè)線程使用中斷喚醒,于是拋出InterruptedException,同時(shí),中斷標(biāo)志清除,這時(shí)候我們通常會在捕獲該異常的地方重新設(shè)置中斷,以便后續(xù)的邏輯通過檢查中斷狀態(tài)來了解該線程是如何結(jié)束的。
在比較穩(wěn)定的JDK 1.0.2版本中,已經(jīng)可以找到Thread和ThreadUsage這樣的類,這也是線程模型中最核心的兩個(gè)類。整個(gè)版本只包含了這樣幾個(gè)包:java.io、 java.util、java.net、java.awt和java.applet,所以說Java從一開始這個(gè)非常原始的版本就確立了一個(gè)持久的線程模型。
值得一提的是,在這個(gè)版本中,原子對象AtomicityXXX已經(jīng)設(shè)計(jì)好了,這里給出一個(gè)例子,說明i++這種操作時(shí)非原子的,而使用原子對象可以保證++操作的原子性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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);
}
}
|
上面這個(gè)例子你也許需要跑幾次才能看到效果,使用非原子性的++操作,結(jié)果經(jīng)常小于1000。
對于鎖的使用,網(wǎng)上可以找到各種說明,但表述都不夠清晰。請看下面的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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();
}
}
|
上面的例子可以反映對一個(gè)鎖競爭的現(xiàn)象,結(jié)合上面的例子,理解下面這兩條,就可以很容易理解synchronized關(guān)鍵字的使用:
- 非靜態(tài)方法使用synchronized修飾,相當(dāng)于synchronized(this)。
- 靜態(tài)方法使用synchronized修飾,相當(dāng)于synchronized(Lock.class)。
JDK 1.2
1998年年底的JDK1.2版本正式把Java劃分為J2EE/J2SE/J2ME三個(gè)不同方向。在這個(gè)版本中,Java試圖用Swing修正在AWT中犯的錯(cuò)誤,例如使用了太多的同步??上У氖?,Java本身決定了AWT還是Swing性能和響應(yīng)都難以令人滿意,這也是Java桌面應(yīng)用難以比及其服務(wù)端應(yīng)用的一個(gè)原因,在IBM后來的SWT,也不足以令人滿意,JDK在這方面到JDK 1.2后似乎反省了自己,停下腳步了。值得注意的是,JDK高版本修復(fù)低版本問題的時(shí)候,通常遵循這樣的原則:
- 向下兼容。所以往往能看到很多重新設(shè)計(jì)的新增的包和類,還能看到deprecated的類和方法,但是它們并不能輕易被刪除。
- 嚴(yán)格遵循JLS(Java Language Specification),并把通過的新JSR(Java Specification Request)補(bǔ)充到JLS中,因此這個(gè)文檔本身也是向下兼容的,后面的版本只能進(jìn)一步說明和特性增強(qiáng),對于一些最初擴(kuò)展性比較差的設(shè)計(jì),也會無能為力。這個(gè)在下文中關(guān)于ReentrantLock的介紹中也可以看到。
在這個(gè)版本中,正式廢除了這樣三個(gè)方法:stop()、suspend()和resume()。下面我就來介紹一下,為什么它們要被廢除:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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來終止一個(gè)線程是不講道理、極其殘暴的,不論目標(biāo)線程在執(zhí)行任何語句,一律強(qiáng)行終止線程,最終將導(dǎo)致一些殘缺的對象和不可預(yù)期的問題產(chǎn)生。
- 被終止的線程沒有任何異常拋出,你在線程終止后找不到任何被終止時(shí)執(zhí)行的代碼行,或者是堆棧信息(上面代碼打印的異常僅僅是main線程執(zhí)行stop語句的異常而已,并非被終止的線程)。
很難想象這樣的設(shè)計(jì)出自一個(gè)連指針都被廢掉的類型安全的編程語言,對不對?再來看看suspend的使用,有引起死鎖的隱患:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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線程被掛起時(shí),依然占有鎖,而當(dāng)main線程期望去獲取該線程來喚醒它時(shí),徹底癱瘓了。由于suspend在這里是無期限限制的,這會變成一個(gè)徹徹底底的死鎖。
相反,看看這三個(gè)方法的改進(jìn)品和替代品:wait()、notify()和sleep(),它們令線程之間的交互就友好得多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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搭配使用的過程中,注意需要把它們鎖定到同一個(gè)資源上(例如對象a),即:
- 一個(gè)線程中synchronized(a),并在同步塊中執(zhí)行a.wait()
- 另一個(gè)線程中synchronized(a),并在同步塊中執(zhí)行a.notify()
再來看一看sleep方法的使用,回答下面兩個(gè)問題:
- 和wait比較一下,為什么sleep被設(shè)計(jì)為Thread的一個(gè)靜態(tài)方法(即只讓當(dāng)前線程sleep)?
- 為什么sleep必須要傳入一個(gè)時(shí)間參數(shù),而不允許不限期地sleep?
如果我前面說的你都理解了,你應(yīng)該能回答這兩個(gè)問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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: <br>
* 1. Why sleep() is designed as a static method comparing with
* wait?<br>
* 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();
}
}
}
|
在這個(gè)JDK版本中,引入線程變量ThreadLocal這個(gè)類:
每一個(gè)線程都掛載了一個(gè)ThreadLocalMap。ThreadLocal這個(gè)類的使用很有意思,get方法沒有key傳入,原因就在于這個(gè)key就是當(dāng)前你使用的這個(gè)ThreadLocal它自己。ThreadLocal的對象生命周期可以伴隨著整個(gè)線程的生命周期。因此,倘若在線程變量里存放持續(xù)增長的對象(最常見是一個(gè)不受良好管理的map),很容易導(dǎo)致內(nèi)存泄露。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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<Object> enclosure = new ThreadLocal<Object>(); // is it must be static?
public void set(Object object) {
enclosure.set(object);
}
public Object get() {
return enclosure.get();
}
}
|
上面的例子會一直打印var1,而不會打印var2,就是因?yàn)椴煌€程中的ThreadLocal是互相獨(dú)立的。
用jstack工具可以找到鎖相關(guān)的信息,如果線程占有鎖,但是由于執(zhí)行到wait方法時(shí)處于wait狀態(tài)暫時(shí)釋放了鎖,會打印waiting on的信息:
1 2 3 4 5 6 7 | "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ù)占有某個(gè)鎖(例如sleep方法在sleep期間不會釋放鎖),會打印locked的信息:
1 2 3 4 5 | "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)
|
而如果是線程希望進(jìn)入某同步塊,而在等待鎖的釋放,會打印waiting to的信息:
1 2 3 4 | "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)
|
JDK 1.4
在2002年4月發(fā)布的JDK1.4中,正式引入了NIO。JDK在原有標(biāo)準(zhǔn)IO的基礎(chǔ)上,提供了一組多路復(fù)用IO的解決方案。
通過在一個(gè)Selector上掛接多個(gè)Channel,通過統(tǒng)一的輪詢線程檢測,每當(dāng)有數(shù)據(jù)到達(dá),觸發(fā)監(jiān)聽事件,將事件分發(fā)出去,而不是讓每一個(gè)channel長期消耗阻塞一個(gè)線程等待數(shù)據(jù)流到達(dá)。所以,只有在對資源爭奪劇烈的高并發(fā)場景下,才能見到NIO的明顯優(yōu)勢。
相較于面向流的傳統(tǒng)方式這種面向塊的訪問方式會丟失一些簡易性和靈活性。下面給出一個(gè)NIO接口讀取文件的簡單例子(僅示意用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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。有個(gè)笑話說,軟件行業(yè)有句話,叫做“不要用3.0版本以下的軟件”,意思是說版本太小的話往往軟件質(zhì)量不過關(guān)——但是按照這種說法,JDK的原有版本命名方式得要到啥時(shí)候才有3.0啊,于是1.4以后通過版本命名方式的改變直接升到5.0了。
JDK 5.0不只是版本號命名方式變更那么簡單,對于多線程編程來說,這里發(fā)生了兩個(gè)重大事件,JSR 133和JSR 166的正式發(fā)布。
JSR 133
JSR 133重新明確了Java內(nèi)存模型,事實(shí)上,在這之前,常見的內(nèi)存模型包括連續(xù)一致性內(nèi)存模型和先行發(fā)生模型。
對于連續(xù)一致性模型來說,程序執(zhí)行的順序和代碼上顯示的順序是完全一致的。這對于現(xiàn)代多核,并且指令執(zhí)行優(yōu)化的CPU來說,是很難保證的。而且,順序一致性的保證將JVM對代碼的運(yùn)行期優(yōu)化嚴(yán)重限制住了。
但是JSR 133指定的先行發(fā)生(Happens-before)使得執(zhí)行指令的順序變得靈活:
- 在同一個(gè)線程里面,按照代碼執(zhí)行的順序(也就是代碼語義的順序),前一個(gè)操作先于后面一個(gè)操作發(fā)生
- 對一個(gè)monitor對象的解鎖操作先于后續(xù)對同一個(gè)monitor對象的鎖操作
- 對volatile字段的寫操作先于后面的對此字段的讀操作
- 對線程的start操作(調(diào)用線程對象的start()方法)先于這個(gè)線程的其他任何操作
- 一個(gè)線程中所有的操作先于其他任何線程在此線程上調(diào)用 join()方法
- 如果A操作優(yōu)先于B,B操作優(yōu)先于C,那么A操作優(yōu)先于C
而在內(nèi)存分配上,將每個(gè)線程各自的工作內(nèi)存(甚至包括)從主存中獨(dú)立出來,更是給JVM大量的空間來優(yōu)化線程內(nèi)指令的執(zhí)行。主存中的變量可以被拷貝到線程的工作內(nèi)存中去單獨(dú)執(zhí)行,在執(zhí)行結(jié)束后,結(jié)果可以在某個(gè)時(shí)間刷回主存:
但是,怎樣來保證各個(gè)線程之間數(shù)據(jù)的一致性?JLS給的辦法就是,默認(rèn)情況下,不能保證任意時(shí)刻的數(shù)據(jù)一致性,但是通過對synchronized、volatile和final這幾個(gè)語義被增強(qiáng)的關(guān)鍵字的使用,可以做到數(shù)據(jù)一致性。要解釋這個(gè)問題,不如看一看經(jīng)典的DCL(Double Check Lock)問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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" );
}
|
在上面這個(gè)例子中,如果不對instance聲明的地方使用volatile關(guān)鍵字,JVM將不能保證getInstance方法獲取到的instance是一個(gè)完整的、正確的instance,而volatile關(guān)鍵字保證了instance的可見性,即能夠保證獲取到當(dāng)時(shí)真實(shí)的instance對象。
但是問題沒有那么簡單,對于上例中的element而言,如果沒有volatile和final修飾,element里的name也無法在前文所述的instance返回給外部時(shí)的可見性。如果element是不可變對象,使用final也可以保證它在構(gòu)造方法調(diào)用后的可見性。
【2014-1-3】=> 我要修正上面劃刪除線的這段話,就這里的對象發(fā)布本身而言,Element的那個(gè)“final”修飾符不加也是沒有問題的,具體的原因可以參見【這篇文章】。
對于volatile的效果,很多人都希望有一段簡短的代碼能夠看到,使用volatile和不使用volatile的情況下執(zhí)行結(jié)果的差別??上н@其實(shí)并不好找。這里我給出這樣一個(gè)不甚嚴(yán)格的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class Volatile {
public static void main(String[] args) {
final Volatile volObj = new Volatile();
Thread t2 = new Thread() {
public void run() {
while ( true ) {
volObj.check();
}
}
};
t2.start();
Thread t1 = new Thread() {
public void run() {
while ( true ) {
volObj.swap();
}
}
};
t1.start();
}
boolean boolValue; // use volatile to print "WTF!"
public void check() {
if (boolValue == !boolValue)
System.out.println( "WTF!" );
}
public void swap() {
try {
Thread.sleep( 100 );
} catch (InterruptedException e) {
e.printStackTrace();
}
boolValue = !boolValue;
}
}
|
代碼中存在兩個(gè)線程,一個(gè)線程通過一個(gè)死循環(huán)不斷在變換boolValue的取值;另一個(gè)線程每100毫秒執(zhí)行“boolValue==!boolValue”,這行代碼會取兩次boolValue,可以想象的是,有一定概率會出現(xiàn)這兩次取boolValue結(jié)果不一致的情況,那么這個(gè)時(shí)候就會打印“WTF!”。
但是,上面的情況是對boolValue使用volatile修飾保證其可見性的情況下出現(xiàn)的,如果不對boolValue使用volatile修飾,運(yùn)行時(shí)就一次不會出現(xiàn)(起碼在我的電腦上)打印“WTF!”的情形,換句話說,這反而是不太正常的,我無法猜測JVM做了什么操作,基本上唯一可以確定的是,沒有用volatile修飾的時(shí)候,boolValue在獲取的時(shí)候,并不能總?cè)〉阶钫鎸?shí)的值。
JSR 166
JSR 166的貢獻(xiàn)就是引入了java.util.concurrent這個(gè)包。前面曾經(jīng)講解過AtomicXXX類這種原子類型,內(nèi)部實(shí)現(xiàn)保證其原子性的其實(shí)是通過一個(gè)compareAndSet(x,y)方法(CAS),而這個(gè)方法追蹤到最底層,是通過CPU的一個(gè)單獨(dú)的指令來實(shí)現(xiàn)的。這個(gè)方法所做的事情,就是保證在某變量取值為x的情況下,將取值x替換為y。在這個(gè)過程中,并沒有任何加鎖的行為,所以一般它的性能要比使用synchronized高。
Lock-free算法就是基于CAS來實(shí)現(xiàn)原子化“set”的方式,通常有這樣兩種形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import java.util.concurrent.atomic.AtomicInteger;
public class LockFree {
private AtomicInteger max = new AtomicInteger();
// type A
public void setA( int value) {
while ( true ) { // 1.circulation
int currentValue = max.get();
if (value > currentValue) {
if (max.compareAndSet(currentValue, value)) // 2.CAS
break ; // 3.exit
} else
break ;
}
}
// type B
public void setB( int value) {
int currentValue;
do { // 1.circulation
currentValue = max.get();
if (value <= currentValue)
break ; // 3.exit
} while (!max.compareAndSet(currentValue, value)); // 2.CAS
}
}
|
不過,對CAS的使用并不總是正確的,比如ABA問題。我用下面這樣一個(gè)棧的例子來說明:
- 線程t1先查看了一下棧的情況,發(fā)現(xiàn)棧里面有A、B兩個(gè)元素,棧頂是A,這是它所期望的,它現(xiàn)在很想用CAS的方法把A pop出去。
- 這時(shí)候線程t2來了,它pop出A、B,又push一個(gè)C進(jìn)去,再把A push回去,這時(shí)候棧里面存放了A、C兩個(gè)元素,棧頂還是A。
- t1開始使用CAS:head.compareAndSet(A,B),把A pop出去了,棧里就剩下B了,可是這時(shí)候其實(shí)已經(jīng)發(fā)生了錯(cuò)誤,因?yàn)镃丟失了。
為什么會發(fā)生這樣的錯(cuò)誤?因?yàn)閷1來說,它兩次都查看到棧頂?shù)腁,以為期間沒有發(fā)生變化,而實(shí)際上呢?實(shí)際上已經(jīng)發(fā)生了變化,C進(jìn)來、B出去了,但是t1它只看棧頂是A,它并不知道曾經(jīng)發(fā)生了什么。
那么,有什么辦法可以解決這個(gè)問題呢?
最常見的辦法是使用一個(gè)計(jì)數(shù)器,對這個(gè)棧只要有任何的變化,就觸發(fā)計(jì)數(shù)器+1,t1在要查看A的狀態(tài),不如看一下計(jì)數(shù)器的情況,如果計(jì)數(shù)器沒有變化,說明期間沒有別人動過這個(gè)棧。JDK 5.0里面提供的AtomicStampedReference就是起這個(gè)用的。
使用immutable對象的拷貝(比如CopyOnWrite)也可以實(shí)現(xiàn)無鎖狀態(tài)下的并發(fā)訪問。舉一個(gè)簡單的例子,比如有這樣一個(gè)鏈表,每一個(gè)節(jié)點(diǎn)包含兩個(gè)值,現(xiàn)在我要把中間一個(gè)節(jié)點(diǎn)(2,3)替換成(4,5),不使用同步的話,我可以這樣實(shí)現(xiàn):
構(gòu)建一個(gè)新的節(jié)點(diǎn)連到節(jié)點(diǎn)(4,6)上,再將原有(1,1)到(2,3)的指針指向替換成(1,1)到(4,5)的指向。
除了這兩者,還有很多不用同步來實(shí)現(xiàn)原子操作的方法,比如我曾經(jīng)介紹過的Peterson算法。
以下這個(gè)表格顯示了JDK 5.0涉及到的常用容器:
其中:
- unsafe這一列的容器都是JDK之前版本有的,且非線程安全的;
- synchronized這一列的容器都是JDK之前版本有的,且通過synchronized的關(guān)鍵字同步方式來保證線程安全的;
- concurrent pkg一列的容器都是并發(fā)包新加入的容器,都是線程安全,但是都沒有使用同步來實(shí)現(xiàn)線程安全。
再說一下對于線程池的支持。在說線程池之前,得明確一下Future的概念。Future也是JDK 5.0新增的類,是一個(gè)用來整合同步和異步的結(jié)果對象。一個(gè)異步任務(wù)的執(zhí)行通過Future對象立即返回,如果你期望以同步方式獲取結(jié)果,只需要調(diào)用它的get方法,直到結(jié)果取得才會返回給你,否則線程會一直hang在那里。Future可以看做是JDK為了它的線程模型做的一個(gè)部分修復(fù),因?yàn)槌绦騿T以往在考慮多線程的時(shí)候,并不能夠以面向?qū)ο蟮乃悸啡ネ瓿伤?,而不得不考慮很多面向線程的行為,但是Future和后面要講到的Barrier等類,可以讓這些特定情況下,程序員可以從繁重的線程思維中解脫出來。把線程控制的部分和業(yè)務(wù)邏輯的部分解耦開。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureUsage {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Object> task = new Callable<Object>() {
public Object call() throws Exception {
Thread.sleep( 4000 );
Object result = "finished" ;
return result;
}
};
Future<Object> future = executor.submit(task);
System.out.println( "task submitted" );
try {
System.out.println(future.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
// Thread won't be destroyed.
}
}
|
上面的代碼是一個(gè)最簡單的線程池使用的例子,線程池接受提交上來的任務(wù),分配給池中的線程去執(zhí)行。對于任務(wù)壓力的情況,JDK中一個(gè)功能完備的線程池具備這樣的優(yōu)先級處理策略:
- 請求到來首先交給coreSize內(nèi)的常駐線程執(zhí)行
- 如果coreSize的線程全忙,任務(wù)被放到隊(duì)列里面
- 如果隊(duì)列放滿了,會新增線程,直到達(dá)到maxSize
- 如果還是處理不過來,會把一個(gè)異常扔到RejectedExecutionHandler中去,用戶可以自己設(shè)定這種情況下的最終處理策略
對于大于coreSize而小于maxSize的那些線程,空閑了keepAliveTime后,會被銷毀。觀察上面說的優(yōu)先級順序可以看到,假如說給ExecutorService一個(gè)無限長的隊(duì)列,比如LinkedBlockingQueue,那么maxSize>coreSize就是沒有意義的。
JDK 6.0
JDK 6.0對鎖做了一些優(yōu)化,比如鎖自旋、鎖消除、鎖合并、輕量級鎖、所偏向等。在這里不一一介紹,但是給一個(gè)例子以有感性認(rèn)識:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import java.util.Vector;
public class LockElimination {
public String getStr() {
Vector v = new Vector();
v.add( 3 );
v.add( 4 );
return v.toString();
}
public static void main(String[] args) {
System.out.println( new LockElimination().getStr());
}
}
|
在這個(gè)例子中,對vector的加鎖完全是沒有必要的,這樣的鎖是可以被優(yōu)化消除的。
CyclicBarrier是JDK 6.0新增的一個(gè)用于流程控制的類,這個(gè)類可以保證多個(gè)任務(wù)在并行執(zhí)行都完成的情況下,再統(tǒng)一執(zhí)行下一步操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class BarrierUsage extends Thread {
private static CyclicBarrier barrier = new CyclicBarrier( 2 , new Thread() {
public void run() {
try {
Thread.sleep( 2000 );
} catch (InterruptedException e) {
}
System.out.println( "finish" );
};
});
private final int sleepMilSecs;
public BarrierUsage( int sleepMilSecs) {
this .sleepMilSecs = sleepMilSecs;
}
@Override
public void run() {
try {
Thread.sleep(sleepMilSecs);
System.out.println(sleepMilSecs + " secs slept" );
barrier.await();
} catch (InterruptedException e) {
} catch (BrokenBarrierException e) {
}
}
public static void main(String[] args) {
new BarrierUsage( 2000 ).start();
new BarrierUsage( 4000 ).start();
}
}
|
上面這個(gè)例子就模擬了,兩個(gè)子任務(wù)(分別執(zhí)行2000毫秒和4000毫秒)完成以后,再執(zhí)行一個(gè)總?cè)蝿?wù)(2000毫秒)并打印完成。
還有一個(gè)類似的類是CountDownLatch(使用倒數(shù)計(jì)數(shù)的方式),這樣的類出現(xiàn)標(biāo)志著,JDK對并發(fā)的設(shè)計(jì)已經(jīng)逐步由微觀轉(zhuǎn)向宏觀了,開始逐步重視并發(fā)程序流程,甚至是框架上的設(shè)計(jì),這樣的思路我們會在下文的JDK 7.0中繼續(xù)看到。
JDK 7.0
2011年的JDK 7.0進(jìn)一步完善了并發(fā)流程控制的功能,比如fork-join框架:
把任務(wù)分解成不同子任務(wù)完成;比如Phaser這個(gè)類,整合了CyclicBarrier和CountDownLatch兩個(gè)類的功能,但是提供了動態(tài)修改依賴目標(biāo)的能力;還有NIO2的新開放特性。這里不詳細(xì)介紹了。
【2015-1-6】后來我寫了一篇文章介紹concurrent包內(nèi)的工具使用,上面這幾個(gè)類都有介紹,歡迎移步鏈接。
Java的未來
在多線程編程方面,Java的未來會怎樣?
JDK 8.0按計(jì)劃將在2013年夏天發(fā)布,Java從動態(tài)語言那里學(xué)了很多過來,比如閉包等等,在多線程方面會怎樣呢?郁于JLS所限,無法有大的突破,還是有另辟蹊徑的辦法?縱觀整個(gè)Java發(fā)展的歷程,都在努力修正多線程模型實(shí)現(xiàn)上的種種弊端,盡可能在保留虛擬機(jī)優(yōu)化特性的基礎(chǔ)上給使用者屏蔽細(xì)節(jié)。
在來回想一下Java最基礎(chǔ)的線程模型,其他語言是怎樣實(shí)現(xiàn)的呢?
比如C#,任何類的任何方法,都可以成為線程的執(zhí)行方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System;
using System.Threading;
public class AnyClass {
public void DoSth() {
Console.WriteLine( "working" );
}
}
class ThreadTest{
public static void Main() {
AnyClass anyClass = new AnyClass();
ThreadStart threadDelegate = new ThreadStart(anyClass.DoSth);
Thread myThread = new Thread(threadDelegate);
myThread.Start();
}
}
|
上面的AnyClass的DoSth方法,就模擬線程執(zhí)行打印了一句話。
再來看一門小眾語言Io,在語法糖的幫助下,實(shí)現(xiàn)更加簡單:
1 2 3 | thread := Object clone
thread start := method( "working" println)
thread @@start
|
因?yàn)镮o是基于原型的語言(如果你有興趣的話,可以在我的blog里找到Io介紹),通過這樣的@符號,就實(shí)現(xiàn)了新啟一個(gè)線程運(yùn)行的功能。
再來看看JDK 5.0的ReentrantLock類,它完全實(shí)現(xiàn)了synchronized語義上的全部功能,并且還能具備諸如條件鎖、鎖超時(shí)、公平鎖等等更優(yōu)越的特性(特別值得一提的是tryLock的功能也實(shí)現(xiàn)了,就是說可以判定假如這個(gè)時(shí)間獲取鎖是否能夠成功),甚至在并發(fā)量居高不下時(shí),性能還更加優(yōu)越……我不禁要問,用一個(gè)Java實(shí)現(xiàn)的鎖類去從功能上代替一個(gè)已有的同步關(guān)鍵字,這豈不是Java自己在抽自己嘴巴?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockUsage implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
System.out.println( "do something 1" );
Thread.sleep( 2000 );
} catch (InterruptedException e) {
} finally {
lock.unlock(); // Why put it in finally block?
}
System.out.println( "finish 1" );
}
public static void main(String[] args) {
new Thread( new ReentrantLockUsage()).start();
lock.lock();
try {
System.out.println( "do something 2" );
Thread.sleep( 2000 );
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
System.out.println( "finish 2" );
}
}
|
其實(shí)這個(gè)問題淵源已久,JLS在最初把Java鎖機(jī)制(包括synchronized關(guān)鍵字)定得太死,以至于無法在上面做進(jìn)一步的修正和優(yōu)化,無奈只好另外重新建一個(gè)類來做這些未竟的事情。如果讓Jame Gosling重新回到二十多年前,他也許會選擇不同的實(shí)現(xiàn)。
關(guān)于協(xié)程(coroutine)。很多語言都內(nèi)置了對協(xié)程的實(shí)現(xiàn)(協(xié)程的含義請自行查閱維基百科),聽起來似乎是一個(gè)嶄新的名字,但是其實(shí)這個(gè)概念一點(diǎn)都不新,JavaScript引擎對單個(gè)頁面的解析就是通過協(xié)程的方式在一個(gè)線程內(nèi)完成的。協(xié)程的實(shí)現(xiàn)困難有兩點(diǎn),一個(gè)是異常的處理,一個(gè)是出入線程時(shí)現(xiàn)場(包括堆棧)的保存和設(shè)置。有一些開源庫已經(jīng)有了Java上協(xié)程的實(shí)現(xiàn),如果你感興趣的話,不妨關(guān)注Kilim和Coroutine for Java。
最后,讓我們來回顧一下Java多線程發(fā)展的歷史。從Java誕生到如今有二十年了,可未來會怎樣,又誰知道呢?
————————————————————————————————————————————————————————
補(bǔ)充2012-09-18:
jinnianshilongnian回復(fù):
一、我覺得非原子性的++操作這句話有點(diǎn)模糊,如下所示:
1、nonAtomicCounter++; 不是原子的原因是因?yàn)樗庆o態(tài)/實(shí)例變量,需要 讀/操作/寫對象成員變量??梢约影焰i保證讀/操作/寫原子性
synchronized(atomicCounter) {
nonAtomicCounter++;
}
2、如果nonAtomicCounter++; 是局部變量 僅有一條指令 iinc i,1;但局部變量又不會線程不安全;
3、nonAtomicCounter如果是long(64位)的在32位機(jī)器即使是局部變量也是線程不安全的(四火補(bǔ)充:在64位機(jī)器上也不是線程安全的);
4、Atomic×××等類通過Unsafe的compareAndSwap××× 即CAS完成的。
應(yīng)該是使用成員變量的++時(shí)。
二、對于文中這段話:
但是,上面的情況是對boolValue使用volatile修飾保證其可見性的情況下出現(xiàn)的,如果不對boolValue使用volatile修飾,運(yùn)行時(shí)就一次不會出現(xiàn)(起碼在我的電腦上)打印“WTF!”的情形,換句話說,這反而是不太正常的,我無法猜測JVM做了什么操作,基本上唯一可以確定的是,沒有用volatile修飾的時(shí)候,boolValue在獲取的時(shí)候,并不能總?cè)〉阶钫鎸?shí)的值。
這個(gè)應(yīng)該是工作內(nèi)存 和 主內(nèi)存 同步的問題。 用volatile修飾的變量,線程在每次使用變量的時(shí)候,都會讀取變量修改后的最的值。
但[boolValue == !boolValue] 和 check/swap 操作并不是原子操作。
也可以通過 在check/swap的兩個(gè)boolValue加鎖來保證同步
synchronized(this) {
boolValue = !boolValue;
}
三、對于DCL問題那段代碼,網(wǎng)上也有文章說即使使用volatile也不能保證DCL的安全性:
四火說明:
你給的ibm那個(gè)文章鏈接也說到,“The memory model allows what is known as "out-of-order writes" and is a prime reason why this idiom fails.” 所以根因在于out of order writes,引起的問題是“partially initialized”,但是文章里面提到使用volatile不能解決問題的原因在于一些JVM的實(shí)現(xiàn)并不能保證順序一致性,換句話說,對于happens-before守則并沒有嚴(yán)格遵守,且不說他的說法是否有根據(jù),我談?wù)撨@個(gè)問題的時(shí)候一定是在JLS明確規(guī)定以下進(jìn)行的。至于虛擬機(jī)實(shí)現(xiàn)上的問題,我不得而知。
FYI:
另外對于element前面如果不加volatile/final的話,也不能保證解決DCL問題,這里四火做一個(gè)說明:
在于instance的可見性由volatile保證了,可是element的name并沒有任何語義上的保證,這里可以使用volatile,但是對于不可變對象其實(shí)也可以使用在這里語義弱一些的final,也是可以解決問題的,JSR133對這兩個(gè)關(guān)鍵字的語義做了強(qiáng)化,我上面給的鏈接里面也提到了,“the values assigned to the final fields in the constructor will be visible to all other threads without synchronization”。
【2014-1-3】對于JSR-133規(guī)范我的閱讀理解,在 這篇文章中有所更新,請移步。
感謝討論。
文章未經(jīng)特殊標(biāo)明皆為本人原創(chuàng),未經(jīng)許可不得用于任何商業(yè)用途,轉(zhuǎn)載請保持完整性并注明來源鏈接《四火的嘮叨》
分享到:
|