1. 程式人生 > >java多執行緒基礎(synchronize關鍵字)

java多執行緒基礎(synchronize關鍵字)

基礎知識

執行緒:程序(process)就是一塊包含了某些資源的記憶體區域。作業系統利用程序把它的工作劃分為一些功能單元。
執行緒:程序中所包含的一個或多個執行單元稱為執行緒(thread)。程序還擁有一個私有的虛擬地址空間,該空間僅能被它所包含的執行緒訪問。
執行緒和程序的區別如下:
1)一個程序至少有一個執行緒。執行緒的劃分尺度小於程序,使得多執行緒程式的併發性高。另外,程序在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,從而極大地提高了程式的執行效率。
2)執行緒在執行過程中與程序的區別在於每個獨立的執行緒有一個程式執行的入口、順序執行序列和程式的出口。但是執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。
3)從邏輯角度來看,多執行緒的意義在於一個應用程式中,有多個執行部分可以同時執行。但作業系統並沒有將多個執行緒看做多個獨立的應用來實現程序的排程和管理以及資源分配。

2 簡述執行緒的狀態及其轉換
1)New,建立一個執行緒,但是執行緒並沒有進行任何的操作。
2)Runnable,新執行緒從New狀態,呼叫start方法轉換到Runnable狀態。執行緒呼叫start方法向執行緒排程程式(JVM或者是作業系統)註冊一個執行緒,這個時候一切就緒只等CPU的時間。
3)Running,從Runnable狀態到Running狀態,執行緒排程根據排程策略的不同調度不同的執行緒,被排程執行的執行緒進入Running狀態,執行run方法。
4)Dead狀態,從Running狀態到Runnable,run方法執行完畢後,執行緒就會被拋棄,執行緒就進入Dead狀態。
5)Block狀態,從Running狀態到Block狀態,如果執行緒在執行的狀態中因為I/O阻塞、呼叫了執行緒的sleep方法以及呼叫物件的wait方法則執行緒將進入阻塞狀態,直到這些阻塞原因被結束,執行緒進入到Runnable狀態。

3 簡述執行緒的兩種建立方式以及它們的區別
建立執行緒的兩種方式:
1)使用Thread建立執行緒。Thread類是執行緒類,其每一個例項表示一個可以併發執行的執行緒。我們可以通過繼承該類並重寫run方法來定義一個具體的執行緒。其中重寫run方法的目的是定義該執行緒要執行的邏輯。啟動執行緒時呼叫執行緒的start()方法而非直接呼叫run()方法。start()方法會將當前執行緒納入執行緒排程,使當前執行緒可以開始併發執行。當執行緒獲取時間片段後會自動開始執行run方法中的邏輯。
2)使用Runnable建立執行緒。實現Runnable介面並重寫run方法來定義執行緒體,然後在建立執行緒的時候將Runnable的例項傳入並啟動執行緒。
兩種建立執行緒方式的區別:
使用Thread建立執行緒,編寫簡單,可以直接操縱執行緒,無需使用Thread.currentThread(),但是不能夠再繼承其他類。
使用Runnable建立執行緒可以將執行緒與執行緒要執行的任務分離開減少耦合,同時Java是單繼承的,定義一個類實現Runnable介面,這樣該類還可以繼承自其他類。

多執行緒實現方法

使用Thread建立線並啟動執行緒

       java.lang.Thread類是執行緒類,其每一個例項表示一個可以併發執行的執行緒。我們可以通過繼承該類並重寫run方法來定義一個具體的執行緒。其中
重寫run方法的目的是定義該執行緒要執行的邏輯。啟動執行緒時呼叫執行緒的start()方法而非直接呼叫run()方法。start()方法會將當前執行緒納入執行緒調
度,使當前執行緒可以開始併發執行。當執行緒獲取時間片段後會自動開始執行run方法中的邏輯。

public class TestThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
        System.out.println("我是執行緒");
        }
    }
}

建立預啟動執行緒

Thread thread = new TestThread();//例項化執行緒
    thread.start();//啟動執行緒

使用Runnable建立並啟動執行緒

實現Runnable介面並重寫run方法來定義執行緒體,然後在建立執行緒的時候將Runnable的例項傳入並啟動執行緒。
這樣做的好處在於可以將執行緒與執行緒要執行的任務分離開減少耦合,同時java是單繼承的,定義一個類實現Runnable介面這樣的做法可以更好的
去實現其他父類或介面。因為介面是多繼承關係。

public class TestRunnable implements Runnable{
 @Override
 public void run() {
     for(int i=0;i<100;i++){
         System.out.println("我是執行緒");
         }
     }
 }

啟動執行緒的方法:

     …
     Runnable runnable = new TestRunnable();
     Thread thread = new Thread(runnable);//例項化執行緒並傳入執行緒體
     thread.start();//啟動執行緒

使用內部類建立執行緒

通常我們可以通過匿名內部類的方式建立執行緒,使用該方式可以簡化編寫程式碼的複雜度,當一個執行緒僅需要一個例項時我們通常使用這種方式來
建立。
例如:
繼承Thread方式:

 Thread thread = new Thread(){ //匿名類方式建立執行緒
 public void run(){
     //執行緒體
     }
 };
 thread.start();//啟動執行緒

Runnable方式:

    Runnable runnable = new Runnable(){ //匿名類方式建立執行緒
         public void run(){
         }
    };
     Thread thread = new Thread(runnable);
     thread.start();//啟動執行緒

執行緒的方法

currentThread:方法可以用於獲取運行當前程式碼片段的執行緒

Thread current = Thread.currentThread();

獲取執行緒資訊
Thread提供了 獲取執行緒資訊的相關方法:
     long getId():返回該執行緒的識別符號
     String getName():返回該執行緒的名稱
     int getPriority():返回執行緒的優先順序
     Thread.state getState():獲取執行緒的狀態
     boolean isAlive():測試執行緒是否處於活動狀態
     boolean isDaemon():測試執行緒是否為守護執行緒
     boolean isInterrupted():測試執行緒是否已經中斷

執行緒優先順序


執行緒的切換是由執行緒排程控制的,我們無法通過程式碼來干涉,但是我們可以通過提高執行緒的優先順序來最大程度的改善執行緒獲取時間片的機率。
執行緒的優先順序被劃分為10級,值分別為1-10,其中1最低,10最高。執行緒提供了3個常量來表示最低,最高,以及預設優先順序:
Thread.MIN_PRIORITY,
Thread.MAX_PRIORITY,
Thread.NORM_PRIORITY
設定優先順序的方法為:
void setPriority(int priority)

守護執行緒

守護執行緒與普通執行緒在表現上沒有什麼區別,我們只需要通過Thread提供的方法來設定即可:
void setDaemon(boolean )
當引數為true時該執行緒為守護執行緒。
守護執行緒的特點是,當程序中只剩下守護執行緒時,所有守護執行緒強制終止。
GC就是執行在一個守護執行緒上的。
需要注意的是,設定執行緒為後臺執行緒要在該執行緒啟動前設定。

    Thread daemonThread = new Thread();
    daemonThread.setDaemon(true);
    daemonThread.start();

sleep方法

Thread的靜態方法sleep用於使當前執行緒進入阻塞狀態:
static void sleep(long ms)
該方法會使當前執行緒進入阻塞狀態指定毫秒,當指定毫秒阻塞後,當前執行緒會重新進入Runnable狀態,等待分配時間片。
該方法宣告丟擲一個InterruptException。所以在使用該方法時需要捕獲這個異常
注:改程式可能會出現”跳秒”現象,因為阻塞一秒後執行緒並非是立刻回到running狀態,而是出於runnable狀態,等待獲取時間片。那麼這段等待
時間就是”誤差”。所以以上程式並非嚴格意義上的每隔一秒鐘執行一次輸出。

yield方法:

Thread的靜態方法yield:
static void yield()
該方法用於使當前執行緒主動讓出當次CPU時間片回到Runnable狀態,等待分配時間片。

join方法

void join()
該方法用於等待當前執行緒結束。此方法是一個阻塞方法。
該方法宣告丟擲InterruptException。

執行緒同步

synchronized關鍵字
多個執行緒併發讀寫同一個臨界資源時候會發生”執行緒併發安全問題“
常見的臨界資源:
多執行緒共享例項變數
多執行緒共享靜態公共變數
若想解決執行緒安全問題,需要將非同步的操作變為同步操作。
所謂非同步操作是指多執行緒併發的操作,相當於各幹各的。
所謂同步操作是指有先後順序的操作,相當於你幹完我再幹。

同步程式碼塊(synchronized 關鍵字 ),同步程式碼塊包含兩部分:一個作為鎖的物件的引用,一個作為由這個鎖保護的程式碼塊
這個比較難理解故寫了下面程式碼幫助理解

/**
 * 多執行緒併發安全問題
 * 當多個執行緒同時操作同一資源時,由於
 * 執行緒切換時機不確定,導致出現邏輯混亂。
 * 嚴重時可能導致系統崩潰。
 * @author ylg
 *
 */
public class SyncDemo1 {
    public static void main(String[] args) {
        /*
         * 當一個方法中的區域性內部類想引用該方法
         * 的其他區域性變數時,這個變數必須被宣告
         * 為final的
         */
        final Table table = new Table();
        Thread t1 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    Thread.yield();//模擬執行緒切換
                    System.out.println(
                        getName()+":"+bean  
                    );
                }
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    Thread.yield();//模擬執行緒切換
                    System.out.println(
                        getName()+":"+bean  
                    );
                }
            }
        };
        t1.start();
        t2.start();
    }
}
class Table{
    //20個豆子
    private int beans = 20;
    /**
     * 當一個方法被synchronized修飾後,該方法
     * 成為"同步方法"。多個執行緒不能同時進入到
     * 方法內部。
     * @return
     */
    public synchronized int getBean(){
        if(beans==0){
            throw new RuntimeException("沒有豆子了!");
        }
        Thread.yield();//模擬執行緒切換
        return beans--;
    }
}
/**
 * 有效的縮小同步範圍可以保證在
 * 安全的前提下提高了併發的效率
 * @author ylg
 *
 */
public class SyncDemo2 {
    public static void main(String[] args) {
        final Shop shop = new Shop();
        Thread t1 = new Thread(){
            public void run(){
                shop.buy();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                shop.buy();
            }
        };
        t1.start();
        t2.start();
    }
}
class Shop{
    /*
     * 在方法上使用synchroinzed,同步監視器物件即當前方法所屬物件:this
     */
//  public synchronized void buy(){
    public void buy(){
        try{
            Thread t = Thread.currentThread();
            System.out.println(t+"正在挑選衣服..");
            Thread.sleep(5000);
            /*
             * 同步塊可以縮小同步範圍。
             * 但是必須保證"同步監視器"即:"上鎖物件"是同一個才可以。
             * 通常,在一個方法中使用this所謂同步監視器物件即可。
             */
            synchronized (this) {
                System.out.println(t+"正在試衣服..");
                Thread.sleep(5000);
            }       

            System.out.println(t+"結賬離開");
        }catch(Exception e){
        }

    }
}

“`
/**
* synchronized也成為”互斥鎖”
* 當synchronzed修飾的是兩段程式碼,但是”鎖物件”相同時,這兩段程式碼就是互斥的。
* @author ylg
*
*/
public class SyncDemo4 {
public static void main(String[] args) {
final Boo b = new Boo();
Thread t1 = new Thread(){
public void run(){
b.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
b.methodB();
}
};
t1.start();
t2.start();
}
}

class Boo{
public synchronized void methodA(){
Thread t = Thread.currentThread();
System.out.println(t+”正在呼叫方法A”);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(t+”呼叫方法A完畢”);
}
public synchronized void methodB(){
Thread t = Thread.currentThread();
System.out.println(t+”正在呼叫方法B”);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(t+”呼叫方法B完畢”);
}
}“`

wait和notify
多執行緒之間需要協調工作。
例如,瀏覽器的一個顯示圖片的 displayThread想要執行顯示圖片的任務,必須等待下載執行緒downloadThread將該圖片下載完畢。如果圖片還
沒有下載完,displayThread可以暫停,當downloadThread完成了任務後,再通知displayThread“圖片準備完畢,可以顯示了”,這時,
displayThread繼續執行。
以上邏輯簡單的說就是:如果條件不滿足,則等待。當條件滿足時,等待該條件的執行緒將被喚醒。在Java中,這個機制的實現依賴於
wait/notify。等待機制與鎖機制是密切關聯的