目錄
線程安全的概念
線程不安全示例
線程不安全的原因?
? ? 多個(gè)線程修改了同一個(gè)變量
? ? 線程是搶占式執(zhí)行的
? ? 原子性
? ? 內(nèi)存可見(jiàn)性
? ? 有序性
線程不安全解決辦法
?synchronized 關(guān)鍵字-監(jiān)視器鎖monitor lock
? ? synchronized 的特性
????????互斥
????????刷新內(nèi)存
????????可重入
? ? synchronized 使用示例
? ? Java 標(biāo)準(zhǔn)庫(kù)中的線程安全類
volatile 關(guān)鍵字
? ? volatile 能保證內(nèi)存可見(jiàn)性
? ? volatile 不保證原子性
? ? synchronized 也能保證內(nèi)存可見(jiàn)性
線程安全的概念
線程不安全示例
public class Insecurity {
// 定義自增操作的對(duì)象
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
// 定義兩個(gè)線程,分別自增5萬(wàn)次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increment();
}
});
// 啟動(dòng)線程
t1.start();
t2.start();
// 等待自增完成
t1.join();
t2.join();
// 打印結(jié)果
System.out.println("count = " + counter.count);
}
}
class Counter {
public int count = 0;
// 自增方法
public void increment () {
count++;
}
}
線程不安全的原因?
? ? 多個(gè)線程修改了同一個(gè)變量
? ? 線程是搶占式執(zhí)行的
多個(gè)線程在CPU上調(diào)度是隨機(jī)的,順序是不可預(yù)知的。
? ? 原子性
要么都執(zhí)行,要么都不執(zhí)行。
- 從內(nèi)存把數(shù)據(jù)讀到 CPU
- 進(jìn)行數(shù)據(jù)更新
- 把數(shù)據(jù)寫回到 CPU
? ? 內(nèi)存可見(jiàn)性
?可見(jiàn)性指, 一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到.
- 線程之間的共享變量存在 主內(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)存.
? ? 有序性
有序性是指編譯過(guò)程中,JVM調(diào)用本地接口,CPU執(zhí)行指令過(guò)程中,指令的有序性。
指令在特殊情況下會(huì)打亂順序,并不是按程序員的預(yù)期去執(zhí)行的。
編譯器對(duì)于指令重排序的前提是 " 保持邏輯不發(fā)生變化 ". 這一點(diǎn)在單線程環(huán)境下比較容易判斷 , 但是在多線程環(huán)境下就沒(méi)那么容易了, 多線程的代碼執(zhí)行復(fù)雜程度更高 , 編譯器很難在編譯階段對(duì)代碼的執(zhí)行效果進(jìn)行預(yù)測(cè), 因此激進(jìn)的重排序很容易導(dǎo)致優(yōu)化后的邏輯和之前不等價(jià) .
線程不安全解決辦法
對(duì)于多線程修改同一個(gè)變量,在真實(shí)業(yè)務(wù)中都是修改同一個(gè)變量,無(wú)法避免。
對(duì)于線程是搶占式執(zhí)行的,CPU調(diào)度是隨機(jī)的,這里CPU是硬件層面,沒(méi)辦法處理。
剩下就是解決其他三個(gè)原因:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-461668.html
public class Main {
// 定義自增操作的對(duì)象
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
// 定義兩個(gè)線程,分別自增5萬(wàn)次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increment();
}
});
// 啟動(dòng)線程
t1.start();
t2.start();
// 等待自增完成
t1.join();
t2.join();
// 打印結(jié)果
System.out.println("count = " + counter.count);
}
}
class Counter {
public volatile int count = 0;
// 自增方法
public synchronized void increment () {
count++;
}
}
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-461668.html
?synchronized 關(guān)鍵字-監(jiān)視器鎖monitor lock
? ? synchronized 的特性
????????互斥
- 進(jìn)入 synchronized 修飾的代碼塊, 相當(dāng)于 加鎖
- 退出 synchronized 修飾的代碼塊, 相當(dāng)于 解鎖
可以粗略理解成 , 每個(gè)對(duì)象在內(nèi)存中存儲(chǔ)的時(shí)候 , 都存有一塊內(nèi)存表示當(dāng)前的 " 鎖定 " 狀態(tài) ( 類似于廁所的 " 有人 / 無(wú)人 ").如果當(dāng)前是 " 無(wú)人 " 狀態(tài) , 那么就可以使用 , 使用時(shí)需要設(shè)為 " 有人 " 狀態(tài) .如果當(dāng)前是 " 有人 " 狀態(tài) , 那么其他人無(wú)法使用 , 只能排隊(duì)
理解 " 阻塞等待 ".針對(duì)每一把鎖 , 操作系統(tǒng)內(nèi)部都維護(hù)了一個(gè)等待隊(duì)列 . 當(dāng)這個(gè)鎖被某個(gè)線程占有的時(shí)候 , 其他線程嘗試進(jìn)行加鎖, 就加不上了 , 就會(huì)阻塞等待 , 一直等到之前的線程解鎖之后 , 由操作系統(tǒng)喚醒一個(gè)新的線程, 再來(lái)獲取到這個(gè)鎖 .注意 :
- 上一個(gè)線程解鎖之后, 下一個(gè)線程并不是立即就能獲取到鎖. 而是要靠操作系統(tǒng)來(lái) "喚醒". 這也就是操作系統(tǒng)線程調(diào)度的一部分工作.
- 假設(shè)有 A B C 三個(gè)線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時(shí) B和 C 都在阻塞隊(duì)列中排隊(duì)等待. 但是當(dāng) A 釋放鎖之后, 雖然 B 比 C 先來(lái)的, 但是 B 不一定就能獲取到鎖, 而是和 C 重新競(jìng)爭(zhēng), 并不遵守先來(lái)后到的規(guī)則.
????????刷新內(nèi)存
- 獲得互斥鎖
- 從主內(nèi)存拷貝變量的最新副本到工作的內(nèi)存
- 執(zhí)行代碼
- 將更改后的共享變量的值刷新到主內(nèi)存
- 釋放互斥鎖
????????可重入
理解 " 把自己鎖死 "一個(gè)線程沒(méi)有釋放鎖 , 然后又嘗試再次加鎖 .
- increase 和 increase2 兩個(gè)方法都加了 synchronized, 此處的 synchronized 都是針對(duì) this 當(dāng)前對(duì)象加鎖的.
- 在調(diào)用 increase2 的時(shí)候, 先加了一次鎖, 執(zhí)行到 increase 的時(shí)候, 又加了一次鎖. (上個(gè)鎖還沒(méi)釋放, 相當(dāng)于連續(xù)加兩次鎖)
static class Counter {
? ?public int count = 0;
? ?synchronized void increase() {
? ? ? ?count++;
? }
? ?synchronized void increase2() {
? ? ? ?increase();
? }
}
- 如果某個(gè)線程加鎖的時(shí)候, 發(fā)現(xiàn)鎖已經(jīng)被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續(xù)獲取到鎖, 并讓計(jì)數(shù)器自增.
- 解鎖的時(shí)候計(jì)數(shù)器遞減為 0 的時(shí)候, 才真正釋放鎖. (才能被別的線程獲取到)
? ? synchronized 使用示例
public class SynchronizedDemo {
? ?public synchronized void methond() {
? }
}
public class SynchronizedDemo {
? ?public synchronized static void method() {
? }
}
public class SynchronizedDemo {
? ?public void method() {
? ? ? ?synchronized (this) {
? ? ? ? ? ?
? ? ? }
? }
}
public class SynchronizedDemo {
? ?public void method() {
? ? ? ?synchronized (SynchronizedDemo.class) {
? ? ? }
? }
}
? ? Java 標(biāo)準(zhǔn)庫(kù)中的線程安全類
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
- Vector (不推薦使用)
- HashTable (不推薦使用)
- ConcurrentHashMap
- StringBuffer
StringBuffer 的核心方法都帶有 synchronized
- String
volatile 關(guān)鍵字
? ? volatile 能保證內(nèi)存可見(jiàn)性
- 改變線程工作內(nèi)存中volatile變量副本的值
- 將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存
- 從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
- 從工作內(nèi)存中讀取volatile變量的副本
前面我們討論內(nèi)存可見(jiàn)性時(shí)說(shuō)了 , 直接訪問(wèn)工作內(nèi)存 ( 實(shí)際是 CPU 的寄存器或者 CPU 的緩存 ), 速度非常快, 但是可能出現(xiàn)數(shù)據(jù)不一致的情況 .加上 volatile , 強(qiáng)制讀寫內(nèi)存 . 速度是慢了 , 但是數(shù)據(jù)變的更準(zhǔn)確了 .
- 創(chuàng)建兩個(gè)線程 t1 和 t2
- t1 中包含一個(gè)循環(huán), 這個(gè)循環(huán)以 flag == 0 為循環(huán)條件.
- t2 中從鍵盤讀入一個(gè)整數(shù), 并把這個(gè)整數(shù)賦值給 flag.
- 預(yù)期當(dāng)用戶輸入非 0 的值的時(shí)候, t1 線程結(jié)束.
static class Counter {
? ?public int flag = 0;
}
public static void main(String[] args) {
? ?Counter counter = new Counter();
? ?Thread t1 = new Thread(() -> {
? ? ? ?while (counter.flag == 0) {
? ? ? ? ? ?// do nothing
? ? ? }
? ? ? ?System.out.println("循環(huán)結(jié)束!");
? });
? ?Thread t2 = new Thread(() -> {
? ? ? ?Scanner scanner = new Scanner(System.in);
? ? ? ?System.out.println("輸入一個(gè)整數(shù):");
? ? ? ?counter.flag = scanner.nextInt();
? });
? ?t1.start();
? ?t2.start();
}
// 執(zhí)行效果
// 當(dāng)用戶輸入非0值時(shí), t1 線程循環(huán)不會(huì)結(jié)束. (這顯然是一個(gè) bug)
static class Counter {
? ?public volatile int flag = 0;
}
// 執(zhí)行效果
// 當(dāng)用戶輸入非0值時(shí), t1 線程循環(huán)能夠立即結(jié)束.
? ? volatile 不保證原子性
- 給 increase 方法去掉 synchronized
- 給 count 加上 volatile 關(guān)鍵字.
static class Counter {
? ?volatile 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);
}
? ? synchronized 也能保證內(nèi)存可見(jiàn)性
- 去掉 flag 的 volatile
- 給 t1 的循環(huán)內(nèi)部加上 synchronized, 并借助 counter 對(duì)象加鎖.
static class Counter {
? ?public int flag = 0;
}
public static void main(String[] args) {
? ?Counter counter = new Counter();
? ?Thread t1 = new Thread(() -> {
? ? ? ?while (true) {
? ? ? ? ? ?synchronized (counter) {
? ? ? ? ? ? ? ?if (counter.flag != 0) {
? ? ? ? ? ? ? ? ? ?break;
? ? ? ? ? ? ? }
? ? ? ? ? }
? ? ? ? ? ?// do nothing
? ? ? }
? ? ? ?System.out.println("循環(huán)結(jié)束!");
? });
? ?Thread t2 = new Thread(() -> {
? ? ? ?Scanner scanner = new Scanner(System.in);
? ? ? ?System.out.println("輸入一個(gè)整數(shù):");
? ? ? ?counter.flag = scanner.nextInt();
? });
? ?t1.start();
? ?t2.start();
}
到了這里,關(guān)于【多線程】線程安全問(wèn)題原因與解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!