JavaSE學習筆記(12)---線程

来源:https://www.cnblogs.com/xjtu-lyh/archive/2020/02/13/12305720.html
-Advertisement-
Play Games

JavaSE學習筆記(12) 線程 多線程 併發與並行 併發 :指兩個或多個事件在 同一個時間段內 發生。 並行 :指兩個或多個事件在 同一時刻 發生(同時發生)。 在操作系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時運行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微 ...


JavaSE學習筆記(12)---線程

多線程

併發與並行

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。

在操作系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時運行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。

而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程式越多,能大大的提高電腦運行的效率。

註意:單核處理器的電腦肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同理,線程也是一樣的,從巨集觀角度上理解線程是並行運行的,但是從微觀角度上分析卻是串列運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之為線程調度。

線程與進程

  • 進程:是指一個記憶體中運行的應用程式,每個進程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個進程;進程也是程式的一次執行過程,是系統運行程式的基本單位;系統運行一個程式即是一個進程從創建、運行到消亡的過程。

  • 線程:線程是進程中的一個執行單元,負責當前進程中程式的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。

    簡而言之:一個程式運行後至少有一個進程,一個進程中可以包含多個線程

線程調度:

  • 分時調度

    所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。

  • 搶占式調度

    優先讓優先順序高的線程使用 CPU,如果線程的優先順序相同,那麼會隨機選擇一個(線程隨機性),Java使用的為搶占式調度。

    • 設置線程的優先順序

    • 搶占式調度詳解

      大部分操作系統都支持多進程併發運行,現在的操作系統幾乎都支持同時運行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos視窗等軟體。此時,這些程式是在同時運行,”感覺這些軟體好像在同一時刻運行著“。

      實際上,CPU(中央處理器)使用搶占式調度模式在多個線程間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
      其實,多線程程式並不能提高程式的運行速度,但能夠提高程式運行效率,讓CPU的使用率更高。

創建線程類

第一種: 通過繼承Thread類實現多線程

Java使用java.lang.Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的代碼。Java使用線程執行體來代表這段程式流。Java中通過繼承Thread類來創建啟動多線程的步驟如下:

  1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
  2. 創建Thread子類的實例,即創建了線程對象
  3. 調用線程對象的start()方法來啟動該線程

代碼如下:

測試類:

public class Demo01 {
    public static void main(String[] args) {
        //創建自定義線程對象
        MyThread mt = new MyThread("新的線程!");
        //開啟新線程
        mt.start();
        //在主方法中執行for迴圈
        for (int i = 0; i < 10; i++) {
            System.out.println("main線程!"+i);
        }
    }
}

自定義線程類:

public class MyThread extends Thread {
    //定義指定線程名稱的構造方法
    public MyThread(String name) {
        //調用父類的String參數的構造方法,指定線程的名稱
        super(name);
    }
    /**
     * 重寫run方法,完成該線程執行的邏輯
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在執行!"+i);
        }
    }
}

第二種: 通過Runnable介面實現多線程

在開發中,我們應用更多的是通過Runnable介面實現多線程。這種方式剋服了11.2.1節中實現線程類的缺點,即在實現Runnable介面的同時還可以繼承某個類。所以實現Runnable介面的方式要通用一些。

通過Runnable介面實現多線程

public class TestThread2 implements Runnable {//自定義類實現Runnable介面;
    //run()方法里是線程體;
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        //創建線程對象,把實現了Runnable介面的對象作為參數傳入;
        Thread thread1 = new Thread(new TestThread2());
        thread1.start();//啟動線程;
        Thread thread2 = new Thread(new TestThread2());
        thread2.start();
    }
}

線程狀態

图11-4 线程生命周期图.png

一個線程對象在它的生命周期內,需要經歷5個狀態。

▪ 新生狀態(New)

用new關鍵字建立一個線程對象後,該線程對象就處於新生狀態。處於新生狀態的線程有自己的記憶體空間,通過調用start方法進入就緒狀態。

▪ 就緒狀態(Runnable)

處於就緒狀態的線程已經具備了運行條件,但是還沒有被分配到CPU,處於“線程就緒隊列”,等待系統為其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會進入執行狀態。一旦獲得CPU,線程就進入運行狀態並自動調用自己的run方法。有4中原因會導致線程進入就緒狀態:

\1. 新建線程:調用start()方法,進入就緒狀態;

\2. 阻塞線程:阻塞解除,進入就緒狀態;

\3. 運行線程:調用yield()方法,直接進入就緒狀態;

\4. 運行線程:JVM將CPU資源從本線程切換到其他線程。

▪ 運行狀態(Running)

在運行狀態的線程執行自己run方法中的代碼,直到調用其他方法而終止或等待某資源而阻塞或完成任務而死亡。如果在給定的時間片內沒有執行結束,就會被系統給換下來回到就緒狀態。也可能由於某些“導致阻塞的事件”而進入阻塞狀態。

▪ 阻塞狀態(Blocked)

阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒)。有4種原因會導致阻塞:

\1. 執行sleep(int millsecond)方法,使當前線程休眠,進入阻塞狀態。當指定的時間到了後,線程進入就緒狀態。

\2. 執行wait()方法,使當前線程進入阻塞狀態。當使用nofity()方法喚醒這個線程後,它進入就緒狀態。

\3. 線程運行時,某個操作進入阻塞狀態,比如執行IO流操作(read()/write()方法本身就是阻塞的方法)。只有當引起該操作阻塞的原因消失後,線程進入就緒狀態。

\4. join()線程聯合: 當某個線程等待另一個線程執行結束後,才能繼續執行時,使用join()方法。

▪ 死亡狀態(Terminated)

死亡狀態是線程生命周期中的最後一個階段。線程死亡的原因有兩個。一個是正常運行的線程完成了它run()方法內的全部工作; 另一個是線程被強制終止,如通過執行stop()或destroy()方法來終止一個線程(註:stop()/destroy()方法已經被JDK廢棄,不推薦使用)。

當一個線程進入死亡狀態以後,就不能再回到其它狀態了。

終止線程我們一般不使用JDK提供的stop()/destroy()方法(它們本身也被JDK廢棄了)。通常的做法是提供一個boolean型的終止變數,當這個變數置為false,則終止線程的運行。

終止線程的典型方法(重要)

public class TestThreadCiycle implements Runnable {
    String name;
    boolean live = true;// 標記變數,表示線程是否可中止;
    public TestThreadCiycle(String name) {
        super();
        this.name = name;
    }
    public void run() {
        int i = 0;
        //當live的值是true時,繼續線程體;false則結束迴圈,繼而終止線程體;
        while (live) {
            System.out.println(name + (i++));
        }
    }
    public void terminate() {
        live = false;
    }
 
    public static void main(String[] args) {
        TestThreadCiycle ttc = new TestThreadCiycle("線程A:");
        Thread t1 = new Thread(ttc);// 新生狀態
        t1.start();// 就緒狀態
        for (int i = 0; i < 100; i++) {
            System.out.println("主線程" + i);
        }
        ttc.terminate();
        System.out.println("ttc stop!");
    }
}

執行結果如圖所示:

圖11-5 示例11-3運行效果圖(因為是多線程,故每次運行結果不一定一致).png

運行效果圖(因為是多線程,故每次運行結果不一定一致)

暫停線程執行sleep/yield

暫停線程執行常用的方法有sleep()和yield()方法,這兩個方法的區別是:

  1. sleep()方法:可以讓正在運行的線程進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。

  2. yield()方法:可以讓正在運行的線程直接進入就緒狀態,讓出CPU的使用權。

暫停線程的方法-sleep()

public class TestThreadState {
    public static void main(String[] args) {
        StateThread thread1 = new StateThread();
        thread1.start();
        StateThread thread2 = new StateThread();
        thread2.start();
    }
}
//使用繼承方式實現多線程
class StateThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
            try {
                Thread.sleep(2000);//調用線程的sleep()方法;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果如下所示(註:以下圖示只是部分結果,運行時可以感受到每條結果輸出之前的延遲,是Thread.sleep(2000)語句在起作用):

圖11-6示例11-4運行效果圖.png

圖11-6示例11-4運行效果圖

暫停線程的方法-yield()

public class TestThreadState {
    public static void main(String[] args) {
        StateThread thread1 = new StateThread();
        thread1.start();
        StateThread thread2 = new StateThread();
        thread2.start();
    }
}
//使用繼承方式實現多線程
class StateThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":" + i);
            Thread.yield();//調用線程的yield()方法;
        }
    }
}

執行結果如圖所示(註:以下圖示只是部分結果,可以引起線程切換,但運行時沒有明顯延遲):

圖11-7示例11-5運行效果圖.png

運行效果圖

線程的聯合join()

線程A在運行期間,可以調用線程B的join()方法,讓線程B和線程A聯合。這樣,線程A就必須等待線程B執行完畢後,才能繼續執行。如下麵示例中,“爸爸線程”要抽煙,於是聯合了“兒子線程”去買煙,必須等待“兒子線程”買煙完畢,“爸爸線程”才能繼續抽煙。

public class TestThreadState {
    public static void main(String[] args) {
        System.out.println("爸爸和兒子買煙故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
 
class FatherThread implements Runnable {
    public void run() {
        System.out.println("爸爸想抽煙,發現煙抽完了");
        System.out.println("爸爸讓兒子去買包紅塔山");
        Thread son = new Thread(new SonThread());
        son.start();
        System.out.println("爸爸等兒子買煙回來");
        try {
            son.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出門去找兒子跑哪去了");
            // 結束JVM。如果是0則表示正常結束;如果是非0則表示非正常結束
            System.exit(1);
        }
        System.out.println("爸爸高興的接過煙開始抽,並把零錢給了兒子");
    }
}
 
class SonThread implements Runnable {
    public void run() {
        System.out.println("兒子出門去買煙");
        System.out.println("兒子買煙需要10分鐘");
        try {
            for (int i = 1; i <= 10; i++) {
                System.out.println("第" + i + "分鐘");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("兒子買煙回來了");
    }
}

图11-8示例11-6运行效果图.png

線程的優先順序

  1. 處於就緒狀態的線程,會進入“就緒隊列”等待JVM來挑選。

  2. 線程的優先順序用數字表示,範圍從1到10,一個線程的預設優先順序是5。

  3. 使用下列方法獲得或設置線程對象的優先順序。

    int getPriority();

void setPriority(int newPriority);

註意:優先順序低只是意味著獲得調度的概率低。並不是絕對先調用優先順序高的線程後調用優先順序低的線程。

什麼是線程同步

▪ 同步問題的提出

現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題。 比如:教室里,只有一臺電腦,多個人都想使用。天然的解決辦法就是,在電腦旁邊,大家排隊。前一人使用完後,後一人再使用。

▪ 線程同步的概念

處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改這個對象。 這時候,我們就需要用到“線程同步”。 線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程再使用。

由於同一進程的多個線程共用同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問造成的這種問題。

由於我們可以通過 private 關鍵字來保證數據對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。

▪ synchronized 方法

通過在方法聲明中加入 synchronized關鍵字來聲明,語法如下:

public  synchronized  void accessVal(int newVal);

synchronized 方法控制對“對象的類成員變數”的訪問:每個對象對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的對象的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。

▪ synchronized塊

synchronized 方法的缺陷:若將一個大的方法聲明為synchronized 將會大大影響效率。

Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。 塊可以讓我們精確地控制到具體的“成員變數”,縮小同步的範圍,提高效率。

synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊,語法如下:

synchronized(syncObject)
   { 
   //允許訪問控制的代碼 
   }

多線程操作同一個對象(使用線程同步)

public class TestSync {
    public static void main(String[] args) {
        Account a1 = new Account(100, "高");
        Drawing draw1 = new Drawing(80, a1);
        Drawing draw2 = new Drawing(80, a1);
        draw1.start(); // 你取錢
        draw2.start(); // 你老婆取錢
    }
}
/*
 * 簡單表示銀行賬戶
 */
class Account {
    int money;
    String aname;
    public Account(int money, String aname) {
        super();
        this.money = money;
        this.aname = aname;
    }
}
/**
 * 模擬提款操作
 * 
 * @author Administrator
 *
 */
class Drawing extends Thread {
    int drawingNum; // 取多少錢
    Account account; // 要取錢的賬戶
    int expenseTotal; // 總共取的錢數
 
    public Drawing(int drawingNum, Account account) {
        super();
        this.drawingNum = drawingNum;
        this.account = account;
    }
 
    @Override
    public void run() {
        draw();
    }
 
    void draw() {
        synchronized (account) {
            if (account.money - drawingNum < 0) {
                System.out.println(this.getName() + "取款,餘額不足!");
                return;
            }
            try {
                Thread.sleep(1000); // 判斷完後阻塞。其他線程開始運行。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money -= drawingNum;
            expenseTotal += drawingNum;
        }
        System.out.println(this.getName() + "--賬戶餘額:" + account.money);
        System.out.println(this.getName() + "--總共取了:" + expenseTotal);
    }
}

图11-12示例11-10运行效果图1.png

图11-13示例11-10运行效果图2.png

synchronized (account)” 意味著線程需要獲得account對象的“鎖”才有資格運行同步塊中的代碼。 Account對象的“鎖”也稱為“互斥鎖”,在同一時刻只能被一個線程使用。A線程擁有鎖,則可以調用“同步塊”中的代碼;B線程沒有鎖,則進入account對象的“鎖池隊列”等待,直到A線程使用完畢釋放了account對象的鎖,B線程得到鎖才可以開始調用“同步塊”中的代碼。


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

-Advertisement-
Play Games
更多相關文章
  • 報錯: gyp verb check python checking for Python executable "python2" in the PATH gyp verb check python checking for Python executable "python" in the PA ...
  • 首先看一段代碼: let obj = { x: 100 }; function fn(y) { this.x += y; console.log(this); } 現在有一個需求:在1秒後,執行函數fn,並讓其this指向obj。 如果寫成 setTimeout(fn, 1000); 這麼寫的話,f ...
  • 1、建造者模式介紹: 2、建造者模式角色分析 3、方式一:建造模式的常規用法(有指揮) 4、方式二:用戶可以自定義套餐和選擇預設套餐(沒有指揮者) 5、優點分析: 6、缺點分析: 7、應用場景: 8、建造者模式與抽象工廠模式的比較 ...
  • CAP定理: 在一個分散式系統中,Consistency(數據一致性)、 Availability(服務可用性)、Partition tolerance(分區容錯性),三者不可兼得。 一致性(Consistency) 在分散式系統中的所有數據備份(副本),在同一時刻數據的值是否一致。(等同於所有節點 ...
  • 當你第一次定義Protocol Buffer的消息的時候,你肯定會給消息設定一套規則需求。但是隨著時間的推進,你的業務可能會發生了變化,與此同時,你的Protocol Buffer消息類型的需求也會隨之變化。 也就是說:有一些欄位可能會發生變化,可能會添加一些欄位,也可能會刪除一些欄位。但是可能有很 ...
  • 00.瞭解反射 請從記憶體的角度分析對象創建的過程 1.找到主函數所在的類,該類.class載入到方法區,通過反射調用這個主方法,方法進棧 01.瞭解IDE,類路徑 02.位元組碼對象Class 源代碼部分截圖: 知識點1: 3種獲取Class位元組碼對象的方式及優缺點 知識點2: 通過位元組碼Class創 ...
  • 前言 今天一番在22:30開始準備今日的日更,冒著極大的斷更風險,研究了一個開源項目,批量下載手機壁紙。 因為一番每天都為文章開頭的配圖撓頭,正向從網上批量抓取一些美圖,以充實庫存。 剛好,一番昨日的文章里有這麼一個抓取手機背景圖片的開源項目,於是一番今天嘗試了下。 要知道,一般調試一段未知代碼,一 ...
  • 一、cookie的保存與讀取 1.cookie的保存-FileCookie.Jar from urllib import request,parse from http import cookiejar #創建cookiejar實例 filename = "cookie.txt" cookie = ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...