(二)物件以及變數的併發訪問--synchronized的使用細節,用法

_Ennio 發表於2019-08-13

具體的記錄synchronized關鍵的各種使用方式,注意事項。感覺一步一步跟我來都可以看懂滴

大致是按照以下思路進行書寫的。黑體字可以理解為結論,

1.synchronized鎖的是什麼?

2.synchronized能夠鎖住所有方法嗎?

3.synchronized能夠用來鎖住一個方法之中的部分程式碼嗎?

4.synchronized能夠鎖住除了this以外的其他物件嗎?有什麼用?有什麼需要注意的?

------------------------------------------------------------------------------------------正文------------------------------------------------------------------------------------------

1.synchronized鎖的是什麼?

首先,要明白非執行緒安全存在於例項變數之中,即大家都可以更改的變數,私有變數不存線上程安全問題。那麼解決非執行緒安全問題,我們需要用用到 synchronized 來給某一個方法或者物件上鎖,避免交叉訪問的現象出現。那麼synchronized到底鎖的是什麼呢?

先說結論,鎖的是一個物件,一個類的例項,而不是將一個方法鎖起來,如果想要在加上synchronized關鍵字之後同步執行,那多個執行緒訪問的必須是同一個物件,這是鎖的前提。也可以理解為加上synchronized關鍵字之後同步訪問的前提是多個執行緒訪問的是同一個資源,相當於他們是資源共享的。

用一個例子來說明:

twoNum.java是我們的測試類,裡面有帶鎖的addNum方法,根據目前的執行緒名字來賦予num不同的值,a執行緒為100,b執行緒為200

MyThread.java:是自定義執行緒類,用於,run方法執行twoNum物件的addNum()方法

test.java:main函式

twoNum.java:

package 第二章;

public class twoNum {
    private int num=0;
    synchronized public void addNum(){
        try{
            if(Thread.currentThread().getName().equals("a")){
                num=100;
                System.out.println("a執行緒設定num的值");
                Thread.sleep(2000);
            }else{
                num=200;
                System.out.println("b執行緒設定num的值");
            }
            System.out.println(Thread.currentThread().getName()+" "+num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

 

MyThread.java,

package 第二章;
import 第二章.twoNum;

public class MyThread extends Thread {
        private twoNum twonum;
        public MyThread(twoNum temp){
            super();
            this.twonum=temp;
        }
        public void run(){
            super.run();
            twonum.addNum();
        }
}

test.java:

package 第二章;

public class test {
    public static void main(String[] args){
        twoNum twonum = new twoNum();
        MyThread threadA = new MyThread(twonum);
        threadA.setName("a");
        MyThread threadB = new MyThread(twonum);
        threadB.setName("b");
        threadA.start();
        threadB.start();
    }
}

執行上述程式碼,不出意料的,是執行緒安全的,並同步執行,因為我們給twonum物件的addNum方法上了鎖,並且執行緒A,B是用一個twoNum初始化的,

更改test.java程式碼如下,用兩個twoNum物件例項分別給A,B執行緒來初始化:

package 第二章;

public class test {
    public static void main(String[] args){
        twoNum twonum1 = new twoNum();
        twoNum twonum2 = new twoNum();
        MyThread threadA = new MyThread(twonum1);
        threadA.setName("a");
        MyThread threadB = new MyThread(twonum2);
        threadB.setName("b");
        threadA.start();
        threadB.start();
    }
}

執行結果如下:

可以看到現在A,B兩個執行緒執行順序雖然是非同步的,但是資料仍然是正常的。為什麼呢?很明顯,因為有兩個twoNum物件,所以有兩個物件鎖,A,B執行緒持有不同的鎖,所以他們在訪問時,訪問的是不同物件,那當然能非同步執行了,同時也有兩個num變數,從屬於不同的執行緒,A執行緒並不能夠更改B執行緒當中的num變數,所以資料也是正常的。

上面的例子看得出,鎖 關鍵字鎖的是物件,

2.synchronized能夠鎖住所有方法嗎?

那麼synchronized鎖的是整個物件裡面的所有方法,還是怎麼樣呢?

先說結論:synchronized只能夠鎖住一個物件當中帶鎖的方法,並不是全部方法。可以理解為區域性同步。

這就意味著假如A執行緒拿到了一個物件的鎖,正在訪問該物件之中的一個同步方法,這時候B執行緒也嘗試拿到同一個物件鎖,如果B執行緒訪問的是該物件當中不帶鎖的方法,那麼久能夠拿到該鎖並訪問,如果訪問的是該物件之中帶鎖的方法,那麼B執行緒無法拿到該鎖,只能等A執行緒釋放鎖之後才能拿到鎖。

下面是一個例子:

修改twoNum.java如下:增加了一個沒有鎖的方法addNum2

package 第二章;

public class twoNum {
    private int num=0;
    synchronized public void addNum(){
        try{
            if(Thread.currentThread().getName().equals("a")){
                num=100;
                System.out.println("a執行緒設定num的值");
                Thread.sleep(2000);
            }else{
                num=200;
                System.out.println("b執行緒設定num的值");
            }
            System.out.println(Thread.currentThread().getName()+" "+num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void addNum2(){
        try {
            System.out.println(Thread.currentThread().getName() + "正在訪問");
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("訪問結束");
    }
}

 

修改MyThread.java檔案如下:兩個執行緒,一個執行有鎖的方法,另一個執行沒有鎖的

package 第二章;
import 第二章.twoNum;

class MyThread1 extends Thread {
        private twoNum twonum;
        public MyThread1(twoNum temp){
            super();
            this.twonum=temp;
        }
        public void run(){
            super.run();
            twonum.addNum();
        }
}
class MyThread2 extends Thread {
    private twoNum twonum;
    public MyThread2(twoNum temp){
        super();
        this.twonum=temp;
    }
    public void run(){
        super.run();
        twonum.addNum2();
    }
}

test.java:

public class test {
    public static void main(String[] args){
        twoNum twonum = new twoNum();
        MyThread1 threadA = new MyThread1(twonum);
        threadA.setName("a");
        MyThread2 threadB = new MyThread2(twonum);
        threadB.setName("b");
        threadA.start();
        threadB.start();
    }
}

執行結果如圖:

可以看到A物件拿到了鎖,但是B執行緒仍然在同時訪問了沒有鎖的addNum2()方法,證明了上述結論,其他執行緒可以在物件已經被佔用的情況下可以非同步訪問同一個物件的沒有鎖的方法,但是有鎖的方法卻不行。有鎖的不行這裡不演示了,很簡單。

理解了這一個概念,就可以解決有時候會碰到的髒讀現象,

髒讀很好理解,比如你現在執行一個setValue()函式,該函式更改兩個值,username和password,當更改完username還沒有更改password的時候,呼叫了getValue()方法獲取這兩個變數的值,那獲取到的username是已經更改過的,但是password是沒有更改的,這就出現了髒讀。

這時候,運用我們所掌握的知識,給setValue()和getValue()方法都加上鎖,這樣子在setValue()執行結束之前,getValue()就無法拿到物件鎖獲取資訊,這就解決了髒讀問題。

接下來記錄幾個結論,比較容易理解就不展示例子了,只做記錄:

1.出現異常,鎖自動釋放

2,同步方法不具有繼承性,即父類有一個同步方法,那麼他的子類如果有一個多型方法,那麼子類中的方法想要同步必須加上synchronized關鍵字,不能繼承;

3.synchronized能夠鎖住一個方法之中的部分程式碼嗎?

可以看到,前面說的都是給整個方法上鎖,但是想一下, 如果這個方法的執行時間會很久,A執行緒先拿到了鎖,B執行緒如果要執行有鎖的方法只能等待它執行完再執行,那麼效率會很低,比如下面的例子:

twoNum.java:模擬一個任務

package 第二章;

public class twoNum {
    private String data;
    synchronized public void addNum(){
        try{
            System.out.println("開始");
            Thread.sleep(3000);
            data = Thread.currentThread().getName();
            System.out.println("結束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

MyThread1.類,執行緒類:記錄執行時間

class MyThread1 extends Thread {
    long time1;
    long time2;
    private twoNum twonum;
        public MyThread1(twoNum temp){
            super();
            this.twonum=temp;
        }
        public void run(){
            super.run();
            time1 = System.currentTimeMillis();
            twonum.addNum();
            time2=System.currentTimeMillis();
        }
}

test.java:主函式

package 第二章;

public class test {
    public static void main(String[] args){
        twoNum twonum = new twoNum();
        MyThread1 threadA = new MyThread1(twonum);
        threadA.setName("a");
        MyThread1 threadB = new MyThread1(twonum);
        threadB.setName("b");
        threadA.start();
        threadB.start();
        try{
            Thread.sleep(6000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("A執行緒花費時間:"+(threadA.time2-threadA.time1));
        System.out.println("B執行緒花費時間:"+(threadB.time2-threadB.time1));
    }
}

執行結果如下:

可以看到B執行緒等待了3秒才執行,相當於多花費了3秒的時間。

那麼怎麼解決呢?使用synchronized同步程式碼塊來解決

首先synchronized同步程式碼塊就是說現在不給整個方法上鎖,只給方法之中的部分關鍵程式碼上鎖,這樣當A執行緒拿到一個物件的鎖時,B執行緒仍然可以訪問相同物件之中沒有上鎖的程式碼塊,但是不能訪問上鎖的程式碼塊。簡單來說,程式碼塊鎖synchronized鎖的是一個物件的區域性程式碼塊,其他執行緒仍然可以在沒有鎖的情況下訪問非同步程式碼塊。

改變上述twoNum.java的程式碼,如下:

package 第二章;

public class twoNum {
private String data;
public void addNum(){
try{
System.out.println("開始");
Thread.sleep(3000);
String temp = Thread.currentThread().getName();
synchronized(this) {
System.out.println("執行緒"+temp+"賦值當中");
data=temp;
System.out.println("執行緒"+temp+"賦值結束");
}
System.out.println("結束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}

只鎖住關鍵的data賦值程式碼,其他程式碼塊並不用鎖,執行如下:

 

可以看到,開始結束時非同步執行的,但是賦值卻是同步的。證明了我們上面的結論,只同步執行synchronized鎖住的程式碼塊。

4.synchronized能夠鎖住除了this以外的其他物件嗎?有什麼用?有什麼需要注意的?

 你可能注意到了,上面synchronized(this) 裡面鎖住了this,這個意思就是說取到當前物件的鎖,那麼這個this能不能換成其他的物件呢?可以,他可以是任何物件,這個物件我們就叫做物件監聽器。那麼不同的物件監聽器y有什麼區別呢?

首先物件監聽器總體分為兩類:

1.this,即自身

2.非this物件,一般是例項變數或者方法的引數

那麼第二種有什麼用處呢?假設這種情況,現在有一個類,裡面有很多個synchronized方法,執行起來確實是同步的,但是會受到阻塞。不過如果我們使用synchronized(非this物件)同步程式碼塊來鎖住一些程式碼塊,這些程式碼塊和其他被鎖住的方法就是非同步的了,因為他們鎖的是不同物件,這樣就提升了效率。

簡單一句話,synchronized(非this物件)可以讓鎖住的程式碼塊和其他方法非同步執行,下面用程式進行演示:
twoNum.java:兩個方法,一個上鎖,另一個是synchronized(非this物件)同步程式碼塊

package 第二章;

public class twoNum {
    private String anything = new String();
    public void addNum(){
        try{
            synchronized(anything) {
                System.out.println("執行緒"+Thread.currentThread().getName()+"開始");
                Thread.sleep(3000);
                System.out.println("執行緒"+Thread.currentThread().getName()+"結束");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    synchronized public void addNum2(){
        try{
            System.out.println("執行緒"+Thread.currentThread().getName()+"開始");
            Thread.sleep(3000);
            System.out.println("執行緒"+Thread.currentThread().getName()+"結束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

MyThread.java:定義了兩個執行緒類,一個執行addNum()另一個執行addNum2()

package 第二章;
import 第二章.twoNum;

class MyThread1 extends Thread {
    long time1;
    long time2;
    private twoNum twonum;
    public MyThread1(twoNum temp){
        super();
        this.twonum=temp;
    }
    public void run(){
        super.run();
        twonum.addNum();
    }
}
class MyThread2 extends Thread {
    private twoNum twonum;
    public MyThread2(twoNum temp){
        super();
        this.twonum=temp;
    }
    public void run(){
        super.run();
        twonum.addNum2();
    }
}

test.java:

public class test {
    public static void main(String[] args){
        twoNum twonum = new twoNum();
        MyThread1 threadA = new MyThread1(twonum);
        threadA.setName("a");
        MyThread2 threadB = new MyThread2(twonum);
        threadB.setName("b");
        threadA.start();
        threadB.start();
    }
}

執行結果如下:

可以看到他們是非同步執行的,就是因為他們鎖的是不同的物件。

 不過要注意兩點

1.java有字串常量池,也就是如果有兩個String物件,但是他們的值是相同的,那麼當他們作為物件監聽器時,他們是被看做同一個鎖的。

 2.synchronized如果加到一個靜態方法上,那麼它鎖的就不是一個物件,而是整個類了。這時候可以理解為只鎖了靜態方法,該類的例項物件的鎖還是可以正常拿到的。

下面看看java多執行緒死鎖:

簡單理解就是多個執行緒都在互相等待對方釋放鎖然後執行,雙方互相持有對方的鎖,這一般是程式bug,這塊簡單理解一下就行。