天天看點

Java多線程知識點全面總結 想學的進來啦!

文章目錄

    • 一、基本概念
    • 二、建立新的執行線程的方法
    • 三、線程的狀态
    • 四、線程常用方法
      • 示例
    • 五、線程排程
      • 常用方法
    • 六、線程安全
      • 1、線程同步機制
      • 2、死鎖
      • 3、守護線程
      • 4、定時器
      • 5、生産者消費者模式

一、基本概念

程序和線程的關系

  程序可以了解為應用程式,一個程式同時執行多個任務。通常,每一個任務稱為一個線程(

thread

)。比如一個團隊如果是一個“程序”,那麼團隊中的每個人都各司其職,就是多個“線程”。

  二者的本質差別在于程序之間記憶體獨立,互不影響;一個程序下的線程之間共享資料,即共享堆記憶體和方法區,但是棧記憶體是互相獨立的。

注釋

  

單核CPU

不能做到真正意義上的多線程并發,即一個時間點上隻能處理一件事情。但是CPU處理速度極快,可以在多個線程之間頻繁切換執行,給人的感覺是多線程并發。

二、建立新的執行線程的方法

1、将一個類聲明為

Thread

的子類。 這個子類應該重寫

Thread

類的

run()

方法,示例如下:

public class ThreadTest {
    public static void main(String[] args){
        CountThread countThread=new CountThread();
        //啟動分支程序
        countThread.start();
        //主線程的代碼
        for(int i=1;i<100;i++)
            System.out.println("主線程---"+i);
    }
}
//數數程序
class CountThread extends Thread{
    @Override
    public void run() {
        for(int i=1;i<100;i++)
            System.out.println("分支線程---"+i);
    }
}
           

結果

主線程和分支線程同時運作,控制台計數無規律,下面是截取的一段輸出:

分支程序---10
主程序---39
分支程序---11
           

注釋

  • start()

    方法啟動一個分支線程,在

    JVM

    中開辟一個新的棧空間,完成任務之後,瞬間就結束了,開始運作下一行代碼;
  • 如果沒有

    start()

    ,而是單純地調用

    countThread

    run()

    方法,就隻是普通的方法調用,分支線程并沒有被啟動;
  • run()

    中的異常隻能

    try...catch...

    捕獲,不能

    throws

    抛出。因為

    run()

    在父類中沒有抛出異常,子類就不能抛出更多的異常。

2、建立一個實作類

Runnable

接口的類,并且實作了

run()

方法。 然後在建立

Thread

時作為參數傳遞,并啟動。示例如下:

public class ThreadTest {
    public static void main(String[] args){
        //建立分支程序
        Thread aThread=new Thread(new CountThread());
        //啟動分支程序
        aThread.start();
        //主線程的代碼
        for(int i=1;i<100;i++)
            System.out.println("主程序---"+i);
    }
}
//數數程序
class CountThread implements Runnable{
    @Override
    public void run() {
        for(int i=1;i<100;i++)
            System.out.println("分支程序---"+i);
    }
}
           

注釋

由于Java是單繼承,一旦用第一種方法建立程序,以後再繼承其他類就不友善,是以第二種方法相對來說靈活一些。

3、建立一個實作

Callable<V>

接口的類,好處在于可以擷取接口中

call

方法的傳回值,但是後續的代碼必須要等待call方法執行完畢之後才能進行。

三、線程的狀态

Java多線程知識點全面總結 想學的進來啦!

四、線程常用方法

方法 解釋

static Thread currentThread()

傳回對目前正在執行的線程對象的引用

String getName()

傳回此線程的名稱

void setName(String name)

将此線程的名稱更改為等于參數

name

static void sleep(long millis)

使目前正在執行的線程以指定的毫秒數暫停(暫時停止執行),具體取決于系統定時器和排程程式的精度和準确性

void interrupt()

中斷這個線程,即讓正在sleep的程序提前解除阻塞

注釋

  1. currentThread()

    的傳回值與

    Thread.currentThread

    調用的位置有關,如果在

    main()

    所在的類中調用,傳回的是主線程;如果在繼承

    Thread

    的類中調用,傳回的就是該類所對應的分支線程。
  2. 線程對象建立時的預設名稱是

    Thread-0、Thread-1...

  3. 主線程的預設名字是

    main

  4. sleep()

    作用是讓目前程序進入阻塞狀态,放棄占有的CPU時間片。寫有

    Thread.sleep(long millis)

    的線程進行休眠,通常用來間隔時間執行代碼。
  5. interrupt()

    運用了Java的異常處理機制,調用後會讓

    sleep()

    try...catch...

    捕獲異常,然後接着執行

    run()

    中的代碼,達到解除阻塞的目的。

示例

1、對sleep方法的了解

public class ThreadTest {
    public static void main(String[] args){
        //建立分支程序
        Thread countThread=new CountThread();
        //啟動分支程序
        countThread.start();
        //分支程序會阻塞2s嗎?
        try {
            countThread.sleep(2*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //主線程的代碼
        for(int i=1;i<100;i++)
            System.out.println("主程序---"+i);
    }
}
//數數程序
class CountThread extends Thread{
    @Override
    public void run() {
        for(int i=1;i<100;i++)
            System.out.println("分支程序---"+i);
    }
}
           

countThread.sleep(2*1000)

會自動轉化為

Thread.sleep(2*1000)

,因為

sleep()

是靜态方法,最終還是主線程會進入阻塞态2秒。想要

countThread

阻塞,就必須在

CountThread

類中寫入

sleep()

2、終止線程的合理方式

public class ThreadTest {
    public static void main(String[] args){
        //建立分支程序
        CountThread countThread=new CountThread();
        //啟動分支程序
        countThread.start();
        //主線程的代碼
        //想讓分支線程在2秒後終止
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //改變标志位
        countThread.flag=false;
    }
}
//數數程序
class CountThread extends Thread{
	//設定标志位
    Boolean flag=true;
    @Override
    public void run() {
        for(int i=1;i<5;i++){
            if(flag){
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            else{
                //到這裡線程終止
                //可以寫一些終止之後的代碼放在這裡,以免資料丢失
            }
        }
    }
}
           

強行終止線程的方法如

countThread.stop()

,有一個緻命的缺點就是容易導緻資料丢失,不建議使用!

五、線程排程

  線程排程是指按照特定機制為多個線程配置設定CPU的使用權。Java中采用搶占式排程模型,每當線程排程器有機會選擇新線程時,首先選擇具有較高優先級的線程,即這些線程搶到CPU時間片的機率就高一些。

常用方法

方法 解釋

int getPriority()

傳回此線程的優先級

void setPriority(int newPriority)

更改此線程的優先級

static void yield()

對排程程式的一個暗示,即讓目前線程重新進入就緒态

void join()

合并線程

注釋

  1. 優先級範圍

    [1,10]

    ,預設優先級是5。優先級高的線程搶到CPU時間片的機率更高,程式運作時大機率偏向優先級高的線程。
  2. yield()

    不是讓線程進入阻塞态,而是從運作态回到就緒态,進而重新搶奪CPU時間片。同樣地,

    Thread.yield()

    要寫到對應的線程中才會起作用。
  3. join()

    方法讓目前線程進入阻塞狀态,直到調用

    join()

    的線程結束為止,比如在

    main()

    方法中如果有其它線程調用這個方法,主線程就會受阻。

六、線程安全

1、線程同步機制

  資料在多線程并發的環境下會存在安全問題,因為共享資料的緣故,當多個線程對資料做出修改時,資料的值變化量就無法控制了。例如:

//多個老師對學生試卷做出評判,在100分的基礎上往下扣
//資料類
public class Paper{
    private String name;
    private int scores;
    public Paper(String name,int scores){
        this.name=name;
        this.scores=scores;
    }
    //得到姓名
    public String getName() {
        return name;
    }
    //得到分數
    public int getScores() {
        return scores;
    }
    //設定分數
    public void setScores(int scores) {
        this.scores = scores;
    }
    //修改試卷
    public void modify(int deduction){
        int begin=getScores();
        int end=begin-deduction;
        //模拟網絡延遲
        try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        setScores(end);
    }
}   
//分支線程類
public class PaperThread extends Thread{
    private Paper paper;
    public PaperThread(Paper paper){
        this.paper=paper;
    }
    @Override
    public void run() {
        paper.modify(10);
        System.out.println(Thread.currentThread().getName()+"扣了"+paper.getName()+"10分,最終得分"+paper.getScores());
    }
}
//測試類
public class TestPaper {
    public static void main(String[] args){
        Paper aPaper=new Paper("小明",100);
        PaperThread thread0=new PaperThread(aPaper);
        PaperThread thread1=new PaperThread(aPaper);
        thread0.start();
        thread1.start();
    }
}
           

  測試過程中學生的最終分數随着測試次數的增加不盡相同,可能會出現一個老師扣了10分,最終學生卻得到了80分。如果用sleep模拟網絡延遲,修改分數一定跟預期不符。

  解決線程安全問題可采用“線程同步機制”—取消線程的并發執行,即必須排隊修改資料。對應同步程式設計模型,即一個線程執行的時候,必須要等待另一個線程結束,兩個線程間産生了等待的關系。雖然降低了效率,但是保證了資料的安全。

解決辦法

//修改試卷
    public void modify(int deduction){
        synchronized (this) {
            int begin = getScores();
            int end = begin - deduction;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setScores(end);
        }
    }
           

注釋

  1. synchronized ()

    括号裡的參數一定是需要排隊執行的線程所共享的資料,這裡用this就表明測試用例中需要完成同步的是paper對象;
  2. 執行原理:在Java中,每個對象都有一把鎖,執行到

    synchronized (this){ }

    中同步代碼塊的程式,先執行的線程拿到對象鎖,此時其它線程無法進入,直到對象鎖被釋放;
  3. public synchronized void modify(int deduction)

    也是可以的,隻不過預設鎖this對象,同步整個方法體,有可能降低效率,但代碼簡潔。靜态方法是類鎖,所有的類對象共同擁有這把鎖;
  4. 局部變量不會有線程安全問題,因為它存在于每個線程的棧中。相反地,存在于方法區中的靜态變量和存在于堆中的執行個體變量則存線上程安全問題。

2、死鎖

  一般嵌套寫

synchronized

會産生這樣一個現象:兩個線程都需要對方釋放各自需要的對象鎖才能繼續運作,但是雙方互不讓步,導緻程式癱瘓。

public class DeadLock {
    public static void main(String[] args){
        Object obj1=new Object();
        Object obj2=new Object();
        Thread thread1=new TestThread1(obj1,obj2);
        Thread thread2=new TestThread2(obj1,obj2);
        thread1.start();
        thread2.start();
    }
}
class TestThread1 extends Thread {
    private Object obj1;
    private Object obj2;
    public TestThread1(Object obj1, Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }
    @Override
    public void run() {
    	//擁有obj1的對象鎖
        synchronized (obj1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //需要得到obj2的對象鎖
            synchronized (obj2) {
            }
        }
    }
}
class TestThread2 extends Thread {
    private Object obj1;
    private Object obj2;
    public TestThread2(Object obj1, Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }
    @Override
    public void run() {
   		//擁有obj2的對象鎖
        synchronized (obj2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //需要得到obj2的對象鎖
            synchronized (obj1) {
            }
        }
    }
}
           

3、守護線程

  Java中的線程分為使用者線程和守護線程(背景線程)。一般守護線程是一個死循環,隻要所有的使用者線程結束,守護線程就會自動結束,如垃圾回收線程。

Thread

類中的

void setDaemon(boolean on)

用來設定線程的類型。

4、定時器

實際應用中,可能需要定時執行一些任務,如資料的備份,是以就需要用到定時器。示例如下:

//功能:每隔3秒列印目前時間
public class TestTimer {
    public static void main(String[] args) throws ParseException {
        //設定開始日期
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date begintime=sdf.parse("2020-04-23 22:12:00");
        //設定定時器
        Timer clockTimer=new Timer();
        clockTimer.schedule(new Clock(),begintime,1000*3);
    }
}
//需要完成的定時任務
class Clock extends TimerTask{
    @Override
    public void run() {
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String nowtime=sdf.format(new Date());
        System.out.println("目前時間:"+nowtime);
    }
}
           

5、生産者消費者模式

生産者消費者模式是多線程程式設計中非常重要的設計模式,生産者負責生産資料,消費者負責消費資料。需要用到Object類的兩個方法:

  1. void wait()

    :讓目前占有使用該方法的對象的程序進入等待狀态,并且釋放線程占有的對象鎖;
  2. void notify()

    :喚醒在目前對象上活動且進入等待狀态的線程,但并不會釋放目前線程占有的對象鎖。
public class TestMode {
    public static void main(String[] args){
        List warehouse=new ArrayList();
        Thread produceThread=new Thread(new Produce(warehouse));
        Thread conductThread=new Thread(new Conduct(warehouse));
        produceThread.start();
        conductThread.start();
    }
}
//生産者
class Produce implements Runnable{
    private List warehouse;
    public Produce(List warehouse){
        this.warehouse=warehouse;
    }
    @Override
    public void run() {
        while(true){
            synchronized (warehouse){
                //倉庫滿了之後喚醒消費者
                if(warehouse.size()>3){
                    try {
                        warehouse.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //搶到鎖而且倉庫不滿時生産,并喚醒消費者
                else{
                    Object obj=new Object();
                    warehouse.add(obj);
                    System.out.println("生産一個,倉庫存貨:"+warehouse.size());
                    warehouse.notify();
                }
            }
        }
    }
}
//消費者
class Conduct implements Runnable{
    private List warehouse;
    public Conduct(List warehouse){
        this.warehouse=warehouse;
    }
    @Override
    public void run() {
        while(true){
            synchronized (warehouse){
                //消費完了之後喚醒生産者
                if(warehouse.size()==0){
                    try {
                        warehouse.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //搶到鎖并且有庫存時消費,并喚醒生産者
                else{
                    warehouse.remove(warehouse.size()-1);
                    System.out.println("消費一個,倉庫存貨:"+warehouse.size());
                    warehouse.notify();
                }
            }
        }
    }
}
           

碼字不易,還請多多支援,可評論交流~👍