国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

javaee初階———多線程(三)

這篇具有很好參考價值的文章主要介紹了javaee初階———多線程(三)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

javaee初階———多線程(三),Java,java,java-ee

T04BF

??專欄: 算法|JAVA|MySQL|C語言

?? 小比特 大夢想

此篇文章與大家分享多線程專題第三篇,關于線程安全方面的內容
如果有不足的或者錯誤的請您指出!

八、線程安全問題(重點)

我們在前面說過,線程之間是搶占式執(zhí)行的,這樣產生的隨機性,使得程序的執(zhí)行順序變得不一致,就會使得程序產生不同的結果,有的時候這些不同的結果,我們是不可接受的,認為是一種bug
那么由多線程引起的bug,這樣的問題就是線程安全問題,存在線程安全問題的代碼,就認為線程是不安全的

1.一個典型的線程不安全的例子

javaee初階———多線程(三),Java,java,java-ee
我們在編寫程序的時候的預期值是10000,但是得到的結果確實不確定的,小于10000的
這就是一個典型的多線程并發(fā)導致的問題
實際上我們的count++這一步操作包含了3步
(1)load : 將內存中count 的值讀取到寄存器里面
(2)add:把寄存器里的count 進行+1操作,后還是保存到寄存器里面
(3)save:將寄存器里的值寫回到內存里面
那么由于搶占式執(zhí)行,在兩個線程執(zhí)行的過程中就有可能出現下面這種情況:
javaee初階———多線程(三),Java,java,java-ee
在上面的執(zhí)行過程中,我們發(fā)現,兩個線程分別執(zhí)行了一次count++,但是由于前一個寫會到內存的count被后一個寫回去的給覆蓋了,最后內存的count還是1
由于當前線程里的執(zhí)行順序是不確定的,.有的時候順序加兩次,結果就是對的,有的時候加兩次,結果只是加了一次,具體有多少次,結果是多少,都是隨機的
因此看到的結果不是一個確定的數,是不可預測的
如果我們能夠保證,一個線程的save是在另一個線程的load之前,那么結果就是我們所預期的

2.出現線程不安全的原因

(1)線程在系統(tǒng)中的執(zhí)行是隨機的,搶占式執(zhí)行,這也是線程安全問題的罪魁禍首
(2)當前代碼,存在多個線程同時修改一個變量
如果是一個/多個線程修改單獨的變量,那就沒事;如果是多個線程讀取同一個變量,也沒事
但是如果是多個線程同時修改同一個變量,那就有問題了
(3)線程針對變量的操作不是"原子"的
有的操作,例如對int 類型的變量進行賦值操作,在cpu就是一個move指令,但是有的修改操作不是一個原子的,像count++這種
(4)內存可見性問題引起的線程不安全
(5)指令重排序問題引起的不安全

3.解決線程不安全的問題

解決問題就要從產生問題的原因入手

3.1針對原因1(線程在系統(tǒng)中的執(zhí)行是隨機的)

這種我們就無能為力了,我們無法干預

3.1針對原因2(存在多個線程同時修改一個變量)

雖然是一個切入點,但是實際上這種做法不是很普適,只是針對一些特定的場景可以使用,例如String就是個不可變的變量

3.2針對原因3(線程針對變量的操作不是"原子"的)

針對原因三是我們解決線程安全問題最普適的方案,可以通過一些操作讓原來不是原子的操作.打包成一個原子的操作,這個操作就是"加鎖"
鎖實際上也是操作系統(tǒng)提供的功能,也就是內核提供的功能,通過系統(tǒng)api給應用程序,而jvm中又對這樣的api進行了封裝.方便我們使用
關于鎖,主要就是兩方面的操作
(1)加鎖:t1線程加鎖后,t2如果嘗試加鎖,就會進入阻塞等待(都是操作系統(tǒng)內核在控制),此時就能看到t2線程處于blocked狀態(tài)
(2)解鎖,直到t1解鎖后,t2才有機會拿到鎖(加鎖成功)
這種就是鎖的"互斥特性",即鎖競爭 / 鎖沖突
那么怎么使用鎖呢??

創(chuàng)建出一個對象,用這個對象作為鎖
Object o = new Object();//在java中,隨便拿一個對象都能作為加鎖的對象(這個是java中特例的設定)
使用synchronized
public class test1 {
    private static int count = 0;
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t = new Thread(()->{
           for(int i = 0; i < 50000; i++){
               synchronized (locker){
                   count++;
               }
           }
        });
        t.start();
    }
}

(1)synchronized是java的關鍵字(不是方法)
(2)synchronized后面帶上的(),()里面帶的對象就是"鎖對象"

注意:鎖對象的用途只有一個,就是用來區(qū)分兩個線程是否是針對同一個對象加鎖,如果是,就會出現"鎖競爭/鎖沖突",就會出現阻塞等待
而至于接下來這個對象是否使用,有什么方法,帶有什么屬性,統(tǒng)統(tǒng)都不關心

(3)synchronized()后面帶著的{ }
表示當進入這個代碼塊,就是給上述( )鎖對象進行了加鎖操作
當出了這個代碼塊,就是給上述的鎖對象解鎖

javaee初階———多線程(三),Java,java,java-ee
如圖所示,此時這個代碼就是兩個線程針對同一個對象進行加鎖,就會出現互斥現象,那么此時的結果就是我們所預期的了

執(zhí)行過程

javaee初階———多線程(三),Java,java,java-ee
當t1先加鎖后,t2嘗試進行加鎖,就會進入阻塞狀態(tài),當t1的save執(zhí)行完后,才會釋放鎖,那么就能夠保證一個線程的save在另一個線程的load之前了,即強行構造出"串行執(zhí)行"的效果

注意:這里的兩個線程中,只是針對count++這個操作是串行執(zhí)行的,但是執(zhí)行for循環(huán)之間的條件判斷 / i++ 等操作,還是并發(fā)執(zhí)行的
此時大部分代碼還是并發(fā)執(zhí)行的,線程任然可以認為是并發(fā)的

另外幾種操作

將鎖加在for循環(huán)外面
javaee初階———多線程(三),Java,java,java-ee
此時for循環(huán)就不能并發(fā)了

在對象里面的方法里加鎖
javaee初階———多線程(三),Java,java,java-ee
由于方法里面只是count++,此時鎖的生命周期和方法的生命周期實際上是一致的
那么我們就可以直接將鎖加在方法上
javaee初階———多線程(三),Java,java,java-ee
這種寫法就相當于一進入方法就進行加鎖操作

注意:這種寫法不是沒有this(鎖對象).而是省略了

那如果是static方法呢??
javaee初階———多線程(三),Java,java,java-ee
由于static方法是不依賴對象的,那么此時就相當于針對該類的類對象進行加鎖
即等效于:

   public void func(){
        synchronized (Counter.class){
            //...
        }
    }

這里的Coout.class就是所謂的類對象

關于類對象,最初我們類,都是在.java源代碼里面提供的,經過javac編譯后,形成.class字節(jié)碼文件,此時上述的類信息依然存在,但是是二進制形式了
而java運行.class字節(jié)碼文件,就會讀取這里的內容加載到類內存中,給后續(xù)使用這個類提供基礎,所以jvm在內存里面保存上述信息的對象,就是類對象
但是此時一旦有多個線程不同的Counter對象調用func,都會觸發(fā)鎖競爭
但是使用我們上面使用this就不一定了,因為this可能指向不同的對象

死鎖
class Counter {
    private static int count = 0;
     public void add(){
         synchronized(this){
             count++;
         }
    }
    public static int getCount() {
        return count;
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                synchronized (counter){
                    counter.add();
                }
            }
        });
        t1.start();
        t1.join();
        System.out.println(Counter.getCount());
    }
}

就類似上面的代碼,我們在進入for循環(huán)的時候,肯定就先獲得了鎖,接著進入add方法中又嘗試針對同一個鎖對象進行加鎖,但是按照我們上面的說法來說,就會進入阻塞等待,直到第一把鎖被釋放,但第一把鎖被釋放,又要先等第二把鎖獲得,這樣就進入了矛盾了

這種情況,就是"死鎖"
那么按照我們上面的邏輯來說,就會卡死
javaee初階———多線程(三),Java,java,java-ee
但是居然可以運行通過???
實際上我們上述分析的過程針對java里面的synchronized是不適用的,在C++或者Python是適用的
是因為在synchronized內部做了特殊處理,在每一個鎖對象里面都會記錄當前是哪個線程持有了這個鎖,當某個多線程里面要進行加鎖時,就會進行判斷,判斷當前進行加鎖的線程是否已經持有了該鎖??
如果沒有,那么就阻塞等待,如果有,那么就放行
那么此時實際上,內層的鎖就沒有什么用了,因為外層的鎖就已經保證了線程安全了.而之所以要搞這一套,就是要防止程序員粗心大意搞出死鎖

死鎖實際上有三種比較典型的場景:

場景1:鎖是不可重入鎖,并且同一個線程針對同一個對象重復加鎖兩次就會造成死鎖(java中synchronized不會出現這種問題)

場景2:兩個線程兩把鎖
存在鎖1和鎖2,以及線程1和線程2,線程1拿到鎖1后,在鎖里面嘗試去拿鎖2;但是此時鎖2已經被線程2拿走了,而恰恰線程2又嘗試去拿鎖1

    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                //...
                try {
                    Thread.sleep(1000);//讓線程2拿到鎖
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    //...
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                //...

                synchronized (locker1) {
                    //...
                }
            }
        });
        t1.start();
        t2.start();
    }

javaee初階———多線程(三),Java,java,java-ee
此時進程就會僵住,卡死了
因為此時t1等待t2釋放鎖2,才能釋放鎖1,但是t2釋放鎖2之前要等待t1釋放鎖1

場景3:N個線程 N 把鎖
一個典型的例子就是哲學家就餐問題
javaee初階———多線程(三),Java,java,java-ee
每個哲學家都坐在兩個筷子之間,每個哲學家啥時候吃面條,啥時候思考人生都是不確定的(搶占式執(zhí)行)

這么模型在大部分情況下是沒問題的,可以最后正常工作的

但是如果出現極端情況,就會出現問題

即在同一時刻,所有的哲學家都拿起左邊的筷子,此時就會出現所有的哲學家都無法拿起右邊的筷子的情況,但是哲學家又是比較固執(zhí),不吃到面條就永遠不會放下筷子
這就是典型的死鎖狀態(tài)

避免死鎖問題

避免死鎖問題,我們就要先理解產生死鎖的必要條件
(1)鎖具有互斥性:這是synchronized的基本特性
(2)鎖不可被搶占(不可被剝奪):即一個線程拿到鎖后,除非他自己釋放,否則別的線程是拿不到鎖的(也是鎖的基本特性)
(3)請求和保持 : 一個線程拿到一個鎖后,不釋放這個鎖的前提下,又嘗試去獲取別的鎖(代碼層面的特性)
(4)循環(huán)等待 多個線程獲取多個鎖的過程中,出現了A等待B,B又等待A的情況(代碼層面的特性)
"必要條件"說明缺一不可
那么我們要避免死鎖就只要避免其中一個就好了
對于前兩點.由于是鎖的基本特性,除非你自己實現鎖.實現可以打破互斥,打破不可剝奪這樣的條件,對于synchronized這樣的鎖是不行的

那么我們就可以從(3)(4)代碼結構入手

第一點就是不要讓鎖嵌套獲取
javaee初階———多線程(三),Java,java,java-ee
但是有的時候,針對某些場景,就必須嵌套獲取了

javaee初階———多線程(三),Java,java,java-ee
上述代碼,就變成了t1執(zhí)行完所有邏輯后釋放locker1之后,才輪到t2執(zhí)行,自然就不會死鎖了
有時候在代碼中確實需要用到多個線程獲取同一把鎖,約定好加鎖的順序,就可以有效避免死鎖了
這是一個最簡單有效的方法

3.3針對原因4(內存可見性引起的線程安全問題)

public class Test6 {
    public static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0){
                //
            }
            System.out.println("t1結束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("輸入一個整數");
            count = scanner.nextInt();
        });
        t1.start();
        t2.start();

    }
}

此時按照我們的需求,輸入任意一個數后,由于count改變了,那么t1里面循環(huán)就會結束,t1線程就會退出
但是實際上:
javaee初階———多線程(三),Java,java,java-ee
t1線程并沒有結束
而當我們在循環(huán)里面嘗試進行一些輸出操作:
javaee初階———多線程(三),Java,java,java-ee
此時就沒問題了

我們來分析一下出現這種問題的原因:
實際上while(count == 0)這個操作的執(zhí)行流程
(1)load 從內存里面讀count到cpu寄存器
(2)cmp 條件成立的話就執(zhí)行流程,不成立就跳轉到別的地方執(zhí)行
實際上,由于while循環(huán)執(zhí)行得太快,短時間內出現大量的load和cmp操作,而執(zhí)行l(wèi)oad操作消耗的時間,比cmp操作要多上幾千倍上萬倍
此時jvm發(fā)現,在t2修改count之前,每次執(zhí)行l(wèi)oad操作的結果實際上都是一樣的,這時候jvm干脆就把load操作給優(yōu)化掉了(把速度慢的優(yōu)化掉,使程序運行速度更快了),這樣的話,只是第一次真正的load,后續(xù)的load都是只是讀取之前保留在寄存器里面的值
而出現優(yōu)化后,t2線程修改count,t1就感知不到了

而當我們上述代碼循環(huán)體中存在IO操作或者阻塞操作(sleep),這時候就會使循環(huán)的旋轉速度大幅度降低了,此時的IO操作才是應該被優(yōu)化的那一個,但是IO操作是不能被優(yōu)化掉的,load被優(yōu)化的前提是反復load的結果是相同的.而IO操作注定是反復執(zhí)行結果是不相同的

致命的是,編譯器到底啥時候優(yōu)化是個"玄學問題"
但是就"內存可見性"問題來說,可以通過特殊的手段來控制,不讓他觸發(fā)優(yōu)化的 ,那就是使用volatile關鍵字

volatile關鍵字

給變量修飾加上這個關鍵字后,此時編譯器就知道,這個變量是"反復無常的",就不能按照上述優(yōu)化策略進行優(yōu)化了
(具體在java中,是讓javac生成字節(jié)碼的時候產生了"內存屏障"相關的指令)
但是這個操作和之前的synchronized保證的原子性是沒有任何關系的
volatile是專門針對內存可見性的場景來解決問題的,并不能解決循環(huán)count++的問題

但是實際上使用synchronized也能一定程度解決問題,此時和代碼里面的Io操作是等價的,相比于Load操作來說加鎖開銷更大,自然就不會對load進行優(yōu)化了

javaee初階———多線程(三),Java,java,java-ee

關于指令重排序引起的線程不安全問題,在下一篇文章會與大家分享!!!

感謝您的訪問!!期待您的關注!!!

javaee初階———多線程(三),Java,java,java-ee文章來源地址http://www.zghlxwxcb.cn/news/detail-853306.html

T04BF

?? 小比特 大夢想

到了這里,關于javaee初階———多線程(三)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 【Java EE初階十三】網絡初識

    【Java EE初階十三】網絡初識

    ? ? ? ? 網絡發(fā)展的幾個主要時期: ????????單機時代-局域網時代-廣域網時代-移動互聯網時代 ????????隨著時代的發(fā)展,越來越需要計算機之間互相通信,共享軟件和數據,即以多個計算機協同工作來完成 業(yè)務,就有了網絡互連。 ????????網絡互連: 將多臺計

    2024年02月20日
    瀏覽(23)
  • 【Java EE初階十七】網絡原理(二)

    【Java EE初階十七】網絡原理(二)

    2.2.2 關于可靠傳輸 4.滑動窗口 ? ? ? ? 前面的三個機制,都是在保證 tcp 的可靠性; ????????TCP 的可靠傳輸,是會影響傳輸的效率的.(多出了一些等待 ack 的時間,單位時間內能傳輸的數據就少了); ????????滑動窗口,就讓可靠傳輸對性能的影響,更少一些.TCP 只要引入了可

    2024年02月20日
    瀏覽(18)
  • 【Java EE初階十六】網絡原理(一)

    【Java EE初階十六】網絡原理(一)

    ? ? ? ? 在網絡原理中主要學習TCP/IP四層模型中的重點網絡協議 ? ? ? ? 應用層是和程序員接觸最密切的; ????????應用程序:在應用層這里,很多時候都是程序員自定義應用層協議(步驟:1、根據需求,明確要傳輸的信息,2、約定好信息按照什么樣的格式來組織)的

    2024年02月20日
    瀏覽(21)
  • 【Java EE 初階】TCP協議的安全效率機制

    【Java EE 初階】TCP協議的安全效率機制

    目錄 1.應用層協議 2.傳輸層協議 3.UDP協議格式 4.TCP協議格式 5.TCP的安全效率機制 1.確認應答機制 2.超時重傳機制 但是,主機A未收到B發(fā)來的確認應答,也可能是因為ACK丟失了; 3.連接管理機制 ?編輯 面試題:會不會有可能變成三次揮手? 面試題:第二個FIN丟包了如何處理?

    2024年02月09日
    瀏覽(22)
  • 【JavaEE基礎學習打卡02】是時候了解Java EE了!

    【JavaEE基礎學習打卡02】是時候了解Java EE了!

    ?? 本系列教程適用于 Java Web 初學者、愛好者,小白白。我們的天賦并不高,可貴在努力,堅持不放棄。堅信量最終引發(fā)質變,厚積薄發(fā)。 ?? 文中白話居多,盡量以小白視角呈現,幫助大家快速入門。 ?? 我是 蝸牛老師 ,之前網名是 Ongoing蝸牛 ,人如其名,干啥都慢,所

    2024年02月12日
    瀏覽(22)
  • 【JavaEE基礎學習打卡03】Java EE 平臺有哪些內容?

    【JavaEE基礎學習打卡03】Java EE 平臺有哪些內容?

    ?? 本系列教程適用于Java Web初學者、愛好者,小白白。我們的天賦并不高,可貴在努力,堅持不放棄。堅信量最終引發(fā)質變,厚積薄發(fā)。 ?? 文中白話居多,盡量以小白視角呈現,幫助大家快速入門。 ?? 我是 蝸牛老師 ,之前網名是 Ongoing蝸牛 ,人如其名,干啥都慢,所以

    2024年02月12日
    瀏覽(20)
  • 【Java EE初階二十二】https的簡單理解

    【Java EE初階二十二】https的簡單理解

    ?????????當前網絡上,主要都是 HTTPS 了,很少能見到 HTTP.實際上 HTTPS 也是基于 HTTP.只不過 HTTPS 在 HTTP 的基礎之上, 引入了\\\"加密\\\"機制;引入 HTTPS 防止你的數據被黑客篡改 ; ????????HTTPS 就是一個重要的保護措施.之所以能夠安全, 最關鍵的在于\\\"加密”; ? ? ? ? 明文:

    2024年02月22日
    瀏覽(19)
  • 【Java EE初階二十一】http的簡單理解(二)

    【Java EE初階二十一】http的簡單理解(二)

    ????????Referer 描述了當前頁面是從哪個頁面跳轉來的,如果是直接在地址欄輸入 url(或者點擊收藏夾中的按鈕) 都是沒有 Referer。如下圖所示: ????????HTTP 最大的問題在于\\\"明文傳輸”,明文傳輸就容易被第三方獲取并篡改. ????????HTTPS 針對 HTTP 數據進行了加密 (h

    2024年02月22日
    瀏覽(17)
  • 【Java EE初階十五】網絡編程TCP/IP協議(二)

    【Java EE初階十五】網絡編程TCP/IP協議(二)

    ? ? ? ? tcp的socket api和U大片的socket api差異很大,但是和前面所講的文件操作很密切的聯系 ? ? ? ? 下面主要講解兩個關鍵的類: ? ? ? ? 1、ServerSocket:給服務器使用的類,使用這個類來綁定端口號 ? ? ? ? 2、Socket:即會給服務器使用,又會給客戶端使用; ????????

    2024年02月20日
    瀏覽(34)
  • 【JavaEE初階】 認識文件與Java中操作文件

    【JavaEE初階】 認識文件與Java中操作文件

    我們先來認識狹義上的文件(file)。針對硬盤這種持久化存儲的I/O設備,當我們想要進行數據保存時,往往不是保存成一個整體,而是獨立成一個個的單位進行保存,這個獨立的單位就被抽象成文件的概念,就類似辦公桌上的一份份真實的文件一般。 文件除了有數據內容之外,

    2024年02月07日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包