什麼是Java多線程?

第五階段 多線程

前言:

一個場景:週末,帶着並不存在的女票去看電影,不管是現場買票也好,又或是手機買票也好,上一秒還有位置,遲鈍了一下之後,就顯示該座位已經沒法選中,一不留神就沒有座位了,影院的票是必定的,可是到底是如何作到,多個窗口或者用戶同時出票而又不重複的呢? 這就是咱們今天所要講解的多線程問題

(一) 線程和進程的概述

(1) 進程

  • 進程:進程是系統進行資源分配和調用的獨立單位。每個進程都有它本身的內存空間和系統資源
  • 多線程:在同一個時間段內能夠執行多個任務,提升了CPU的使用率

(2) 線程

  • 線程:進程的執行單元,執行路徑
  • 單線程:一個應用程序只有一條執行路徑
  • 多線程:一個應用程序有多條執行路徑
  • 多進程的意義?—— 提升CPU的使用率
  • 多線程的意義? —— 提升應用程序的使用率

(3) 補充

並行和併發java

  • 並行是物理上同時發生,指在某一個時間點同時運行多個程序
  • 併發是邏輯上同時發生,指在某一個時間段內同時運行多個程序

Java程序運行原理和JVM的啓動是不是多線程的 ?緩存

  • Java程序的運行原理:安全

    • 由java命令啓動JVM,JVM啓動就至關於啓動了一個進程
    • 接着有該進程建立了一個主線程去調用main方法
  • JVM虛擬機的啓動是單線程的仍是多線程的 ?多線程

    • 垃圾回收線程也要先啓動,不然很容易會出現內存溢出
    • 如今的垃圾回收線程加上前面的主線程,最低啓動了兩個線程,因此,jvm的啓動實際上是多線程的
    • JVM啓動至少啓動了垃圾回收線程和主線程,因此是多線程的

(二) 多線程代碼實現

需求:咱們要實現多線程的程序。併發

如何實現呢?框架

因爲線程是依賴進程而存在的,因此咱們應該先建立一個進程出來。jvm

而進程是由系統建立的,因此咱們應該去調用系統功能建立一個進程。ide

Java是不能直接調用系統功能的,因此,咱們沒有辦法直接實現多線程程序。工具

可是呢?Java能夠去調用C/C++寫好的程序來實現多線程程序。性能

由C/C++去調用系統功能建立進程,而後由Java去調用這樣的東西,

而後提供一些類供咱們使用。咱們就能夠實現多線程程序了。

經過查看API,咱們知道了有2種方式實現多線程程序。

方式1:繼承Thread類

步驟:

  • 自定義MyThread(自定義類名)繼承Thread類
  • MyThread類中重寫run()
  • 建立對象
  • 啓動線程
public class MyThread extends Thread{
    public MyThread() {
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            System.out.println(getName() + ":" + i);
        }
    }
}
public class MyThreadTest {
    public static void main(String[] args) {
        //建立線程對象
        MyThread my = new MyThread();
        //啓動線程,run()至關於普通方法的調用,單線程效果
        //my.run();
        //首先啓動了線程,而後再由jvm調用該線程的run()方法,多線程效果
        my.start();

        //兩個線程演示,多線程效果須要建立多個對象而不是一個對象屢次調用start()方法
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

//運行結果
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:0
Thread-1:3
Thread-0:1
Thread-0:2
......
Thread-0:95
Thread-0:96
Thread-0:97
Thread-0:98
Thread-0:99

方式2:實現Runnable接口 (推薦)

步驟:

  • 自定義類MyuRunnable實現Runnable接口
  • 重寫run()方法
  • 建立MyRunable類的對象
  • 建立Thread類的對象,並把C步驟的對象做爲構造參數傳遞
public class MyRunnable implements Runnable {
    public MyRunnable() {
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            //因爲實現接口的方式不能直接使用Thread類的方法了,可是能夠間接的使用
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class MyRunnableTest {
    public static void main(String[] args) {
        //建立MyRunnable類的對象
        MyRunnable my = new MyRunnable();

        //建立Thread類的對象,並把C步驟的對象做爲構造參數傳遞
//        Thread t1 = new Thread(my);
//        Thread t2 = new Thread(my);
        //下面具體講解如何設置線程對象名稱
//        t1.setName("User1");
//        t1.setName("User2");

        Thread t1 = new Thread(my,"User1");
        Thread t2 = new Thread(my,"User2");

        t1.start()
        t2.start();
    }
}

實現接口方式的好處

能夠避免因爲Java單繼承帶來的侷限性

適合多個相同程序的代碼去處理同一個資源的狀況,把線程同程序的代碼,數據有效分離,較好的體現了面向對象的設計思想

如何理解------能夠避免因爲Java單繼承帶來的侷限性

好比說,某個類已經有父類了,而這個類想實現多線程,可是這個時候它已經不能直接繼承Thread類了

(接口能夠多實現implements,可是繼承extends只能單繼承) ,它的父類也不想繼承Thread由於不須要實現多線程

(三) 獲取和設置線程對象

//獲取線程的名稱
public final String getName()

//設置線程的名稱
public final void setName(String name)

設置線程的名稱 (若是不設置名稱的話,默認是Thread-? (編號) )

方法一:無參構造 + setXxx (推薦)

//建立MyRunnable類的對象
MyRunnable my = new MyRunnable();

//建立Thread類的對象,並把C步驟的對象做爲構造參數傳遞
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("User1");
t1.setName("User2");
        
//與上面代碼等價
Thread t1 = new Thread(my,"User1");
Thread t2 = new Thread(my,"User2");

方法二:(稍微麻煩,要手動寫MyThread的帶參構造方法,方法一不用)

//MyThread類中

public MyThread(String name){
    super(name);//直接調用父類的就好
}

//MyThreadTest類中
MyThread my = new MyThread("admin");

獲取線程名稱

注意:重寫run方法內獲取線程名稱的方式

//Thread
getName()

//Runnable
//因爲實現接口的方式不能直接使用Thread類的方法了,可是能夠間接的使用
Thread.currentThread().getName()

使用實現Runnable接口方法的時候注意:main方法所在的測試類並不繼承Thread類,所以並不能直接使用getName()方法來獲取名稱。

//這種狀況Thread類提供了一個方法:
//public static Thread currentThread():

//返回當前正在執行的線程對象,返回值是Thread,而Thread恰巧能夠調用getName()方法
System.out.println(Thread.currentThread().getName());

(四) 線程調度及獲取和設置線程優先級

假如咱們的計算機只有一個 CPU,那麼 CPU 在某一個時刻只能執行一條指令,線程只有獲得 CPU時間片,也就是使用權,才能夠執行指令。那麼Java是如何對線程進行調用的呢?

線程有兩種調度模型:

分時調度模型 :全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間片

搶佔式調度模型 :優先讓優先級高的線程使用 CPU,若是線程的優先級相同,那麼會隨機選擇一個,優先級高的線程獲取的 CPU 時間片相對多一些。

Java使用的是搶佔式調度模型

//演示如何設置和獲取線程優先級

//返回線程對象的優先級
public final int getPriority()

//更改線程的優先級
public final void setPriority(int newPriority)

線程默認優先級是5。

線程優先級的範圍是:1-10。

線程優先級高僅僅表示線程獲取的 CPU時間片的概率高,可是要在次數比較多,或者屢次運行的時候才能看到比較好的效果。

(五) 線程控制

在後面的案例中會用到一些,這些控制功能不是很難,能夠自行測試。

//線程休眠
public static void sleep(long millis)

//線程加入(等待該線程終止,主線程結束後,其他線程開始搶佔資源)
public final void join()

//線程禮讓(暫停當前正在執行的線程對象,而且執行其餘線程讓多個線程的執行更加和諧,可是不能保證一人一次)
public static void yield()

//後臺線程(某線程結束後,其餘線程也結束)
public final void setDaemon(boolean on)

//(過期了但還能夠用)
public final void stop()

//中斷線程
public void interrupt()

(六) 線程的生命週期

新建 —— 建立線程對象

就緒 —— 線程對象已經啓動,可是尚未獲取到CPU的執行權

運行 —— 獲取到了CPU的執行權

  • 阻塞 —— 沒有CPU的執權,回到就緒

死亡 —— 代碼運行完畢,線程消亡

(七) 多線程電影院出票案例

public class SellTickets implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true){
            if (tickets > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() 
                                   + "正在出售第" + (tickets--) + "張票");
            }
        }
    }
}
public class SellTicketsTest {
    public static void main(String[] args) {
        //建立資源對象
        SellTickets st = new SellTickets();

        //建立線程對象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        //啓動線程
        t1.start();
        t2.start();
        t3.start();
    }
}

在SellTicket類中添加sleep方法,延遲一下線程,拖慢一下執行的速度

經過加入延遲後,就產生了連個問題:

A:相同的票賣了屢次

CPU的一次操做必須是原子性(最簡單的)的 (在讀取tickets--的原來的數值和減1以後的中間擠進了兩個線程而出現重複)

B:出現了負數票

隨機性和延遲致使的 (三個線程同時擠進一個循環裏,tickets--的減法操做有可能在同一個循環中被執行了屢次而出現越界的狀況,好比說 tickets要大於0卻越界到了-1)

也就是說,線程1執行的同時線程2也可能在執行,而不是線程1執行的時候線程2不能執行。

咱們先要知道一下哪些問題會致使出問題:

並且這些緣由也是之後咱們判斷一個程序是否會有線程安全問題的標準

A:是不是多線程環境

B:是否有共享數據

C:是否有多條語句操做共享數據

咱們對照起來,咱們的程序確實存在上面的問題,由於它知足上面的條件

那咱們怎麼來解決這個問題呢?

把多條語句操做共享數據的代碼給包成一個總體,讓某個線程在執行的時候,別人不能來執行

Java給咱們提供了:同步機制

//同步代碼塊:

synchronized(對象){
    須要同步的代碼;
}

同步的好處

同步的出現解決了多線程的安全問題

同步的弊端

當線程至關多時,由於每一個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會下降程序的運行效率

概述:

A:同步代碼塊的鎖對象是誰呢?

任意對象

B:同步方法的格式及鎖對象問題?

把同步關鍵字加在方法上

同步方法的鎖對象是誰呢?

this

C:靜態方法及鎖對象問題?

靜態方法的鎖對象是誰呢?

類的字節碼文件對象。

咱們使用 synchronized 改進咱們上面的程序,前面線程安全的問題,

public class SellTickets implements Runnable {
    private int tickets = 100;

    //建立鎖對象
    //把這個關鍵的鎖對象定義到run()方法(獨立於線程以外),形成同一把鎖
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() 
                                       + "正在出售第" + (tickets--) + "張票");
                }
            }
        }
    }
}

(八) lock鎖的概述和使用

爲了更清晰的表達如何加鎖和釋放鎖,JDK5之後提供了一個新的鎖對象Lock

(能夠更清晰的看到在哪裏加上了鎖,在哪裏釋放了鎖,)

void lock() 加鎖

void unlock() 釋放鎖
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTickets2 implements Runnable {

    private int tickets = 100;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                ;
                if (tickets > 0) {
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "張票");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

(九) 死鎖問題 (簡單認識)

同步弊端

效率低

若是出現了同步嵌套,就容易產生死鎖問題

死鎖問題

是指兩個或者兩個以上的線程在執行的過程當中,因爭奪資源產生的一種互相等待現象

(十) 等待喚醒機制

咱們前面假定的電影院場景,其實仍是有必定侷限的,咱們所假定的票數是必定的,可是實際生活中,每每是一種供需共存的狀態,例如去買早點,當消費者買走一些後,而做爲生產者的店家就會補充一些商品,爲了研究這一種場景,咱們所要學習的就是Java的等待喚醒機制

生產者消費者問題(英語:Producer-consumer problem),也稱 有限緩衝問題(英語:Bounded-buffer problem),是一個多進程同步問題的經典案例。該問題描述了共享固定大小緩衝區的兩個進程——即所謂的「生產者」和「消費者」——在實際運行時會發生的問題。生產者的主要做用是生成必定量的數據放到緩衝區中,而後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。

咱們用通俗一點的話來解釋一下這個問題

Java使用的是搶佔式調度模型

  • A:若是消費者先搶到了CPU的執行權,它就會去消費數據,可是如今的數據是默認值,若是沒有意義,應該等數據有意義再消費。就比如買家進了店鋪早點卻尚未作出來,只能等早點作出來了再消費
  • B:若是生產者先搶到CPU的執行權,它就回去生產數據,可是,當它產生完數據後,還繼續擁有執行權,它還能繼續產生數據,這是不合理的,你應該等待消費者將數據消費掉,再進行生產。 這又比如,店鋪不能無止境的作早點,賣一些,再作,避免虧本

梳理思路

  • A:生產者 —— 先看是否有數據,有就等待,沒有就生產,生產完以後通知消費者來消費數據
  • B:消費者 —— 先看是否有數據,有就消費,沒有就等待,通知生產者生產數據

解釋喚醒——讓線程池中的線程具有執行資格

Object類提供了三個方法:

//等待
wait()
//喚醒單個線程
notify()
//喚醒全部線程
notifyAll()

注意:這三個方法都必須在同步代碼塊中執行 (例如synchronized塊),同時在使用時必須標明所屬鎖,這樣才能夠得出這些方法操做的究竟是哪一個鎖上的線程

爲何這些方法不定義在Thread類中呢 ?

這些方法的調用必須經過鎖對象調用,而咱們剛纔使用的鎖對象是任意鎖對象。

因此,這些方法必須定義在Object類中。

咱們來寫一段簡單的代碼實現等待喚醒機制

public class Student {
    String name;
    int age;
    boolean flag;// 默認狀況是沒有數據(false),若是是true,說明有數據

    public Student() {
    }
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            synchronized (s) {
                //判斷有沒有數據
                //若是有數據,就wait
                if (s.flag) {
                    try {
                        s.wait(); //t1等待,釋放鎖
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //沒有數據,就生產數據
                if (x % 2 == 0) {
                    s.name = "admin";
                    s.age = 20;
                } else {
                    s.name = "User";
                    s.age = 30;
                }
                x++;
                //如今數據就已經存在了,修改標記
                s.flag = true;

                //喚醒線程
                //喚醒t2,喚醒並不表示你立馬能夠執行,必須還得搶CPU的執行權。
                s.notify();
            }
        }
    }
}
package cn.bwh_05_Notify;

public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            synchronized (s){
                //若是沒有數據,就等待
                if (!s.flag){
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println(s.name + "---" + s.age);

                //修改標記
                s.flag = false;
                //喚醒線程t1
                s.notify();
            }
        }
    }
}
package cn.bwh_05_Notify;

public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();

        //設置和獲取的類
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        //線程類
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        //啓動線程
        t1.start();
        t2.start();
    }
}
//運行結果依次交替出現

生產者消費者之等待喚醒機制代碼優化

最終版代碼(在Student類中有大改動,而後GetThread類和SetThread類簡潔不少)

public class Student {
    private String name;
    private int age;
    private boolean flag;

    public synchronized void set(String name, int age) {
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name;
        this.age = age;

        this.flag = true;
        this.notify();
    }

    public synchronized void get() {
        if (!this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(this.name + "---" + this.age);

        this.flag = false;
        this.notify();
    }
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                s.set("admin", 20);
            } else {
                s.set("User", 30);
            }
            x++;
        }
    }
}
public class GetThread implements Runnable{
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true){
            s.get();
        }
    }
}
public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();
        //設置和獲取的類

        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        t1.start();
        t2.start();
    }
}

最終版代碼特色:

  • 把Student的成員變量給私有的了。
  • 把設置和獲取的操做給封裝成了功能,並加了同步。
  • 設置或者獲取的線程裏面只須要調用方法便可

(十一) 線程池

程序啓動一個新線程成本是比較高的,由於它涉及到要與操做系統進行交互。而使用線程池能夠很好的提升性能,尤爲是當程序中要建立大量生存期很短的線程時,更應該考慮使用線程池

線程池裏的每個線程代碼結束後,並不會死亡,而是再次回到線程池中成爲空閒狀態,等待下一個對象來使用

在JDK5以前,咱們必須手動實現本身的線程池,從JDK5開始,Java內置支持線程池

JDK5新增了一個Executors工廠類來產生線程池,有以下幾個方法
//建立一個具備緩存功能的線程池
//緩存:百度瀏覽過的信息再次訪問
public static ExecutorService newCachedThreadPool()

//建立一個可重用的,具備固定線程數的線程池
public static ExecutorService newFixedThreadPool(intnThreads)
                       
//建立一個只有單線程的線程池,至關於上個方法的參數是1 
public static ExecutorService newSingleThreadExecutor()
                       
這些方法的返回值是ExecutorService對象,該對象表示一個線程池,能夠執行Runnable對象或者Callable對象表明的線程。它提供了以下方法

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {
    public static void main(String[] args) {
        //建立一個線程池對象,控制要建立幾個線程對象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //能夠執行Runnalble對象或者Callable對象表明的線程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        //結束線程池
        pool.shutdown();
    }
}

(十二) 匿名內部類的方式實現多線程程序

匿名內部類的格式:

new 類名或者接口名( ) {
              重寫方法;
          };

本質:是該類或者接口的子類對象

public class ThreadDemo {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }.start();
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }).start();
    }
}

(十三) 定時器

定時器是一個應用十分普遍的線程工具,可用於調度多個定時任務之後臺線程的方式執行。在Java中,能夠經過Timer和TimerTask類來實現定義調度的功能

Timer

·public Timer()

public void schedule(TimerTask task, long delay)

public void schedule(TimerTask task,long delay,long period)

TimerTask

abstract void run()

public boolean cancel()

開發中

Quartz是一個徹底由java編寫的開源調度框架

結尾:

若是內容中有什麼不足,或者錯誤的地方,歡迎你們給我留言提出意見, 蟹蟹你們 !^_^

若是能幫到你的話,那就來關注我吧!(系列文章均會在公衆號第一時間更新)

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創Java技術的公衆號:理想二旬不止

相關文章
相關標籤/搜索