1. 程式人生 > >java基礎回顧(五)線程詳解以及synchronized關鍵字

java基礎回顧(五)線程詳解以及synchronized關鍵字

dom com stack 相互 ++ 關於 而是 。。 str

本文將從線程的使用方式、源碼、synchronized關鍵字的使用方式和陷阱以及一些例子展開java線程和synchronized關鍵字的內容。

一、線程的概念

線程就是程序中單獨順序的流控制。線程本 身不能運行,它只能用於程序中。

二、線程的實現

線程的實現有兩種方式:

1.繼承Thread類並重寫run方法

2.通過定義實現Runnable接口的類進而實現run方法

當用第一種方式時我們需要重寫run方法因為Thread類裏的run方法什麽也不做(見下邊的源碼),當用第二種方式時我們需要實現Runnable接口的run方法,然後使用new Thread(new Runnable())來生成線程對象,這時的線程對象的run方法就會調用Runnable的run方法,這樣我們自己編寫的run方法就執行了。

技術分享

將我們希望線程執行的代碼放在run方法中,然後通過start方法來啟動線程,start方法首先為線程的執行準備好系統資源,然後再去調用run方法。下邊例子中將會有代碼。

在jdk源碼裏start方法裏面調用了start0()方法,他是一個native方法,我們不可見。線程一經運行就不會受我們的控制,Thtead類裏其實有stop方法,但是不能通過挑用stop()方法,而是應該讓run方法自然結束。

有一些需要註意的地方用代碼舉例說明:

例一:

public class ThreadTest2 {
    public static void main(String[] args) {
//        Thread thread = new Thread(new Runnable() {
// @Override // public void run() { // for(int i = 0; i < 100; i++) { // System.out.println("hello" + i); // } // } // }); MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.start(); } }
class MyThread implements Runnable { @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println("hello" + i); } } }

上面的代碼是采用第二種方式通過定義實現Runnable接口的類進而實現run方法,重點看註釋掉的部分當生命一個簡單的Thread類時用匿名內部類來實現是一個比較好的方式。

例二:

public class ThreadTest3 {
    public static void main(String[] args) {
        Runnable r = new Thread3();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        
        t1.start();
        t2.start();
    }
}

class Thread3 implements Runnable {

    int i;
    
    @Override
    public void run() {
        
//        i是成員變量和局部變量的結果是不一樣的
//        對於i是成員變量的時候,所有線程共享這一個成員變量,不管有幾個線程只要i加到了10就會終止線程,所以最後的結果一定只有10個。
//        對於i是一個局部變量時,每個線程都會有一份局部變量的拷貝,並且線程與線程之間是互不影響的
//        int i = 0;
        
        while(true) {
            System.out.println("number = " + i++);
            
            try {
                Thread.sleep((long)(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(10 == i) {
                break;
            }
        }
    }
}

關於成員變量與局部變量:如果一個變量是成員變量,那麽多個線程對同一個對象的成員變量進行操作時,他們對成員變量是彼此影響的(也就是說一個線程對成員變量的海邊會影響到另一個線程)。如果一個變量是局部變量那麽每個線程都會有一個該局部變量的拷貝,一個線程對局部變量的改變是不會影響到其他線程的。

例三:

public class ThreadTest4 {
    public static void main(String[] args) {
        Runnable r = new Thread4();
        Thread t1 = new Thread(r);
        
//        重新聲明一個對象的話,就又會打印20個結果。這是因為不是原來的對象了相對應的成員變量是分別在兩個對象裏的,所以當然也是不會相互影響的。所以會打印20個結果。
//        r = new Thread4();
        Thread t2 = new Thread(r);
        
        t1.start();
        t2.start();
    }
}

class Thread4 implements Runnable {

    int i;
    
    @Override
    public void run() {
        
        while(true) {
            System.out.println("number = " + i++);
            
            try {
                Thread.sleep((long)(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(10 == i) {
                break;
            }
        }
    }
}

結果是輸出number = 0到number = 9而且只順序輸出一遍,但是如果沒有註釋掉r = new Thread4();的話程序就會交替輸出兩遍number = 0到number = 9。其實就是因為重新聲明了對象兩個線程作用的成員變量不是一個,從而會輸出兩次。

三、synchronized關鍵字(重點介紹)

為什麽要引入同步機制?

在多線程環境中,可能會有兩個甚至多個線程試圖同時訪問一個有限的資源。這時就會發生許多意想不到的資源沖突,必須對這種沖突進行預防由此引入同步機制。

synchronize關鍵字簡介:當synchronize關鍵字修飾一個方法時,該方法叫做同步方法。java中的每個對象都有一個鎖(或者叫做監視器),當訪問某個對象的synchronize方法時,表示將該對象上鎖(不是方法),此時其他任何線程都無法再去訪問該方法了,直到之前的那個線程執行方法完畢後或者是拋出了異常,那麽將該對象的鎖釋放掉,其他線程才能再去訪問該synchronize方法。這樣就實現了方法的單線程訪問。

synchronized關鍵字有兩種使用方法:
  1. 在方法前用synchronized關鍵字修飾。
  2. 使用synchronized代碼塊。
synchronized方法是一種粗粒度的並發控制,某一時刻只能有一個線程執行該synchronized方法,synchronized塊是一種細粒度的並發控制,只會將代碼塊中的代碼同步,位於方法內,synchronized塊之外的方法裏的代碼是可以被多個線程同時訪問到的。

接下來我們用實例來看一看synchronized關鍵字的特性以及工作方式

例一:

public class ThreadTest5 {
    public static void main(String[] args) {
        Example example = new Example();
        Thread t1 = new TheThread(example);//        example = new Example(); //加上這行代碼的話,兩個線程就會交替執行,說明synchronize關鍵字是作用於對象層面上的。
        Thread t2 = new TheThread2(example);

        t1.start();
        t2.start();
    }
}

class Example {
    
    public synchronized void execute() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("hello: " + i);
        }
    }

    public synchronized void execute2() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("world: " + i);
        }
    }
}

class TheThread extends Thread {
    private Example example;

    public TheThread(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread2 extends Thread {
    private Example example;

    public TheThread2(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

執行結果先順序輸出hello: 0到hello: 19 再順序輸出world: 0到world: 19

需要註意的是如果重新生成一個Example對象的話就不是順序輸出了而是交替不規則的輸出了:

技術分享

只截取了部分數據,之所以交替輸出是因為synchronized關鍵字是作用於對象上的,兩個線程調用兩個不同的對象的synchronized方法不會發生搶占,所以兩個線程同時進行。只有一個對象的時候,如果一個對象有多個synchronize方法,某一個時刻某個線程已經進入到了某個synchronize方法,那麽在該方法沒有執行完畢前,其他線程是無法訪問到該對象的任何synchronize方法的。所以是順序輸出。

例二:

public class ThreadTest6 {
    public static void main(String[] args) {
        Example2 example = new Example2();
        Thread t1 = new TheThread3(example);
        example = new Example2();
        Thread t2 = new TheThread4(example);

        t1.start();
        t2.start();
    }
}

class Example2 {
//    synchronized修飾的方法如果是靜態方法那麽synchronized就不是作用於方法所在的對象了,而是方法所在對象的class對象。也就是類本身,對象有多個但是對象所對應的的class對象肯定是只有一個
    public synchronized static void execute() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("hello: " + i);
        }
    }

    public synchronized static void execute2() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("world: " + i);
        }
    }
}

class TheThread3 extends Thread {
    private Example2 example;

    public TheThread3(Example2 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread4 extends Thread {
    private Example2 example;

    public TheThread4(Example2 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

例二的執行結果是 先順序輸出hello: 0到hello: 19 再順序輸出world: 0到world: 19

這時候就奇怪了,例二和例一(沒有註釋第五行代碼的情況下)的區別只有static修飾的區別怎麽結果完全不一樣。

如果某個synchronize方法是static的,那麽當線程訪問該方法時,他的鎖並不是synchronize方法所在的對象,而是synchronize方法所在的對象所對應的的class對象,因為java中無論一個類有多少個對象,這些對象只會唯一對應一個class對象,因此當線程分別訪問同一個類的兩個對象的兩個static synchronized方法時,他們的執行順序也是順序的,也就是說一個線程先去執行方法,執行完畢後另一個線程才開始執行。

例三:

public class ThreadTest7 {
    public static void main(String[] args) {
        Example3 e = new Example3();
        TheThread5 t1 = new TheThread5(e);
        // e = new Example3();
        TheThread6 t2 = new TheThread6(e);

        t1.start();
        t2.start();
    }
}

class Example3 {
    // 沒有實際意義,任何一個對象都行,不寫也沒事用this就好。
    private Object object = new Object();

    public void execute() {
        // synchronized代碼塊
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("hello: " + i);
            }
        }

    }

    public void execute2() {
        synchronized (this) {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("world: " + i);
            }
        }

    }
}

class TheThread5 extends Thread {
    private Example3 example;

    public TheThread5(Example3 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread6 extends Thread {
    private Example3 example;

    public TheThread6(Example3 example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

這個例子主要說明的是synchronized關鍵字第二種用法:synchronized代碼塊。

上面已經介紹過了synchronized代碼塊是一種細粒度的並發控制,只會將代碼塊中的代碼同步,位於方法內,synchronized塊之外的方法裏的代碼是可以被多個線程同時訪問到的。相對於synchronized代碼塊來說synchronized方法是一種更加重量級的並發控制機制。

好啦今天就這些吧,洗洗睡了。。。

java基礎回顧(五)線程詳解以及synchronized關鍵字