淺談java線程狀態(tài)與線程安全解析
1.線程的幾種狀態(tài)
1.1 線程的狀態(tài)
以下就是我們線程所有的狀態(tài)和意義:
NEW | 已經(jīng)創(chuàng)建Thread但未創(chuàng)建線程 |
RUNNABLE | 可工作的. 又可以分成正在工作中和即將開始工作 |
BLOCKED | 等待鎖(阻塞狀態(tài)) |
WAITING | 調(diào)用wati方法(阻塞狀態(tài)) |
TIMED_WAITING | 調(diào)用sleep方法(阻塞狀態(tài)) |
TERMINATED | 系統(tǒng)線程執(zhí)行完畢已銷毀,但Thread還存在 |
注意:
BLOCKED 表示等待獲取鎖, WAITING 和 TIMED_WAITING 表示等待其他線程發(fā)來通知.
TIMED_WAITING 線程在等待喚醒,但設置了時限; WAITING 線程在無限等待喚醒
1.2 線程狀態(tài)的轉移
各線程之間的轉移關系可以簡化成下圖:
關于yield方法:
在多線程中我們存在一個yield方法可以讓線程在就緒隊列中重新”排隊“,不改變線程狀態(tài)。相當于你去幫別人排隊,但是輪到你了那個人還沒回來,你就就讓原本排在你后面的人換到你的位置上,但你仍然處于排隊狀態(tài)。這種”大公無私“的行為可以類比到我們的yield方法幫助我們理解。
public class Demo{ public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("張三"); // 先注釋掉, 再放開 //Thread.yield(); } } }, "t1"); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("李四"); } } }, "t2"); t2.start(); } }
可以看到:
1. 不使用 yield 的時候, 張三李四大概五五開
2. 使用 yield 時, 張三的數(shù)量遠遠少于李四
結論: yield 不改變線程的狀態(tài), 但是會重新去排隊.
2.有關線程安全問題
2.1 一個簡單的例子
// 創(chuàng)建兩個線程, 讓這倆線程同時并發(fā)的對一個變量, 自增 5w 次. 最終預期能夠一共自增 10w 次. class Counter { // 用來保存計數(shù)的變量 public int count; public void increase() { count++; } } public class Demo { // 這個實例用來進行累加. // public static Counter counter = new Counter(); public static void main(String[] args) { 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(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count: " + counter.count); } }
大家先看到以上的代碼,意思很簡單,用兩個線程對同一個變量進行自增操作,運行的結果如下
看起來不太對,我們再試一次
結果有了變化,但仍然不是我們想要的結果,是什么導致了5w+5w<10w呢?其中一個原因就是線程的隨機調(diào)度和改操作不具有原子性。 這些概念我們下面會詳細講,這里我們先簡單了解一下。
首先我們的自增操作在cpu內(nèi)其實分為三步:
1.LOAD:cpu從內(nèi)存中讀取數(shù)據(jù)到寄存器
2.ADD:在寄存器內(nèi)實現(xiàn)自增
3.SAVE:將寄存器的數(shù)據(jù)寫回內(nèi)存中
而我們已經(jīng)知道cpu對于線程調(diào)度我們可以理解為是隨機的,所以會有很多種可能,比如下圖
其中縱軸代表運行時間,這里我們可以看到兩個線程相當于互不影響,線程1完成自增操作后又將數(shù)據(jù)寫回內(nèi)存由線程2再去操作,這種情況下是沒有問題的。但是也可能是下面的一種情況
此時線程1還沒有將自增后的數(shù)據(jù)寫回內(nèi)存而線程2就已經(jīng)將要修改的數(shù)據(jù)讀入了寄存器,此時相當于線程2讀到了那個還未自增的數(shù)據(jù),相當于兩個線程對同一個數(shù)進行了自增,所以此時相當于只自增了一次。其實情況還有很多,這里我們僅舉例比較經(jīng)典的例子。所以這也能夠解釋為什么結果大于5w而小于10w了。
2.2 造成線程不安全的原因
2.2.1 操作系統(tǒng)的隨機調(diào)度/搶占式運行
這種是操作系統(tǒng)內(nèi)核就已經(jīng)決定的,我們無能為力。類似于我們上一個例子,就是因為線程的隨機調(diào)度和操作不具有原子性造成的。
2.2.2 操作不具有原子性
什么是原子性
我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證,A進入 房間之后,還沒有出來;B 是不是也可以進入房間,打斷 A 在房間里的隱私。這個就是不具備原子性 的。轉換成代碼我們可以理解成只具有一條指令的操作。
當然這個問題我們可以通過加鎖操作解決(以后會提到)。
一條 java 語句不一定是原子的,也不一定只是一條指令,比如我們上面提到的自增操作。
不保證原子性會給多線程帶來什么問題
如果一個線程正在對一個變量操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。 這點也和線程的搶占式調(diào)度密切相關. 如果線程不是 "搶占" 的, 就算沒有原子性, 也問題不大.
2.2.3 多個線程修改同一個變量
1.一個線程修改變量沒事
2.多個線程同時讀一個變量也沒事
3.多個線程同時修改不同變量也沒有問題
唯獨需要注意多個線程修改同一個變量,如果不加以處理可能會造成我們之前講到的例子的問題
2.2.4 內(nèi)存可見性問題
jvm中規(guī)定了java的內(nèi)存模型
線程之間的共享變量存在 主內(nèi)存 (Main Memory).
每一個線程都有自己的 "工作內(nèi)存" (Working Memory) .
當線程要讀取一個共享變量的時候, 會先把變量從主內(nèi)存拷貝到工作內(nèi)存, 再從工作內(nèi)存讀取數(shù)據(jù).
當線程要修改一個共享變量的時候, 也會先修改工作內(nèi)存中的副本, 再同步回主內(nèi)存.
正是因為這種機制,所以可能會出現(xiàn)下面的問題:
由于每個線程有自己的工作內(nèi)存, 這些工作內(nèi)存中的內(nèi)容相當于同一個共享變量的 "副本". 此時修改線程 1 的工作內(nèi)存中的值, 線程2 的工作內(nèi)存不一定會及時變化.通俗的講就是 線程1針對工作內(nèi)容修改了數(shù)據(jù),而線程2此時并不一定能夠及時同步修改的數(shù)據(jù),所以可能會引發(fā)各種問題。
2.2.5 指令重排序
所謂指令重排序是指jvm針對我們的代碼,可能會在保證邏輯不變的情況下去調(diào)整指令執(zhí)行的順序以達到運行效率更高的效果。這種情況在單線程的情況下可以很好實現(xiàn),而在多線程的情況下就可能會出現(xiàn)bug,導致程序邏輯改變。比如對于下面這行代碼:
Test t=new Test();
它其實總共有三個步驟:
1.創(chuàng)建內(nèi)存空間
2.往這個內(nèi)存空間構造一個對象
3.將這個內(nèi)存引用賦給t
在單線程的情況下2,3互換并不會有上面影響,但假如在多線程情況下我們按1,3,2來執(zhí)行,當執(zhí)行到3時t為非null,此時線程2讀取t,但是卻發(fā)現(xiàn)是一個無效對象。
到此這篇關于淺談java線程狀態(tài)與線程安全解析的文章就介紹到這了,更多相關java線程狀態(tài)與線程安全內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springcloud gateway聚合swagger2的方法示例
這篇文章主要介紹了springcloud gateway聚合swagger2的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04SpringBoot應用啟動慢的原因分析及優(yōu)化方法
在使用Spring Boot進行開發(fā)時,快速啟動應用程序是一個非常重要的需求,然而,在某些情況下,我們會遇到Spring Boot應用啟動緩慢的問題,本文將分析Spring Boot應用啟動慢的常見原因,并提供一些優(yōu)化方法,需要的朋友可以參考下2024-08-08Springboot如何使用logback實現(xiàn)多環(huán)境配置?
上一篇文章中老顧介紹了logback基本配置,了解了日志配置的基本方式.我們平時在系統(tǒng)開發(fā)時,開發(fā)環(huán)境與生產(chǎn)環(huán)境的日志配置會不一樣;那今天老顧就跟大家介紹一下如何實現(xiàn)多環(huán)境配置,需要的朋友可以參考下2021-06-06SpringBoot使用PropertiesLauncher加載外部jar包
這篇文章主要介紹了SpringBoot使用PropertiesLauncher加載外部jar包,本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07