Java多線程入門(絕對詳細,多Demo幫助瞭解)

多線程入門

先大致瞭解下進程與線程
程序運行起來叫進程
進程包含若干線程(默認含有主線程、gc線程)

一、創建線程的三個方法:

  • 繼承Thread類
package threadTest;

/**
 * 創建線程方式一
 * 繼承Thread類
 * 重寫run()方法
 * 調用start()方法啓動線程
 */
public class ThreadTest extends Thread {
    public static void main(String[] args) {
        Thread thread = new ThreadTest();
        thread.start();
        for (int i=0;i<1000;i++){
            System.out.println("main線程運行中"+i);
        }
    }

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("Thread子線程運行中"+i);
        }
    }
}
  • 實現Runnable接口
package threadTest;

/**
 * 創建線程方式二
 * 實現Runnable接口
 * 實現run()方法
 * 創建Thread類傳入Runnable實現類對象
 * 調用Thread的start()方法
 */
public class RunnableTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable創建子線程運行中"+i);
        }
    }

    public static void main(String[] args) {

        Thread thread = new Thread(new RunnableTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main線程運行中"+i);
        }
    }
}
  • 實現Callable接口(需要返回值類型,該方法目前僅做了解)
package threadTest;

import java.util.concurrent.*;

/**
 * 創建線程方式三
 * 實現Callable接口(擁有返回值)
 * 實現call方法,需要拋出異常
 * 創建Callable實現類對象
 * 創建執行服務:ExecutorService ser = Executors.newFixedThreadPool()
 * 提交執行線程:Future result = ser.submit(new CallableTest)
 * 獲取結果:boolean res = result.get();
 * 關閉服務:ser.shutdownNow();
 */
public class CallableTest implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("Callable創建子線程運行中"+i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableTest callableTest = new CallableTest();
        ExecutorService ser = Executors.newFixedThreadPool(1);
        Future<Boolean> result = ser.submit(callableTest);
        for (int i = 0; i < 1000; i++) {
            System.out.println("main線程運行中"+i);
        }
        boolean res = result.get();
        ser.shutdownNow();
    }
}

Thread方式與Runnable方式比較:

其一:

  • Runnable方式可以將程序代碼與數據進行有效的分離
  • Thread方式則代碼與數據具有較高的耦合性

其二:

  • Runnable方式可以避免由於Java單繼承所帶來的侷限性
  • Thread只能夠創建已繼承的打單個Thread方式

二、Lambda表達式:

​ 函數式接口:任何接口如果只包含唯一一個抽象方法,那麼它就是一個函數式接口

package threadTest;

/**
 * Lambda表達式
 * 前提條件需要一個函數式接口
 * 優點:避免內部類定義過多,簡化代碼
 * 僅留下核心邏輯
 */
public class LambdaTest {
    public static void main(String[] args) {
        LambdaInterface lambdaInterface;
        lambdaInterface =()->{
            System.out.println("Lambda表達式重寫使用測試");
        };
        lambdaInterface.run();
    }
}
interface LambdaInterface{
    void run();
}
class LambdaImpl implements LambdaInterface{
    @Override
    public void run() {
        System.out.println("Lambda表達式使用測試");
    }
}

三、線程狀態:

線程的生命週期和狀態轉換:

線程生命週期共有五個階段:

  • 新建狀態:新創建的對象所處狀態,此時不能運行,但是JVM爲其分配了內存,就和普通java對象一樣
  • 就緒狀態:線程對象調用start()方法後所處狀態,此時可運行進入可運行池中,等待CPU調度
  • 運行狀態:此時線程獲取CPU使用權,開始執行run()方法
  • 阻塞狀態:在某些特殊情況下會放棄CPU使用權,進入阻塞狀態
    • 舉例:
    • 當線程試圖獲取某個對象的同步鎖時,如果鎖被其他線程持有
    • 當線程調用阻塞式的IO方法時
    • 調用了某個對象的wait()方法
    • 調用了Thread的sleep()方法
    • 調用了另一個線程的join()方法
    • tip:線程只能從阻塞狀態到就緒狀態,不能直接進入運行狀態
  • 死亡狀態:線程run()方法執行完畢或拋出未捕獲的Exception、錯誤Error時線程就會進入死亡狀態。此時線程不可運行,也不可轉換到其他狀態

在這裏插入圖片描述

四、線程的調度:

在計算機中,線程調度有兩種模型:分時調度模型和搶佔式調度模型

分時調度:

線程輪流獲取CPU使用權,平均分配每個線程佔用的時間片

搶佔式調度:

讓可運行池中優先級較高的線程優先佔用CPU,若優先級相同則隨機選擇。JVM默認採用搶佔式調度

4.1 線程休眠

靜態方法sleep(long millis):

  • 該方法可以讓當前正在執行的線程暫停,進入休眠等待狀態
  • 該方法拋出InterruptedException異常,使用時需要拋出或捕獲
package ThreadMethodTest;

import threadTest.RunnableTest;

public class SleepTest implements Runnable{
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                if(i==50){
                        Thread.sleep(1000);
                }
                System.out.println("Runnable創建子線程運行中,50會休眠"+i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new SleepTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main線程正運行中"+i);
        }
    }
}

4.2 線程讓步:

靜態方法yield():

將當前正在執行的前程暫停,轉換爲就緒狀態,讓CPU重新調度一次

package ThreadMethodTest;

public class YieldTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable創建的子線程在運行"+i);
            if(i==50){
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new YieldTest());
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main線程正在運行"+i);
        }
    }
}

4.3 線程插隊:

join():

  • 在某個線程中調用其他線程的join()方法調用的線程將被阻塞,直到join()方法加入的線程執行完它纔會執行
  • 需要拋出或處理異常InterruptedException
package ThreadMethodTest;

public class JoinTest implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread().getName()獲取當前執行線程的名字
            System.out.println(Thread.currentThread().getName()+"正在執行中"+i);
            if(i==50){

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new JoinTest());
        thread1.start();
        for (int i = 0; i < 100; i++) {
            if(i==50){
                thread1.join();
            }
            System.out.println("main線程執行"+i);
        }
    }
}

五、多線程同步:

多線程可以提高程序的效率,但是也會引發一些安全問題。例如:售票時,如果多個線程同時取同一張票,就可能導致錯誤。

引發錯誤的代碼:

package synchronizedTest;


public class ErrorTest implements Runnable{
    private int tickets = 10;
    @Override
    public void run() {
        try {
            while (tickets>0){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"當前售出第"+(tickets--)+"張票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ErrorTest errorTest = new ErrorTest();
        Thread thread1 = new Thread(errorTest,"售票員1");
        Thread thread2 = new Thread(errorTest,"售票員2");
        Thread thread3 = new Thread(errorTest,"售票員3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

可以看出一張票可能被售出多次,甚至可能會出現票爲負數的情況。這是因爲當線程A正在取票,但是票的數量還未減1時,線程B也要取票,這樣就導致取出了重複的票,顯然這是不正確的,所以我們需要進行同步,來避免問題的出現

5.1 同步代碼塊:

保證共享資源在任何時刻都只能有一個線程訪問,Java提供了同步機制。當多個線程使用同一個共享資源時,可以將處理共享資源的代碼放置在一個代碼塊中,使用synchronized關鍵字修飾,稱爲同步代碼塊

synchronized(lock){
    //操縱共享資源的代碼塊
}

lock是一個鎖對象,它是同步代碼塊的關鍵。默認情況下lock爲1,表示可以訪問。如果當前有線程正在訪問共享資源,則lock爲0。不允許新的線程訪問共享資源,使新線程進入阻塞狀態。只有正在訪問的線程離開後lock會重新置爲1,允許訪問。

對上面的錯誤代碼進行修改:

package synchronizedTest;



public class ErrorTest_yes implements Runnable{
    private int tickets = 10;
    private Object lock = new Object();
    @Override
    public void run() {

        synchronized (lock){
            try {
                while (tickets>0) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName()
                            + "當前售出第" + (tickets--) + "張票");
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ErrorTest_yes yes = new ErrorTest_yes();
        Thread thread1 = new Thread(yes,"售票員A");
        Thread thread2 = new Thread(yes,"售票員B");
        Thread thread3 = new Thread(yes,"售票員C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

tip:同步代碼塊中lock對象可以是任意類型的對象,但是多個線程共享的對象必須是唯一的。lock對象的創建不能放在run方法中,這樣的話每一個線程都會擁有一個自己的鎖,無法起到同步作用。

5.2 同步方法:

被synchronized修飾的方法就是同步方法,可以實現與同步代碼塊相同的功能

//synchronized 返回值 方法名([參數1,...]){}

使用同步方法修改上面的錯誤代碼:

package synchronizedTest;



public class ErrorTest_yes implements Runnable{
    private int tickets = 10;
    private Object lock = new Object();
    @Override
    public void run() {
        saleTicket();
    }
    private synchronized void saleTicket(){
        try {
            while (tickets>0) {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()
                        + "當前售出第" + (tickets--) + "張票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ErrorTest_yes yes = new ErrorTest_yes();
        Thread thread1 = new Thread(yes,"售票員A");
        Thread thread2 = new Thread(yes,"售票員B");
        Thread thread3 = new Thread(yes,"售票員C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
  • 通過上面的代碼大家可以看出也實現了同步。但是同步方法沒有傳入lock對象啊,他是怎麼進行同步的呢?

答:其實同步方法的鎖就是this對象。例如上代碼,因爲同步方法是被線程共享的,所以所有的線程都使用同一個yes對象,自然也就可以使用this來保證同步效果

  • 那麼問題又來了?如果我們**用靜態同步方法呢?**這時候是沒有this的,他又是如何同步的呢?

答:靜態同步方法的鎖是靜態方法所在的類的Class對象。因爲在Java類加載機制中,類只被創建一次。所以也就可以被用來作爲鎖對象了

5.3 死鎖問題:

有這樣一個場景:一箇中國人和一個美國人在一起喫飯,美國人拿了中國人的筷子,
中國人拿了美國人的刀叉,兩個人開始爭執不休: .
中國人:“你先給我筷子,我再給你刀叉!”
美國人:“你先給我刀叉,我再給你筷子!”

結果可想而知:兩個人都喫不到飯。類似的問題還有哲學家就餐問題。有興趣大家可以自行了解。此處不做贅述

代碼模擬死鎖問題:

package synchronizedTest;

public class DeadLock implements Runnable {
    private static Object chopsticks = new Object();   //筷子的鎖
    private static Object knifeAndFork = new Object(); //刀叉的鎖
    private boolean flag;       //flag帶表是美國人還是中國人
    public DeadLock(boolean flag){
        this.flag = flag;
    }
    @Override
    public void run() {
        if(flag){   //當前說老美

                //筷子鎖對象上的同步代碼塊
                synchronized (chopsticks){
                    System.out.println("把叉子給我,我就把筷子給你");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (knifeAndFork){    //開始伸手要刀叉
                        System.out.println("雙標老美拿到刀叉");
                    }
                }

        }else{
                //刀叉對象的同步代碼塊
                synchronized (knifeAndFork) {
                    System.out.println("把筷子給我,我就把叉子給你");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (chopsticks) {
                        System.out.println("偉大的中國人民拿到筷子");
                    }
                }

        }
    }

    public static void main(String[] args) {
        DeadLock American = new DeadLock(true);
        DeadLock Chinese = new DeadLock(false);
        //創建並開啓兩個線程
        new Thread(American,"雙標美").start();
        new Thread(Chinese,"博愛中").start();
    }
}

由上面代碼可以看出雙方互不鬆手,程序陷入死鎖。所以在編程中我們需要避免死鎖問題的發生

5.6 多線程通信

經典例子:生產者和消費者問題。

假設有一個場景:有一個倉庫,生產者往裏面放貨物,消費者從裏面取貨物。如果倉庫滿了,如何讓生產者停下後通知消費者取貨。如果倉庫空了,如何停止取貨,讓消費者通知生產者生產?

代碼模擬一下:

package communicationTest;

/**
 * 定義一個倉庫類
 */
public class Storage {
    //數據存儲數組
    private int[] cells = new int[10];
    //inPos表示存入時數組下標,outPos表示取出時數組下標
    private int inPos;
    private int outPos;
    public void put(int num){
        cells[inPos] = num;
        System.out.println("在cells["+inPos+"]中放入數據--"+cells[inPos]);
        inPos++;
        //每當數據已經放滿就從0位置重新開始放數據
        if(inPos == cells.length){
            inPos=0;    //當inPos爲數組長度時,將其置爲0
        }
    }
    //定義一個get方法從數組中取出數據
    public void get(){
        int data = cells[outPos];
        System.out.println("在cells["+outPos+"]中取出數據--"+cells[outPos]);
        outPos++;   //取完讓元素位置++
        //每當數據已經取完就從0位置重新開始取數據
        if(outPos==cells.length){
            outPos=0;
        }
    }
}

生產者和消費者類:

package communicationTest;

/**
 * 生產者和消費者類
 * 生產者不斷生產
 * 消費者不斷消費
 */
class Input implements Runnable {
    private Storage st;
    private int flag=100;
    private int num;
    Input(Storage st){
        this.st = st;
    }
    @Override
    public void run() {
        while ((flag--)>0){
            st.put(num++);
        }
    }
}
class Output implements Runnable {
    private Storage st;
    private int flag=100;
    Output(Storage st){
        this.st = st;
    }
    @Override
    public void run() {
        while ((flag--)>0){
            st.get();
        }
    }
}
public class InputAndOutput{
    public static void main(String[] args) {
        //創建一個倉庫對象
        Storage st = new Storage();
        Input input = new Input(st);
        Output output = new Output(st);
        new Thread(input).start();
        new Thread(output).start();
    }
}

根據運行結果能夠發現,已經被放過數據還未被取出的位置又被重複放上數據,這是錯誤的。

那麼如何解決問題呢?

此時就需要讓線程之間彼此通信。Object類中提供了wait()、notify()、notifyAll()方法用於解決線程間的通信問題

  • wait():使當前線程放棄同步鎖並進入等待,直到其他線程進入此同步鎖,並調用notify()方法,或notifyAll()方法喚醒該線程爲止
  • notify():喚醒此同步鎖上等待的第一個調用wait()方法的線程
  • notifyAll():喚醒此同步鎖上調用wait()方法的所有線程

注意:以上三個方法的調用者都應該是同步鎖對象,如果不是則會拋出異常

對上面Storage代碼的修改

package communicationTest;

/**
 * 定義一個倉庫類
 */
public class Storage {
    //數據存儲數組
    private int[] cells = new int[10];
    //inPos表示存入時數組下標,outPos表示取出時數組下標
    private int inPos;
    private int outPos;
    private int count;  //存入或取出數據的數量
    public synchronized void put(int num) {
        if(count==cells.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cells[inPos] = num;
        System.out.println("在cells["+inPos+"]中放入數據--"+cells[inPos]);
        inPos++;
        count++;    //放入一個元素count++
        //如果已經放到最後一個位置,則從頭開始放。(模擬循環隊列)
        if (inPos==cells.length){
            inPos=0;
        }
        this.notify();
    }
    //定義一個get方法從數組中取出數據
    public synchronized void get() {
        if(count==0){   //如果已經全部取出
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        int data = cells[outPos];
        System.out.println("在cells["+outPos+"]中取出數據--"+data);
        cells[outPos]=-1;   //代表此處無元素
        outPos++;   //取完讓元素位置++
        count--;
        if(outPos==cells.length){
            outPos=0;
        }
        this.notify();  //表示倉庫已經可以放貨物,通知生產者生產
    }
}

此時便不會出現重複放入元素或重複取出元素的情況

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章