最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java多線程并發(fā)基礎實戰(zhàn)舉例

 更新時間:2025年09月22日 09:44:07   作者:鍵盤俠Gu  
進程與線程的區(qū)別,解釋并行與并發(fā)的概念,分析多線程導致的線程安全問題(可見性、原子性、有序性),并探討Java通過volatile、synchronized及Happens-Before規(guī)則解決這些問題的機制,最后分類線程安全程度及實現(xiàn)方案,感興趣的朋友一起看看吧

1、進程與線程

1.1 進程

進程(Process)是計算機中正在運行的程序。程序是一種靜態(tài)的概念,而進程是程序再執(zhí)行過程中創(chuàng)建的動態(tài)實體。每個進程都有自己的內(nèi)存空間、代碼、數(shù)據(jù)和資源,他也是操作系統(tǒng)進行任務調(diào)度和資源分配的基本單位。

  • 進程是正在運行一個軟件或者腳本。這樣理解更為簡單一點。

多個進程可以同事運行在計算機上,彼此獨立并且互不干擾。操作系統(tǒng)通過進程管理來控制和監(jiān)視進程的創(chuàng)建、運行、暫停和終止等操作。進程還可以通過進程間通信機制來實現(xiàn)進程之間的數(shù)據(jù)交換和同步。

打開windows的任務管理其,運行的程序就是一個個進程。

1.2 線程

線程(Thread)是程序執(zhí)行的最小單位,它是進程的一部分。一個進程可以包含一個或多個線程。同一個進程中的多個線程共享同一塊內(nèi)存空間和其他資源,他們可以同時執(zhí)行不同的任務。每個線程都有自己的程序計數(shù)器、棧和一組寄存器,這使得線程能夠獨立的執(zhí)行代碼。

線程的特點

  • 輕量級:相對于進程來說,線程的創(chuàng)建和上下文切換開銷較??;
  • 共享資源:同一進程中的線程可以共享內(nèi)存和其他資源,因此可以更方便地進行數(shù)據(jù)共享和通信;
  • 并發(fā)執(zhí)行:多個線程可以同時執(zhí)行,提高了程序的并發(fā)性和效率。

線程的使用可以提升程序的性能和響應性,特別是再多核處理器上可以實現(xiàn)并行計算。但多線程編程也需要注意線程間的同步和共享數(shù)據(jù)的安全性問題,以避免出現(xiàn)竟態(tài)條件和數(shù)據(jù)不一致的情況。

  • 線程的時間效率和空間效率都比進程要高。

2、并行與并發(fā)

  • 并行
    • 并行(Parallel)是指同時進行多個任務,人物之間可以同時執(zhí)行,彼此獨立。例如,在多核處理器上,可以同時執(zhí)行多個線程或進程,這就是并行。
  • 并發(fā)
    • 并發(fā)(Concurrent)則是指同時交替進行多個任務,人物之間可能有依賴關系或競爭條件。例如,在單核處理器上,通過時間片輪轉(zhuǎn)的方式,多個線程或進程通過快速切換來執(zhí)行,看起來是同時進行的,但實際上是交替執(zhí)行的,并發(fā)中的任務可能需要依賴共享資源或競爭臨界區(qū)資源。

并行是多個任務同時進行,而并發(fā)是多個任務交替進行,并且可能存在資源競爭和依賴。

并行和并發(fā)之間的區(qū)別在于任務是否可以 同時執(zhí)行以及是否需要競爭共享資源。并行通常需要硬件支持,如多核處理器,能同時執(zhí)行多個任務。而并發(fā)則可以在單核處理器上通過時間片輪轉(zhuǎn)等技術實現(xiàn)。

在實際應用中,可以用并行提高計算性能和執(zhí)行速度,可適用于多線程編程或分布式計算;而并發(fā)則可以提高資源利用率和系統(tǒng)吞吐量,可適用于任務調(diào)度和資源管理。

3、多線程的必要性

我們先來看個JVM(Java內(nèi)存模型)簡單模型

Java內(nèi)存模型

  • CPU、內(nèi)存(主存)、I/O(讀寫),這三者的處理速度有著極大的差異。為了平衡這三者的速度差異,需要在計算機體系結構、操作系統(tǒng)、編譯程序上進行優(yōu)化。

3.1 CPU緩存優(yōu)化

  • CPU(中央處理器)是計算機和核心部件,負責處理大部分的計算任務。然而,CPU處理數(shù)據(jù)的速度遠高于內(nèi)存讀取或?qū)懭霐?shù)據(jù)的速度,這種速度上的差異就會導致CPU在等待數(shù)據(jù)時可能無事可做,從而浪費其處理能力;
  • 為了解決這個問題,專門在CPU和內(nèi)存之間增加了緩存(Cache)作為一個數(shù)據(jù)的臨時存儲區(qū),用于存放CPU預期會用到的數(shù)據(jù)。因為緩存是位于CPU和內(nèi)存之間的臨時存儲區(qū),它的存取速度比內(nèi)存要快得多,所以能夠有效地解決CPU和內(nèi)存速度上的差異;
  • 當CPU需要讀取或?qū)懭霐?shù)據(jù)時,它會首先查找緩存中是否有這些數(shù)據(jù)。如果有(這成為“緩存命中”),CPU就可以直接從緩存讀取或?qū)懭霐?shù)據(jù),從而避免了等待內(nèi)存的消耗時間。如果沒有(這成為“緩存未命中”),CPU就需要從內(nèi)存中讀取數(shù)據(jù),并同時將這些數(shù)據(jù)寫入緩存,以供后續(xù)使用;
  • 通過這種方式,緩存能夠有效地利用CPU的告訴處理能力,提高計算機的整體性能。

CPU增強緩存,會導致可見性問題。

3.2 操作系統(tǒng)優(yōu)化

操作系統(tǒng)增加了進程和線程的概念來實現(xiàn)分時復用CPU資源,進而均衡CPU和I/O設備的速度差異。

  • 進程是指計算機中正在運行的程序的實例。操作系統(tǒng)通過為每個進程分配一段獨立的內(nèi)存空間和一組資源(如CPU時間片、文件掃描符等)來管理并控制進程的運行。通過輪流分配CPU時間片,操作系統(tǒng)可以讓多個進程交替運行,從而實現(xiàn)leCPU的分時復用;
  • 線程是指進程中的一個獨立執(zhí)行單元。一個進程可以有多個線程,他們共享該進程的資源和狀態(tài)。每個線程有自己的??臻g和程序計數(shù)器,但它們共享同一進程的內(nèi)存空間、文件掃描符等。操作系統(tǒng)可以 通過調(diào)度算法在不通的線程之間切換,從而實現(xiàn)多個下昵稱在單個進程中并發(fā)執(zhí)行;
  • 通過引入進程和線程的概念,操作系統(tǒng)可以將CPU時間片分配給不同的進程,實現(xiàn)進程之間的輪流執(zhí)行并提高CPU的利用率。同時,操作系統(tǒng)可以通過線程的并發(fā)執(zhí)行來隱藏I/O設備操作的等待時間,提高系統(tǒng)的響應速度;
  • 總而言之,操作系統(tǒng)通過增加進程和線程的概念,實現(xiàn)了分時復用CPU資源,使得多個進程和現(xiàn)車給你可以并發(fā)執(zhí)行,從而在CPU和I/O設備的速度差異中實現(xiàn)均衡。

操作系統(tǒng)增加了進程、線程,會導致原子性問題。

3.3 編譯程序優(yōu)化

編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。

編譯程序優(yōu)化指令執(zhí)行次序是指在編譯過程中,編譯器根據(jù)各種優(yōu)化策略和規(guī)則,自動調(diào)整和重排程序中指令的順序,以提高程序運行的效率和性能。

編譯器進行指令優(yōu)化的主要目的是為了解決以下問題:

  • 硬件資源利用:通過優(yōu)化調(diào)整指令次序,可以更好地利用CPU的并行性,提高CPU利用率,減少資源浪費。例如,通過指令調(diào)度可以讓CPU同時 執(zhí)行多條物館的指令,從而提高指令的并行等級;
  • 減少等待時間:某些指令執(zhí)行需要等待前面指令的結果,這會導致CPU空閑等待。通過優(yōu)化指令順序,可以盡可能地避免這種依賴,減少CPU空閑時間,提高運行效率;
  • 克服性能瓶頸:例如,優(yōu)化內(nèi)存訪問指令地順序,可以減少緩沖區(qū)溢出或者下溢地可能性,避免因為內(nèi)存訪問導致地性能瓶頸;
  • 管線浪費:現(xiàn)代CPU通常將指令執(zhí)行過程分解成多個解讀按,并行執(zhí)行以提高性能,這就是所謂地管線技術。如果指令地執(zhí)行順序不能很好地匹配CPU管線,就會導致管線解讀按地閑置,造成性能下降。

通過這些優(yōu)化,編譯器可以幫助程序員在不需要手動干預地情況下自動提高程序地運行效率和性能,使得程序在各種硬件平臺上都能獲得更好的運行效果。

編譯程序優(yōu)化指令執(zhí)行次序,會導致有序性問題。

4、線程安全問題

如果多個線程對同一個共享數(shù)據(jù)進行訪問而不采取同步操作地的話,那么操作的結果是不一致的。

以下代碼演示了1000個線程同時對cnt執(zhí)行自增操作,操作結束之后它的值有可能嘯宇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());
}
// 輸出結果:992

為什么多線程會導致結果嘯宇1000呢?示什么導致 線程安全問題?

  • 導致線程安全問題的主要原因有三個方面:CPU緩存導致的可見性問題、進程或線程導致的原子性問題、編譯器優(yōu)化導致的有序性問題。

5、線程安全導致原因

多線程操作共享變量時,才會引起線程安全問題。所以下面操作的變量默認都是共享變量。

5.1 可見性

可見性:一個線程對共享變量的修改,另外一個線程能夠立刻看到。

CPU緩存會導致可見性規(guī)則打破,案例:

//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
  • 假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2.由上面的分析可知,當線程1執(zhí)行i=10這句時,會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么CPU1的告訴緩存檔中i的值變?yōu)?0了,卻沒有立即寫入到主存檔中;
  • 此時線程2執(zhí)行j=i,它會先去主存讀取i的值并加載到CPU2的緩存檔中,注意此時內(nèi)存檔中i的值還是0,那么就會使得j的值為0,而不是10.

線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。所以CPU緩存會導致可見性問題。

5.2 原子性

原子性:即一個操作或者多個操作,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

分時復用會引起原子性打破,案例:

int i = 1;
// 線程1執(zhí)行
i += 1;
// 線程2執(zhí)行
i += 1;

這里需要注意的是:i += 1需要三條CPU指令。

  • 將變量i從內(nèi)存讀取到CPU寄存器;
  • 在CPU寄存器中執(zhí)行i + 1 操作;
  • 將最后的結果 i 寫入內(nèi)存(緩存機制導致可能寫入的是 CPU 緩存而不是內(nèi)存)。

由于CPU分時復用(線程切換)的存在,線程1執(zhí)行了第一條指令后,就切換到線程2執(zhí)行,例如線程2執(zhí)行了這三條指令后,再切換會線程1執(zhí)行后續(xù)兩條指令,將造成最后寫道內(nèi)存中的 i 值是2而不是3。所以 CPU分時復用會導致原子性問題。

5.3 有序性

有序性:即程序執(zhí)行的順序按照代碼的縣厚順序執(zhí)行。

重排序優(yōu)化會打破程序有序性,案例:

int i = 0;              
boolean flag = false;
i = 1;                //語句1  
flag = true;          //語句2

雖然上述代碼語句2在語句1之后,但是多線程執(zhí)行順序不一定按語句1、語句2這個順序執(zhí)行。重排序是指計算機系統(tǒng)(如處理器、編譯器等)在執(zhí)行程序時,按照某種規(guī)則重新調(diào)整指令或操作的順序,以提高程序性能或滿足其他需求。

重排序可以分為三種類型:編譯器重排序、處理器重排序和內(nèi)存系統(tǒng)重排序。

  • 編譯器重排序:在編譯階段,編譯器可能會對源代碼中的指令重新排序,以優(yōu)化代碼的執(zhí)行效率。編譯器重排序不會改變程序的語義,即保證最終的執(zhí)行結果與源代碼的順序一致。編譯器重排序可以通過指令級并行、循環(huán)展開、常量傳播等技術來實現(xiàn);
  • 處理器重排序:在處理器執(zhí)行指令時,由于處理器采用了流水線技術,它可以對指令進行重排序,以盡可能地利用處理器資源。處理器重排序可能包括指令級重排序(亂序執(zhí)行)和內(nèi)存訪問重排序。指令級重排序是指處理器可以改變指令地執(zhí)行順序,以提高指令地并行度和執(zhí)行效率。內(nèi)存訪問重排序是指處理器可以改變對內(nèi)存地讀寫操作地順序,以充分利用內(nèi)存系統(tǒng)地各級緩存;
  • 內(nèi)存系統(tǒng)重排序:由于現(xiàn)代計算機系統(tǒng)中存在多級緩存、總線和內(nèi)存等層次結構,因此對于內(nèi)存的讀寫操作也可能存在重排序。內(nèi)存系統(tǒng)重排序可以通過緩存一致性協(xié)議和寫緩沖區(qū)等技術來實現(xiàn)。

重排序在一定程度上可以提高程序地執(zhí)行速度和效率,但必須在確保程序正確性和語義一致性地前提下進行。在并發(fā)編程中,重排序可能會引發(fā)數(shù)據(jù)競爭、原子性問題等多線程并發(fā)問題,因此需要采取同步和內(nèi)存屏障等手段進行控制和保護。

編譯程序優(yōu)化地重排序下,程序地有序性會打破。所以編譯器優(yōu)化和處理器重排序可能會導致有序性問題。

Java重排序流程:

  • 1 屬于編譯器重排序,2 和 3 屬于處理器重排序。這些重排序都可能會導致多線程程序出現(xiàn)內(nèi)存可見性問題;
  • 對于編譯器,JMM 的編譯器重排序規(guī)則會禁止類型的編譯器重排序(不是所有的編譯器重排序都要禁止);
  • 對于處理器重排序,JMM 的處理器重排序規(guī)則會要求Java編譯器在生成指令序列是,插入特定類型的內(nèi)存屏障(memory barriers,intel 稱之為 memory fence)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

6、Java解決并發(fā)問題

Java需要解決多線程并發(fā)安全問題,就需要解決 可見性、有序性、原子性 這三個問題。那么,Java是如何解決的呢?

  • JMM(Java內(nèi)存模型)規(guī)范了 JVM 如何按需金用緩存和編譯優(yōu)化的方法。

6.1 volatile、synchronized 和 final 關鍵字

這幾個關鍵字,后面會進行詳解,這里先看下概念:

  • volatile:volatile 是 Java 提供的一種輕量級的同步機制。
    • 保證共享變量的可見性:當一個線程修改了volatile變量的值,新值對于其他線程來說是可以立即得知的。也就是說,volatile變量在各個線程中式一致的,這就是所謂的可見性;
    • 禁止指令重排序:有些場合下,為了提高性能,編譯器和處理器可能會對輸入代碼進行優(yōu)化,它們會把存在數(shù)據(jù)依賴關系的操作,重新進行排序。聲明為volatile的變量,編譯器和處理器就不會對其進行重排序;
    • 不提供原子性:雖然volatile變量能保證可見性和有序性,但沒辦法保證復合操作的原子性。例如,num++這樣的操作,其實包含了多個子操作,包括:讀取原有的值、進行加 1 操作、將新值協(xié)會到內(nèi)存。這三個子操作并不是原子性的,也并不會因為volatile聲明而編程原子操作。因此,若需要保證原子性,通常需要結合synchronized或者Atomic變量來使用。
  • synchronized:synchronized關鍵字在Java中被用來作為一種同步鎖。
    • 保證線程安全:synchronized可以修飾方法或者以同步塊的形式來修飾代碼段,能夠保證在同一時刻最多只有一個線程執(zhí)行該段代碼,從而保證了類實例的成員變量的線程安全;
    • 保證可見性和有序性:synchronized可以保證被 其修飾的變量的修改能夠及時地被其他線程看到,從而避免出現(xiàn)數(shù)據(jù)不一致地情況。此外,其還能夠保證線程地執(zhí)行是有序的,防止出現(xiàn)指令重排地情況;
    • 鎖的釋放與獲取:包括以下三種情況會釋放鎖。一是當前線程執(zhí)行完同步代碼就會釋放掉鎖。二是如果線程執(zhí)行同步代碼塊地過程中,出現(xiàn)了異常且異常被捕獲,也會導致鎖地釋放。三是當前線程在執(zhí)行同步代碼塊的過程中執(zhí)行了鎖所屬地對象地 wait() 方法,這也會導致線程釋放掉鎖。
  • final:final保證不可修改,不變地內(nèi)容不會引起多線程安全問題。
    • final修飾變量:final修飾地變量表示常量,它地值不能被修改。一旦賦值后,就不能再改變。常量一般使用大寫字母表示,并使用下劃線分隔單詞;
    • final修飾方法:final修飾地方法不能被子類重寫。這種方法再繼承關系中起到了保護作用,可以確保父類地方法行為不被子類修改;
    • final修飾類:final修飾的類不能被繼承,即不能有子類繼承該類。這樣地類通常是不希望被修改和擴展的最終版本;
    • final修飾參數(shù):final修飾方法的參數(shù),表示該參數(shù)再方法內(nèi)部不可修改。這可以用來保護方法內(nèi)部的參數(shù)不被意外改變。

6.2 可見性、有序性、原子性的理解

  • 可見性
    • Java提供了volatile關鍵字來保證可見性。
      • volatile關鍵字可以保證共享變量的可見性。當一個共享變量被volatile修飾時,它的值的修改會立即被更新到主存中,當其他縣城需要讀取該共享變量時,它會去主存中獲取最新的值。相比之下,不同的共享變量不能保證可見性,因為其修改的值可能會延遲寫入主存,當其他線程需要讀取時,可能得到的仍然是舊值,從而無法保證可見性。
    • 通過synchronized和Lock保證可見性。
      • 通過synchronized和Lock也能夠保證可見性,synchronized和Lock能夠保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且再釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
  • 有序性
    • 在Java中,使用volatile關鍵字可以確保一定的有序性。此外,也可以使用synchronized和Lock來確保有序性。顯然,synchronized和Lock保證同步代碼每次只有一個線程執(zhí)行,這相當于讓線程按順序執(zhí)行同步代碼,自然而然地保證了有序性。當然,Java內(nèi)存模型(JMM)通過Happens-Before規(guī)則來保證有序性。
  • 原子性
    • 在Java中,對基本數(shù)據(jù)類型地變量地讀取和賦值操作是原子性操作,即這些操作是不可被中斷地,要么執(zhí)行,要么不執(zhí)行。請分析以下那些操作是原子性操作:
      x = 10;        //語句1: 直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個語句的會直接將數(shù)值10寫入到工作內(nèi)存中
      y = x;         //語句2: 包含2個操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存,雖然讀取x的值以及 將x的值寫入工作內(nèi)存 這2個操作都是原子性操作,但是合起來就不是原子性操作了。
      x++;           //語句3: x++包括3個操作:讀取x的值,進行加1操作,寫入新的值。
      x = x + 1;     //語句4: 同語句3

      上面4個語句只有語句1地操作具備原子性。
      也就是說,只有簡單地讀取、賦值(而且必須是將數(shù)字賦值給某個變量,變量之間地相互賦值不是原子操作)才是原子操作。

    • 從上面可以看出,Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作,如果要實現(xiàn)更大范圍操作地原子性,可以通過synchronized和Lock來實現(xiàn)。由于synchronized和Lock能夠保證任一時刻只有一個線程執(zhí)行改代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。

6.3 Happens-Before 規(guī)則

Java內(nèi)存模型(JMM)通過Happens-Before規(guī)則用于確保多線程環(huán)境下地可見性和有序性,制定了對于一個操作地結果,在另一個操作中地可見性和有序性地保證。

Java中地Happens-Before規(guī)則:

  • 程序順序規(guī)則(Program Order Rule):在同一線程中,按照程序地順序,前一個操作地結果對于后續(xù)操作是可見的。換句話說,線程中地操作按照代碼順序執(zhí)行;
  • 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作對于后續(xù)地lock操作是可見的。之前已經(jīng)釋放地鎖,之后地加索操作可以感知到;
  • volatile變量規(guī)則(Volatile Varizble Tule):對一個volatile變量地寫操作,對于后續(xù)對該變量地讀操作,是可見的。volatile關鍵字會禁止指令重排,保證了寫操作地可見性;
  • 線程啟動規(guī)則(Thread Start Tule):一個線程地start操作對于其他線程中地后續(xù)操作是可見的。換句話說,其他線程可以看到線程啟動之后地操作;
  • 中斷規(guī)則(Thread Interrupt Tule):一個線程中斷地發(fā)生(調(diào)用interrupt方法),對于該線程地后續(xù)操作是可見的;
  • 線程終結規(guī)則(Thread Termination Tule):主線程地所有操作對于所有已經(jīng)加入到該線程地子線程地join操作是可見的。換句話說,主線程地操作對于子線程地join操作是可見的;
  • 線程中斷規(guī)則(Thread Interruption Rule):對于在線程A中調(diào)用線程B地interrupt方法,如果線程B捕獲到該中斷,則線程A地所有操作對于線程B捕獲終端之后地操作是可見的;
  • 對象終結規(guī)則(Finalizer Rule):一個對象地構造函數(shù)完成對該對象地初始化后,對于finalize方法地調(diào)用是可見的。

7、線程安全程度

線程安全可以從強到弱分為以下幾個級別:不可變、絕對線程安全、相對線程安全、線程兼容、線程對立。

  • 不可變
    • 不可變對象時線程安全的,因為它們的狀態(tài)在創(chuàng)建后不可更改。多個線程可以同時 訪問和使用不可變對象,而無需任何同步控制。
      • final關鍵字修飾的基本數(shù)據(jù)類型
      • String
      • 枚舉類型
      • Number 部分子類,如 Long 和 Double 等數(shù)值包裝類型,BigInteger 和 BigDecimal 等大數(shù)據(jù)類型。但同為Number的原子類 AtomicInteger 和 AtomicLong 則是可變的。
  • 絕對線程安全
    • 絕對線程安全意味著對象的所有方法都是線程安全的,可以多線程并發(fā)地訪問和修改對象,而不需要額外的同步控制。這通常是通過使用同步機制(如synchronized關鍵字或使用Lock接口)或線程安全的數(shù)據(jù)結構來實現(xiàn)的。
  • 相對線程安全
    • 相對線程安全需要保證對這個對象單獨的操作是線程安全的,在調(diào)用的時候不需要做額外的保障措施。但是對于一些特定的順序的連續(xù)調(diào)用,就可能需要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性;
    • 在Java中,大部分的線程安全類都屬于這種類型,例如,Vector、HashTable、Collections 的 synchronizedCollection() 方法包裝的集合等。
  • 線程兼容
    • 線程兼容意味著對象在單線程環(huán)境中是安全的,但在多線程環(huán)境中可能會有問題。在多線程環(huán)境中訪問和修改該對象時,可能需要使用同步控制來確保多個線程之間的訪問和修改順序。這種級別通常需要開發(fā)人員來注意使用同步機制。
    • Java API 中大部分的類都是屬于線程兼容的,如與前面的 Vector 和 HashTable 相對應的集合類 ArrayList 和 HashMap 等。
  • 線程對立
    • 縣城對立是指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼。由于Java語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現(xiàn)的,而且通常都是有害的,應當盡量避免。

8、線程安全實現(xiàn)

8.1 互斥同步

互斥同步是一種保證多個線程在訪問共享資源時的互斥性的機制,以防止競態(tài)條件和數(shù)據(jù)不一致問題。

核心

  • 加鎖:synchronized 和 ReentranLock。

互斥同步的詳解

  • 互斥(Mutual exclusion):互斥是指同一時刻只允許一個線程訪問共享資源,其他線程必須等待。互斥機制可以保證在任何時刻只能有一個線程對共享資源進行操作;
  • 臨界區(qū)(Critical section):臨界區(qū)是指一段代碼,其中訪問共享資源的部分。在進入臨界區(qū)前,線程需要獲得互斥鎖,執(zhí)行完臨界區(qū)代碼后釋放互斥鎖,也就是說只有獲得互斥鎖的線程才能進入臨界區(qū);
  • 互斥鎖(Mutex):互斥鎖是一種同步機制,用于保護臨界區(qū)的訪問。在進入臨界區(qū)之前,線程必須獲取互斥鎖,如果互斥鎖已經(jīng)被其他線程持有,則請求線程會被阻塞,直到互斥鎖被釋放為止。一旦線程獲得了互斥鎖,其他線程將無法獲得該鎖,直到它被釋放;
  • 條件變量(Condition variable):條件變量是一種同步機制,用于在共享資源的狀態(tài)發(fā)生變化時進行線程的等待和喚醒。條件變量通常與互斥鎖一起使用。當某個線程發(fā)現(xiàn)共享資源的狀態(tài)不滿足其要求時,它會進入等待狀態(tài),同時釋放互斥鎖,允許其他線程繼續(xù)執(zhí)行。當其他線程更改了共享資源的狀態(tài)并滿足該線程的要求時,它會被喚醒,并重新獲取互斥鎖。

互斥同步的主要問題就是線程阻塞和喚醒所帶來的性能問題,因此這種同步也成為阻塞同步。

8.2 非阻塞同步

阻塞同步采用的是悲觀策略,認為一個線程在修改時,一定會有其他線程進行訪問修改,導致數(shù)據(jù)不一致。

非阻塞同步采用的是樂觀策略,認為一個線程在修改時,不會有其他線程進行訪問修改,那就修改成功了,如果有其他線程修改,那就采取步長措施(不斷地重試,直到成功為止)。一個線程在進行某段特定代碼(臨界區(qū))操作時,其他線程可以進行其他代碼地操作,不需要進行等待。

8.2.1 CAS

CAS(Compare And Swap)是用于解決多線程環(huán)境下地并發(fā)問題地一個方案(非阻塞同步方案),保證原子性。

通過比較一個內(nèi)存位置地值與預期值,如果相等,則將新值寫入該內(nèi)存位置,否則不做任何操作。

  • CAS操作包括三個操作數(shù):內(nèi)存位置(內(nèi)存地址)V、預期值A和新值B。具體操作步驟如下:
    • 將內(nèi)存位置地當前值(即預期值)V與預期值A進行比較;
    • 如果相等,則將新值B寫入內(nèi)存位置,操作成功;
    • 如果不相等,則表示內(nèi)存位置得到值已經(jīng)被其他線程修改,操作失敗。根據(jù)需要可以重試或者執(zhí)行其他處理邏輯。
  • CAS地特點和優(yōu)勢包括:
    • 原子性:CAS 操作是原子操作,保證了操作的完整性。在操作中,其他線程不能修改內(nèi)存位置的值,因此可以確保數(shù)據(jù)的一致性。
    • 無鎖:相比于使用鎖進行同步,CAS 是一種無鎖的方式。它避免了線程阻塞和上下文切換帶來的開銷,在高并發(fā)的情況下性能較好。
    • 忙等待:由于 CAS 是基于自旋的方式進行操作,當操作失敗時,線程會忙等待直到操作成功。這可能會造成一定的 CPU 開銷。
    • 無阻塞:由于 CAS 不涉及線程阻塞,因此不存在死鎖的問題。

CAS 主要應用于一些需要高并發(fā)和原子性操作的場景,比如非阻塞算法、無鎖隊列和樂觀鎖等。在 Java 中,java.util.concurrent.atomic 包提供了一些原子類,如 AtomicInteger 和 AtomicLong,它們底層使用了 CAS 來實現(xiàn)線程安全的操作。

8.2.2 ABA

如果一個變量初次讀取的時候是 A 值,它的值被改成了 B,后來又被改回為 A,那 CAS 操作就會誤認為它從來沒有被改變過。

在使用 CAS 時可能存在ABA問題,也就是說即使內(nèi)存位置的值已經(jīng)變化,但其實際含義對當前線程來說是沒有變化的。為了解決ABA問題,可以使用版本號或引用的方式進行解決,比如 AtomicStampedReference 和 AtomicMarkableReference 類。

8.3 無同步方案

要保證線程安全,并不是一定就要進行同步。如果一個方法本來就不涉及共享數(shù)據(jù),那它自然就無須任何同步措施去保證正確性。

8.3.1 棧封閉

多個線程訪問同一個方法的局部變量時,不會出現(xiàn)線程安全問題,因為局部變量存儲在虛擬機棧中,屬于線程私有的。

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();
}
/**
 * 輸出結果:
 * 100
 * 100
 */

8.3.2 線程本地存儲

如果能保證共享變量每次使用時,都在同一個線程中執(zhí)行,那么就沒有線程安全問題可言。

8.3.3 可重入代碼

可重入代碼(reentrant code)是指可以由多個任務并發(fā)使用,且不會引發(fā)數(shù)據(jù)錯誤的代碼。換言之,一個可重入的程序、函數(shù)或例程在執(zhí)行過程中被中斷,然后在中斷返回前再次調(diào)用,它都將產(chǎn)生可預期的結果。

可重入性是一個重要的概念,尤其是在多線程或多任務的并發(fā)編程環(huán)境中,它確保了代碼的執(zhí)行不會被其他線程或任務的干擾。

要編寫可重入代碼,就必須避免使用全局變量、靜態(tài)變量或其他非局部的狀態(tài),也需要避免調(diào)用非重入的函數(shù),并確保對互斥對象的訪問(如鎖)是正確的。

到此這篇關于Java多線程并發(fā)基礎實戰(zhàn)舉例的文章就介紹到這了,更多相關Java多線程并發(fā)基礎內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 在SpringBoot中使用MongoDB的簡單場景案例

    在SpringBoot中使用MongoDB的簡單場景案例

    MongoDB 是一種非關系型數(shù)據(jù)庫,也被稱為 NoSQL 數(shù)據(jù)庫,它主要以文檔的形式存儲數(shù)據(jù),本文給大家介紹了在SpringBoot中使用MongoDB的簡單場景案例,并通過代碼示例講解的非常詳細,需要的朋友可以參考下
    2024-09-09
  • 解決Springboot中Feignclient調(diào)用時版本問題

    解決Springboot中Feignclient調(diào)用時版本問題

    這篇文章主要介紹了解決Springboot中Feign?client調(diào)用時版本問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • 詳解Mybatis是如何解析配置文件的

    詳解Mybatis是如何解析配置文件的

    這篇文章主要介紹了詳解Mybatis是如何解析配置文件的,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-12-12
  • SpringBoot整合Knife4j的實戰(zhàn)示例

    SpringBoot整合Knife4j的實戰(zhàn)示例

    Knife4j是一個為Java MVC框架集成Swagger生成API文檔的增強解決方案,本文就來介紹一下SpringBoot整合Knife4j的實戰(zhàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-08-08
  • 基于Java代碼實現(xiàn)支付充值的通用流程

    基于Java代碼實現(xiàn)支付充值的通用流程

    本文給大家分享一段java核心代碼實現(xiàn)支付充值的通用流程,非常不錯,具有參考借鑒價值,感興趣的朋友一起看看吧
    2016-05-05
  • java類的組成結構詳解

    java類的組成結構詳解

    大家好,本篇文章主要講的是java類的組成結構詳解,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽
    2021-12-12
  • SpringCloud之服務注冊與發(fā)現(xiàn)Spring Cloud Eureka實例代碼

    SpringCloud之服務注冊與發(fā)現(xiàn)Spring Cloud Eureka實例代碼

    這篇文章主要介紹了SpringCloud之服務注冊與發(fā)現(xiàn)Spring Cloud Eureka實例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • 解決JavaWeb-file.isDirectory()遇到的坑問題

    解決JavaWeb-file.isDirectory()遇到的坑問題

    JavaWeb開發(fā)中,使用`file.isDirectory()`判斷路徑是否為文件夾時,需要特別注意:該方法只能判斷已存在的文件夾,若路徑不存在,無論其實際是否應為文件夾,均會返回`false`,為了解決這個問題,可以采用正則表達式進行判斷,但要求路徑字符串的結尾必須添加反斜杠(\)
    2025-02-02
  • spring boot Lombok @Slf4j日志處理教程(一篇足夠)

    spring boot Lombok @Slf4j日志處理教程(一篇足夠)

    @Slf4j是Lombok 庫提供的一個注解,用于自動生成一個名為log的Logger實例,這篇文章主要介紹了spring boot Lombok @Slf4j日志處理的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2025-09-09
  • SpringBoot快速整合RabbitMq小案例(使用步驟)

    SpringBoot快速整合RabbitMq小案例(使用步驟)

    這篇文章主要介紹了SpringBoot快速整合RabbitMq小案例,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-06-06

最新評論

乡宁县| 远安县| 易门县| 西乌珠穆沁旗| 河津市| 盖州市| 宜兰县| 饶河县| 兴化市| 巴南区| 商丘市| 芷江| 长丰县| 静宁县| 兰考县| 资阳市| 仲巴县| 广河县| 黎平县| 阿拉善左旗| 迁西县| 鹤山市| 伊吾县| 刚察县| 南澳县| 南城县| 景东| 永德县| 信宜市| 塔城市| 磐安县| 吉安县| 墨玉县| 铜山县| 临桂县| 鄂托克旗| 合阳县| 保亭| 祁连县| 霍林郭勒市| 南川市|