理解Java執行緒
使用多執行緒的目的是更好的利用cpu資源,大部分多執行緒程式碼都可以用單執行緒來實現,但也有無法用單執行緒實現的,如:生產者消費者模型
下面對一些常用的概念進行區分:
多執行緒:指的是這個程式(一個程序)執行時產生了不止一個執行緒。
並行與併發:
並行:多個cpu例項或者多臺機器同時執行一段處理邏輯,真正的同時。
併發:通過cpu排程演算法,讓使用者看上去同時執行,實際上從cpu操作層面不是真正的同時。
執行緒安全:經常用來描繪一段程式碼。指在併發的情況之下,該程式碼經過多執行緒使用,執行緒的排程順序不影響任何結果。這個時候使用多執行緒,我們只需要關注系統的記憶體,cpu是不是夠用即可。若執行緒不安全則意味著執行緒排程順序影響最終結果。如轉賬操作。
同步:Java中的同步指的是通過人為的控制和排程,保證共享資源的多執行緒訪問成為執行緒安全,來保證結果的準確。如在轉賬中加入@synchronized關鍵字。在保證結果準確的同時,提高效能,才是優秀的程式。
執行緒的狀態
執行緒共有五種狀態,其狀態轉換如下圖所示:
- 新建態(New):新建立的執行緒物件。
- 就緒態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於“可執行執行緒池”中,變得可執行,只等待獲取CPU的使用權。 即在就緒狀態的程序除CPU之外,其它的執行所需資源都已全部獲得。
- 執行態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
- 死亡態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
- 阻塞態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。
阻塞的情況分三種:
- 等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入“等待池”中。進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()或notifyAll()方法才能被喚醒。
- 同步阻塞:執行的執行緒在獲取物件的同步鎖時(Synchronized),同步鎖被釋放進入可執行狀態(Runnable),若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入“鎖池”中。
- 其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
注:在runnable狀態的執行緒是處於被排程的執行緒,此時的排程順序是不一定的。Thread類中的yield方法可以讓一個running狀態的執行緒轉入runnable。
控制執行緒的基本方法
首先就要明確monitor的概念,Java中的每個物件都有一個監視器,來監測併發程式碼的重入。在非多執行緒編碼時該監視器不發揮作用,反之如果在synchronized 範圍內,監視器發揮作用。
sleep()
sleep()方法屬於Thread類,是一個靜態方法,主要的作用是讓當前執行緒停止執行,把cpu讓給其他執行緒執行,但不會釋放物件鎖和監控的狀態,到了指定時間後執行緒又會自動恢復執行狀態
Java有兩種sleep方法,一個只有一個毫秒引數,另一個有毫秒和納秒兩個引數
sleep(long millis)
sleep(long millis, int nanos)
注意:執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始執行。
wait()與notify()
wait()屬於Object類,與sleep()的區別是當前執行緒會釋放鎖,進入等待此物件的等待鎖定池。若執行緒A呼叫Obj.wait(),執行緒A就會停止執行,而轉為等待狀態。至於等待時間,看其他執行緒是否呼叫Obj.notify(),成為多個執行緒之間進行通訊的有手段。
注意:無論是wait()還是notify()都需要首先獲得目標的物件的一個監視器。wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某物件的監視器)。這意味著wait之後,其他執行緒可以進入同步塊執行。當某程式碼並不持有監視器的使用權時去wait或notify,會丟擲java.lang.IllegalMonitorStateException。也包括在synchronized塊中去呼叫另一個物件的wait/notify,因為不同物件的監視器不同,同樣會丟擲此異常。
"Synchronzied"是一種同步鎖。作用是實現執行緒間同步,對同步的程式碼加鎖,使得每一次,只能有一執行緒進入同步塊,從而保證執行緒間的安全性。它修飾的物件有以下幾種:
- 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的部分,進入同步程式碼前要獲得給定物件的鎖
- 修飾一個例項方法,進入同步程式碼前要獲得當前例項的鎖
- 修飾一個靜態方法,進入同步程式碼前要獲得當前類的鎖
public class Test {
final static Object object=new Object();
public static class Thread1 extends Thread{
@Override
public void run() {
synchronized (object) {
System.out.println("T1 開始");
try {
System.out.println("T1 等待");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1 結束");
}
}
}
public static class Thread2 extends Thread{
@Override
public void run() {
synchronized (object) {
System.out.println("T2 開始");
System.out.println("釋放一個執行緒");
object.notify();
System.out.println("T2 結束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread1();
Thread t2=new Thread2();
t1.start();
t2.start();
t1.join();
t2.join();
}
}
執行結果:
T1 開始
T1 等待
T2 開始
釋放一個執行緒
T2 結束
T1 結束
如下為生產者消費者模型:
public class Test {
private final int MAX_SIZE = 100;
private LinkedList<Object> list = new LinkedList<Object>();
/**
* 生產產品
* @param producer
*/
public void produce(String producer) {
synchronized (list) {
// 如果倉庫已滿
while (list.size() == MAX_SIZE) {
System.out.println("倉庫已滿,【"+producer+"】: 暫時不能執行生產任務!");
try {
// 由於條件不滿足,生產阻塞
list.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生產產品
list.add(new Object());
System.out.println("【"+producer+"】:生產了一個產品\t【現倉儲量為】:" + list.size());
list.notifyAll();
}
}
/**
* 消費產品
* @param consumer
*/
public void consume(String consumer) {
synchronized (list) {
//如果倉庫儲存量不足
while (list.size()==0) {
System.out.println("倉庫已空,【"+consumer+"】: 暫時不能執行消費任務!");
try {
// 由於條件不滿足,消費阻塞
list.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【"+consumer+"】:消費了一個產品\t【現倉儲量為】:" + list.size());
list.notifyAll();
}
}
public static void main(String[] args){
Test test = new Test();
for(int i=1;i<6;i++){
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
test.produce(String.format("生產者%d:", finalI));
}
}).start();
}
for(int i=1;i<4;i++){
int finalI = i;
new Thread(()-> test.consume(String.format("消費者%d:", finalI))).start();
}
}
}
執行結果:
倉庫已空,【消費者2:】: 暫時不能執行消費任務!
倉庫已空,【消費者1:】: 暫時不能執行消費任務!
【生產者4:】:生產了一個產品 【現倉儲量為】:1
【生產者1:】:生產了一個產品 【現倉儲量為】:2
【消費者3:】:消費了一個產品 【現倉儲量為】:1
【消費者1:】:消費了一個產品 【現倉儲量為】:0
【生產者2:】:生產了一個產品 【現倉儲量為】:1
【消費者2:】:消費了一個產品 【現倉儲量為】:0
【生產者5:】:生產了一個產品 【現倉儲量為】:1
【生產者3:】:生產了一個產品 【現倉儲量為】:2
join()
在某些情況下,子執行緒需要進行大量的耗時運算,主執行緒可能會在子執行緒執行結束之前結束,但是如果主執行緒又需要用到子執行緒的結果,換句話說,就是主執行緒需要在子執行緒執行之後再結束。這就需要用到join()方法
public class Test {
public static int count;
public static class AddThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000000000; i++) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread addThread=new AddThread();
Thread t1=new Thread(addThread);
t1.start();
t1.join();
System.out.println(count);
}
}
yield()
一個執行緒呼叫yield()意味著告訴虛擬機器可以把自己的位置讓給其他執行緒(這只是暗示,並不表絕對)。但要注意,讓出cpu並不代表當前執行緒不執行了。當前執行緒讓出cpu後,還會進行cpu資源的爭奪,但是能不能再次分配到,就不一定了。使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
volatile
多執行緒的記憶體模型:main memory(主存)、working memory(執行緒棧),在處理資料時,執行緒會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變數的操作都激發一次load and save)。
針對多執行緒使用的變數如果不是volatile或者final修飾的,很有可能產生不可預知的結果(另一個執行緒修改了這個值,但是之後在某執行緒看到的是修改之前的值)。其實道理上講同一例項的同一屬性本身只有一個副本。但是多執行緒是會快取值的,本質上,volatile就是不去快取,直接取值。線上程安全的情況下加volatile會犧牲效能。
執行緒建立
Java多執行緒實現方式主要有四種:繼承Thread類、實現Runnable介面、實現Callable介面通過FutureTask包裝器來建立Thread執行緒、使用ExecutorService、Callable、Future實現有返回結果的多執行緒。
其中,其中前兩種方式執行緒執行完後都沒有返回值,後兩種是帶返回值的。
繼承Thread類建立執行緒
Thread類本質上是實現了Runnable介面的一個例項,代表一個執行緒的例項。啟動執行緒的唯一方法就是通過Thread類的start()例項方法。start()方法是一個native方法,它將啟動一個新執行緒,並執行run()方法。這種方式實現多執行緒很簡單,通過自己的類直接extend Thread,並複寫run()方法,就可以啟動新執行緒並執行自己定義的run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
實現Runnable介面建立執行緒
如果自己的類已經extends另一個類,就無法直接extends Thread,此時,可以實現一個Runnable介面,程式碼如下:
public class MyThread implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
實現Callable介面,通過FutureTask包裝器建立執行緒
/**
* 實現Callable介面建立執行緒,相較於實現Runnable介面的方式,方法可以有返回值,並且可以丟擲異常
* 執行Callable方式,需要FutureTask實現類的支援,用於接收運算結果
*/
public class Test {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
// 執行Callable方式,需要FutureTask實現類的支援,用於接收運算結果
FutureTask<Integer> result = new FutureTask<>(td);
new Thread(result).start();
// 接收執行緒運算後的結果
Integer sum;
try {
//等所有執行緒執行完,獲取值,因此FutureTask 可用於 閉鎖
sum = result.get();
System.out.println("-----------------------------");
System.out.println(sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
return sum;
}
}
使用執行緒池建立返回結果的執行緒
使用ExecutorService、Callable、Future實現有返回結果的執行緒,ExecutorService、Callable、Future三個介面實際上都是屬於Executor框架。返回結果的執行緒是在JDK1.5中引入的新特徵,有了這種特徵可以很方便的得到返回值了,可返回值的任務必須實現Callable介面。類似的,無返回值的任務必須實現Runnable介面。
執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了。
注意:get方法是阻塞的,即:執行緒無返回結果,get方法會一直等待。再結合線程池介面ExecutorService就可以實現傳說中有返回結果的多執行緒了。
public class Demo {
public static void main(String[] args) throws Exception {
System.out.println("----程式開始執行----");
Date date1 = new Date();
int taskSize = 5;
// 建立一個執行緒池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 建立多個有返回值的任務
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 執行任務並獲取Future物件
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 關閉執行緒池
pool.shutdown();
// 獲取所有併發任務的執行結果
for (Future f : list) {
// 從Future物件上獲取任務的返回值,並輸出到控制檯
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程式結束執行----,程式執行時間【"+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任務啟動");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任務終止");
return taskNum + "任務返回執行結果,當前任務時間【" + time + "毫秒】";
}
}
執行結果:
----程式開始執行----
>>>0 任務啟動
>>>2 任務啟動
>>>4 任務啟動
>>>1 任務啟動
>>>3 任務啟動
>>>2 任務終止
>>>4 任務終止
>>>0 任務終止
>>>0 任務返回執行結果,當前任務時間【1000毫秒】
>>>1 任務終止
>>>3 任務終止
>>>1 任務返回執行結果,當前任務時間【1000毫秒】
>>>2 任務返回執行結果,當前任務時間【1000毫秒】
>>>3 任務返回執行結果,當前任務時間【1000毫秒】
>>>4 任務返回執行結果,當前任務時間【1000毫秒】
----程式結束執行----,程式執行時間【1074毫秒】
程式說明:
上述程式碼中Executors類,提供了一系列工廠方法用於建立執行緒池,返回的執行緒池都實現了ExecutorService介面。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目執行緒的執行緒池。
public static ExecutorService newCachedThreadPool()
建立一個可快取的執行緒池,呼叫execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。
public static ExecutorService newSingleThreadExecutor()
建立一個單執行緒化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用來替代Timer類。
ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor後臺執行緒池還沒有完成Callable的計算,這呼叫返回Future物件的get()方法,會阻塞直到計算完成。
本文參考了:
https://www.cnblogs.com/wxd0108/p/5479442.html
https://www.cnblogs.com/Ming8006/p/7243858.html
https://www.cnblogs.com/felixzh/p/6036074.html
https://www.cnblogs.com/ccfdod/p/6396012.html
http://www.cnblogs.com/jijijiefang/articles/7222955.html