併發編程基礎(下)

来源:https://www.cnblogs.com/CodeBear/archive/2019/05/06/10817779.html
-Advertisement-
Play Games

書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,join等,本篇繼續介紹併發編程的基礎知識。 sleep 當一個執行的線程調用了Thread的sleep方法,調用線程會暫時讓出指定時間的執行權,在這期間不參與CPU的調度,不占用CPU,但是不會釋 ...


書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,join等,本篇繼續介紹併發編程的基礎知識。

sleep

當一個執行的線程調用了Thread的sleep方法,調用線程會暫時讓出指定時間的執行權,在這期間不參與CPU的調度,不占用CPU,但是不會釋放該線程鎖持有的監視器鎖。指定的時間到了後,該線程會回到就緒的狀態,再次等待分配CPU資源,然後再次執行。

我們有時會看到sleep(1),甚至還有sleep(0)這種寫法,肯定會覺得非常奇怪,特別是sleep(0),睡0秒鐘,有意義嗎?其實是有的,sleep(1),sleep(0)的意義就在於告訴操作系統立刻觸發一次CPU競爭。

讓我們來看看正在sleep的進程被中斷了,會發生什麼事情:

class MySleepTask implements Runnable{
    @Override
    public void run() {
        System.out.println("MyTask1");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("中斷");
            e.printStackTrace();
        }
        System.out.println("MyTask2");
    }
}

public class Sleep {
    public static void main(String[] args) {
        MySleepTask mySleepTask=new MySleepTask();
        Thread thread=new Thread(mySleepTask);
        thread.start();
        thread.interrupt();
    }
}

運行結果:

MyTask1
中斷
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.codebear.MySleepTask.run(Sleep.java:10)
    at java.lang.Thread.run(Thread.java:748)
MyTask2

yield

我們知道線程是以時間片的機制來占用CPU資源並運行的,正常情況下,一個線程只有把分配給自己的時間片用完之後,線程調度器才會進行下一輪的線程調度,當執行了Thread的yield後,就告訴操作系統“我不需要CPU了,你現在就可以進行下一輪的線程調度了 ”,但是操作系統可以忽略這個暗示,也有可能下一輪還是把時間片分配給了這個線程。

我們來寫一個例子加深下印象:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
        }
    }
}


public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}

運行結果:
image.png

當然由於線程的特性,所以每次運行結果可能都不太相同,但是當我們運行多次後,會發現絕大多數的時候,兩個線程的列印都是比較平均的,我用完時間片了,你用,你用完了時間片了,我再用。

當我們調用yield後:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
            Thread.yield();
        }
    }
}


public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}

運行結果:
image.png

當然在一般情況下,可能永遠也不會用到yield,但是還是要對這個方法有一定的瞭解。

sleep 和 yield 區別

當線程調用sleep後,會阻塞當前線程指定的時間,在這段時間內,線程調度器不會調用此線程,當指定的時間結束後,該線程的狀態為“就緒”,等待分配CPU資源。
當線程調用yield後,不會阻塞當前線程,只是讓出時間片,回到“就緒”的狀態,等待分配CPU資源。

死鎖

死鎖是指多個線程在執行的過程中,因為爭奪資源而造成的相互等待的現象,而且無法打破這個“僵局”。

死鎖的四個必要條件:

  • 互斥:指線程對於已經獲取到的資源進行排他性使用,即該資源只能被一個線程占有,如果還有其他線程也想占有,只能等待,直到占有資源的線程釋放該資源。
  • 請求並持有:指一個線程已經占有了一個資源,但是還想占有其他的資源,但是其他資源已經被其他線程占有了,所以當前線程只能等待,等待的同時並不釋放自己已經擁有的資源。
  • 不可剝奪:當一個線程獲取資源後,不能被其他線程占有,只有在自己使用完畢後自己釋放資源。
  • 環路等待:即 T1線程正在等待T2占有的資源,T2線程正在等待T3線程占有的資源,T3線程又在等待T1線程占有的資源。

要想打破“死鎖”僵局,只需要破壞以上四個條件中的任意一個,但是程式員可以干預的只有“請求並持有”,“環路等待”兩個條件,其餘兩個條件是鎖的特性,程式員是無法干預的。

聰明的你,一定看出來了,所謂“死鎖”就是“悲觀鎖”造成的,相對於“死鎖”,還有一個“活鎖”,就是“樂觀鎖”造成的。

守護線程與用戶線程

Java中的線程分為兩類,分別為 用戶線程和守護線程。在JVM啟動時,會調用main函數,這個就是用戶線程,JVM內部還會啟動一些守護線程,比如垃圾回收線程。那麼守護線程和用戶線程到底有什麼區別呢?當最後一個用戶線程結束後,JVM就自動退出了,而不管當前是否有守護線程還在運行。
如何創建一個守護線程呢?

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        });
        thread.setDaemon(true);
        thread.start();
    }
}

只需要設置線程的daemon為true就可以。
下麵來演示下用戶線程與守護線程的區別:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });

        thread.start();
    }
}

當我們運行後,可以發現程式一直沒有退出:
image.png
因為這是用戶線程,只要有一個用戶線程還沒結束,程式就不會退出。

再來看看守護線程:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });
        thread.setDaemon(true);
        thread.start();
    }
}

當我們運行後,發現程式立刻就停止了:
image.png
因為這是守護線程,當用戶線程結束後,不管有沒有守護線程還在運行,程式都會退出。

線程中斷

之所以把線程中斷放在後面,是因為它是併發編程基礎中最難以理解的一個,當然這也與不經常使用有關。現在就讓我們好好看看線程中斷。
Thread提供了stop方法,用來停止當前線程,但是已經被標記為過期,應該用線程中斷方法來代替stop方法。

interrupt

中斷線程。當線程A運行(非阻塞)時,線程B可以調用線程A的interrupt方法來設置線程A的中斷標記為true,這裡要特別註意,調用interrupt方法並不會真的去中斷線程,只是設置了中斷標記為true,線程A還是活的好好的。如果線程A被阻塞了,比如調用了sleep、wait、join,線程A會在調用這些方法的地方拋出“InterruptedException”。
我們來做個試驗,證明下interrupt方法不會中斷正在運行的線程:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 150000; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}

運行結果:

結束了,時間是7643
true

在子線程中,我們通過一個迴圈往copyOnWriteArrayList裡面添加數據來模擬一個耗時操作。這裡要特別要註意,一般來說,我們模擬耗時操作都是用sleep方法,但是這裡不能用sleep方法,因為調用sleep方法會讓當前線程阻塞,而現在是要讓線程處於運行的狀態。我們可以很清楚的看到,雖然子線程剛運行,就被interrupt了,但是卻沒有拋出任何異常,也沒有讓子線程終止,子線程還是活的好好的,只是最後列印出的“中斷標記”為true。

如果沒有調用interrupt方法,中斷標記為false:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 500; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
    }
}

運行結果:

結束了,時間是1
false

在介紹sleep,wait,join方法的時候,大家已經看到了,如果中斷調用這些方法而被阻塞的線程會拋出異常,這裡就不再演示了,但是還有一點需要註意,當我們catch住InterruptedException異常後,“中斷標記”會被重置為false,我們繼續做實驗:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(3);
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
        } catch (Exception ex) {
            System.out.println(Thread.currentThread().isInterrupted());
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}

運行結果:

false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.codebear.InterruptTask.run(InterruptTest.java:20)
    at java.lang.Thread.run(Thread.java:748)

可以很清楚的看到,“中斷標記”被重置為false了。

還有一個問題,大家可以思考下,代碼的本意是當前線程被中斷後退出死迴圈,這段代碼有問題嗎?

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
 
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}

本題來自 極客時間 王寶令 老師的 《Java併發編程實戰》

代碼是有問題的,因為catch住異常後,會把“中斷標記”重置。如果正好在sleep的時候,線程被中斷了,又重置了“中斷標記”,那麼下一次迴圈,檢測中斷標記為false,就無法退出死迴圈了。

isInterrupted

這個方法在上面已經出現過了,就是 獲取對象線程的“中斷標記”。

interrupted

獲取當前線程的“中斷標記”,如果發現當前線程被中斷,會重置中斷標記為false,該方法是static方法,通過Thread類直接調用。

併發編程基礎到這裡就結束了,可以看到內容還是相當多的,雖說是基礎,但是每一個知識點,如果要深究的話,都可以牽扯到“操作系統”,所以只有深入到了“操作系統”,才可以說真的懂了,現在還是僅僅停留在Java的層面,唉。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ```js class TrieNode { constructor(data){ this.data = data this.children = new Array(26) this.isEndingChar = false this.text = '' } } class TrieTree {... ...
  • 如果第二次看到我的文章,歡迎右側掃碼訂閱我喲~ 👉 每周五早8點 按時送達。當然了,也會時不時加個餐~ Z哥在前面的三篇文章里和你一起聊了「高性能」主題下與「緩存」相關的內容。這次和你來聊聊提高性能的另一個大招——「非同步」。 如果你已經對「非同步」有所瞭解的話,這次可以讓你有更深刻的理解。如果你對「 ...
  • [TOC] 一. 簡述一致性哈希演算法 這裡不詳細介紹一致性哈希演算法的起源了, 網上能方便地搜到許多介紹一致性哈希演算法的好文章. 本文主要想動手實現一致性哈希演算法, 並搭建一個環境進行實戰測試. 在開始之前先整理一下 演算法的思路 : 一致性哈希演算法通過把每台伺服器的哈希值打在哈希環上, 把哈希環分成不 ...
  • const:靜態常量,也稱編譯時常量(compile-time constants),屬於類型級,通過類名直接訪問,被所有對象共用! a、叫編譯時常量的原因是它編譯時會將其替換為所對應的值; b、靜態常量在速度上會稍稍快一些,但是靈活性卻比動態常量差一些; c、靜態常量,隱式是靜態的,即被stati ...
  • 背景 電商中有這樣的一個場景: 1. 下單成功之後送積分的操作,我們使用mq來實現 2. 下單成功之後,投遞一條消息到mq,積分系統消費消息,給用戶增加積分 我們主要討論一下,下單及投遞消息到mq的操作,如何實現?每種方式優缺點? 方式一 step1:start transaction step2: ...
  • 老王的股票 大家好,我是小趙,目前任職藏劍山莊高級鑄劍師,在山莊裡和我玩的比較好的有老王和老劉他們幾個,都是組長級別的二貨們,經常混在一起打牌。 今天上午閑得蛋疼晃悠晃悠的晃到的老王的地盤,看到老王在埋頭寫程式: 這老王似乎在炒股票,好專業的樣子。 於是我伸手拍了拍老王的肩膀:“幹啥呢?”。 老王一 ...
  • [toc] 一.題目要求 我們在剛開始上課的時候介紹過一個小學四則運算自動生成程式的例子,請實現它,要求: 能夠自動生成四則運算練習題 可以定製題目數量 用戶可以選擇運算符 用戶設置最大數(如十以內、百以內等) 用戶選擇是否有括弧、是否有小數 用戶選擇輸出方式(如輸出到文件、印表機等) 最好能提供圖 ...
  • 剛開始學習php的時候是在wamp環境下開發的,後來才接觸到 lnmp 環境當時安裝lnmp是按照一大長篇文檔一步步的編譯安裝,當時是真不知道是在做什麼啊!腦袋一片空白~~,只知道按照那麼長的一篇文檔一步步的來做就能實現lnmp的搭建。最近工作閑暇之餘又想起來了這個悲慘的事情,然後我就想能不能不看文 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...