Java多線程發展簡史
多線程編程方面的大事件和發展脈絡有一個描述,并且提及一些在多線程編程方面常見的問題。對于Java程序員來說,如果從歷史的角度去了解一門語言一個特性的演進,或許能有不同收獲。
引言
首先問這樣一個問題,如果提到Java多線程編程,你會想到什么?
● volatile、synchronized關鍵字?
● 競爭和同步?
● 鎖機制?
● 線程安全問題?
● 線程池和隊列?
好吧,請原諒我在這里賣的關子,其實這些都對,但是又不足夠全面,如果我們這樣來談論Java多線程會不會全面一些:
1.模型:JMM(Java內存模型)和JCM(Java并發模型)
2.使用:JDK中的并發包
3.實踐:怎樣寫線程安全的代碼
4.除錯:使用工具來分析并發問題
5.……
可是,這未免太死板了,不是么?
不如換一個思路,我們少談一些很容易查到的語法,不妨從歷史的角度看看Java在多線程編程方面是怎樣進化的,這個過程中,它做了哪些正確的決定,犯了哪些錯誤,未來又會有怎樣的發展趨勢?
另外,還有一點要說是,我希望通過大量的實例代碼來說明這些事情。Linus說:“Talk is cheap, show me the code.”。下文涉及到的代碼我已經上傳,可以在此打包下載。
誕生
Java的基因來自于1990年12月Sun公司的一個內部項目,目標設備正是家用電器,但是C++的可移植性和API的易用性都讓程序員反感。旨在解決這樣的問題,于是又了Java的前身Oak語言,但是知道1995年3月,它正式更名為Java,才算Java語言真正的誕生。
JDK 1.0
1996年1月的JDK1.0版本,從一開始就確立了Java最基礎的線程模型,并且,這樣的線程模型再后續的修修補補中,并未發生實質性的變更,可以說是一個具有傳承性的良好設計。
搶占式和協作式是兩種常見的進程/線程調度方式,操作系統非常適合使用搶占式方式來調度它的進程,它給不同的進程分配時間片,對于長期無響應的進程,它有能力剝奪它的資源,甚至將其強行停止(如果采用協作式的方式,需要進程自覺、主動地釋放資源,也許就不知道需要等到什么時候了)。Java語言一開始就采用協作式的方式,并且在后面發展的過程中,逐步廢棄掉了粗暴的stop/resume/suspend這樣的方法,它們是違背協作式的不良設計,轉而采用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();
}
}</pre>
這是中斷的一種使用方式,看起來就像是一個標志位,線程A設置這個標志位,線程B時不時地檢查這個標志位。另外還有一種使用中斷通信的方式,如下:
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();
}
}</pre>
在這種方式下,如果使用wait方法處于等待中的線程,被另一個線程使用中斷喚醒,于是拋出InterruptedException,同時,中斷標志清除,這時候我們通常會在捕獲該異常的地方重新設置中斷,以便后續的邏輯通過檢查中斷狀態來了解該線程是如何結束的。
在比較穩定的JDK 1.0.2版本中,已經可以找到Thread和ThreadUsage這樣的類,這也是線程模型中最核心的兩個類。整個版本只包含了這樣幾個包:java.io、 java.util、java.net、java.awt和java.applet,所以說Java從一開始這個非常原始的版本就確立了一個持久的線程模型。
值得一提的是,在這個版本中,原子對象AtomicityXXX已經設計好了,這里給出一個例子,說明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);
}
}</pre>
上面這個例子你也許需要跑幾次才能看到效果,使用非原子性的++操作,結果經常小于1000。
對于鎖的使用,網上可以找到各種說明,但表述都不夠清晰。請看下面的代碼:
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();
}
}</pre>
上面的例子可以反映對一個鎖競爭的現象,結合上面的例子,理解下面這兩條,就可以很容易理解synchronized關鍵字的使用:
● 非靜態方法使用synchronized修飾,相當于synchronized(this)。
● 靜態方法使用synchronized修飾,相當于synchronized(Lock.class)。
JDK 1.2
1998年年底的JDK1.2版本正式把Java劃分為J2EE/J2SE/J2ME三個不同方向。在這個版本中,Java試圖用Swing修正在AWT中犯的錯誤,例如使用了太多的同步。可惜的是,Java本身決定了AWT還是Swing性能和響應都難以令人滿意,這也是Java桌面應用難以比及其服務端應用的一個原因,在IBM后來的SWT,也不足以令人滿意,JDK在這方面到JDK 1.2后似乎反省了自己,停下腳步了。值得注意的是,JDK高版本修復低版本問題的時候,通常遵循這樣的原則:
1.向下兼容。所以往往能看到很多重新設計的新增的包和類,還能看到deprecated的類和方法,但是它們并不能輕易被刪除。
2。嚴格遵循JLS(Java Language Specification),并把通過的新JSR(Java Specification Request)補充到JLS中,因此這個文檔本身也是向下兼容的,后面的版本只能進一步說明和特性增強,對于一些最初擴展性比較差的設計,也會無能為力。這個在下文中關于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
}
}</pre>
從上面的代碼你應該可以看出兩件事情:
1.使用stop來終止一個線程是不講道理、極其殘暴的,不論目標線程在執行任何語句,一律強行終止線程,最終將導致一些殘缺的對象和不可預期的問題產生。
2。被終止的線程沒有任何異常拋出,你在線程終止后找不到任何被終止時執行的代碼行,或者是堆棧信息(上面代碼打印的異常僅僅是main線程執行stop語句的異常而已,并非被終止的線程)。
很難想象這樣的設計出自一個連指針都被廢掉的類型安全的編程語言,對不對?再來看看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();
}
}
}</pre>
從上面的代碼可以看出,Suspend線程被掛起時,依然占有鎖,而當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();
}
}
}</pre>
在wait和notify搭配使用的過程中,注意需要把它們鎖定到同一個資源上(例如對象a),即:
1.一個線程中synchronized(a),并在同步塊中執行a.wait()
2.另一個線程中synchronized(a),并在同步塊中執行a.notify()
再來看一看sleep方法的使用,回答下面兩個問題:
1.和wait比較一下,為什么sleep被設計為Thread的一個靜態方法(即只讓當前線程sleep)?
2.為什么sleep必須要傳入一個時間參數,而不允許不限期地sleep?
如果我前面說的你都理解了,你應該能回答這兩個問題。
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();
}
}
}</pre>
在這個JDK版本中,引入線程變量ThreadLocal這個類:
每一個線程都掛載了一個ThreadLocalMap。ThreadLocal這個類的使用很有意思,get方法沒有key傳入,原因就在于這個key就是當前你使用的這個ThreadLocal它自己。ThreadLocal的對象生命周期可以伴隨著整個線程的生命周期。因此,倘若在線程變量里存放持續增長的對象(最常見是一個不受良好管理的map),很容易導致內存泄露。
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();
}
}</pre>
上面的例子會一直打印var1,而不會打印var2,就是因為不同線程中的ThreadLocal是互相獨立的。
用jstack工具可以找到鎖相關的信息,如果線程占有鎖,但是由于執行到wait方法時處于wait狀態暫時釋放了鎖,會打印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)</pre>
如果程序持續占有某個鎖(例如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)</pre>
而如果是線程希望進入某同步塊,而在等待鎖的釋放,會打印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)</pre>
JDK 1.4
在2002年4月發布的JDK1.4中,正式引入了NIO。JDK在原有標準IO的基礎上,提供了一組多路復用IO的解決方案。
通過在一個Selector上掛接多個Channel,通過統一的輪詢線程檢測,每當有數據到達,觸發監聽事件,將事件分發出去,而不是讓每一個channel長期消耗阻塞一個線程等待數據流到達。所以,只有在對資源爭奪劇烈的高并發場景下,才能見到NIO的明顯優勢。
相較于面向流的傳統方式這種面向塊的訪問方式會丟失一些簡易性和靈活性。下面給出一個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();
}
}</pre>
JDK 5.0
2004年9月起JDK 1.5發布,并正式更名到5.0。有個笑話說,軟件行業有句話,叫做“不要用3.0版本以下的軟件”,意思是說版本太小的話往往軟件質量不過關——但是按照這種說法,JDK的原有版本命名方式得要到啥時候才有3.0啊,于是1.4以后通過版本命名方式的改變直接升到5.0了。
JDK 5.0不只是版本號命名方式變更那么簡單,對于多線程編程來說,這里發生了兩個重大事件,JSR 133和JSR 166的正式發布。
JSR 133
JSR 133重新明確了Java內存模型,事實上,在這之前,常見的內存模型包括連續一致性內存模型和先行發生模型。
對于連續一致性模型來說,程序執行的順序和代碼上顯示的順序是完全一致的。這對于現代多核,并且指令執行優化的CPU來說,是很難保證的。而且,順序一致性的保證將JVM對代碼的運行期優化嚴重限制住了。
但是JSR 133指定的先行發生(Happens-before)使得執行指令的順序變得靈活:
● 在同一個線程里面,按照代碼執行的順序(也就是代碼語義的順序),前一個操作先于后面一個操作發生
● 對一個monitor對象的解鎖操作先于后續對同一個monitor對象的鎖操作
● 對volatile字段的寫操作先于后面的對此字段的讀操作
● 對線程的start操作(調用線程對象的start()方法)先于這個線程的其他任何操作
● 一個線程中所有的操作先于其他任何線程在此線程上調用 join()方法
● 如果A操作優先于B,B操作優先于C,那么A操作優先于C
而在內存分配上,將每個線程各自的工作內存(甚至包括)從主存中獨立出來,更是給JVM大量的空間來優化線程內指令的執行。主存中的變量可以被拷貝到線程的工作內存中去單獨執行,在執行結束后,結果可以在某個時間刷回主存:
但是,怎樣來保證各個線程之間數據的一致性?JLS給的辦法就是,默認情況下,不能保證任意時刻的數據一致性,但是通過對synchronized、volatile和final這幾個語義被增強的關鍵字的使用,可以做到數據一致性。要解釋這個問題,不如看一看經典的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");
}</pre>
在上面這個例子中,如果不對instance聲明的地方使用volatile關鍵字,JVM將不能保證getInstance方法獲取到的instance是一個完整的、正確的instance,而volatile關鍵字保證了instance的可見性,即能夠保證獲取到當時真實的instance對象。
但是問題沒有那么簡單,對于上例中的element而言,如果沒有volatile和final修飾,element里的name也無法在前文所述的instance返回給外部時的可見性。如果element是不可變對象,使用final也可以保證它在構造方法調用后的可見性。
對于volatile的效果,很多人都希望有一段簡短的代碼能夠看到,使用volatile和不使用volatile的情況下執行結果的差別。可惜這其實并不好找。這里我給出這樣一個不甚嚴格的例子:
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;
}
}</pre>
代碼中存在兩個線程,一個線程通過一個死循環不斷在變換boolValue的取值;另一個線程每100毫秒執行“boolValue==!boolValue”,這行代碼會取兩次boolValue,可以想象的是,有一定概率會出現這兩次取boolValue結果不一致的情況,那么這個時候就會打印“WTF!”。
但是,上面的情況是對boolValue使用volatile修飾保證其可見性的情況下出現的,如果不對boolValue使用volatile修飾,運行時就一次不會出現(起碼在我的電腦上)打印“WTF!”的情形,換句話說,這反而是不太正常的,我無法猜測JVM做了什么操作,基本上唯一可以確定的是,沒有用volatile修飾的時候,boolValue在獲取的時候,并不能總取到最真實的值。
JSR 166
JSR 166的貢獻就是引入了java.util.concurrent這個包。前面曾經講解過AtomicXXX類這種原子類型,內部實現保證其原子性的其實是通過一個compareAndSet(x,y)方法(CAS),而這個方法追蹤到最底層,是通過CPU的一個單獨的指令來實現的。這個方法所做的事情,就是保證在某變量取值為x的情況下,將取值x替換為y。在這個過程中,并沒有任何加鎖的行為,所以一般它的性能要比使用synchronized高。
Lock-free算法就是基于CAS來實現原子化“set”的方式,通常有這樣兩種形式:
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
}
}</pre>
不過,對CAS的使用并不總是正確的,比如ABA問題。我用下面這樣一個棧的例子來說明:
1.線程t1先查看了一下棧的情況,發現棧里面有A、B兩個元素,棧頂是A,這是它所期望的,它現在很想用CAS的方法把A pop出去。
2.這時候線程t2來了,它pop出A、B,又push一個C進去,再把A push回去,這時候棧里面存放了A、C兩個元素,棧頂還是A。
3.t1開始使用CAS:head.compareAndSet(A,B),把A pop出去了,棧里就剩下B了,可是這時候其實已經發生了錯誤,因為C丟失了。
為什么會發生這樣的錯誤?因為對t1來說,它兩次都查看到棧頂的A,以為期間沒有發生變化,而實際上呢?實際上已經發生了變化,C進來、B出去了,但是t1它只看棧頂是A,它并不知道曾經發生了什么。
那么,有什么辦法可以解決這個問題呢?
最常見的辦法是使用一個計數器,對這個棧只要有任何的變化,就觸發計數器+1,t1在要查看A的狀態,不如看一下計數器的情況,如果計數器沒有變化,說明期間沒有別人動過這個棧。JDK 5.0里面提供的AtomicStampedReference就是起這個用的。
使用immutable對象的拷貝(比如CopyOnWrite)也可以實現無鎖狀態下的并發訪問。舉一個簡單的例子,比如有這樣一個鏈表,每一個節點包含兩個值,現在我要把中間一個節點(2,3)替換成(4,5),不使用同步的話,我可以這樣實現:
構建一個新的節點連到節點(4,6)上,再將原有(1,1)到(2,3)的指針指向替換成(1,1)到(4,5)的指向。
除了這兩者,還有很多不用同步來實現原子操作的方法,比如我曾經介紹過的Peterson算法。
以下這個表格顯示了JDK 5.0涉及到的常用容器:
其中:
● unsafe這一列的容器都是JDK之前版本有的,且非線程安全的;
● synchronized這一列的容器都是JDK之前版本有的,且通過synchronized的關鍵字同步方式來保證線程安全的;
● concurrent pkg一列的容器都是并發包新加入的容器,都是線程安全,但是都沒有使用同步來實現線程安全。
再說一下對于線程池的支持。在說線程池之前,得明確一下Future的概念。Future也是JDK 5.0新增的類,是一個用來整合同步和異步的結果對象。一個異步任務的執行通過Future對象立即返回,如果你期望以同步方式獲取結果,只需要調用它的get方法,直到結果取得才會返回給你,否則線程會一直hang在那里。Future可以看做是JDK為了它的線程模型做的一個部分修復,因為程序員以往在考慮多線程的時候,并不能夠以面向對象的思路去完成它,而不得不考慮很多面向線程的行為,但是Future和后面要講到的Barrier等類,可以讓這些特定情況下,程序員可以從繁重的線程思維中解脫出來。把線程控制的部分和業務邏輯的部分解耦開。
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.
}
}</pre>
上面的代碼是一個最簡單的線程池使用的例子,線程池接受提交上來的任務,分配給池中的線程去執行。對于任務壓力的情況,JDK中一個功能完備的線程池具備這樣的優先級處理策略:
1. 請求到來首先交給coreSize內的常駐線程執行
2.如果coreSize的線程全忙,任務被放到隊列里面
3.如果隊列放滿了,會新增線程,直到達到maxSize
4.如果還是處理不過來,會把一個異常扔到RejectedExecutionHandler中去,用戶可以自己設定這種情況下的最終處理策略
對于大于coreSize而小于maxSize的那些線程,空閑了keepAliveTime后,會被銷毀。觀察上面說的優先級順序可以看到,假如說給ExecutorService一個無限長的隊列,比如LinkedBlockingQueue,那么maxSize>coreSize就是沒有意義的。
JDK 6.0
JDK 6.0對鎖做了一些優化,比如鎖自旋、鎖消除、鎖合并、輕量級鎖、所偏向等。在這里不一一介紹,但是給一個例子以有感性認識:
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());
}
}</pre>
在這個例子中,對vector的加鎖完全是沒有必要的,這樣的鎖是可以被優化消除的。
CyclicBarrier是JDK 6.0新增的一個用于流程控制的類,這個類可以保證多個任務在并行執行都完成的情況下,再統一執行下一步操作:
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();
}
}
上面這個例子就模擬了,兩個子任務(分別執行2000毫秒和4000毫秒)完成以后,再執行一個總任務(2000毫秒)并打印完成。
還有一個類似的類是CountDownLatch(使用倒數計數的方式),這樣的類出現標志著,JDK對并發的設計已經逐步由微觀轉向宏觀了,開始逐步重視并發程序流程,甚至是框架上的設計,這樣的思路我們會在下文的JDK 7.0中繼續看到。
JDK 7.0
2011年的JDK 7.0進一步完善了并發流程控制的功能,比如fork-join框架:
把任務分解成不同子任務完成;比如Phaser這個類,整合了CyclicBarrier和CountDownLatch兩個類的功能,但是提供了動態修改依賴目標的能力;還有NIO2的新開放特性。這里不詳細介紹了。
Java的未來
在多線程編程方面,Java的未來會怎樣?
JDK 8.0按計劃將在2013年夏天發布,Java從動態語言那里學了很多過來,比如閉包等等,在多線程方面會怎樣呢?郁于JLS所限,無法有大的突破,還是有另辟蹊徑的辦法?縱觀整個Java發展的歷程,都在努力修正多線程模型實現上的種種弊端,盡可能在保留虛擬機優化特性的基礎上給使用者屏蔽細節。
在來回想一下Java最基礎的線程模型,其他語言是怎樣實現的呢?
比如C#,任何類的任何方法,都可以成為線程的執行方法:
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();
}
}</pre>
上面的AnyClass的DoSth方法,就模擬線程執行打印了一句話。
再來看一門小眾語言Io,在語法糖的幫助下,實現更加簡單:
thread := Object clone
thread start := method("working" println)
thread @@start
因為Io是基于原型的語言(如果你有興趣的話,可以在我的blog里找到Io介紹),通過這樣的@符號,就實現了新啟一個線程運行的功能。
再來看看JDK 5.0的ReentrantLock類,它完全實現了synchronized語義上的全部功能,并且還能具備諸如條件鎖、鎖超時、公平鎖等等更優越的特性(特別值得一提的是tryLock的功能也實現了,就是說可以判定假如這個時間獲取鎖是否能夠成功),甚至在并發量居高不下時,性能還更加優越……我不禁要問,用一個Java實現的鎖類去從功能上代替一個已有的同步關鍵字,這豈不是Java自己在抽自己嘴巴?
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");
}
}</pre>
其實這個問題淵源已久,JLS在最初把Java鎖機制(包括synchronized關鍵字)定得太死,以至于無法在上面做進一步的修正和優化,無奈只好另外重新建一個類來做這些未竟的事情。如果讓Jame Gosling重新回到二十多年前,他也許會選擇不同的實現。
關于協程(coroutine)。很多語言都內置了對協程的實現(協程的含義請自行查閱維基百科),聽起來似乎是一個嶄新的名字,但是其實這個概念一點都不新,JavaScript引擎對單個頁面的解析就是通過協程的方式在一個線程內完成的。協程的實現困難有兩點,一個是異常的處理,一個是出入線程時現場(包括堆棧)的保存和設置。有一些開源庫已經有了Java上協程的實現,如果你感興趣的話,不妨關注Kilim和Coroutine for Java。
最后,讓我們來回顧一下Java多線程發展的歷史。從Java誕生到如今有二十年了,可未來會怎樣,又誰知道呢?
———————————————————————————
補充2012-09-18:
一、我覺得非原子性的++操作這句話有點模糊,如下所示:
1、nonAtomicCounter++; 不是原子的原因是因為它是靜態/實例變量,需要 讀/操作/寫對象成員變量。可以加把鎖保證讀/操作/寫原子性
synchronized(atomicCounter) {
nonAtomicCounter++;
}
2、如果nonAtomicCounter++; 是局部變量 僅有一條指令 iinc i,1;但局部變量又不會線程不安全;
3、nonAtomicCounter如果是long(64位)的在32位機器即使是局部變量也是線程不安全的(四火補充:在64位機器上也不是線程安全的);
4、Atomic×××等類通過Unsafe的compareAndSwap××× 即CAS完成的。
應該是使用成員變量的++時。
二、對于文中這段話:
但是,上面的情況是對boolValue使用volatile修飾保證其可見性的情況下出現的,如果不對boolValue使用volatile修飾,運行時就一次不會出現(起碼在我的電腦上)打印“WTF!”的情形,換句話說,這反而是不太正常的,我無法猜測JVM做了什么操作,基本上唯一可以確定的是,沒有用volatile修飾的時候,boolValue在獲取的時候,并不能總取到最真實的值。
這個應該是工作內存 和 主內存 同步的問題。 用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改后的最的值。
但[boolValue == !boolValue] 和 check/swap 操作并不是原子操作。
也可以通過 在check/swap的兩個boolValue加鎖來保證同步
synchronized(this) {
boolValue = !boolValue;
}
三、對于DCL問題那段代碼,網上也有文章說即使使用volatile也不能保證DCL的安全性:
http://www.ibm.com/developerworks/java/library/j-dcl/index.html
四火說明:
你給的ibm那個文章鏈接也說到,“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的實現并不能保證順序一致性,換句話說,對于happens-before守則并沒有嚴格遵守,且不說他的說法是否有根據,我談論這個問題的時候一定是在JLS明確規定以下進行的。至于虛擬機實現上的問題,我不得而知。
FYI:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
另外對于element前面如果不加volatile/final的話,也不能保證解決DCL問題,這里四火做一個說明:
在于instance的可見性由volatile保證了,可是element的name并沒有任何語義上的保證,這里可以使用volatile,但是對于不可變對象其實也可以使用在這里語義弱一些的final,也是可以解決問題的,JSR133對這兩個關鍵字的語義做了強化,我上面給的鏈接里面也提到了,“the values assigned to the final fields in the constructor will be visible to all other threads without synchronization”。
</div>
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!








