Java多線程共享變量控制


1. 可見性

如果一個線程對共享變量值的修改,能夠及時的被其他線程看到,叫做共享變量的可見性。如果一個變量同時在多個線程的工作內存中存在副本,那么這個變量就叫共享變量

 

2. JMM(java內存模型)

多個線程同時對主內存的一個共享變量進行讀取和修改時,首先會讀取這個變量到自己的工作內存中成為一個副本,對這個副本進行改動之后,再更新回主內存中變量所在的地方。

(由於CPU時間片是以線程為最小單位,所以這里的工作內存實際上就是指的物理緩存,CPU運算時獲取數據的地方;而主內存也就是指的是內存,也就是原始的共享變量存放的位置)

兩條規定:

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只能保證可見性

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM