前言
在本篇文章當中主要跟大家介紹并發的基礎知識,從最基本的問題出發層層深入,幫助大家了解并發知識,并且打好并發的基礎,為后面深入學習并發提供保證,本篇文章的篇章結構如下:
并發的需求
- 我們常用的軟體就可能會有這種需求,對于一種軟體我們可能有多重需求,程式可能一邊在運行一邊在后臺更新,因此在很多情況下對于一個行程或者一個任務來說可能想要同時執行兩個不同的子任務,因此就需要在一個行程當中產生多個子執行緒,不同的執行緒執行不同的任務,
- 現在的機器的CPU核心個數一般都有很多個,比如現在一般的電腦都會有4個CPU,而每一個CPU在同一個時刻都可以執行一個任務,因此為了充分利用CPU的計算資源,我們可以讓這多個CPU同時執行不同的任務,讓他們同時作業起來,而不是空閑沒有事可做,
- 還有就是在科學計算和高性能計算領域有這樣的需求,比如矩陣計算,如果一個執行緒進行計算的話需要很長的時間,那么我們就可能使用多核的優勢,讓多個CPU同時進行計算,這樣一個計算任務的計算時間就會比之前少很多,比如一個任務單執行緒的計算時間為24小時,如果我們有24個CPU核心,那么我們的計算任務可能在1-2小時就計算完成了,可以節約非常多的時間,
并發的基礎概念
在并發當中最常見的兩個概念就是行程和執行緒了,那什么是行程和執行緒呢?
- 行程簡單的說來就是一個程式的執行,比如說你在windows作業系統當中雙擊一個程式,在linux當中在命令列執行一條命令等等,就會產生一個行程,總之行程是一個獨立的主體,他可以被作業系統調度和執行,
- 而執行緒必須依賴行程執行,只有在行程當中才能產生執行緒,現在通常會將執行緒稱為輕量級行程(Light Weight Process),一個行程可以產生多個執行緒,二者多個執行緒之間共享行程當中的某些資料,比如全域資料區的資料,但是執行緒的本地資料是不進行共享的,
你可能會聽過行程是資源分配的基本單位,這句話是怎么來的呢?在上面我們已經提到了執行緒必須依賴于行程而存在,在我們啟動一個程式的時候我們就會開啟一個行程,而這個行程會向作業系統申請資源,比如記憶體,磁盤和CPU等等,這就是為什么作業系統是申請資源的基本單位,
你可能也聽過執行緒是作業系統調度的基本單位,那這又是為什么呢?首先你需要明白CPU是如何作業的,首先需要明白我們的程式會被編譯成一條條的指令,而這些指令會存在在記憶體當中,而CPU會從記憶體當中一一的取出這些指令,然后CPU進行指令的執行,而一個執行緒通常是執行一個函式,而這個函式也是會被編譯成很多指令,因此這個執行緒也可以被CPU執行,因為執行緒可以被作業系統調度,將其放到CPU上進行執行,而且沒有比執行緒更小的可以被CPU調度的單位了,因此說執行緒是作業系統調度的基本單位,
Java實作并發
繼承Thread類
public class ConcurrencyMethod1 extends Thread {
@Override
public void run() {
// Thread.currentThread().getName() 得到當前正在執行的執行緒的名字
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
// 新開啟一個執行緒
ConcurrencyMethod1 t = new ConcurrencyMethod1();
t.start();// 啟動這個執行緒
}
}
}
// 某次執行輸出的結果(輸出的順序不一定)
Thread-0
Thread-4
Thread-1
Thread-2
Thread-3
上面代碼當中不同的執行緒需要得到CPU資源,在CPU當中被執行,而這些執行緒需要被作業系統調度,然后由作業系統放到不同的CPU上,最終輸出不同的字串,
使用匿名內部類實作runnable介面
public class ConcurrencyMethod2 extends Thread {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// Thread.currentThread().getName() 得到當前正在執行的執行緒的名字
System.out.println(Thread.currentThread().getName());
}
});
thread.start();
}
}
}
// 某次執行輸出的結果(輸出的順序不一定)
Thread-0
Thread-1
Thread-2
Thread-4
Thread-3
當然你也可以采用Lambda函式去實作:
public class ConcurrencyMethod3 {
public static void main(String[] args) {
for (int i=0; i < 5; i++) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
thread.start();
}
}
}
// 輸出結果
Thread-0
Thread-1
Thread-2
Thread-4
Thread-3
其實還有一種JDK給我們提供的方法去實作多執行緒,這個點我們在后文當中會進行說明,
理解主執行緒和Join函式
假如現在我們有一個任務,子執行緒輸出一下自己的執行緒的名字,在執行緒輸出完自己的名字之后,主執行緒再輸出字串“執行緒執行完成”,
在完成上面的任務之前,首先我們需要明白什么是主執行緒和子執行緒,所謂主執行緒就是在執行Java程式的時候不是通過new Thread操作這樣顯示的創建的執行緒,比如在我們的非并發的程式當中,執行程式的執行緒就是主執行緒,
public class MainThread {
public static void main(String[] args) {
System.out.println("我是主執行緒");
}
}
比如在上面的代碼當中執行陳述句System.out.println("我是主執行緒");的執行緒就是主執行緒,
public class MainThread {
public static void main(String[] args) {
// 下面這段代碼是由主執行緒執行的
// 主執行緒通過下面這段代碼創建一個子執行緒
Thread thread = new Thread(() -> {
System.out.println("我是主執行緒創建的子執行緒");
});
// 這句代碼也是主執行緒執行的
// 主要意義就是主執行緒啟動子執行緒
thread.start();
System.out.println("我是主執行緒");
}
}
現在我們再來看一下我們之前的任務:
假如現在我們有一個任務,子執行緒輸出一下自己的執行緒的名字,在執行緒輸出完自己的名字之后,主執行緒再輸出字串“執行緒執行完成”,
上面的任務很明確就是主執行緒在執行輸出自己執行緒的名字的陳述句之前,必須等待子執行緒執行完成,而在Java執行緒當中給我提供了一種方式,幫助我們實作這一點,可以保證主執行緒的某段代碼可以在子執行緒執行完成之后再執行,
public class MainThread {
public static void main(String[] args) throws InterruptedException {
// 下面這段代碼是由主執行緒執行的
// 主執行緒通過下面這段代碼創建一個子執行緒
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
// 這句代碼也是主執行緒執行的
// 主要意義就是主執行緒啟動子執行緒
thread.start();
// 這句代碼的含義就是阻塞主執行緒
// 直到 thread 的 run 函式執行完成
thread.join();
System.out.println(Thread.currentThread().getName());
}
}
// 輸出結果
Thread-0
main
上面代碼的執行流程大致如下圖所示:
我們需要知道的一點是thread.join()這條陳述句是主執行緒執行的,它的主要功能就是等待執行緒thread執行完成,只有thread執行完成之后主執行緒才會繼續執行thread.join()后面的陳述句,
第一個并發任務——求\(x^2\)的積分
接下來我們用一個例子去具體體會并發帶來的效果提升,我們的這個例子就是求函式的積分,我們的函式為最簡單的二次函式\(x^2\),當然我們就積分(下圖當中的陰影部分)完全可以根據公式進行求解(如果你不懂積分也沒有關系,下文我們會把這個函式寫出來,不會影響你對并發的理解):
\[\int_0^{10} x^2\mathrm{d}x = \frac{1}{3}x^3+C \]但是我們用程式去求解的時候并不是采用上面的方法,而是使用微元法:
\[\int_0^{10} x^2\mathrm{d}x =\sum_{ i= 0}^{1000000}(i * 0.00001) ^2 * 0.00001 \]下面我們用一個單執行緒先寫出求\(x^2\)積分的代碼:
public class X2 {
public static double x2integral(double a, double b) {
double delta = 0.001;
return x2integral(a, b, delta);
}
/**
* 這個函式是計算 x^2 a 到 b 位置的積分
* @param a 計算積分的起始位置
* @param b 計算積分的最終位置
* @param delta 表示微元法的微元間隔
* @return x^2 a 到 b 位置的積分結果
*/
public static double x2integral(double a, double b, double delta) {
double sum = 0;
while (a <= b) {
sum += delta * Math.pow(a, 2);
a += delta;
}
return sum;
}
public static void main(String[] args) {
// 這個輸出的結果為 0.3333333832358528
// 這個函式計算的是 x^2 0到1之間的積分
System.out.println(x2integral(0, 1, 0.0000001));
}
}
上面代碼當中的函式x2integral主要是用于計算區間\([a, b]\)之間的二次函式\(x^2\)的積分結果,我們現在來看一下如果我們想計算區間[0, 10000]之間的積分結果且delta = 0.000001需要多長時間,其中delta表示每一個微元之間的距離,
public static void main(String[] args) {
long start = System.currentTimeMillis();
System.out.println(x2integral(0, 10000, 0.000001));
long end = System.currentTimeMillis();
System.out.println(end - start);
}
從上面的結果來看計算區間[0, 10000]之間的積分結果且delta = 0.000001,現在假設我們使用8個執行緒來做這件事,我們該如何去規劃呢?
因為我們是采用8個執行緒來做這件事兒,因此我們可以將這個區間分成8段,每個執行緒去執行一小段,最終我們將每一個小段的結果加起來就行,整個程序大致如下,
首先我們先定義一個繼承Thread的類(因為我們要進行多執行緒計算,所以要繼承這個類)去計算區間[a, b]之間的函式\(x^2\)的積分:
class ThreadX2 extends Thread {
private double a;
private double b;
private double sum = 0;
private double delta = 0.000001;
public double getSum() {
return sum;
}
public void setSum(double sum) {
this.sum = sum;
}
public double getDelta() {
return delta;
}
public void setDelta(double delta) {
this.delta = delta;
}
/**
* 重寫函式 run
* 計算區間 [a, b] 之間二次函式的積分
*/
@Override
public void run() {
while (a <= b) {
sum += delta * Math.pow(a, 2);
a += delta;
}
}
public double getA() {
return a;
}
public void setA(double a) {
this.a = a;
}
public double getB() {
return b;
}
public void setB(double b) {
this.b = b;
}
}
我們最終開啟8個執行緒的代碼如下所示:
public static void main(String[] args) throws InterruptedException {
// 單執行緒測驗計算時間
System.out.println("單執行緒");
long start = System.currentTimeMillis();
ThreadX2 x2 = new ThreadX2();
x2.setA(0);
x2.setB(1250 * 8);
x2.start();
x2.join();
System.out.println(x2.getSum());
long end = System.currentTimeMillis();
System.out.println("花費時間為:" + (end - start));
System.out.println("多執行緒");
// 多執行緒測驗計算時間
start = System.currentTimeMillis();
ThreadX2[] threads = new ThreadX2[8];
for (int i = 0; i < 8; i++) {
threads[i] = new ThreadX2();
threads[i].setA(i * 1250);
threads[i].setB((i + 1) * 1250);
}
// 這里要等待每一個執行緒執行完成
// 因為只有執行完成才能得到計算的結果
for (ThreadX2 thread : threads) {
thread.start();
}
for (ThreadX2 thread : threads) {
thread.join();
}
end = System.currentTimeMillis();
System.out.println("花費時間為:" + (end - start));
double ans = 0;
for (ThreadX2 thread : threads) {
ans += thread.getSum();
}
System.out.println(ans);
}
// 輸出結果
單執行緒
3.333332302493948E11
花費時間為:14527
多執行緒
花費時間為:2734
3.333332303236695E11
| 單執行緒 | 多執行緒(8個執行緒) | |
|---|---|---|
| 計算結果 | 3.333332302493948E11 | 3.333332303236695E11 |
| 執行時間 | 14527 | 2734 |
從上面的結果來看,當我們使用多個執行緒執行的時候花費的時間比單執行緒少的多,幾乎減少了7倍,由此可見并發的“威力”,
FutureTask機制
在前文和代碼當中,我們發現不論是我們繼承自Thread類或者寫匿名內部內我們都沒有回傳值,我們的回傳值都是void,那么如果我們想要我們的run函式有回傳值怎么辦呢?JDK為我們提供了一個機制,可以讓執行緒執行我們指定函式并且帶有回傳值,我們來看下面的代碼:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FT {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();
// get 函式如果結果沒有計算出來
// 主執行緒會在這里阻塞,如果計算
// 出來了將回傳結果
Integer integer = task.get();
System.out.println(integer);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("執行緒正在執行");
return 101;
}
}
// 輸出結果
執行緒正在執行
101
從上面的繼承結構我們可以看出FutureTask實作了Runnable介面,而上面的代碼當中我們最侄訓將一個FutureTask作為引數傳入到Thread類當中,因此執行緒最侄訓執行FutureTask當中的run方法,而我們也給FutureTask傳入了一個Callable介面實作類物件,那么我們就可以在FutureTask當中的run方法執行我們傳給FutureTask的Callable介面中實作的call方法,然后將call方法的回傳值保存下來,當我們使用FutureTask的get函式去取結果的時候就將call函式的回傳結果回傳回來,在了解這個程序之后你應該可以理解上面代碼當中FutureTask的使用方式了,
需要注意的一點是,如果我們在呼叫get函式的時候call函式還沒有執行完成,get函式會阻塞呼叫get函式的執行緒,關于這里面的實作還是比較復雜,我們在之后的文章當中會繼續討論,大家現在只需要在邏輯上理解上面使用FutureTask的使用程序就行,
總結
在本篇文章當中主要給大家介紹了一些并發的需求和基礎概念,并且使用了一個求積分的例子帶大家切身體會并發帶來的效果提升,并且給大家介紹了在Java當中3中實作并發的方式,并且給大家梳理了一下FutureTask的方法的大致作業程序,幫助大家更好的理解FutureTask的使用方式,除此之外給大家介紹了join函式,大家需要好好去理解這一點,仔細去了解join函式到底是阻塞哪個執行緒,這個是很容搞錯的地方,
以上就是本文所有的內容了,希望大家有所識訓,我是LeHung,我們下期再見!!!(記得點贊收藏哦!)
更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,了解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/499615.html
標籤:Java
下一篇:03-SpringAMQP入門
