1. 程式人生 > >java多執行緒(1):執行緒的建立和多執行緒的安全問題

java多執行緒(1):執行緒的建立和多執行緒的安全問題

前言

java多執行緒多用於服務端的高併發程式設計,本文就java執行緒的建立和多執行緒安全問題進行討論。

正文

一,建立java執行緒

建立java執行緒有2種方式,一種是繼承自Thread類,另一種是實現Runnable介面。由於java只支援單繼承,所以很多時候繼承也是一種很寶貴的資源,我們多采用繼承Runnable介面的方式。下面來看一下這兩種方式。

1,繼承Thread,其中包括關鍵的4步

package com.jimmy.basic;

class MyThread extends Thread{  // 1,繼承Thread

    public void run() {         // 2,重寫run()方法
for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()); } } } public class ExtendsThread { public static void main(String[] args) { MyThread myThread1 = new MyThread(); // 3,建立執行緒例項 MyThread myThread2 = new MyThread(); myThread2.start(); // 4,start()方法啟動執行緒
myThread1.start(); } }

多執行緒執行的程式碼都寫在run()方法體裡面。上面程式碼run方法中表示迴圈輸出10次執行緒的名字。測試程式碼中建立2個執行緒並啟動,那麼這兩個執行緒交替執行各自run方法中的程式碼,共產生20條輸出記錄。上面這段程式碼的輸出如下:

Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread
-0 Thread-1 Thread-1 Thread-1

2,實現Runnable介面

package com.jimmy.basic;

class MyThread2 implements Runnable{   // 1,類實現Runnable介面

    @Override
    public void run() {                // 2,實現run方法
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

}
public class ImplementsRunnable {
    public static void main(String[] args) {
        MyThread2 mt = new MyThread2(); //  3,例項化介面Runnable子類物件

        Thread thread1 = new Thread(mt);//  4,將Runnable子類物件傳遞給Thread類的建構函式
        Thread thread2 = new Thread(mt);

        thread1.start();// 5,開啟執行緒
        thread2.start();
    }
}

實現介面是我們推薦的建立執行緒的方法。Runnable介面中只有一個run方法,我們在建立Thread執行緒物件時,將實現了Runnable介面的子類物件傳遞給Thread的建構函式:Thread(Runnable target)。此時再使用start()方法開啟執行緒時,就會執行Runnable介面的子類中的run方法。我們看下Thread的原始碼

//Thread類的部分原始碼
class Thread implements Runnable {

    private Runnable target;  // 持有Runnable型別變數

    public Thread(Runnable target) {  // 建構函式,構造過程藉助於init函式
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        //這裡略去了函式其他傳來的引數的操作 
        this.target = target;
    }
    public synchronized void start() {
        //這裡略去了其他初始化步驟
        run();
    }
    @Override
    public void run() {  
        if (target != null) {  // 如果有Runnable介面子類物件傳進來,就執行其run方法。不然什麼都不做。
            target.run();
        }
    }
}

截取了Thread類原始碼中的一部分。可以看出,Thread類中持有一個Runnable介面型別變數,並提供該介面變數的建構函式,雖然其構造過程放到了init()方法中了,一樣的。重點是Thread類的run方法會判斷建立執行緒的時候是否傳入了Runnable子類物件,如果有,就執行Runnable子類物件的run()方法。

所以Runnable介面在建立執行緒時,跟前面直接繼承Thread類不同。要先例項化Runnable子類物件,然後在建立Thread類時,將其作為引數傳遞給Thread類的建構函式。其執行結果跟前面類似,20條記錄交替執行。

二,多執行緒的安全問題

我們看到,前面的程式碼中,每個執行緒在各自的棧記憶體中交替執行,互不影響。之所以互不影響,是因為run方法中程式碼沒有操作執行緒共享的變數。一旦各個執行緒都要操作共享變數,那麼就可能會出現執行緒安全問題。下面來看一個小例子,這個例子中4個執行緒操作同一個共享變數。我們來看一下會出現什麼問題,以及怎麼解決。

package com.jimmy.basic;

class SellTickets implements Runnable {

    private int tickets = 10;  // 共享變數

    @Override
    public void run() {   // 實現run方法
        sell();      // 呼叫sell方法
    }

    public void sell(){
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + "..." + tickets);
            tickets--;
        }
    }
}

public class TicketsSharedVariable {
    public static void main(String[] args) {

        SellTickets sellTickets = new SellTickets(); // 例項化介面物件

        Thread thread1 = new Thread(sellTickets); // 只有傳入Runnable子類物件的Thread才能共享變數
        Thread thread2 = new Thread(sellTickets);
        Thread thread3 = new Thread(sellTickets);
        Thread thread4 = new Thread(sellTickets);

        thread4.start();  // 啟動執行緒
        thread3.start();
        thread2.start();
        thread1.start();
    }
}

注意,tickets變數定義在Runnable介面子類中,並不是我們說它是共享變數,它就是共享變數。而是將Runnable子類物件傳遞給Thread的建構函式,傳遞後的執行緒才能共享這個tickets變數。

像下面這樣就不會是共享變數,而是各個執行緒的私有變數。

Thread thread1 = new SellTickets2();  // 不傳參而建立的執行緒,每一個都有自己的變數
Thread thread2 = new SellTickets2();
Thread thread3 = new SellTickets2();
Thread thread4 = new SellTickets2();

當執行緒操作共享變數時,問題就出現了。下面是上面程式碼的輸出。

Thread-3...10
Thread-0...10
Thread-2...10
Thread-1...10
Thread-2...7
Thread-0...8
Thread-3...9
Thread-0...4
Thread-2...5
Thread-1...6
Thread-2...1
Thread-0...2
Thread-3...3

從輸出上來看,很明顯出現了執行緒安全的問題,這樣的操作顯然是不正確的。究其原因,是各個執行緒在進行sell方法操作時,搶佔了執行順序。我們希望一個執行緒在操作變數的時候,不會被其他執行緒干擾。也就是說,如果一個執行緒在執行sell方法的時候具有原子性,也就是不能有其他執行緒再來執行sell方法。

java保證操作的原子性很簡單,就是synchronized關鍵字。該關鍵字既可以用來修飾程式碼塊,也可以用來修飾函式。synchronized可以理解為加鎖,為程式碼塊加鎖,為函式加鎖。既然是加鎖,那麼鎖怎麼來表示呢?“鎖”也是物件,在程式碼塊上使用要顯示加鎖,如下:

Object obj = new Object();

public void sell(){
        synchronized (obj) { // 鎖物件可以是任意物件
            while (true) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "..." + tickets);
                    tickets--;
                }

            }
        }       
    }

上面就是同步程式碼塊的使用,將需要同步的程式碼放進同步程式碼塊,就可以實現執行緒同步。既然是對需要同步的程式碼進行封裝,就可以將synchronized用在函式上,用法如下:

public synchronized void sell() {  // synchronized修飾函式,使用的是this鎖物件。
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "..." + tickets);
                tickets--;
            }

        }
    }

一般都會使用函式來封裝同步程式碼,再用synchronized來修飾函式,實現執行緒同步。注:static靜態函式使用的是“類名.class”鎖物件。

最後說一下同步程式碼塊和同步函式的區別。函式使用固定的“this”鎖,而程式碼塊的鎖物件可以任意,如果執行緒任務只需要一個同步時可用同步函式,如果需要多個同步時,必須使用不同的鎖來區分。

總結

執行緒的安全需要同步來實現。