1. 程式人生 > >JAVA 多線程(3)

JAVA 多線程(3)

同步方法 exc 並不是 釋放 unsafe args 時間 數據 get

再講線程安全:

一、臟讀

臟讀:在於讀字,意在在讀取實例變量時,實例變量有可能被另外一個線程更改了,導致獲取到的數據出現異常。

在非線程安全的情況下,如果線程A與線程B 共同使用對象實例C中的方法method,如果實例C存在實例變量,同時在method中會操作這個實例變量a,則有可能出現臟讀的情況。

也就是期望值不同,讀取的數據不同,因為線程A與B會同時使用實例C的方法。

例如:

private String name;

    public static void main(String[] args){
        Test2 test2 = new Test2();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
//                System.out.println(Thread.currentThread().getName());
                test2.testUnsafe("a","我是A");
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
//                System.out.println(Thread.currentThread().getName());
                test2.testUnsafe("b","我是B");
            }
        });

        t.start();
        t2.start();
    }

    public void testUnsafe(String name,String param){
        this.name = name;
        try {
      Thread.sleep(1000);
      System.out.println(this.name+":"+param);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}

執行結果如下:

技術分享圖片

代碼中加上了sleep 為了模擬運算所需的時間。

可以看出來,大概是這樣的過程:線程B在run時首先搶到了資源,並運行了實例方法,並把實例變量name修改為b,

然後繼續執行,這時候線程A搶回了資源(因為不是同步的),它把實例變量那麽又修改為b,這個是時候開始執行其他邏輯操作(這裏用sleep模擬),

然後2個線程輪番執行最後的操作-打印。

因為這個時候實例變量name 已經變為a了, 所以線程B 出現臟讀,和期望輸出的 b:我是B 結果出現差異。

如果操作的是不同的對象實例(在synchronized 同步裏 jvm 會創建多個鎖,下面的例子控制2個實例,也就是創建了2個鎖),就不會出現這個問題了,還有如果name不是實例變量,只是私有變量的話也不會出現這種情況。

修改一下看看:

 public static void main(String[] args){

        Thread t = new Thread(new Runnable() {
            @Override
            
public void run() { Test2 test2 = new Test2(); test2.testUnsafe("a","我是A"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { Test2 test2 = new Test2(); test2.testUnsafe("b","我是B"); } }); t.start(); t2.start(); }

輸出結果:

技術分享圖片

如果想要保證使用實例變量而又不出現這種問題,怎麽辦,同步~ 可以使用 synchronized 方法或 synchronized代碼塊。

例如:

public synchronized void testUnsafe(String name,String param){
        this.name = name;
        try {
            Thread.sleep(1000);
            System.out.println(this.name+":"+param);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

輸出結果:

技術分享圖片

關於synchronized 想看的可以下看之前的隨筆(《JAVA 多線程(1)》)。

這裏我想記錄一下類與對象和實例的個人理解():

Class 是類,編寫時候我們稱為類,編譯好的class我們可以稱為class對象,或者說類對象,由類對象new出來的是實例或者說叫對象實例也就是instance。

它們分時期,分關系。類是抽象的概念,對象為具體的事物,人是類,張三是人,張三是人這個類具象,是一個對象實例的存在,它的嘴巴是實例變量,吃飯是實例方法,米飯和菜是吃飯這個對象方法中的方法私有變量,

打比方張三、李四在一起去吃飯(2個線程),都調用了吃飯這個方法(吃飯這個方法是人類共有的),分布是土豆A與土豆B,如果不做同步,有可能2個人會夾到同一條土豆絲。

好吧,上面的比方看看就好。

二、可重入鎖

如果一個線程獲取了或者說搶到了cpu資源,拿到了實例對象鎖,那麽在實例方法中在調用其他同步方法時,依然會獲取到同一個鎖,因為已經搶到了,

比方說,一個屋子有3個房間(方法),張三(線程),搶到了房間1的鎖,由於李四和王五壓根就沒進入這個屋子,所有他們倆必須等張三出來才行,這樣的話

張三進過房間1後,還能獲得房間2、3的鎖。

這個鎖指的是對象鎖,或者說一個實例對象只有一把鎖會更好理解。因為李四和王五想進房間2,雖然不是房間1,但是只有一把鎖,這個鎖在張三手上。

可重入鎖個人感覺主要貢獻在於可繼承,如果B基礎了A,那麽如果操作B,獲取到了實例對象B的鎖,那麽也可以繼續獲得A的鎖。

三、異常釋放鎖

當線程出現異常時,鎖會自動釋放。

四、同步不具有繼承

如果類A 有同步方法 methodA,類B繼承了類A並重寫了methodA 方法,但是沒有加上同步關鍵字,那麽實際上,B類中重寫的methodA 並不是同步方法。

五、同步方法與同步代碼塊

之前在1中寫過,這裏再提及,同步代碼塊同樣時獲得的是對象鎖,當不同的實例方法各自有各自的同步代碼塊,當線程A訪問methodA 時獲得實例對象鎖,如果線程B訪問的實例對象與線程A相同,那麽線程B如想

訪問methodB中的同步代碼塊時同樣需要等線程A使用完methodA,釋放對象鎖,才能執行。

六、非當前實例對象鎖(任意對象監視器)

優點:同一個類中的多個方法或者同一個方法中有多個代碼塊,為了提高性能,使用多個對象鎖,來加快運行速度,或者說減少阻塞。

例如:

private Object object = new Object();

public static void main(String[] args){
Test2 test2 = new Test2();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
test2.test();
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
test2.test2();
}
});

t.start();
t2.start();
}
public void test2(){
synchronized (object){
System.out.println("我是第二個塊"+Thread.currentThread().getName());
}
}

public void test(){
synchronized (this){
try {
System.out.println("我是第一個塊 開始:"+Thread.currentThread().getName());
Thread.sleep(100);
System.out.println("我是第一個塊 結束:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

輸出:

技術分享圖片

通過控制臺打印結果可以看出,在A線程訪問test方法執行的同時,B線程同樣訪問可test2方法。

所以,他們互不幹擾,因為不是一把鎖。

但是問題又出來,就因為這樣提高了性能,但是又有可能會導致臟讀,如果methodA與methodB 中有對同一個實例變量的寫操作,那麽及有可能出現臟讀。

如下:

private Object object = new Object();
    private static List<String> list = new ArrayList<>();
    public static void main(String[] args){
        Test2 test2 = new Test2();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                test2.test();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                test2.test2();
            }
        });

        t.start();
        t2.start();
        try {
            Thread.sleep(6000);
            System.out.println(list.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void test2(){
        synchronized (object){
            judge("test2");
        }
    }

    public void test(){
        synchronized (this){
            judge("test");

        }
    }

    public void judge(String what){
        try {
            if(list.size() < 1){
                Thread.sleep(2000);
                list.add(what);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

輸出結果:

技術分享圖片

出現了,以為是異步的,所以線程A和線程B都可以同時訪問到judge方法,又同時進入到了if語句中,導致最終結果輸出為2,而不是我們期望的1。

所以,方法就是,給judge加上同步鎖:

synchronized (list){
                if(list.size() < 1){
                    Thread.sleep(2000);
                    list.add(what);
                }
            }

這樣拿到list對象鎖的操作就變成同步的了~

明兒個繼續~

JAVA 多線程(3)