Java多執行緒學習

平遙發表於2020-11-20

多執行緒基礎知識

執行緒與程式

程式:每個程式都擁有獨立的記憶體空間,程式切換有較大的開銷,一個程式包含1-n個執行緒(是資源分配的最小單位)
執行緒:同一程式的執行緒共享記憶體空間,每個執行緒有獨立的執行棧和程式計數器,執行緒切換開銷小(是CPU排程的最小單位)

同步與非同步

同步: 排隊執行 , 效率低但是安全.
非同步: 同時執行 , 效率高但是資料不安全.

併發與並行

併發: 指兩個或多個事件在同一個時間段內發生。
並行: 指兩個或多個事件在同一時刻發生(同時發生)。

執行緒的建立

執行緒的建立一般有兩種方法,一是繼承java.lang.Thread類,二是實現java.lang.Runnable介面。(其實還有第三種實現Callable介面)。

繼承Thread

(1)只使用一條執行緒的話,可以通過繼承java.lang.Thead類來建立執行緒,語法如下:
建立MyThread繼承Thread類:

/**
 * 繼承Thread
 */
public class MyThread extends Thread{
    /**
     * run方法是執行緒要執行的任務方法
     * 觸發條件時通過Thread物件的start()來啟動任務
     */
    @Override
    public void run() {
        //這裡的程式碼是一條新的執行路徑
        System.out.println("這是一條執行緒執行的程式碼");
    }
}

在main方法呼叫start()方法:

        //繼承Tread
        MyThread m = new MyThread();
        m.start();
實現Runnable介面

(2)需要多個執行緒同時執行任務的就需要實現java.lang.Runnable介面。語法如下:
建立MyRunnable繼承Runnable方法:

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //執行緒的任務
        System.out.println("這是一個任務");
        
    }
}

在main方法建立任務物件,再建立Thread並傳入任務,然後啟動:

        
        //1.    建立一個任務物件
        MyRunnable r = new MyRunnable();
        //2.    建立一個執行緒,併為其分配一個任務
        Thread t = new Thread(r);
        //3.    執行這個執行緒
        t.start();
 

相比於繼承Thread,實現Runnable有如下優點

  • 通過建立任務,然後給執行緒分配任務的方式實現多執行緒,更適合多個執行緒同時執行任務的情況
  • 可以避免單繼承所帶來的侷限性
  • 任務與執行緒是分離的,提高了程式的健壯性
  • 執行緒池技術中,接受Runnable型別的任務,不接受Thread型別的執行緒
實現Callable介面

(3)實現Callable介面
使用語法:


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
* Callable的使用
*/
public class demo2 {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
      //1.    編寫類實現Callable介面,實現call方法
      Callable<Integer> call = new MyCallable();
      //2.    建立FutureTask物件,傳入第一步的Callable物件
      FutureTask<Integer> task = new FutureTask<>(call);
      //3.    通過Thread類,啟動執行緒
      new Thread(task).start();
      //呼叫FutureTask的get()方法會阻塞主執行緒,先執行Callable再執行主執行緒
      Integer num = task.get();
      //task.isDone();//判斷子執行緒是否執行完畢
      //task.cancel(true);//取消任務
      System.out.println(num);
      for (int i=0;i<10;i++){
          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("我是主執行緒"+i);
      }

  }


  static class MyCallable implements Callable<Integer>{
      @Override
      public Integer call() throws Exception {
          for (int i=0;i<10;i++){
              Thread.sleep(100);
              System.out.println("我是Callable執行緒"+i);
          }
          return 100;
      }
  }
}

Callable和Runnable的相同點與不同點

相同點:

  • 都是介面
  • 都可以編寫多執行緒程式
  • 都採用Thread.start()啟動執行緒

不同點:

  • Runnable沒有返回值;
  • Callable可以返回執行結果
  • Callable介面的call()允許丟擲異常;
  • Runnable的run()不能丟擲
  • Callable獲取返回值
  • Callalble介面支援返回執行結果,需要呼叫FutureTask.get()得到,此方法會阻塞主程式的繼續往下執行,如果不呼叫不會阻塞。

執行緒的六種狀態轉換

JDK11API文件說明執行緒有以下6種狀態

  • 初始(NEW) :尚未啟動的執行緒處於此狀態。
  • 執行(RUNNABLE) :在Java虛擬機器中執行的執行緒處於此狀態(包括等待CPU時間片(READY)的執行緒和執行中(RUNNING)的執行緒)。
  • 阻塞(BLOCKED) :被阻塞等待監視器鎖定的執行緒處於此狀態。
  • 等待(WAITING) :無限期等待另一個執行緒執行特定操作的執行緒處於此狀態。(被休眠)
  • 超時等待(TIMED_WAITING) :正在等待另一個執行緒執行最多指定等待時間的操作的執行緒處於此狀態。(休眠指定時間)
  • (終止)TERMINATED :已退出的執行緒處於此狀態。

執行緒狀態轉換圖:
來源於http://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html

引用自http://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html
執行緒常用方法:

方法作用
long getId()返回此Thread的識別符號
String getName()返回此執行緒的名稱
int getPriority()返回此執行緒的優先順序
void interrupt()中斷此執行緒(只是中斷的訊號)
static boolean interrupted()測試當前執行緒是否已被中斷
void setDaemon(boolean on)引數為true將此執行緒標記為守護執行緒
static void sleep(long millis)令當前正在執行的執行緒休眠(暫時停止執行)指定的毫秒數
static void sleep(long millis, int nanos)令當前正在執行的執行緒休眠(暫時停止執行)指定的毫秒數加上指定的納秒數
static void yield()暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。
void join()等待這個執行緒執行完畢
void join​(long millis)此執行緒等待 millis毫秒
void join​(long millis, int nanos)此執行緒等待 millis毫秒加上 nanos納秒
void start()令此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法
void setName(String name)將此執行緒的名稱更改為等於引數 name
void setPriority(int newPriority)更改此執行緒的優先順序
static Thread currentThread()返回對當前正在執行的執行緒物件的引用

執行緒的排程

1.執行緒的優先順序範圍是整數1~10,預設優先順序是5。
優先順序越高不一定先執行,只是搶到時間片的概率高一點。執行緒優先順序具有繼承關係,如果A執行緒中建立了B執行緒,則B執行緒擁有和A執行緒一樣的優先順序。
Thread類有以下三個欄位描述執行緒優先順序。

變數和型別欄位描述
static intMAX_PRIORITY執行緒可以擁有的最大優先順序,取值為10
static intMIN_PRIORITY執行緒可以擁有的最低優先順序,取值為1
static intNORM_PRIORITY分配給執行緒的預設優先順序,取值為5

2.執行緒等待:Object類的wait()方法。需要依賴synchronized關鍵字。會釋放鎖。
3.執行緒喚醒:Object類的notify()和notifyAll()方法。需要依賴synchronized關鍵字。notify()喚醒此物件下的任意一個執行緒,notifyAll()喚醒當前物件下的所有執行緒。
3.執行緒休眠:Thread類的sleep()方法,讓執行緒休眠指定時間,期間不會釋放鎖,不依賴synchronized關鍵字
4.執行緒讓步:Thread類的yield()方法,暫停當前執行緒,讓步給同級別或高階別執行緒。
5.執行緒加入:Thread類的join()方法,等待該執行緒執行完畢。

守護執行緒

執行緒分為守護執行緒和使用者執行緒
使用者執行緒: 當一個程式不包含任何的存活的使用者執行緒時,進行結束
守護執行緒: 守護使用者執行緒的,當最後一個使用者執行緒結束時,所有守護執行緒自動死亡。
通過Thread類的setDaemon()方法可以設定當前執行緒為守護執行緒。

執行緒同步

實現執行緒同步可以通過鎖機制來實現,鎖又分為隱式鎖和顯式鎖。

隱式鎖

隱式鎖是靠synchronized關鍵字完成的,一共有2種使用方式,分別是同步程式碼塊和同步方法。

同步程式碼塊

下面通過一段程式碼演示同步程式碼塊的使用:

public class Demo8 {
    public static void main(String[] args) {
        //格式:synchronized(鎖物件){
        //
        //
        //      }
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //總票數
        private int count = 10;
        private Object o = new Object();//成員變數才是同一把鎖
        @Override
        public void run() {
            //Object o = new Object();    //區域性變數則是每人一把鎖。這裡不是同一把鎖,所以鎖不住
                while (true) {
                    synchronized (o) {//鎖住if判斷語句
                        if (count > 0) {
                         //賣票
                            System.out.println("正在準備賣票");
                            try {
                            Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            e.printStackTrace();
                            }
                            count--;
                            System.out.println(Thread.currentThread().getName()+"賣票結束,餘票:" + count);
                        }else {
                            break;
                        }

                }
           }
        }
    }
}

同步程式碼塊的使用首先需要建立一個鎖物件,需要主要建立的位置,再鎖住需要同步的程式碼部分就可以實現同步效果。

同步方法

依然通過一段程式碼演示同步方法的使用


//執行緒同步synchronized

public class Demo9 {
    public static void main(String[] args) {
      
   
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //總票數
        private int count = 10;
        @Override
        public void run() {

            while (true) {
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }
        }
        public synchronized boolean sale(){
            if (count > 0) {
                //賣票
                System.out.println("正在準備賣票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName()+"賣票結束,餘票:" + count);
                return true;
            }
                return false;

        }
    }
}

使用同步方法只需要再方法的許可權修飾符後面加上synchronized關鍵字就可以完成同步效果,需要注意的是非靜態同步方法則呼叫this作為鎖物件。靜態同步方法呼叫類.class作為鎖物件。

顯式鎖

顯示鎖使用的是Lock類的子類ReentrantLock,lock()方法上鎖,unlock方法解鎖。依然通過下面程式碼進行演示。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class Demo10 {
    public static void main(String[] args) {
      
        // 顯示鎖  Lock  子類 ReentrantLock

        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //總票數
        private int count = 10;
        //引數為true表示公平鎖    預設是false 不是公平鎖
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                l.lock();
                    if (count > 0) {
                        //賣票
                        System.out.println("正在準備賣票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"賣票結束,餘票:" + count);
                    }else {
                        break;
                    }
                    l.unlock();
            }
        }
    }
}

關於公平鎖和非公平鎖,隱式鎖和顯式鎖都預設使用非公平鎖。
公平鎖:執行緒按照它們發出請求的順序獲取鎖。
非公平鎖:可以“插隊”,哪個執行緒搶到就是哪個執行緒的。

Java的四種執行緒池

如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒 就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒需要時間. 執行緒池就是一個容納多個執行緒的容器,池中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,節省了大量的時間和資源。

執行緒池的好處:

  • 降低資源消耗。
  • 提高響應速度。
  • 提高執行緒的可管理性。
快取執行緒池

特點:無長度限制

執行流程:
A:判斷執行緒池是否存在空閒執行緒
B:存在則使用
C:不存在則建立執行緒並使用

建立語法:

      //建立快取執行緒池
      ExecutorService service = Executors.newCachedThreadPool();
      //指揮執行緒池執行新的任務
      service.execute(new Runnable() {
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName()+"鋤禾日當午");
          }

      });
定長執行緒池

特點:長度指定

執行流程:
A: 判斷執行緒池是否存在空閒執行緒
B: 存在則使用
C:不存在空閒執行緒,且執行緒池未滿的情況下,則建立執行緒,並放入執行緒池, 然後使用
D:不存在空閒執行緒,且執行緒池已滿的情況下,則等待執行緒池存在空閒執行緒單執行緒執行緒池

建立語法:

        ExecutorService service = Executors.newFixedThreadPool(2);//指定執行緒池長度
         //指揮執行緒池執行新的任務
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"鋤禾日當午");
            }

單執行緒執行緒池

特點:長度為1的定長執行緒池

執行流程:
A:判斷執行緒池的那個執行緒是否空閒
B :空閒則使用
C :不空閒則等待空閒後使用

建立語法:

        ExecutorService service = Executors.newSingleThreadExecutor();//建立單執行緒執行緒池
        //執行任務
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"鋤禾日當午");
            }
        });
週期性任務定長執行緒池

有定時執行任務和週期執行任務兩種方式

//建立週期性任務定長
 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //定時執行一次
        //引數1:定時執行的任務
        //引數2:時長數字
        //引數3:2的時間單位    Timeunit的常量指定
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"鋤禾日當午");
            }
        },5, TimeUnit.SECONDS);      //5秒鐘後執行

        /*
        週期性執行任務
            引數1:任務
            引數2:延遲時長數字(第一次在執行上面時間以後)
            引數3:週期時長數字(沒隔多久執行一次)
            引數4:時長數字的單位
        * **/
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"鋤禾日當午");
            }
        },5,1,TimeUnit.SECONDS);
    }

相關文章