1. 程式人生 > >理解 JAVA多執行緒技術之詳解

理解 JAVA多執行緒技術之詳解

1.    虛假的多執行緒

    例1:

    public class TestThread
    {
    int i=0, j=0;
    public void go(int flag){
    while(true){
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    if(flag==0)

    i++;
    System.out.println("i=" + i);
    }
    else{
    j++;
    System.out.println("j=" + j);
    }
    }
    }
    public static void main(String[] args){
    new TestThread().go(0);
    new TestThread().go(1);
    }
    }
    上面程式的執行結果為:

    i=1
    i=2
    i=3
    。。。

    結果將一直打印出I的值。我們的意圖是當在while迴圈中呼叫sleep()時,另一個執行緒就將起動,打印出j的值,但結果卻並不是這樣。關於sleep()為什麼不會出現我們預想的結果,在下面將講到。

    2.    實現多執行緒

    通過繼承classThread或實現Runnable介面,我們可以實現多執行緒
    2.1    通過繼承classThread實現多執行緒

    classThread中有兩個最重要的函式run()和start()。

    1)    run()函式必須進行覆寫,把要在多個執行緒中並行處理的程式碼放到這個函式中。

    2)    雖然run()函式實現了多個執行緒的並行處理,但我們不能直接呼叫run()函式,而是通過呼叫start()函式來呼叫run()函式。在呼叫start()的時候,start()函式會首先進行與多執行緒相關的初始化(這也是為什麼不能直接呼叫run()函式的原因),然後再呼叫run()函式。

    例2:

    public class TestThread extends Thread{
    private static int threadCount = 0;
    private int threadNum = ++threadCount;
    private int i = 5;
    public void run(){
    while(true){
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    System.out.println("Thread " + threadNum + " = " + i);
    if(--i==0) return;
    }
    }
    public static void main(String[] args){
    for(int i=0; i<5; i++)

    new TestThread().start();
    }
    }
    執行結果為:

    Thread 1 = 5
    Thread 2 = 5
    Thread 3 = 5
    Thread 4 = 5
    Thread 5 = 5
    Thread 1 = 4
    Thread 2 = 4
    Thread 3 = 4
    Thread 4 = 4
    Thread 1 = 3
    Thread 2 = 3
    Thread 5 = 4
    Thread 3 = 3
    Thread 4 = 3
    Thread 1 = 2
    Thread 2 = 2
    Thread 5 = 3
    Thread 3 = 2
    Thread 4 = 2
    Thread 1 = 1
    Thread 2 = 1
    Thread 5 = 2
    Thread 3 = 1
    Thread 4 = 1
    Thread 5 = 1
    從結果可見,例2能實現多執行緒的並行處理。

    **:在上面的例子中,我們只用new產生Thread物件,並沒有用reference來記錄所產生的Thread物件。根據垃圾回收機制,當一個物件沒有被reference引用時,它將被回收。但是垃圾回收機制對Thread物件“不成立”。因為每一個Thread都會進行註冊動作,所以即使我們在產生Thread物件時沒有指定一個reference指向這個物件,實際上也會在某個地方有個指向該物件的reference,所以垃圾回收器無法回收它們。

    3)    通過Thread的子類產生的執行緒物件是不同物件的執行緒

    class TestSynchronized extends Thread{
    public TestSynchronized(String name){
    super(name);
    }
    public synchronized static void prt(){
    for(int i=10; i<20; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    public synchronized void run(){
    for(int i=0; i<3; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    }
    public class TestThread{
    public static void main(String[] args){
    TestSynchronized t1 = new TestSynchronized("t1");
    TestSynchronized t2 = new TestSynchronized("t2");
    t1.start();
    t1.start();//(1)

    //t2.start();(2)

    }
    }
    執行結果為:

    t1 : 0
    t1 : 1
    t1 : 2
    t1 : 0
    t1 : 1
    t1 : 2
    由於是同一個物件啟動的不同執行緒,所以run()函式實現了synchronized。如果去掉(2)的註釋,把程式碼(1)註釋掉,結果將變為:

    t1 : 0
    t2 : 0
    t1 : 1
    t2 : 1
    t1 : 2
    t2 : 2
    由於t1和t2是兩個物件,所以它們所啟動的執行緒可同時訪問run()函式。

    2.2    通過實現Runnable介面實現多執行緒

    如果有一個類,它已繼承了某個類,又想實現多執行緒,那就可以通過實現Runnable介面來實現。

    1)    Runnable介面只有一個run()函式。

    2)    把一個實現了Runnable介面的物件作為引數產生一個Thread物件,再呼叫Thread物件的start()函式就可執行並行操作。如果在產生一個Thread物件時以一個Runnable介面的實現類的物件作為引數,那麼在呼叫start()函式時,start()會呼叫Runnable介面的實現類中的run()函式。

    例3.1:

    public class TestThread implements Runnable{
    private static int threadCount = 0;
    private int threadNum = ++threadCount;
    private int i = 5;
    public void run(){
    while(true){
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    System.out.println("Thread " + threadNum + " = " + i);
    if(--i==0) return;
    }
    }
    public static void main(String[] args){
    for(int i=0; i<5; i++)

    new Thread(new TestThread()).start();//(1)

    }
    }
    執行結果為:

    Thread 1 = 5
    Thread 2 = 5
    Thread 3 = 5
    Thread 4 = 5
    Thread 5 = 5
    Thread 1 = 4
    Thread 2 = 4
    Thread 3 = 4
    Thread 4 = 4
    Thread 4 = 3
    Thread 5 = 4
    Thread 1 = 3
    Thread 2 = 3
    Thread 3 = 3
    Thread 4 = 2
    Thread 5 = 3
    Thread 1 = 2
    Thread 2 = 2
    Thread 3 = 2
    Thread 4 = 1
    Thread 5 = 2
    Thread 1 = 1
    Thread 2 = 1
    Thread 3 = 1
    Thread 5 = 1
    例3是對例2的修改,它通過實現Runnable介面來實現並行處理。程式碼(1)處可見,要呼叫TestThread中的並行操作部分,要把一個TestThread物件作為引數來產生Thread物件,再呼叫Thread物件的start()函式。

    3)    同一個實現了Runnable介面的物件作為引數產生的所有Thread物件是同一物件下的執行緒。

    例3.2:

    package mypackage1;
    public class TestThread implements Runnable{
    public synchronized void run(){
    for(int i=0; i<5; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    public static void main(String[] args){
    TestThread testThread = new TestThread();
    for(int i=0; i<5; i++)

    //new Thread(testThread, "t" + i).start();(1)

    new Thread(new TestThread(), "t" + i).start();(2)

    }
    }
    執行結果為:

    t0 : 0
    t1 : 0
    t2 : 0
    t3 : 0
    t4 : 0
    t0 : 1
    t1 : 1
    t2 : 1
    t3 : 1
    t4 : 1
    t0 : 2
    t1 : 2
    t2 : 2
    t3 : 2
    t4 : 2
    t0 : 3
    t1 : 3
    t2 : 3
    t3 : 3
    t4 : 3
    t0 : 4
    t1 : 4
    t2 : 4
    t3 : 4
    t4 : 4
    由於程式碼(2)每次都是用一個新的TestThread物件來產生Thread物件的,所以產生出來的Thread物件是不同物件的執行緒,所以所有Thread物件都可同時訪問run()函式。如果註釋掉程式碼(2),並去掉程式碼(1)的註釋,結果為:

    t0 : 0
    t0 : 1
    t0 : 2
    t0 : 3
    t0 : 4
    t1 : 0
    t1 : 1
    t1 : 2
    t1 : 3
    t1 : 4
    t2 : 0
    t2 : 1
    t2 : 2
    t2 : 3
    t2 : 4
    t3 : 0
    t3 : 1
    t3 : 2
    t3 : 3
    t3 : 4
    t4 : 0
    t4 : 1
    t4 : 2
    t4 : 3
    t4 : 4
    由於程式碼(1)中每次都是用同一個TestThread物件來產生Thread物件的,所以產生出來的Thread物件是同一個物件的執行緒,所以實現run()函式的同步。

    二.    共享資源的同步

    1.    同步的必要性

    例4:

    class Seq{
    private static int number = 0;
    private static Seq seq = new Seq();
    private Seq() {}
    public static Seq getInstance(){
    return seq;
    }
    public int get(){
    number++; //(a)

    return number;//(b)

    }
    }
    public class TestThread{
    public static void main(String[] args){
    Seq.getInstance().get();//(1)

    Seq.getInstance().get();//(2)

    }
    }
    上面是一個取得序列號的單例模式的例子,但呼叫get()時,可能會產生兩個相同的序列號:

    當代碼(1)和(2)都試圖呼叫get()取得一個唯一的序列。當代碼(1)執行完程式碼(a),正要執行程式碼(b)時,它被中斷了並開始執行程式碼(2)。一旦當代碼(2)執行完(a)而程式碼(1)還未執行程式碼(b),那麼程式碼(1)和程式碼(2)就將得到相同的值。

    2.    通過synchronized實現資源同步

    2.1    鎖標誌

    2.1.1    每個物件都有一個標誌鎖。當物件的一個執行緒訪問了物件的某個synchronized資料(包括函式)時,這個物件就將被“上鎖”,所以被宣告為synchronized的資料(包括函式)都不能被呼叫(因為當前執行緒取走了物件的“鎖標誌”)。只有當前執行緒訪問完它要訪問的synchronized資料,釋放“鎖標誌”後,同一個物件的其它執行緒才能訪問synchronized資料。

    2.1.2    每個class也有一個“鎖標誌”。對於synchronized static資料(包括函式)可以在整個class下進行鎖定,避免static資料的同時訪問。

    例5:

    class Seq{
    private static int number = 0;
    private static Seq seq = new Seq();
    private Seq() {}
    public static Seq getInstance(){
    return seq;
    }
    public synchronized int get(){ //(1)

    number++;
    return number;
    }
    }
    例5在例4的基礎上,把get()函式宣告為synchronized,那麼在同一個物件中,就只能有一個執行緒呼叫get()函式,所以每個執行緒取得的number值就是唯一的了。

    例6:

    class Seq{
    private static int number = 0;
    private static Seq seq = null;
    private Seq() {}
    synchronized public static Seq getInstance(){ //(1)

    if(seq==null)    seq = new Seq();
    return seq;
    }
    public synchronized int get(){
    number++;
    return number;
    }
    }
    例6把getInstance()函式宣告為synchronized,那樣就保證通過getInstance()得到的是同一個seq物件。

    2.2    non-static的synchronized資料只能在同一個物件的純種實現同步訪問,不同物件的執行緒仍可同時訪問。

    例7:

    class TestSynchronized implements Runnable{
    public synchronized void run(){//(1)

    for(int i=0; i<10; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    /*(2)*/
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    }
    public class TestThread{
    public static void main(String[] args){
    TestSynchronized r1 = new TestSynchronized();
    TestSynchronized r2 = new TestSynchronized();
    Thread t1 = new Thread(r1, "t1");
    Thread t2 = new Thread(r2, "t2");//(3)

    //Thread t2 = new Thread(r1, "t2");(4)

    t1.start();
    t2.start();
    }
    }
    執行結果為:

    t1 : 0
    t2 : 0
    t1 : 1
    t2 : 1
    t1 : 2
    t2 : 2
    t1 : 3
    t2 : 3
    t1 : 4
    t2 : 4
    t1 : 5
    t2 : 5
    t1 : 6
    t2 : 6
    t1 : 7
    t2 : 7
    t1 : 8
    t2 : 8
    t1 : 9
    t2 : 9
    雖然我們在程式碼(1)中把run()函式宣告為synchronized,但由於t1、t2是兩個物件(r1、r2)的執行緒,而run()函式是non-static的synchronized資料,所以仍可被同時訪問(程式碼(2)中的sleep()函式由於在暫停時不會釋放“標誌鎖”,因為執行緒中的迴圈很難被中斷去執行另一個執行緒,所以程式碼(2)只是為了顯示結果)。

    如果把例7中的程式碼(3)註釋掉,並去年程式碼(4)的註釋,執行結果將為:

    t1 : 0
    t1 : 1
    t1 : 2
    t1 : 3
    t1 : 4
    t1 : 5
    t1 : 6
    t1 : 7
    t1 : 8
    t1 : 9
    t2 : 0
    t2 : 1
    t2 : 2
    t2 : 3
    t2 : 4
    t2 : 5
    t2 : 6
    t2 : 7
    t2 : 8
    t2 : 9
    修改後的t1、t2是同一個物件(r1)的執行緒,所以只有當一個執行緒(t1或t2中的一個)執行run()函式,另一個執行緒才能執行。

    2.3    物件的“鎖標誌”和class的“鎖標誌”是相互獨立的。

    例8:

    class TestSynchronized extends Thread{
    public TestSynchronized(String name){
    super(name);
    }
    public synchronized static void prt(){
    for(int i=10; i<20; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    public synchronized void run(){
    for(int i=0; i<10; i++){
    System.out.println(Thread.currentThread().getName() + " : " + i);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    }
    public class TestThread{
    public static void main(String[] args){
    TestSynchronized t1 = new TestSynchronized("t1");
    TestSynchronized t2 = new TestSynchronized("t2");
    t1.start();
    t1.prt();//(1)

    t2.prt();//(2)

    }
    }
    執行結果為:

    main : 10
    t1 : 0
    main : 11
    t1 : 1
    main : 12
    t1 : 2
    main : 13
    t1 : 3
    main : 14
    t1 : 4
    main : 15
    t1 : 5
    main : 16
    t1 : 6
    main : 17
    t1 : 7
    main : 18
    t1 : 8
    main : 19
    t1 : 9
    main : 10
    main : 11
    main : 12
    main : 13
    main : 14
    main : 15
    main : 16
    main : 17
    main : 18
    main : 19
    在程式碼(1)中,雖然是通過物件t1來呼叫prt()函式的,但由於prt()是靜態的,所以呼叫它時不用經過任何物件,它所屬的執行緒為main執行緒。

    由於呼叫run()函式取走的是物件鎖,而呼叫prt()函式取走的是class鎖,所以同一個執行緒t1(由上面可知實際上是不同執行緒)呼叫run()函式且還沒完成run()函式時,它就能呼叫prt()函式。但prt()函式只能被一個執行緒呼叫,如程式碼(1)和程式碼(2),即使是兩個不同的物件也不能同時呼叫prt()。

    3.    同步的優化

    1)    synchronizedblock

    語法為:synchronized(reference){ do this }
    reference用來指定“以某個物件的鎖標誌”對“大括號內的程式碼”實施同步控制。

    例9:

    class TestSynchronized implements Runnable{
    static int j = 0;
    public synchronized void run(){
    for(int i=0; i<5; i++){
    //(1)

    System.out.println(Thread.currentThread().getName() + " : " + j++);
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    }
    public class TestThread{
    public static void main(String[] args){
    TestSynchronized r1 = new TestSynchronized();
    TestSynchronized r2 = new TestSynchronized();
    Thread t1 = new Thread(r1, "t1");
    Thread t2 = new Thread(r1, "t2");
    t1.start();
    t2.start();
    }
    }
    執行結果為:

    t1 : 0
    t1 : 1
    t1 : 2
    t1 : 3
    t1 : 4
    t2 : 5
    t2 : 6
    t2 : 7
    t2 : 8
    t2 : 9
    上面的程式碼的run()函式實現了同步,使每次打印出來的j總是不相同的。但實際上在整個run()函式中,我們只關心j的同步,而其餘程式碼同步與否我們是不關心的,所以可以對它進行以下修改:

    class TestSynchronized implements Runnable{
    static int j = 0;
    public void run(){
    for(int i=0; i<5; i++){
    //(1)

    synchronized(this){
    System.out.println(Thread.currentThread().getName() + " : " + j++);
    }
    try{
    Thread.sleep(100);
    }
    catch(InterruptedException e){
    System.out.println("Interrupted");
    }
    }
    }
    }
    public class TestThread{
    public static void main(String[] args){
    TestSynchronized r1 = new TestSynchronized();
    TestSynchronized r2 = new TestSynchronized();
    Thread t1 = new Thread(r1, "t1");
    Thread t2 = new Thread(r1, "t2");
    t1.start();
    t2.start();
    }
    }
    執行結果為:

    t1 : 0
    t2 : 1
    t1 : 2
    t2 : 3
    t1 : 4
    t2 : 5
    t1 : 6
    t2 : 7
    t1 : 8
    t2 : 9
    由於進行同步的範圍縮小了,所以程式的效率將提高。同時,程式碼(1)指出,當對大括號內的println()語句進行同步控制時,會取走當前物件的“鎖標誌”,即對當前物件“上鎖”,不讓當前物件下的其它執行緒執行當前物件的其它synchronized資料。