一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Java多線程發(fā)展簡史 | 四火的嘮叨

 redalx 2016-01-16

Java多線程發(fā)展簡史 這篇文章,大部分內(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多線程會不會全面一些:

  1. 模型:JMM(Java內(nèi)存模型)和JCM(Java并發(fā)模型)
  2. 使用:JDK中的并發(fā)包
  3. 實(shí)踐:怎樣寫線程安全的代碼
  4. 除錯(cuò):使用工具來分析并發(fā)問題
  5. ……

可是,這未免太死板了,不是么?

不如換一個(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í)候,通常遵循這樣的原則:

  1. 向下兼容。所以往往能看到很多重新設(shè)計(jì)的新增的包和類,還能看到deprecated的類和方法,但是它們并不能輕易被刪除。
  2. 嚴(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)該可以看出兩件事情:

  1. 使用stop來終止一個(gè)線程是不講道理、極其殘暴的,不論目標(biāo)線程在執(zhí)行任何語句,一律強(qiáng)行終止線程,最終將導(dǎo)致一些殘缺的對象和不可預(yù)期的問題產(chǎn)生。
  2. 被終止的線程沒有任何異常拋出,你在線程終止后找不到任何被終止時(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),即:

  1. 一個(gè)線程中synchronized(a),并在同步塊中執(zhí)行a.wait()
  2. 另一個(gè)線程中synchronized(a),并在同步塊中執(zhí)行a.notify()

再來看一看sleep方法的使用,回答下面兩個(gè)問題:

  1. 和wait比較一下,為什么sleep被設(shè)計(jì)為Thread的一個(gè)靜態(tài)方法(即只讓當(dāng)前線程sleep)?
  2. 為什么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è)類:

Java多線程發(fā)展簡史

每一個(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)勢。

Java多線程發(fā)展簡史

相較于面向流的傳統(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í)間刷回主存:

Java多線程發(fā)展簡史

但是,怎樣來保證各個(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è)棧的例子來說明:

Java多線程發(fā)展簡史

  1. 線程t1先查看了一下棧的情況,發(fā)現(xiàn)棧里面有A、B兩個(gè)元素,棧頂是A,這是它所期望的,它現(xiàn)在很想用CAS的方法把A pop出去。
  2. 這時(shí)候線程t2來了,它pop出A、B,又push一個(gè)C進(jìn)去,再把A push回去,這時(shí)候棧里面存放了A、C兩個(gè)元素,棧頂還是A。
  3. 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):

Java多線程發(fā)展簡史

構(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涉及到的常用容器:

Java多線程發(fā)展簡史

其中:

  • 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)先級處理策略:

  1. 請求到來首先交給coreSize內(nèi)的常駐線程執(zhí)行
  2. 如果coreSize的線程全忙,任務(wù)被放到隊(duì)列里面
  3. 如果隊(duì)列放滿了,會新增線程,直到達(dá)到maxSize
  4. 如果還是處理不過來,會把一個(gè)異常扔到RejectedExecutionHandler中去,用戶可以自己設(shè)定這種情況下的最終處理策略

對于大于coreSize而小于maxSize的那些線程,空閑了keepAliveTime后,會被銷毀。觀察上面說的優(yōu)先級順序可以看到,假如說給ExecutorService一個(gè)無限長的隊(duì)列,比如LinkedBlockingQueue,那么maxSize>coreSize就是沒有意義的。

Java多線程發(fā)展簡史

 

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í)行下一步操作:

Java多線程發(fā)展簡史

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框架:

Java多線程發(fā)展簡史

把任務(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誕生到如今有二十年了,可未來會怎樣,又誰知道呢?

Java多線程發(fā)展簡史

————————————————————————————————————————————————————————

補(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)載請保持完整性并注明來源鏈接《四火的嘮叨》

分享到:

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    97人妻精品免费一区二区| 亚洲一区二区精品免费视频| 不卡一区二区在线视频| 国产肥女老熟女激情视频一区| 亚洲av一区二区三区精品| 欧美一区二区日韩一区二区| 国产男女激情在线视频| 日韩丝袜诱惑一区二区| 中国一区二区三区不卡| 国产精品久久香蕉国产线| 一区二区三区四区亚洲另类| 免费观看成人免费视频| 日韩人妻一区中文字幕| 午夜福利网午夜福利网| 99秋霞在线观看视频| 最新69国产精品视频| 久久99热成人网不卡| 久久亚洲精品中文字幕| 日本加勒比不卡二三四区| 国产在线不卡中文字幕| 久久精品久久久精品久久| 精品视频一区二区不卡| 深夜福利欲求不满的人妻| 福利新区一区二区人口| 国产一区二区三区不卡| 亚洲男人天堂网在线视频| 欧美精品久久男人的天堂| 91免费精品国自产拍偷拍| 欧美在线观看视频三区| 国产欧美日韩在线一区二区| 自拍偷拍福利视频在线观看| 亚洲精品小视频在线观看| 日本东京热加勒比一区二区| 中文字幕乱码一区二区三区四区| 亚洲欧洲成人精品香蕉网| 亚洲一级二级三级精品| 亚洲av成人一区二区三区在线| 成人精品一级特黄大片| 又大又长又粗又猛国产精品| 又大又长又粗又黄国产| 日韩av欧美中文字幕|