淺談java線程狀態(tài)與線程安全解析
1.線程的幾種狀態(tài)
1.1 線程的狀態(tài)
以下就是我們線程所有的狀態(tài)和意義:
| NEW | 已經(jīng)創(chuàng)建Thread但未創(chuàng)建線程 |
| RUNNABLE | 可工作的. 又可以分成正在工作中和即將開(kāi)始工作 |
| BLOCKED | 等待鎖(阻塞狀態(tài)) |
| WAITING | 調(diào)用wati方法(阻塞狀態(tài)) |
| TIMED_WAITING | 調(diào)用sleep方法(阻塞狀態(tài)) |
| TERMINATED | 系統(tǒng)線程執(zhí)行完畢已銷(xiāo)毀,但Thread還存在 |
注意:
BLOCKED 表示等待獲取鎖, WAITING 和 TIMED_WAITING 表示等待其他線程發(fā)來(lái)通知.
TIMED_WAITING 線程在等待喚醒,但設(shè)置了時(shí)限; WAITING 線程在無(wú)限等待喚醒
1.2 線程狀態(tài)的轉(zhuǎn)移
各線程之間的轉(zhuǎn)移關(guān)系可以簡(jiǎn)化成下圖:

關(guān)于yield方法:
在多線程中我們存在一個(gè)yield方法可以讓線程在就緒隊(duì)列中重新”排隊(duì)“,不改變線程狀態(tài)。相當(dāng)于你去幫別人排隊(duì),但是輪到你了那個(gè)人還沒(méi)回來(lái),你就就讓原本排在你后面的人換到你的位置上,但你仍然處于排隊(duì)狀態(tài)。這種”大公無(wú)私“的行為可以類(lè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("張三");
// 先注釋掉, 再放開(kāi)
//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 的時(shí)候, 張三李四大概五五開(kāi)
2. 使用 yield 時(shí), 張三的數(shù)量遠(yuǎn)遠(yuǎn)少于李四
結(jié)論: yield 不改變線程的狀態(tài), 但是會(huì)重新去排隊(duì).
2.有關(guān)線程安全問(wèn)題
2.1 一個(gè)簡(jiǎn)單的例子
// 創(chuàng)建兩個(gè)線程, 讓這倆線程同時(shí)并發(fā)的對(duì)一個(gè)變量, 自增 5w 次. 最終預(yù)期能夠一共自增 10w 次.
class Counter {
// 用來(lái)保存計(jì)數(shù)的變量
public int count;
public void increase() {
count++;
}
}
public class Demo {
// 這個(gè)實(shí)例用來(lái)進(jìn)行累加.
// 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);
}
}大家先看到以上的代碼,意思很簡(jiǎn)單,用兩個(gè)線程對(duì)同一個(gè)變量進(jìn)行自增操作,運(yùn)行的結(jié)果如下

看起來(lái)不太對(duì),我們?cè)僭囈淮?/p>

結(jié)果有了變化,但仍然不是我們想要的結(jié)果,是什么導(dǎo)致了5w+5w<10w呢?其中一個(gè)原因就是線程的隨機(jī)調(diào)度和改操作不具有原子性。 這些概念我們下面會(huì)詳細(xì)講,這里我們先簡(jiǎn)單了解一下。
首先我們的自增操作在cpu內(nèi)其實(shí)分為三步:
1.LOAD:cpu從內(nèi)存中讀取數(shù)據(jù)到寄存器
2.ADD:在寄存器內(nèi)實(shí)現(xiàn)自增
3.SAVE:將寄存器的數(shù)據(jù)寫(xiě)回內(nèi)存中
而我們已經(jīng)知道cpu對(duì)于線程調(diào)度我們可以理解為是隨機(jī)的,所以會(huì)有很多種可能,比如下圖

其中縱軸代表運(yùn)行時(shí)間,這里我們可以看到兩個(gè)線程相當(dāng)于互不影響,線程1完成自增操作后又將數(shù)據(jù)寫(xiě)回內(nèi)存由線程2再去操作,這種情況下是沒(méi)有問(wèn)題的。但是也可能是下面的一種情況

此時(shí)線程1還沒(méi)有將自增后的數(shù)據(jù)寫(xiě)回內(nèi)存而線程2就已經(jīng)將要修改的數(shù)據(jù)讀入了寄存器,此時(shí)相當(dāng)于線程2讀到了那個(gè)還未自增的數(shù)據(jù),相當(dāng)于兩個(gè)線程對(duì)同一個(gè)數(shù)進(jìn)行了自增,所以此時(shí)相當(dāng)于只自增了一次。其實(shí)情況還有很多,這里我們僅舉例比較經(jīng)典的例子。所以這也能夠解釋為什么結(jié)果大于5w而小于10w了。
2.2 造成線程不安全的原因
2.2.1 操作系統(tǒng)的隨機(jī)調(diào)度/搶占式運(yùn)行
這種是操作系統(tǒng)內(nèi)核就已經(jīng)決定的,我們無(wú)能為力。類(lèi)似于我們上一個(gè)例子,就是因?yàn)榫€程的隨機(jī)調(diào)度和操作不具有原子性造成的。
2.2.2 操作不具有原子性
什么是原子性
我們把一段代碼想象成一個(gè)房間,每個(gè)線程就是要進(jìn)入這個(gè)房間的人。如果沒(méi)有任何機(jī)制保證,A進(jìn)入 房間之后,還沒(méi)有出來(lái);B 是不是也可以進(jìn)入房間,打斷 A 在房間里的隱私。這個(gè)就是不具備原子性 的。轉(zhuǎn)換成代碼我們可以理解成只具有一條指令的操作。
當(dāng)然這個(gè)問(wèn)題我們可以通過(guò)加鎖操作解決(以后會(huì)提到)。
一條 java 語(yǔ)句不一定是原子的,也不一定只是一條指令,比如我們上面提到的自增操作。
不保證原子性會(huì)給多線程帶來(lái)什么問(wèn)題
如果一個(gè)線程正在對(duì)一個(gè)變量操作,中途其他線程插入進(jìn)來(lái)了,如果這個(gè)操作被打斷了,結(jié)果就可能是錯(cuò)誤的。 這點(diǎn)也和線程的搶占式調(diào)度密切相關(guān). 如果線程不是 "搶占" 的, 就算沒(méi)有原子性, 也問(wèn)題不大.
2.2.3 多個(gè)線程修改同一個(gè)變量
1.一個(gè)線程修改變量沒(méi)事
2.多個(gè)線程同時(shí)讀一個(gè)變量也沒(méi)事
3.多個(gè)線程同時(shí)修改不同變量也沒(méi)有問(wèn)題
唯獨(dú)需要注意多個(gè)線程修改同一個(gè)變量,如果不加以處理可能會(huì)造成我們之前講到的例子的問(wèn)題
2.2.4 內(nèi)存可見(jiàn)性問(wèn)題
jvm中規(guī)定了java的內(nèi)存模型

線程之間的共享變量存在 主內(nèi)存 (Main Memory).
每一個(gè)線程都有自己的 "工作內(nèi)存" (Working Memory) .
當(dāng)線程要讀取一個(gè)共享變量的時(shí)候, 會(huì)先把變量從主內(nèi)存拷貝到工作內(nèi)存, 再?gòu)墓ぷ鲀?nèi)存讀取數(shù)據(jù).
當(dāng)線程要修改一個(gè)共享變量的時(shí)候, 也會(huì)先修改工作內(nèi)存中的副本, 再同步回主內(nèi)存.
正是因?yàn)檫@種機(jī)制,所以可能會(huì)出現(xiàn)下面的問(wèn)題:
由于每個(gè)線程有自己的工作內(nèi)存, 這些工作內(nèi)存中的內(nèi)容相當(dāng)于同一個(gè)共享變量的 "副本". 此時(shí)修改線程 1 的工作內(nèi)存中的值, 線程2 的工作內(nèi)存不一定會(huì)及時(shí)變化.通俗的講就是 線程1針對(duì)工作內(nèi)容修改了數(shù)據(jù),而線程2此時(shí)并不一定能夠及時(shí)同步修改的數(shù)據(jù),所以可能會(huì)引發(fā)各種問(wèn)題。
2.2.5 指令重排序
所謂指令重排序是指jvm針對(duì)我們的代碼,可能會(huì)在保證邏輯不變的情況下去調(diào)整指令執(zhí)行的順序以達(dá)到運(yùn)行效率更高的效果。這種情況在單線程的情況下可以很好實(shí)現(xiàn),而在多線程的情況下就可能會(huì)出現(xiàn)bug,導(dǎo)致程序邏輯改變。比如對(duì)于下面這行代碼:
Test t=new Test();
它其實(shí)總共有三個(gè)步驟:
1.創(chuàng)建內(nèi)存空間
2.往這個(gè)內(nèi)存空間構(gòu)造一個(gè)對(duì)象
3.將這個(gè)內(nèi)存引用賦給t
在單線程的情況下2,3互換并不會(huì)有上面影響,但假如在多線程情況下我們按1,3,2來(lái)執(zhí)行,當(dāng)執(zhí)行到3時(shí)t為非null,此時(shí)線程2讀取t,但是卻發(fā)現(xiàn)是一個(gè)無(wú)效對(duì)象。
到此這篇關(guān)于淺談java線程狀態(tài)與線程安全解析的文章就介紹到這了,更多相關(guān)java線程狀態(tài)與線程安全內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaWeb項(xiàng)目中JSP訪問(wèn)的問(wèn)題解決
JSP文件一般有兩個(gè)存放位置,本文主要介紹了JavaWeb項(xiàng)目中JSP訪問(wèn)的問(wèn)題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
SpringBoot3集成Thymeleaf的過(guò)程詳解
在現(xiàn)代的Web開(kāi)發(fā)中,構(gòu)建靈活、動(dòng)態(tài)的用戶界面是至關(guān)重要的,Spring Boot和Thymeleaf的結(jié)合為開(kāi)發(fā)者提供了一種簡(jiǎn)單而強(qiáng)大的方式來(lái)創(chuàng)建動(dòng)態(tài)的Web應(yīng)用,本文將介紹如何在Spring Boot項(xiàng)目中集成Thymeleaf,并展示一些基本的使用方法,需要的朋友可以參考下2024-01-01
詳解java集成支付寶支付接口(JSP+支付寶20160912)
本篇文章主要介紹了java集成支付寶支付接口,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
Java技巧函數(shù)方法實(shí)現(xiàn)二維數(shù)組遍歷
這篇文章主要介紹了Java技巧函數(shù)方法實(shí)現(xiàn)二維數(shù)組遍歷,二維數(shù)組遍歷,每個(gè)元素判斷下是否為偶數(shù),相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-08-08
關(guān)于Spring源碼深度解析(AOP功能源碼解析)
這篇文章主要介紹了關(guān)于Spring源碼深度解析(AOP功能源碼解析),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Java中的CopyOnWriteArrayList原理詳解
這篇文章主要介紹了Java中的CopyOnWriteArrayList原理詳解,如源碼所示,CopyOnWriteArrayList和ArrayList一樣,都在內(nèi)部維護(hù)了一個(gè)數(shù)組,操作CopyOnWriteArrayList其實(shí)就是在操作內(nèi)部的數(shù)組,需要的朋友可以參考下2023-12-12

