1. 程式人生 > >Thread類相關問題

Thread類相關問題

1、start()方法和run()方法

呼叫start()方法會啟動新執行緒,新執行緒會自動執行run()方法,run()方法執行完畢,執行緒生命週期結束。start()方法不能被重複呼叫,否則拋異常程式終止。
run()方法是執行緒的執行體,可以與普通方法一樣被主動重複呼叫,單獨呼叫run()方法的話,會在當前執行緒中執行run(),而不會啟動新執行緒,啟動新執行緒需要start()方法。

public static void main(String[] args) {
		Thread thread = new Thread(new Runnable() {
			
			public
void run() { System.out.println("currentThreadName=" + Thread.currentThread()); } }); thread.run();//將run()方法當普通方法呼叫,不會建立新執行緒 thread.run();//將run()方法當普通方法呼叫,可以重複呼叫 thread.start();//建立新執行緒,並自動呼叫run()方法 //thread.start();//多次呼叫start()拋異常 }

執行結果:
這裡寫圖片描述
Thread的start()方法原始碼
這裡寫圖片描述
思考:為何start()方法要加synchronized關鍵字修飾,不加會有什麼問題?

2、synchronized關鍵字

在Java中,每個物件有且僅有一個同步鎖,意味著同步鎖依賴物件而存在。當執行緒訪問某物件的synchronized區域(synchronized方法或程式碼塊)時,其他執行緒對該物件的所有synchronized區域的訪問將被阻塞(因為方法或程式碼塊所屬的物件被鎖定)。而對該物件的非同步區域可以正常訪問。
synchronized關鍵字被編譯後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件。如果程式中synchronized明確指定了物件引數(synchronized修飾程式碼塊),那就是這個物件的reference;如果沒有明確指定(synchronized修飾的是方法),那就根據synchronized修飾的是例項方法還是類方法,去取對應的物件例項或者類物件作為鎖物件。

                            synchronized例項1
                  執行緒鎖定相同物件,其他執行緒對該synchronized區域不能訪問
class MyRunable implements Runnable {
    
    public void run() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread() + " loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }
}

public class ThreadSynchronized {

    public static void main(String[] args) {  
        Runnable runnable = new MyRunable();     // 新建“Runnable物件”

        Thread t1 = new Thread(runnable, "t1");  // 新建“執行緒t1”, t1是基於demo這個Runnable物件
        Thread t2 = new Thread(runnable, "t2");  // 新建“執行緒t2”, t2是基於demo這個Runnable物件
        t1.start();                          // 啟動“執行緒t1”
        t2.start();                          // 啟動“執行緒t2” 
    } 
}

執行結果
這裡寫圖片描述
先將t1執行緒執行完,再執行t2執行緒。
因為synchronized(this)鎖定的是MyRunable類的物件例項runnable,而t1與t2兩個執行緒的執行體是同一個runnable物件,因此當t1鎖定時,t2並不能訪問,直到t1執行完畢釋放鎖,t2才能獲取鎖繼續執行。

                             synchronized示例2
               執行緒鎖定相同物件,其他執行緒對該物件的其他synchronized區域也不能訪問
class Count {

    // 含有synchronized同步塊的方法
    public void synMethod() {
        synchronized(this) {//鎖定Count物件例項
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }

    // 也包含synchronized同步塊的方法
    public void synMethod2() {
        synchronized(this) {//鎖定Count物件例項
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " synMethod2 loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }
    }
}

public class ThreadSynchronizedTwo {

    public static void main(String[] args) {  
        final Count count = new Count();
        // 新建t1, t1會呼叫“count物件”的synMethod()方法
        Thread t1 = new Thread(
                new Runnable() {
                    public void run() {
                        count.synMethod();
                    }
                }, "t1");

        // 新建t2, t2會呼叫“count物件”的nonSynMethod()方法
        Thread t2 = new Thread(
                new Runnable() {
                    public void run() {
                        count.synMethod2();
                    }
                }, "t2");  


        t1.start();  // 啟動t1
        t2.start();  // 啟動t2
    } 
} 

執行結果
這裡寫圖片描述
雖然兩個執行緒訪問的程式碼塊不同,但仍是先將t1執行緒執行完,再執行t2執行緒。
因為兩個執行緒鎖定的是同一個物件count,即使兩執行緒訪問不同的synchronized同步程式碼塊,t2仍需要等待t1執行完畢,釋放count物件鎖之後才能執行。

synchronized示例3
執行緒鎖定相同物件,其他執行緒對該物件的非synchronized區域可以訪問
class MyCount {

    // 含有synchronized同步塊的方法
    public void synMethod() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }

    // 非同步的方法
    public void nonSynMethod() {
        try {  
            for (int i = 0; i < 5; i++) {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);  
            }
        } catch (InterruptedException ie) {  
        }
    }
}

public class ThreadSynchronizedThree {

    public static void main(String[] args) {  
        final MyCount count = new MyCount();
        // 新建t1, t1會呼叫“count物件”的synMethod()方法
        Thread t1 = new Thread(
                new Runnable() {
                    public void run() {
                        count.synMethod();
                    }
                }, "t1");

        // 新建t2, t2會呼叫“count物件”的nonSynMethod()方法
        Thread t2 = new Thread(
                new Runnable() {
                    public void run() {
                        count.nonSynMethod();
                    }
                }, "t2");  


        t1.start();  // 啟動t1
        t2.start();  // 啟動t2
    } 
}

執行結果
這裡寫圖片描述
t1與t2交替執行。
雖然t1保留了count物件鎖,但t2執行的是非synchronized同步區域,不需要等待物件鎖,所以t1獲取物件鎖,不影響t2的執行。

synchronized示例4
執行緒鎖定不同物件,對不同物件的synchronized區域可以訪問
class MyThread extends Thread {
    
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        synchronized(this) {
            try {  
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 休眠100ms
                    System.out.println(Thread.currentThread().getName() + " loop " + i);  
                }
            } catch (InterruptedException ie) {  
            }
        }  
    }
}

public class ThreadSynchronizedFour {

    public static void main(String[] args) {  
        Thread t1 = new MyThread("t1");  // 新建“執行緒t1”
        Thread t2 = new MyThread("t2");  // 新建“執行緒t2”
        t1.start();                      // 啟動“執行緒t1”
        t2.start();                      // 啟動“執行緒t2” 
    } 
}

執行結果
這裡寫圖片描述
t1與t2交替執行。
因為t1與t2鎖定的是不同的MyThread物件例項,因此相互不影響。
這裡寫圖片描述

3、例項鎖與全域性鎖

例項鎖:鎖在某個例項物件上,例項鎖對應的是synchronized關鍵字。
全域性鎖:鎖針對的是類,無論該類有多少物件,執行緒都共享該鎖,全域性鎖對應的是static synchronized關鍵字(或者是鎖在該類的class或者classloader物件上)。
假設程式碼如下:

pulbic class Something {
    public synchronized void syncA(){}
    public synchronized void syncB(){}
    public static synchronized void staticSyncA(){}
    public static synchronized void staticSyncB(){}
}   
 Something x = new Something();
Something y = new Something();

分析下面4組情況能否同時訪問:
(1)x.syncA與x.syncB
(2)x.syncA與y.syncA
(3)x.staticSyncA與y.staticSyncB
(4)x.syncA與Something.staticSyncB
抓住synchronized同步鎖依賴物件而存在,只要分析出鎖定的物件是否相同,不難分析出:
(1) 不能同時訪問,因為它們鎖定的物件相同,都是x物件例項;
(2) 可以同時訪問,因為它們鎖定的物件不同,分別是x物件例項和y物件例項;
(3) 不能同時訪問,因為它們鎖定的物件相同,都是Something類例項;
(4) 不能同時訪問,因為它們鎖定的物件不同,分別是x物件例項和Something類例項。

4、執行緒等待wait()與執行緒喚醒notify()

在Object.java中,定義了wait(), notify()和notifyAll()等介面。
wait()的作用是讓當前執行緒(CPU正在執行的執行緒)進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖(而阻塞在sleep()方法時不會釋放鎖)。
而notify()和notifyAll()的作用,則是喚醒當前物件上的等待執行緒;notify()是喚醒單個執行緒(如有多個執行緒等待,隨機喚醒其中一個執行緒),而notifyAll()是喚醒所有的執行緒。


                      //Wait()使當前執行緒等待
class TestThread extends Thread{
	 public TestThread(String name) {
	     super(name);
	 }
	
	 public void run() {
	     synchronized (this) {
	         System.out.println(Thread.currentThread().getName()+" call notify()");
	         // 喚醒當前的wait執行緒
	         notify();
	     }
	 }
}

public class WhichThreadWait {

 public static void main(String[] args) {

	 TestThread t1 = new TestThread("t1");

     synchronized(t1) {
         try {
             // 啟動“執行緒t1”
             System.out.println(Thread.currentThread().getName()+" start t1");
             t1.start();

             // 主執行緒等待t1通過notify()喚醒。
             System.out.println(Thread.currentThread().getName()+" wait()");
             t1.wait();//為何這裡是主執行緒等待,而不是t1執行緒等待?

             System.out.println(Thread.currentThread().getName()+" continue");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 }
}

執行結果:
這裡寫圖片描述
t1.wait()為何這裡是主執行緒等待,而不是t1執行緒等待?
JDK中的解釋 “ Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. ”, wait()的作用是讓“當前執行緒”等待,即正在CPU上執行的執行緒。雖然t1.wait()是通過“執行緒物件t1”呼叫的wait()方法,但是執行t1.wait()的地方是在主執行緒,即主執行緒是當前執行緒,需要處於等待狀態。

喚醒所有在此監視器上等待的執行緒
public class WaitAndNotify {
    private static Object obj = new Object();
    public static void main(String[] args) {
    	  MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
    	  MyThread t3 = new MyThread("t3");
        t1.start();
        t2.start();
        t3.start();

        try {
            System.out.println(Thread.currentThread().getName()+" sleep(3000)");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized(obj) {
            System.out.println(Thread.currentThread().getName()+" notifyAll()");
            obj.notifyAll();//喚醒所有wait()執行緒
            //obj.notify();//喚醒單個wait()執行緒
        }
    }

    static class MyThread extends Thread{
        public MyThread(String name){
            super(name);
        }

        public void run() {
            synchronized (obj) {
                try {
                    // 列印輸出結果
                    System.out.println(Thread.currentThread().getName() + " wait");
                    //當前執行緒進入等待狀態,釋放鎖
                    obj.wait();
                    // 列印輸出結果
                    System.out.println(Thread.currentThread().getName() + " continue");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行結果:
這裡寫圖片描述
主執行緒通過notifyAll()喚醒所有等待的子執行緒。

思考: notify()是依據什麼喚醒等待執行緒的,即wait()等待執行緒與notify()之間通過什麼關聯起來的?為何notify(),wait()等方法是定義在Object中,而不是Thread中?
wait()與notify()方法的聯絡依據是物件同步鎖。使用wait()與notify()方法必須要有物件鎖,否則執行時拋非法監視器的異常。喚醒執行緒(負責喚醒等待執行緒的那個執行緒),只有在獲取該物件的同步鎖(必須與等待執行緒是同一個鎖),並且呼叫notify()或notifyAll()方法之後,才能喚醒等待執行緒。雖然等待執行緒被喚醒,但是它不能立即執行,需要等待喚醒執行緒釋放物件的同步鎖之後,等待執行緒才能獲取物件同步鎖進而繼續執行。
既然notify()與wait()依賴於同步鎖(不是依賴執行緒),而同步鎖是物件所持有,並且每個物件有且僅有一個。因此notify()與wait()等方法需要定義在Object()類,而不是Thread類中。

##5、執行緒同步之join()##
讓當前執行join()方法的執行緒等待,直到等待超時或擁有join()方法的執行緒執行完畢。

public class JoinTest{ 

 public static void main(String[] args){ 
     try {
         ThreadA t1 = new ThreadA("t1"); // 新建“執行緒t1”

         t1.start();                     // 啟動“執行緒t1”
         t1.join();                        //“主執行緒main()會等待"執行緒t1"完成”
         System.out.printf("%s finish\n", Thread.currentThread().getName()); 
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
 } 

 static class ThreadA extends Thread{

     public ThreadA(String name){ 
         super(name); 
     } 
     public void run(){ 
         System.out.printf("%s start\n", this.getName()); 

         // 延時操作
         try {
			sleep(3000);
		} catch (Exception e) {
			// TODO: handle exception