Java 多线程之 synchronized 和 volatile 的比较
概述
在做多線程并發處理時,經常需要對資源進行可見性訪問和互斥同步操作。有時候,我們可能從前輩那里得知我們需要對資源進行 volatile 或是 synchronized 關鍵字修飾處理。可是,我們卻不知道這兩者之間的區別,我們無法分辨在什么時候應該使用哪一個關鍵字。本文就針對這個問題,展開討論。
版權說明
著作權歸作者所有。
商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
本文作者:Coding-Naga
發表日期: 2016年4月5日
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/51062421
來源:CSDN
更多內容:分類 >> 并發與多線程
內存語義分析
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;}@Overridepublic 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 代碼:
這樣一來, volatile 修改了 isRunning 的可見性,使得主線程的 thread.setRunFlag(false) 將會 happens-before 子線程中的 while 。最終,使得子線程從 while 的循環中跳出,問題解決。
下面我們來看看 volatile 是如何修改了 isRunning 的可見性的吧。
這里,因為 isRunning 被 volatile 修飾,那么當子線程想要訪問工作內存中的 inRunning 時,被強制地直接從共享內存中獲取。而共享內存中的 isRunning 被主線程修改過了,已經被修改成了 false ,while 被打破,這樣子線程就從 while 的循環中跳出來了。
volatile 原子性測試
volatile 確實有很多優點,可是它卻有一個致命的缺點,那就是 volatile 并不是原子操作。也就是在多線程的情況,仍然是不安全的。
可能,這個時候你會發問說,既然 volatile 保證了它在線程間的可見性,那么在什么時候修改它,怎么修改它,對于其他線程是可見的,某一個線程讀到的都會是修改過的值,為什么還要說它還是不安全的呢?
我們通過一個例子來說明吧,這樣更形象一些。大家看下面這樣一段代碼:
這是一個未經任何處理的,很直白的過程。可是它的結果,也很直白。其實這個結果并不讓人意外,從我們學習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++ 可以類比成以下的過程:
可見,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);}@Overridepublic 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并發編程的藝術》
征集
如果你也需要使用ProcessOn這款在線繪圖工具,可以使用如下邀請鏈接進行注冊:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d
總結
以上是生活随笔為你收集整理的Java 多线程之 synchronized 和 volatile 的比较的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java设计模式——桥模式
- 下一篇: MySQL 性能优化技巧(一)