第一步:細說多線程之Thread VS Runnable
https://www.imooc.com/learn/312
第二步:Java Socket應用
https://www.imooc.com/learn/161
第三步:Java高并發(fā)之魂:synchronized深度解析
https://www.imooc.com/learn/1086
第四步:Java多線程之內(nèi)存可見性
https://www.imooc.com/learn/352
一冶伞、 線程的實現(xiàn)方式
Java多線程的4種實現(xiàn)方式
- 1.繼承Thread并重寫run方法,并調(diào)用start方法
/**
* Java實現(xiàn)多線程的方式1
* 繼承Thread類潘拱,重寫run方法
*/
class MyThread extends Thread {
@Override
public void run() {
//此處為thread執(zhí)行的任務內(nèi)容
System.out.println(Thread.currentThread().getName());
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
Thread t = new MyThread();
//輸出:
//Thread-0 Thread-1
t.start();
}
}
}
- 2.實現(xiàn)Runnable接口,并用其初始化Thread瘟判,然后創(chuàng)建Thread實例锣险,并調(diào)用start方法
/**
* Java實現(xiàn)多線程的方式2
* 實現(xiàn)Runnable接口
*/
class MyThread implements Runnable {
@Override
public void run() {
//此處為thread執(zhí)行的任務內(nèi)容
System.out.println(Thread.currentThread().getName());
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
Thread t = new Thread(new MyThread());
//輸出:
//Thread-0 Thread-1
t.start();
}
}
}
- 3.實現(xiàn)Callable接口,并用其初始化Thread般眉,然后創(chuàng)建Thread實例我纪,并調(diào)用start方法
/**
* Java實現(xiàn)多線程的方式3
* 實現(xiàn)Callable接口
*/
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return null;
}
}
public class Demo03 {
public static void main(String[] args) {
for(int i=0;i<2;i++) {
//創(chuàng)建MyThread實例
Callable<Integer> c = new MyThread();
//獲取FutureTask
FutureTask<Integer> ft = new FutureTask<Integer>(c);
//使用FutureTask初始化Thread
Thread t = new Thread(ft);
//輸出:
//Thread-0 Thread-1
t.start();
}
}
}
- 4.使用線程池創(chuàng)建
/**
* Java實現(xiàn)多線程的方式4
* 線程池
*/
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 0;
}
}
public class Demo03 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0;i<2;i++) {
executorService.execute(new MyThread());
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread2());
//輸出
// pool-1-thread-1
// pool-1-thread-2
// pool-1-thread-3
// pool-1-thread-4
executorService.submit(ft);
}
executorService.shutdown();
}
}
二慎宾、 正確啟動線程的方式
啟動線程的三種方式
- 1)猎莲、繼承Thread類:
- a.定義Thread類的子類惧所,并重寫該類的run()方法胸嘴,該run()方法的方法體就代表了線程
- b. 需要完成的任務埠对。因此把run方法稱為線程執(zhí)行體。
創(chuàng)建Thread子類的實例之宿,即創(chuàng)建了線程對象族操。 - c.調(diào)用線程對象的start()方法來啟動該線程。
- 2)比被、實現(xiàn)Runnable接口:
a.定義Runnable接口的實現(xiàn)類色难,并重寫該接口的run方法,該run方法的方法體同樣是該線程的線程執(zhí)行體
b.創(chuàng)建Runnable實現(xiàn)類的實例對象等缀,并以此實例對象作為Thread的target來創(chuàng)建Thread類枷莉,該Thread對象才是真正的線程對象。
c.調(diào)用線程對象的start()方法來啟動該線程尺迂。
- 3)笤妙、匿名內(nèi)部類:
匿名內(nèi)部類本質(zhì)上也是一個類實現(xiàn)了Runnable接口,重寫了run方法噪裕,只不過這個類沒有名字蹲盘,直接作為參數(shù)傳入Thread類,示例代碼:
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "執(zhí)行" + i);
}
}
}).start();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "執(zhí)行" + i);
}
}
}
三膳音、 如何停止線程
使用標志位
//定義一個標志位:cancelled召衔,cancelled為true是即run方法結(jié)束,反之繼續(xù)while里面的任務祭陷。通過cancell方法來停止任務苍凛。
static class CancelledTaggedRunnnable implements Runnable{
private volatile boolean cancelled = false;
@Override
public void run() {
while(!cancelled){
//沒有停止需要做什么操作
}
//線程停止后需要干什么
System.out.println("任務結(jié)束了");
}
public void cancell(){
cancelled = true;
}
}
@Test
public void testCancelledTaggedRunnnable() throws InterruptedException {
CancelledTaggedRunnnable taggedRunnnable = new CancelledTaggedRunnnable();
Thread thread = new Thread(taggedRunnnable);
thread.start();
System.err.println(thread.isAlive());
Thread.sleep(1000);
taggedRunnnable.cancell();
Thread.sleep(1000);
System.err.println(thread.isAlive());
Thread.sleep(1000);
System.err.println(thread.isAlive());
}
強行停止
對于多線程,使用中斷策略 較為優(yōu)雅兵志,也是官方的推薦醇蝴。線程停止前你應該做一些操作,從而保證該線程運行后的數(shù)據(jù)不會被丟失想罕,這一點在多線程中極為重要悠栓。
但是對于中斷策略還是有一個很大的缺陷,那就是按价,必須通過中斷的阻塞函數(shù)惭适,如:我使用的BlockingQueue的put方法,也可以是Thread.sleep()方法俘枫,才能拋出InterruptedException腥沽。如果拋不出這樣的異常呢逮走?
對于單線程鸠蚪,還有一個簡單粗暴的方式,那就是Java已經(jīng)不再使用的Thread stop方法。
使用方法即直接調(diào)用:thread.stop()即可茅信。
如果你對該線程的操作不再關(guān)心了盾舌,對結(jié)果也不再在意了,使用該方法也是可以的蘸鲸。
但多線程情況下妖谴,或者線程帶鎖的情況下 那就要慎用了。該方法不安全
四酌摇、 如何中斷線程
現(xiàn)在我們知道了使用 stop() 方式停止線程是非常不安全的方式膝舅,那么我們應該使用什么方法來停止線程呢?答案就是使用 interrupt() 方法來中斷線程窑多。
需要明確的一點的是:interrupt() 方法并不像在 for 循環(huán)語句中使用 break 語句那樣干脆仍稀,馬上就停止循環(huán)。調(diào)用 interrupt() 方法僅僅是在當前線程中打一個停止的標記埂息,并不是真的停止線程技潘。
也就是說,線程中斷并不會立即終止線程千康,而是通知目標線程享幽,有人希望你終止。至于目標線程收到通知后會如何處理拾弃,則完全由目標線程自行決定值桩。這一點很重要,如果中斷后砸彬,線程立即無條件退出颠毙,那么我們又會遇到 stop() 方法的老問題。
public class InterruptThread1 extends Thread{
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
System.out.println("i=" + i);
}
}
}
從輸出的結(jié)果我們會發(fā)現(xiàn) interrupt 方法并沒有停止線程 t 中的處理邏輯砂碉,也就是說即使 t 線程被設(shè)置為了中斷狀態(tài)蛀蜜,但是這個中斷并不會起作用,那么該如何停止線程呢增蹭?
這就需要使用到另外兩個與線程中斷有關(guān)的方法了:
public boolean Thread.isInterrupted() //判斷是否被中斷
public static boolean Thread.interrupted() //判斷是否被中斷滴某,并清除當前中斷狀態(tài)
這兩個方法使得當前線程能夠感知到是否被中斷了(通過檢查標志位)。
所以如果希望線程 t 在中斷后停止滋迈,就必須先判斷是否被中斷霎奢,并為它增加相應的中斷處理代碼:
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
//判斷是否被中斷
if(Thread.currentThread().isInterrupted()){
//處理中斷邏輯
break;
}
System.out.println("i=" + i);
}
}
輸出結(jié)果,for 循環(huán)在執(zhí)行完成前就提前結(jié)束了:
在上面這段代碼中饼灿,我們增加了 Thread.isInterrupted() 來判斷當前線程是否被中斷了幕侠,如果是,則退出 for 循環(huán)碍彭,結(jié)束線程晤硕。
這種方式看起來與之前介紹的“使用標志位終止線程”非常類似悼潭,但是在遇到 sleep() 或者 wait() 這樣的操作,我們只能通過中斷來處理了舞箍。
public static native void sleep(long millis) throws InterruptedException
Thread.sleep() 方法會拋出一個 InterruptedException 異常舰褪,當線程被 sleep() 休眠時,如果被中斷疏橄,這會就拋出這個異常占拍。
(注意:Thread.sleep() 方法由于中斷而拋出的異常,是會清除中斷標記的捎迫。)
五晃酒、 線程的生命周期
線程的生命周期包含5個階段,包括:新建窄绒、就緒掖疮、運行、阻塞颗祝、銷毀浊闪。
- 新建:就是剛使用new方法,new出來的線程螺戳;
- 就緒:就是調(diào)用的線程的start()方法后搁宾,這時候線程處于等待CPU分配資源階段,誰先搶的CPU資源倔幼,誰開始執(zhí)行;
- 運行:當就緒的線程被調(diào)度并獲得CPU資源時盖腿,便進入運行狀態(tài),run方法定義了線程的操作和功能;
- 阻塞:在運行狀態(tài)的時候损同,可能因為某些原因?qū)е逻\行狀態(tài)的線程變成了阻塞狀態(tài)翩腐,比如sleep()、wait()之后線程就處于了阻塞狀態(tài)膏燃,這個時候需要其他機制將處于阻塞狀態(tài)的線程喚醒茂卦,比如調(diào)用notify或者notifyAll()方法。喚醒的線程不會立刻執(zhí)行run方法组哩,它們要再次等待CPU分配資源進入運行狀態(tài);
-
銷毀:如果線程正常執(zhí)行完畢后或線程被提前強制性的終止或出現(xiàn)異常導致結(jié)束,那么線程就要被銷毀伶贰,釋放資源;
完整的生命周期圖如下:
image.png
六、 notify黍衙、join、yield的方法說明
- notify: 在執(zhí)行notify方法后琅翻,當前線程不會馬上釋放該對象鎖位仁,呈wait狀態(tài)的線程也并不能馬上獲取該對象鎖浅妆,要等到執(zhí)行notify方法的線程將程序執(zhí)行完障癌,也就是退出synchronized代碼塊后,當前線程才會釋放鎖涛浙,而呈wait狀態(tài)所在的線程才可以獲取該對象鎖。
- join: 在B線程里面寫threadA.join( )摄欲,B掛起轿亮,讓A運行,看起來B很禮貌讓A運行了胸墙,但其實B很虛偽,讓別人運行又不釋放鎖迟隅。
class Demo implements Runnable
{
public void run()
{
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
}
}
}
//將執(zhí)行join()方法的線程終止,釋放其CPU執(zhí)行權(quán)和執(zhí)行資格奔缠,等待join()方法所屬的線程執(zhí)行完成后,才能重新獲取CPU執(zhí)行資格
class JoinDemo
{
public static void main(String[] args)throws Exception
{
Demo st=new Demo();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
t1.start();
t1.join(); //main 線程終止校哎,等待t1線程執(zhí)行完成才繼續(xù)執(zhí)行。
t2.start();
//t1.join();//main 線程終止闷哆,t1和t2搶奪CPU執(zhí)行權(quán),main線程等待t1線程執(zhí)行完成才繼續(xù)執(zhí)行抱怔。
for(int x=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
}
System.out.println("over");
}
}
- yield: 方法:使當前線程從執(zhí)行狀態(tài)(運行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。cpu會從眾多的可執(zhí)行態(tài)里隨機選擇野蝇,也就是說括儒,當前也就是剛剛的那個線程還是有可能會被再次執(zhí)行到的绕沈。也就是從Running變?yōu)镽eady帮寻,一直在Runnable里面乍狐。
class Demo implements Runnable
{
public void run()
{
for(int i=0;x<50;x++)
{
System.out.println(Thread.currentThread().getName()+"...."+x);
Thread.yield();//釋放CPU執(zhí)行權(quán)固逗,注意:釋放CPU執(zhí)行權(quán)藕帜,不代表它自己不能再次獲取CPU執(zhí)行權(quán)
}
}
}
七惜傲、 線程的異常處理
方法一:子線程中try... catch...
public class ChildThread implements Runnable {
public void run() {
doSomething1();
try {
// 可能發(fā)生異常的方法
exceptionMethod();
} catch (Exception e) {
// 處理異常
System.out.println(String.format("handle exception in child thread. %s", e));
}
doSomething2();
}
}
方法二:為線程設(shè)置“未捕獲異常處理器”UncaughtExceptionHandler
為線程設(shè)置異常處理器。具體做法可以是以下幾種:
(1)Thread.setUncaughtExceptionHandler設(shè)置當前線程的異常處理器盗誊;
(2)Thread.setDefaultUncaughtExceptionHandler為整個程序設(shè)置默認的異常處理器;
如果當前線程有異常處理器(默認沒有)哈踱,則優(yōu)先使用該UncaughtExceptionHandler類;否則开镣,如果當前線程所屬的線程組有異常處理器刀诬,則使用線程組的
UncaughtExceptionHandler邪财;否則,使用全局默認的DefaultUncaughtExceptionHandler树埠;如果都沒有的話,子線程就會退出弥奸。
注意:子線程中發(fā)生了異常,如果沒有任何類來接手處理的話盛霎,是會直接退出的,而不會記錄任何日志愤炸。
所以期揪,如果什么都不做的話规个,是會出現(xiàn)子線程任務既沒執(zhí)行成功凤薛,也沒有任何日志提示的“詭異”現(xiàn)象的诞仓。
設(shè)置當前線程的異常處理器:
public class ChildThread implements Runnable {
private static ChildThreadExceptionHandler exceptionHandler;
static {
exceptionHandler = new ChildThreadExceptionHandler();
}
public void run() {
Thread.currentThread().setUncaughtExceptionHandler(exceptionHandler);
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
}
public static class ChildThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(String.format("handle exception in child thread. %s", e));
}
}
}
或者,設(shè)置所有線程的默認異常處理器
public class ChildThread implements Runnable {
private static ChildThreadExceptionHandler exceptionHandler;
static {
exceptionHandler = new ChildThreadExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
}
public void run() {
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
}
private void exceptionMethod() {
throw new RuntimeException("ChildThread exception");
}
public static class ChildThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(String.format("handle exception in child thread. %s", e));
}
}
}
命令行輸出:
do something 1
handle exception in child thread. java.lang.RuntimeException: ChildThread exception
方法三:通過Future的get方法捕獲異常(推薦)
使用線程池提交一個能獲取到返回信息的方法活玲,也就是ExecutorService.submit(Callable)
在submit之后可以獲得一個線程執(zhí)行結(jié)果的Future對象,而如果子線程中發(fā)生了異常舒憾,通過future.get()獲取返回值時镀钓,可以捕獲到
ExecutionException異常,從而知道子線程中發(fā)生了異常镀迂。
子線程代碼:
public class ChildThread implements Callable<String> {
public String call() throws Exception {
System.out.println("do something 1");
exceptionMethod();
System.out.println("do something 2");
return "test result";
}
private void exceptionMethod() {
throw new RuntimeException("ChildThread1 exception");
}
}
父線程代碼:
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(8);
Future future = executorService.submit(new ChildThread());
try {
future.get();
} catch (InterruptedException e) {
System.out.println(String.format("handle exception in child thread. %s", e));
} catch (ExecutionException e) {
System.out.println(String.format("handle exception in child thread. %s", e));
} finally {
if (executorService != null) {
executorService.shutdown();
}
}
}
}
命令行輸出:
do something 1
handle exception in child thread. java.util.concurrent.ExecutionException: java.lang.RuntimeException: ChildThread1 exception
八、 死鎖的解決方案
java 死鎖產(chǎn)生的四個必要條件:
1探遵、互斥使用,即當資源被一個線程使用(占有)時别凤,別的線程不能使用
2领虹、不可搶占,資源請求者不能強制從資源占有者手中奪取資源塌衰,資源只能由資源占有者主動釋放。
3最疆、請求和保持杯巨,即當資源請求者在請求其他的資源的同時保持對原有資源的占有。
4努酸、循環(huán)等待,即存在一個等待隊列:P1占有P2的資源获诈,P2占有P3的資源,P3占有P1的資源笼踩。這樣就形成了一個等待環(huán)路。
當上述四個條件都成立的時候嚎于,便形成死鎖。當然于购,死鎖的情況下如果打破上述任何一個條件,便可讓死鎖消失知染。下面用java代碼來模擬一下死鎖的產(chǎn)生价涝。
當上述四個條件都成立的時候,便形成死鎖色瘩。當然,死鎖的情況下如果打破上述任何一個條件居兆,便可讓死鎖消失。下面用java代碼來模擬一下死鎖的產(chǎn)生泥栖。
解決死鎖問題的方法是:一種是用synchronized,一種是用Lock顯式鎖實現(xiàn)魏割,
而如果不恰當?shù)氖褂昧随i,且出現(xiàn)同時要鎖多個對象時钞它,會出現(xiàn)死鎖情況殊鞭。
死鎖是兩個甚至多個線程被永久阻塞時的一種運行局面遭垛,這種局面的生成伴隨著至少兩個線程和兩個或者多個資源操灿。
避免死鎖方針:
a:避免嵌套封鎖:這是死鎖最主要的原因的,如果你已經(jīng)有一個資源了就要避免封鎖另一個資源庶喜。如果你運行時只有一個對象封鎖,那是幾乎不可能出現(xiàn)一個死鎖局面的溃卡。
b:只對有請求的進行封鎖:你應當只想你要運行的資源獲取封鎖.如果我們只對它所屬領(lǐng)域中的一個感興趣蜒简,那我們應當封鎖住那個特殊的領(lǐng)域而并非完全的對象瘸羡。
c:避免無限期的等待:如果兩個線程正在等待對象結(jié)束搓茬,無限期的使用線程加入,如果你的線程必須要等待另一個線程的結(jié)束卷仑,若是等待進程的結(jié)束加入最好準備最長時間。