標籤:
1. 可見度
如果一個線程對共用變數值的修改,能夠及時的被其他線程看到,叫做共用變數的可見度。如果一個變數同時在多個線程的工作記憶體中存在副本,那麼這個變數就叫共用變數
2. JMM(java記憶體模型)
多個線程同時對主記憶體的一個共用變數進行讀取和修改時,首先會讀取這個變數到自己的背景工作執行緒成為一個副本中,改動之後,再更新回主線程中變數記憶體。
兩條規定:
a.線程對共用變數的所有操作必須在工作記憶體中進行,不能直接操作主記憶體
b.不同線程間不能訪問彼此的工作記憶體中的變數,線程間變數值的傳遞都必須經過主記憶體
如果一個線程1對共用變數x的修改對線程2可見的話,需要經過下列步驟:
a.線程1將更改x後的值更新到主記憶體
b.主記憶體將更新後的x的值更新到線程2的工作記憶體中x的副本
所以,要實現共用變數的可見度必須保證下列兩點:
a.線程對工作記憶體中副本的更改能夠及時的更新到主記憶體上
b.其他線程能夠及時的將主記憶體上共用變數的更新重新整理到自己工作記憶體的該變數的副本上
Java中可以通過synchronized、volatile、java concurrent類來實現共用變數的可見度
3. synchronized實現可見度
synchronized 實際上是對訪問修改共用變數的代碼塊進行加互斥鎖,多個線程對synchronized代碼塊的訪問時,某一時刻僅僅有一個線程在訪問和修改代碼塊中的內 容(加鎖),其他所有的線程等待該線程離開代碼塊時(釋放鎖)才有機會進入synchronized代碼塊。
所以某一個線程進入synchronized代碼塊前後,執行過程入如下:
a.線程獲得互斥鎖
b.清空工作記憶體
c.從主記憶體拷貝共用變數最新的值到工作記憶體成為副本
d.執行代碼
e.將修改後的副本的值重新整理回主記憶體中
f.線程釋放鎖
隨後,其他代碼在進入synchronized代碼塊的時候,所讀取到的工作記憶體上共用變數的值都是上一個線程修改後的最新值。
多個線程之間執行共同的代碼塊(訪問修改共用變數),由於線程交叉執行,最終共用變數的最後值可能有多種結果:
public class SynchronizedTest { private boolean ready = false; private int result = 0; private int number = 1; public void write(){ ready = true; number = 2; } public void read(){if(ready){ result = number * 3; } System.out.println("result is " + result); } private class TestThread extends Thread{ private boolean flag; public TestThread(boolean flag){ this.flag = flag; } @Override public void run() { // TODO Auto-generated method stub if(flag){ write(); }else{ read(); } } } public static void main(String[] args){ SynchronizedTest test = new SynchronizedTest(); test.new TestThread(true).start(); test.new TestThread(false).start(); }}
如上代碼,由於兩個線程交叉執行,最後result的結果可能是0或者6或者3
共用變數不可見主要有下列原因:
a.線程的交叉執行
b.重排序
c.共用變數未能及時更新
通過使用synchronized可以保證原子性(synchronized代碼塊內容要麼不執行,要執行就保證全部執行完畢)和可見度,修改後的代碼為在write和read方法上加synchronized關鍵字
4. volatile實現可見度(jdk 1.5後)
volatile如何?可見度?
volatile變數每次被線程訪問時,都強迫線程從主記憶體中重讀該變數的最新值,而當該變數發生修改變化時,也會強迫線程將最新的值重新整理回主記憶體中。這樣一來,不同的線程都能及時的看到該變數的最新值。
但是volatile不能保證變數更改的原子性:
比 如number++,這個操作實際上是三個操作的集合(讀取number,number加1,將新的值寫回number),volatile只能保證每一 步的操作對所有線程是可見的,但是假如兩個線程都需要執行number++,那麼這一共6個操作集合,之間是可能會交叉執行的,那麼最後導致number 的結果可能會不是所期望的。
所以對於number++這種非原子性操作,推薦用synchronized:
synchronized(this){ number++; }
如下代碼:最後的number的結果不一定是500,有可能是比500小,因為number++不是一個原子性的操作,用volatile不能保證可見度
public class VolatileTest { public static int number = 0; public void increase(){ try { Thread.sleep(300); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } number++; } /** * @param args */ public static void main(String[] args) { final VolatileTest test = new VolatileTest(); for(int i = 0 ; i < 500 ; i++){ new Thread(new Runnable() { @Override public void run() { test.increase(); } }).start(); } //若當期依然有子線程沒有執行完畢 while(Thread.activeCount() > 1){ Thread.yield();//使得當前線程(主線程)讓出CPU時間片 } System.out.println("number is " + number); }}
對於自增之類的非原子性操作,只能通過如下方式保證可見度:
a. synchronized
b. ReentrantLock
c. AtomicInteger
synchronized修改如下:
public void increase(){ try { Thread.sleep(300); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(this){ number++; } }
ReentrantLock修改方式如下:
public class VolatileTest { public static int number = 0; public Lock lock = new ReentrantLock(); public void increase(){ try { Thread.sleep(300); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } lock.lock(); try{ number++;//這塊的代碼實際項目中可能會出現異常,所以要捕獲 }finally{ lock.unlock();//用try finally塊保證Unlock一定要執行 } }。。。}
AtomicInteger,一個提供原子操作的Integer的類。在Java語言中,++i和i++操作並不是安全執行緒的,在使用的時候,不可避免的會用到synchronized關鍵字。而AtomicInteger則通過一種安全執行緒的加減操作介面。
修改如下:
package com.mooc.test;import java.util.concurrent.atomic.AtomicInteger;public class VolatileTest { public static AtomicInteger number = new AtomicInteger(0); public void increase(){ try { Thread.sleep(300); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } number.getAndIncrement();//獲得當前值並且加1 } /** * @param args */ public static void main(String[] args) { final VolatileTest test = new VolatileTest(); for(int i = 0 ; i < 500 ; i++){ new Thread(new Runnable() { @Override public void run() { test.increase(); } }).start(); } //若當期依然有子線程沒有執行完畢 while(Thread.activeCount() > 1){ Thread.yield();//使得當前線程(主線程)讓出CPU時間片 } System.out.println("number is " + number.get()); }}
5. volatile適用情況
a.對變數的寫入操作不依賴當前值
比如自增自減、number = number + 5等(不滿足)
b.當前volatile變數不依賴於別的volatile變數
比如 volatile_var > volatile_var2這個不等式(不滿足)
6. synchronized和volatile比較
a. volatile不需要同步操作,所以效率更高,不會阻塞線程,但是適用情況比較窄
b. volatile讀變數相當於加鎖(即進入synchronized代碼塊),而寫變數相當於解鎖(退出synchronized代碼塊)
c. synchronized既能保證共用變數可見度,也可以保證鎖內操作的原子性;volatile只能保證可見度
Java多線程共用變數控制