Java多線程并發(fā)基礎(chǔ)實(shí)戰(zhàn)舉例
1、進(jìn)程與線程
1.1 進(jìn)程
進(jìn)程(Process)是計(jì)算機(jī)中正在運(yùn)行的程序。程序是一種靜態(tài)的概念,而進(jìn)程是程序再執(zhí)行過(guò)程中創(chuàng)建的動(dòng)態(tài)實(shí)體。每個(gè)進(jìn)程都有自己的內(nèi)存空間、代碼、數(shù)據(jù)和資源,他也是操作系統(tǒng)進(jìn)行任務(wù)調(diào)度和資源分配的基本單位。
- 進(jìn)程是正在運(yùn)行一個(gè)軟件或者腳本。這樣理解更為簡(jiǎn)單一點(diǎn)。
多個(gè)進(jìn)程可以同事運(yùn)行在計(jì)算機(jī)上,彼此獨(dú)立并且互不干擾。操作系統(tǒng)通過(guò)進(jìn)程管理來(lái)控制和監(jiān)視進(jìn)程的創(chuàng)建、運(yùn)行、暫停和終止等操作。進(jìn)程還可以通過(guò)進(jìn)程間通信機(jī)制來(lái)實(shí)現(xiàn)進(jìn)程之間的數(shù)據(jù)交換和同步。
打開(kāi)windows的任務(wù)管理其,運(yùn)行的程序就是一個(gè)個(gè)進(jìn)程。

1.2 線程
線程(Thread)是程序執(zhí)行的最小單位,它是進(jìn)程的一部分。一個(gè)進(jìn)程可以包含一個(gè)或多個(gè)線程。同一個(gè)進(jìn)程中的多個(gè)線程共享同一塊內(nèi)存空間和其他資源,他們可以同時(shí)執(zhí)行不同的任務(wù)。每個(gè)線程都有自己的程序計(jì)數(shù)器、棧和一組寄存器,這使得線程能夠獨(dú)立的執(zhí)行代碼。
線程的特點(diǎn)
- 輕量級(jí):相對(duì)于進(jìn)程來(lái)說(shuō),線程的創(chuàng)建和上下文切換開(kāi)銷(xiāo)較小;
- 共享資源:同一進(jìn)程中的線程可以共享內(nèi)存和其他資源,因此可以更方便地進(jìn)行數(shù)據(jù)共享和通信;
- 并發(fā)執(zhí)行:多個(gè)線程可以同時(shí)執(zhí)行,提高了程序的并發(fā)性和效率。
線程的使用可以提升程序的性能和響應(yīng)性,特別是再多核處理器上可以實(shí)現(xiàn)并行計(jì)算。但多線程編程也需要注意線程間的同步和共享數(shù)據(jù)的安全性問(wèn)題,以避免出現(xiàn)竟態(tài)條件和數(shù)據(jù)不一致的情況。
- 線程的時(shí)間效率和空間效率都比進(jìn)程要高。
2、并行與并發(fā)
- 并行
- 并行(Parallel)是指同時(shí)進(jìn)行多個(gè)任務(wù),人物之間可以同時(shí)執(zhí)行,彼此獨(dú)立。例如,在多核處理器上,可以同時(shí)執(zhí)行多個(gè)線程或進(jìn)程,這就是并行。
- 并發(fā)
- 并發(fā)(Concurrent)則是指同時(shí)交替進(jìn)行多個(gè)任務(wù),人物之間可能有依賴(lài)關(guān)系或競(jìng)爭(zhēng)條件。例如,在單核處理器上,通過(guò)時(shí)間片輪轉(zhuǎn)的方式,多個(gè)線程或進(jìn)程通過(guò)快速切換來(lái)執(zhí)行,看起來(lái)是同時(shí)進(jìn)行的,但實(shí)際上是交替執(zhí)行的,并發(fā)中的任務(wù)可能需要依賴(lài)共享資源或競(jìng)爭(zhēng)臨界區(qū)資源。
并行是多個(gè)任務(wù)同時(shí)進(jìn)行,而并發(fā)是多個(gè)任務(wù)交替進(jìn)行,并且可能存在資源競(jìng)爭(zhēng)和依賴(lài)。
并行和并發(fā)之間的區(qū)別在于任務(wù)是否可以 同時(shí)執(zhí)行以及是否需要競(jìng)爭(zhēng)共享資源。并行通常需要硬件支持,如多核處理器,能同時(shí)執(zhí)行多個(gè)任務(wù)。而并發(fā)則可以在單核處理器上通過(guò)時(shí)間片輪轉(zhuǎn)等技術(shù)實(shí)現(xiàn)。
在實(shí)際應(yīng)用中,可以用并行提高計(jì)算性能和執(zhí)行速度,可適用于多線程編程或分布式計(jì)算;而并發(fā)則可以提高資源利用率和系統(tǒng)吞吐量,可適用于任務(wù)調(diào)度和資源管理。
3、多線程的必要性
我們先來(lái)看個(gè)JVM(Java內(nèi)存模型)簡(jiǎn)單模型
Java內(nèi)存模型

- CPU、內(nèi)存(主存)、I/O(讀寫(xiě)),這三者的處理速度有著極大的差異。為了平衡這三者的速度差異,需要在計(jì)算機(jī)體系結(jié)構(gòu)、操作系統(tǒng)、編譯程序上進(jìn)行優(yōu)化。
3.1 CPU緩存優(yōu)化
- CPU(中央處理器)是計(jì)算機(jī)和核心部件,負(fù)責(zé)處理大部分的計(jì)算任務(wù)。然而,CPU處理數(shù)據(jù)的速度遠(yuǎn)高于內(nèi)存讀取或?qū)懭霐?shù)據(jù)的速度,這種速度上的差異就會(huì)導(dǎo)致CPU在等待數(shù)據(jù)時(shí)可能無(wú)事可做,從而浪費(fèi)其處理能力;
- 為了解決這個(gè)問(wèn)題,專(zhuān)門(mén)在CPU和內(nèi)存之間增加了緩存(Cache)作為一個(gè)數(shù)據(jù)的臨時(shí)存儲(chǔ)區(qū),用于存放CPU預(yù)期會(huì)用到的數(shù)據(jù)。因?yàn)榫彺媸俏挥贑PU和內(nèi)存之間的臨時(shí)存儲(chǔ)區(qū),它的存取速度比內(nèi)存要快得多,所以能夠有效地解決CPU和內(nèi)存速度上的差異;
- 當(dāng)CPU需要讀取或?qū)懭霐?shù)據(jù)時(shí),它會(huì)首先查找緩存中是否有這些數(shù)據(jù)。如果有(這成為“緩存命中”),CPU就可以直接從緩存讀取或?qū)懭霐?shù)據(jù),從而避免了等待內(nèi)存的消耗時(shí)間。如果沒(méi)有(這成為“緩存未命中”),CPU就需要從內(nèi)存中讀取數(shù)據(jù),并同時(shí)將這些數(shù)據(jù)寫(xiě)入緩存,以供后續(xù)使用;
- 通過(guò)這種方式,緩存能夠有效地利用CPU的告訴處理能力,提高計(jì)算機(jī)的整體性能。
CPU增強(qiáng)緩存,會(huì)導(dǎo)致可見(jiàn)性問(wèn)題。
3.2 操作系統(tǒng)優(yōu)化
操作系統(tǒng)增加了進(jìn)程和線程的概念來(lái)實(shí)現(xiàn)分時(shí)復(fù)用CPU資源,進(jìn)而均衡CPU和I/O設(shè)備的速度差異。
- 進(jìn)程是指計(jì)算機(jī)中正在運(yùn)行的程序的實(shí)例。操作系統(tǒng)通過(guò)為每個(gè)進(jìn)程分配一段獨(dú)立的內(nèi)存空間和一組資源(如CPU時(shí)間片、文件掃描符等)來(lái)管理并控制進(jìn)程的運(yùn)行。通過(guò)輪流分配CPU時(shí)間片,操作系統(tǒng)可以讓多個(gè)進(jìn)程交替運(yùn)行,從而實(shí)現(xiàn)leCPU的分時(shí)復(fù)用;
- 線程是指進(jìn)程中的一個(gè)獨(dú)立執(zhí)行單元。一個(gè)進(jìn)程可以有多個(gè)線程,他們共享該進(jìn)程的資源和狀態(tài)。每個(gè)線程有自己的棧空間和程序計(jì)數(shù)器,但它們共享同一進(jìn)程的內(nèi)存空間、文件掃描符等。操作系統(tǒng)可以 通過(guò)調(diào)度算法在不通的線程之間切換,從而實(shí)現(xiàn)多個(gè)下昵稱(chēng)在單個(gè)進(jìn)程中并發(fā)執(zhí)行;
- 通過(guò)引入進(jìn)程和線程的概念,操作系統(tǒng)可以將CPU時(shí)間片分配給不同的進(jìn)程,實(shí)現(xiàn)進(jìn)程之間的輪流執(zhí)行并提高CPU的利用率。同時(shí),操作系統(tǒng)可以通過(guò)線程的并發(fā)執(zhí)行來(lái)隱藏I/O設(shè)備操作的等待時(shí)間,提高系統(tǒng)的響應(yīng)速度;
- 總而言之,操作系統(tǒng)通過(guò)增加進(jìn)程和線程的概念,實(shí)現(xiàn)了分時(shí)復(fù)用CPU資源,使得多個(gè)進(jìn)程和現(xiàn)車(chē)給你可以并發(fā)執(zhí)行,從而在CPU和I/O設(shè)備的速度差異中實(shí)現(xiàn)均衡。
操作系統(tǒng)增加了進(jìn)程、線程,會(huì)導(dǎo)致原子性問(wèn)題。
3.3 編譯程序優(yōu)化
編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。
編譯程序優(yōu)化指令執(zhí)行次序是指在編譯過(guò)程中,編譯器根據(jù)各種優(yōu)化策略和規(guī)則,自動(dòng)調(diào)整和重排程序中指令的順序,以提高程序運(yùn)行的效率和性能。
編譯器進(jìn)行指令優(yōu)化的主要目的是為了解決以下問(wèn)題:
- 硬件資源利用:通過(guò)優(yōu)化調(diào)整指令次序,可以更好地利用CPU的并行性,提高CPU利用率,減少資源浪費(fèi)。例如,通過(guò)指令調(diào)度可以讓CPU同時(shí) 執(zhí)行多條物館的指令,從而提高指令的并行等級(jí);
- 減少等待時(shí)間:某些指令執(zhí)行需要等待前面指令的結(jié)果,這會(huì)導(dǎo)致CPU空閑等待。通過(guò)優(yōu)化指令順序,可以盡可能地避免這種依賴(lài),減少CPU空閑時(shí)間,提高運(yùn)行效率;
- 克服性能瓶頸:例如,優(yōu)化內(nèi)存訪問(wèn)指令地順序,可以減少緩沖區(qū)溢出或者下溢地可能性,避免因?yàn)閮?nèi)存訪問(wèn)導(dǎo)致地性能瓶頸;
- 管線浪費(fèi):現(xiàn)代CPU通常將指令執(zhí)行過(guò)程分解成多個(gè)解讀按,并行執(zhí)行以提高性能,這就是所謂地管線技術(shù)。如果指令地執(zhí)行順序不能很好地匹配CPU管線,就會(huì)導(dǎo)致管線解讀按地閑置,造成性能下降。
通過(guò)這些優(yōu)化,編譯器可以幫助程序員在不需要手動(dòng)干預(yù)地情況下自動(dòng)提高程序地運(yùn)行效率和性能,使得程序在各種硬件平臺(tái)上都能獲得更好的運(yùn)行效果。
編譯程序優(yōu)化指令執(zhí)行次序,會(huì)導(dǎo)致有序性問(wèn)題。
4、線程安全問(wèn)題
如果多個(gè)線程對(duì)同一個(gè)共享數(shù)據(jù)進(jìn)行訪問(wèn)而不采取同步操作地的話,那么操作的結(jié)果是不一致的。
以下代碼演示了1000個(gè)線程同時(shí)對(duì)cnt執(zhí)行自增操作,操作結(jié)束之后它的值有可能?chē)[宇1000。
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
// 輸出結(jié)果:992為什么多線程會(huì)導(dǎo)致結(jié)果嘯宇1000呢?示什么導(dǎo)致 線程安全問(wèn)題?
- 導(dǎo)致線程安全問(wèn)題的主要原因有三個(gè)方面:CPU緩存導(dǎo)致的可見(jiàn)性問(wèn)題、進(jìn)程或線程導(dǎo)致的原子性問(wèn)題、編譯器優(yōu)化導(dǎo)致的有序性問(wèn)題。
5、線程安全導(dǎo)致原因
多線程操作共享變量時(shí),才會(huì)引起線程安全問(wèn)題。所以下面操作的變量默認(rèn)都是共享變量。
5.1 可見(jiàn)性
可見(jiàn)性:一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到。
CPU緩存會(huì)導(dǎo)致可見(jiàn)性規(guī)則打破,案例:
//線程1執(zhí)行的代碼 int i = 0; i = 10; //線程2執(zhí)行的代碼 j = i;
- 假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2.由上面的分析可知,當(dāng)線程1執(zhí)行i=10這句時(shí),會(huì)先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么CPU1的告訴緩存檔中i的值變?yōu)?0了,卻沒(méi)有立即寫(xiě)入到主存檔中;
- 此時(shí)線程2執(zhí)行j=i,它會(huì)先去主存讀取i的值并加載到CPU2的緩存檔中,注意此時(shí)內(nèi)存檔中i的值還是0,那么就會(huì)使得j的值為0,而不是10.
線程1對(duì)變量i修改了之后,線程2沒(méi)有立即看到線程1修改的值。所以CPU緩存會(huì)導(dǎo)致可見(jiàn)性問(wèn)題。
5.2 原子性
原子性:即一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
分時(shí)復(fù)用會(huì)引起原子性打破,案例:
int i = 1; // 線程1執(zhí)行 i += 1; // 線程2執(zhí)行 i += 1;
這里需要注意的是:i += 1需要三條CPU指令。
- 將變量i從內(nèi)存讀取到CPU寄存器;
- 在CPU寄存器中執(zhí)行i + 1 操作;
- 將最后的結(jié)果 i 寫(xiě)入內(nèi)存(緩存機(jī)制導(dǎo)致可能寫(xiě)入的是 CPU 緩存而不是內(nèi)存)。
由于CPU分時(shí)復(fù)用(線程切換)的存在,線程1執(zhí)行了第一條指令后,就切換到線程2執(zhí)行,例如線程2執(zhí)行了這三條指令后,再切換會(huì)線程1執(zhí)行后續(xù)兩條指令,將造成最后寫(xiě)道內(nèi)存中的 i 值是2而不是3。所以 CPU分時(shí)復(fù)用會(huì)導(dǎo)致原子性問(wèn)題。
5.3 有序性
有序性:即程序執(zhí)行的順序按照代碼的縣厚順序執(zhí)行。
重排序優(yōu)化會(huì)打破程序有序性,案例:
int i = 0; boolean flag = false; i = 1; //語(yǔ)句1 flag = true; //語(yǔ)句2
雖然上述代碼語(yǔ)句2在語(yǔ)句1之后,但是多線程執(zhí)行順序不一定按語(yǔ)句1、語(yǔ)句2這個(gè)順序執(zhí)行。重排序是指計(jì)算機(jī)系統(tǒng)(如處理器、編譯器等)在執(zhí)行程序時(shí),按照某種規(guī)則重新調(diào)整指令或操作的順序,以提高程序性能或滿(mǎn)足其他需求。
重排序可以分為三種類(lèi)型:編譯器重排序、處理器重排序和內(nèi)存系統(tǒng)重排序。
- 編譯器重排序:在編譯階段,編譯器可能會(huì)對(duì)源代碼中的指令重新排序,以?xún)?yōu)化代碼的執(zhí)行效率。編譯器重排序不會(huì)改變程序的語(yǔ)義,即保證最終的執(zhí)行結(jié)果與源代碼的順序一致。編譯器重排序可以通過(guò)指令級(jí)并行、循環(huán)展開(kāi)、常量傳播等技術(shù)來(lái)實(shí)現(xiàn);
- 處理器重排序:在處理器執(zhí)行指令時(shí),由于處理器采用了流水線技術(shù),它可以對(duì)指令進(jìn)行重排序,以盡可能地利用處理器資源。處理器重排序可能包括指令級(jí)重排序(亂序執(zhí)行)和內(nèi)存訪問(wèn)重排序。指令級(jí)重排序是指處理器可以改變指令地執(zhí)行順序,以提高指令地并行度和執(zhí)行效率。內(nèi)存訪問(wèn)重排序是指處理器可以改變對(duì)內(nèi)存地讀寫(xiě)操作地順序,以充分利用內(nèi)存系統(tǒng)地各級(jí)緩存;
- 內(nèi)存系統(tǒng)重排序:由于現(xiàn)代計(jì)算機(jī)系統(tǒng)中存在多級(jí)緩存、總線和內(nèi)存等層次結(jié)構(gòu),因此對(duì)于內(nèi)存的讀寫(xiě)操作也可能存在重排序。內(nèi)存系統(tǒng)重排序可以通過(guò)緩存一致性協(xié)議和寫(xiě)緩沖區(qū)等技術(shù)來(lái)實(shí)現(xiàn)。
重排序在一定程度上可以提高程序地執(zhí)行速度和效率,但必須在確保程序正確性和語(yǔ)義一致性地前提下進(jìn)行。在并發(fā)編程中,重排序可能會(huì)引發(fā)數(shù)據(jù)競(jìng)爭(zhēng)、原子性問(wèn)題等多線程并發(fā)問(wèn)題,因此需要采取同步和內(nèi)存屏障等手段進(jìn)行控制和保護(hù)。
編譯程序優(yōu)化地重排序下,程序地有序性會(huì)打破。所以編譯器優(yōu)化和處理器重排序可能會(huì)導(dǎo)致有序性問(wèn)題。
Java重排序流程:

- 1 屬于編譯器重排序,2 和 3 屬于處理器重排序。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題;
- 對(duì)于編譯器,JMM 的編譯器重排序規(guī)則會(huì)禁止類(lèi)型的編譯器重排序(不是所有的編譯器重排序都要禁止);
- 對(duì)于處理器重排序,JMM 的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列是,插入特定類(lèi)型的內(nèi)存屏障(memory barriers,intel 稱(chēng)之為 memory fence)指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類(lèi)型的處理器重排序(不是所有的處理器重排序都要禁止)。
6、Java解決并發(fā)問(wèn)題
Java需要解決多線程并發(fā)安全問(wèn)題,就需要解決 可見(jiàn)性、有序性、原子性 這三個(gè)問(wèn)題。那么,Java是如何解決的呢?
- JMM(Java內(nèi)存模型)規(guī)范了 JVM 如何按需金用緩存和編譯優(yōu)化的方法。
6.1 volatile、synchronized 和 final 關(guān)鍵字
這幾個(gè)關(guān)鍵字,后面會(huì)進(jìn)行詳解,這里先看下概念:
- volatile:volatile 是 Java 提供的一種輕量級(jí)的同步機(jī)制。
- 保證共享變量的可見(jiàn)性:當(dāng)一個(gè)線程修改了volatile變量的值,新值對(duì)于其他線程來(lái)說(shuō)是可以立即得知的。也就是說(shuō),volatile變量在各個(gè)線程中式一致的,這就是所謂的可見(jiàn)性;
- 禁止指令重排序:有些場(chǎng)合下,為了提高性能,編譯器和處理器可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它們會(huì)把存在數(shù)據(jù)依賴(lài)關(guān)系的操作,重新進(jìn)行排序。聲明為volatile的變量,編譯器和處理器就不會(huì)對(duì)其進(jìn)行重排序;
- 不提供原子性:雖然volatile變量能保證可見(jiàn)性和有序性,但沒(méi)辦法保證復(fù)合操作的原子性。例如,num++這樣的操作,其實(shí)包含了多個(gè)子操作,包括:讀取原有的值、進(jìn)行加 1 操作、將新值協(xié)會(huì)到內(nèi)存。這三個(gè)子操作并不是原子性的,也并不會(huì)因?yàn)関olatile聲明而編程原子操作。因此,若需要保證原子性,通常需要結(jié)合synchronized或者Atomic變量來(lái)使用。
- synchronized:synchronized關(guān)鍵字在Java中被用來(lái)作為一種同步鎖。
- 保證線程安全:synchronized可以修飾方法或者以同步塊的形式來(lái)修飾代碼段,能夠保證在同一時(shí)刻最多只有一個(gè)線程執(zhí)行該段代碼,從而保證了類(lèi)實(shí)例的成員變量的線程安全;
- 保證可見(jiàn)性和有序性:synchronized可以保證被 其修飾的變量的修改能夠及時(shí)地被其他線程看到,從而避免出現(xiàn)數(shù)據(jù)不一致地情況。此外,其還能夠保證線程地執(zhí)行是有序的,防止出現(xiàn)指令重排地情況;
- 鎖的釋放與獲?。?/strong>包括以下三種情況會(huì)釋放鎖。一是當(dāng)前線程執(zhí)行完同步代碼就會(huì)釋放掉鎖。二是如果線程執(zhí)行同步代碼塊地過(guò)程中,出現(xiàn)了異常且異常被捕獲,也會(huì)導(dǎo)致鎖地釋放。三是當(dāng)前線程在執(zhí)行同步代碼塊的過(guò)程中執(zhí)行了鎖所屬地對(duì)象地 wait() 方法,這也會(huì)導(dǎo)致線程釋放掉鎖。
- final:final保證不可修改,不變地內(nèi)容不會(huì)引起多線程安全問(wèn)題。
- final修飾變量:final修飾地變量表示常量,它地值不能被修改。一旦賦值后,就不能再改變。常量一般使用大寫(xiě)字母表示,并使用下劃線分隔單詞;
- final修飾方法:final修飾地方法不能被子類(lèi)重寫(xiě)。這種方法再繼承關(guān)系中起到了保護(hù)作用,可以確保父類(lèi)地方法行為不被子類(lèi)修改;
- final修飾類(lèi):final修飾的類(lèi)不能被繼承,即不能有子類(lèi)繼承該類(lèi)。這樣地類(lèi)通常是不希望被修改和擴(kuò)展的最終版本;
- final修飾參數(shù):final修飾方法的參數(shù),表示該參數(shù)再方法內(nèi)部不可修改。這可以用來(lái)保護(hù)方法內(nèi)部的參數(shù)不被意外改變。
6.2 可見(jiàn)性、有序性、原子性的理解
- 可見(jiàn)性
- Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。
- volatile關(guān)鍵字可以保證共享變量的可見(jiàn)性。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它的值的修改會(huì)立即被更新到主存中,當(dāng)其他縣城需要讀取該共享變量時(shí),它會(huì)去主存中獲取最新的值。相比之下,不同的共享變量不能保證可見(jiàn)性,因?yàn)槠湫薷牡闹悼赡軙?huì)延遲寫(xiě)入主存,當(dāng)其他線程需要讀取時(shí),可能得到的仍然是舊值,從而無(wú)法保證可見(jiàn)性。
- 通過(guò)synchronized和Lock保證可見(jiàn)性。
- 通過(guò)synchronized和Lock也能夠保證可見(jiàn)性,synchronized和Lock能夠保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且再釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見(jiàn)性。
- Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。
- 有序性
- 在Java中,使用volatile關(guān)鍵字可以確保一定的有序性。此外,也可以使用synchronized和Lock來(lái)確保有序性。顯然,synchronized和Lock保證同步代碼每次只有一個(gè)線程執(zhí)行,這相當(dāng)于讓線程按順序執(zhí)行同步代碼,自然而然地保證了有序性。當(dāng)然,Java內(nèi)存模型(JMM)通過(guò)Happens-Before規(guī)則來(lái)保證有序性。
- 原子性
- 在Java中,對(duì)基本數(shù)據(jù)類(lèi)型地變量地讀取和賦值操作是原子性操作,即這些操作是不可被中斷地,要么執(zhí)行,要么不執(zhí)行。請(qǐng)分析以下那些操作是原子性操作:
x = 10; //語(yǔ)句1: 直接將數(shù)值10賦值給x,也就是說(shuō)線程執(zhí)行這個(gè)語(yǔ)句的會(huì)直接將數(shù)值10寫(xiě)入到工作內(nèi)存中 y = x; //語(yǔ)句2: 包含2個(gè)操作,它先要去讀取x的值,再將x的值寫(xiě)入工作內(nèi)存,雖然讀取x的值以及 將x的值寫(xiě)入工作內(nèi)存 這2個(gè)操作都是原子性操作,但是合起來(lái)就不是原子性操作了。 x++; //語(yǔ)句3: x++包括3個(gè)操作:讀取x的值,進(jìn)行加1操作,寫(xiě)入新的值。 x = x + 1; //語(yǔ)句4: 同語(yǔ)句3
上面4個(gè)語(yǔ)句只有語(yǔ)句1地操作具備原子性。
也就是說(shuō),只有簡(jiǎn)單地讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量,變量之間地相互賦值不是原子操作)才是原子操作。 從上面可以看出,Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作,如果要實(shí)現(xiàn)更大范圍操作地原子性,可以通過(guò)synchronized和Lock來(lái)實(shí)現(xiàn)。由于synchronized和Lock能夠保證任一時(shí)刻只有一個(gè)線程執(zhí)行改代碼塊,那么自然就不存在原子性問(wèn)題了,從而保證了原子性。
- 在Java中,對(duì)基本數(shù)據(jù)類(lèi)型地變量地讀取和賦值操作是原子性操作,即這些操作是不可被中斷地,要么執(zhí)行,要么不執(zhí)行。請(qǐng)分析以下那些操作是原子性操作:
6.3 Happens-Before 規(guī)則
Java內(nèi)存模型(JMM)通過(guò)Happens-Before規(guī)則用于確保多線程環(huán)境下地可見(jiàn)性和有序性,制定了對(duì)于一個(gè)操作地結(jié)果,在另一個(gè)操作中地可見(jiàn)性和有序性地保證。
Java中地Happens-Before規(guī)則:
- 程序順序規(guī)則(Program Order Rule):在同一線程中,按照程序地順序,前一個(gè)操作地結(jié)果對(duì)于后續(xù)操作是可見(jiàn)的。換句話說(shuō),線程中地操作按照代碼順序執(zhí)行;
- 管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作對(duì)于后續(xù)地lock操作是可見(jiàn)的。之前已經(jīng)釋放地鎖,之后地加索操作可以感知到;
- volatile變量規(guī)則(Volatile Varizble Tule):對(duì)一個(gè)volatile變量地寫(xiě)操作,對(duì)于后續(xù)對(duì)該變量地讀操作,是可見(jiàn)的。volatile關(guān)鍵字會(huì)禁止指令重排,保證了寫(xiě)操作地可見(jiàn)性;
- 線程啟動(dòng)規(guī)則(Thread Start Tule):一個(gè)線程地start操作對(duì)于其他線程中地后續(xù)操作是可見(jiàn)的。換句話說(shuō),其他線程可以看到線程啟動(dòng)之后地操作;
- 中斷規(guī)則(Thread Interrupt Tule):一個(gè)線程中斷地發(fā)生(調(diào)用interrupt方法),對(duì)于該線程地后續(xù)操作是可見(jiàn)的;
- 線程終結(jié)規(guī)則(Thread Termination Tule):主線程地所有操作對(duì)于所有已經(jīng)加入到該線程地子線程地join操作是可見(jiàn)的。換句話說(shuō),主線程地操作對(duì)于子線程地join操作是可見(jiàn)的;
- 線程中斷規(guī)則(Thread Interruption Rule):對(duì)于在線程A中調(diào)用線程B地interrupt方法,如果線程B捕獲到該中斷,則線程A地所有操作對(duì)于線程B捕獲終端之后地操作是可見(jiàn)的;
- 對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象地構(gòu)造函數(shù)完成對(duì)該對(duì)象地初始化后,對(duì)于finalize方法地調(diào)用是可見(jiàn)的。
7、線程安全程度
線程安全可以從強(qiáng)到弱分為以下幾個(gè)級(jí)別:不可變、絕對(duì)線程安全、相對(duì)線程安全、線程兼容、線程對(duì)立。
- 不可變
- 不可變對(duì)象時(shí)線程安全的,因?yàn)樗鼈兊臓顟B(tài)在創(chuàng)建后不可更改。多個(gè)線程可以同時(shí) 訪問(wèn)和使用不可變對(duì)象,而無(wú)需任何同步控制。
- final關(guān)鍵字修飾的基本數(shù)據(jù)類(lèi)型
- String
- 枚舉類(lèi)型
- Number 部分子類(lèi),如 Long 和 Double 等數(shù)值包裝類(lèi)型,BigInteger 和 BigDecimal 等大數(shù)據(jù)類(lèi)型。但同為Number的原子類(lèi) AtomicInteger 和 AtomicLong 則是可變的。
- 不可變對(duì)象時(shí)線程安全的,因?yàn)樗鼈兊臓顟B(tài)在創(chuàng)建后不可更改。多個(gè)線程可以同時(shí) 訪問(wèn)和使用不可變對(duì)象,而無(wú)需任何同步控制。
- 絕對(duì)線程安全
- 絕對(duì)線程安全意味著對(duì)象的所有方法都是線程安全的,可以多線程并發(fā)地訪問(wèn)和修改對(duì)象,而不需要額外的同步控制。這通常是通過(guò)使用同步機(jī)制(如synchronized關(guān)鍵字或使用Lock接口)或線程安全的數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)的。
- 相對(duì)線程安全
- 相對(duì)線程安全需要保證對(duì)這個(gè)對(duì)象單獨(dú)的操作是線程安全的,在調(diào)用的時(shí)候不需要做額外的保障措施。但是對(duì)于一些特定的順序的連續(xù)調(diào)用,就可能需要在調(diào)用端使用額外的同步手段來(lái)保證調(diào)用的正確性;
- 在Java中,大部分的線程安全類(lèi)都屬于這種類(lèi)型,例如,Vector、HashTable、Collections 的 synchronizedCollection() 方法包裝的集合等。
- 線程兼容
- 線程兼容意味著對(duì)象在單線程環(huán)境中是安全的,但在多線程環(huán)境中可能會(huì)有問(wèn)題。在多線程環(huán)境中訪問(wèn)和修改該對(duì)象時(shí),可能需要使用同步控制來(lái)確保多個(gè)線程之間的訪問(wèn)和修改順序。這種級(jí)別通常需要開(kāi)發(fā)人員來(lái)注意使用同步機(jī)制。
- Java API 中大部分的類(lèi)都是屬于線程兼容的,如與前面的 Vector 和 HashTable 相對(duì)應(yīng)的集合類(lèi) ArrayList 和 HashMap 等。
- 線程對(duì)立
- 縣城對(duì)立是指無(wú)論調(diào)用端是否采取了同步措施,都無(wú)法在多線程環(huán)境中并發(fā)使用的代碼。由于Java語(yǔ)言天生就具備多線程特性,線程對(duì)立這種排斥多線程的代碼是很少出現(xiàn)的,而且通常都是有害的,應(yīng)當(dāng)盡量避免。
8、線程安全實(shí)現(xiàn)
8.1 互斥同步
互斥同步是一種保證多個(gè)線程在訪問(wèn)共享資源時(shí)的互斥性的機(jī)制,以防止競(jìng)態(tài)條件和數(shù)據(jù)不一致問(wèn)題。
核心
- 加鎖:synchronized 和 ReentranLock。
互斥同步的詳解
- 互斥(Mutual exclusion):互斥是指同一時(shí)刻只允許一個(gè)線程訪問(wèn)共享資源,其他線程必須等待?;コ鈾C(jī)制可以保證在任何時(shí)刻只能有一個(gè)線程對(duì)共享資源進(jìn)行操作;
- 臨界區(qū)(Critical section):臨界區(qū)是指一段代碼,其中訪問(wèn)共享資源的部分。在進(jìn)入臨界區(qū)前,線程需要獲得互斥鎖,執(zhí)行完臨界區(qū)代碼后釋放互斥鎖,也就是說(shuō)只有獲得互斥鎖的線程才能進(jìn)入臨界區(qū);
- 互斥鎖(Mutex):互斥鎖是一種同步機(jī)制,用于保護(hù)臨界區(qū)的訪問(wèn)。在進(jìn)入臨界區(qū)之前,線程必須獲取互斥鎖,如果互斥鎖已經(jīng)被其他線程持有,則請(qǐng)求線程會(huì)被阻塞,直到互斥鎖被釋放為止。一旦線程獲得了互斥鎖,其他線程將無(wú)法獲得該鎖,直到它被釋放;
- 條件變量(Condition variable):條件變量是一種同步機(jī)制,用于在共享資源的狀態(tài)發(fā)生變化時(shí)進(jìn)行線程的等待和喚醒。條件變量通常與互斥鎖一起使用。當(dāng)某個(gè)線程發(fā)現(xiàn)共享資源的狀態(tài)不滿(mǎn)足其要求時(shí),它會(huì)進(jìn)入等待狀態(tài),同時(shí)釋放互斥鎖,允許其他線程繼續(xù)執(zhí)行。當(dāng)其他線程更改了共享資源的狀態(tài)并滿(mǎn)足該線程的要求時(shí),它會(huì)被喚醒,并重新獲取互斥鎖。
互斥同步的主要問(wèn)題就是線程阻塞和喚醒所帶來(lái)的性能問(wèn)題,因此這種同步也成為阻塞同步。
8.2 非阻塞同步
阻塞同步采用的是悲觀策略,認(rèn)為一個(gè)線程在修改時(shí),一定會(huì)有其他線程進(jìn)行訪問(wèn)修改,導(dǎo)致數(shù)據(jù)不一致。
非阻塞同步采用的是樂(lè)觀策略,認(rèn)為一個(gè)線程在修改時(shí),不會(huì)有其他線程進(jìn)行訪問(wèn)修改,那就修改成功了,如果有其他線程修改,那就采取步長(zhǎng)措施(不斷地重試,直到成功為止)。一個(gè)線程在進(jìn)行某段特定代碼(臨界區(qū))操作時(shí),其他線程可以進(jìn)行其他代碼地操作,不需要進(jìn)行等待。
8.2.1 CAS
CAS(Compare And Swap)是用于解決多線程環(huán)境下地并發(fā)問(wèn)題地一個(gè)方案(非阻塞同步方案),保證原子性。
通過(guò)比較一個(gè)內(nèi)存位置地值與預(yù)期值,如果相等,則將新值寫(xiě)入該內(nèi)存位置,否則不做任何操作。
- CAS操作包括三個(gè)操作數(shù):內(nèi)存位置(內(nèi)存地址)V、預(yù)期值A(chǔ)和新值B。具體操作步驟如下:
- 將內(nèi)存位置地當(dāng)前值(即預(yù)期值)V與預(yù)期值A(chǔ)進(jìn)行比較;
- 如果相等,則將新值B寫(xiě)入內(nèi)存位置,操作成功;
- 如果不相等,則表示內(nèi)存位置得到值已經(jīng)被其他線程修改,操作失敗。根據(jù)需要可以重試或者執(zhí)行其他處理邏輯。
- CAS地特點(diǎn)和優(yōu)勢(shì)包括:
- 原子性:CAS 操作是原子操作,保證了操作的完整性。在操作中,其他線程不能修改內(nèi)存位置的值,因此可以確保數(shù)據(jù)的一致性。
- 無(wú)鎖:相比于使用鎖進(jìn)行同步,CAS 是一種無(wú)鎖的方式。它避免了線程阻塞和上下文切換帶來(lái)的開(kāi)銷(xiāo),在高并發(fā)的情況下性能較好。
- 忙等待:由于 CAS 是基于自旋的方式進(jìn)行操作,當(dāng)操作失敗時(shí),線程會(huì)忙等待直到操作成功。這可能會(huì)造成一定的 CPU 開(kāi)銷(xiāo)。
- 無(wú)阻塞:由于 CAS 不涉及線程阻塞,因此不存在死鎖的問(wèn)題。
CAS 主要應(yīng)用于一些需要高并發(fā)和原子性操作的場(chǎng)景,比如非阻塞算法、無(wú)鎖隊(duì)列和樂(lè)觀鎖等。在 Java 中,java.util.concurrent.atomic 包提供了一些原子類(lèi),如 AtomicInteger 和 AtomicLong,它們底層使用了 CAS 來(lái)實(shí)現(xiàn)線程安全的操作。
8.2.2 ABA
如果一個(gè)變量初次讀取的時(shí)候是 A 值,它的值被改成了 B,后來(lái)又被改回為 A,那 CAS 操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有被改變過(guò)。
在使用 CAS 時(shí)可能存在ABA問(wèn)題,也就是說(shuō)即使內(nèi)存位置的值已經(jīng)變化,但其實(shí)際含義對(duì)當(dāng)前線程來(lái)說(shuō)是沒(méi)有變化的。為了解決ABA問(wèn)題,可以使用版本號(hào)或引用的方式進(jìn)行解決,比如 AtomicStampedReference 和 AtomicMarkableReference 類(lèi)。
8.3 無(wú)同步方案
要保證線程安全,并不是一定就要進(jìn)行同步。如果一個(gè)方法本來(lái)就不涉及共享數(shù)據(jù),那它自然就無(wú)須任何同步措施去保證正確性。
8.3.1 棧封閉
多個(gè)線程訪問(wèn)同一個(gè)方法的局部變量時(shí),不會(huì)出現(xiàn)線程安全問(wèn)題,因?yàn)榫植孔兞看鎯?chǔ)在虛擬機(jī)棧中,屬于線程私有的。
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
/**
* 輸出結(jié)果:
* 100
* 100
*/8.3.2 線程本地存儲(chǔ)
如果能保證共享變量每次使用時(shí),都在同一個(gè)線程中執(zhí)行,那么就沒(méi)有線程安全問(wèn)題可言。
8.3.3 可重入代碼
可重入代碼(reentrant code)是指可以由多個(gè)任務(wù)并發(fā)使用,且不會(huì)引發(fā)數(shù)據(jù)錯(cuò)誤的代碼。換言之,一個(gè)可重入的程序、函數(shù)或例程在執(zhí)行過(guò)程中被中斷,然后在中斷返回前再次調(diào)用,它都將產(chǎn)生可預(yù)期的結(jié)果。
可重入性是一個(gè)重要的概念,尤其是在多線程或多任務(wù)的并發(fā)編程環(huán)境中,它確保了代碼的執(zhí)行不會(huì)被其他線程或任務(wù)的干擾。
要編寫(xiě)可重入代碼,就必須避免使用全局變量、靜態(tài)變量或其他非局部的狀態(tài),也需要避免調(diào)用非重入的函數(shù),并確保對(duì)互斥對(duì)象的訪問(wèn)(如鎖)是正確的。
到此這篇關(guān)于Java多線程并發(fā)基礎(chǔ)實(shí)戰(zhàn)舉例的文章就介紹到這了,更多相關(guān)Java多線程并發(fā)基礎(chǔ)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)word,pdf轉(zhuǎn)html并保留格式
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)將word,pdf轉(zhuǎn)換為html并保留格式,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-07-07
JavaWeb項(xiàng)目中classpath路徑詳解
今天小編就為大家分享一篇關(guān)于JavaWeb項(xiàng)目中classpath路徑詳解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
詳解spring boot整合JMS(ActiveMQ實(shí)現(xiàn))
本篇文章主要介紹了詳解spring boot整合JMS(ActiveMQ實(shí)現(xiàn)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10
Java中bcrypt算法實(shí)現(xiàn)密碼加密的方法步驟
我們可以在Spring Boot和SSM中實(shí)現(xiàn)密碼加密,使用bcrypt算法可以保障密碼的安全性,并且減少了手動(dòng)編寫(xiě)哈希函數(shù)的工作量,本文就來(lái)詳細(xì)的介紹一下,感興趣的可以了解一下2023-08-08
Java Lambda表達(dá)式原理及多線程實(shí)現(xiàn)
這篇文章主要介紹了Java Lambda表達(dá)式原理及多線程實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
Apache?SkyWalking?監(jiān)控?MySQL?Server?實(shí)戰(zhàn)解析
這篇文章主要介紹了Apache?SkyWalking?監(jiān)控?MySQL?Server?實(shí)戰(zhàn)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
企業(yè)級(jí)Kubernetes管理平臺(tái)Wayne功能特性介紹
這篇文章主要為大家介紹了企業(yè)級(jí)Kubernetes管理平臺(tái)Wayne的功能特性及架構(gòu)設(shè)計(jì),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02
詳解servlet的url-pattern匹配規(guī)則
本篇文章主要介紹了=servlet的url-pattern匹配規(guī)則,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12

