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

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

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線程得到鎖才可以開始調用“同步塊”中的代碼。


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

更多相關文章
  • 報錯: 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 = ...
一周排行
  • 1. 泛型Generic 1.1 引入泛型:延遲聲明 泛型方法聲明時,並未寫死類型,在調用的時候再指定類型。 延遲聲明:推遲一切可以推遲的。 1.2 如何聲明和使用泛型 泛型方法:方法名稱後面加上尖括弧,裡面是類型參數 類型參數實際上就是一個類型T聲明,方法就可以用這個類型T了。 如下所示: pub ...
  • 本筆記摘抄自:https://www.cnblogs.com/PatrickLiu/p/7903617.html,記錄一下學習過程以備後續查用。 一、引言 今天我們要講行為型設計模式的第三個模式--迭代器模式,先從名字上來看。迭代是遍歷的意思,迭代器可以理解為是遍歷某某的工具,遍歷什麼呢?在軟 件設 ...
  • 本筆記摘抄自:https://www.cnblogs.com/PatrickLiu/p/7928521.html,記錄一下學習過程以備後續查用。 一、引言 今天我們要講行為型設計模式的第四個模式--觀察者模式,先從名字上來看。觀察者模式可以理解為既然有“觀察者”,那肯定就有“被觀察者”了。“觀察者” ...
  • 先看核心代碼: public List<DataEntity> SearchShopSalesReport(DateTimeOffset? dateFrom, DateTimeOffset? dateTo,string groupBy) { var query = data.DataEntity / ...
  • 首先新建一個項目,名稱叫Caliburn.Micro.ActionConvertions 然後刪掉MainWindow.xaml 然後去app.xaml刪掉StartupUri這行代碼 其次,安裝Caliburn.Micro,Caliburn.Micro.Core,這兩個Nuget包,如下圖 然後新 ...
  • 一文帶你瞭解 C DLR 的世界 在很久之前,我寫了一片文章 "dynamic結合匿名類型 匿名對象傳參" ,裡面我以為DLR內部是用反射實現的。因為那時候是心中想當然的認為只有反射能夠在運行時解析對象的成員信息並調用成員方法。後來也是因為其他的事一直都沒有回過頭來把這一節知識給補上,正所謂亡羊補牢 ...
  • ​ 在C#8.0中,針對介面引入了一項新特性,就是可以指定預設實現,方便對已有實現進行擴展,也對面向Android和Swift的Api進行互操作提供了可能性。下麵我們來看看該特性的具體規則與實現。 一、主要應用場景: 在不破壞影響已有實現的情況下,可以添加新成員。這解決了在第三方已經大量使用了的介面 ...
  • 前言 通常在應用程式開發到正式上線,在這個過程中我們會分為多個階段,通常會有 開發、測試、以及正式環境等。每個環境的參數配置我們會使用不同的參數,因此呢,在ASP.NET Core中就提供了相關的環境API,方便我們更好的去做這些事情。 環境 ASP.NET Core使用ASPNETCORE_ENV ...
  • 擱置了幾天,工作忙的一塌糊塗,今天終於抽空來繼續看看MVC的知識。先來看看MVC的路由是如何處理的。以下為替代的路由: app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{ ...
  • 多用www.bing.com國際版解決代碼報錯 代碼運行的時候,報異常,國內的搜索引擎一搜, 浮誇的廣告太多,解決方案准確性不足, 盜版又很嚴重(導致一錯皆錯),方案未及時更新等詬病。 www.bing.com國際版可以關聯到: (1). 外國官網,可以獲得官方的解決方案。 (2). stackov ...
x