全面深入的講解線程
線程超級詳解:http://blog.csdn.net/cuigx1991/article/details/48219741
需要掌握的知識點:
1)線程的概念
2)線程創(chuàng)建的幾種方式
3)線程的狀態(tài)及轉(zhuǎn)換
4)影響線程調(diào)度方式的幾個方法
5)線程的同步與鎖
6)線程交互協(xié)作
知識點一:線程創(chuàng)建的幾種方式
1.直接new Thread,實現(xiàn)runnable方法。
2.Thread構(gòu)造方法傳入Runnable接口
3.線程池創(chuàng)建
知識點二:線程安全(同步與鎖)
1.線程同步問題的產(chǎn)生
產(chǎn)生原因:多個線程同時訪問同一數(shù)據(jù)/方法
比如:同一線程A里的方法、代碼都是一行一行執(zhí)行的,但是再來個線程B,可能線程A執(zhí)行到某個方法某一行代碼的時候,這時線程B獲取了CPU執(zhí)行權(quán),線程B對線程A下面要執(zhí)行的一行代碼相關(guān)的變量作了改動,就會導(dǎo)致安全問題。
2.同步和鎖定
1)鎖的原理:一個對象(一般是一個實例,也可能是一個字節(jié)碼)一把鎖 ...
一般的文章都認為對象鎖就是this或者字節(jié)碼,這個是不準(zhǔn)確的。可以是任意的Object,我認為一種情況是為了避免多創(chuàng)建一個Object,所以就用的this或者字節(jié)碼;另外一種情況是不同的對象要使用同一把鎖,就不能用this了。
2)同步代碼塊
靜態(tài)方法的同步代碼塊,鎖可以用當(dāng)前類的字節(jié)碼,因為靜態(tài)方法里無法引用this.
public static intsetName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}
--synchronized(xxx):可以理解為給當(dāng)前代碼單元A(方法、代碼塊)加上鎖xxx,只要鎖沒有釋放,任何其它的線程執(zhí)行到具有相同鎖xxx的代碼單元B(無論AB是否是同一個對象或者同一個類,鎖可以綁定到任何的執(zhí)行載體上),都會等待代碼單元A執(zhí)行完畢才去執(zhí)行。曾經(jīng)老畢的課程里就用去洗手間反鎖門作了一個形象的比喻,鎖住的衛(wèi)生間某個時間段只能由一個人使用。
看兩個代碼單元執(zhí)行是否互斥,首先看它們的鎖是不是同一把鎖。
--synchronized釋放鎖的時機:http://m.itdecent.cn/p/888469ebe713
--鎖的類型:鎖一般分為3大類型,對象(Object)、this、class,this理論上來說也是對象。
非靜態(tài)synchronized方法的鎖是this
靜態(tài)synchronized方法的鎖是class
上面這個結(jié)論自己寫個小demo很容易證明,我就不作闡述了。
3)線程安全類
當(dāng)一個類已經(jīng)很好的同步以保護它的數(shù)據(jù)時,這個類就稱為“線程安全的”。某個類的成員類是安全的,不代表這個類就一定安全。
4)死鎖
https://blog.csdn.net/nangeali/article/details/80304456
概念:2個線程在運行時,都互相持有對方的鎖,導(dǎo)致互相等待的現(xiàn)象。
解決方案:
https://www.cnblogs.com/xzlf/p/12681525.html
1》不要在同一個代碼塊中,持有多個對象的鎖
https://blog.csdn.net/sinat_41144773/article/details/89476679
2》加鎖順序:當(dāng)多個線程要相同的一些鎖,但是按照不同的順序加鎖,死鎖的情況發(fā)生率較高,如果,程序運行能確保所有線程都是按照相同的順序去獲得鎖,那么死鎖就不會發(fā)生。
3》加鎖時限:加一個超時時間,若一個線程沒有在給定的時間內(nèi)成功獲取所需的鎖,則進行回退操作,并釋放自己本身所持有的鎖,一段隨機時間之后,重新去獲取鎖。
4》死鎖檢測:死鎖檢測,每當(dāng)線程去獲取鎖的時候,會在線程和鎖相關(guān)的數(shù)據(jù)結(jié)構(gòu)中將其記下,除此之外,每當(dāng)線程請求鎖,都需要記錄在數(shù)據(jù)結(jié)構(gòu)中。死鎖檢測是一個死鎖避免機制。他主要針對的時那些不可能實現(xiàn)按序加鎖并且鎖超時也不可行的應(yīng)用場景。
5)volatile關(guān)鍵字
2.鎖對象
相當(dāng)于對syncronised的封裝,使用起來更加的方便。
1)普通鎖
2)讀寫鎖
讀與讀不互斥,讀寫鎖分離,提高讀寫效率。(就是當(dāng)前有一個Read線程獲取了鎖,如果這時候有另外一個Read線程起來了,這個線程仍然可以正常執(zhí)行。)
http://m.itdecent.cn/p/9cd5212c8841
3)信號量
1)Semaphore不一定是鎖定某個資源(通常情況下不是線程安全的),而是流程上的概念。
2)信號量的概念不光作用于線程,也可適用于操作系統(tǒng)的進程控制。
https://www.php.cn/java-article-407669.html
https://blog.csdn.net/shenjixiang/article/details/78724705
知識點三:線程間的交互協(xié)作
1.wait、notify、notifyAll
https://blog.csdn.net/weixin_42541254/article/details/101291768
void notify()——喚醒在此對象監(jiān)視器上等待的單個線程。
void notifyAll()——喚醒在此對象監(jiān)視器上等待的所有線程。
void wait()——導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify()方法或 notifyAll()方法。
void wait(longtimeout)——導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
void wait(longtimeout, int nanos)——導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify()方法或 notifyAll()方法,或者其他某個線程中斷當(dāng)前線程,或者已超過某個實際時間量。
wait、notify對鎖的影響
進入 wait()方法后,當(dāng)前線程釋放鎖
notify 后,當(dāng)前線程不會馬上釋放該對象鎖,wait 所在的線程并不能馬上獲取該對象鎖,要等到程序退出 synchronized 代碼塊后,當(dāng)前線程才會釋放鎖,wait所在的線程也才可以獲取該對象鎖。
調(diào)用某個對象的wait、notify等方法,必須
1.在當(dāng)前線程里獲取這個對象的鎖
2.且鎖一定是同一把鎖(同一個對象)
也就是說wait()、notify()等方法調(diào)用的格式是固定的。
---wait()方法調(diào)用
synchronized (鎖對象) {
鎖對象.wait();
}
如果不加synchronized直接調(diào)用wait()方法就會報錯java.lang.IllegalMonitorStateException: object not locked by thread before wait()
---notify()方法調(diào)用
synchronized (鎖對象) {
鎖對象.notify();
}
如果不加synchronized直接調(diào)用notify()方法就會報錯java.lang.IllegalMonitorStateException: object not locked by thread before notify()
1)簡單的示例:
第1種情況:協(xié)作的線程對象本身作為鎖
/**
* 計算1+2+3+...+100的和
*/
public class ThreadB1 extends Thread {
int total;
public ThreadB1() {
}
public void run(){
synchronized (this) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<101;i++){
total+=i;
}
//(完成計算了)喚醒在此對象監(jiān)視器上等待的單個線程,在本例中線程A被喚醒
notify();
}
}
}
private void waitTest1() {
ThreadB1 b=new ThreadB1();
//啟動計算線程
b.start();
//線程A擁有b對象上的鎖。線程為了調(diào)用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (b) {
try {
System.out.println("等待對象b完成計算......");
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b對象計算的總和是:" + b.total);
}
}
第2種形式:創(chuàng)建新的對象作為鎖
/**
* 計算1+2+3+...+100的和
*/
public class ThreadB2 extends Thread {
int total;
private Object lock;
public ThreadB2(Object lock) {
this.lock = lock;
}
public void run(){
synchronized (lock) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<101;i++){
total+=i;
}
//(完成計算了)喚醒在此對象監(jiān)視器上等待的單個線程,在本例中線程A被喚醒
lock.notify();
}
}
}
private void waitTest2() {
Object lock = new Object();
ThreadB2 b=new ThreadB2(lock);
//啟動計算線程
b.start();
//線程A擁有b對象上的鎖。線程為了調(diào)用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (lock) {
try {
System.out.println("等待對象b完成計算......");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b對象計算的總和是:" + b.total);
}
}
》》》將線程稍做改動,在計算和之前就notify。
public void run(){
Log.e(TAG, "run: 入口");
synchronized (this) {
//(完成計算了)喚醒在此對象監(jiān)視器上等待的單個線程,在本例中線程A被喚醒
Log.e(TAG, "run: notify");
notify();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "run: 開始計算");
for (int i=0;i<101;i++){
total+=i;
}
}
}
發(fā)現(xiàn)程序并不會立馬切換到調(diào)用wait的線程,而是等notify的線程執(zhí)行完畢,wait的線程才會繼續(xù)執(zhí)行。
如果wait的線程是main線程,notify后面的操作時間很長,main線程會一直等待,會算作是阻塞主線程,提示Skipped xxx frames! The application may be doing too much work on its main thread.,所以這個錯誤不一定就是主線程做耗時操作導(dǎo)致的。
2)多個線程等待同一個對象鎖時使用notifyAll
3)notify比wait先執(zhí)行,導(dǎo)致wait無法解除。
對上面的簡單示例第一種情況,作一下改動。使子線程快速執(zhí)行完notify方法
public class ThreadB1 extends Thread {
private static final String TAG = "ThreadB1";
int total;
private boolean isFinish = false;
public ThreadB1() {
}
public void run(){
Log.e(TAG, "run: 入口");
synchronized (this) {
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// Log.e(TAG, "run: 開始計算");
// for (int i=0;i<101;i++){
// total+=i;
// }
//(完成計算了)喚醒在此對象監(jiān)視器上等待的單個線程,在本例中線程A被喚醒
Log.e(TAG, "run: notify");
notify();
isFinish = true;
}
}
public boolean isFinish() {
return isFinish;
}
}
/**
* 使notify先于wait執(zhí)行
*/
private void waitTest11() {
final ThreadB1 b=new ThreadB1();
//啟動計算線程
b.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//線程A擁有b對象上的鎖。線程為了調(diào)用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (b) {
try {
// while (!b.isFinish()){
System.out.println("等待對象b完成計算......");
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("嘗試notify一下");
b.notify();
}
},5000);
b.wait();
// }
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b對象計算的總和是:" + b.total);
}
}
日志 一直停留在

可以看到上面,即使我事先使用了timer準(zhǔn)備notify解除一下wait,也是無濟于事,因為wait造成當(dāng)前線程等待,而timer也是執(zhí)行在當(dāng)前線程,所以也等待了。
官方給出的wait用法
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
這里的while判斷就是防止notify執(zhí)行完畢了,wait才執(zhí)行,導(dǎo)致當(dāng)前線程意外的一直等待。
2.線程調(diào)度
- 1.休眠-sleep (不釋放線程鎖)
- 2.優(yōu)先級-setPriority
- 3.讓步-yield(不釋放線程鎖)
- 4.合并-join
作用:保證當(dāng)前線程停止執(zhí)行,直到該線程所加入的線程完成為止。使得當(dāng)前線程里開啟的新線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。
ThreadJoinTest t1 = new ThreadJoinTest("小明");
ThreadJoinTest t2 = new ThreadJoinTest("小東");
t1.start();
/**join的意思是使得放棄當(dāng)前線程的執(zhí)行,并返回對應(yīng)的線程,例如下面代碼的意思就是:
程序在main線程中調(diào)用t1線程的join方法,則main線程放棄cpu控制權(quán),并返回t1線程繼續(xù)執(zhí)行直到線程t1執(zhí)行完畢
所以結(jié)果是t1線程執(zhí)行完后,才到主線程執(zhí)行,相當(dāng)于在main線程中同步t1線程,t1執(zhí)行完了,main線程才有執(zhí)行的機會
*/
t1.join();
t2.start();
- 5.守護-deamon (慎用)
- 6.wait/notify
并發(fā)協(xié)作-生產(chǎn)者消費者模型
知識點四:線程池
線程池作用
1)復(fù)用線程,避免創(chuàng)建開銷。
線程池分類
1)固定大小的線程池
2)單一任務(wù)線程池
3)可變尺寸線程池
4)延遲線程池
https://blog.csdn.net/h610968110/article/details/78894513
5)單任務(wù)延遲線程池
6)自定義線程池
7)帶返回值的線程(依賴于線程池)
線程池的適用場景
https://blog.csdn.net/qq_43692920/article/details/90266704
線程池的關(guān)閉
https://www.cnblogs.com/aspirant/p/10265863.html
shutdown與shutDownNow方法