日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

一篇文章弄懂Java多线程基础和Java内存模型

發(fā)布時(shí)間:2025/1/21 java 102 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一篇文章弄懂Java多线程基础和Java内存模型 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

      • 一、多線程的生命周期及五種基本狀態(tài)
      • 二、Java多線程的創(chuàng)建及啟動(dòng)
        • 1.繼承Thread類,重寫該類的run()方法
        • 2.通過(guò)實(shí)現(xiàn)Runnable接口創(chuàng)建線程類
        • 3.通過(guò)Callable和Future接口創(chuàng)建線程
      • 三、Java內(nèi)存模型概念
      • 四、內(nèi)存間的交互操作
      • 五、volatile和synchronized的區(qū)別

寫在前面:提起多線程大部門同學(xué)可能都會(huì)皺起眉頭不知道多線程到底是什么、什么時(shí)候可以用到、用的時(shí)候是不是有共享變量問(wèn)題等等一大堆問(wèn)題。本篇文章將分為兩部分第一部分是講解多線程基礎(chǔ)、第二部分講解Java內(nèi)存模型。

一、多線程的生命周期及五種基本狀態(tài)

Java多線程生命周期,首先看下面這張經(jīng)典的圖,圖中基本上囊括了Java中多線程重要知識(shí)點(diǎn)。

Java線程具有五中基本狀態(tài)

  • 新建狀態(tài)(New):當(dāng)線程對(duì)象對(duì)創(chuàng)建后,即進(jìn)入了新建狀態(tài),如:Thread t = new MyThread();

  • 就緒狀態(tài)(Runnable):當(dāng)調(diào)用線程對(duì)象的start()方法(t.start();),線程即進(jìn)入就緒狀態(tài)。處于就緒狀態(tài)的線程,只是說(shuō)明此線程已經(jīng)做好了準(zhǔn)備,隨時(shí)等待CPU調(diào)度執(zhí)行,并不是說(shuō)執(zhí)行了t.start()此線程立即就會(huì)執(zhí)行;

  • 運(yùn)行狀態(tài)(Running):當(dāng)CPU開始調(diào)度處于就緒狀態(tài)的線程時(shí),此時(shí)線程才得以真正執(zhí)行,即進(jìn)入到運(yùn)行狀態(tài)。注:就緒狀態(tài)是進(jìn)入到運(yùn)行狀態(tài)的唯一入口,也就是說(shuō),線程要想進(jìn)入運(yùn)行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中;

  • 阻塞狀態(tài)(Blocked):處于運(yùn)行狀態(tài)中的線程由于某種原因,暫時(shí)放棄對(duì)CPU的使用權(quán),停止執(zhí)行,此時(shí)進(jìn)入阻塞狀態(tài),直到其進(jìn)入到就緒狀態(tài),才有機(jī)會(huì)再次被CPU調(diào)用以進(jìn)入到運(yùn)行狀態(tài)。根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:

1.等待阻塞:運(yùn)行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);

2.同步阻塞 – 線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用),它會(huì)進(jìn)入同步阻塞狀態(tài);

3.其他阻塞 – 通過(guò)調(diào)用線程的sleep()或join()或發(fā)出了I/O請(qǐng)求時(shí),線程會(huì)進(jìn)入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。

死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。

二、Java多線程的創(chuàng)建及啟動(dòng)

Java中線程的創(chuàng)建常見(jiàn)有如三種基本形式

1.繼承Thread類,重寫該類的run()方法

繼承Thread類,重寫該類的run()方法

public class MyThread extends Thread{@Overridepublic void run() {for (int i = 0 ;i < 50;i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}}public static void main(String[] args) {for (int i = 0;i<50;i++) {//調(diào)用Thread類的currentThread()方法獲取當(dāng)前線程System.out.println(Thread.currentThread().getName() + " " + i);if (i == 10) {new MyThread().start();new MyThread().start(); }}} }

運(yùn)行結(jié)果:

... main 48 main 49 Thread-00 Thread-01 Thread-02 Thread-03 Thread-04 Thread-10 ...

這是代碼運(yùn)行后的結(jié)果,從圖中可以看出:

1、有三個(gè)線程:main、Thread-0 、Thread-1

2、Thread-0 、Thread-1兩個(gè)線程輸出的成員變量 i 的值不連續(xù)(這里的 i 是實(shí)例變量而不是局部變量)。因?yàn)?#xff1a;通過(guò)繼承Thread類實(shí)現(xiàn)多線程時(shí),每個(gè)線程的創(chuàng)建都要?jiǎng)?chuàng)建不同的子類對(duì)象,導(dǎo)致Thread-0 、Thread-1兩個(gè)線程不能共享成員變量 i ;

3、線程的執(zhí)行是搶占式,并沒(méi)有說(shuō)Thread-0 或者Thread-1一直占用CPU(這也與線程優(yōu)先級(jí)有關(guān),這里Thread-0 、Thread-1線程優(yōu)先級(jí)相同,關(guān)于線程優(yōu)先級(jí)的知識(shí)這里不做展開)

2.通過(guò)實(shí)現(xiàn)Runnable接口創(chuàng)建線程類

定義一個(gè)類實(shí)現(xiàn)Runnable接口;創(chuàng)建該類的實(shí)例對(duì)象obj;將obj作為構(gòu)造器參數(shù)傳入Thread類實(shí)例對(duì)象,這個(gè)對(duì)象才是真正的線程對(duì)象

public class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0 ;i < 50 ;i++) {System.out.println(Thread.currentThread().getName()+":" +i);}}public static void main(String[] args) {for (int i = 0;i < 50;i++) {System.out.println(Thread.currentThread().getName() + ":" +i);if (i == 10) {MyRunnable myRunnable = new MyRunnable();new Thread(myRunnable).start();new Thread(myRunnable).start();}}//java8 labdam方式new Thread(() -> {System.out.println(Thread.currentThread().getName());},"線程3").start();} }

運(yùn)行結(jié)果:

... main:46 main:47 main:48 main:49 Thread-028 Thread-029 Thread-030 Thread-130 ...

1、線程1和線程2輸出的成員變量i是連續(xù)的,也就是說(shuō)通過(guò)這種方式創(chuàng)建線程,可以使多線程共享線程類的實(shí)例變量,因?yàn)檫@里的多個(gè)線程都使用了同一個(gè)target實(shí)例變量。但是,當(dāng)你使用我上述的代碼運(yùn)行的時(shí)候,你會(huì)發(fā)現(xiàn),其實(shí)結(jié)果有些并不連續(xù),這是因?yàn)槎鄠€(gè)線程訪問(wèn)同一資源時(shí),如果資源沒(méi)有加鎖,那么會(huì)出現(xiàn)線程安全問(wèn)題(這是線程同步的知識(shí),這里不展開);
2、java8 可以使用lambda方式創(chuàng)建多線程。

3.通過(guò)Callable和Future接口創(chuàng)建線程

創(chuàng)建Callable接口實(shí)現(xiàn)類,并實(shí)現(xiàn)call()方法,該方法將作為線程執(zhí)行體,且該方法有返回值,再創(chuàng)建Callable實(shí)現(xiàn)類的實(shí)例;使用FutureTask類來(lái)包裝Callable對(duì)象,該FutureTask對(duì)象封裝了該Callable對(duì)象的call()方法的返回值;使用FutureTask對(duì)象作為Thread對(duì)象的target創(chuàng)建并啟動(dòng)新線程;調(diào)用FutureTask對(duì)象的get()方法來(lái)獲得子線程執(zhí)行結(jié)束后的返回值

public class MyCallable implements Callable<Integer> {private int i = 0;@Overridepublic Integer call() throws Exception {int sum = 0;for (; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " " + i);sum += i;}return sum;}public static void main(String[] args) throws ExecutionException, InterruptedException {// 創(chuàng)建MyCallable對(duì)象Callable<Integer> myCallable = new MyCallable();//使用FutureTask來(lái)包裝MyCallable對(duì)象FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);for (int i = 0;i<50;i++) {System.out.println(Thread.currentThread().getName() + ":" + i);if (i == 30) {Thread thread = new Thread(ft);thread.start();}}System.out.println("主線程for循環(huán)執(zhí)行完畢..");Integer integer = ft.get();System.out.println("sum = "+ integer);} }

call()方法的返回值類型與創(chuàng)建FutureTask對(duì)象時(shí)<>里的類型一致。

三、Java內(nèi)存模型概念

在并發(fā)編程中,我們需要處理兩個(gè)關(guān)鍵問(wèn)題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通信是指線程之間以何種機(jī)制來(lái)交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞。

在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過(guò)寫-讀內(nèi)存中的公共狀態(tài)來(lái)隱式進(jìn)行通信。在消息傳遞的并發(fā)模型里,線程之間沒(méi)有公共狀態(tài),線程之間必須通過(guò)明確的發(fā)送消息來(lái)顯式進(jìn)行通信。

堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語(yǔ)代指實(shí)例域,靜態(tài)域和數(shù)組元素)。局部變量(Local variables),方法定義參數(shù)(java語(yǔ)言規(guī)范稱之為formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響。

主內(nèi)存和工作內(nèi)存解釋

主內(nèi)存(main memory): 類的實(shí)例所存在的區(qū)域,所有的實(shí)例都存在主存儲(chǔ)器內(nèi),并且實(shí)例的字段也位于這里。主存儲(chǔ)器為所有的線程所共享,主內(nèi)存主要對(duì)應(yīng)于Java堆中對(duì)象的實(shí)例數(shù)據(jù)部分。

工作內(nèi)存(working memory): 每個(gè)線程各自獨(dú)立所擁有的作業(yè)區(qū),在working memory中,存有main memory中的部分拷貝,稱之為工作拷貝(working copy)。

Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:


從上圖來(lái)看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:

  • 首先,線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
  • 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。
  • 下面通過(guò)示意圖來(lái)說(shuō)明這兩個(gè)步驟:

    如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0。
    1、線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。
    2、線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。
    從整體來(lái)看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為java程序員提供內(nèi)存可見(jiàn)性保證。

    四、內(nèi)存間的交互操作

    主內(nèi)存與工作內(nèi)存之間的交互操作定義了8種原子性操作。具體如下:

    • lock(鎖定):作用于主內(nèi)存的變量,將一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)

    • unlock(解鎖):作用于主內(nèi)存的變量,將一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái)

    • read(讀取):作用于主內(nèi)存的變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中

    • load(載入):作用于工作內(nèi)存的變量,把read傳輸?shù)淖兞恐捣湃牖蛘呖截惖焦ぷ鲀?nèi)存的變量副本

    • use(使用):作用于工作內(nèi)存的變量,表示線程引用工作內(nèi)存中的變量值,將工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎

    • assign(賦值):作用于工作內(nèi)存的變量,表示線程將指定的值賦值給工作內(nèi)存中的某個(gè)變量。

    • store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送給主內(nèi)存中

    • write(寫入):作用于主內(nèi)存的變量,將store傳遞的變量值放入到主內(nèi)存中對(duì)應(yīng)的變量里

    下面圖片能幫我們加深印象

    五、volatile和synchronized的區(qū)別

    首先需要理解線程安全的兩個(gè)方面:執(zhí)行控制和內(nèi)存可見(jiàn)。

    執(zhí)行控制的目的是控制代碼執(zhí)行(順序)及是否可以并發(fā)執(zhí)行。

    內(nèi)存可見(jiàn)控制的是線程執(zhí)行結(jié)果在內(nèi)存中對(duì)其它線程的可見(jiàn)性。根據(jù)Java內(nèi)存模型的實(shí)現(xiàn),線程在具體執(zhí)行時(shí),會(huì)先拷貝主存數(shù)據(jù)到線程本地(CPU緩存),操作完成后再把結(jié)果從線程本地刷到主存。

    synchronized關(guān)鍵字解決的是執(zhí)行控制的問(wèn)題,它會(huì)阻止其它線程獲取當(dāng)前對(duì)象的監(jiān)控鎖,這樣就使得當(dāng)前對(duì)象中被synchronized關(guān)鍵字保護(hù)的代碼塊無(wú)法被其它線程訪問(wèn),也就無(wú)法并發(fā)執(zhí)行。更重要的是,synchronized還會(huì)創(chuàng)建一個(gè)內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會(huì)直接刷到主存中,從而保證了操作的內(nèi)存可見(jiàn)性,同時(shí)也使得先獲得這個(gè)鎖的線程的所有操作,都happens-before于隨后獲得這個(gè)鎖的線程的操作。

    了解happens-before請(qǐng)看這篇文章 【并發(fā)重要原則】happens-before理解和應(yīng)用

    volatile關(guān)鍵字解決的是內(nèi)存可見(jiàn)性的問(wèn)題,會(huì)使得所有對(duì)volatile變量的讀寫都會(huì)直接刷到主存,即保證了變量的可見(jiàn)性。這樣就能滿足一些對(duì)變量可見(jiàn)性有要求而對(duì)讀取順序沒(méi)有要求的需求。

    使用volatile關(guān)鍵字僅能實(shí)現(xiàn)對(duì)原始變量(如boolen、 short 、int 、long等)操作的原子性,但需要特別注意, volatile不能保證復(fù)合操作的原子性。

    對(duì)于volatile關(guān)鍵字,當(dāng)且僅當(dāng)滿足以下所有條件時(shí)可使用:

  • 對(duì)變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。
  • 該變量沒(méi)有包含在具有其他變量的不變式中
  • volatile和synchronized的區(qū)別

    • volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問(wèn)該變量,其他線程被阻塞住。
    • volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類級(jí)別的
    • volatile僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,不能保證原子性;而synchronized則可以保證變量的修改可見(jiàn)性和原子性
    • volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
    • volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。

    本文參考:深入理解Java內(nèi)存模型(一)——基礎(chǔ)

    如果有想要了解Double Checked Locking Pattern的危險(xiǎn)性請(qǐng)看這篇文章: 【單例模式】DCL的問(wèn)題和解決方法

    總結(jié)

    以上是生活随笔為你收集整理的一篇文章弄懂Java多线程基础和Java内存模型的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。