java 多執行緒簡介
程序和執行緒
程式(program)是對資料描述與操作的程式碼的集合,是應用程式執行的指令碼。
程序(process)是程式的一次執行過程,是系統執行程式的基本單位。程式是靜態的,程序是動態的。系統執行一個程式即是一個程序從建立、執行到消亡的過程。可以認為每個程式就是一個程序,但有些應用程式會有多個程序,即一個應用程式至少會啟動一個程序.,多程序的實現基礎是CPU時間片輪訓或者搶佔式執行
執行緒(thread)是程序中的一個執行場景,一個程序可以啟動多個執行緒,使用多執行緒可以提高CPU的使用率,不是提高執行速度。
執行緒是程序內部的一個獨立執行單元,相當於一個子程式,一個程序中的所有執行緒都在該程序的虛擬地址空間中,
舉個例子,我們知道機械磁碟的IO時間大概是ms級別,CPU處理速度是ns級別,電腦要同時讀取兩個壓縮檔案並解壓,讀取每個檔案需要10秒,解壓操作需要5秒,先讀去A,再讀取B的這段時間解壓A,再解壓B,可以將讀取B檔案的空餘時間充分利用
多工(multi task)一個系統中可以同時執行多個程式,即有多個獨立執行的任務,每個任務對應一個程序。
程序示例:下圖顯示了多個程式同時執行(多執行緒),每個程式可能會有多個程序.
並行和併發
並行是指兩個或兩個以上的任務同時執行,即A任務在執行,B任務也在執行,C任務也在執行,常指程序(需要多核CPU或者多臺電腦,比如常見的Xgboost可以並行執行一個演算法)
併發是指在同一時間片段同時執行,比如兩個任務請求執行,CPU只能先執行一個,將兩個任務輪流執行,類似於序列.
上述一個程式最少有一個程序(可以多個),一個程序最少有一個執行緒(可以多個),多個程序之間相互獨立,可以並行執行,多個執行緒只能併發執行,實際還是順序執行,單核的cpu同一個時刻只支援一個執行緒任務,多執行緒併發就是多個執行緒排隊申請呼叫cpu
以下重點論述多執行緒:
優缺點:
多執行緒的優點
提高CPU利用率;可以隨時停止任務;可以設定各個任務的優先順序以優化效能,提高程式的執行效率;執行緒執行完成後會自動銷燬節省記憶體。
多執行緒的缺點:
設計複雜:多執行緒共享堆記憶體和方法區,因此裡面的一些資料是可以共享的,在設計時要確保資料的準確性
資源消耗增多:多執行緒不共享棧記憶體,開啟多執行緒會增加記憶體的消耗
建立多執行緒的三種方式
1Thread類
繼承Thread可以建立執行緒,2中的單獨實現Runnable介面並不能建立執行緒,還要藉助Thread類的物件
使用Thread類建立執行緒的方式:
1自定義類繼承Thread類
2.重寫run方法
3.在run方法中編寫執行緒中執行的程式碼
4.建立上面自定義類的物件
5.呼叫start方法啟動執行緒
先看Thread的常用的構造方法:
Thread() 建立新的執行緒物件
Thread(String name) 基於指定的名字建立一個執行緒物件
Thread(Runnable target)基於Runnable介面實現類的例項(可以是匿名內部類)建立一個執行緒物件
Thread(Runnable t,String name) 根據給定的Runnable介面實現類的例項和指定的名字建立一個執行緒物件
常用方法:
void run() :包括執行緒執行時執行的程式碼,通常在子類中重寫它。
synchronized void start():啟動一個新的執行緒,然後虛擬機器呼叫新執行緒的run方法
程式碼示例
package createThreads;
public class Thread1 {
public static void main(String[] args){
FirstThread ft=new FirstThread();
ft.start();
for (int i=0;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
class FirstThread extends Thread{
@Override
public void run() {
for (int i=0;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
out:
Thread-0 : 0
main : 0
Thread-0 : 1
main : 1
Thread-0 : 2
Thread-0 : 3
Thread-0 : 4
Thread-0 : 5
Thread-0 : 6
Thread-0 : 7
main : 2
Thread-0 : 8
main : 3
Thread-0 : 9
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9
可以看到兩個執行緒的方法在同時執行,如果將執行緒啟動語句放到for迴圈後面,則不會多執行緒列印,原因是main方法的內容是順序執行的,只有start()方法在main方法體中前面部分,執行緒啟動後繼續執行後面的語句,才能實現多執行緒交替列印
練習,不考慮執行緒安全的問題,讓兩個執行緒一起列印0-100直接的數字,兩種方式,共享靜態變數,共享堆記憶體中的物件(屬性和方法)
1靜態變數共享,通過共享靜態變數的方式
public class ThreadTest01 {
public static void main(String[] args){
int i=0;
MyThread m1=new MyThread("執行緒1",i);
MyThread m2=new MyThread("執行緒2",i);
m1.start();
m2.start();
}
}
class MyThread extends Thread{
static int i;
public MyThread(String name,int i){
super(name);
this.i=i;
}
@Override
public void run() {
for (;i<100;i++){
System.out.println(getName()+" : "+i);
}
}
}
改進版:在上述程式碼上稍作精簡
public class ThreadTest02 {
public static void main(String[] args){
MyThread1 m1=new MyThread1("執行緒1");
MyThread1 m2=new MyThread1("執行緒2");
m1.start();
m2.start();
}
}
class MyThread1 extends Thread{
private static int i;
public MyThread1(String name){
super(name);
setI(0);
}
public static int getI() {
return i;
}
public static void setI(int i) {
MyThread.i = i;
}
@Override
public void run() {
for (;i<100;i++){
System.out.println( getName()+" :"+i);
}
}
}
物件成員變數共享,這種方法在定義類構造方法時,接收一個物件,共用物件的成員變數,這種方式稍顯麻煩
public class ThreadTest03 {
int i=0;
public static void main(String[] args){
ThreadTest03 tt03=new ThreadTest03();
MyThread2 mt1= new MyThread2("執行緒1",tt03);
MyThread2 mt2=new MyThread2("執行緒2",tt03);
mt1.start();
mt2.start();
}
}
class MyThread2 extends Thread{
ThreadTest03 tt;
public MyThread2(String name,ThreadTest03 tt03){
super(name);
tt=tt03;
}
@Override
public void run() {
for (;tt.i<100;tt.i++){
System.out.println(getName()+" : "+tt.i);
}
}
}
2Runnable介面
Runnable介面的原始碼:
public interface Runnable {
public abstract void run();
}
Runnable 介面中只有一個 run抽象 方法,實現該介面的類要重寫該方法.
使用Runnable介面建立執行緒的步驟:
.1自定義類實現Runnable介面
2.重寫run方法,run方法中是執行緒體
4.建立上述自定義類的物件
5.建立Thread物件,將上述自定義類物件作為引數傳入Thread的構造方法
6.呼叫start方法啟動執行緒
Thread類的其中一個構造方法
Thread(Runnable target)基於Runnable介面實現類的例項(可以是匿名內部類)建立一個執行緒物件
使用示例:兩個執行緒各自列印0-20,不考慮執行緒安全
public class RunnableTest01 {
public static void main(String[] args){
MyRunnable mr=new MyRunnable();
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<20;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
out:
Thread-1 : 0
Thread-0 : 0
Thread-1 : 1
Thread-0 : 1
Thread-1 : 2
Thread-0 : 2
Thread-1 : 3
Thread-0 : 3
Thread-1 : 4
Thread-0 : 4
Thread-1 : 5
Thread-0 : 5
Thread-1 : 6
Thread-0 : 6
Thread-1 : 7
Thread-0 : 7
Thread-1 : 8
Thread-0 : 8
Thread-1 : 9
Thread-0 : 9
Thread-1 : 10
Thread-0 : 10
Thread-1 : 11
Thread-0 : 11
Thread-1 : 12
Thread-0 : 12
Thread-1 : 13
Thread-0 : 13
Thread-1 : 14
Thread-0 : 14
Thread-0 : 15
Thread-0 : 16
Thread-1 : 15
Thread-0 : 17
Thread-1 : 16
Thread-0 : 18
Thread-1 : 17
Thread-0 : 19
Thread-1 : 18
Thread-1 : 19
接上面的練習,實現Runnable介面,兩個執行緒共同列印0-50,不考慮執行緒安全,程式碼要簡潔的多
public class RunnableTest02 {
public static void main(String[] args){
MyRunnable1 mr1=new MyRunnable1();
Thread t1=new Thread(mr1,"執行緒1");
Thread t2=new Thread(mr1,"執行緒2");
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
int i=0;
@Override
public void run() {
for (;i<50;i++){
System.out.println( Thread.currentThread().getName()+" : "+i);
}
}
}
Thread類和Runnable介面的區別:
- 1 Runnable介面更適合資源的共享
- 2 java的單繼承,多實現功能,繼承了其他類的類可以實現Runnable介面(必須重寫run方法),而Thread類的繼承類可以不重寫run方法
- 3 Runnable介面的實現類,並不是真正的執行緒類,只是執行緒執行的目標類,若要以執行緒的方式執行run方法,需要依賴Thread類(及其子類)
3Callable介面
jdk1.5加入了Callable介面,實現Callable介面建立的執行緒會獲取一個返回值,並且可以宣告異常(Callable介面再java.lang包)
在敘述Cllable介面建立執行緒之前,先介紹一個概念(在這裡不深入探究,以後寫篇文章專門敘述這塊內容):
執行緒池:
新來一個任務,要建立一個執行緒,執行緒任務結束後,執行緒會被銷燬,資源回收。多次這樣建立執行緒,銷燬執行緒的話帶來嚴重的系統開銷,同時也不好管理工作執行緒。執行緒池解決了建立單個執行緒耗費時間和資源的問題
執行緒池是一種預建立執行緒的技術,先線上程池建立多個執行緒的集合,當執行完任務需要建立一個執行緒時,不需要再建立一個執行緒,而是直接去執行緒集合中獲取,當任務結束時,不銷燬這個執行緒,將這個執行緒放入執行緒池管理,等待執行緒池任務排程.
建立執行緒池的方式:
Executor介面,該介面只有一個接收Runnable物件的execute方法
ExecutorService是執行緒池介面,繼承自Executor介面,ExecutorService裡面有操作執行緒池的方法
- Future<T> submit(Callable<T> task),傳入Callable物件的實現類,即提交任務,返回Future<T>型別物件
- void shutdown(),關閉執行緒池,不會再接收新的執行緒,未執行完成的執行緒不會被關閉
Executers:Executers類提供了四種執行緒池,有許多方法,其中列舉幾個需要用到的
- static ExecutorService newFixedThreadPool(int nThreads)返回一個建立好的固定執行緒個數的執行緒池物件,如果任務數量大於執行緒數量,則任務會進行等待。
- public static ExecutorService newCachedThreadPool(),返回執行緒池物件,該執行緒池物件根據需要建立執行緒個數,如果執行緒池內執行緒個數小於人物個數則會建立執行緒,,最大執行緒數量是Integer.MAX_VALUE,如果執行緒的處理速度小於任務的提交速度時,會不斷建立新的執行緒來執行任務,可能會因為建立執行緒過多而耗盡系統資源(CPU和記憶體)
Future<T>介面的實現類用來接收多執行緒的執行結果
V get() throws InterruptedException, ExecutionException;get方法用來接收Callable實現類的call方法的返回值
Callable介面
public interface Callable<V> {
V call() throws Exception;
}
Callable介面只有一個call方法,返回一個泛型型別的物件,並且可以丟擲異常,實現Callable介面,重寫call方法可以建立執行緒
使用Callable介面建立執行緒的步驟:
- 1建立自定義類實現Callable介面,並重寫call方法(執行緒體),call方法有返回值,且要宣告或捕獲異常
- 2建立ExecutorService執行緒池物件
- 3.將自定義類的物件放入執行緒池裡面
- 4.獲取執行緒的返回結果
- 5.關閉執行緒池,不再接收新的執行緒,未執行完的執行緒不會被關閉
使用示例:
package createThreads;
import java.util.concurrent.*;
public class CallableTest01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//建立執行緒池物件,使用了多型
ExecutorService es=Executors.newFixedThreadPool(3);
//建立Callable介面的實現類物件
MyCallable mc1=new MyCallable(5);
MyCallable mc2=new MyCallable(4);
MyCallable mc3=new MyCallable(3);
//將自定義物件方法執行緒池中
Future <Integer>f1=es.submit(mc1);
Future <Integer>f2=es.submit(mc2);
Future <Integer>f3=es.submit(mc3);
//輸出get方法返回的結果
System.out.println(f1.get()+":"+f2.get()+":"+f3.get());
//關閉執行緒池
es.shutdown();
}
}
class MyCallable implements Callable<Integer>{
private int count;
public MyCallable(int count) {
setCount(count);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
//重寫call方法
@Override
public Integer call() throws Exception {
//計算立方
int temp=0;
temp=count*count*count;
return temp;
}
}
out:
125:64:27
三種基本的建立方式介紹完了,回過頭來看,三種方式建立執行緒的優缺點
繼承Thread
優點:可以直接使用Thread類中的方法,重寫run方法,程式碼簡單
缺點:繼承Thread類之後不能繼承其他類,且不方便資料共享
實現Runnable介面
優點:在自定義類有繼承父類的情況下,還可以使用Runnable介面,重寫run方法,且方便資料共享
缺點: 在run方法內部需要獲取到當前執行緒的Thread物件後才能使用Thread中的方法
實現Callable介面
優點:可以獲取返回值,可以丟擲異常
缺點:編寫程式碼比較繁瑣
參考:http://www.monkey1024.com/javase/655
http://www.monkey1024.com/javase/653