單線程,多線程,線程池


1,單線程和多線程

我們通過一個實例來理解單線程和多線程。

假設有一個同學通訊錄,通訊錄長度為1000,用於記錄同學的姓名、電話、地址信息,用戶可以並發檢索該通訊錄,輸入通訊錄中的姓名,程序從通訊錄中查找該姓名,如果存在則輸出與該姓名相關的電話、地址信息。任務要求簡單模擬1000個用戶的並發訪問,檢索功能分別采用單線程和多線程實現,比較在1000個用戶的並發訪問下,單線程和多線程的檢索效率。

線程的創建和啟動

Java提供了兩種創建線程的方式。

一種方式是定義實現Java.lang.Runnable接口的類。Runnable接口中只有一個run()方法,用來定義線程運行體。代碼如下:

package com.example.demo;

/**
 * Created with IntelliJ IDEA.
 * Author: zhangz
 * Date: 2019/1/8
 * Time: 16:12
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //在線程中執行的代碼
        for(int i=0;i<100;i++){
            System.out.println("MyRunner+"+i);
        }
    }
}
View Code

定義好MyRunnable類后,需要把MyRunnable類的實例作為參數傳入到Thread的構造方法中,來創建一個新線程。然后在main方法中,實例化Thread對象,並將MyRunnable類的實例作為參數傳入進去,然后調用Thread對象的start方法啟動線程。代碼如下:

public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MyRunnable.java 實現Java.lang.Runnable接口的類*/
        Thread thread = new Thread(new MyRunnable());
        thread.start();
}
View Code

另外一種方式是將類定義為Thread類的子類,並重寫Thread類的run()方法,代碼如下:

public class MyThread extends Thread{
    @Override
    public void run(){
        //在線程中執行的代碼
        for(int i=0;i<100;i++){
            System.out.println("MyRunner+"+i);
        }
    }
}
View Code

定義好Thread類的子類后,創建一個線程,只需要創建Thread子類的一個實例即可,然后在main方法中,只需實例化Thread對象即可,然后調用Thread對象的start方法啟動線程。代碼如下:

public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MyThread.java 將類定義為Thread類的子類,並重寫Thread類的run()方法*/
        MyThread myThread =new MyThread();
        myThread.start();
}
View Code

注意:在兩種創建線程的方式中,建議使用第一種方式。因為采用實現接口的方式可以避免由於Java的單一繼承帶來的局限,有利於代碼的健壯性。

下面,使用單線程來實現同學通訊錄檢索任務:

首先建立一個同學通訊錄類,代碼如下:

@Data
public class PhoneBook {
    String name;
    String phone;
    String address;
}
View Code

加上一個初始化通訊錄數據的昂發,用ArrayList集合類存儲1000個PhoneBook對象,代碼如下:

public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同學"+String.valueOf(i));
            temp.setPhone("號碼"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }
View Code

用for循環模擬1000個並發客戶檢索通訊錄,並輸出通訊錄信息,記錄檢索全部完成時間,代碼如下:

public class PhoneSearch {

    public static void main(String[] args) {
        ArrayList<PhoneBook> array =new ArrayList<PhoneBook>();
        PhoneSearch phoneSearch = new PhoneSearch();
        phoneSearch.phoneBookInit(array);
        long start =System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            phoneSearch.PhoneBookSearch("同學"+String.valueOf(i),array);
        }
        long end = System.currentTimeMillis();
        long total = end-start;
        System.out.println("test spend time:------->"+total+"ms");

    }

    public void PhoneBookSearch(String name,ArrayList<PhoneBook> inArray){
        Iterator iterator = inArray.iterator();
        int num =1;
        while(iterator.hasNext()){
            PhoneBook temp =(PhoneBook) iterator.next();
            if(temp.getName().equals(name)){
                System.out.println("姓名:"+temp.getName()+"電話:"+temp.getPhone()+"地址:"+temp.getAddress());
                return ;
            }


        }

    }

    public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同學"+String.valueOf(i));
            temp.setPhone("號碼"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }
}
View Code

程序用for循環模擬1000個並發客戶檢索通訊錄,在模擬檢索任務開始之前調用System的currentTimeMillis方法獲取系統當前時間,模擬檢索任務執行結束后,再獲取任務執行完成后的時間,然后計算兩個時間的差值,該差值就是檢索任務運行的時間。程序輸出結果如下圖所示:

耗時:178ms

接下來用多線程完成同學通訊錄檢索任務:

上面代碼PhoneSearch 類PhoenBookSearch方法完成通訊錄的檢索及信息輸出。下面的代碼把該方法改造為線程,這樣就可以實現當多用戶檢索通訊錄時,程序針對每個用戶的檢索請求,都會啟動一個線程去執行檢索任務,由順序執行改為並發執行。改造代碼如下:

package com.example.demo;

import com.example.demo.entity.PhoneBook;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Created with IntelliJ IDEA.
 * Author: zhangz
 * Date: 2019/1/8
 * Time: 17:32
 */
public class MutiThreadPhoneSearch implements Runnable{
    ArrayList<PhoneBook> array;
    String name;
    public MutiThreadPhoneSearch(String inName,ArrayList<PhoneBook> inArray){
        array = inArray;
        name = inName;
    }
    @Override
    public void run(){
        Iterator iterator = array.iterator();
        int num =1;
        while(iterator.hasNext()){
            PhoneBook temp =(PhoneBook) iterator.next();
            if(temp.getName().equals(name)){
                System.out.println("姓名:"+temp.getName()+"電話:"+temp.getPhone()+"地址:"+temp.getAddress());
                return ;
            }


        }

    }


}
View Code

代碼定義一個MutiThreadPhoneSearch,該類實現Runnable接口,並重寫Runnable接口的run()方法,在run方法中,完成通訊錄的檢索及輸出功能。

 在main方法中,不再調用PhoenBookSearch方法,而是實例化Thread對象,並將MutiThreadPhoneSearch類的實例作為參數傳入進去,然后調用Thread對象的start方法啟動線程,代碼如下:

public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MutiThreadPhoneSearch.java 多線程實現*/
        ArrayList<PhoneBook> array =new ArrayList<PhoneBook>();
        PhoneSearch phoneSearch = new PhoneSearch();
        phoneSearch.phoneBookInit(array);
        long start =System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            MutiThreadPhoneSearch mutiThreadPhoneSearch = new MutiThreadPhoneSearch("同學"+String.valueOf(i),array);
            Thread thread = new Thread(mutiThreadPhoneSearch);
            thread.start();
        }
        long end = System.currentTimeMillis();
        long total = end-start;
        System.out.println("test spend time:------->"+total+"ms");
    }
    public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同學"+String.valueOf(i));
            temp.setPhone("號碼"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }

}
View Code

執行結果如下:

耗時278ms,還沒執行完。

從輸出結果看,檢索結果並沒有按照順序輸出,整個檢索耗時278ms。用多線程技術實現通訊錄的並發檢索,並沒有提高檢索效率,反而不如單線程的運行速度快。主要原因是系統每啟動一個線程,都要耗費一定的系統資源,導致運行效率降低,多線程在這個例子程序中,並沒有體現出多線程的性能優勢。

我們換個場景,假如把通訊錄的檢索放到服務器端,1000個用戶在同一時間並發檢索通訊錄,如果服務端是單線程服務,雖然1000個用戶是並發訪問,但要在服務器端隨機排隊等候服務器響應,如果1個用戶的響應時間為1秒,那么依次類推,最后1個用戶的響應時間為1000秒。如果是多線程服務,平均每個用戶的響應時間為2到3秒左右,顯然能夠滿足大多數用戶的響應需求。在這個場景下,多線程就體現出了性能優勢。

在正常情況下,讓程序來完成多個任務,只使用單個線程來完成比用多個線程完成所用的時間會更短。因為JVM在調度管理每個線程上肯定要花費一定資源和時間的。那么,在什么場景下使用多線程呢?一是對用戶響應要求比較高,又允許用戶並發訪問的場景;二是程序存在耗費時間的計算,整個系統都會等待這個操作,為了提高程序的響應,將耗費時間的計算通過線程來完成。

 

線程池及其特點,場景

線程池作用就是限制系統中執行線程的數量。根據系統的環境情況,可以自動或手動設置線程數量,達到運行的最佳效果;少了浪費了系統資源,多了造成系統擁擠效率不高。用線程池控制線程數量,其他線程排隊等候。一個任務執行完畢,再從隊列的中取最前面的任務開始執行。若隊列中沒有等待進程,線程池的這一資源處於等待。當一個新任務需要運行時,如果線程池中有等待的工作線程,就可以開始運行了;否則進入等待隊列。

為什么要用線程池:

1.減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。

2.可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。

如果用生活中的列子來說明,我們可以把線程池當做一個客服團隊,如果同時有1000個人打電話進行咨詢,按照正常的邏輯那就是需要1000個客服接聽電話,服務客戶。現實往往需要考慮到很多層面的東西,比如:資源夠不夠,招這么多人需要費用比較多。正常的做法就是招100個人成立一個客服中心,當有電話進來后分配沒有接聽的客服進行服務,如果超出了100個人同時咨詢的話,提示客戶等待,稍后處理,等有客服空出來就可以繼續服務下一個客戶,這樣才能達到一個資源的合理利用,實現效益的最大化。

 

Java中的線程池種類

1. newSingleThreadExecutor

創建方式:

ExecutorService pool = Executors.newSingleThreadExecutor();

一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
   public static void main(String[] args) {
       ExecutorService pool = Executors.newSingleThreadExecutor();
       for (int i = 0; i < 10; i++) {
           pool.execute(() -> {
               System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
           });
       }
   }
}

輸出結果如下:

pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....
pool-1-thread-1    開始發車啦....

從輸出的結果我們可以看出,一直只有一個線程在運行。

2.newFixedThreadPool

創建方式:

ExecutorService pool = Executors.newFixedThreadPool(10);

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

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
   public static void main(String[] args) {
       ExecutorService pool = Executors.newFixedThreadPool(10);
       for (int i = 0; i < 10; i++) {
           pool.execute(() -> {
               System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
           });
       }
   }
}

輸出結果如下:

pool-1-thread-1    開始發車啦....
pool-1-thread-4    開始發車啦....
pool-1-thread-3    開始發車啦....
pool-1-thread-2    開始發車啦....
pool-1-thread-6    開始發車啦....
pool-1-thread-7    開始發車啦....
pool-1-thread-5    開始發車啦....
pool-1-thread-8    開始發車啦....
pool-1-thread-9    開始發車啦....
pool-1-thread-10 開始發車啦....

3. newCachedThreadPool

創建方式:

ExecutorService pool = Executors.newCachedThreadPool();

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑的線程,當任務數增加時,此線程池又添加新線程來處理任務。

使用方式如上2所示。

4.newScheduledThreadPool

創建方式:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);

此線程池支持定時以及周期性執行任務的需求。

使用方式:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPool {
   public static void main(String[] args) {
       ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
       for (int i = 0; i < 10; i++) {
           pool.schedule(() -> {
               System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
           }, 10, TimeUnit.SECONDS);
       }
   }
}

上面演示的是延遲10秒執行任務,如果想要執行周期性的任務可以用下面的方式,每秒執行一次

//pool.scheduleWithFixedDelay也可以
pool.scheduleAtFixedRate(() -> {
               System.out.println(Thread.currentThread().getName() + "\t開始發車啦....");
}, 1, 1, TimeUnit.SECONDS);

5.newWorkStealingPool

newWorkStealingPool是jdk1.8才有的,會根據所需的並行層次來動態創建和關閉線程,通過使用多個隊列減少競爭,底層用的ForkJoinPool來實現的。ForkJoinPool的優勢在於,可以充分利用多cpu,多核cpu的優勢,把一個任務拆分成多個“小任務”,把多個“小任務”放到多個處理器核心上並行執行;當多個“小任務”執行完成之后,再將這些執行結果合並起來即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM