1. 程式人生 > >多執行緒執行順序以及run方法的呼叫

多執行緒執行順序以及run方法的呼叫

1.多執行緒的執行順序

先來看一個例子:

public class test {

	public static void main(String[] args) {
	    System.out.println(Thread.currentThread().getName());
	}
}

輸出結果:

main

這個例子說明一個問題:一個Java程式至少會有1個執行緒在執行,就如上面的main()方法,它是由JVM建立的一個叫main的執行緒呼叫的。控制檯輸出的就是這個執行緒的名字。

接著再看下面這段程式碼:

public class MyThread extends Thread{

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println(Thread.currentThread().getName());
        System.out.println("執行結束!");
    }

    @Override
    public void run() {
        //super.run();
        System.out.println(Thread.currentThread().getName());

    }
}

輸出結果:

main
執行結束!
Thread-0

或者是:

main
Thread-0
執行結束!

上段程式碼說明了兩個問題:

  1. 上述程式碼有兩個執行緒同時在執行:一個是JVM建立的main執行緒,該執行緒呼叫main函式,另一個是自己建立的MyThread執行緒類,名稱是Thread-0,該執行緒呼叫run函式
  2. main()函式和run()函式中的列印結果順序不確定。這是因為在使用多執行緒時,程式碼的執行結果與程式碼執行順序或呼叫順序是無關的。執行緒是一個子任務,上面程式碼有兩個執行緒,意味著兩個子任務各幹各的事,互不干擾,一個執行緒正在呼叫main函式,另一個執行緒在這個時候去呼叫run函式,先列印哪個函式裡面的東西都不一定。雖然main()這個函式比run()函式先呼叫,意思是時間起點早,但是具體到函式裡面的語句呼叫順序就說不清楚了。
再比如下面的例子:
public class Test3 {

    public static void main(String[] args) {
        MyThread3 t1 = new MyThread3("thread1");
        MyThread3 t2 = new MyThread3("thread2");
        MyThread3 t3 = new MyThread3("thread3");
        MyThread3 t4 = new MyThread3("thread4");
        MyThread3 t5 = new MyThread3("thread5");
        System.out.println(Thread.currentThread().getName());
        t1.start();
        System.out.println("t1準備好:");
        t2.start();
        System.out.println("t2準備好");
        t3.start();
        System.out.println("t3準備好");
        t4.start();
        System.out.println("t4準備好");
        t5.start();
        System.out.println("t5準備好");

    }
}
public class MyThread3 extends Thread {

    private String name;
    public MyThread3(String name){
        super(name);
        this.name = name;
    }

    @Override
    public void run() {

        System.out.println("當前執行run方法的執行緒是:" + Thread.currentThread().getName());
        try {
            for(int j = 0; j < 5; j++){
                //int time = (int)(Math.random() * 1000);
                int time = 500;
                Thread.sleep(time);
                System.out.println("j=" + j +",run=" + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果:

main
t1準備好:
t2準備好
t3準備好
t4準備好
當前執行run方法的執行緒是:thread1
當前執行run方法的執行緒是:thread2
t5準備好
當前執行run方法的執行緒是:thread3
當前執行run方法的執行緒是:thread5
當前執行run方法的執行緒是:thread4
j=0, run=thread5
j=0, run=thread3
j=0, run=thread4
j=0, run=thread2
j=0, run=thread1
j=1, run=thread1
j=1, run=thread3
j=1, run=thread5
j=1, run=thread2
j=1, run=thread4
j=2, run=thread4
j=2, run=thread2
j=2, run=thread1
j=2, run=thread3
j=2, run=thread5
j=3, run=thread4
j=3, run=thread5
j=3, run=thread1
j=3, run=thread3
j=3, run=thread2
j=4, run=thread2
j=4, run=thread1
j=4, run=thread4
j=4, run=thread3
j=4, run=thread5

上述程式碼開了5個執行緒,每個執行緒的run函式被呼叫5次,從結果中可以看出,5個執行緒run函式的執行順序是隨機的。執行緒執行的順序和程式碼的順序無關。

2.關於run()方法的呼叫

原始碼中是這樣說明run()方法的呼叫的:

If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns. 

Subclasses of Thread should override this method.

意思就是:如果這個執行緒是使用Runnable物件構造的,那麼實際上是呼叫的Runnable物件的run方法;否則的話,方法不做任何處理。Thread類的子類應該重寫該方法。

上面的內容可以進一步補充一下,因為Thread類自身繼承了Runnable介面,因此也可以採用繼承了Thread的一個類的物件作為引數傳入,構造Thread例項,這種情況下,實際上呼叫的則是該傳入的Thread物件的run方法。

run方法原始碼如下(原始碼中的target就是一個Runnable物件):

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

我們先進行測試一下:

public class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
        System.out.println("構造方法的列印:" + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run方法的列印:" + Thread.currentThread().getName());
    }
}
public class Run2 {
    public static void main(String[] args) {
        
        MyThread myThread = new MyThread("myThread");
        myThread.start();
        //myThread.run();
    }
}

列印結果為:

構造方法的列印:main

run方法的列印:myThread

把Run2修改為如下:

public class Run2 {
    public static void main(String[] args) {
        
        MyThread myThread = new MyThread("myThread");
        //myThread.start();
        myThread.run();
    }
}

列印結果:

構造方法的列印:main
run方法的列印:main

從上面的例子我們可以看出:MyThread.java類的建構函式是被main執行緒呼叫的,啟動start()後的隱式呼叫的run方法是被建立的名叫myThread執行緒呼叫的,而顯式呼叫的run方法則是main執行緒呼叫的。

接著,我們用下面的例子進一步來說明run的呼叫問題:

public class MyThread_t4 extends Thread {

    private int count = 5;

    @Override
    public void run() {
        super.run();
        count--;
        System.out.println("由" + this.currentThread().getName() + "計算.count=" + count);
    }
}
public class Run_t4 {
    public static void main(String[] args) {
//        MyThread_t4 myThread_t4 = new MyThread_t4();
//        Thread a = new Thread(myThread_t4,"A");
//        Thread b = new Thread(myThread_t4,"B");
//        Thread c = new Thread(myThread_t4,"C");
//        Thread d = new Thread(myThread_t4,"D");
//        Thread e = new Thread(myThread_t4,"E");
        MyThread_t4 a = new MyThread_t4();
        MyThread_t4 b = new MyThread_t4();
        MyThread_t4 c = new MyThread_t4();
        MyThread_t4 d = new MyThread_t4();
        MyThread_t4 e = new MyThread_t4();

        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}
上面的程式碼會建立a,b,c,d,e四個執行緒,每個執行緒都有自己的run方法,每個執行緒都有各自的count變數,自己減少自己的count變數的值,所以輸出結果為4.

輸出結果:

由Thread-0計算.count=4
由Thread-3計算.count=4
由Thread-1計算.count=4
由Thread-2計算.count=4
由Thread-4計算.count=4

我們把測試程式碼修改為如下:

public class Run_t5 {
    public static void main(String[] args) {
        MyThread_t4 myThread_t4 = new MyThread_t4();
        Thread a = new Thread(myThread_t4,"A");
        Thread b = new Thread(myThread_t4,"B");
        Thread c = new Thread(myThread_t4,"C");
        Thread d = new Thread(myThread_t4,"D");
        Thread e = new Thread(myThread_t4,"E");
//        MyThread_t4 a = new MyThread_t4();
//        MyThread_t4 b = new MyThread_t4();
//        MyThread_t4 c = new MyThread_t4();
//        MyThread_t4 d = new MyThread_t4();
//        MyThread_t4 e = new MyThread_t4();

        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

輸出結果如下:

由A計算.count=4
由C計算.count=2
由B計算.count=3
由E計算.count=0
由D計算.count=1

從該結果可以看出,產生了非執行緒安全問題。造成非執行緒安全問題的主要原因是因為多個執行緒同時訪問了一個變數。為什麼上面都是各自執行緒訪問各自的count變數,為什麼這裡就是訪問同一個count變數呢。原因是因為這裡建立了a,b,c,d,e四個執行緒,但是這四個執行緒都是使用Thread物件myThread_t4作為引數構造的,那麼呼叫的run函式實際上是myThread_t4物件的run函式(如果是使用Runnable物件構造的,那麼實際上是呼叫的Runnable物件的run方法),因此不再是各自的四個run函式,而是一個run函數了,都去呼叫這個函式,當呼叫一個run函式的時候,對這個函式而言,內部就是同步操作,但是由於這裡run函式內部count--這麼一句,在某些JVM中,i--操作分3步,在3個步驟中,如果有多個執行緒同時訪問,就會出現執行緒安全問題。解決的辦法就是將run方法進行同步(加synchronized),當然,這裡主要想說明的不是執行緒安全問題,而是run函式的呼叫問題,修改程式碼MyThread_t4如下:

public class MyThread_t4 extends Thread {

    private int count = 5;

    @Override
    synchronized public void run() {
        super.run();
        count--;
        System.out.println("由" + this.currentThread().getName() + "計算.count=" + count);
    }
}

當然如下把程式碼修改成下面這樣(把count--替換成count=count-1),也不會出現執行緒安全問題(因為多個執行緒訪問同一個run()函式,run函式內部是同步執行的):

public class MyThread_t4 extends Thread {

    private int count = 5;

    @Override
    public void run() {
        super.run();
        count=count-1;
        //count--;
        System.out.println("由" + this.currentThread().getName() + "計算.count=" + count);
    }
}

再執行測試函式Run_t5,

輸出結果為:

由C計算.count=4
由D計算.count=3
由A計算.count=2
由B計算.count=1
由E計算.count=0