線程安全問題
本篇主要講解 線程安全問題,演示什么情況下會出現線程安全問題,以及介紹了 Java內存模型 、volatile關鍵字 、CAS 等 ,最后感謝吳恆同學的投稿! 一起來了解吧!!
1. 如何會發生線程安全
運行如下程序:
/**
* @program:
* @description: 多線程操作的對象
* @author:
* @create:
**/
public class MyCount {
private int myCount = 0 ;
public int getMyCount() {
return myCount;
}
public void setMyCount(int myCount) {
this.myCount = myCount;
}
@Override
public String toString() {
return "MyCount{" +
"myCount=" + myCount +
'}';
}
}
創建線程
public class CountThread1 extends Thread{
private MyCount myCount ;
private static Object synch = new Object();
public CountThread1( MyCount myCount) {
this.myCount = myCount;
}
@Override
public void run() {
//myCount 加到100
while (true) {
if(myCount.getMyCount()<100) {
myCount.setMyCount(myCount.getMyCount() + 1);
System.out.println(Thread.currentThread().getName() + " set myCount值:" + myCount.getMyCount());
}else{
break;
}
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行下列線程
public static void main(String[] args) {
MyCount myCount = new MyCount();
CountThread1 c1 = new CountThread1(myCount);
CountThread1 c2 = new CountThread1(myCount);
CountThread1 c3 = new CountThread1(myCount);
c1.setName("c1");
c2.setName("c2");
c3.setName("c3");
c1.start();
c2.start();
c3.start();
}
測試結果:
以上是多線程同時對同一變量進行操作時,發生的非線程安全問題。換句話說只用共享資源的讀寫訪問才需要同步化,如果不是共享資源,那么根本沒有同步的必要。
2.線程內存模型
2.1 線程內存模型如下:
某些JVM運行中,有兩塊主要的內存,一個是主內存,另外一個是每個線程都具有的工作內存。
2.2 線程運行的流程如下:
1. 從主內存中copy要操作的數據到自己的工作內存中去。
2. 線程主體從自己的工作內存中讀取數據進行操作。
3. 操作完成后,在同步到主內存中去。
結合線程運行的流程,上述多線程可能會出現以下執行流程:
1. c3線程獲得cpu資源,執行+1操作,在c3想同步count值1到主內存中去時。
2. c2線程得到了cpu的資源,也同樣執行+1操作,但沒+1前count的值是0,而不是1,c2執行完后,打印count=1的值,並且把數據同步到主內存中。
3. 此時c3又得到了cpu的資源,於所執行剛才沒有完成的同步操作,同時又打印count=1的值。
這就導致出現上圖結果的原因。
解決上述問題最常見的方法就是在線程的run方法上添加synchronized關鍵字
while (true) {
synchronized (synch) {
if(myCount.getMyCount()<100) {
myCount.setMyCount(myCount.getMyCount() + 1);
System.out.println(Thread.currentThread().getName() + " set myCount值:" + myCount.getMyCount());
}else{
break;
}
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.volatile關鍵字
如下線程:
public class CountThread2 extends Thread{
private boolean isRunning = true;
@Override
public void run() {
System.out.println("進入到run方法了");
while (isRunning) {
}
System.out.println("run方法結束");
}
public void setNotRunning() {
System.out.println("isRunning為false");
isRunning = false;
}
}
執行如下方法
CountThread2 ct2 = new CountThread2();
ct2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ct2.setNotRunning();
結果如下:
明明isRunning被設置為false,為什么線程還沒有結束?
還要把這個圖拿過來
當主線程把isRunning設置為false,並同步到主線程后,當 ct2 線程執行while(isRunning)時,isRunning的值是從工作內存中獲取的,也就是多線程同時操作同一屬性時,當主內存中變量被修改時,其它線程並沒有感知到該變量被修改。
代碼修改
//變量添加 volatile關鍵字
private volatile boolean isRunning = true;
執行結果如下:
發現當isRunning被修改為false時,ct2線程就結束了。這是為什么了?
當線程修改volatile變量后,會立刻同步到主內存中,同時迫使在使用該變量的其它線程去主內存中同步該變量到各自的工作內存中。但遺憾的是volatile不具有原子性
4.原子性和可見性
4.1 線程安全包含原子性和可見性兩個方面, Java的同步就在都是圍繞這兩個方面來確保線程安全的
4.1.1 什么是可見性?可見性就是volatile修飾變量表現出來的性質,使變量在多個線程中可見。
4.1.2 什么是原子性操作?原子性操作就是不可分的操作,int i = a;就是原子性操作,而上述count++就不是原子性操作
上圖是對線程內存模型進一步的描述,一線程在執行use操作時,突然時去了cpu執行權限(cpu執行任何原子性操作時,是不可能出現中斷的),也就出現上述非線程安全的問題了。
5.synchronized
多線程在訪問synchronized同步區域時,如果一線程獲取到同步鎖,其它線程就會被阻塞,就會形成線程安全的機制。
既然線程安全包含原子性和可見性 ,synchronized具有線程安全的功能,那么synchronized具有原子性和可見性?
這種等比性思想對嗎?為什么synchronized具有原子性和可見性
方法一:
private int i= 1;
synchronized public void run() {
System.out.println("i++:"+i);
}
方法二:
private volatile int i= 1;
public void run() {
System.out.println("i++:"+i);
}
當多線程訪問方法一,多個線程依次訪問方法二,我想兩種類型都是線程安全,且結果一致的。那你對synchronized又有什么新的想法了?synchronized = volatile+非當前線程阻塞
6.悲觀鎖 VS 樂觀鎖
修改MyCount代碼
public class MyCount {
private AtomicInteger myCount = new AtomicInteger(0);
public int getMyCount() {
return myCount.get();
}
public int setMyCount() {
return myCount.incrementAndGet();
}
}
繼續執行如下操作
MyCount myCount = new MyCount();
CountThread1 c1 = new CountThread1(myCount);
CountThread1 c2 = new CountThread1(myCount);
CountThread1 c3 = new CountThread1(myCount);
c1.setName("c1");
c2.setName("c2");
c3.setName("c3");
c1.start();
c2.start();
c3.start(
你會發現線程是安全的,這又是為什么了?這種線程安全的原因和synchronized又有什么聯系和區別?
synchronized是一種悲觀鎖機制,有一線程獲取鎖后,其它線程就被阻塞,通過這種方法可以到達線程全的效果。多線程競爭的情況下會出現阻塞和喚醒的性能問題。
CAS compare and swap ,先比較然后交換
上述代碼 myCount.incrementAndGet();源碼如下
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
getAndAddInt(this, valueOffset, 1) + 1;源碼如下
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
現描述:getAndAddInt()方法中個參數的意思
①: var5 = this.getIntVolatile(var1, var2);
var1 : myCount值在內存中的首地址
var2:myCount值在內存中的偏移量
var5 = this.getIntVolatile(var1, var2); 也就是獲取當前myCount的值
②:!this.compareAndSwapInt(var1, var2, var5, var5 + var4)
var1,和 var2 獲取內存中myCount最新的值,
var4: var4就是1,
var5是 this.getIntVolatile(var1, var2)獲取的舊值
var5 + var4是myCount將要得到的值
判斷這兩個值是否相等,如果不相等,就表明myCount的值被其它線程操作,myCount值不是最新的,需要從新獲取,也就是從新執行 ① 和②,直到值新舊值相等時,表明沒有其它線程在操作此變量,然后
就把var5 + var4 賦值給 var5,從而達到線程安全。
上述的流程,就是CAS想要表達的思想,多線程訪問同一臨界區域時,都認為沒有上鎖,通過先比較來確認是否發生沖突,直到沒有沖突時執行交換操作。這也同時解決了synchronized 多線程競爭的情況下會出現阻塞和喚醒的性能問題。
備注:以上線程內存模型相關圖片來自於高洪岩《Java 多線程編程核心技術》
7.總結
本篇主要介紹了 線程安全問題,Java內存模型,volatile關鍵字 synchronized關鍵字 CAS 等
最后 感謝吳恆同學的投稿 !!!
個人博客地址: https://www.askajohnny.com 歡迎訪問!
本文由博客一文多發平台 OpenWrite 發布!