Java多線程編程核心技術(二)對象及變量的併發訪問

本文主要介紹Java多線程中的同步,也就是如何在Java語言中寫出線程安全的程序,如何在Java語言中解決非線程安全的相關問題。閱讀本文應該着重掌握以下技術點:html

  1. synchronized對象監視器爲Object時的使用。
  2. synchronized對象監視器爲Class時的使用。
  3. 非線程安全是如何出現的。
  4. 關鍵字volatile的主要做用。
  5. 關鍵字volatile與synchronized的區別及使用狀況。

1.synchronized同步方法

「非線程安全」其實會在多個線程對同一個對象中的實例變量進行併發訪問時發生,產生的後果就是」髒讀「,也就是讀取到的數據實際上是被更改過的。而「線程安全」就是已得到的實例變量的值是通過線程同步處理的,不會出現髒讀的現象。java

1.1 方法內的變量爲線程安全

「非線程安全」問題存在於「實例變量」中,若是是方法內部的私有變量,則不存在「非線程安全」問題,所得結果也就是「線程安全」的了。編程

1.2 實例變量的非線程安全

若是多個線程共同訪問1個對象中的實例變量,則有可能出現「非線程安全」問題。緩存

用線程訪問的對象中若是有多個實例對象,則運行的結果有可能出現交叉的狀況。安全

若是對象僅有一個實例變量,則有可能出現覆蓋的狀況。服務器

若是兩個線程同時訪問一個沒有同步的方法,若是兩個線程同時操做業務對象中的實例變量,則有可能出現「非線程安全」問題。解決這個問題的方法就是在方法前加關鍵字synchronized便可。多線程

1.3 多個對象多個鎖

代碼示例:併發

public class Run {
    public static void main(String[] args) {
        MyService service1 = new MyService();
        Thread thread1 = new Thread(service1);
        thread1.start();
        MyService service2 = new MyService();
        Thread thread2 = new Thread(service2);
        thread2.start();
    }
}
public class MyService implements Runnable {
    private int i = 0;
    @Override
    synchronized public void run() {
        System.out.println(++i);
    }
}

上面示例是兩個線程分別訪問同一個類的兩個不一樣實例的相同的同步方法,效果倒是以異步的方式運行的。本示例因爲建立了2個業務對象,在系統中產生出2個鎖,因此運行結果是異步的,打印的效果就是1 1。當咱們把線程2的參數service2改爲service1,打印結果變爲1 2。爲何是這樣的結果?異步

關鍵字 synchronized 取得的線程對象都是對象鎖,而不是把一段代碼或方法(函數)當作鎖,因此在上面的示例中,哪一個線程先執行帶 synchronized 關鍵字的方法,哪一個線程就持有該方法所屬對象的鎖Lock,那麼其餘線程只能呈等待狀態,前提是多個線程訪問的是同一個對象。jvm

但若是多個線程訪問多個對象,則JVM會建立多個鎖。

1.4 synchronized方法與鎖對象

爲了證實前面講的線程鎖是對象,示例代碼以下:

public class MyService implements Runnable {
    @Override
    public void run() {
        System.out.println("begin: "+Thread.currentThread().getName());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}
public class Run {
    public static void main(String[] args) {
        MyService service = new MyService();
        Thread thread1 = new Thread(service,"A");
        thread1.start();
        Thread thread2 = new Thread(service,"B");
        thread2.start();
    }
}

運行結果:

begin: B
begin: A
end
end

在run方法前加入關鍵字synchronized進行同步處理。再次運行結果以下:

begin: A
end
begin: B
end

經過上面的實驗得出結論,調用關鍵字synchronized聲明的方法必定是排隊運行的。另外須要緊緊記住「共享」這兩個字,只有共享資源讀寫訪問才須要同步化,若是不是共享資源,那麼基本就沒有同步的必要。

1.5 髒讀

public class MyService{
    private String username = "AA";
    private String password = "aa";

    public void getValue() {
        System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
    }

    synchronized public void setValue(String username,String password){
        this.username = username;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        Thread thread1 = new Thread(() -> service.setValue("BB","bb"),"Thread-A");
        thread1.start();
        Thread.sleep(200);
        Thread thread2 = new Thread(service::getValue,"Thread-B");
        thread2.start();
    }
}

打印結果:

Thread-B : BB aa

出現髒讀是由於getValue方法不是同步的,因此能夠在任意時候進行調用。解決方法就是加上同步synchronized關鍵字,代碼以下:

synchronized public void getValue() {
   System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
}

運行結果:

Thread-B : BB bb

經過上述示例不只要知道髒讀是經過synchronized關鍵字解決的,還要知道以下內容:

當A線程調用實例對象的加入synchronized關鍵字的 X 方法時,A線程就得到了 X 方法鎖,更準確地講,是得到了對象的鎖,因此其餘線程必須等A線程執行完畢了才能夠調用 X 方法,但B線程能夠隨意調用其餘的非 synchronized 同步方法。

髒讀必定會出現操做實例變量的狀況下,這就是不一樣線程「爭搶」實例變量的結果。

1.6 synchronized鎖重入

關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程獲得一個對象後,再次請求此對象鎖時是能夠再次獲得該對象的鎖的。這也證實了在一個synchronized方法/塊的內部調用本類的其餘synchronized方法/塊,是永遠能夠獲得鎖的。

示例代碼:

public class MyService{
    synchronized public void service1(){
        System.out.println("service1");
        service2();
    }
    
    synchronized public void service2(){
        System.out.println("service2");
    }
}

「可重入鎖」的概念是:本身能夠再次獲取本身的內部鎖。可重入鎖也支持在父子類繼承的環境中。

示例代碼:

public class MyServiceChild extends MyService{
    synchronized public void service(){
        System.out.println("service1");
        this.service2();
    }
}

說明子類是徹底能夠經過「可重入鎖」調用父類的同步方法的。

1.7 出現異常,鎖自動釋放

當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。

1.8 同步不具備繼承性

同步不能夠繼承。子類繼承父類的同步方法時還須要添加synchronized關鍵字才能保持同步。

2.synchronized同步語句塊

用關鍵字synchronized聲明方法在某些狀況下是有弊端的,好比A線程調用同步方法執行一個長時間的任務,那麼B線程則必須等待比較長的時間。在這樣的狀況下可使用synchronized同步語句塊來解決。synchronized 方法是對當前對象進行加鎖,而 synchronized代碼塊是對某一個對象進行加鎖。

2.1 synchronized同步代碼塊的使用

當兩個併發線程訪問同一個對象object中的synchronized(this)同步代碼塊時,一段時間內只能有一個線程被執行,另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。

示例代碼:

public class Test {

    public void service(){
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537000799741
Thread-A end: 1537000802742
Thread-B begin: 1537000802742
Thread-B end: 1537000805742

上述示例證實了同步synchronized代碼塊真的是同步的。

2.2 一半同步,一半異步

咱們把前面的示例代碼的service方法改造一下:

public void service(){
        System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
        synchronized (this) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }

再次運行:

Thread-A begin: 1537001008952
Thread-B begin: 1537001008952
Thread-A end: 1537001011953
Thread-B end: 1537001014954

本實驗說明:不在synchronized代碼塊中就是異步執行,在synchronized塊中就是同步執行。

2.3 synchronized代碼塊間的同步性

在使用synchronized(this)代碼塊須要注意的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其它線程對同一個object中全部其餘synchronized(this)同步訪問被阻塞,這說明synchronized使用的「對象監視器」是一個。

和synchronized關鍵字修飾的方法同樣,synchronize(this)代碼塊也是鎖定的當前對象。

2.4 將任意對象做爲對象監視器

多個線程調用同一個對象中得不一樣名稱的synchronized同步方法或synchronized(this)同步代碼塊時,調用的效果就是按順序執行,也就是同步的,阻塞的。

這說明synchronized同步方法或synchronized同步代碼塊分別有兩種做用。

(1)對其餘synchronized同步方法或synchronized(this)同步代碼塊調用呈阻塞狀態。

(2)同一時間只有一個線程能夠執行synchronized同步方法或synchronized(this)同步代碼塊中的代碼。

在前面咱們使用synchronized(this)格式來同步代碼塊,其實Java還支持對「任意對象」做爲「對象監視器」來實現同步的功能。這個」任意對象「大多數是實例變量及方法的參數,使用格式爲synchronized(非this對象)。

根據前面對synchronized(this)同步代碼塊的做用總結可知,synchronized(非this對象)格式的做用只有1種:synchronized(非this對象 X )同步代碼塊。

(1)在多個線程持有」對象監視器「爲同一個對象的前提下,同一時間只有一個線程能夠執行synchronized(非this對象 X)同步代碼塊。

(2)當持有」對象監視器「爲同一個對象的前提下,同一時間只有一個線程能夠執行synchronized(非this對象X)同步代碼塊中的代碼。

下面演示下任意對象做爲對象監視器的示例:

public class Test {
    private String anyObject = new String();
    public void service(){
        synchronized (anyObject) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537008016172
Thread-A end: 1537008019173
Thread-B begin: 1537008019173
Thread-B end: 1537008022173

鎖非this對象具備必定的優勢:若是在一個類中有不少個synchronized方法,這時雖然能實現同步,但會受到阻塞,因此影響運行效率;但若是使用同步代碼塊鎖非this對象,則synchronized(非this)代碼塊中的程序與同步方法是異步的,不與其餘鎖this同步方法爭搶this鎖,則可大大提升運行效率。

再來看下面的示例代碼:

public class Test {
    private String anyObject = new String();
    public void service(){
        synchronized (anyObject) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }
    synchronized public void service2(){
        System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
    }
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service2();
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537009027680
Thread-B begin: 1537009027681
Thread-A end: 1537009030680

可見,使用「synchronized(非this對象x)同步代碼塊」格式進行同步操做時,對象監視器必須是同一個對象,若是不是同一個對象。若是不是同一個對象監視器,運行的結果就是異步調用了,就會交叉運行。

2.5 細化三個結論

」synchronized(非this對象X)「格式的寫法是將x對象自己做爲「對象監視器」,這樣就能夠得出如下3個結論:

  1. 當多個線程同時執行synchronized(X){}同步代碼塊時呈同步效果。
  2. 當其餘線程執行X對象中synchronized同步方法時呈同步效果。
  3. 當其餘線程執行X對象方法裏面的synchronized(this)代碼塊時也呈現同步效果。
  4. 但須要注意的是,若是其餘線程調用不加synchronized關鍵字的方法時,仍是異步調用。

2.6 靜態同步synchronized方法與synchronized(class)代碼塊

關鍵字synchronized還能夠在static靜態方法上,若是這樣寫,那是對當前的*.java文件對應的Class類進行持鎖。

下面測試靜態同步方法:

public class Test2 {
    synchronized public static void service() {
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test2.service();
            }
        }, "Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test2.service();
            }
        }, "Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537011409603
Thread-A end: 1537011412608
Thread-B begin: 1537011412608
Thread-B end: 1537011415608

synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。

爲了驗證對象鎖和Class鎖不是同一個鎖,來看下面的代碼:

public class Test2 {
    synchronized public static void service() {
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }
    synchronized public void service2(){
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Test2.service();
            }
        }, "Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test2().service2();
            }
        }, "Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537012019151
Thread-B begin: 1537012019152
Thread-A end: 1537012022152
Thread-B end: 1537012022152

異步的緣由是持有不一樣的鎖,一個是對象鎖,另一個是Class鎖,Class鎖能夠對全部類的實例對象起做用。

下面咱們測試synchronized(class)代碼塊,示例代碼以下:

public class Test {
    public void service(){
        synchronized (Test.class) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service();
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service();
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537011197190
Thread-A end: 1537011200191
Thread-B begin: 1537011200191
Thread-B end: 1537011203191

同步synchronized(class)代碼塊的做用其實和synchronized static方法的做用同樣。

2.7 數據類型String的常量池特性

在JVM中具備String常量池緩存的功能,將synchronized(String)同步塊與String聯合使用時,要注意常量池以帶來的一些例外。

public class Test {
    public void service(String str){
        synchronized (str) {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " time: " + System.currentTimeMillis());
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service("AA");
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service("AA");
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A time: 1537013470535
Thread-A time: 1537013470535
Thread-A time: 1537013470535
...

運行結果顯示A線程陷入了死循環,而B線程一直在等待未執行。出現這樣的結果就是兩個持有相同的鎖,因此形成B線程不能執行。這就是String常量池帶來的問題。所以在大多數狀況下,同步synchronized代碼塊都不使用String做爲鎖對象,而改用其餘,好比new Object()實例化一個Object對象,但它並不放入緩存中。

改造後的代碼:

public class Test {
    public void service(Object str){
        synchronized (str) {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " time: " + System.currentTimeMillis());
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service(new Object());
            }
        },"Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Test().service(new Object());
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A time: 1537015931981
Thread-A time: 1537015931982
Thread-B time: 1537015931982
Thread-B time: 1537015931982
...

交替打印的緣由是持有的鎖不是一個。

2.8 同步synchronized方法無限等待與解決

同步方法極易形成死循環。示例代碼:

public class Test {
    synchronized public void serviceA() {
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
        boolean is = true;
        while (is){

        }
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }

    synchronized public void serviceB() {
        System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
        System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
    }

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.serviceA();
            }
        }, "Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.serviceB();
            }
        }, "Thread-B").start();
    }
}

線程B永遠得不到運行的機會,鎖死了。

解決的方法就是使用同步塊。更改後的代碼以下:

public class Test {
    private Object objectA = new Object();
    public void serviceA() {
        synchronized (objectA) {
            System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
            boolean is = true;
            while (is) {

            }
            System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
        }
    }

    private Object objectB = new Object();
    synchronized public void serviceB() {
        synchronized (objectB) {
            System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
        }
    }
    ....
}

2.9 多線程的死鎖

Java多線程死鎖是一個經典問題,由於不一樣的線程都在等待根本不可能被釋放的鎖,從而致使全部的任務都沒法完成。在多線程技術中,「死鎖」是必須避免的,由於這會形成線程的「假死」。

示例代碼:

public class DealThread implements Runnable {
    public String username;
    public Object locak1 = new Object();
    public Object locak2 = new Object();
    public void setFlag(String username){
        this.username = username;
    }

    @Override
    public void run() {
        if (username.equals("a")){
            synchronized (locak1){
                System.out.println("username:"+username);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locak2){
                    System.out.println("按lock1-》lock2執行");
                }
            }
        }

        if (username.equals("b")){
            synchronized (locak2){
                System.out.println("username:"+username);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locak1){
                    System.out.println("按lock2-》lock1執行");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DealThread dealThread = new DealThread();
        dealThread.setFlag("a");
        Thread threadA = new Thread(dealThread);
        threadA.start();
        Thread.sleep(100);
        dealThread.setFlag("b");
        Thread threadB = new Thread(dealThread);
        threadB.start();
    }
}

運行結果,出現死鎖:

username:a
username:b

死鎖是程序設計的Bug,在設計程序時就須要避免雙方互相持有對方的鎖的狀況。須要說明的是,本實驗使用synchronized嵌套的代碼結構來實現死鎖,其實不使用嵌套的代碼結構也會出現死鎖,與嵌套不嵌套無任何關係,不要被代碼結構所誤導。只要互相等待對方釋放鎖就有可能出現死鎖。

可使用JDK自帶的工具來檢測是否有死鎖的現象。首先進入CMD命令行界面,再進入JDK的安裝文件夾中的

bin目錄,執行jps命令。獲得運行的線程Run的id值。再執行jstack命令,查看結果。

完整命令演示以下:

D:\Java\jdk1.8.0\bin>jps
8240 Launcher
13252 Jps
12312
7948 DealThread
D:\Java\jdk1.8.0\bin>jstack -l 7948
....

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at cn.zyzpp.thread2_3.DealThread.run(DealThread.java:39)
        - waiting to lock <0x00000000d6089e80> (a java.lang.Object)
        - locked <0x00000000d6089e90> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:745)
"Thread-0":
        at cn.zyzpp.thread2_3.DealThread.run(DealThread.java:25)
        - waiting to lock <0x00000000d6089e90> (a java.lang.Object)
        - locked <0x00000000d6089e80> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

2.10 鎖對象的改變

在任何數據類型做爲同步鎖時,須要注意的是,是否有多個線程同時持有鎖對象,若是同時持有鎖對象,則這些線程之間就是同步的;若是分別得到鎖對象,這些線程之間就是異步的。

public class Test {
    private String lock = "123";
    public void service(){
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
            lock = "456";
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-A").start();
        Thread.sleep(50);
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.service();
            }
        },"Thread-B").start();
    }
}

運行結果:

Thread-A begin: 1537019992452
Thread-B begin: 1537019992652
Thread-A end: 1537019994453
Thread-B end: 1537019994653

爲何是亂序?由於50ms事後線程取得的鎖時「456」。

把lock = "456"放在Thread.sleep(2000)以後,再次運行。

Thread-A begin: 1537020101553
Thread-A end: 1537020103554
Thread-B begin: 1537020103554
Thread-B end: 1537020105558

線程A和線程B持有的鎖都是「123」,雖然將鎖改爲了「456」,但結果仍是同步的,由於A和B爭搶的鎖是「123」。

還須要提示一下,只要對象不變,即便對象的屬性被改變,運行的結果仍是同步的。

3.volatile關鍵字

關鍵字volatile的主要做用是使變量在多個線程間可見。

3.1 關鍵字volatile與死循環

若是不是在多繼承的狀況下,使用繼承Thread類和實現Runnable接口在取得程序運行的結果上並無多大的區別。若是一旦出現」多繼承「的狀況,則用實現Runable接口的方式來處理多線程的問題就是頗有必要的。

public class PrintString implements Runnable{
    private boolean isContinuePrint = true;

    @Override
    public void run() {
        while (isContinuePrint){
            System.out.println("Thread: "+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public boolean isContinuePrint() {
        return isContinuePrint;
    }

    public void setContinuePrint(boolean continuePrint) {
        isContinuePrint = continuePrint;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(100);
        System.out.println("我要中止它!" + Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

運行結果:

Thread: Thread-A
我要中止它!main

上面的代碼運行起來沒毛病,可是一旦運行在 -server服務器模式中64bit的JVM上時,會出現死循環。解決的辦法是使用volatile關鍵字。

關鍵字volatile的做用是強制從公共堆棧中取得變量的值,而不是從線程私有數據棧中取得變量的值。

3.2 解決異步死循環

在研究volatile關鍵字以前先來作一個測試用例,代碼以下:

public class PrintString implements Runnable{
    private boolean isRunnning = true;

    @Override
    public void run() {
        System.out.println("Thread begin: "+Thread.currentThread().getName());
        while (isRunnning == true){
        }
        System.out.println("Thread end: "+Thread.currentThread().getName());
    }

    public boolean isRunnning() {
        return isRunnning;
    }

    public void setRunnning(boolean runnning) {
        isRunnning = runnning;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(1000);
        printString.setRunnning(false);
        System.out.println("我要中止它!" + Thread.currentThread().getName());
    }

}

JVM有Client和Server兩種模式,咱們能夠經過運行:java -version來查看jvm默認工做在什麼模式。咱們在IDE中把JVM設置爲在Server服務器的環境中,具體操做只需配置運行參數爲 -server。而後啓動程序,打印結果:

Thread begin: Thread-A
我要中止它!main

代碼 System.out.println("Thread end: "+Thread.currentThread().getName());從未被執行。

是什麼樣的緣由形成將JVM設置爲-server就出現死循環呢?

在啓動thread線程時,變量boolean isContinuePrint = true;存在於公共堆棧及線程的私有堆棧中。在JVM設置爲-server模式時爲了線程運行的效率,線程一直在私有堆棧中取得isRunning的值是true。而代碼thread.setRunning(false);雖然被執行,更新的倒是公共堆棧中的isRunning變量值false,因此一直就是死循環的狀態。內存結構圖:

這個問題其實就是私有堆棧中的值和公共堆棧中的值不一樣步形成的。解決這樣的問題就要使用volatile關鍵字了,它主要的做用就是當線程訪問isRunning這個變量時,強制性從公共堆棧中進行取值。

將代碼更改以下:

volatile private boolean isRunnning = true;

再次運行:

Thread begin: Thread-A
我要中止它!main
Thread end: Thread-A

經過使用volatile關鍵字,強制的從公共內存中讀取變量的值,內存結構如圖所示:

使用volatile關鍵字增長了實例變量在多個線程之間的可見性。但volatile關鍵字最致命的缺點是不支持原子性。

下面將關鍵字synchronized和volatile進行一下比較:

  1. 關鍵字volatile是線程同步的輕量級實現,因此volatile性能確定比synchronized要好,而且volatile只能修飾於變量,而synchronized能夠修飾方法,以及代碼塊。隨着JDK新版本的發佈,synchronized關鍵字在執行效率上獲得很大提高,在開發中使用synchronized關鍵字的比率仍是比較大的。
  2. 多線程訪問volatile不會發生阻塞,而synchronized會出現阻塞。
  3. volatile能保證數據的可見性,但不能保證原子性;而synchronized能夠保證原子性,也能夠間接保證可見性,由於它會將私有內存和公共內存中的數據作同步。
  4. 再次重申一下,關鍵字volatile解決的是變量在多個線程之間的可見性;而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。

線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。

3.3 volatile非原子性的特徵

關鍵字雖然增長了實例變量在多個線程之間的可見性,但它卻不具有同步性,那麼也就不具有原子性。

示例代碼:

public class MyThread extends Thread {
    volatile private static int count;
    @Override
    public void run() {
        addCount();
    }

    private void addCount() {
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println(count);
    }

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

運行結果:

...
8253
8353
8153
8053
7875
7675

在addCount方法上加入synchronized同步關鍵字與static關鍵字,達到同步的效果。

再次運行結果:

....
9600
9700
9800
9900
10000

關鍵字volatile提示線程每次從共享內存中讀取變量,而不是從私有內存中讀取,這樣就保證了同步數據的可見性。但在這裏須要注意的是:若是修改實例變量中的數據,好比i++,也就是比

i=i+1,則這樣的操做其實並非一個原子操做,也就是非線程安全。表達式i++的操做步驟分解爲下面三步:

  1. 從內存中取i的值;
  2. 計算i的值;
  3. 將i值寫入到內存中。

假如在第二步計算i值的時候,另一個線程也修改i的值,那麼這個時候就會髒數據。解決的方法其實就是使用synchronized關鍵字。因此說volatile關鍵字自己並不處理數據的原子性,而是強制對數據的讀寫及時影響到主內存中。

3.4 使用原子類進行i++操做

除了在i++操做時使用synchronized關鍵字實現同步外,還可使用AtomicInteger原子類進行實現。

原子操做是不可分割的總體,沒有其餘線程可以中斷或檢查正在原子操做中的變量。它能夠在沒有鎖的狀況下作到線程安全。

示例代碼:

public class MyThread extends Thread {
    private static AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        addCount();
    }

    private static void addCount() {
        for (int i = 0;i<100;i++){
            System.out.println(count.incrementAndGet());
        }
    }

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

打印結果:

....
9996
9997
9998
9999
10000

成功達到累加的效果。

3.5 原子類也不安全

原子類在具備有邏輯性的狀況下輸出結果也具備隨機性。

示例代碼:

public class MyThread extends Thread {
    private static AtomicInteger count = new AtomicInteger(0);

    public MyThread(String name) {
        super(name);
    }

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

    private void addCount() {
        System.out.println(Thread.currentThread().getName()+"加100以後:"+count.addAndGet(100));
        count.addAndGet(1);
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread[] myThreads = new MyThread[10];
        for (int i = 0; i < 10; i++) {
            myThreads[i] = new MyThread("Thread-"+i);
        }
        for (int i = 0; i < 10; i++) {
            myThreads[i].start();
        }
        Thread.sleep(2000);
        System.out.println(MyThread.count);
    }
}

打印結果:

Thread-0加100以後:100
Thread-2加100以後:201
Thread-1加100以後:302
Thread-5加100以後:602
Thread-4加100以後:502
Thread-3加100以後:402
Thread-6加100以後:706
Thread-7加100以後:807
Thread-9加100以後:908
Thread-8加100以後:1009
1010

能夠看到,結果值正確可是打印順序出錯了,出現這樣的緣由是由於AtomicInteger的addAndGet()方法是原子的,但方法與方法之間的調用卻不是原子的。也就是方法addCount的調用不是原子的。解決這樣的問題必需要用同步。

3.6 synchronized代碼塊有volatile同步的功能

關鍵字synchronized可使多個線程訪問同一個資源具備同步性,並且它還具備將線程工做內存中的私有變量與公共內存中的變量同步的功能。

咱們把前面講到的異步死循環代碼改造一下:

public class PrintString implements Runnable{
    private boolean isRunnning = true;

    @Override
    public void run() {
        String lock = new String();
        System.out.println("Thread begin: "+Thread.currentThread().getName());
        while (isRunnning == true){
            synchronized (lock){
                //加與不加的效果就是是否死循環
            }
        }
        System.out.println("Thread end: "+Thread.currentThread().getName());
    }

    public boolean isRunnning() {
        return isRunnning;
    }

    public void setRunnning(boolean runnning) {
        isRunnning = runnning;
    }

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        Thread thread = new Thread(printString,"Thread-A");
        thread.start();
        Thread.sleep(1000);
        printString.setRunnning(false);
        System.out.println("我要中止它!" + Thread.currentThread().getName());
    }

}

打印結果:

Thread begin: Thread-A
我要中止它!main
Thread end: Thread-A

關鍵字synchronized能夠保證在同一時刻,只有一個線程能夠執行某一個方法或某一個代碼塊。它包含兩個特徵:互斥相和可見性。同步synchronized不只能夠解決一個線程看到對象處於不一致的狀態,還能夠保證進入同步方法或者同步代碼塊的每一個線程,都看到由同一個鎖保護以前全部的修改效果。

學習多線程併發。要着重「外修互斥,內修可見」,這是掌握多線程、學習多線程併發的重要技術點。

參考

《Java多線程編程核心技術》高洪巖著

擴展

擴展

Java多線程編程核心技術(一)Java多線程技能

Java多線程編程核心技術(三)多線程通訊

Java多線程核心技術(四)Lock的使用

Java多線程核心技術(五)單例模式與多線程

Java多線程核心技術(六)線程組與線程異常

相關文章
相關標籤/搜索