?你還怕多線程嗎?全網最全多線程筆記(番外篇)?
大

家
好
,
我
是
會
寫
B
u
g
又
會
R
a
p
的
X
i
a
o
L
i
n
。
遇
事
先
百
度
,
學
習
關
注
我
,
今
天
我
們
來
學
學
多
線
程
六、 volatile關鍵字
6.1、問題引入
public class VolatileThread extends Thread { // 定義成員變量 private boolean flag = false ; public boolean isFlag() { return flag;} @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 將flag的值更改為true this.flag = true ; System.out.println("flag=" + flag); } } public class VolatileThreadDemo {// 測試類 public static void main(String[] args) { // 創建VolatileThread線程對象 VolatileThread volatileThread = new VolatileThread() ; volatileThread.start(); // main方法 while(true) { if(volatileThread.isFlag()) { System.out.println("執行了======"); } } } }
6.2、多線程下變量的不可見性
6.2.1、概述
在介紹多線程并發修改變量不可見現象的原因之前,我們先看看另一種Java內存模型(和Java并發編程有關的模型):JMM。
JMM(Java Memory Model):Java內存模型是Java虛擬機規范中定義的一種內存模型,Java內存模型是標準化的,他屏蔽了底層不同計算機的硬件的不同
Java內存模型描述了Java程序中各種變量(線程共享變量)的訪問規則以及在JVM中將變量存儲到內存和從內存中讀取變量的底層細節。
JMM有以下規定:
所有的共享變量都存儲于主內存(這里的變量是指實例變量和類變量,不包含局部變量,因為局部變量的線程是私有的,不存在競爭的問題)
每一個線程都有自己獨立的工作內存,線程的工作內存保留了被線程使用的變量的工作副本
線程對變量的所有操作(讀、取)都必須在工作內存中完成,而不能直接讀寫主內存的變量。
本地內存和主內存之間的關系:
6.2.2、問題分析
子線程1從主內存中讀取到數據并復制到其對應的工作內存。
修改flag的值為true,但是這個時候flag的值還并沒有寫會主內存。
此時main方法讀取到了flag的值為false。
當子線程1將flag的值寫回去之后,由于main函數中的while(true)調用的是系統底層的代碼,速度快,快到沒有時間再去讀取主內存中的值,所以此時while(true)讀取到的值一直是flag = false。
此時我們能想到的辦法是,如果main線程從主內存中讀取到了flag最新的值,那么if語句就可以執行了。
6.2.3、多線程下變量的不可見性的原因
每個線程都有自己的工作內存,線程都是從主內存中拷貝到共享變量的副本值
每個線程都是在自己的工作內存中操作共享變量的。
6.2.4、解決方案
while(true){ synchronized(t){ if(t.isFlag()){ System.out.print("主線程進入循環") } } }
第一個線程進入synchronized代碼塊前后,執行過程如下:
線程獲得鎖
清空工作內存
從主內存中拷貝共享變量的最新值變成副本
執行代碼
將修改后的值重新放回主內存中
線程釋放鎖
JMM中主內存和本地內存-第 2 頁我們還可以對共享變量用volatile關鍵字修飾,volatile關鍵字的作用是在多線程并發下修改共享變量實現可見性。,一旦一線程修改了volatile修飾的變量,另一個線程可以立即讀取到最新值。
6.2.5、volatile和synchronized
volatile只能修飾實例變量和類變量,而synchronized可以修飾方法以及代碼塊
volatile保證數據的可見性,但是不保證原子性(多線程進行寫操作,不保證線程安全),而synchronized是一種排他互斥的機制,可以保證線程安全。
七、原子性
所謂的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了執行并且不會受到任何因素的干擾而中斷,要么所有的操作都不執行。
7.1、問題引入
public class VolatileAtomicThread implements Runnable { // 定義一個int類型的遍歷 private int count = 0 ; @Override public void run() { // 對該變量進行++操作,100次 for(int x = 0 ; x < 100 ; x++) { count++ ; System.out.println("count =========>>>> " + count); } } } public class VolatileAtomicThreadDemo { public static void main(String[] args) { // 創建VolatileAtomicThread對象 VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ; // 開啟100個線程對count進行++操作 for(int x = 0 ; x < 100 ; x++) { new Thread(volatileAtomicThread).start(); } } }
執行結果:不保證一定是10000
7.2、問題原理說明
以上問題主要是發生在count++操作上,count++操作包含3個步驟:
從主內存中讀取數據到工作內存
對工作內存中的數據進行++操作
將工作內存中的數據寫回到主內存
count++操作不是一個原子性操作,也就是說在某一個時刻對某一個操作的執行,有可能被其他的線程打斷。
1)假設此時x的值是100,線程A需要對改變量進行自增1的操作,首先它需要從主內存中讀取變量x的值。由于CPU的切換關系,此時CPU的執行權被切換到了B線程。A線程就處于就緒狀態,B線程處于運行狀態
2)線程B也需要從主內存中讀取x變量的值,由于線程A沒有對x值做任何修改因此此時B讀取到的數據還是100
3)線程B工作內存中x執行了+1操作,但是未刷新之主內存中
4)此時CPU的執行權切換到了A線程上,由于此時線程B沒有將工作內存中的數據刷新到主內存,因此A線程工作內存中的變量值還是100,沒有失效。A線程對工作內存中的數據進行了+1操作
5)線程B將101寫入到主內存
6)線程A將101寫入到主內存
雖然計算了2次,但是只對A進行了1次修改。
7.3、volition的原子性
在多線程環境下,volatile關鍵字可以保證共享數據的可見性,但是并不能保證對數據操作的原子性(在多線程環境下volatile修飾的變量也是線程不安全的)。
在多線程環境下,要保證數據的安全性,我們還需要使用鎖機制。
7.4、問題解決辦法
7.4.1、使用鎖機制(加鎖)
我們可以給count++操作添加鎖,那么count++操作就是臨界區的代碼,臨界區只能有一個線程去執行,所以count++就變成了原子操作。
缺點:性能差。
public class VolatileAtomicThread implements Runnable { // 定義一個int類型的變量 private volatile int count = 0 ; private static final Object obj = new Object(); @Override public void run() { // 對該變量進行++操作,100次 for(int x = 0 ; x < 100 ; x++) { synchronized (obj) { count++ ; System.out.println("count =========>>>> " + count); } } } }
7.4.2、使用原子類
Java從JDK5開始提供了java.util.concurrent.atomic包(簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單,性能高效,線程安全地更新一個變量的方式。我們可以使用原子類來保證原子性操作,從而保證線程安全。
我們以Integer的原子類進行講解。
public class VolatileAtomicThread implements Runnable { // 定義一個int類型的變量,默認值是0,我們也可以指定長度 private AtomicInteger atomicInteger = new AtomicInteger() ; @Override public void run() { // 對該變量進行++操作,100次 for(int x = 0 ; x < 100 ; x++) { int i = atomicInteger.getAndIncrement(); System.out.println("count =========>>>> " + i); } } }
7.5、原子類CAS機制
CAS的全成是: Compare And Swap(比較再交換); 是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令。CAS可以將read-modify-check-write轉換為原子操作,這個原子操作直接由處理器保證。
CAS機制當中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。
7.5.1、CAS機制詳解
在內存地址V當中,存儲著值為10的變量。
此時線程1想要把變量的值增加1。對線程1來說,舊的預期值A=10,要修改的新值B=11。
在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等于V的實際值,說明值已經被更改過了,提交失敗。
線程1重新獲取內存地址V的當前值,并重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。
這一次比較幸運,沒有其他線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的,說明并沒有人修改過值。
線程1進行SWAP(交換),把地址V的值替換為B,也就是12。
7.6、樂觀鎖和悲觀鎖
CAS和Synchronized都可以保證多線程環境下共享數據的安全性。那么他們兩者有什么區別?
7.6.1、悲觀鎖
Synchronized是從悲觀的角度出發,是一個典型的悲觀鎖。
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程。因此Synchronized我們也將其稱之為悲觀鎖。jdk中的ReentrantLock也是一種悲觀鎖。性能較差!
7.6.2、樂觀鎖
CAS是從樂觀的角度出發,總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。
CAS這種機制我們也可以將其稱之為樂觀鎖。綜合性能較好!很多數據庫都會使用到樂觀鎖機制。
Java 多線程 數據結構
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。