1. 程式人生 > 其它 >多執行緒知識點總結

多執行緒知識點總結

一、java多執行緒三種建立方式
1.1 第一種,通過繼承Thread類建立執行緒類
通過繼承Thread類來建立並啟動多執行緒的步驟如下:

1、定義一個類繼承Thread類,並重寫Thread類的run()方法,run()方法的方法體就是執行緒要完成的任務,因此把run()稱為執行緒的執行體;

2、建立該類的例項物件,即建立了執行緒物件;

3、呼叫執行緒物件的start()方法來啟動執行緒;
public class ExtendThread extends Thread {

private int i;

public static void main(String[] args) {
for(int j = 0;j < 50;j++) {

//呼叫Thread類的currentThread()方法獲取當前執行緒
System.out.println(Thread.currentThread().getName() + " " + j);

if(j == 10) {
//建立並啟動第一個執行緒
new ExtendThread().start();

//建立並啟動第二個執行緒
new ExtendThread().start();
}
}
}

public void run() {
for(;i < 100;i++) {
//當通過繼承Thread類的方式實現多執行緒時,可以直接使用this獲取當前執行的執行緒
System.out.println(this.getName() + " " + i);
}
}
}

1.2 第二種,通過實現Runnable介面建立執行緒類
這種方式建立並啟動多執行緒的步驟如下:

1、定義一個類實現Runnable介面;

2、建立該類的例項物件obj;

3、將obj作為構造器引數傳入Thread類例項物件,這個物件才是真正的執行緒物件;

4、呼叫執行緒物件的start()方法啟動該執行緒;

程式碼例項:

public class ImpRunnable implements Runnable {

private int i;

@Override
public void run() {
for(;i < 50;i++) {
//當執行緒類實現Runnable介面時,要獲取當前執行緒物件只有通過Thread.currentThread()獲取
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args) {
for(int j = 0;j < 30;j++) {
System.out.println(Thread.currentThread().getName() + " " + j);
if(j == 10) {
ImpRunnable thread_target = new ImpRunnable();
//通過new Thread(target,name)的方式建立執行緒
new Thread(thread_target,"執行緒1").start();
new Thread(thread_target,"執行緒2").start();
}

}

}

}

1.3 第三種,通過Callable和Future介面建立執行緒

Callable介面提供了一個call()方法可以作為執行緒執行體,但call()方法比run()方法功能更強大,call()方法的功能的強大體現在:

1、call()方法可以有返回值;

2、call()方法可以宣告丟擲異常;

二、執行緒的生命週期

new Thread().start; 之後,執行完run()方法裡的程式碼後執行緒就自動結束並自我銷燬。無法再次呼叫start。只有使用了執行緒池,執行緒池可以保留核心執行緒不被銷燬,每次來了任務可以直接使用空閒執行緒執行程式碼,不用每次都建立和銷燬執行緒,節約資源。

三、new Ruanble().run 和new thread().start() 和的區別


new Ruanble().run :沒有多執行緒。這個執行緒都在單個(現有main)執行緒中執行。沒有執行緒建立,就和呼叫普通的方法是一樣的。

R1 r1 = new R1();R2 r2 = new R2();
r1和r2類的兩個不同物件實現Runnable介面,從而實現run()方法。當你呼叫r1.run()您正在當前執行緒中執行它。

new thread().start:起了獨立的執行緒。

Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);
t1和t2是類的物件。 當執行t1.start(),它啟動一個新執行緒並呼叫run()方法在內部執行這個新執行緒。

四、Java中Synchronized的用法(簡單介紹)
參考:
https://www.cnblogs.com/weibanggang/p/9470718.html


synchronized是Java中的關鍵字,是一種同步鎖。它修飾的物件有以下幾種:
  1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;
  2. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件;
  3. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;
  4. 修改一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。

修飾一個程式碼塊:
1、一個執行緒訪問一個物件中的synchronized(this)同步程式碼塊時,其他試圖訪問該物件的執行緒將被阻塞。
2、當一個執行緒訪問物件的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該物件中的非synchronized(this)同步程式碼塊。
修飾一個方法:
Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized,public synchronized void method(){}; synchronized修飾方法和修飾一個程式碼塊類似,只是作用範圍不一樣,修飾程式碼塊是大括號括起來的範圍,而修飾方法範圍是整個函式。


修飾一個靜態的方法:

我們知道靜態方法是屬於類的而不屬於物件的。同樣的,synchronized修飾的靜態方法鎖定的是這個類的所有物件。

修飾一個類:

class ClassName {
public void method() {
synchronized(ClassName.class) {

}
}
}
鎖定的是這個類的所有物件。


總結:
1、 無論synchronized關鍵字加在方法上還是物件上,如果它作用的物件是非靜態的,則它取得的鎖是物件;如果synchronized作用的物件是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的物件同一把鎖。
2、每個物件只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以執行它所控制的那段程式碼。
3、實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制

關於synchronized(this)中this指的是什麼意思:

參考:https://www.cnblogs.com/uoar/p/7202358.html

public class SynchronizedDEmo {

public static void main(String[] args) {

TestThread tt = new TestThread();

Thread t1 = new Thread(tt);

Thread t2 = new Thread(tt);

t1.setName("t1");

t2.setName("t2");

t1.start(); t2.start();


}

public static void main2(String[] args) {

TestThread tt1 = new TestThread();
TestThread tt2 = new TestThread();

Thread t1 = new Thread(tt1);

Thread t2 = new Thread(tt2);

t1.setName("t1");

t2.setName("t2");

t1.start(); t2.start();


}

}

class TestThread implements Runnable{

private static int num = 0;

public void run() {

synchronized(this){ //此處this指的是 TestThread tt = new TestThread()物件,如果t1進來了,那麼 t1獲得了次物件的鎖,因為t1和t2使用的是同一個物件tt,一個物件對應一把鎖,他們是互斥的,t2走到此處也只能在上一句程式碼處等待t1獲得了時間片後執行完synchronized鎖住的所有程式碼,t2才能進去執行,若去掉synchronized(this),則t1和t2隨時都可以進來執行此段程式碼中的任何一步,時間到了另一個接著進來執行。
//如果使用main2,tt1和tt2是兩個物件,對應兩把鎖,則不互斥,則執行緒1和執行緒2可以同時執行這個同步程式碼塊。

for( int i = 0; i < 20 ; i++){

num ++ ;

try {
Thread.sleep(100);
} catch (InterruptedException e) {

e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" + num);

}

}

}
}


五、java併發程式設計:Executor、Executors、ExecutorService

可參考:https://blog.csdn.net/weixin_40304387/article/details/80508236 講解的比較好

Executor框架是Java 5中引入的,其內部使用了執行緒池機制,它在java.util.cocurrent 包下,通過該框架來控制執行緒的啟動、執行和關閉,可以簡化併發程式設計的操作。因此,在Java 5之後,通過Executor來啟動執行緒比使用Thread的start方法更好,更易管理,效率更好。Eexecutor作為靈活且強大的非同步執行框架,其支援多種不同型別的任務執行策略,提供了一種標準的方法將任務的提交過程和執行過程解耦開發,基於生產者-消費者模式,其提交任務的執行緒相當於生產者,執行任務的執行緒相當於消費者,並用Runnable來表示任務,Executor的實現還提供了對生命週期的支援,以及統計資訊收集,應用程式管理機制和效能監視等機制。

Executor框架包括:執行緒池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

5.1、 Executor和ExecutorService
Executor:一個介面,其定義了一個接收Runnable物件的方法executor,其方法簽名為executor(Runnable command),該方法接收一個Runable例項,它用來執行一個任務,任務即一個實現了Runnable介面的類,一般來說,Runnable任務開闢在新執行緒中的使用方法為:new Thread(new RunnableTask())).start(),但在Executor中,可以使用Executor而不用顯示地建立執行緒:executor.execute(new RunnableTask()); // 非同步執行

ExecutorService:是一個比Executor使用更廣泛的子類介面,其提供了生命週期管理的方法,返回 Future 物件,以及可跟蹤一個或多個非同步任務執行狀況返回Future的方法;可以呼叫ExecutorService的shutdown()方法來平滑地關閉 ExecutorService,呼叫該方法後,將導致ExecutorService停止接受任何新的任務且等待已經提交的任務執行完成(已經提交的任務會分兩類:一類是已經在執行的,另一類是還沒有開始執行的),當所有已經提交的任務執行完畢後將會關閉ExecutorService。因此我們一般用該介面來實現和管理多執行緒。

通過 ExecutorService.submit() 方法返回的 Future 物件,可以呼叫isDone()方法查詢Future是否已經完成。當任務完成時,它具有一個結果,你可以呼叫get()方法來獲取該結果。你也可以不用isDone()進行檢查就直接呼叫get()獲取結果,在這種情況下,get()將阻塞,直至結果準備就緒,還可以取消任務的執行。Future 提供了 cancel() 方法用來取消執行 pending 中的任務。

5.2、Executors類: 主要用於提供執行緒池相關的操作

Executors類,提供了一系列工廠方法用於建立執行緒池,返回的執行緒池都實現了ExecutorService介面。

1、public static ExecutorService newFiexedThreadPool(int Threads) 建立固定數目執行緒的執行緒池。

2、public static ExecutorService newCachedThreadPool():建立一個可快取的執行緒池,呼叫execute 將重用以前構造的執行緒(如果執行緒可用)。如果沒有可用的執行緒,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。


3、public static ExecutorService newSingleThreadExecutor():建立一個單執行緒化的Executor。

4、public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用來替代Timer類。

5.3、Executor VS ExecutorService VS Executors
這三者均是 Executor 框架中的一部分。 總結一下這三者間的區別,以便大家更好的理解:

1、Executor 和 ExecutorService 這兩個介面主要的區別是:ExecutorService 介面繼承了 Executor 介面,是 Executor 的子介面
2、Executor 和 ExecutorService 第二個區別是:Executor 介面定義了 execute()方法用來接收一個Runnable介面的物件,而 ExecutorService 介面中的 submit()方法可以接受Runnable和Callable介面的物件。
3、Executor 和 ExecutorService 介面第三個區別是 Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法可以通過一個 Future 物件返回運算結果。
4、Executor 和 ExecutorService 介面第四個區別是除了允許客戶端提交一個任務,ExecutorService 還提供用來控制執行緒池的方法。比如:呼叫 shutDown() 方法終止執行緒池。
5、Executors 類提供工廠方法用來建立不同型別的執行緒池。比如: newSingleThreadExecutor() 建立一個只有一個執行緒的執行緒池,newFixedThreadPool(int numOfThreads)來建立固定執行緒數的執行緒池,newCachedThreadPool()可以根據需要建立新的執行緒,但如果已有執行緒是空閒的會重用已有執行緒。

5.4、自定義執行緒池

//建立等待佇列
BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);
//建立執行緒池,池中儲存的執行緒數為3,允許的最大執行緒數為5
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)

根據ThreadPoolExecutor原始碼前面大段的註釋,我們可以看出,當試圖通過excute方法將一個Runnable任務新增到執行緒池中時,按照如下順序來處理:
1、如果執行緒池中的執行緒數量少於corePoolSize,即使執行緒池中有空閒執行緒,也會建立一個新的執行緒來執行新新增的任務;
2、如果執行緒池中的執行緒數量大於等於corePoolSize,但緩衝佇列workQueue未滿,則將新新增的任務放到workQueue中,按照FIFO的原則依次等待執行(執行緒池中有執行緒空閒出來後依次將緩衝佇列中的任務交付給空閒的執行緒執行);

3、如果執行緒池中的執行緒數量大於等於corePoolSize,且緩衝佇列workQueue已滿,但執行緒池中的執行緒數量小於maximumPoolSize,則會建立新的執行緒來處理被新增的任務;


4、如果執行緒池中的執行緒數量等於了maximumPoolSize,有4種處理方式(該構造方法呼叫了含有5個引數的構造方法,並將最後一個構造方法為RejectedExecutionHandler型別,它在處理執行緒溢位時有4種方式,這裡不再細說,要了解的,自己可以閱讀下原始碼)。

總結起來,也即是說,當有新的任務要處理時,先看執行緒池中的執行緒數量是否大於corePoolSize,再看緩衝佇列workQueue是否滿,最後看執行緒池中的執行緒數量是否大於maximumPoolSize。

另外,當執行緒池中的執行緒數量大於corePoolSize時,如果裡面有執行緒的空閒時間超過了keepAliveTime,就將其移除執行緒池,這樣,可以動態地調整執行緒池中執行緒的數量。


下面說說幾種排隊的策略:

1、直接提交。緩衝佇列採用 SynchronousQueue,它將任務直接交給執行緒處理而不保持它們。如果不存在可用於立即執行任務的執行緒(即執行緒池中的執行緒都在工作),則試圖把任務加入緩衝佇列將會失敗,因此會構造一個新的執行緒來處理新新增的任務,並將其加入到執行緒池中。直接提交通常要求無界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒絕新提交的任務。newCachedThreadPool採用的便是這種策略。

2、無界佇列。使用無界佇列(典型的便是採用預定義容量的 LinkedBlockingQueue,理論上是該緩衝佇列可以對無限多的任務排隊)將導致在所有 corePoolSize 執行緒都工作的情況下將新任務加入到緩衝佇列中。這樣,建立的執行緒就不會超過 corePoolSize,也因此,maximumPoolSize 的值也就無效了。當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界佇列。newFixedThreadPool採用的便是這種策略。

3、有界佇列。當使用有限的 maximumPoolSizes 時,有界佇列(一般緩衝佇列使用ArrayBlockingQueue,並制定佇列的最大長度)有助於防止資源耗盡,但是可能較難調整和控制,佇列大小和最大池大小需要相互折衷,需要設定合理的引數。

5.5、比較Executor和new Thread()
new Thread的弊端如下:

a. 每次new Thread新建物件效能差。
b. 執行緒缺乏統一管理,可能無限制新建執行緒,相互之間競爭,及可能佔用過多系統資源導致宕機或oom。
c. 缺乏更多功能,如定時執行、定期執行、執行緒中斷。
相比new Thread,Java提供的四種執行緒池的好處在於:
a. 重用存在的執行緒,減少物件建立、消亡的開銷,效能佳。
b. 可有效控制最大併發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
c. 提供定時執行、定期執行、單執行緒、併發數控制等功能。