六、Java高級特性(多線程對象及變量的併發訪問)

一、非線程安全

多個線程對同一個對象中的實例變量進行併發操作時會出現值被更改、值不同步的情況,進而影響程序的執行流程。

二、線程安全

線程安全就是獲得實例變量的值是經過同步處理的、不會出現被更改不同步的情況。
兩個例子來演示非線程安全和線程安全:

  • 非線程安全
    創建一個User類,聲明一個成員變量sex,和一個getUserInfo方法。
package com.company;
class User {
    String sex;
    public void getUserInfo(int type) {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            if (type == 0) {
                sex = "男";
                Thread.sleep(2000);
            } else {
                sex = "女";
            }
            System.out.println("type = " + type + ":sex = " + sex);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

兩個線程類

package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo(0);
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo(1);
    }
}

mai函數中調用

package com.company;

public class Main {
    public static void main(String[] args) {
        User user = new User();
        new AThread(user).start();
        new BThread(user).start();
    }
}

打印結果:

type = 1:sex = 女
type = 0:sex = 女

上面的例子我們看到,AThread 線程啓動的時候,調用getUserInfo方法傳的參數是0,這個時候sex的值應該是男,然後休眠了2000毫秒,於此同時BThread 線程也已經啓動了,調用getUserInfo方法穿的參數是1,這個時候sex的值應該是女,接着B線程調用先打印type = 1:sex = 女;因爲此時A線程還在休眠,等2000毫秒之後A線程繼續打印,這個時候sex的值已經被改成女,所以A線程調用打印的結果是type = 0:sex = 女。這就產生了一個很明顯的線程安全的問題,明明A線程傳入的是0,期望結果是男,卻打印成女。

  • 方法內的局部變量是線程安全的
    針對以上的非線程安全的問題,我們把代碼改一下:
package com.company;
class User {
    public void getUserInfo(int type) {
        String sex;
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            if (type == 0) {
                sex = "男";
                Thread.sleep(2000);
            } else {
                sex = "女";
            }
            System.out.println("type = " + type + ":sex = " + sex);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
type = 1:sex = 女
type = 0:sex = 男

我們把User類中的sex改成方法內的局部變量,就解決了這一非線程安全的問題。因此我們得出結論。方法內的變量不存在非線程安全問題,因爲方法內的局部變量是私有的。
那如果我們在開發中一定要用到成員變量,那要怎麼解決這一問題呢?

三、解決線程安全問題的辦法

1、synchronized同步方法

package com.company;

class User {
    String sex;
    synchronized public void getUserInfo(int type) {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            if (type == 0) {
                sex = "男";
                Thread.sleep(2000);
            } else {
                sex = "女";
            }
            System.out.println("type = " + type + ":sex = " + sex);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
type = 0:sex = 男
type = 1:sex = 女

我們在getUserInfo方法中加入synchronized 關鍵字,結果打印就正常了。由於A線程先拿到了鎖,所以B線程必須先等待A線程打印完,B線程纔打印。

使用synchronized關鍵字取得的鎖都是對象鎖,而不是把一段代碼或者方法當做鎖,也就是說,哪個線程先執行了帶關鍵字synchronized 的方法就拿到了該方法所屬對象的鎖,其他線程只能等待狀態,等待線程執行方法完畢之後,釋放鎖,才能執行該方法拿到對象的鎖。這裏的前提是同一個對象,如果是多個對象,那JVM就會創建多個鎖,每個線程拿到屬於自己的鎖,就不會存在線程安全的問題。

package com.company;

public class Main {
    public static void main(String[] args) {
        new AThread(new User()).start();
        new BThread(new User()).start();
    }
}

type = 1:sex = 女
type = 0:sex = 男

因爲這個時候是兩個User對象,A線程和B線程各自擁有自己的鎖,不需要等待,因此B線程先打印了,而A線程休眠了2000毫秒之後纔打印。

1.1、synchronized方法與對象鎖

上面我們提到synchronized關鍵字用來修飾方法的時候,是鎖該方法所屬的對象,而不是鎖住某個方法。

package com.company;

class User {
    synchronized public void getUserInfo() {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            Thread.sleep(1000);
            System.out.println("1S之後");
            System.out.println("用戶信息...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    synchronized public void getWorkInfo() {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            System.out.println("工作信息...");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo();
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getWorkInfo();
    }
}
package com.company;

public class Main {
    public static void main(String[] args) {
        User user = new User();
        new AThread(user).start();
        new BThread(user).start();
    }
}

1S之後
用戶信息...
工作信息...

以上的例子,我們在User類中創建了兩個方法,都加上了鎖,A線程調用了getUserInfo方法,B現象調用了getWorkInfo。這個時候由於A線程先拿到了CPU資源執行了getUserInfo方法拿到了User對象的鎖,然後休眠1S,這個時候B線程雖然調用的是getWorkInfo,但是這個時候鎖不在自己身上,在A身上,所以只能等待A線程先執行完畢,所以1S之後A線程先打印了用戶信息...,釋放鎖之後,B線程拿到鎖打印了工作信息...。根據以上結論我們知道:synchronized關鍵字鎖的是方法所在的對象,不是鎖住某個方法。

1.2、髒讀

以上我們演示了在一個User類中聲明瞭兩個方法都加上了synchronized 關鍵自己,根據synchronized 是鎖對象的機制,一旦有一個線程先執行了帶synchronized 的方法,就拿到了該對象的鎖,其他線程只能處於等待狀態,暫時無法執行帶synchronized 關鍵字的方法,直到之前的線程執行完畢釋放了鎖。上面我們說的是其他線程等待狀態,不能調用帶synchronized 的方法,那如果是不帶synchronized 的普通方法呢?也就是User類中,一個帶synchronized 一個不帶。

package com.company;

class User {
    private int i = 0;
    synchronized public void getUserInfo() {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            Thread.sleep(1000);
            i++;
            System.out.println("1S之後");
            System.out.println("getUserInfo = 當前I的值:"+i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     public void getWorkInfo() {
        //方法內的變量是私有的,不會存在線程安全問題
        try {
            i++;
            System.out.println("getWorkInfo = 當前I的值:"+i);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
getWorkInfo = 當前I的值:1
1S之後
getUserInfo = 當前I的值:2

結果很明顯A線程調用的getUserInfo 方法雖然加了鎖,拿到了對象的鎖,但是B線程依然可以調用getWorkInfo 方法,不需要等待A線程執行完,由於A線程等待了1S,而I的值被B線程又加了一次,結果A線程最終打印的結果是2。這就是出現了髒讀的現象,簡單的來說就是:A線程和B線程是異步的,不存在排隊的情況。原因就是getWorkInfo方法沒有加鎖。即使A線程拿到了對象的鎖,但是針對沒有解鎖的方法是無效的。

1.3、重入鎖

關鍵字synchronized具有重入鎖的功能,也就是當一個線程拿到對象鎖的時候,再次請求對象鎖的時候再次得到該對象的鎖。
User類創建了四個方法並且都加上了鎖。

package com.company;

class User {
    synchronized public void test1() {
        System.out.println("test1");
        test2();
    }
    synchronized public void test2() {
        System.out.println("test2");
        test3();
    }
    synchronized public void test3() {
        System.out.println("test3");
    }
    synchronized public void test4() {
        System.out.println("test4");
    }

}

創建線程A和B,A線程調用test1(test1內部調用了test2,test2內部調用了test3都是枷鎖的方法),B線程調用了test4(也是枷鎖的方法)

package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.test1();
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.test4();
    }
}

main 函數中啓AB動線程調用

package com.company;

public class Main {
    public static void main(String[] args) {
        User user = new User();
        new AThread(user).start();
        new BThread(user).start();
    }
}

test1
test2
test3
test4

我們看到打印結果就很明顯,A線程調用了test1優先拿到了鎖,接着內部會繼續調用test2,test2繼續調用test3。最後A執行完畢,B纔拿到鎖,調用test4。也就說明了synchronized具有重入鎖的功能

1.4、同步(synchronized)不具有繼承性
package com.company;

public class Parent {
    synchronized public void test(){
        System.out.println("你好這是父親...");
    }
}
package com.company;

class User extends Parent {
    @Override
    public  void test() {
        System.out.println("你好這是孩子開始..."+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("你好這是孩子結束..."+Thread.currentThread().getName());
    }
}

User繼承了Parent ,重寫了test方法,但是重寫的方法沒有加synchronized 修飾。我們看調用打印結果

package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.test();
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.test();
    }
}
package com.company;

public class Main {
    public static void main(String[] args) {
        User user = new User();
        new AThread(user).start();
        new BThread(user).start();
    }
}
你好這是孩子開始...Thread-0
你好這是孩子開始...Thread-1
你好這是孩子結束...Thread-1
你好這是孩子結束...Thread-0

從打印結果來看,A線程和B線程不存在排隊執行。因此可知同步不具有繼承性,當然我們可以在子類的test方法中加synchronized 關鍵字就變成了同步的方法。

2、synchronized同步語句塊

用synchronized關鍵字同步方法有些情況下是有弊端的,舉個例子:

package com.company;

import java.util.Date;
class User {
    private String name;
    synchronized public void getUserInfo(int type) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (type == 0) {
            name = "小明";
        }
        if (type == 1) {
            name = "小紅";
        }
        System.out.println("當前時間:" + new Date().getTime() + "-獲取的name爲:" + name + "-當前線程:" + Thread.currentThread().getName());
    }
}
當前時間:1607477056617-獲取的name爲:小明-當前線程:A
當前時間:1607477058617-獲取的name爲:小紅-當前線程:B

我們假設 getUser是從服務器獲取用戶信息的一個方法,需要耗時2S,那麼如果A線程先拿到鎖的話,B線程必須等待A線程執行完畢,才能繼續執行,這樣最終需要時間就是4S。會導致效率比較慢。這就是synchronized同步方法的一個弊端,我們可以通過同步語句塊來解決。

package com.company;

import java.util.Date;

class User {
    private String name;

     public void getUserInfo(int type) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (this) {
            if (type == 0) {
                name = "小明";
            }
            if (type == 1) {
                name = "小紅";
            }
            System.out.println("當前時間:" + new Date().getTime() + "-獲取的name爲:" + name + "-當前線程:" + Thread.currentThread().getName());
        }

    }
}
當前時間:1607477244343-獲取的name爲:小紅-當前線程:B
當前時間:1607477244343-獲取的name爲:小明-當前線程:A

我們把鎖加在拿到數據後給成員變量賦值的地方,因爲只有在給成員變量賦值的時候可能會出現線程不安全,最終他們的打印結果是同時等待了2S最後打印。以上的結論就是在synchronized 修飾的代碼中是同步執行的,沒有修飾的部分就是異步執行的。和之前提到的用synchronized 修飾的方法和沒有用synchronized 修飾的方法是一個道理。

2.1、同步語句塊也是鎖對象

前面提到了synchronized 關鍵字修飾方法的時候,鎖的是該方法所屬的對象,而不是鎖住該方法。對於同步語句塊來說也是一樣的,synchronized 修飾語句塊的時候,也是鎖住當前所屬的對象。

package com.company;

class User {
     public void getUserInfo() {
        synchronized (this) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("當前線程getUserInfo:"+Thread.currentThread().getName());
        }
    }
    public void getWorkInfo() {
        synchronized (this) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("當前線程getWorkInfo:"+Thread.currentThread().getName());
        }
    }
}
當前線程getUserInfo:A
當前線程getWorkInfo:B

以上線程A 先拿到了鎖,所以B線程只能等待A線程執行完畢,才能執行。說明A拿到的鎖是對象鎖。同步語句塊的方式,相對同步方法來說雖然能提高了一些效率,但是因爲同步語句塊也是拿到的是該語句塊所屬對象的鎖,其他線程調用其他鎖方法的時候,只能等待。

2.2、將任意對象作爲監視器

爲了解決以上的效率問題,我們可以將某個共享變量作爲監視器,加上鎖

package com.company;

class User {
    String name = new String();

    public void getUserInfo() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (name) {
            System.out.println("當前線程getUserInfo:" + Thread.currentThread().getName());
        }
    }

    synchronized public void getWorkInfo() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("當前線程getWorkInfo:" + Thread.currentThread().getName());
    }
}
當前線程getWorkInfo:B
當前線程getUserInfo:A
當前線程getUserInfo:B

根據打印結果我們看到B調用getWorkInfo先打印了,而因爲getUserInfo方法中同步的是name屬性,因此A和B調用的時候,在同步語句塊裏是同步的。簡單的來說 synchronized (name)鎖住某個變量,其實就是鎖住該name對象,而不是鎖住當前對象。因此A線程調用getUserInfo和B線程調用getWorkInfo是異步的。

2.3、數據類型String常量池的特性

我們先來看一個例子

package com.company;

public class Main {
    public static void main(String[] args) {
        String a = "AA";
        String b = "AA";
        System.out.println(a==b);

    }
}

true

由於jvm具有String 常量池緩存的功能,所以我們看到以上打印的結果爲true。
接着我們繼續看

package com.company;

class User {
    public void getUserInfo(String value) {
        synchronized (value) {
            while (true){
                System.out.println("當前線程:" + Thread.currentThread().getName());
            }

        }
    }

}
package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo("AA");
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo("AA");
    }
}
當前線程:A
當前線程:A
當前線程:A
當前線程:A
當前線程:A
當前線程:A
當前線程:A

上面的例子,我們通過synchronized 關鍵字鎖住的是傳入的String變量,由於A線程和B線程傳入的都是AA,JVM具有String常量池緩存功能,因此可知他們傳入的都是同一個對象,所以A線程先拿到鎖的時候,鎖住了傳入的變量AA,而B線程傳入的也是AA,同一個對象。而該對象被A鎖住了,並且裏面執行了一個死循環,所以一直沒有釋放鎖。因此B線程一直不能執行。我們修改一下代碼

package com.company;

class User {
    public void getUserInfo(Object value) {
        synchronized (value) {
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("當前線程:" + Thread.currentThread().getName());
            }

        }
    }

}
package com.company;

class AThread extends Thread {
    private User user;

    public AThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo(new Object());
    }
}
package com.company;

class BThread extends Thread {
    private User user;

    public BThread(User user) {
        this.user = user;
    }
    @Override
    public void run() {
        user.getUserInfo(new Object());
    }
}
當前線程:B
當前線程:A
當前線程:B
當前線程:A
當前線程:B
當前線程:A
當前線程:B
當前線程:A
當前線程:B
當前線程:A

我們通過new Object的形式傳入了Object對象,由於A線程和B線拿到是各自不同的鎖,因此A線程和B線程是異步執行的。

2.4、死鎖

我們先來看一段代碼

package com.company;

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable1 = new MyRunnable();
        myRunnable1.setName("a");
        new Thread(myRunnable1).start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myRunnable1.setName("b");
        new Thread(myRunnable1).start();
    }
}

class MyRunnable implements Runnable {
    private String name;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        if ("a".equals(name)) {
            synchronized (lock1) {
                System.out.println("當前name = " + name);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("執行lock1完執行lock2");
                }
            }
        }
        if ("b".equals(name)) {
            synchronized (lock2) {
                System.out.println("當前name = " + name);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("執行lock2完執行lock1");
                }
            }
        }
    }
}
當前name = a
當前name = b

A線程先啓動之後,拿到了lock1對象的鎖,接着休眠1000毫秒,這個時候B線程已經啓動拿到了lock2對象的鎖,也休眠了1000毫秒,這個時候A線程休眠結束後要拿到lock2的鎖執行完畢,才能釋放lock1,而B線程也要拿到lock1執行完畢才能釋放lock2,也就是A線程和B線程都在等待拿到對方的鎖執行完才能釋放自己的鎖。所以就會造成死鎖,所以A線程和B線程最終都沒法執行完成。我們通過cmd命令。進入到jdk的bin目錄輸入jps命令


然後看到運行的id是6128,我們使用jstack -l 6128命令,看到了死鎖的信息。


3、volatile關鍵字

我們先來看一段代碼

package com.company;

public class MyTest {
    public static void main(String[] args) {
        MyThead t = new MyThead();
        t.start();
        while (true) {
            if (t.isFlag()) {
                System.out.println("當前線程-" + Thread.currentThread().getName() + " 有點東西...");
            }
        }
    }
}

class MyThead extends Thread {
    private boolean flag = false;
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("當前線程-" + Thread.currentThread().getName() + " flag = " + flag);
    }
}

當前線程-Thread-0 flag = true

以上打印的結果中只有flag = true,而 “有點東西”幾個字樣一直沒打印,我們分析代碼理論上MyThead 的 run方法中休眠了1S之後修改了成員變量flag = true值,而在Main函數也就是main線程中通過MyThead的實例去拿flag的值也應該是true,接着跳出循環打印“有點東西”。但是爲什麼一直沒有執行呢?我們先看一張圖:


在jvm的內存模型中,所有的共享變量都存在於主內存中,這裏說的共享變量指的的成員變量,因爲局部變量是線程私有的。每一個線程都有自己的工作內存,線程之間不能之間訪問對方的工作內存,線程之間變量值的傳遞通過主內存作爲橋樑。也就是在上面的例子中,main線程首先從主內存中讀取flag的數據拷貝到自己的工作內存,因爲此時MyThead 休眠了1S,還沒有更改flag的值,所以flag的值還是false,接着main線程一直從自己的工作內存讀取flag的值,所以一直是false,儘管1S之後,MyThead 線程更改了flag的值爲true,MyThead 修改了flag的值之後,修改了自己的工作內存並同步到主內存中,此時主內存的flag 的值是true,但是在main線程中一直讀的flag值還是自己工作內存中的值,一直爲false,所以一直沒法跳出循環。

解決辦法

1、使用synchronized
當前線程-Thread-0 flag = true
當前線程-main 有點東西...
當前線程-main 有點東西...
當前線程-main 有點東西...
當前線程-main 有點東西...

當前線程-Thread-0 flag = true
當前線程-main 有點東西...
當前線程-main 有點東西...
當前線程-main 有點東西...
當前線程-main 有點東西...

使用synchronized關鍵字,當main線程執行synchronized代碼快的時候,拿到鎖,接着清空自己的工作內存,從主線程把數據拷貝到自己的工作內存中,所以拿到的值就是最新的flag = true。如果對數據有修改的話,會將工作內存中的數據重新刷回主內存中。然後釋放鎖。因爲其他線程拿不到鎖一直處於等待的狀態,所以主內存中的數據一直是最新的。

2、Volatile修飾共享變量
package com.company;

public class MyTest {
    public static void main(String[] args) {
        MyThead t = new MyThead();
        t.start();
        while (true) {
                if (t.isFlag()) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("當前線程-" + Thread.currentThread().getName() + " 有點東西...");
                }
        }
    }
}

class MyThead extends Thread {
    volatile private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("當前線程-" + Thread.currentThread().getName() + " flag = " + flag);
    }
}

使用volatile 修飾共享變量flag 也能解決該問題,Volatile做了啥?Volatile修飾的變量,保證了線程讀取該變量的時候都是從主內存中讀取的,也就是線程共享變量可見性。當MyThead 線程修改了flag 值之後刷新了主內存,因爲是用Volatile修飾的變量,所以main線程讀取也是從主內存讀取,因此讀出的flag值是最新的true。這一特性成爲可見性。也就是Volatile提供了可見性。那麼Volatile是否具有原子性呢?(原子性也就是一致性,線程是否安全,共享變量是否同步),在之前我們知道解決線程安全問題我們使用synchronized關鍵字,而Volatile是否能做到呢?做不到。

package com.company;

public class MyTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new AThread().start();
        }
    }
}

class AThread extends Thread {
    volatile public static int count;

    @Override
    public void run() {
        add();
    }

    private static void add() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName()+":"+count);
    }
}

Thread-1:300
Thread-6:700
Thread-5:600
Thread-4:500
Thread-3:300
Thread-2:500
Thread-0:300
Thread-9:1000

我們啓動了100個線程對共享變量count進行操作,結果打印的時候,有重複的,說明volatile 不具有原子性,也就是線程不安全的。我們把代碼加上解決了這一問題。

  synchronized private static void add() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName()+":"+count);
    }
  synchronized private static void add() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName()+":"+count);
    }

總結:

  • volatile用於修飾共享變量,當某個變量被多個線程共享的時候,其中某一個線程修改了變量的值,其他線程可以立即得到修改後的值。也就是volatile具有可見性。
  • volatile屬性的讀寫操作都是無鎖的,也就是多個線程不會進行排隊同步,所以volatile不是線程安全的,也就是不具有原子性,不能替代synchronized。
  • synchronized也擁有volatile的特性,當某個線程訪問其他線程的共享變量的時候,加上synchronized關鍵字,確保從主內存中讀取該變量的值。也就是synchronized修飾共享變量的時候,確保拿到最新的值。
  • synchronized擁有原子性,線程安全。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章