併發的相關概念
- 進程:
操作系統必須全方位的管理計算機系統中運行的程序,因此操作系統爲正在運行的程序建立了一個管理實體——進程,進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動,進程是操作系統進行資源分配和調度的一個獨立單位。 - 線程
線程是輕量級的進程,是程序執行的最小單位。使用多線程而不使用多進程,是應爲線程之間的切換和調度成本遠小於進程。一個進程可以包含多個線程 - 並行和併發
並行是真正意義上的多個任務同時執行,併發實際上任務仍然是串行的,只不過是多個任務交替執行 - 臨界區
共享資源或者說是共享數據 - 原子性,可見性,有序性
原子性指一個操作是不能被中斷的,即使是在多個線程一起執行的時候,一個操作一旦開始就不會被其他線程干擾。
可見性是指一個線程修改了一個共享的變量之後,其他線程是否能夠馬上知道這個數據被修改了。
有序性指在併發的操作中很有可能會發生亂序的情況即指令重排。
線程的狀態
關於線程的狀態有很多說法,大家可以參考操作系統中進程的狀態,不過在java.lang.Thread中給出了線程的狀態只有六種:
public enum State {
NEW,//表示剛剛創建,但是還沒有開始執行,等待start()方法調用
RUNNALE,//當線程執行的時候,,表示線程一切的資源都已經準備好了,就緒
BLOCKED,//當線程在執行的時候遇到了synchronized同步代碼塊,進入到blocked(阻塞)狀態直到獲得請求的鎖
WAITING,//無限期地等待另一個線程來執行某一特定操作的線程處於這種狀態。
TIME_WAITING,//等待另一個線程來執行取決於指定等待時間的操作的線程處於這種狀態。
TERMINATED//當線程執行完畢之後
}
線程中斷
線程中斷並不會使線程立即退出,而是給線程發送一個通知。告知目標線程需要退出,而目標線程接到通知後會如何處理,完全由目標線程決定。
- 關於線程中斷的三個方法
public void interrupt() //中斷當前線程(中斷標記)
public boolean isInterrupted()// 判斷是否被中斷
public static boolean interrupted()// 判斷是否被中斷 ,並清除中斷狀態
示例
public static void main(String[] ars) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if(Thread.currentThread().isInterrupted())
break;
System.out.println("222");
}
}
};
t1.start();
Thread.sleep(400);
t1.interrupt();
}
}
注意的是:雖然中斷了線程但是如果沒有做中斷處理邏輯,即使被置上中斷狀態,這個中斷也不會發生作用,只是做上一個標記。
Thread.sleep()
Thread.sleep()方法會讓當前線程休眠一段時間,並拋出一個InterruptedException.這個異常不是運行時異常,必須捕獲並處理它。如果當前線程正在休眠時被中斷就會拋出這個異常。並且會清除中斷標記。所以在必要的時候我們必須在異常的處理中重新設置中斷標記。
public class Demo3 {
public static void main(String[] ars) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("中斷");
break;
}
System.out.println("222");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("正在休眠");
//重新設置中斷標記
//Thread.currentThread().interrupt();
}
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
}
我們發現如果不在異常捕獲中重新設置中斷標記,在t1休眠的時候會清除中斷標記,導致下一次循環無法在捕獲這個中斷位
等待(wait)通知(notify)
public final void notify()
public final void notifyAll()
public final void wait(long timeout)throws InterruptedException
//這三個方法都是object中的方法
如果一個線程調用了obj.wait() ,就會進入object對象等待隊列,在這個隊列中可能會有多個線程。因爲系統中可能運行多個線程等待一個對象,當obj.notify()被調用時,他會從這個隊列中隨機選擇一個線程,將其喚醒。這個選擇完全是隨機的。但是在調用wait()方法的時候必須要先獲得object對象的監視器,執行之後會釋放這個監視器。這樣做的目的是使其他在等待object對象上的線程不至於因爲T1線程的休眠而全部無法正常執行。而T2在執行notify()之前也必須獲得object的監視器。接着喚醒一個等待的線程,T1被喚醒之後第一件事是重新獲得object的監視器。只有順利獲得這個監視器才能順利執行下面的代碼。在消費者和生產者模型會具體說明這兩個方法。
面試相關:解釋Object.wait()和Thread.sleep()。
上面的區別很詳細了,但是最主要的一個區別就是wait()會釋放目標對象的鎖,而sleep()不會釋放任何資源。
掛起(suspend)和繼續執行(resume)
掛起和執行時一對相反的操作,掛起起後導致線程暫停,只有resume後纔會繼續執行線程但是他們已經被廢棄了,因爲掛起不會釋放任何的鎖資源,只有當resume,掛起的線程才能被繼續執行,由於鎖不會被釋放會導致整個系統異常,而且從線程的狀態上看竟然還是Runnable,會嚴重影響我們對系統當前狀態的判斷。
等待線程結束(join)和謙讓(yield)
public final void join() throws InterruptedException 無限阻塞當前線程直到目標線程執行完畢
public final void join(long millis,int nanos)throws InterruptedException 給出等待的最大時間
示例
public class Demo4 {
public volatile static int i = 0;
public static class AddThread extends Thread {
@Override
public void run() {
for (i = 0; i < 1000; i++) {
}
}
}
// 在主函數中如果沒有at.join(),那麼得到的i可能是一個非常小的數或者是0
// 因爲AddThread還沒有開始執行主線程就已經執行完畢了。
// 但是在加入at.join()表示主線程願意等待AddThread執行完畢之後,再執行主線程。
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
public static void yield()
//他會使當前線程放棄CPU的執行權,但是不表示當前線程不執行了。仍然會進行CPU的資源爭奪。但是是隨機的
volatile關鍵字
當一個變量可能會不斷被修改的時候使用volatile修飾。使所有的線程都能“看到”這個改動。可以簡單的解決原子性和可見性的問題。
守護線程
守護線程是一種特殊的線程,它在後臺默默的執行一些系統性的服務,例如垃圾回收線程,JIT線程,與之對應的用戶線程。當用戶線程全部結束意味着這個程序的業務操作已經全部完成,守護線程要守護的對象已經不存在了。所以當一個java應用只有守護線程的時候,java虛擬機會自然退出。設置守護線程:Thread.setDeamon(true),但是必須要在線程start()之前設置。
內部鎖(sychronized)
三種方式:
指定對象加鎖對象,必須是同一個對象
直接作用於實例方法(使用當前類的對象this同步)
直接作用於靜態方法(使用Class字節碼文件對象同步)
後面會具體示範內部鎖的使用
該隨筆主要閱讀《Java高併發程序設計》總結