java線程和線程池的使用

java線程和線程池

一、創建多線程的方式

    java多線程很常見,如何使用多線程,如何創建線程,java中有兩種方式,第一種是讓自己的類實現Runnable接口,第二種是讓自己的類繼承Thread類。其實Thread類自己也是實現了Runnable接口。具體使用實例如下:

1、通過實現Runnable接口方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThread1 implements Runnable//通過實現Runnable接口方式
{
    String sign = "thread#1@";
 
    @Override
    public void run()
    {
        for (int i = 0; i < 30; i++)
        {
            System.out.println(sign + "->" + i);
            try
            {
                Thread.sleep(100L);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
 
}

2、通過繼承Thread類的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThread2 extends Thread//通過繼承Thread類的方式
{
    String sign = "thread#2@";
 
    @Override
    public void run()
    {
        for (int i = 0; i < 30; i++)
        {
            System.out.println(sign + "->" + i);
            try
            {
                Thread.sleep(100L);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
 
}

    再啓用上面創建的兩種線程,調運代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws InterruptedException
{
    long startTime = System.currentTimeMillis();
    // 通過主線程啓動自己的線程
 
    // 通過實現runnable接口
    Runnable myThread1 = new MyThread1();
    Thread thread1 = new Thread(myThread1);
    thread1.start();
 
    // 通過繼承thread類
    Thread thread2 = new Thread(new MyThread2());
    thread2.start();
 
    // 注意這裏不是調運run()方法,而是調運線程類Thread的start方法,在Thread方法內部,會調運本地系統方法,最終會自動調運自己線程類的run方法
 
    // 讓主線程睡眠
    Thread.sleep(1000L);
    System.out.println("主線程結束!用時:"
            + (System.currentTimeMillis() - startTime));
    // System.exit(0);
}

    輸入結果(每次輸入可能不同)不再詳細列出。對於上面的兩種方式,更推薦使用實現Runnable接口的方式實現多線程。一方面是可以防止java單繼承的顧慮,另一方面Runnable是面向接口編程,擴展性比起繼承Thread更好。所以儘量使用implement Runnable的方式。

 

二、java線程類型說明

    上面是將多線程跑起來了,但是有個問題,如果不讓主線程睡眠,當主線程(比如main線程)結束以後,如果子線程還沒結束,那麼子線程是否還會執行呢?答案是會繼續執行,爲了說明這個問題,就又涉及到java中線程的類型。java中線程一共有兩種類型:守護線程(daemon thread)和用戶線程(user thread)又叫非守護線程。可以通過thread.setDaemon()方法設置線程是否爲守護線程,默認是設置非守護線程(false)。java虛擬機停止運行的時間是虛擬機中運行的所有線程都是守護線程的時候,也就是說如果jvm中沒有user thread的時候,jvm就停止運行。或者說jvm在最後一個非守護線程結束的時候,將停止所有的守護進程,然後退出jvm。

    當使用main方法開啓線程時,主線程默認是非守護進程,而用戶自己開的進程也是非守護進程。當主線程結束,但是子線程(默認是非守護線程)還沒結束,所以虛擬機是不停止運行的,當子線程運行完以後,如果主線程也運行完畢,jvm發現沒有非守護線程,就將jvm關閉,所以當main方法的主線程執行完畢以後,子線程是會繼續執行的。當然我們也可以讓在main主線程執行完畢以後,子線程不再執行,方法就是將所有的子線程設置爲守護進程setDaemon(true)即可。但是需要注意的是這個設置需要在線程運行之前設置,不能在線程運行的過程中修改線程類型。

    更直白點說如果用戶將線程設置爲守護進程,那實際的意思就是告訴jvm你不用搭理我這個線程,jvm想停止的時候,不用考慮我這個線程是否執行結束。這種線程具體的使用比如在垃圾回收機制的線程,就是一個守護線程。

1
2
3
4
5
6
7
8
9
10
11
12
13
Runnable myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
thread1.setDaemon(true);
thread1.start();
 
Thread thread2 = new Thread(new MyThread2());
thread2.setDaemon(false);
thread2.start();
 
System.out.println("mainThread isDaemon:"
        + Thread.currentThread().isDaemon());
System.out.println("thread1 isDaemon:" + thread1.isDaemon());
System.out.println("thread2 isDaemon:" + thread2.isDaemon());

 

三、線程池的使用

    上面已經看過了可以在代碼中直接新起線程,如果我們在主線程中新起一百個線程,讓這一百個線程同時工作,邏輯上是沒有任何問題的,但是這樣做對系統資源的開銷很大,這樣會在短時間內處理很多的任務,當然包括新起線程等等。基於這樣的考慮,我們是有必要引入線程池這個東西的。線程池就是一個池子,池子裏有很多可用線程資源,如果需要就直接從這個池子裏拿就是。當不用的時候,放入池子中,線程池會自動幫我們管理。所以使用線程池主要有以下兩個好處:1、減少在創建和銷燬線程上所花的時間以及系統資源的開銷 2、如不使用線程池,有可能造成系統創建大量線程而導致消耗完系統內存 。

    如果我們想要使用線程池,就需要先定義這個線程池。定義線程池的時候,其中的幾個主要參數說明如下:
-corePoolSize(int):線程池中保持的線程數量,包括空閒線程在內。也就是線程池釋放的最小線程數量界限。

-maximumPoolSize(int):線程池中嫩容納最大線程數量。

-keepAliveTime(long):空閒線程保持在線程池中的時間,當線程池中線程數量大於corePoolSize的時候。

-unit(TimeUnit枚舉類):上面參數時間的單位,可以是分鐘,秒,毫秒等等。

-workQueue(BlockingQueue<Runnable>):任務隊列,當線程任務提交到線程池以後,首先放入隊列中,然後線程池按照該任務隊列依次執行相應的任務。可以使用的workQueue有很多,比如:LinkedBlockingQueue等等。

-threadFactory(ThreadFactory類):新線程產生工廠類。

-handler(RejectedExecutionHandler類):當提交線程拒絕執行、異常的時候,處理異常的類。該類取值如下:(注意都是內部類)

ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務,重複此過程。
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務。

   除了自定義線程池以外, java提供了幾種常用的線程池,可以快捷的供程序員使用,他們分別是:

1、newFixedThreadPool 創建固定大小數量線程池,數量通過傳入的參數決定。

2、newSingleThreadExecutor 創建一個線程容量的線程池,所有的線程依次執行,相當於創建固定數量爲1的線程池。

3、newCachedThreadPool 創建可緩存的線程池,沒有最大線程限制(實際上是Integer.MAX_VALUE)。如果用空閒線程等待時間超過一分鐘,就關閉該線程。

4、newScheduledThreadPool 創建計劃(延遲)任務線程池,線程池中的線程可以讓其在特定的延遲時間之後執行,也可以以固定的時間重複執行(週期性執行)。相當於以前的Timer類的使用。
5、newSingleThreadScheduledExecutor 創建單線程池延遲任務,創建一個線程容量的計劃任務。

    其實通過靜態方法創建的上面幾種線程池,也都是通過傳入默認的各個參數,然後返回一個有各自特點的線程池。具體參數可以通過查看jdk源碼閱讀。

    有了線程池,那麼我們如何利用線程池中線程執行我們的任務,由於Java將線程池的封裝,我們拿到的線程池的線程其實是一個包含線程任務的執行器,只需要調運執行器的執行方法,就會自動執行我們線程中的任務。對於非計劃任務,我們需要拿到一個ThreadPoolExecutor,對於計劃任務,我們需要拿到一個ScheduledThreadPoolExecutor(它是ThreadPoolExecutor的子類)。在瞭解這兩個類之前,需要先了解兩個接口,ExecutorService以及它的子接口ScheduleThreadExecutorService接口,上面兩個接口分別實現了這兩個接口,這個兩接口定義了execute(Runnable r)方法,這個方法去執行線程的任務。也就是我們通過調運ThreadPoolExecutor或者ScheduledThreadPoolExecutor的execute(Runnable r)方法開啓我們的線程,並且執行我們的線程任務,具體代碼如下:

    定義一個單例的線程池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MyPool
{
    private static MyPool myPool = null;
    //單例線程池中有兩種具體的線程池
    private ThreadPoolExecutor threadPool = null;
    private ScheduledThreadPoolExecutor scheduledPool = null;
 
    public ThreadPoolExecutor getThreadPool()
    {
        return threadPool;
    }
 
    public ScheduledThreadPoolExecutor getScheduledPool()
    {
        return scheduledPool;
    }
 
    //設置線程池的各個參數的大小
    private int corePoolSize = 10;// 池中所保存的線程數,包括空閒線程。
    private int maximumPoolSize = 20;// 池中允許的最大線程數。
    private long keepAliveTime = 3;// 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
    private int scheduledPoolSize = 10;
 
    private static synchronized void create()
    {
        if (myPool == null)
            myPool = new MyPool();
    }
 
    public static MyPool getInstance()
    {
        if (myPool == null)
            create();
        return myPool;
    }
 
    private MyPool()
    {
        //實例化線程池,這裏使用的LinkedBlockingQueue作爲workQueue,使用DiscardOldestPolicy作爲handler
        this.threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        //實例化計劃任務線程池
        this.scheduledPool = new ScheduledThreadPoolExecutor(scheduledPoolSize);
    }
}

    獲取線程池中的線程,並且執行線程中任務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
public void testThreadPool()
{
 
    ThreadPoolExecutor pool1 = (ThreadPoolExecutor) Executors
            .newCachedThreadPool();
    pool1.execute(new Runnable()
    {
 
        @Override
        public void run()
        {
            System.out.println("快捷線程池中的線程!");
        }
    });
 
    ThreadPoolExecutor pool2 = MyPool.getInstance().getThreadPool();
    pool2.execute(new Runnable()
    {
 
        @Override
        public void run()
        {
            System.out.println("普通線程池中的線程");
        }
    });
 
    ScheduledThreadPoolExecutor pool3 = MyPool.getInstance()
            .getScheduledPool();
    pool3.scheduleAtFixedRate(new Runnable()
    {
 
        @Override
        public void run()
        {
            System.out.println("計劃任務線程池中的線程");
        }
    }, 0, 1000, TimeUnit.MILLISECONDS);
}

 

四、計劃任務執行使用

    通過上面的例子,也看到了計劃任務線程池的使用方式。對於計劃任務,除了可以執行普不同線程池中線程的任務以外,還可以執行計劃任務特殊的線程要求,比如:scheduleWithFixedDelay(command, initialDelay, delay, unit);在初始化延遲之後,以特定的延遲時間重複執行。scheduleAtFixedRate(command, initialDelay, period, unit);在初始化延遲時間之後,以固定頻率重複執行。這兩種的區別是下一次執行時間延遲計算的開始時間不同,第一種是從上一次任務開始執行的時候計算,第二種是從上一次任務執行結束的時候計算。這兩種和java之前版本中Timer類很相似。但是Timer有很多缺陷,具體的缺陷不再詳細說明。而這些缺陷都可以通過ScheduledExecutorService給完美解決。所以是時候該丟棄Timer了

 

 

 

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