前言
今天我進一步了解了java多線程的相關知識,根據幾個相關問題進行學習并作出了解答,這篇文章將用來記錄我學習到的有關知識。
問題列表
- 怎么實現多線程?
- 正確啟動線程的方式
- 線程中常用的方法說明
- 如何停止線程?
- 如和中斷線程?
- 線程的生命周期
- 線程的異常處理
- 死鎖的解決方案
1. 怎么實現多線程?
實現多線程的方法有兩種:
- 繼承Thread類、并重寫run方法。
- 實現Runnable接口、并重寫run方法。
關于這兩種方法有一個比較,如圖:
2. 正確啟動線程的方式
- 如果線程類繼承了Thread類,那么在主方法中可以之間創建該線程類對象,使該對象調用start()方法即可啟動線程。
- 如果線程類實現了Runnable接口,那么在主方法中同樣需要創建該線程類對象,然后作為Thread的構造方法的參數創建一個線程,再調用start()方法即可啟動線程。
現在對剛剛上面的兩個問題進行總結:
3. 線程中常用的方法說明
sleep方法:
當在當前線程中調用Thread.sleep(long millis)方法時,當前線程會進入阻塞狀態。millis參數指定了線程睡眠的時間,單位是毫秒。 當時間結束之后,線程會重新進入就緒狀態。
代碼演示:
public class SleepTest extends Thread {
static int i=0;
public void run(){
while(++i < 5) {
System.out.println(Thread.currentThread().getName()+ "輸出" + i);
try {
Thread.sleep(1000); //間隔一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SleepTest t = new SleepTest();
t.start();
}
}
運行結果:
結果的輸出是由間隔時間的。
join方法:
它是Thread的實例方法,在當前線程里的另外一個線程調用了join()方法時,當前線程會進入阻塞狀態等待這個調用了join()方法的線程執行完畢后,再由阻塞狀態進入到就緒狀態。
代碼演示:
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new app("t1"));
Thread t2 = new Thread(new app("t2"));
Thread t3 = new Thread(new app("t3"));
t1.start();
t1.join(); //這里會等待t1線程執行完畢才會開啟t2線程
t2.start(); //同上
t2.join();
t3.start();
t3.join();
}
static class app implements Runnable{
int i = 0;
String name;
public app(String name) {
this.name = name;
}
@Override
public void run() {
while(i < 5) {
System.out.println(name + "輸出" + i++);
}
}
}
}
運行結果:
yield方法:
它是Thread的靜態方法,如果在當前的線程中寫了“Thread.yield”,那么就會使當前線程主動放棄cpu資源,進入就緒狀態,讓其它的線程來獲取cpu資源,也有可能當前的線程還會獲取cpu資源。
代碼演示:
public class YieldTest {
public static void main(String[] args) {
Thread t1 = new Thread(new app("t1"));
Thread t2 = new Thread(new app("t2"));
t1.start();
t2.start();
}
static class app implements Runnable{
int i = 0;
String name;
public app(String name) {
this.name = name;
}
@Override
public void run() {
while(i < 8){
if(i == 5) {
Thread.yield();
System.out.println(name+"放棄cpu資源");
}
System.out.println(name+"輸出了"+i);
i++;
}
}
}
}
運行結果:
wait、notify/notifyAll方法:
它們是Object的方法,需要用到synchronized關鍵字,一般在synchronized 同步代碼塊里使用 wait()、notify/notifyAll() 方法。
1.wait()使當前線程阻塞,前提是必須先獲得鎖,當線程執行wait()方法時候,會釋放當前的鎖,然后讓出CPU,進入等待狀態。
2.只有當 notify/notifyAll() 被執行時候,才會喚醒一個或多個正處于等待狀態的線程,然后繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到wait(),再次釋放鎖。
代碼演示:
public class LockTest {
static Object lock = new Object();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new app1("t1"));
Thread t2 = new Thread(new app2("t2"));
t2.start();
Thread.sleep(3000);
t1.start();
}
static class app1 implements Runnable{
String name;
public app1(String name) {
this.name = name;
}
@Override
public void run() {
synchronized(lock) {
while(++i < 10) {
if(i == 5) {
System.out.println(name+"線程喚醒其他線程");
lock.notify();
}
System.out.println(name+"輸出"+i);
}
}
}
}
static class app2 implements Runnable{
String name;
public app2(String name) {
this.name = name;
}
@Override
public void run() {
synchronized(lock){
if(i != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"線程被喚醒");
System.out.println(name+"輸出"+i);
}
}
}
}
這段代碼里面有兩個線程,讓t1線程一直輸出直到i等于5的時候,喚醒被阻塞的t2線程,但喚醒t2線程不代表釋放鎖,會先等待t1線程執行完畢后才會執行t2線程。
運行結果:
4. 如何停止線程?
首先,stop()方法不推薦使用,因為stop()會讓線程戛然而止,使得在實際開發中不能知道線程還有哪些工作還沒做、完成了哪些工作,還有我們不能夠去做一些應有的清理工作。
正確的停止線程方法:設置退出標志
public class StopThread extends Thread{
public void run() {
test01 t = new test01();
Thread thread = new Thread(t);
//開始線程
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//停止線程
t.flag = false;
}
public static void main(String[] args) {
Thread stopThread = new Thread(new StopThread());
stopThread.start();
}
static class test01 implements Runnable{
//設置線程停止的標志
//volatile保證了線程可以正確的讀取其他線程寫入的值
volatile boolean flag = true;
@Override
public void run() {
System.out.println("線程開始.......");
while(flag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程正在進行.....");
}
System.out.println("線程停止.......");
}
}
}
運行結果:
5. 如何中斷線程?
調用interrupt()方法:
當線程調用了某些方法,比如sleep()方法而進入一種阻塞狀態,此時該線程如果調用了interrupt()方法,會產生兩個結果:1.線程的中斷狀態被清除(退出阻塞狀態)2.會拋出一個InterruptedException異常,表明線程被中斷了。
代碼演示:
public class InterruptThread extends Thread{
public void run() {
while(true) {
System.out.println("線程正在進行.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("中斷阻塞狀態");
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread stopThread = new Thread(new InterruptThread());
//開始線程
System.out.println("線程開始.......");
stopThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中斷線程
stopThread.interrupt();
}
}
運行結果:
雖然主線程已經執行結束,但stopThread 線程卻還會繼續進行,這里只是為了演示interrupt()方法中斷阻塞狀態的功能。
6. 線程的生命周期
相關解釋:
1.就緒狀態:創建了線程對象后,調用了線程的start()方法(注意此時線程只是進入了線程隊列,等待獲取cpu服務,具備了運行條件,但并不一定已經開始運行)。
2.運行狀態:處于就緒狀態的線程,一旦獲取了cpu資源,便進入到運行狀態,開始執行run()方法里面的邏輯。
3.終止狀態:線程的run()方法執行完畢,或者線程調用了stop()方法(此方法已過時,不推薦使用),線程便進入終止狀態。
4.阻塞狀態:一個正在執行的線程在某種情況下,由于某種原因(比如線程調用了sleep()方法、wait()方法、join()方法)而暫時讓出了cpu資源,暫停了自己的執行,便進入到阻塞狀態。
7. 線程的異常處理
關于線程的異常處理我是這么理解的,每一個線程在運行的時候都是相互獨立的,那么它出現的異常應該由各自的線程自己來處理。
下面嘗試不在線程中處理異常,嘗試在主線程中處理異常:
public class EpTest implements Runnable{
public static void main(String[] args) {
EpTest instance = new EpTest();
try{
Thread t1 = new Thread(instance);
t1.start();
}catch (Exception e){
System.out.println("主線程捕獲線程中的異常");
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
Exception in thread "Thread-0" java.lang.RuntimeException
at EpTest.run(EpTest.java:19)
at java.base/java.lang.Thread.run(Thread.java:834)
結果顯示此時這個主線程并不能處理該異常
那么處理線程中出現的異常有以下我學習到的幾種方法:
- 直接在線程中使用try、cath語句處理異常。
@Override
public void run() {
try{
...
}catch (Exception e){
...
}
}
- 對于不在線程中處理的異常,可以交給異常處理器處理UncaughtExceptionHandler
public class EpTest implements Runnable{
public static void main(String[] args) {
EpTest instance = new EpTest();
Thread t1 = new Thread(instance);
t1.setUncaughtExceptionHandler(new EpHandler());
t1.start();
}
@Override
public void run() {
throw new RuntimeException();
}
}
class EpHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t,Throwable e){
System.out.println("捕獲未處理的異常");
}
}
首先需要自定義一個異常處理器:
class EpHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t,Throwable e){
System.out.println("捕獲未處理的異常");
}
}
接著在主方法中設置這個異常處理器:
public static void main(String[] args) {
EpTest instance = new EpTest();
Thread t1 = new Thread(instance);
//調用Thread類的setUncaughtExceptionHandler()設置異常處理器
t1.setUncaughtExceptionHandler(new EpHandler());
t1.start();
}
3.使用線程池
public class EpTest2 implements Runnable {
public static void main(String[] args) {
Thread t1 = new Thread(new EpTest2());
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(t1);
executorService.shutdown();
}
@Override
public void run() {
//在run方法中設置異常處理器
Thread.currentThread().setUncaughtExceptionHandler(new EpHandler());
throw new RuntimeException();
}
}
需要注意的是,只有通過execute()方法提交任務,才能將它拋出的異常交給未捕獲異常處理器。
另外的一種方法:
public class EpTest2 implements Runnable {
public static void main(String[] args) {
Thread t1 = new Thread(new EpTest2());
ExecutorService executorService = Executors.newCachedThreadPool();
Future<?> future=executorService.submit(t1);
executorService.shutdown();
try {
//拋出異常
future.get();
}catch (Exception e){
System.out.println("捕獲未處理的異常");
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
這種方法通過submit()方法提交的任務由于拋出了異常而結束,該異常將被Future.get()封裝在ExecutionException中重新拋出。
8. 死鎖的解決方案
線程死鎖是指由于兩個或者多個線程互相持有對方所需要的資源,導致這些線程處于等待狀態,無法前往執行。當線程互相持有對方所需要的資源時,會互相等待對方釋放資源,如果線程都不主動釋放所占有的資源,將產生死鎖。
產生死鎖的必要條件:
(1)互斥條件:一個資源每次只能被一個線程使用,直到被該進程釋放。
(2)請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3)不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪。
(4)循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
解決死鎖的問題:
- 保證加鎖的順序(按照一定的順序給線程加鎖)
- 設置加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,并釋放自己占有的鎖)
相關教程:
細說多線程之Thread VS Runnable
https://www.imooc.com/learn/312
Java Socket應用
https://www.imooc.com/learn/161
Java高并發之魂:synchronized深度解析
https://www.imooc.com/learn/1086
Java多線程之內存可見性
https://www.imooc.com/learn/352