本篇博客主要介紹Java中線程的相關概念。
什麼是線程?
線程是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一個線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。所以線程可以理解爲進程中的一個執行流。進程可以理解爲線程組。
- 進程是資源分配的最小單位;
- 線程是CPU調度的最小單位。
同一進程中的不同線程將共享該進程中的全部資源,如虛擬地址空間,文件描述符表和信號處理等。但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境等。
下面,我們通過代碼來感受一下什麼是線程:
public class ThreadTest {
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(this.getName() + " is running!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
MyThread t3 = new MyThread();
t3.start();
while (true) {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(3000);
}
}
}
我們可以使用jconsole工具來觀察一個Java程序的線程情況,jconsole工具在JDK安裝目錄下的bin文件中,由於我們在配置Java環境的時候,已經將這個路徑添加到環境變量,所以我們可以直接使用win + r進行操作,然後輸入jconsole即可。
可以看到除了main、Thread-0、Thread-1、Thread-2和Thread-3這些線程之外還有很多的其他線程,這些都是JVM啓動的一些守護線程。
多線程的優勢
我們來看一個代碼,觀察一下多線程在某些場合的優勢:
public class ThreadTest {
private static final long COUNT = 10_0000_0000L;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
/**
* 併發
* @throws InterruptedException
*/
private static void concurrency() throws InterruptedException {
long begin = System.currentTimeMillis();
// 利用一個線程計算a的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < COUNT; ++i) {
--a;
}
}
});
thread.start();
// 主線程內計算b的值
int b = 0;
for (long i = 0; i < COUNT; ++i) {
--b;
}
// 等待thread線程運行結束
thread.join();
long end = System.currentTimeMillis();
System.out.printf("併發: %d毫秒%n", end - begin);
}
/**
* 串行
*/
private static void serial() {
long begin = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < COUNT; ++i) {
--a;
}
int b = 0;
for (long i = 0; i < COUNT; ++i) {
--b;
}
long end = System.currentTimeMillis();
System.out.printf("串行: %d毫秒%n", end - begin);
}
}
可以看出併發還是比串行要快一些的。
線程的創建
繼承Thread類
可以通過繼承Thread類來創建一個線程類,該方法的好處是this代表的就是當前線程,不需要通過Thread.currentThread()
來獲取當前線程的引用。
我們來看一下代碼:
public class ThreadCreate {
public static void main(String[] args) {
MyThread thread = new MyThread("我的線程");
thread.start();
}
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
System.out.println(this.getName() + " is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我們還可以以匿名類的方式來實現:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread("我的線程") {
@Override
public void run() {
while (true) {
try {
System.out.println(this.getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
實現Runnable接口
通過實現Runnable接口,並且調用Thread的構造方法時將Runnable對象作爲target參數傳入來創建線程對象。該方法的好處是可以規避類的單繼承的限制;但需要通過Thread.currentThread()來獲取當前線程的引用。
下面看一下代碼:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(), "我的線程");
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
除了上述實現一個MyRunnable類創建實例之外,我們還可以使用匿名類的方式來實現,下面看代碼實現:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "我的線程");
thread.start();
}
}
還可以使用Lambda表達式來創建Runnable子類對象。
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "我的線程");
thread.start();
}
}
Thread類常用方法
Thread類是JVM用來管理線程的一個類,換句話說,每個線程都有一個唯一的Thread對象與之關聯。
每個執行流,也需要一個對象來描述,而Thread類的對象就是用來描述一個線程執行流的,JVM會將這些Thread對象組織起來,用於線程調度,線程管理。
Thread的常見構造方法
方法 | 說明 |
Thread() | 創建線程對象 |
Thread(Runnable target) | 使用Runnable對象創建線程對象 |
Thread(String name) | 創建線程對象,並命名 |
Thread(Runnable target, String name) | 使用Runnable對象創建線程對象,並命名 |
Thread(ThreadGroup group, Runnable target) | 線程可以被用來分組管理,分號的組即線程組 |
Thread的幾個常見的屬性
屬性 | 獲取方法 |
ID | getID() |
名稱 | getName() |
狀態 | getState() |
優先級 | getPriority() |
是否後臺線程 | isDaemon() |
是否存活 | isAlive() |
是否被中斷 | isInterrupted() |
- 關於後臺線程,需要記住一點:JVM會在一個進程的所有非後臺線程結束後,纔會結束運行。
- 是否存活:可以簡單的理解爲run方法是否運行結束了。
啓動一個線程
我們可以通過覆寫run方法來創建一個線程對象,但線程對象被創建出來並不意味着線程就開始運行了。
- 覆寫run()方法是提供給線程要做的事情;
- 而調用start()方法,線程才真正獨立去執行了。
一定要區分run和start方法的區別,下面我們通過代碼來演示一下:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("我的線程") {
@Override
public void run() {
while (true) {
try {
System.out.println("我的線程");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.run();
while (true) {
System.out.println("main");
Thread.sleep(1000);
}
}
}
我們用jconsole來觀察一下:
可以看到只有一個main線程,我們創建的線程名爲“我的線程”的線程並沒有被啓動。thread.run();
就是在調用thread對象的方法run();並不是在啓動線程。
下面,我們來看一下真正的啓動線程:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("我的線程") {
@Override
public void run() {
while (true) {
try {
System.out.println("我的線程");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
while (true) {
System.out.println("main");
Thread.sleep(1000);
}
}
}
同樣的,我們使用jconsole來觀察一下:
可以看到,我的線程和main線程都運行起來了。
中斷一個線程
中斷一個線程有兩種方式:
- 通過共享的標記來進行溝通;
- 通過調用interrupt()方法來通知。
我們先來看一下通過共享的標記進行溝通的方法:
public class ThreadDemo {
private static boolean flag= false;
public static void main(String[] args) throws InterruptedException {
new Thread("我的線程") {
@Override
public void run() {
while (!flag) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread.sleep(5000);
flag= true;
}
}
但是這種方式有一個小問題,那就是如果目標線程正在sleep(),那麼目標線程不會被立即中斷,而是等sleep()結束後纔會結束中斷。
通過調用實例方法或者靜態方法來中斷一個線程:
我們來看一下三個方法:
方法 | 說明 |
public void interrupt() | 中斷對象關聯的線程,如果線程處於阻塞狀態,則以異常的方式通知,否則設置標誌位 |
public static boolean interrupted() | 判斷當前線程的中斷標誌位是否被設置,調用後清除標誌位 |
public boolean isInterrupted() | 判斷當前對象關聯的線程的標誌位是否被設置,調用後不清除標誌位 |
interrupt()方法比較好理解,就是中斷對象關聯的線程。
下面,我們來看一下interrupted()和isInterrupted()的區別:
- 可以看到interrupted()方法內部調用的就是isInterrupted()方法,但是interrupted()會清除標誌位,isInterrupted()不會清除標誌位。
- interrupted()是靜態方法,isInterrupted()是實例方法。
對於這三個方法,我們可以理解爲底層也是通過一個共享的標誌位flag來實現的,isInterrupted()方法只是返回這個flag的值,而interrupted()不僅返回這個flag的值,還會將這個flag的值置爲false。
我們再來看一下interrupt()方法:
- 通過thread對象調用interrupt()方法通知該線程停止運行;
- thread收到通知的方式有兩種:
① 如果線程調用了wait/join/sleep等方法而阻塞掛起,則以InterruptedException異常的形式通知,清除中斷標誌;
② 否則,只是內部的一箇中斷標誌被設置,thread可以通過Thread.interrupted()方法或者thread.isInterrupt()方法來判斷當前線程的中斷標誌是否被設置。其中前者在調用之後會清除中斷標誌。
下面,我們來看一個代碼:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (!Thread.interrupted()) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
Thread.sleep(3000);
t1.interrupt();
t2.interrupt();
}
}
可以看到try…catch和while的位置不同,兩個線程表現出來的結果是不同的。
線程等待
有時候,我們需要等待一個線程完成它的工作後,才能進行自己下一步工作。這時候我們就需要一個方法等待線程的結束。
常用的線程等待的方法如下:
方法 | 說明 |
public void join() | 等待線程結束 |
public void join(long millis) | 等待線程結束,最多等millis毫秒 |
public void join(long millis, int nanos) | 等待線程結束,最多等millis毫秒nanos納秒 |
我們來看一個代碼,該代碼可以實現threads數組中的線程挨個順序執行:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2; ++i) {
System.out.println(Thread.currentThread().getName());
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
threads[i].join();
}
}
}
如果我們把threads[i].join()
這句代碼去掉會怎麼樣,我們來看一下:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2; ++i) {
System.out.println(Thread.currentThread().getName());
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
}
}
可以看到去掉join()順序就亂了。
線程的狀態
線程的狀態是一個枚舉類型Thread.State,我們來看一下:
public class ThreadDemo {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
- 初始狀態(NEW):只是安排了工作,還未開始行動;
- 運行中(RUNNABLE):CPU正在調度該線程;
- 就緒(READY):等待被系統調度;
- 等待(WAITING)、超時等待(TIMED_WAITING)、阻塞(BLOCKED):排隊等着其他事情;
- 終止(TERMINATED):工作全部完成。
線程的超時等待、阻塞和等待都對應操作系統中進程的阻塞狀態。只不過Java中對阻塞狀態進行了細分。劃分出了三種。
線程安全
我們來看一個例子來體會一下什麼是線程不安全:
我們啓動20個線程來對同一個變量進行自增操作,每個線程自增一萬次。
public class ThreadDemo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
++num;
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
我們預期的結果是20萬,但是這裏只有17萬多。這種現象我們可以稱之爲線程不安全,爲什麼會出現線程不安全的問題呢?
線程不安全的原因
原子性
我們把一段代碼想像成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證,A進入房間之後,還沒有出來;B是不是也可以進入房間,打斷A在房間裏的隱私。這個就是不具備原子性的。
如何解決這個問題呢?我們可以給房間加一把鎖,A進去把門鎖上,其他人就進不來了,這樣我們就保證了這段代碼的原子性了。
有時也把這個現象叫做同步互斥,表示操作是互相排斥的。
一條Java語句不一定是原子的,也不一定只是一條指令。
比如剛纔我們看到的++num,其實是由三步操作組成的:
- 從內存中把數據讀到CPU;
- 進行數據更新;
- 把數據寫回到內存中。
不保證原子性會給多線程帶來什麼問題?
如果一個線程正在對一個變量進行操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。
可見性
JVM將內存組織爲主內存和工作內存兩部分:
- 主內存:包括本地方法區和堆;
- 工作內存:每個線程都有一個工作內存,工作內存中主要包括兩個部分,一個是屬於該線程私有的棧和對主存部分變量拷貝的寄存器。
所有的變量都存儲在主內存中,對於所有線程都是共享的;線程之間無法直接訪問對方的工作內存中的變量,線程間變量的傳遞均需要通過主內存來完成。
JVM執行過程中,共享變量在多線程之間不能及時看到改變,這個就是可見性問題。
有序性
一段代碼邏輯如下:
- 去前臺取U盤;
- 去教室寫10分鐘作業;
- 去前臺取下快遞。
如果是在單線程情況下,JVM、CPU指令集會對其進行優化,比如,按照1->3->2的方式執行,也是沒問題的,可以少跑一次前臺。這種叫做指令重排序。
但是如果在多線程場景下就有問題了,可能快遞是在你寫作業的10分鐘內被另一個線程放過來的,或者被人變過了,如果指令重排序,代碼就會是錯誤的。
有序性:如果在線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句指:線程內表現爲串行語義,後半句是指:指令重排現象和工作內存與主內存同步延遲現象。
synchronized關鍵字
對於前面的線程不安全的問題,我們可以使用synchronized關鍵字來實現線程安全。
synchronized的用法:
- synchronized修飾普通方法,此時鎖的是當前實例的對象;
- synchronized修飾靜態方法,此時鎖的是類的class對象;
- synchronized修飾代碼塊,此時鎖的是括號內的對象。
我們分別使用synchronized關鍵字來解決一下前面的線程不安全問題。
靜態同步方法:
public class ThreadDemo {
private static int num = 0;
public static synchronized void increment() {
++num;
}
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
increment();
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
同步代碼塊:
public class ThreadDemo {
private static int num = 0;
public static void main(String[] args) {
Object o = new Object();
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
synchronized (o) {
++num;
}
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
注意:
- 想要使多個線程保持同步,需要保證多個線程鎖的是同一個對象。
- 使用同步代碼塊時,不建議去鎖一個Integer或String對象,因爲它們有時候不在常量池,而在堆中,就不是唯一的了,有可能多個線程鎖的是不同的對象,就無法達到同步的效果。
同步方法、靜態同步方法可以和同步代碼塊之間相互轉換。
同步方法和同步代碼塊:
// 同步方法
public synchronized void method() {}
// 同步代碼塊
public void method() {
synchronized (this) {}
}
靜態同步方法和同步代碼塊:
// 靜態同步方法
public static synchronized void method() {}
// 同步代碼塊
public static void method() {
synchronized (ThreadDemo.class) {}
}
synchronized能夠保證原子性、可見性和有序性。
synchronized不能鎖null,因爲synchronized鎖在對象頭上。null是沒有對象頭的。
線程間通信
我們主要來看三個方法:wait()
、notify()
和notifyAll()
。
wait方法
wait()方法就是使線程停止運行。
- 方法wait()的作用是使當前執行的線程進行等待,wait()方法是Object類的方法,該方法是用來將當前線程置入“等待隊列”,並且在wait()所在的代碼處停止執行,直到接到通知或被中斷爲止;
- wait()方法只能在同步方法中或同步塊中調用。如果調用wait()時,沒有持有適當的鎖,會拋出異常;
- wait()方法執行後,當前線程釋放鎖,其他線程競爭獲取鎖。
我們來看一段代碼:
package Thread;
public class WaitTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
System.out.println("等待中...");
o.wait();
System.out.println("等待結束!");
}
System.out.println("main方法結束!");
}
}
這段代碼在執行到o.wait()的時候會一直等待下去。除非被中斷或喚醒。
notify方法
notify()方法就是使停止的線程繼續運行。
- 方法notify()也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象鎖的其它線程,對其發出通知,使其可以重新競爭鎖。如果有多個線程等待,則有線程規劃器隨機挑選出一個處於等待隊列的線程進行喚醒;
- 在notify()方法後,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程執行完同步代碼塊中的代碼之後纔會釋放對象鎖。雖然此時已經有線程被喚醒,但是執行notify()方法的線程還持有鎖,所以被喚醒的線程依舊會等待在鎖上。
我們來看一下代碼:
package Thread;
public class NotifyTest {
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (NotifyTest.class) {
try {
System.out.println("t1線程正在運行!");
System.out.println("t1線程正在等待!");
NotifyTest.class.wait();
System.out.println("t1線程被喚醒!");
System.out.println("t1線程即將退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (NotifyTest.class) {
System.out.println("t2線程正在運行!");
NotifyTest.class.notify();
System.out.println("t2線程即將退出!");
}
}
};
t2.start();
}
}
notifyAll方法
上面的notify()方法只能喚醒某一個等待線程,那麼如果有多個線程都在等待中怎麼辦呢,這個時候就可以使用notifyAll方法可以一次喚醒所有的等待線程,我們直接來看代碼:
package Thread;
import com.sun.xml.internal.bind.annotation.OverrideAnnotationOf;
public class NotifyAllTest {
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
try {
System.out.println("t1線程正在運行!");
System.out.println("t1線程正在等待!");
NotifyAllTest.class.wait();
System.out.println("t1線程被喚醒!");
System.out.println("t1線程即將退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
try {
System.out.println("t2線程正在運行!");
System.out.println("t2線程正在等待!");
NotifyAllTest.class.wait();
System.out.println("t2線程被喚醒!");
System.out.println("t2線程即將退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t2.start();
Thread t3 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
System.out.println("t3線程正在運行!");
NotifyAllTest.class.notifyAll();
System.out.println("t3線程即將退出!");
}
}
};
t3.start();
}
}
線程間通信總結:
- wait()、notify()、notifyAll()三個方法的執行都必須在synchronized代碼塊中;
- 執行這三個方法必有持有相應的鎖對象;
- wait、notify、notifyAll都是java.lang.Object類的方法,而不是Thread固有的方法。換句話說,wait、notify和notifyAll這三個方法與其說是針對線程的操作,倒不如說是針對實例的等待隊列的操作。由於所有實例都有等待隊列,所以wait、notify和notifyAll也就成爲了Object類的方法。
wait和sleep的區別
- wait之前需要請求鎖,而wait執行時會先釋放鎖,等被喚醒時再重新請求鎖。這個鎖是wait對象上的monitor lock;
- sleep是無視鎖的存在的,即之前請求的鎖不會釋放,沒有鎖也不會請求;
- wait方法是Object的方法;
- sleep方法是Thread類的靜態方法。