從我開始寫博客到如今,已經寫了很多關於併發編程的了,差很少還有一半內容整個併發編程系列就結束了,而今天這篇博客是比較簡單的,只是介紹下併發編程的基礎知識( = =!其實,對於大神來講,前面全部博客都是基礎)。原本我不太想寫這篇博客,由於這篇博客的不少內容都是以記憶爲主,並且網上也有大把大把的博客,都寫的至關不錯,可是我最終決定仍是要寫一寫,由於沒有這篇博客,併發編程系列就不能算是一個完整的系列。java
說到線程,不得不說到進程,由於線程是沒法單獨存在的,它只是進程中的一部分。面試
進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位。線程則是進程的一個執行路徑,一個進程中至少有一個線程。操做系統在分配系統資源的時候,會把CPU資源分配給線程,由於真正執行工做,須要佔用CPU運行的是線程,因此也能夠說線程是CPU分配的基本單位。編程
在Java中,咱們啓動一個main函數,就啓動了一個JVM的進程,而main函數所在的線程被稱爲「主線程」。 每一個線程都有一個叫「程序計數器」的私有的內存區域,用來記錄當前線程下一個要執行的指令地址,爲何要把程序計數器設計成私有的呢?由於線程是佔用CPU的基本單位,而CPU通常是使用時間片輪轉的方式來讓線程佔有的,因此當某個線程的時間片用完後,要讓出CPU,等下一次得到時間片了,再繼續執行。那麼線程怎麼知道以前的程序執行到哪裏了呢?就是靠程序計數器。另外須要注意的是,若是執行的是native方法,那麼程序計數器記錄的是undefined地址。bash
線程有三種建立方式,分別是併發
class MyThread extends Thread {
@Override
public void run() {
System.out.println("run");
}
}
public class MyTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
複製代碼
class MyRannable implements Runnable {
@Override
public void run() {
System.out.println("run");
}
}
public class MyTest {
public static void main(String[] args) {
MyRannable myRannable = new MyRannable();
new Thread(myRannable).start();
}
}
複製代碼
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "MyCallable";
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String>futureTask=new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
複製代碼
相信前面兩種方式不用多說,你們都懂。咱們如今來看看第三種方式,首先定義了MyCallable類,並實現了Callable接口的call方法。在main方法中建立了FutureTask對象,傳入了MyCallable對象,而後用Thread包裝了FutureTask對象,隨後啓動,最後用futureTask提供的get方法獲取結果,獲取結果這一步是阻塞的。ide
在面試中,常常會問以下的問題:函數
關於死亡和阻塞狀態,其實說的不太完整,由於除了線程運行結束後這種「天然死亡」,還有一個狀況,就是被stop了,可是Java已經不推薦使用stop等操做了,因此就忘記吧,阻塞也是一樣的道理,也不推薦使用suspend方法了,也忘記它把。網站
在Java中,每一個對象都繼承了Object類,而在Object類中提供了通知和等待的操做,因此每一個對象都有這樣的操做,既然是線程的通知與等待,爲何要把它定義在Object類中?由於Java提供的鎖,鎖的是對象,而不是方法或是線程,因此天然要定義在Object類中。ui
當一個線程調用共享變量的wait方法後,該線程會被阻塞掛起,直到發生如下的兩個事情才返回:this
class MyRunnable implements Runnable {
Object object=new Object();
@Override
public void run() {
try {
synchronized (object){
object.wait();
System.out.println("run");
}
} catch (InterruptedException e) {
System.out.println("被中斷了");
e.printStackTrace();
}
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread = new Thread(new MyRunnable());
thread.start();
thread.interrupt();
}
}
複製代碼
運行結果:
被中斷了
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.codebear.MyRunnable.run(MyTest.java:13)
at java.lang.Thread.run(Thread.java:748)
複製代碼
首先新建了一個子線程,子線程內部獲取了object的監視器鎖,隨後調用object的wait方法阻塞當前線程,主線程調用interrupt方法中斷子線程,子線程被返回,而且產生了異常。
這也就是爲何咱們在調用共享變量的wait方法的時候,Java「死皮賴臉」的要咱們對異常進行處理的緣由:
調用wait方法後,還會釋放對共享變量的監視器鎖,讓其餘線程能夠進入臨界區:
class MyRunnable implements Runnable {
@Override
public void run() {
try {
synchronized (MyRunnable.class) {
System.out.println("我是" + Thread.currentThread().getName() + ",我進入了臨界區");
MyRunnable.class.wait();
Thread.sleep(Integer.MAX_VALUE);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("run");
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
thread1.start();
Thread thread2 = new Thread(myRunnable);
thread2.start();
}
}
複製代碼
運行結果:
我是Thread-1,我進入了臨界區
我是Thread-0,我進入了臨界區
複製代碼
能夠很清楚的看到,兩個線程都進入了臨界區。 線程A獲取了共享對象的監視器鎖後,進入了臨界區,線程B只能等待,線程A調用了共享對象的wait方法後,釋放了共享對象的監視器鎖,讓線程B也能夠得到共享變量的監視器鎖,而且進入臨界區。
在調用共享變量的wait方法前,必須先對該共享變量進行synchronized操做,不然會拋出IllegalMonitorStateException異常:
class MyRunnable implements Runnable {
Object object = new Object();
@Override
public void run() {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("run");
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread = new Thread(new MyRunnable());
thread.start();
thread.interrupt();
}
}
複製代碼
運行結果:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.codebear.MyRunnable.run(MyTest.java:12)
at java.lang.Thread.run(Thread.java:748)
複製代碼
另外須要注意的是,一個線程雖然從阻塞掛起的狀態到就緒的狀態,可是可能其餘線程並無喚醒它,這就是虛假喚醒,雖然虛假喚醒在實踐中不多發生,可是防患於未然,比較嚴謹的作法就是在wait方法外面,包裹一個while循環,while循環的條件就是檢測是否知足了被喚醒的條件,這樣即便虛假喚醒發生了,該線程被返回了,因爲被while包裹了,發現並無知足被喚醒的條件,又會被再次wait。 以下所示:
while(是否知足了被喚醒的條件) {
object.wait();
}
複製代碼
wait方法是將當前線程阻塞掛起,那麼一定有一個方法是喚醒此線程的,就像沉睡的白雪公主也在等待王子的到來,將她喚醒同樣。 被喚醒的線程不能立刻從wait方法處返回,而且繼續執行,由於還須要再次獲取共享變量的監視器鎖(由於調用wait方法後,已經釋放了監視器,因此這裏須要再次獲取)。 若是有多個線程都調用了共享變量的wait方法而被阻塞掛起,那麼調用notify方法後,只會隨機喚醒其中一個線程。 還有一點尤爲須要注意:當調用共享變量的notify方法後,並無釋放共享變量的監視器鎖,只有退出臨界區或者調用wait方法後,纔會釋放共享變量的監視器鎖,咱們能夠作一個實驗:
class CodeBearRunnable implements Runnable {
private Object object = new Object();
@Override
public void run() {
synchronized (object) {
object.notify();
System.out.println("我是" + Thread.currentThread().getName() + LocalDateTime.now());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class NotifyTest {
public static void main(String[] args) {
CodeBearRunnable codeBearRunnable = new CodeBearRunnable();
new Thread(codeBearRunnable).start();
new Thread(codeBearRunnable).start();
}
}
複製代碼
運行結果:
我是Thread-02019-04-28T18:12:19.195
我是Thread-12019-04-28T18:12:22.196
複製代碼
咱們來分析下代碼:當線程A獲取了共享變量的監視器鎖,進入了臨界區,調用共享變量的notify方法,打印出當前的時間,隨後sleep當前線程3秒。若是notify方法會釋放鎖,那麼線程B打印出來時間和線程A打印出來的時間應該相差不大,可是能夠很清楚的看到,打印出來的時間相差了3秒,說明了線程A調用共享變量的notify方法後,並無釋放共享變量的鎖,只有退出了臨界區,才釋放了共享變量的鎖。
若是有多個線程都調用了共享變量的wait方法而被阻塞掛起,那麼調用notifyAll方法後,全部線程都會被喚醒。
最後,咱們用一個常見的面試題來熟悉下wait/notify的應用:兩個線程交替打印奇偶數:
class MyRunnable implements Runnable {
static private int i = 0;
@Override
public void run() {
try {
while (i < 100) {
synchronized (MyRunnable.class) {
MyRunnable.class.notify();
MyRunnable.class.wait();
System.out.println("我是" + Thread.currentThread() + ":" + i++);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
Thread thread2 = new Thread(new MyRunnable());
thread2.start();
}
}
複製代碼
運行結果:
在開發中,咱們常常會遇到這樣的需求:等待某些事情都完成後,才能夠繼續執行。好比旅遊網站查詢某個產品的航班,航班能夠分爲去程和返程,咱們能夠開兩個線程同時查詢去程和返程的航班,等他們的結果都返回後,再執行其餘操做。
class GoRunnable implements Runnable {
@Override
public void run() {
System.out.println("查詢去程航班");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ReturnRunnable implements Runnable {
@Override
public void run() {
System.out.println("查詢返程航班");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread1 = new Thread(new GoRunnable());
thread1.start();
Thread thread2 = new Thread(new ReturnRunnable());
thread2.start();
System.out.println("開始查詢航班,如今的時間是"+ LocalDateTime.now());
thread1.join();
thread2.join();
System.out.println("航班查詢完畢,如今的時間是"+ LocalDateTime.now());
}
}
複製代碼
運行結果:
查詢去程航班
查詢返程航班
開始查詢航班,如今的時間是2019-04-28T21:18:05.719
航班查詢完畢,如今的時間是2019-04-28T21:18:10.654
複製代碼
若是是同步查詢,那麼查詢航班的耗時應該在(5+3)秒左右,如今利用線程+join方法,兩個線程同時執行,耗時5秒左右(取決於慢的那個),在實際項目中,能夠提高用戶的體驗,大幅提升查詢的效率。
這裏僅僅是演示join的功能,若是在實際項目中遇到這樣的場景應該不會用join這麼「粗糙」的方法。
讓咱們再來看看當join遇到interrupt方法會擦出怎樣的火花:
class GoRunnable implements Runnable {
@Override
public void run() {
System.out.println("查詢去程航班");
for (; ; ) {
}
}
}
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread1 = new Thread(new GoRunnable());
thread1.start();
Thread.currentThread().interrupt();
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println("主線程" + e.toString());
}
}
}
複製代碼
運行結果:
主線程java.lang.InterruptedException
查詢去程航班
複製代碼
子線程內部是一個死循環,執行子線程後,中斷主線程,在主線程中的thread1.join處拋出了異常。可是須要注意的是,由於中斷的是主線程,因此是在主線程中拋出異常,這裏我用try包住thread1.join()只是爲了更好的展示錯誤,其實這裏並不強制要求對異常進行捕獲。
原本想用一篇博客就結束併發編程基礎的,可是寫起來才發現想多了,一是想把每一個知識點都說的清楚一點,並給出各類例子來幫助你們更好的理解,二是併發編程基礎的知識點確實挺多的,因此仍是分兩篇博客來吧。