程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java多線程之synchronized和volatile的比較

Java多線程之synchronized和volatile的比較

編輯:JAVA綜合教程

Java多線程之synchronized和volatile的比較


概述

在做多線程並發處理時,經常需要對資源進行可見性訪問和互斥同步操作。有時候,我們可能從前輩那裡得知我們需要對資源進行 volatile 或是 synchronized 關鍵字修飾處理。可是,我們卻不知道這兩者之間的區別,我們無法分辨在什麼時候應該使用哪一個關鍵字。本文就針對這個問題,展開討論。


內存語義分析

happens-before 模型簡介

如果你單從字面上的意思來理解 happens-before 模型,你可能會覺得這是在說某一個操作在另一個操作之前執行。不過,學習完 happens-before 之後,你就不會還這樣理解了。以下是《Java 並發編程的藝術》書上對 happens-before 的定義:

在 JMM(Java Memory Model) 中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在 happens-before 關系。這裡提到的兩個操作既可以在一個線程之內,也可以是在不同的線程之間。

volatile 的內存語義

對於多線程編程來說,每個線程是可以擁有共享內存中變量的一個拷貝,這一點在後面還是會講到,這裡就不作過多說明。如果一個變量被 volatile 關鍵字修飾時,那麼對這的變量的寫是將本地內存中的拷貝刷新到共享內存中;對這個變量的讀會有一些不同,讀的時候是無視他的本地內存的拷貝的,只是從共享變量中去讀取數據。

synchronized 的內存語義

我們說 synchronized 實際上是對變量進行加鎖處理。那麼不管是讀也好,寫也好都是基於對這個變量的加鎖操作。如果一個變量被 synchronized 關鍵字修飾,那麼對這的變量的寫是將本地內存中的拷貝刷新到共享內存中;對這個變量的讀就是將共享內存中的值刷新到本地內存,再從本地內存中讀取數據。因為全過程中變量是加鎖的,其他線程無法對這個變量進行讀寫操作。所以可以理解成對這個變量的任何操作具有原子性,即線程是安全的。


實例論證

上面的一些說明或是定義可能會有一些乏味枯燥,也不太好理解。這裡我們就列舉一些例子來說明,這樣比較具體和形象一些。

volatile 可見性測試

RunThread.java

public class RunThread extends Thread {
    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunFlag(boolean flag) {
        isRunning = flag;
    }

    @Override
    public void run() {
        System.out.println("I'm come in...");
        boolean first = true;
        while(isRunning) {
            if (first) {
                System.out.println("I'm in while...");
                first = false;
            }
        }
        System.out.println("I'll go out.");
    }
}

MyRun.java

public class MyRun {
    public static void main(String[] args) throws InterruptedException {
        RunThread thread = new RunThread();
        thread.start();
        Thread.sleep(100);
        thread.setRunFlag(false);
        System.out.println("flag is reseted: " + thread.isRunning());
    }
}

對於上面的例子只是一個很普通的多線程操作,這裡我們很容易就得到了 RunThread 線程在 while 中進入了死循環。
我們可以在 main() 方法裡看到一句 Thread.sleep(100) ,結合前面說到的 happens-before 內存模型,可知下面的 thread.setRunFlag(false) 並不會 happens-before 子線程中的 while 。這樣一來,雖然主線程中對 isRunning 進行了修改,然而對子線程中的 while 來說,並沒有改變,所以這就會引發在 while 中的死循環。
在這種情況下,線程工作時的內存模型像下面這樣
這裡寫圖片描述
在這裡,可能你會奇怪,為什麼會有兩個“內存塊”?這是出於多線程的性能考慮的。雖然對象以及成員變量分配的內存是在共享內存中的,不過對於每個線程而言,還是可以擁有這個對象的拷貝,這樣做的目的是為了加快程序的執行,這也是現代多核處理器的一個顯著特征。從上面的內存模型可以看出,Java的線程是直接與它自身的工作內存(本地內存)交互,工作內存再與共享內存交互。這樣就形成了一個非原子的操作,在Java裡多線程的環境下非原子的操作是很危險的。這個我們都已經知道了,因為這可能會被異步的讀寫操作所破壞。
這裡工作內存被 while 占用,無法去更新主線程對共享內存 isRunning 變量的修改。所以,如果我們想要打破這種限制,可以通過 volatile 關鍵字來處理。通過 volatile 關鍵字修飾 while 的條件變量,即 isRunning。就像下面這樣修改 RunThread.java 代碼:<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> private volatile boolean isRunning = true;

這樣一來, volatile 修改了 isRunning 的可見性,使得主線程的 thread.setRunFlag(false) 將會 happens-before 子線程中的 while 。最終,使得子線程從 while 的循環中跳出,問題解決。
下面我們來看看 volatile 是如何修改了 isRunning 的可見性的吧。
這裡寫圖片描述
這裡,因為 isRunning 被 volatile 修飾,那麼當子線程想要訪問工作內存中的 inRunning 時,被強制地直接從共享內存中獲取。而共享內存中的 isRunning 被主線程修改過了,已經被修改成了 false ,while 被打破,這樣子線程就從 while 的循環中跳出來了。

volatile 原子性測試

volatile 確實有很多優點,可是它卻有一個致命的缺點,那就是 volatile 並不是原子操作。也就是在多線程的情況,仍然是不安全的。
可能,這個時候你會發問說,既然 volatile 保證了它在線程間的可見性,那麼在什麼時候修改它,怎麼修改它,對於其他線程是可見的,某一個線程讀到的都會是修改過的值,為什麼還要說它還是不安全的呢?
我們通過一個例子來說明吧,這樣更形象一些。大家看下面這樣一段代碼:

public class DemoNoProtected {

    static class MyThread extends Thread {
        static int count = 0;

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

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

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

        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}
count = 300
count = 300
count = 300
count = 400
... ...
count = 7618
count = 7518
count = 9918

這是一個未經任何處理的,很直白的過程。可是它的結果,也很直白。其實這個結果並不讓人意外,從我們學習Java的時候,就知道Java的多線程並不安全。是不是從上面的學習中,你感覺這個可以通過 volatile 關鍵字解決?既然你這麼說,那麼我們就來試一試,給 count 變量添加 volatile 關鍵字,如下:

public class DemoVolatile {
    static class MyThread extends Thread {
        static volatile int count = 0;
        ... ...
    }

    public static void main(String[] args) {
        ... ...
    }
}
count = 100
count = 300
count = 400
count = 200
... ...
count = 9852
count = 9752
count = 9652
... ...
count = 8154
count = 8054

不知道這個結果是不是會讓你感覺到意外。對於 count 的混亂的數字倒是好理解一些,應該多個線程同時修改時就發生這樣的事情。可是我們在結果為根本找不到邏輯上的最大值“10000”,這就有一些奇怪了。因為從邏輯上來說, volatile修改了 count 的可見性,對於線程 A 來說,它是可見線程 B 對 count 的修改的。只是從結果中並沒有體現這一點。
我們說,volatile並沒有保證線程安全。在上面子線程中的 addCount() 方法裡,執行的是 count++ 這樣一句代碼。而像 count++ 這樣一句代碼從學習Java變量自增的第一堂課上,老師就應該強調過它的執行過程。count++ 可以類比成以下的過程:

int tmp = count;
tmp = tmp + 1;
count = tmp;

可見,count++ 並非原子操作。任何兩個線程都有可能將上面的代碼分離進行,安全性便無從談起了。
所以,到這裡我們知道了 volatile 可以改變變量在線程之間的可見性,卻不能改變線程之間的同步。而同步操作則需要其他的操作來保證。

synchronized 同步測試

上面說到 volatile 不能解決線程的安全性問題,這是因為 volatile 不能構建原子操作。而在多線程編程中有一個很方便的同步處理,就是 synchronized 關鍵字。下面來看看 synchronized 是如何處理多線程同步的吧,代碼如下:

public class DemoSynchronized {

    static class MyThread extends Thread {
        static int count = 0;

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

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

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

        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}
count = 100
count = 200
count = 300
... ...
count = 9800
count = 9900
count = 10000

通過 synchronized 我們可以很容易就獲得了理想的結果。而關於 synchronized 關鍵字的內存模型可以這樣來表示:
這裡寫圖片描述
某一個線程在訪問一個被 synchronized 修飾的變量時,會對此變量的共享內存進行加鎖,那麼這個時候其他線程對其的訪問就會被互斥。 synchronized 的內部實現其實也是鎖的概念。


Ref

《Java多線程編程核心技術》 《Java並發編程的藝術》

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved