問題分析
現代計算機一般都是多核cpu,多線程的可以大大提高效率,但是可能會有疑問,那單核CPU使用多線程是不是沒有必要了,假定一種情況,web應用服務器,單核CPU、單線程,用戶發過來請求,單個線程處理,CPU等待這個線程的處理結果返回,查詢數據庫,CPU等待查詢結果...,只有一個線程的話,每次線程在處理的過程中CPU都有大量的空閑等待時間,那這樣來說並行和串行似乎並沒有體現並行的優勢,因為任務的總量在那里,實際情況肯定不是這樣的,即便是單核CPU,一個進程中往往也是有多個線程存在的,每個線程各司其職,CPU來調度各線程。
這里需要區分CPU處理指令和IO讀取的不同,CPU的執行速度要遠大於IO的過程,因此在大多數情況下多一些復雜的CPU計算都比增加一次IO要快,這一塊深入理解要學習計算機原理相關的知識。
現實生活中也是有很多類似的例子,比如廚師做一道菜,買菜和買配料需要去不同的兩個商店,如果這個過程只依靠他一個人來做,那耗費的總時間就是買菜再去買調料的總時間,如果有一個幫廚,那么就可以兵分兩路,再來匯總結果,時間基本可以減半,廚師和幫廚就是不同的線程。
編程是高度抽象生活的一門藝術。
場景模擬
模擬單線程和多線程的效率差距,這里使用連接數據庫,和讀取磁盤文件來模擬IO操作,期望結果:
單線程總耗時:數據庫連接耗時 + 磁盤文件讀取耗時
多線程總耗時:約等於耗時最長的那個時間
讀取文件:https://gitee.com/chsoul/javase/blob/master/medias/testIO.txt
MySQL8.0 連接驅動:https://gitee.com/chsoul/javase/blob/master/medias/mysql-connector-java-8.0.18.jar
代碼如下:
package com.thread.demo;
import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author Vicente
* @version 1.0
* @date 2020/4/6 21:53
*/
public class Test {
/**
* 數據庫連接
*/
public static void getMysqlData(){
long t1 = System.currentTimeMillis();
String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true";
String userName = "root";
String password = "root";
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, userName, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
long t2 = System.currentTimeMillis();
System.out.println("數據庫連接完成!耗時:"+(t2 - t1));
}
/**
* 磁盤讀取
*/
public static void getDiskData(){
long t1 = System.currentTimeMillis();
File file = new File("src/com/thread/demo/test.txt");
StringBuilder stb = new StringBuilder();
InputStream is = null;
try {
is = new FileInputStream(file);
int read = 0;
while ((read = is.read()) != -1) {
char c = (char) read;
stb.append(c);
}
} catch (IOException e) {
e.printStackTrace();
}
long t2 = System.currentTimeMillis();
System.out.println("磁盤文件讀取結束!耗時:"+(t2 - t1));
}
public static void main(String[] args){
System.out.println("-----------------單線程執行任務開始-----------------");
long start = System.currentTimeMillis();
getMysqlData();
getDiskData();
long end = System.currentTimeMillis();
System.out.println("總耗時:"+(end - start));
System.out.println("-----------------單線程執行任務結束-----------------");
System.out.println("\r\n");
try {
System.out.println("-----------------多線程執行任務開始-----------------");
long start1 = System.currentTimeMillis();
FutureTask dbWork = new FutureTask(new DbDataWork());
FutureTask diskWork = new FutureTask(new DiskDataWork());
new Thread(dbWork).start();
new Thread(diskWork).start();
while (diskWork.get().equals("DISK_OK") && dbWork.get().equals("DB_OK")){
long end1 = System.currentTimeMillis();
System.out.println("總耗時:"+(end1 - start1));
System.out.println("-----------------多線程執行任務結束-----------------");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
/**
* 數據庫連接任務類
*/
class DbDataWork implements Callable<String> {
@Override
public String call() throws Exception {
long t1 = System.currentTimeMillis();
String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true";
String userName = "root";
String password = "root";
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, userName, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
long t2 = System.currentTimeMillis();
System.out.println("數據庫連接線程任務結束!耗時:"+(t2 - t1));
return "DB_OK";
}
}
/**
* 磁盤讀取任務類
*/
class DiskDataWork implements Callable<String>{
@Override
public String call() throws Exception {
long t1 = System.currentTimeMillis();
File file = new File("src/com/thread/demo/test.txt");
StringBuilder stb = new StringBuilder();
InputStream is = null;
try {
is = new FileInputStream(file);
int read = 0;
while ((read = is.read()) != -1) {
char c = (char) read;
stb.append(c);
}
} catch (IOException e) {
e.printStackTrace();
}
long t2 = System.currentTimeMillis();
System.out.println("磁盤讀取線程任務結束!耗時:"+(t2 - t1));
return "DISK_OK";
}
}
執行結果:
-----------------單線程執行任務開始-----------------
數據庫連接完成!耗時:694
磁盤文件讀取結束!耗時:558
總耗時:1253
-----------------單線程執行任務結束-----------------
-----------------多線程執行任務開始-----------------
數據庫連接線程任務結束!耗時:743
磁盤讀取任務結束!耗時:752
總耗時:755
-----------------多線程執行任務結束-----------------
總結
結果符合預期,這也說明在有頻繁的IO操作時使用多線程會大大提高程序的執行效率。
有興趣的同學可以試一下在執行i++的情況下,多線程就一定快嗎?單線程和多線程的臨界值是什么?
附:
- 為什么Redis單線程卻很快,在沒有磁盤IO的情況下單核CPU綁定一塊內存效率最高,Redis把讀寫操作都放在了CPU和內存的部分,又減少了多線程上下文 切換的過程,因此Redis即便是單線程也很快,在現代多核CPU的服務器中,往往會通過根據綁定Redis進程和CPU提高性能。
- 《性能調優攻略》在多核CPU調優章節提到,我們不能任由操作系統負載均衡,因為我們自己更了解自己的程序,所以,我們可以手動地為其分配CPU核,而不會過多地占用CPU0,或是讓我們關鍵進程和一堆別的進程擠在一起。在文章中提到了Linux下的一個工具,taskset,可以設定單個進程運行的CPU。
詳細可參考:https://www.cnblogs.com/blogtech/p/11742057.html
