多線程(一)

進程與線程

1.進程:進程是指一個內存中運行的應用程序,比如在Windows系統中,一個運行的exe就是一個進程。

2.線程:進程是負責程序執行的執行單元,線程本身依靠程序進行運行。

3 單線程:程序中只存在一個線程,實際上主方法就是一個主線程。

4 多線程:在一個程序中運行多個任務,目的是更好地使用CPU資源。

線程的生命周期

關于Java中線程的生命周期,首先看一下下面這張較為經典的圖:

image

上圖中基本上囊括了Java中多線程各重要知識點。掌握了上圖中的各知識點,Java中的多線程也就基本上掌握了。主要包括:

Java線程具有五中基本狀態

新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,并不是說執行了t.start()此線程立即就會執行;

運行狀態(Running):當CPU開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中;

阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:

1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

4死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

4種線程實現的方式

1.繼承Thread類

在java.lang包中定義, 繼承Thread類必須重寫run()方法

class MyThread extends Thread{
    private static int num = 0;
     
    public MyThread(){
        num++;
    }
     
    @Override
    public void run() {
        System.out.println("主動創建的第"+num+"個線程");
    }
}

創建好了自己的線程類之后,就可以創建線程對象了,然后通過start()方法去啟動線程。注意,不是調用run()方法啟動線程,run方法中只是定義需要執行的任務,如果調用run方法,即相當于在主線程中執行run方法,跟普通的方法調用沒有任何區別,此時并不會創建一個新的線程來執行定義的任務

public class ThreadTest {
    public static void main(String[] args) {
        MyThread thread=new MyThread();
        thread.start();
    }
}

2實現Runnable接口

在Java中創建線程除了繼承Thread類之外,還可以通過實現Runnable接口來實現類似的功能。實現Runnable接口必須重寫其run方法。
下面是一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子線程ID:"+Thread.currentThread().getId());
    }
}

Runnable的中文意思是“任務”,顧名思義,通過實現Runnable接口,我們定義了一個子任務,然后將子任務交由Thread去執行。注意,這種方式必須將Runnable作為Thread類的參數,然后通過Thread的start方法來創建一個新線程來執行該子任務。如果調用Runnable的run方法的話,是不會創建新線程的,這根普通的方法調用沒有任何區別。

事實上,查看Thread類的實現源代碼會發現Thread類是實現了Runnable接口的。

在Java中,這2種方式都可以用來創建線程去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable接口看起來更加簡潔,但是由于Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable接口。

3使用執行器(Executor)創建線程池(thread pool)

從JDK1.5開始,java.util.concurrent包中的執行器(Executor)將為你管理Thread對象,從而簡化了并發編程。如果我們的程序需要用到很多生命周期比較短的線程,那么應該使用線程池,線程池中包含了很多空閑線程,而且這些線程的生命周期不需要我們操心。另一個使用線程池的原因是:如果你的代碼需要大量的線程,那么最好使用一個線程池來規定總線程數的上線,防止虛擬機崩潰。這樣可以限制最大的并發數量。
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
(1) newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。示例代碼如下:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   try {  
    Thread.sleep(index * 1000);  
   } catch (InterruptedException e) {  
    e.printStackTrace();  
   }  
   cachedThreadPool.execute(new Runnable() {  
    public void run() {  
     System.out.println(index);  
    }  
   });  
  }  
 }  
}  

線程池為無限大,當執行第二個任務時第一個任務已經完成,會復用執行第一個任務的線程,而不用每次新建線程。
(2) newFixedThreadPool
創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。示例代碼如下:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   fixedThreadPool.execute(new Runnable() {  
    public void run() {  
     try {  
      System.out.println(index);  
      Thread.sleep(2000);  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
  }  
 }  
}  

因為線程池大小為3,每個任務輸出index后sleep 2秒,所以每兩秒打印3個數字。
定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors();
(3) newScheduledThreadPool
創建一個定長線程池,支持定時及周期性任務執行。延遲執行示例代碼如下:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
  scheduledThreadPool.schedule(new Runnable() {  
   public void run() {  
    System.out.println("delay 3 seconds");  
   }  
  }, 3, TimeUnit.SECONDS);  
 }  
}  

表示延遲3秒執行。
定期執行示例代碼如下:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
  scheduledThreadPool.scheduleAtFixedRate(new Runnable() {  
   public void run() {  
    System.out.println("delay 1 seconds, and excute every 3 seconds");  
   }  
  }, 1, 3, TimeUnit.SECONDS);  
 }  
}  

表示延遲1秒后每3秒執行一次。
(4) newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。示例代碼如下:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   singleThreadExecutor.execute(new Runnable() {  
    public void run() {  
     try {  
      System.out.println(index);  
      Thread.sleep(2000);  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
  }  
 }  
}  

結果依次輸出,相當于順序執行各個任務。
你可以使用JDK自帶的監控工具來監控我們創建的線程數量,運行一個不終止的線程,創建指定量的線程,來觀察:
工具目錄:C:\Program Files\Java\jdk1.8.0_06\bin\jconsole.exe
運行程序做稍微修改:

public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService singleThreadExecutor = Executors.newCachedThreadPool();  
  for (int i = 0; i < 100; i++) {  
   final int index = i;  
   singleThreadExecutor.execute(new Runnable() {  
    public void run() {  
     try {  
      while(true) {  
       System.out.println(index);  
       Thread.sleep(10 * 1000);  
      }  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
   try {  
    Thread.sleep(500);  
   } catch (InterruptedException e) {  
    e.printStackTrace();  
   }  
  }  
 }  
}  

4.使用Callable與Future創建線程并獲取返回值

我們使用Runnable封裝了一個異步運行的任務,我們可以把它想象成一個沒有參數和返回值的異步方法,Callable與Runnable相似,但是Callable具有返回值,可以從線程中返回數據。
Callable

我們從jdk中找到了Callable<V>的源代碼,去掉一些無用的部分:

package java.util.concurrent;

public interface Callable<V> {

    V call() throws Exception;
}

可以看出Callable接口只是將run方法換成了call方法,其他并沒有太多的改動。
Future

Future類負責保存異步計算的結果。可以啟動一個計算,將Future對象交給某個線程,然后我們去做其他的事情,Future對象的所有者在結果計算好之后就可以調用get方法獲得它。我們在上面線程池的submit方法中也提到過。

V get() throws InterruptedException,ExecutionException
如有必要,等待計算完成,然后通過get方法獲取其結果。
FutureTask包裝器

我們雖然有了Callable和Future類,但是我們仍然需要一種方法將他們結合起來使用。而且還存在的問題是,Callable的出現替代了Runnable。我們需要一種手段讓Thread類能夠接受Callable做參數。在這里我們使用非常好用的FutureTask包裝器。它可以將Callable轉換成Futrue和Runnable,因為他同時實現了Runnable和Future<V>兩個接口。
代碼實現:

public class CallablePool {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        ExecutorService es = Executors.newFixedThreadPool(5);
        // 創建一個5個線程大小的線程池

        ArrayList<Future<Integer>> results = new ArrayList<Future<Integer>>(5);
        // 創建一個Future<Integer>類型的數組

        FutureTask<Integer> ft = null;

        for (int i = 0; i < 5; i++) {
            ft = new FutureTask<Integer>(new ImplCallable(i));
            // 將Callable類型轉化成FutureTask類型

            es.submit(ft);
            // 提交線程

            results.add(ft);
            // 將返回的結果提交,因為FutureTask同時也可變為Future類型,所以這里不需要其他類型轉化

        }
        for (int i = 0; i < 5; i++)
            try {
                System.out.println(results.get(i).get());
                // 打印結果,發現數組中為0到4五個數字,成功。

            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    }

}
public class ImplCallable implements Callable<Integer> {

    private int index;

    ImplCallable(int index) {
        this.index = index;
    }

    @Override
    public Integer call() throws Exception {
        // TODO Auto-generated method stub
        return index;

    }

}

上面采用了線程池的方法來表現Callable和Future的使用方法,如果是簡單實用Thread道理也是相同的,我們需要把:

ExecutorService es = Executors.newFixedThreadPool(5);
es.submit(ft);

這兩句去掉,在for循環中替換成下面兩句。創建Thread實例,開啟新的線程。

Thread th = new Thread(ft);
th.start();
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。