[Java]多線程與併發

1.進程與線程

1.1 進程

程序是指令和數據的有序集合,其本身沒有任何運行的含義,是一個靜態的概念。而進程是程序在處理機上的一次執行過程,它是一個動態的概念。進程是一個具有一定獨立功能的程序,一個實體,每一個進程都有它自己的地址空間。

1.2 進程的狀態

進程執行時的間斷性,決定了進程可能具有多種狀態,有以下三種:

  • 就緒狀態 Ready
  • 運行狀態 Running
  • 阻塞狀態 Blocked

在這裏插入圖片描述

1.3 線程

線程實際上是在進程基礎之上的進一步劃分,一個進程啓動之後,裏面的若干程序又可以劃分成若干個線程。線程:是進程中的一個執行路徑,共享一個內存空間,線程之間可以自由切換,併發執行,一個進程最少有一個線程(單線程程序)

一個程序可以同時執行多個任務,來提高效率

  • 並行:就是兩個任務同時運行(多個cpu)
  • 併發:是指兩個任務同時請求運行,而處理器一次只能接受一個任務,就會把兩個任務安排輪流執行,由於CPU時間片運行時間較短,就會感覺兩個任務在同時執行。

2.線程的基本使用

實現線程的兩種方式:

  • 繼承Thread類
  • 實現Runnable接口(推薦,因爲功能封裝在接口裏,更加靈活)
public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
    }
}

3.線程休眠

public static void sleep(long millis) 使當前正在執行的線程以指定的毫秒數暫停,釋放cpu時間片,具體取決於系統定時器和調度程序的精度和準確性。線程不會丟失任何顯示器的所有權。

如果millis爲負數,會拋出IllegalArgumentException異常

在使用sleep()進行休眠的時候,此時如果任何線程中斷當前線程(使用interrupt函數),sleep()會拋出異常InterruptedException,當前線程的中斷狀態將被清除。

public static void sleep(long millis, int nanos) 參數是毫秒,納秒

static Thread currentThread() 返回對當前正在執行的線程對象的引用

4.join與中斷線程

  • public final join() throws InterruptedException

    該方法等待此線程死亡,即主進程調用子線程的這個方法時,會等待子線程執行完畢,主進程纔會繼續往下執行。

  • public void interrupt()

    中斷這個線程,只是爲該線程做了一箇中斷標記,要不要中斷還是該線程自己決定,如果該線程正在sleep,則無法中斷,拋出InterruptedException異常。

  • public static boolean interrupted()

    測試當前線程是否中斷。該方法可以清除線程的中斷狀態,即如果該方法被連續調用兩次,那麼第二次調用將返回false(除非當前線程再次中斷,第一個調用已經清除其中斷狀態後)

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable2 mr2 = new MyRunnable2();
        Thread t = new Thread(mr2);
        t.start();
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
                if(i==5){
//                    t.join();
                    t.interrupt();//中斷線程,只是做了中斷標記
                    //此時只會拋出異常,不會中斷t線程
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
class MyRunnable2 implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

上面的例子會由於t線程在休眠中,無法進行中斷操作,所以會拋出異常。下面的例子則正確展示瞭如何解決這種問題:

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable2 mr2 = new MyRunnable2();
        Thread t = new Thread(mr2);
        t.start();
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
                if(i==5){
//                    t.join();
                    t.interrupt();//中斷線程,只是做了中斷標記
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
class MyRunnable2 implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            if(Thread.interrupted()){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        }

    }
}

在t線程裏拋出異常後,會在catch裏打上中斷標記,那麼在下一次循環裏,會被Thread.interrupted()獲取到,返回true,從而正確中斷循環。這樣會比較麻煩,建議使用自定義中斷標記

class MyRunnable3 implements Runnable{
    private boolean flag;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public MyRunnable3(){
        flag=true;
    }
    @Override
    public void run() {
        while (flag){
            
        }
    }
}

5.守護線程與yield

Java程序入口就是由JVM啓動main線程,main線程又可以啓動其他線程。當所有線程都運行結束時,JVM退出,進程結束。

如果有一個線程沒有退出,JVM進程就不會退出。所以,必須保證所有線程都能及時結束。

但是有一種線程的目的就是無限循環,例如,一個定時觸發任務的線程:

class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果這個線程不結束,JVM進程就無法結束。問題是,由誰負責結束這個線程?

然而這類線程經常沒有負責人來負責結束它們。但是,當其他線程結束時,JVM進程又必須要結束,怎麼辦?

答案是使用守護線程(Daemon Thread)。

守護線程是指爲其他線程服務的線程。在JVM中,所有非守護線程都執行完畢後,無論有沒有守護線程,虛擬機都會自動退出。

因此,JVM退出時,不必關心守護線程是否已結束。

在守護線程中,編寫代碼要注意:守護線程不能持有任何需要關閉的資源,例如打開文件等,因爲虛擬機退出時,守護線程沒有任何機會來關閉文件,這會導致數據丟失。

** 引用自:廖雪峯的官方網站

方法 介紹
public final void setDaemon(boolean on) 將此線程標記爲deamon線程或用戶線程。
public final boolean isDaemon() 測試這個線程是否爲守護線程
public static void yield() 暫停當前正在執行的線程對象,並執行其他線程。
public class ThreadDemo3 {
    public static void main(String[] args) {
        MyRunnable4 mr4 = new MyRunnable4();
        Thread t = new Thread(mr4);
        t.setName("Thread-t");
        //優先級高可以提高該線程搶點CPU時間片的概率
        t.setPriority(Thread.MAX_PRIORITY);
        
        //線程可以分爲守護線程和用戶線程,當進程中沒有用戶線程時,jvm會退出
        t.setDaemon(true);//把線程設置爲守護線程
        System.out.println(t.isAlive());
        t.start();
        System.out.println(t.isAlive());

        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(i==5){
                Thread.yield();//讓出本次cpu時間片
            }
        }
    }
}
class MyRunnable4 implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.線程同步

7.1 多線程共享數據

在多線程的操作中,多個線程有可能同時處理同一個資源,這就是多線程的共享數據。

7.2 線程同步

解決數據共享問題,必須使用同步,所謂同步就是指多個線程在同一個時間段內只能有一個線程執行指定代碼,其他線程需要等待此線程完成之後纔可以繼續執行。犧牲性能換取安全,所以同步的代碼塊儘量保持簡短。

線程同步的方法:

  • 同步代碼塊

    synchronized(要同步的對象){

    ​ 要同步的操作;

    }

  • 同步方法

    public synchronized void method(){

    ​ 要同步的操作;

    }

  • Lock(ReentrantLock)

7.3 同步準則

  • 使代碼塊保持簡短,把不隨線程變化的預處理和後處理移除synchronized塊
  • 不要阻塞,如InputStream.read()
  • 在持有鎖時,不要對其他對象調用方法
public class ThreadDemo4 {
    public static void main(String[] args) {
        MyRunnable5 mr5 = new MyRunnable5();
        Thread t1 = new Thread(mr5);
        Thread t2 = new Thread(mr5);
        t1.start();
        t2.start();
    }
}

class MyRunnable5 implements Runnable{
    private int ticket=10;//售票
    //public Object obj = new Object();
    @Override
    public void run() {
        for(int i=0;i<20;i++){
                synchronized (this){//可以用obj
                    if(ticket>0){
                        ticket--;
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("您購買的票已剩餘"+ticket+"張");
                    }
                }
                method();
        }
    }
    private synchronized void method(){
        if(ticket>0) {
            ticket--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("您購買的票已剩餘" + ticket + "張");
        }
    }
    //lock實現同步
    //互斥鎖
    ReentrantLock lock = new ReentrantLock();
    private void method2(){
        lock.lock();//鎖
        if(ticket>0) {
            ticket--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("您購買的票已剩餘" + ticket + "張");
        }
        lock.unlock();//釋放鎖
    }
}

8.死鎖

多線程中要進行資源的共享,就需要同步,但同步過多,就可能造成死鎖。

Java多線程,死鎖

9.生產者與消費者案例

生產者不斷生產產品,消費者不斷取走產品

public class ProducterCustomerDemo {
    public static void main(String[] args) {
        Food food = new Food();
        Producter p = new Producter(food);
        CustomerR c = new CustomerR(food);
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();

    }
}
//生產者
class Producter implements Runnable{
    private Food food;

    public Producter(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(i%2==0){
                food.set("鍋包肉","酸甜口味");
            }else {
                food.set("佛跳牆","xxxxxxx");
            }
        }
    }
}
//消費者
class CustomerR implements Runnable{
    private Food food;

    public CustomerR(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for(int i=0;i<20;i++){
            food.get();
        }
    }
}


class Food{
    private String name;
    private String desc;
    private boolean flag = true;//true表示可以生產,false表示不能生產
    //有了flag能使兩個線程協同工作,先生產,再消費,交替進行。
    //sleep:讓線程進入休眠,讓出cpu的時間片,不釋放對象監視器的所有權(對象鎖)
    //wait: 讓線程進入等待狀態,讓出cpu的時間片,並釋放對象監視器的所有權,等待其他線程通過notify方法來喚醒
    //生產產品
    public synchronized void set(String name,String desc){
        if(!flag){
            //不能生產
            try {
                this.wait();//線程進入等待狀態,釋放監視器的所有權(對象鎖)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.setName(name);
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setDesc(desc);
        flag=false;
        this.notify();//喚醒等待的線程(隨機的一個)
    }
    //獲得產品
    public synchronized void get(){
        if(flag){
            //不能消費
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.getName()+" - "+this.getDesc());
        flag = true;
        this.notify();
    }


    public Food(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
    public Food() {
    }

    @Override
    public String toString() {
        return "Food{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

10.線程的生命週期

在這裏插入圖片描述

11.線程池

線程池是預先創建線程的一種技術,在還沒有任務到來之前,創建一定數量的線程,放入空閒隊列中,然後對這些資源進行復用,減少頻繁地創建和銷燬對象。

java裏線程池的頂級接口是Executor,是一個執行線程的工具,線程池接口是ExecutorService。

java.util.concurrent包:併發編程中很常用的實用工具類

Executor接口:執行已提交的Runnable任務對象

ExecutorService接口:Executor提供了管理終止的方法,以及可以爲跟蹤一個或多個異步任務執行狀況而生成Future的方法。

Executors類:此包中所定義的Executor、ExecutorService等的工廠和實用方法。

四種創建線程池的方法:

  • newSingleThreadExecutor:

    創建一個單線程的線程池。只有一個線程在工作,串行執行所有任務。如果這個唯一的線程因爲異常而結束,則會有一個新的線程來代替它。此線程池能保證所有任務的執行順序按照任務的提交順序執行

  • newFixedThreadPool:

    創建固定大小的線程池,每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值則保持不變,如果某個線程因執行異常而結束,那麼線程池會補充一個新線程。

  • newCachedThreadPool:

    創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,又可以智能地添加新線程來處理任務,此線程池不會對線程池大小做限制,完全依賴於操作系統(或者說jvm)能夠創建的最大線程大小

  • newScheduledThreadPool:

    創建一個大小無限的線程池,支持定時以及週期性執行任務的需求。需要傳一個代表線程池初始大小的變量作爲參數。

public class ThreadDemo5 {
    public static void main(String[] args) {
        //創建線程池的四種方法
        //創建一個單線程的線程池
//        ExecutorService es = Executors.newSingleThreadExecutor();
        //創建一個固定大小的線程池
//        ExecutorService es = Executors.newFixedThreadPool(2);
        ExecutorService es = Executors.newCachedThreadPool();
        es.execute(new MyRunnable6());
        es.execute(new MyRunnable6());
        es.shutdown();

        //延遲2秒執行
        ScheduledExecutorService es2 = Executors.newScheduledThreadPool(3);
        es2.schedule(new MyRunnable6(),9000, TimeUnit.MILLISECONDS);
        es2.shutdown();
    }
}
class MyRunnable6 implements  Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--"+i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章