<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          線程安全問題就在我身邊 !

          共 1126字,需瀏覽 3分鐘

           ·

          2021-01-30 11:59


          • 串行和并行

          • 線程安全常見錯誤

            • 運行結(jié)果錯誤

            • 發(fā)布和初始化導(dǎo)致線程安全問題

            • 活躍性問題

          • 需要用到線程安全的場景

            • 訪問共享變量或資源

            • 依賴時序的操作

            • 不同數(shù)據(jù)之間存在綁定關(guān)系

            • 對方?jīng)]有聲明自己是線程安全的

          • 總結(jié)

          • 尾語


          串行和并行

          提到多線程這里要說兩個概念,就是串行并行,搞清楚這個我們才能更好的理解多線程。

          串行其實是相對于單條線程來執(zhí)行多個任務(wù)來說的,我們就拿下載文件來舉個例子,我們下載多個文件,在串行中它是按照一定的順序去進行下載的,也就是說必須等下載完A之后,才能開始下載B,它們在時間上是不可能發(fā)生重疊的。

          并行:下載多個文件,開啟多條線程,多個文件同時進行下載,這里是嚴格意義上的在同一時刻發(fā)生的,并行在時間上是重疊的。

          線程安全常見錯誤

          線程安全是Java面試中的???,而在Java中有一些類本身是線程安全的,這些類就是線程安全類,例如ConcurrentHashMap。但是有時候錯誤地使用線程安全類反而會出現(xiàn)線程不安全的情況。

          對象線程安全的定義是:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行問題,也不需要進行額外的同步,而調(diào)用這個對象的行為都可以獲得正確的結(jié)果。

          如果某個對象是線程安全的,那么對于使用者而言,在使用時就不需要考慮方法間的協(xié)調(diào)問題,比如不需要考慮不能同時寫入或讀寫不能并行的問題,也不需要考慮任何額外的同步問題。

          在擁有共享數(shù)據(jù)的多條線程并行執(zhí)行的程序中,線程安全的代碼會通過同步機制如自己加 synchronized 鎖,保證各個線程都可以正常且正確的執(zhí)行,不會出現(xiàn)數(shù)據(jù)污染等意外情況。

          而我們在實際開發(fā)中經(jīng)常會遇到線程不安全的情況,那么一共有哪 3 種典型的線程安全問題呢?

          1. 運行結(jié)果錯誤;
          2. 發(fā)布和初始化先后順序混亂導(dǎo)致線程安全問題;
          3. 活躍性問題,例如死鎖和活鎖。

          運行結(jié)果錯誤

          首先,來看多線程同時操作一個變量導(dǎo)致的運行結(jié)果錯誤。

          public?class?WrongResult?{
          ?
          ???volatile?static?int?i;

          ???public?static?void?main(String[]?args)?throws?InterruptedException?{
          ???????Runnable?r?=?new?Runnable()?{
          ???????????@Override
          ???????????public?void?run()?{
          ???????????????for?(int?j?=?0;?j?10000;?j++)?{
          ???????????????????i++;
          ???????????????}
          ???????????}
          ???????};
          ???????Thread?thread1?=?new?Thread(r);
          ???????thread1.start();
          ???????Thread?thread2?=?new?Thread(r);
          ???????thread2.start();
          ???????thread1.join();
          ???????thread2.join();
          ???????System.out.println(i);
          ????}
          }

          理論上得到的結(jié)果應(yīng)該是20000,但實際結(jié)果呢?

          你可以去試驗10000次,應(yīng)該沒有一次是20000。

          結(jié)果可能是16326,也可能是12362,每次的結(jié)果都還不一樣,這是為什么呢?

          是因為在多線程下,CPU 的調(diào)度是以時間片為單位進行分配的,這也意味著在短時間內(nèi)很有可能有多個線程對某一個變量進行操作。

          i++ 操作,表面上看只是一行代碼,但實際上它并不是一個原子操作,它的執(zhí)行步驟主要分為三步,而且在每步操作之間都有可能被打斷。

          1. 從內(nèi)存中讀取變量;
          2. 增加變量;
          3. 將變量進行保存。

          線程 1 首先拿到 i=1 的結(jié)果,然后進行 i+1 操作,但此時 i+1 的結(jié)果并沒有保存下來,線程 1 就被切換走了,于是 CPU 開始執(zhí)行線程 2,它所做的事情和線程 1 是一樣的 i++ 操作。

          實際上和線程 1 拿到的 i 的結(jié)果一樣都是 1,為什么呢?因為線程 1 雖然對 i 進行了 +1 操作,但結(jié)果沒有保存,所以線程 2 看不到修改后的結(jié)果。

          然后假設(shè)等線程 2 對 i 進行 +1 操作后,又切換到線程 1,讓線程 1 完成未完成的操作,即將 i+1 的結(jié)果 2 保存下來,然后又切換到線程 2 完成 i=2 的保存操作,雖然兩個線程都執(zhí)行了對 i 進行 +1 的操作,但結(jié)果卻最終保存了 i=2 的結(jié)果,而不是我們期望的 i=3,這樣就發(fā)生了線程安全問題,導(dǎo)致了數(shù)據(jù)結(jié)果錯誤,這也是最典型的線程安全問題。

          發(fā)布和初始化導(dǎo)致線程安全問題

          第二種是對象發(fā)布和初始化時導(dǎo)致的線程安全問題。如果我們發(fā)布和初始化的順序沒有同步,容易導(dǎo)致線程安全問題。

          public?class?WrongInit?{

          ?

          ????private?Map?students;

          ?

          ????public?WrongInit()?{

          ????????new?Thread(new?Runnable()?{

          ????????????@Override

          ????????????public?void?run()?{

          ????????????????students?=?new?HashMap<>();

          ????????????????students.put(1,?"張三");

          ????????????????students.put(2,?"李四");

          ????????????????students.put(3,?"周五");

          ????????????????students.put(4,?"趙六");

          ????????????}

          ????????}).start();

          ?????}

          ?

          ????public?Map?getStudents()?{

          ????????return?students;

          ????}

          ?

          ????public?static?void?main(String[]?args)?throws?InterruptedException?{

          ????????WrongInit?multiThreadsError6?=?new?WrongInit();

          ????????System.out.println(multiThreadsError6.getStudents().get(1));

          ?

          ????}

          }

          在類中,定義一個類型為 Map 的成員變量 students,Integer 是學(xué)號,String 是姓名。然后在構(gòu)造函數(shù)中啟動一個新線程,并在線程中為 students 賦值。

          1. 學(xué)號:1,姓名:張三;
          2. 學(xué)號:2,姓名:李四;
          3. 學(xué)號:3,姓名:周五;
          4. 學(xué)號:4,姓名:趙六。

          只有當線程運行完 run() 方法中的全部賦值操作后,4 名同學(xué)的全部信息才算是初始化完畢,可是我們看在主函數(shù) mian() 中,初始化 WrongInit 類之后并沒有進行任何休息就直接打印 1 號同學(xué)的信息。

          試想這個時候程序會出現(xiàn)什么情況?實際上會發(fā)生空指針異常。

          Exception?in?thread?"main"?java.lang.NullPointerException

          at?lesson6.WrongInit.main(WrongInit.java:32)

          這又是為什么呢?

          因為 students 這個成員變量是在構(gòu)造函數(shù)中新建的線程中進行的初始化和賦值操作,而線程的啟動需要一定的時間,但是我們的 main 函數(shù)并沒有進行等待就直接獲取數(shù)據(jù),導(dǎo)致 getStudents 獲取的結(jié)果為 null,這就是在錯誤的時間或地點發(fā)布或初始化造成的線程安全問題。這說明初始化和發(fā)布操作順序必須一致,才能避免此類問題。

          活躍性問題

          第三種線程安全問題統(tǒng)稱為活躍性問題,最典型的有三種,分別為死鎖、活鎖和饑餓。

          什么是活躍性問題呢,活躍性問題就是程序始終得不到運行的最終結(jié)果,相比于前面兩種線程安全問題帶來的數(shù)據(jù)錯誤或報錯,活躍性問題帶來的后果可能更嚴重,比如發(fā)生死鎖會導(dǎo)致程序完全卡死,無法向下運行。

          死鎖

          最常見的活躍性問題是死鎖,死鎖是指兩個線程之間相互等待對方資源,但同時又互不相讓,都想自己先執(zhí)行,如代碼所示。

          ?public?class?MayDeadLock?{


          ????Object?o1?=?new?Object();

          ????Object?o2?=?new?Object();

          ?

          ????public?void?thread1()?throws?InterruptedException?{

          ????????synchronized?(o1)?{

          ????????????Thread.sleep(1000);

          ????????????synchronized?(o2)?{

          ????????????????System.out.println("線程1成功拿到兩把鎖");

          ???????????}

          ????????}

          ????}

          ?

          ????public?void?thread2()?throws?InterruptedException?{

          ????????synchronized?(o2)?{

          ????????????Thread.sleep(1000);

          ????????????synchronized?(o1)?{

          ????????????????System.out.println("線程2成功拿到兩把鎖");

          ????????????}

          ????????}

          ????}

          ?

          ????public?static?void?main(String[]?args)?{

          ????????MayDeadLock?mayDeadLock?=?new?MayDeadLock();

          ????????new?Thread(new?Runnable()?{

          ????????????@Override

          ????????????public?void?run()?{

          ????????????????try?{

          ????????????????????mayDeadLock.thread1();

          ????????????????}?catch?(InterruptedException?e)?{

          ????????????????????e.printStackTrace();

          ????????????????}

          ????????????}

          ????????}).start();

          ????????new?Thread(new?Runnable()?{

          ????????????@Override

          ????????????public?void?run()?{

          ????????????????try?{

          ????????????????????mayDeadLock.thread2();

          ????????????????}?catch?(InterruptedException?e)?{

          ????????????????????e.printStackTrace();

          ????????????????}

          ????????????}

          ????????}).start();

          ????}

          }

          首先,代碼中創(chuàng)建了兩個 Object 作為 synchronized 鎖的對象,線程 1 先獲取 o1 鎖,sleep(1000) 之后,獲取 o2 鎖;線程 2 與線程 1 執(zhí)行順序相反,先獲取 o2 鎖,sleep(1000) 之后,獲取 o1 鎖。

          假設(shè)兩個線程幾乎同時進入休息,休息完后,線程 1 想獲取 o2 鎖,線程 2 想獲取 o1 鎖,這時便發(fā)生了死鎖,兩個程序既不會主動停止申請,也不會釋放自己本身持有的資源,處于循環(huán)等待的過程中。

          在死鎖發(fā)生時,必然存在一個“進程-資源環(huán)形鏈”,即:{p0,p1,p2,…pn},進程p0(或線程)等待p1占用的資源,p1等待p2占用的資源,pn等待p0占用的資源。(最直觀的理解是,p0等待p1占用的資源,而p1而在等待p0占用的資源,于是兩個進程就相互等待)

          這種死鎖的出現(xiàn)解決方案是什么?

          申請鎖的時候執(zhí)行順序要一致,每個線程的申請順序都固定為先申請o1,再申請o2。這樣可以避免死鎖情況的發(fā)生。

          活鎖

          活鎖指的是任務(wù)或者執(zhí)行者沒有被阻塞,由于某些條件沒有滿足,導(dǎo)致一直重復(fù)嘗試—失敗—嘗試—失敗的過程。

          假設(shè)有一個消息隊列,隊列里放著各種各樣需要被處理的消息,而某個消息由于自身被寫錯了導(dǎo)致不能被正確處理,執(zhí)行時會報錯,可是隊列的重試機制會重新把它放在隊列頭進行優(yōu)先重試處理,但這個消息本身無論被執(zhí)行多少次,都無法被正確處理,每次報錯后又會被放到隊列頭進行重試,周而復(fù)始,最終導(dǎo)致線程一直處于忙碌狀態(tài),便發(fā)生了活鎖問題。

          饑餓

          饑餓是指線程需要某些資源時始終得不到。在 Java 中有線程優(yōu)先級的概念,Java 中優(yōu)先級分為 1 到 10,1 最低,10 最高。如果我們把某個線程的優(yōu)先級設(shè)置為 1,這是最低的優(yōu)先級,在這種情況下,這個線程就有可能始終分配不到 CPU 資源,而導(dǎo)致長時間無法運行?;蛘呤悄硞€線程始終持有某個文件的鎖,而其他線程想要修改文件就必須先獲取鎖,這樣想要修改文件的線程就會陷入饑餓,長時間不能運行。

          需要用到線程安全的場景

          訪問共享變量或資源

          第一種場景是訪問共享變量或共享資源的時候,典型的場景有訪問共享對象的屬性,訪問 static 靜態(tài)變量,訪問共享的緩存,等等。因為這些信息不僅會被一個線程訪問到,還有可能被多個線程同時訪問,那么就有可能在并發(fā)讀寫的情況下發(fā)生線程安全問題。比如多線程同時 i++ 的例子:

          /**

          ?*?描述:?????共享的變量或資源帶來的線程安全問題

          ?*/


          public?class?ThreadNotSafe1?{



          ????static?int?i;



          ????public?static?void?main(String[]?args)?throws?InterruptedException?{

          ????????Runnable?r?=?new?Runnable()?{

          ????????????@Override

          ????????????public?void?run()?{

          ????????????????for?(int?j?=?0;?j?10000;?j++)?{

          ????????????????????i++;

          ????????????????}

          ????????????}

          ????????};

          ????????Thread?thread1?=?new?Thread(r);

          ????????Thread?thread2?=?new?Thread(r);

          ????????thread1.start();

          ????????thread2.start();

          ????????thread1.join();

          ????????thread2.join();

          ????????System.out.println(i);

          ????}

          }

          如代碼所示,兩個線程同時對 i 進行 i++ 操作,最后的輸出可能是 15875 等小于20000的數(shù),而不是我們期待的20000,這便是非常典型的共享變量帶來的線程安全問題。

          依賴時序的操作

          第二個需要我們注意的場景是依賴時序的操作,如果我們操作的正確性是依賴時序的,而在多線程的情況下又不能保障執(zhí)行的順序和我們預(yù)想的一致,這個時候就會發(fā)生線程安全問題

          if?(map.containsKey(key))?{

          ????map.remove(obj)

          }

          代碼中首先檢查 map 中有沒有 key 對應(yīng)的元素,如果有則繼續(xù)執(zhí)行 remove 操作。

          此時,這個組合操作就是危險的,因為它是先檢查后操作,而執(zhí)行過程中可能會被打斷。

          如果此時有兩個線程同時進入 if() 語句,然后它們都檢查到存在 key 對應(yīng)的元素,于是都希望執(zhí)行下面的 remove 操作,隨后一個線程率先把 obj 給刪除了,而另外一個線程它剛已經(jīng)檢查過存在 key 對應(yīng)的元素,if 條件成立,所以它也會繼續(xù)執(zhí)行刪除 obj 的操作,但實際上,集合中的 obj 已經(jīng)被前面的線程刪除了,這種情況下就可能導(dǎo)致線程安全問題。

          不同數(shù)據(jù)之間存在綁定關(guān)系

          第三種需要我們注意的線程安全場景是不同數(shù)據(jù)之間存在相互綁定關(guān)系的情況。

          有時候,我們的不同數(shù)據(jù)之間是成組出現(xiàn)的,存在著相互對應(yīng)或綁定的關(guān)系,最典型的就是 IP 和端口號。有時候我們更換了 IP,往往需要同時更換端口號,如果沒有把這兩個操作綁定在一起,就有可能出現(xiàn)單獨更換了 IP 或端口號的情況,而此時信息如果已經(jīng)對外發(fā)布,信息獲取方就有可能獲取一個錯誤的 IP 與端口綁定情況,這時就發(fā)生了線程安全問題。在這種情況下,我們也同樣需要保障操作的原子性。

          對方?jīng)]有聲明自己是線程安全的

          第四種值得注意的場景是在我們使用其他類時,如果對方?jīng)]有聲明自己是線程安全的,那么這種情況下對其他類進行多線程的并發(fā)操作,就有可能會發(fā)生線程安全問題。舉個例子,比如說我們定義了 ArrayList它本身并不是線程安全的,如果此時多個線程同時對 ArrayList 進行并發(fā)讀/寫,那么就有可能會產(chǎn)生線程安全問題,造成數(shù)據(jù)出錯,而這個責(zé)任并不在 ArrayList,因為它本身并不是并發(fā)安全的,正如源碼注釋所寫的:

          Note?that?this?implementation?is?not?synchronized.?
          If?multiple?threads?access?an?ArrayList?instance?concurrently,?
          and?at?least?one?of?the?threads?modifies?the?list?structurally,?it?must?be?synchronized?externally.

          這段話的意思是說,如果我們把 ArrayList 用在了多線程的場景,需要在外部手動用 synchronized 等方式保證并發(fā)安全。

          所以 ArrayList 默認不適合并發(fā)讀寫,是我們錯誤地使用了它,導(dǎo)致了線程安全問題。所以,我們在使用其他類時如果會涉及并發(fā)場景,那么一定要首先確認清楚,對方是否支持并發(fā)操作。

          總結(jié)

          線程安全問題是多線程并發(fā)的重中之重,這里只是講了幾個有可能存在線程安全的例子,就此也打開了線程并發(fā)的學(xué)習(xí)。接下來將講述一些關(guān)于Java中的一些鎖機制和案例,敬請期待。

          END


          有熱門推薦??

          1.?在 IDEA 中用了熱部署神器 JRebel 之后,開發(fā)效率提升10倍!

          2.?想讓進程后臺運行,試試Linux的nohup命令,3分鐘學(xué)會。

          3.?RPC 實現(xiàn)以及相關(guān)學(xué)習(xí) ~

          4.?Google 開源的依賴注入庫,比 Spring 更小更快!

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99精品视频国产 | 日逼大黄片| 黑人大屌成人 | 国产精品成人片 | 九九九视频完整版 |