Java線程
線程是程序中的執行線程,Java 虛擬機允許應用程序併發地運行多個執行線程。
每個線程都有一個優先級,高優先級線程的執行優先於低優先級線程。每個線程都可以或不可以標記爲一個守護程序。當某個線程中運行的代碼創建一個新 Thread 對象時,該新線程的初始優先級被設定爲創建線程的優先級,並且當且僅當創建線程是守護線程時,新線程纔是守護程序。
創建新執行線程有兩種方法:
- 一種方法是將類聲明爲
Thread
的子類,該子類應重寫 Thread 類的run()
方法,接下來可以分配並啓動該子類的實例。 - 另一種方法是聲明實現
Runnable
接口的類,該類實現run()
方法,然後可以分配該類的實例,在創建 Thread 時作爲一個參數來傳遞並啓動。
當 Java 虛擬機啓動時,通常都會有單個非守護線程(它通常會調用某個指定類的 main() 方法)。Java 虛擬機會繼續執行線程,直到下列任一情況出現時爲止:
- 調用了 Runtime 類的
exit()
方法,並且安全管理器允許退出操作發生。 - 非守護線程的所有線程都已停止運行,無論是通過從對 run() 方法的調用中返回,還是通過拋出一個傳播到 run() 方法之外的異常。
一、創建線程類
1 創建Thread類的子類
Java使用 java.lang.Thread
類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。
Java中通過繼承Thread類來創建並啓動多線程的步驟如下:
- 定義Thread類的子類,並重寫該類的
run()
方法,該run()方法的方法體中設置線程需要完成的任務,因此把run()方法稱爲線程執行體。 - 創建Thread子類的實例,即創建了線程對象
- 調用線程對象的
start()
方法來啓動該線程。
start()方法使得該線程開始執行,JVM調用該線程的run()方法,其執行的結果是兩個線程併發地運行;當前線程(調用 start() 方法的那個線程)和另一個線程(創建的新線程,會執行其 run() 方法)。多次啓動一個線程是非法的,特別是當線程已經結束執行後,不能再重新啓動。 主線程是指執行主方法(main()方法)的那個線程,注意父線程不一定是主線程。
注:Java程序屬於搶佔式調度,哪個線程的優先級高哪個線程就優先執行,同一個優先級的線程則隨機選擇一個執行。
src/main/java/top/onefine/demo/thread/MyThread
.java:
package top.onefine.demo.thread;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("run: " + i);
}
}
}
src/main/java/top/onefine/demo/thread/Demo01Thread
.java
package top.onefine.demo.thread;
public class Demo01Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread(); // mt對象
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main: " + i);
}
}
}
執行結果(某一種):
main: 0
main: 1
run: 0
run: 1
run: 2
run: 3
run: 4
main: 2
main: 3
run: 5
main: 4
main: 5
main: 6
main: 7
main: 8
main: 9
run: 6
run: 7
run: 8
run: 9
多線程內存圖解
程序啓動運行main時候,java虛擬機啓動一個進程,主線程main在main()調用時候被創建。隨着調用mt的對象的start方法,另外一個新的線程也啓動了,這樣,整個應用就在多線程下運行。
多線程執行時,在棧內存中每一個執行線程都有一片自己所屬的棧內存空間,進行方法的壓棧和彈棧。
當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。
Thread類
至此已經可以完成最基本的線程開啓,在完成操作過程中用到了 java.lang.Thread 類,此類的API中中定義了有關線程的一些方法,具體如下:
構造方法:
public Thread()
:分配一個新的線程對象。public Thread(String name)
:分配一個指定名字的新的線程對象。public Thread(Runnable target)
:分配一個帶有指定目標新的線程對象。public Thread(Runnable target, String name)
:分配一個帶有指定目標新的線程對象並指定名字。
常用方法:
public String getName()
:獲取當前線程名稱。可以先獲取到當前正在執行的線程,再通過此方法獲取當前線程名稱。public void setName(String name)
:改變線程名稱,使之與參數name相同。public void start()
:導致此線程開始執行; Java虛擬機調用此線程的run方法。public void run()
:此線程要執行的任務在此處定義代碼。public static void sleep(long millis)
:使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。public static Thread currentThread()
:返回對當前正在執行的線程對象的引用。
栗子1:獲取線程名稱
src/main/java/top/onefine/demo/thread/MyThread.java
package top.onefine.demo.thread;
public class MyThread extends Thread {
@Override
public void run() {
String name = super.getName();// 直接獲取線程的名稱
System.out.println("線程名稱:" + name);
}
}
src/main/java/top/onefine/demo/thread/Demo01Thread.java
package top.onefine.demo.thread;
public class Demo01Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
Thread currentThread = Thread.currentThread();
System.out.println("主線程名稱:" + currentThread.getName()); // Thread.currentThread().getName();
new MyThread().start();
new MyThread().start();
}
}
執行結果(不唯一):
線程名稱:Thread-0
主線程名稱:main
線程名稱:Thread-2
線程名稱:Thread-1
栗子2:設置線程名稱
src/main/java/top/onefine/demo/thread/MyThread.java:
package top.onefine.demo.thread;
public class MyThread extends Thread {
public MyThread() {
super();
}
// 創建一個帶參數的構造方法,參數傳遞線程的名稱;調用父類的帶參構造方法,
// 把線程名稱傳遞給父類,讓父類(Thread)給子線程起一個名字
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("線程名稱:" + Thread.currentThread().getName());
}
}
src/main/java/top/onefine/demo/thread/Demo01Thread.java:
package top.onefine.demo.thread;
public class Demo01Thread {
public static void main(String[] args) {
// MyThread myThread = new MyThread();
// myThread.setName("Thread-one");
MyThread myThread = new MyThread("Thread-fine");
myThread.start();
System.out.println("主線程名稱:" + Thread.currentThread().getName());
new MyThread().start();
new MyThread().start();
}
}
栗子3:sleep()使用
src/main/java/top/onefine/demo/thread/MyThread.java:
package top.onefine.demo.thread;
public class MyThread extends Thread {
@Override
public void run() {
// 模擬秒錶
for (int i = 0; i < 60; i++) {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + 1);
}
}
}
src/main/java/top/onefine/demo/thread/Demo01Thread.java:
package top.onefine.demo.thread;
public class Demo01Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2 實現Runnable接口方式(重點)
實現 java.lang.Runnable
接口也是非常常見的一種,只需要重寫run()方法即可。Runnable 接口應該由那些打算通過某一線程執行其實例的類來實現。
步驟如下:
- 定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- 創建Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象。
- 調用線程對象的start()方法來啓動線程。
src/main/java/top/onefine/demo/thread/MyThread.java:
package top.onefine.demo.thread;
public class MyThread implements Runnable {
// 實現Runnable接口的run()方法,設置線程任務
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
src/main/java/top/onefine/demo/thread/Demo02Runnable.java:
package top.onefine.demo.thread;
public class Demo02Runnable {
public static void main(String[] args) {
// 1. 創建Runnable接口的實現類對象
MyThread run = new MyThread();
// 2. 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象
Thread myThread = new Thread(run);
// 3. 調用Thread類中的start方法,開啓新的線程執行run方法
myThread.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
實現Runnable接口創建多線程程序的好處:
1.避免了單繼承的侷限性
- Java中一個類只能繼承一個類,類繼承了Thread類就不能繼承其他的類
- 實現了Runnable接口,還可以繼承其他的類,實現其他的接口
2.增強了程序的擴展性,降低了程序的耦合性(解耦)
- 實現Runnable接口的方式,把設置線程任務和開啓新線程進行了分離(解耦)
- 實現類中,重寫了run方法:用來設置線程任務
- 創建Thread類對象,調用start方法:用來開啓新線程
通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。
在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
Runnable對象僅僅作爲Thread對象的target,Runnable實現類裏包含的run()方法僅作爲線程執行體。
而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。
3. Thread和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
實現Runnable接口比繼承Thread類所具有的優勢總結:
- 適合多個相同的程序代碼的線程去共享同一個資源。
- 可以避免java中的單繼承的侷限性。
- 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
- 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
擴充:在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進程。
4. 匿名內部類方式實現線程的創建
src/main/java/top/onefine/demo/thread/Demo03InnerClassThread.java:
package top.onefine.demo.thread;
public class Demo03InnerClassThread {
public static void main(String[] args) {
// Thread
new Thread() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}.start();
// Runnable
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}).start();
}
}
執行結果(不唯一):
Thread-1: 0
Thread-0: 0
Thread-0: 1
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
Thread-0: 2
Thread-0: 3
Thread-1: 6
Thread-1: 7
Thread-0: 4
Thread-0: 5
Thread-0: 6
Thread-0: 7
Thread-1: 8
Thread-0: 8
Thread-1: 9
Thread-1: 10
Thread-0: 9
Thread-1: 11
Thread-1: 12
Thread-1: 13
Thread-1: 14
Thread-1: 15
Thread-1: 16
Thread-1: 17
Thread-1: 18
Thread-1: 19
Thread-0: 10
Thread-0: 11
Thread-0: 12
Thread-0: 13
Thread-0: 14
Thread-0: 15
Thread-0: 16
Thread-0: 17
Thread-0: 18
Thread-0: 19
二、線程安全
1. 線程安全
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
src/main/java/top/onefine/demo/thread/RunnableImpl.java:
package top.onefine.demo.thread;
/**
* 實現賣票案例
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private int ticket = 100;
// 設置線程任務:賣票
@Override
public void run() {
// 讓賣票操作重複執行
while (true) {
// 先判斷票是否存在
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
this.ticket--;
} else break;
}
}
}
src/main/java/top/onefine/demo/thread/Demo04ThreadSafe.java:
package top.onefine.demo.thread;
/**
* 模擬賣票案例
*/
public class Demo04ThreadSafe {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread thread_1 = new Thread(runnable);
Thread thread_2 = new Thread(runnable);
Thread thread_3 = new Thread(runnable);
thread_1.start();
thread_2.start();
thread_3.start();
}
}
執行結果(不唯一):
Thread-1正在賣第100張票
Thread-0正在賣第100張票
Thread-2正在賣第100張票
Thread-2正在賣第97張票
Thread-1正在賣第97張票
Thread-0正在賣第97張票
Thread-2正在賣第94張票
Thread-0正在賣第94張票
Thread-1正在賣第94張票
Thread-0正在賣第91張票
Thread-2正在賣第91張票
Thread-1正在賣第91張票
Thread-0正在賣第88張票
Thread-2正在賣第88張票
Thread-1正在賣第88張票
Thread-2正在賣第85張票
Thread-1正在賣第85張票
Thread-0正在賣第85張票
Thread-1正在賣第82張票
Thread-0正在賣第82張票
Thread-2正在賣第82張票
Thread-2正在賣第79張票
Thread-0正在賣第79張票
Thread-1正在賣第79張票
Thread-1正在賣第76張票
Thread-0正在賣第76張票
Thread-2正在賣第76張票
Thread-2正在賣第73張票
Thread-1正在賣第73張票
Thread-0正在賣第73張票
Thread-1正在賣第70張票
Thread-0正在賣第70張票
Thread-2正在賣第70張票
Thread-0正在賣第67張票
Thread-1正在賣第67張票
Thread-2正在賣第67張票
Thread-2正在賣第64張票
Thread-1正在賣第64張票
Thread-0正在賣第64張票
Thread-1正在賣第61張票
Thread-2正在賣第61張票
Thread-0正在賣第61張票
Thread-2正在賣第58張票
Thread-1正在賣第58張票
Thread-0正在賣第58張票
Thread-0正在賣第55張票
Thread-2正在賣第55張票
Thread-1正在賣第55張票
Thread-0正在賣第52張票
Thread-2正在賣第52張票
Thread-1正在賣第52張票
Thread-2正在賣第49張票
Thread-1正在賣第49張票
Thread-0正在賣第49張票
Thread-2正在賣第46張票
Thread-1正在賣第46張票
Thread-0正在賣第46張票
Thread-2正在賣第43張票
Thread-0正在賣第42張票
Thread-1正在賣第42張票
Thread-1正在賣第40張票
Thread-2正在賣第40張票
Thread-0正在賣第40張票
Thread-0正在賣第37張票
Thread-1正在賣第36張票
Thread-2正在賣第37張票
Thread-1正在賣第34張票
Thread-0正在賣第34張票
Thread-2正在賣第32張票
Thread-1正在賣第31張票
Thread-0正在賣第31張票
Thread-2正在賣第29張票
Thread-0正在賣第28張票
Thread-1正在賣第28張票
Thread-2正在賣第26張票
Thread-0正在賣第25張票
Thread-1正在賣第25張票
Thread-2正在賣第23張票
Thread-0正在賣第22張票
Thread-1正在賣第22張票
Thread-2正在賣第20張票
Thread-0正在賣第19張票
Thread-1正在賣第19張票
Thread-2正在賣第17張票
Thread-1正在賣第16張票
Thread-0正在賣第16張票
Thread-2正在賣第14張票
Thread-1正在賣第13張票
Thread-0正在賣第13張票
Thread-2正在賣第11張票
Thread-0正在賣第10張票
Thread-1正在賣第10張票
Thread-2正在賣第8張票
Thread-1正在賣第7張票
Thread-0正在賣第7張票
Thread-2正在賣第5張票
Thread-1正在賣第4張票
Thread-0正在賣第4張票
Thread-2正在賣第2張票
Thread-0正在賣第1張票
Thread-1正在賣第1張票
Thread-2正在賣第-1張票
發現程序出現了兩個問題:
- 相同的票數,比如1這張票被賣了兩回。
- 不存在的票,比如-1票,是不存在的。
這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。
線程安全問題是不能產生的,可以讓一個線程在訪問共享數據的時候,無論是否失去了cpu的執行權(控制權),讓其他的線程只能等待,等待當前程序賣完票,其他線程再進行賣票。保證只有一個線程在賣票。
線程安全問題都是由**全局變量(如類中定義int i = 9)及靜態變量(如static int j = 8;)**引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
2. 線程同步
當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程併發訪問一個資源的安全性問題,也就是解決重複票與不存在票問題,就要保證每個線程都能正常執行原子操作,Java中提供了同步機制(synchronized)來解決。
有三種方式完成同步操作:
-
同步代碼塊
-
同步方法
-
鎖機制
2.1 同步代碼塊
同步代碼塊: synchronized
關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
同步鎖(對象鎖或對象監視器):對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖。鎖的作用是把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行。
- 鎖對象 可以是任意類型。
- 多個線程對象 要使用同一把鎖。
src/main/java/top/onefine/demo/thread/RunnableImpl.java:
package top.onefine.demo.thread;
/**
* 使用同步代碼塊解決線程安全問題
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private int ticket = 100;
// 創建一個鎖對象
Object obj = new Object();
// 設置線程任務:賣票
@Override
public void run() {
// 讓賣票操作重複執行
while (true) {
// 同步代碼塊
synchronized (obj) {
// 先判斷票是否存在
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
this.ticket--;
} else break;
}
}
}
}
線程遇到synchronized代碼塊時會檢查synchronized代碼塊是否有鎖對象,若有就會獲取到鎖對象,進入到同步中執行。執行完同步代碼塊中的代碼,線程就會把鎖對象歸還。若沒有就會進入到阻塞狀態,一直等待其他線程歸還鎖對象。即同步中的線程沒有執行完不會釋放鎖,同步外的線程沒有鎖進不去同步。
同步保證了只能有一個線程在同步中執行共享數據,保證了安全,但程序頻繁的判斷鎖,獲取鎖,釋放鎖,會造成程序的效率低下。
注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着(BLOCKED)。
2.2 同步方法
同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。同步方法把方法內部的代碼鎖住。
同步方法的鎖對象——同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。
package top.onefine.demo.thread;
/**
* 使用同步代碼方法解決線程安全問題
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private int ticket = 100;
// 創建一個鎖對象
Object obj = new Object();
// 把訪問了共享數據的代碼抽取出來,放到一個方法中
// 設置線程任務:賣票
@Override
public void run() {
// 可以優化,比如先判斷是否大於0
// 讓賣票操作重複執行
do {
payTicket();
} while (ticket > 0);
}
// 定義一個同步方法
public synchronized void payTicket() {
// 先判斷票是否存在
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
this.ticket--;
}
}
/*
public void payTicket() {
// 先判斷票是否存在
synchronized (this) {
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
this.ticket--;
}
}
}
*/
}
靜態方法:
package top.onefine.demo.thread;
/**
* 使用同步代碼方法解決線程安全問題
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private static int ticket = 100;
// 把訪問了共享數據的代碼抽取出來,放到一個方法中
// 設置線程任務:賣票
@Override
public void run() {
// 可以優化,比如先判斷是否大於0
if (!(ticket > 0))
return;
// 讓賣票操作重複執行
do {
payTicketStatic();
} while (ticket > 0);
}
// 定義一個靜態同步方法
public static synchronized void payTicketStatic() {
// 先判斷票是否存在
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
this是創建對象之後產生的,靜態方法優先於對象,所以靜態方法的鎖對象是本類的class屬性,即class文件對象(反射):
package top.onefine.demo.thread;
/**
* 使用同步代碼方法解決線程安全問題
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private static int ticket = 100;
// 把訪問了共享數據的代碼抽取出來,放到一個方法中
// 設置線程任務:賣票
@Override
public void run() {
// 可以優化,比如先判斷是否大於0
if (!(ticket > 0))
return;
// 讓賣票操作重複執行
do {
payTicketStatic();
} while (ticket > 0);
}
// 定義一個同步方法
public static void payTicketStatic() {
// 先判斷票是否存在
synchronized (RunnableImpl.class) {
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
}
2.3 鎖機制-Lock鎖
java.util.concurrent.locks.Lock
機制提供了比synchronized
代碼塊和synchronized
方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
public void lock()
:加同步鎖。public void unlock()
:釋放同步鎖。
java.util.concurrent.locks.ReentrantLock
類實現了Lock接口
package top.onefine.demo.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用Lock鎖解決線程安全問題
*/
public class RunnableImpl implements Runnable {
// 定義一個多線程共享的票源
private int ticket = 100;
Lock lock = new ReentrantLock();
// 把訪問了共享數據的代碼抽取出來,放到一個方法中
// 設置線程任務:賣票
@Override
public void run() {
// 可以優化,比如先判斷是否大於0
if (!(ticket > 0))
return;
// 讓賣票操作重複執行
do {
// 先判斷票是否存在
lock.lock();
if (ticket > 0) {
// 讓程序睡眠,大概率暴露出安全問題
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 無論程序是否異常,都釋放鎖
}
}
} while (ticket > 0);
}
}
三、線程狀態
當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,有幾種狀態呢?在API中 java.lang.Thread.State
這個枚舉中給出了六種線程狀態:
線程狀態 | 導致狀態發生條件 |
---|---|
NEW (新建) |
線程剛被創建,但是並未啓動。還沒調用start方法。 |
Runnable (可運行) |
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器。 |
Blocked (鎖阻塞) |
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。 |
Waiting (無限等待) |
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 |
TimedWaiting (計時等待) |
同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。 |
Teminated (被終止) |
因爲run方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run方法而死亡。 |
1. Timed Waiting(計時等待)
Timed Waiting在API中的描述爲:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態。單獨的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪裏呢?
在我們寫賣票的案例中,爲了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。
其實當我們調用了sleep方法之後,當前執行的線程就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等待),那麼我們通過一個案例加深對該狀態的一個理解。
sleep方法的使用還是很簡單的。我們需要記住下面幾點:
- 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協作關係。
- 爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程中會睡眠
- sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態。
小提示:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始立刻執行。
Timed Waiting 線程狀態圖:
2. BLOCKED(鎖阻塞)
Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。
如同步機制中,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。
這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態(瞭解)。
Blocked 線程狀態圖:
3. Waiting(無限等待)
Wating狀態在API中介紹爲:一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。
package top.onefine.demo.thread;
/**
* 等待喚醒案例:線程之間的通信
* 創建一個顧客線程(消費者):告知老闆要的包子的種類和數量,調用wait方法,放棄cpu的執行,進入到WAITING狀態(無限等待)
* 創建一個老闆線程(生產者):花了5秒做包子,做好包子之後,調用notify方法,喚醒顧客喫包子
*
* 注意:
* 顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行
* 同步使用的鎖對象必須保證唯一
* 只有鎖對象才能調用wait和notify方法
*
* Obejct類中的方法
* void wait()
* 在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
* void notify()
* 喚醒在此對象監視器上等待的單個線程。
* 會繼續執行wait方法之後的代碼
*
*/
public class Demo01WaitAndNotify {
public static void main(String[] args) {
// 創建鎖對象,保證鎖對象唯一
Object obj = new Object();
// 創建一個顧客線程(消費者)
new Thread() {
@Override
public void run() {
// 一直等着買包子
while (true) {
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj) {
System.out.println("告知老闆要的包子的種類和數量");
// 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒之後執行的代碼
System.out.println("開喫做好的包子!");
}
}
}
}.start();
// 創建老闆線程(生產者)
new Thread() {
@Override
public void run() {
// 一直做包子
while (true) {
// 老闆花5s做包子
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj) {
System.out.println("老闆5s之後做好包子,告知顧客可以喫包子了");
// 做好包子後調用notify方法喚醒顧客喫包子
obj.notify(); // 注意,與wait()方法使用的鎖是同一個
}
}
}
}.start();
}
}
package top.onefine.demo.thread;
/**
*
* 進入到TimeWaiting(計時等待)有兩種方式
* 1.使用sleep(long m)方法,在毫秒值結束之後,線程睡醒進入到Runnable/Blocked狀態
* 2.使用wait(long m)方法,wait方法如果在毫秒值結束之後,還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blocked狀態
*
* 喚醒的方法:
* void notify() 喚醒在此對象監視器上等待的單個線程。
* void notifyAll() 喚醒在此對象監視器上等待的所有線程。
*/
public class Demo01WaitAndNotify {
public static void main(String[] args) {
// 創建鎖對象,保證鎖對象唯一
Object obj = new Object();
// 創建一個顧客線程(消費者)
new Thread() {
@Override
public void run() {
// 一直等着買包子
while (true) {
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj) {
System.out.println("顧客1告知老闆要的包子的種類和數量");
// 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
try {
// obj.wait(5000); // 自己等5s,時間到立刻醒來,無論老闆做沒做好包子
obj.wait(); // 只有等老闆喚醒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒之後執行的代碼
System.out.println("顧客1開喫做好的包子!");
}
}
}
}.start();
// 創建一個顧客線程(消費者)
new Thread() {
@Override
public void run() {
// 一直等着買包子
while (true) {
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj) {
System.out.println("顧客2告知老闆要的包子的種類和數量");
// 調用wait方法,自動放棄cpu的執行,進入到waiting狀態(無限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒之後執行的代碼
System.out.println("顧客2開喫做好的包子!");
}
}
}
}.start();
// 創建老闆線程(生產者)
new Thread() {
@Override
public void run() {
// 一直做包子
while (true) {
// 老闆花5s做包子
try {
Thread.sleep(5000); // 睡眠5s
} catch (InterruptedException e) {
e.printStackTrace();
}
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj) {
System.out.println("老闆5s之後做好包子,告知顧客可以喫包子了");
// 做好包子後調用notify方法喚醒顧客喫包子
// 若有多個等待線程,則隨機喚醒一個等待的進程
// obj.notify(); // 注意,與wait()方法使用的鎖是同一個
// 喚醒所有等待的線程
obj.notifyAll();
}
}
}
}.start();
}
}
通過上述案例我們會發現,一個調用了某個對象的Object.wait()
方法的線程會等待另一個線程調用此對象的Object.notify()
方法或Object.notifyAll()
方法。
其實waiting狀態並不是一個線程的操作,它體現的是多個線程間的通信,可以理解爲多個線程之間的協作關係,多個線程會爭取鎖,同時相互之間又存在協作關係。就好比在公司裏你和你的同事們,你們可能存在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。
當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那麼A線程喚醒後就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。
Waiting 線程狀態圖:
4. 總結
在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀態聯繫還是很緊密的,比如Waiting(無限等待) 狀態中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。這種帶參的方法,其實是一種倒計時操作,相當於我們生活中的小鬧鐘,我們設定好時間,到時通知,可是如果提前得到(喚醒)通知,那麼設定好時間在通知也就顯得多此一舉了,那麼這種設計方案其實是一舉兩得。如果沒有得到(喚醒)通知,那麼線程就處於Timed Waiting狀態,直到倒計時完畢自動醒來;如果在倒計時期間得到(喚醒)通知,那麼線程從Timed Waiting狀態立刻喚醒。
四、等待喚醒機制
1. 線程間通信
**概念:**多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。
比如:線程A用來生成包子的,線程B用來喫包子的,包子可以理解爲同一資源,線程A與線程B處理的動作,一個是生產,一個是消費,那麼線程A與線程B之間就存在線程通信問題。
爲什麼要處理線程間通信:
多個線程併發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。
如何保證線程間通信有效利用資源:
多個線程在處理同一個資源,並且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
2. 等待喚醒機制
什麼是等待喚醒機制
這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,線程間也會有協作機制。就好比在公司裏你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。
就是在一個線程進行了規定操作後,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過後 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。
wait/notify 就是線程間的一種協作機制。
等待喚醒中的方法
等待喚醒機制就是用於解決線程間通信的問題的,使用到的3個方法的含義如下:
- wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等着別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
- notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
- notifyAll:則釋放所通知對象的 wait set 上的全部線程。
注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因爲它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法之後的地方恢復執行。
總結如下:
- 如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
- 否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態
調用wait和notify方法需要注意的細節
- wait方法與notify方法必須要由同一個鎖對象調用。因爲:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法後的線程。
- wait方法與notify方法是屬於Object類的方法的。因爲:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
- wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因爲:必須要通過鎖對象調用這2個方法。
3. 生產者與消費者問題
等待喚醒機制其實就是經典的“生產者與消費者”的問題。
就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:
包子鋪線程生產包子,喫貨線程消費包子。當包子沒有時(包子狀態爲false),喫貨線程等待,包子鋪線程生產包子(即包子狀態爲true),並通知喫貨線程(解除喫貨的等待狀態),因爲已經有包子了,那麼包子鋪線程進入等待狀態。接下來,喫貨線程能否進一步執行則取決於鎖的獲取情況。如果喫貨獲取到鎖,那麼就執行喫包子動作,包子喫完(包子狀態爲false),並通知包子鋪線程(解除包子鋪的等待狀態),喫貨線程進入等待。包子鋪線程能否進一步執行則取決於鎖的獲取情況。
包子類:
package top.onefine.demo.thread;
/**
* 資源類:包子類
*/
public class BaoZi {
String pi; // 包子屬性:皮
String xian; // 包子屬性:餡
Boolean flag = false; // 包子狀態,默認沒有
}
包子鋪類:
package top.onefine.demo.thread;
/**
*
* 生產者(包子鋪)
* 注意:包子鋪線程和包子線程關係爲通信關係(互斥)
* 必須使用同步技術保證兩個線程只能有一個在執行
* 鎖對象必須唯一,可以使用包子對象作爲鎖對象
* 包子鋪類和喫貨類就需要把包子作爲參數傳遞進來
*/
public class BaoZiPu extends Thread {
// 定義包子變量
private BaoZi baoZi;
public BaoZiPu(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
int count = 0; // 交替生產兩種包子
// 讓包子鋪一直生產包子
while (true) {
synchronized (baoZi) {
if (baoZi.flag) // 有包子
try {
baoZi.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 沒包子
// 被喚醒之後執行,包子鋪生產包子
if (count % 2 == 0) {
// 生產薄皮肉餡包子
baoZi.pi = "薄皮";
baoZi.xian = "肉餡";
} else {
// 生產冰皮菜餡包子
baoZi.pi = "冰皮";
baoZi.xian = "菜餡";
}
count++;
System.out.println("包子鋪正在生產:" + baoZi.pi + baoZi.xian + "的包子!");
// 生產包子需要3s鍾
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改包子狀態爲有包子
baoZi.flag = true;
// 喚醒喫貨線程
baoZi.notify();
System.out.println("包子鋪已經生產好了" + baoZi.pi + baoZi.xian + "的包子!");
}
}
}
}
喫貨類:
package top.onefine.demo.thread;
/**
* 消費者(喫貨類)
*/
public class ChiHu extends Thread {
// 定義包子變量
private BaoZi baoZi;
public ChiHu(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
// 使用死循環讓喫貨一直喫包子
while (true) {
synchronized (baoZi) {
if (!baoZi.flag) // 沒有包子
try {
baoZi.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 被喚醒後喫貨喫包子
System.out.println("喫貨正在喫:" + baoZi.pi + baoZi.xian + "的包子!");
// 喫貨喫完包子
baoZi.flag = false;
// 喚醒包子鋪線程生產包子
baoZi.notify();
System.out.println("喫貨喫完了" + baoZi.pi + baoZi.xian + "的包子,包子鋪開始快生產包子!");
}
}
}
}
場景類:
package top.onefine.demo.thread;
/**
* 場景類
*/
public class Client {
public static void main(String[] args) {
// 創建包子對象
BaoZi baoZi = new BaoZi();
// 創建包子鋪線程生產包子
new BaoZiPu(baoZi).start();
// 創建喫貨線程喫包子
new ChiHu(baoZi).start();
}
}
五、線程池
在使用線程的時候就要去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬而是可以繼續執行其他的任務?
在Java中可以通過線程池來達到這樣的效果,在JDK1.5之後,JDK內置了線程池,可以直接使用!
1. 線程池概念
**線程池:**其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。
合理利用線程池能夠帶來三個好處:
- 降低資源消耗。減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。
2. 線程池的使用
Java裏面線程池的頂級接口是 java.util.concurrent.Executor
,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具(線程池的工廠類),真正的線程池接口是 java.util.concurrent.ExecutorService
。
要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在 java.util.concurrent.Executors
線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。
官方建議使用Executors工廠類來創建線程池對象。Executors類中有個創建線程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回線程池對象——ExecutorService接口的實現類對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)
獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:
public Future<?> submit(Runnable task)
:獲取線程池中的某一個線程對象並執行。Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用。
使用線程池中線程對象的步驟:
- 創建線程池對象。
- 創建Runnable接口子類對象。(task)
- 提交Runnable接口子類對象。(take task)
- 關閉線程池(一般不做)。
package top.onefine.demo.threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*
* 線程池:JDK1.5之後提供的
* java.util.concurrent.Executors:線程池的工廠類,用來生成線程池
* Executors類中的靜態方法:
* static ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定線程數的線程池
* 參數:
* int nThreads:創建線程池中包含的線程數量
* 返回值:
* ExecutorService接口,返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)
* java.util.concurrent.ExecutorService:線程池接口
* 用來從線程池中獲取線程,調用start方法,執行線程任務
* submit(Runnable task) 提交一個 Runnable 任務用於執行
* 關閉/銷燬線程池的方法
* void shutdown()
* 線程池的使用步驟:
* 1.使用線程池的工廠類Executors裏邊提供的靜態方法newFixedThreadPool生產一個指定線程數量的線程池
* 2.創建一個類,實現Runnable接口,重寫run方法,設置線程任務
* 3.調用ExecutorService中的方法submit,傳遞線程任務(實現類),開啓線程,執行run方法
* 4.調用ExecutorService中的方法shutdown銷燬線程池(不建議執行)
*/
public class DemoThreadPool {
public static void main(String[] args) {
//1.使用線程池的工廠類Executors裏邊提供的靜態方法newFixedThreadPool生產一個指定線程數量的線程池
ExecutorService es = Executors.newFixedThreadPool(2);
//3.調用ExecutorService中的方法submit,傳遞線程任務(實現類),開啓線程,執行run方法
es.submit(new RunnableImpl());
//線程池會一直開啓,使用完了線程,會 自動 把線程歸還給線程池,線程可以繼續使用
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
//4.調用ExecutorService中的方法shutdown銷燬線程池(不建議執行)
es.shutdown();
//拋異常,線程池都沒有了,就不能獲取線程了
// es.submit(new RunnableImpl());
}
}
/**
* 2.創建一個類,實現Runnable接口,重寫run方法,設置線程任務
*/
class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "創建了一個新的線程執行");
}
}
執行結果(不唯一):
pool-1-thread-2創建了一個新的線程執行
pool-1-thread-2創建了一個新的線程執行
pool-1-thread-1創建了一個新的線程執行