1、多線程相關概念
1.1 什麼是多線程
在早期的計算機中時沒有操做系統的,計算機開啓後只能執行一個程序,直到結束。操做系統的出現使得計算機能夠同時執行多個程序,操做系統爲每一個程序分配不一樣的進程,每一個進程擁有獨立的句柄、資源等,使得計算機能夠同時執行多個程序。可是進程的建立和銷燬耗費的代價太大,所以衍生出線程的概念。容許在一個進程中建立多個線程,這些線程共享進程的資源,而且每一個線程擁有本身獨立的程序計數器、線程局部變量等資源。線程也被稱爲進程的輕量型運動實體。
1.2 線程的優點
線程的出現帶來不少的好處:
一、發揮多核處理器的性能:在多喝處理器上執行單線程任務是對多核的浪費,由於總有核心在空閒着,多線程的出現能充分發揮多核的優點。
二、化整爲零:每每在一個複雜的應用中包含許多不一樣類型的任務,將這些不一樣類型的任務分配給不一樣的線程去執行會比將其混在同一個線程中去執行要好,由於每一個線程更加的簡單清晰,更容易測試等。
三、異步事件處理:當一個線程處理的任務遇到阻塞時如IO阻塞,cpu能夠調度其餘線程去執行而不是在那傻傻的等到IO結束在執行其餘任務。
四、更好的用戶體驗:當多個用戶像你的服務發送請求時,你一個線程依次執行任務會使得排在後面的用戶等待時間過長得不到響應,帶來很差的體驗。但使用多個線程可讓每一個用戶都能很快的獲得響應(儘管這不能高執行速度),用戶會以爲本身的請求正在被處理,得到更好的體驗。
1.3 多線程引入的問題
多線程雖然帶來了許多優點,可是不當使用多線程也會引入許多問題。
一、安全性:多線程同時訪問操做共享資源會形成不可預料的結果。
二、活躍性問題:因爲多線程爭奪資源致使無限循環,出現死鎖、活鎖、線程飢餓等問題。
三、性能問題:多線程的引入是爲了提升處理器的性能,可是濫用線程不只不能提升性能反而會致使性能降低。如建立過多的線程使CPU頻繁地去執行線程切換致使性能下降。
2、Java中建立線程
在java中建立線程的方式有三種:繼承Thread類重寫run方法、實現runnable接口重寫run方法和實現callable接口重寫call方法配合futureTask使用
2.一、繼承Thread類
public class MyThread extends Thread{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
@Override
public void run() {
int i = 0;
for(;i<100;i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
繼承Thread類的方式建立線程算是最簡單的了,可是你的線程類每每要繼承項目中的其餘類,而Java是單繼承機制的,因此使用此方法會有很大的侷限性。
2.二、實現runnable接口
public class MyRunnable implements Runnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
@Override
public void run() {
int i = 0;
for (; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Thread類的構造方法容許傳入一個實現runnable接口的target進去,線程啓動將會執行target.run方法。實現runnable接口的方式能夠很好的避免單繼承問題。
2.三、實現callable接口
public static void main(String[] args) throws Exception {
System.out.println("12123");
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask).start();
// futureTask能夠在指定時間內獲取線程執行的返回值,超時則丟棄任務
// 所以futureTask能夠用做異步任務處理
futureTask.get(1000, TimeUnit.SECONDS);
}
@Override
public Integer call() throws Exception {
int i = 0;
for(;i<100;i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
return i;
}
call方法與run方法最大的區別在於call方法存在返回值futureTask的get方法能夠獲取這個返回值。使用此種方法實現線程的好處是當你建立的任務的結果不是當即就要時,你能夠提交一個線程在後臺執行,而你的程序仍能夠正常運行下去,在須要執行結果時使用futureTask去獲取便可。這是一種典型的異步任務處理的方法。
3、線程的各類狀態及其轉換
一個線程的生命週期包含如下五種狀態:
一、初始狀態:線程被new出來時處於此種狀態
二、就緒狀態:當一個線程的start方法被調用或正在運行的線程CPU時間片耗盡或處於阻塞狀態的線程知足了運行條件時處於此種狀態。就緒狀態的線程處於CPU調度隊列中等待得到CPU時間片執行。
三、運行狀態:處於就緒狀態的線程獲取到CPU時間片,正在執行。
四、阻塞狀態:當線程要執行的必要條件沒法知足時處於此種狀態,如等待其餘線程釋放鎖、等待IO等。當這些條件獲得知足後線程變爲就緒狀態。
五、死亡:當線程run方法執行完或者遇到異常退出時線程死亡,線程的生命週期就此終止。
如下爲線程狀態轉換圖:
4、經常使用方法總結
4.一、wait、notify和notifyAll
這三個方法並不屬於Thread類而是屬於Object類,這意味着在Java總全部的類都擁有這三個方法。
在Java中每一個對象均可以用做實現一個同步的鎖,這些鎖被稱爲內置鎖或者監視器鎖。當一個線程調用一個對象的wait方法時,他會讓本身(即當前線程)陷入等待狀態,並放棄所持有的鎖,當其餘線程再調用這個對象的notify或notify方法時會喚醒這個對象內置鎖上等待的線程,此時剛剛陷入等待的線程能夠在停下來的地方繼續向下執行。
wait方法:使調用此方法的線程當即進入阻塞狀態並放棄所持有的鎖。
notify與notifyAll:使當前線程放棄持有的某個對象上的鎖並喚醒在這個對象鎖上等待的其餘線程,notify喚醒單個線程,notifyAll喚醒所有線程。調用notify方法後當前線程會在執行完剩餘代碼後放棄該對象的鎖而不是像wait方法那樣當即釋放。
wait和notify方法能夠實現線程之間的通訊,典型的使用就是用來實現生產者和消費者問題--當隊列滿了生產者阻塞並喚醒消費者,當隊列空了消費者阻塞並喚醒生產者。
以下爲使用wait與notify實現的生產者消費者問題:
public class Test {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedBlockingQueue<>();
new Producer(queue).start();
new Customer(queue).start();
}
}
class Producer extends Thread {
Queue<Integer> queue;
public Producer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
try{
Thread.sleep(500);
} catch (Exception e) {}
if (queue.size() >= 3) {
try {
System.out.println("隊列已滿,生產者阻塞");
queue.wait();
} catch (InterruptedException e) {
}
}
// 喚醒消費者
queue.notify();
int i = new Random().nextInt(100);
System.out.println("生產" + i);
queue.add(i);
}
}
}
}
class Customer extends Thread {
Queue<Integer> queue;
public Customer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
try{
Thread.sleep(500);
} catch (Exception e) {}
if (queue.isEmpty()) {
try {
System.out.println("隊列已空,消費者阻塞");
queue.wait();
} catch (InterruptedException e) {
}
}
// 喚醒生產者
queue.notify();
int i = queue.poll();
System.out.println("消費:" + i);
}
}
}
}
運行結果以下:
4.二、join()方法
join方法爲Thread類全部方法,當在一個線程A中調用另外一個線程B的join方法時,線程A會等待線程B執行完再進行執行。
4.三、yield()方法
yield方法會發送一個通知給調度器表示當前線程願意讓出CPU時間片給其餘線程先執行,可是這種讓步策略並不能獲得保障,由於調度器能夠選擇忽視這個通知,這取決於當前下同的線程調度策略。
4.四、sleep(long millis)方法
sleep方法會使當前線程休眠指定時間,處於休眠的線程並不會放棄已經持有鎖,放不放棄鎖是sleep方法與wait方法的最大區別。
4.五、setPriority(int newPriority)
setPriority能夠設置線程的優先級,傳入一個1到10之間的整數,數值越大表示優先級越高,在一個母線程中建立一個子線程則子線程的優先級默認與母線程相同。
java中設置線程優先級有10個等級可選,可是這種等級制度在不一樣的系統中不能獲得很好的映射,某些系統可能沒有10個線程等級,某些則超過10個等級,Java中默認給咱們提供了三種可選等級,大部分時候使用這三種等級便可:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
4.六、stop()和suspend()
這兩個方法是被廢棄的方法。
stop():當即中止一個線程的運行並放棄他所持有的全部的鎖。
爲什麼棄用:這個當即中止是強制性的,你甚至能夠在線程還沒運行時就調用 stop使其強制停下,這樣線程在運行伊始就會被中止。顯然這種強制的中止一個線程並釋放鎖的方式是不安全的,由於當這個線程在處理時會使所持有鎖的對象處於中間狀態,這種狀態在系統中不該該存在,若是線程能正常執行或異常退出時,這種中間狀態是不會被其餘線程看到的。可是調用stop方法會使當前線程強制性的釋放這個對象的鎖,使其在中間狀態時就對其餘線程可見,這顯然會致使線程安全問題。
stop被棄用後的替代:stop不能使用時如何中止一個線程呢?官方推薦的方式是在線程運行時添加一個狀態變量,線程會不斷的檢查這個變量來決定是否停下,以下:
@Override
public void run() {
while(!isStop) {
doSomething();
}
}
run方法一旦發現isStop爲true會理解結束,這也是最好的中止線程的方法了。可是這種方式有個弊端:一旦doSomething方法執行時間過長或者在等待時會致使當前線程無暇去檢測isStop變量,此時建議使用interrupt()方法去中斷這個線程。
suspend():暫停一個線程可是不放棄所持有的鎖。
suspend被棄用緣由:suspend被調用後當前線程會暫停下來可是不會放棄持有的對象鎖,直到其餘的線程調用resume方法將此線程喚醒。若是喚醒線程須要拿到被暫停的線程所控制的鎖才能運行時會出現死鎖狀態,即喚醒線程不拿到鎖則沒法喚醒暫停線程,而暫停線程不被喚醒則沒法釋放鎖。所以suspend被棄用,他的兄弟resume方法天然也被棄用。
suspend與wait和sleep的區別:wait會使當前線程等待可是會釋放鎖,sleep不釋放鎖可是會本身喚醒本身,所以sleep與wait都不會死鎖。而suspend即不釋放鎖也不能本身喚醒,會產生死鎖因此被棄用。
4.七、interrupt()、interrupted()和isInterrupted()
理論上來說一個線程不能被別的線程中斷或終止,只能由本身中斷和終止,這也是stop方法被棄用的緣由。
interrupt:此方法會向此線程發送一個信號:你被中斷了,並將該線程的中斷標誌改成true。還記得stop方法被廢棄的替代方案嗎,使用一個標誌位來決定是否停下線程,interrupt方法實際上也是這樣操做的。該線程正常處理時會不斷的去讀取這個中斷標誌,發現爲true時則中止,當該線程處於阻塞狀態時(如調用了wait、sleep、join)會讓該線程當即拋出一個InterruptedException而且清除該線程的中斷狀態,這樣能夠直接進入該線程的catch代碼塊中進行執行,在catch中能夠對此線程進行特殊處理(當即結束或者作一些處理後再次運行)
interrupted:此方法會執行兩步操做:一、返回該線程的中斷標誌。二、將該線程的中斷標誌設置爲false。顯然這兩部操做是要知足原子性的,此方法也是被synchronized修飾過的。有些線程在被調用了interrupt方法後並不想停下,能夠在他的catch代碼塊中調用本身的interrupted方法,讓本身繼續正常運行。
isInterrupted:此方法會返回該線程的中斷標誌
java