深入探究Java線程不安全的原因與解決
一、什么是線程安全
想給出一個線程安全的確切定義是復雜的,但我們可以這樣認為:
如果多線程環(huán)境下代碼運行的結果是符合我們預期的,即在單線程環(huán)境應該的結果,則說這個程序是線程安全的
二、線程不安全的原因
1、修改共享數(shù)據(jù)
static class Counter { public int count = 0; void increase() { count++; } } public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); }
上面的線程不安全的代碼中, 涉及到多個線程針對 counter.count 變量進行修改.此時這個 counter.count 是一個多個線程都能訪問到的 “共享數(shù)據(jù)”
2、原子性
原子性就是 提供互斥訪問,同一時刻只能有一個線程對數(shù)據(jù)進行操作,有時也把這個現(xiàn)象叫做同步互斥,表示操作是互相排斥的
不保證原子性會給多線程帶來什么問題 如果一個線程正在對一個變量操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。 這點也和線程的搶占式調度密切相關. 如果線程不是 “搶占” 的, 就算沒有原子性, 也問題不大
3、內存可見性
可見性指, 一個線程對共享變量值的修改,能夠及時地被其他線程看到.
Java 內存模型 (JMM): Java虛擬機規(guī)范中定義了Java內存模型. 目的是屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達到一致的并發(fā)效果.
private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (count == 0) { } System.out.println(Thread.currentThread().getName() + "執(zhí)?完成"); }); t1.start(); Scanner scanner = new Scanner(System.in); System.out.print("->"); count = scanner.nextInt(); }
4、指令重排序
一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序,該觀察結果一般雜亂無序,(happens-before原則)。
編譯器對于指令重排序的前提
“保持邏輯不發(fā)生變化”. 這一點在單線程環(huán)境下比較容易判斷, 但是在多線程環(huán)境下就沒那么容易了, 多線程的代碼執(zhí)行復雜程度更高, 編譯器很難在編譯階段對代碼的執(zhí)行效果進行預測, 因此激進的重排序很容易導致優(yōu)化后的邏輯和之前不等價
三、解決線程安全方案
- volatile解決內存可見性和指令重排序
代碼在寫入 volatile 修飾的變量的時候:
改變線程?作內存中volatile變量副本的值,將改變后的副本的值從?作內存刷新到主內存
- 直接訪問工作內存,速度快,但是可能出現(xiàn)數(shù)據(jù)不?致的情況
- 加上 volatile , 強制讀寫內存. 速度是慢了, 但是數(shù)據(jù)變的更準確了
代碼示例:
/** * 內存可見性 * 線程1沒感受到flag的變化,實際線程2已經改變了flag的值 * 使用volatile,解決內存可見性和指令重排序 */ public class ThreadSeeVolatile { //全局變量 private volatile static boolean flag = true; public static void main(String[] args) { //創(chuàng)建子線程 Thread t1 = new Thread(() ->{ System.out.println("1開始執(zhí)行:" + LocalDateTime.now()); while(flag){ } System.out.println("2結束執(zhí)行" + LocalDateTime.now()); }); t1.start(); Thread t2 = new Thread(() ->{ //休眠1s try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("修改flag=false"+ LocalDateTime.now()); flag = false; }); t2.start(); } }
volatile的缺點
volatile 雖然可以解決內存可見性和指令重排序的問題,但是解決不了原子性問題,因此對于 ++ 和 --操作的線程非安全問題依然解決不了
- 通過synchronized鎖實現(xiàn)原子性操作
JDK提供鎖分兩種:
①一種是synchronized,依賴JVM實現(xiàn)鎖,因此在這個關鍵字作用對象的作用范圍內是同一時刻只能有一個線程進行操作;
②另一種是LOCK,是JDK提供的代碼層面的鎖,依賴CPU指令,代表性的是ReentrantLock。
- synchronized 會起到互斥效果, 某個線程執(zhí)行到某個對象的synchronized 中時, 其他線程如果也執(zhí)行到同一個對象 synchronized 就會阻塞等待.
- 進入 synchronized 修飾的代碼塊, 相當于 加鎖
- 退出 synchronized 修飾的代碼塊, 相當于 解鎖
synchronized修飾的對象有四種:
(1)修飾代碼塊,作用于調用的對象
(2)修飾方法,作用于調用的對象
(3)修飾靜態(tài)方法,作用于所有對象
(4)修飾類,作用于所有對象
// 修飾一個代碼塊: 明確指定鎖哪個對象 public void test1(int j) { synchronized (this) { } } // 修飾一個方法 public synchronized void test2(int j) { } // 修飾一個類 public static void test1(int j) { synchronized (SynchronizedExample2.class) { } } // 修飾一個靜態(tài)方法 public static synchronized void test2(int j) { }
到此這篇關于深入探究Java線程不安全的原因與解決的文章就介紹到這了,更多相關Java線程不安全內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring與Struts整合之讓Spring管理控制器操作示例
這篇文章主要介紹了Spring與Struts整合之讓Spring管理控制器操作,結合實例形式詳細分析了Spring管理控制器相關配置、接口實現(xiàn)與使用技巧,需要的朋友可以參考下2020-01-01springmvc+spring+mybatis實現(xiàn)用戶登錄功能(下)
這篇文章主要為大家詳細介紹了springmvc+spring+mybatis實現(xiàn)用戶登錄功能的第二篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Java中CountDownLatch進行多線程同步詳解及實例代碼
這篇文章主要介紹了Java中CountDownLatch進行多線程同步詳解及實例代碼的相關資料,需要的朋友可以參考下2017-03-03