前言
在java中,線程非常重要,我們要分清楚進程和線程的區別:進程是指一個內存中運行的應用程序,每個進程都擁有自己的一塊獨立的內存空間,進程之間的資源不共享;線程是CPU調度的最小單元,一個進程可以有多個線程,線程之間的堆空間是共享的,但棧空間是獨立的,java程序的進程至少包含主線程和後臺線程(垃圾回收線程)。瞭解這些知識後,來看下文有關線程的知識。
併發和並行
我們先來看一下概念:
- 並行:指兩個或多個事件在同一時刻點發生
- 併發:指兩個或多個事件在同一時間段內發生
對於單核CPU的計算機來說,它是不能並行的處理多個任務,它的每一時刻只能有一個程序執行時間片(時間片是指CPU分配給各個程序的運行時間),故在微觀上這些程序只是分時交替的運行,所以在宏觀看來在一段時間內有多個程序在同時運行,看起來像是並行運行。
對於多核CPU的計算機來說,它就可以並行的處理多個任務,可以做到多個程序在同一時刻同時運行。
同理對線程也一樣,但系統只有一個CPU時,線程會以某種順序執行,我們把這種情況稱爲線程調度,所以從宏觀角度上看線程是並行運行的,但是從微觀角度來看,卻是串行運行,即一個線程一個線程的運行。
線程的創建與啓動
有3種方式使用線程。
方式1:繼承Thread類
定義一個類繼承java.lang.Thread類,重寫Thread類中的run方法,如下:
public class MyThread extends Thread {
public void run() {
// ...
}
}
//使用線程
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
方式2:實現Runnable接口
2.1:定義一個類實現Runnable接口
實現 Runnable只能當做一個可以在線程中運行的任務,不是真正意義上的線程,因此最後還需要通過 Thread 來調用,如下:
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
//使用線程
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
2.2、使用匿名內部類
這種方式只適用於這個線程只使用一次的情況,如下:
public class MyRunnable implements Runnable {
//使用線程
public static void main(String[] args) {
new Thread(new Runnable(){
public void run(){
// ...
}
}).start();
}
方式3:實現Callable接口
與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝,所以在創建Thread時,要把FutureTask 傳進去,如下:
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
//使用線程
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
繼承與實現的區別
1、繼承方式:
(1)java中類是單繼承的,如果繼承了Thread,該類就不能有其他父類了,但是可以實現多個接口
(2)從操作上分析,繼承方式更簡單,獲取線程名字也簡單
2、實現方式:
(1)java中類可以實現多接口,此時該類還可以繼承其他類,並且還可以實現其他接口
(2)從操作上分析,實現方式稍複雜,獲取線程名字也比較複雜,得通過Thread.currentThread來獲取當前線程得引用
綜上所述,實現接口會更好一些。
線程的生命週期
線程也是有生命週期,也就是存在不同的狀態,狀態之間相互轉換,線程可以處於以下的狀態之一:
1、NEW(新建狀態)
使用new創建一個線程對象,但還沒有調用線程的start方法,Thread t = new Thread(),此時屬於新建狀態。
2、RUNNABLE(可運行狀態)
但在新建狀態下線程調用了start方法,t.start(),此時進入了可運行狀態。可運行狀態又分爲兩種狀態:
- ready(就緒狀態):線程對象調用stat方法後,等待JVM的調度,此時線程並沒有運行。
- running(運行狀態):線程對象獲得JVM調度,此時線程開始運行,如果存在多個CPU,那麼允許多個線程並行運行。
線程的start方法只能調用一次,否則報錯(IllegalThreadStateException)。
3、BLOCKED(阻塞狀態)
正在運行的線程因爲某些原因放棄CPU,暫時停止運行,就會進入阻塞狀態,此時JVM不會給該線程分配CPU,直到線程重新進入就緒狀態,纔有機會轉到運行狀態,阻塞狀態只能先進入就緒狀態,不能跳過就緒狀態直接進入運行狀態。線程進入阻塞狀態常見的情況有:
- 1、當A線程處於運行狀態時,試圖獲取同步鎖,卻被B線程獲取,此時JVM把當前A線程放到對象的鎖池中,A線程進入阻塞狀態,等待獲取對象的同步鎖。
- 2、當線程處於運行狀態時,發出了IO請求,此時進入阻塞狀態。
4、WAITING(等待狀態)
正在運行的線程調用了無參數的wait方法,此時JVM把該線程放入對象的等待池中,此時線程進入等待狀態,等待狀態的線程只能被其他線程喚醒,否則不會被分配 CPU 時間片。下面是讓線程進入等待狀態的方法:
進入方法 | 退出方法 |
---|---|
無Timeout參數的Object.wait() | Object.notify() / Object.notifyAll() |
無Timeout參數的Thread.join() 方法 | 被調用的線程執行完畢 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
5、TIMED WAITING(計時等待狀態)
正在運行的線程調用了有參數的wait方法,此時JVM把該線程放入對象的等待池中,此時線程進入計時等待狀態,計時等待狀態的線程被其它線程顯式地喚醒,在一定時間之後會被系統自動喚醒。下面是讓線程進入等待狀態的方法:
進入方法 | 退出方法 |
---|---|
調用Thread.sleep(int timeout) 方法 | 時間結束 |
有Timeout 參數的 Object.wait() 方法 | 時間結束 / Object.notify() / Object.notifyAll() |
有Timeout 參數的 Thread.join() 方法 | 時間結束 / 被調用的線程執行完畢 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
ps:阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,通過調用 Thread.sleep() 和 Object.wait() 等方法進入。
6、TREMINATED(終止狀態)
又稱死亡狀態,表示線程的終止。線程進入終止狀態的情況有:
- 1、正常執行完run方法,線程正常退出。
- 2、遇到異常而退出
線程一旦終止了,就不能再次啓動,否則報錯(IllegalThreadStateException)
線程的狀態轉換圖
Thread類中過時的方法
因爲存在線程安全問題,所以棄用了,如下:
- void suspend():暫停當前線程。
- void resume():恢復當前線程。
- void stop():結束當前線程
線程之間的通信
如果一個線程從頭到尾的執行完一個任務,不需要和其他線程打交道的話,那麼就不會存在安全性問題了,由於java內存模式的存在,如下:
每一個java線程都有自己的工作內存,線程之間要想協作完成一個任務,就必須通過主內存來通信,所以這裏就涉及到對共享資源的競爭,在主內存中的東西都是線程之間共享,所以這裏就必須通過一些手段來讓線程之間完成正常通信。主要有以下兩種方法:
1、wait() / notify() notifyAll() 機制
它們都是Object類中的方法,它們的主要作用如下:
- wait():執行該方法的線程對象釋放同步鎖(這是因爲,如果沒有釋放鎖,那麼其它線程就無法進入對象的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,造成死鎖),然後JVM把該線程存放在等待池中,等待其他線程喚醒該線程
- notify():執行該方法的線程喚醒在等待池中等待的任意一個線程,把線程轉到鎖池中等待
- notifyAll():執行該方法的線程喚醒在等待池中等待的所有線程,把線程轉到鎖池中等待
注意:上述方法只能在同步方法或者同步代碼中使用,否則會報IllegalMonitorStateException異常,還有上述方法只能被同步監聽鎖對象來調用,不能使用其他對象調用,否則會報IllegalMonitorStateException異常。
假設A線程和B線程共同操作一個X對象,A、B線程可以通過X對象的wait方法和notify方法進行通信,流程如下:
1、當A線程執行X對象的同步方法時,A線程持有X對象的鎖,則B線程沒有執行同步方法的機會,B線程在X對象的鎖池中等待。
2、A線程在同步方法中執行X.wait()時,A線程釋放X對象的鎖,進入X對象的等待池中。
3、在X對象的鎖池中等待獲取鎖的B線程在這時獲取X對象的鎖,執行X對象的另一個同步方法。
4、B線程在同步方法中執行X.notify()或notifyAll()時,JVM把A線程從X對象的等待池中移到X對象的鎖池中,等待獲取鎖。
5、B線程執行完同步方法,釋放鎖,A線程獲取鎖,從上次停下來的地方繼續執行同步方法。
下面以一個ATM機存錢取錢的例子說明,ATM機要在銀行把錢存進去後,其他人才能取錢,如果沒錢取,只能先回家等待,等銀行通知你有錢取了,再來取,如果有錢取,就直接取錢。
ATM機,存錢和取錢方法都是同步方法:
public class ATM {
private int money;
private boolean isEmpty = true;//標誌ATM是否有錢的狀態
/**
* 往ATM機中存錢
*/
public synchronized void push(int money){
try{
//ATM中有錢,等待被人把錢取走
while (!isEmpty){
this.wait();
}
//ATM中沒錢了,開始存錢
System.out.println(Thread.currentThread().getName() + ":" + "發現ATM機沒錢了,存錢中...");
Thread.sleep(2000);
this.money = money;
System.out.println(Thread.currentThread().getName() + ":" + "存錢完畢,存了" + money + "元");
//存錢完畢,把標誌置爲false
isEmpty = false;
//ATM中有錢了,通知別人取錢
this.notify();
}catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 從ATM機中取錢
*/
public synchronized void pop(){
try {
//ATM中沒錢取,等待通知
while (isEmpty){
System.out.println(Thread.currentThread().getName() + ":" + "ATM機沒錢,等待中...");
this.wait();
}
//ATM中有錢了,開始取錢
System.out.println(Thread.currentThread().getName() + ":" + "收到通知,取錢中...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":"+ "取出完畢,取出了" + this.money + "錢");
//取錢完畢,把標誌置爲true
isEmpty = true;
//ATM沒錢了,通知銀行存錢
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
銀行, 需要傳入同一個ATM示例:
public class Blank implements Runnable {
private ATM mAtm;//共享資源
public Blank(ATM atm){
this.mAtm = atm;
}
@Override
public void run() {
//銀行來存錢
for(int i = 0; i < 2; i++){
mAtm.push(100);
}
}
}
小明, 需要傳入同一個ATM示例:
public class Person implements Runnable{
private ATM mAtm;//共享資源
public Person(ATM atm){
this.mAtm = atm;
}
@Override
public void run() {
//這個人來取錢
mAtm.pop();
}
}
客戶端操作,我特地讓小明提前來取錢,此時ATM機中是沒錢的,小明要等待:
public static void main(String[] args){
//創建一個ATM機
ATM atm = new ATM();
//小明來取錢
Thread tPerson = new Thread(new Person(atm), "XiaoMing");
tPerson.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//銀行來存錢
Thread tBlank = new Thread(new Blank(atm), "Blank");
tBlank.start();
}
輸出結果:
XiaoMing:ATM機沒錢,等待中...
Blank:發現ATM機沒錢了,存錢中...
Blank:存錢完畢,存了100元
XiaoMing:收到通知,取錢中...
XiaoMing:取出完畢,取出了100錢
Blank:發現ATM機沒錢了,存錢中...
Blank:存錢完畢,存了100元
可以看到,小明總是在收到ATM的通知後纔來取錢,如果通過這個存錢取錢的例子還不瞭解wait/notify機制的話,可以看看這個修廁所的例子。
ps: wait() 和 sleep() 的區別是什麼,首先wait()是Object的方法,而sleep()是Thread的靜態方法,其次調用wait()會釋放同步鎖,而sleep()不會,最後一點不同的是調用
wait
方法需要先獲得鎖,而調用sleep
方法是不需要的。
2、await() / signal() signalAll()機制
從java5開始,可以使用Lock機制取代synchronized代碼塊和synchronized方法,使用java.util.concurrent 類庫中提供的Condition 接口的await / signal() signalAll()方法取代Object的wait() / notify() notifyAll() 方法。
下面使用Lock機制和Condition 提供的方法改寫上面的那個例子,如下:
ATM2:
public class ATM2 {
private int money;
private boolean isEmpty = true;//標誌ATM是否有錢的狀態
private Lock mLock = new ReentrantLock();//新建一個lock
private Condition mCondition = mLock.newCondition();//通過lock的newCondition方法獲得一個Condition對象
/**
* 往ATM機中存錢
*/
public void push(int money){
mLock.lock();//獲取鎖
try{
//ATM中有錢,等待被人把錢取走
while (!isEmpty){
mCondition.await();
}
//ATM中沒錢了,開始存錢
System.out.println(Thread.currentThread().getName() + ":" + "發現ATM機沒錢了,存錢中...");
Thread.sleep(2000);
this.money = money;
System.out.println(Thread.currentThread().getName() + ":" + "存錢完畢,存了" + money + "元");
//存錢完畢,把標誌置爲false
isEmpty = false;
//ATM中有錢了,通知別人取錢
mCondition.signal();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
mLock.unlock();//釋放鎖
}
}
/**
* 從ATM機中取錢
*/
public void pop(){
mLock.lock();//獲取鎖
try {
//ATM中沒錢取,等待通知
while (isEmpty){
System.out.println(Thread.currentThread().getName() + ":" + "ATM機沒錢,等待中...");
mCondition.await();
}
//ATM中有錢了,開始取錢
System.out.println(Thread.currentThread().getName() + ":" + "收到通知,取錢中...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":"+ "取出完畢,取出了" + this.money + "錢");
//取錢完畢,把標誌置爲true
isEmpty = true;
//ATM沒錢了,通知銀行存錢
mCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
mLock.unlock();//釋放鎖
}
}
}
可以看到ATM2改寫ATM後,把方法的synchronized去掉,因爲Lock機制沒有同步鎖的概念,然後獲取lock鎖,在finally裏釋放lock鎖,還把原本Object.wait()用Condition.await()代替,原本Object.notify()用Condition.signal()代替。
客戶端操作只是把ATM換成ATM2,輸出結果和上面的一樣,就不在累述。
死鎖
多線程通信的時候很容易造成死鎖,死鎖無法解決,只能避免。
1、死鎖是什麼?
當A線程等待獲取由B線程持有的鎖,而B線程正在等待獲取由A線程持有的鎖,發生死鎖現象,JVM既不檢測也不會避免這種情況,所以程序員必須保證不導致死鎖。
2、如何避免死鎖?
1、當多個線程都要訪問共享資源A、B、C時,保證每一個線程都按照相同的順序去訪問去訪問他們,比如先訪問A,接着訪問B,最後訪問C。
2、不要使用Thread類中過時的方法,因爲容易導致死鎖,所以被廢棄,例如A線程獲得對象鎖,正在執行一個同步方法,如果B線程調用A線程的suspend(),此時A線程暫停運行,放棄CPU,但是不會放棄鎖,所以B就永遠不會得到A持有的鎖。
線程的控制操作
下面來看一些可以控制線程的操作。
1、線程休眠
讓執行的線程暫停等待一段時間,進入計時等待狀態,使用如下:
public static void main(String[] args){
Thread.sleep(1000);
}
調用sleep()後,當前線程放棄CPU,在指定的時間段內,sleep所在的線程不會獲得執行的機會,在此狀態下該線程不會釋放同步鎖。
2、聯合線程
在線程中調用另一個線程的 join() 方法,會將當前線程置於阻塞狀態,等待另一個線程完成後才繼續執行,使用如下:
public class JoinThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("JoinThread執行完畢!");
}
}
public static void main(String[] args) throws InterruptedException {
JoinThread joinThread = new JoinThread();
joinThread.start();
System.out.println("主線程等待...");
joinThread.join();//主線程等join線程執行完畢後才繼續執行
System.out.println("主線程執行完畢");
}
輸出結果:
主線程等待...
JoinThread執行完畢!
主線程執行完畢
對於以上代碼,主線程會等join線程執行完畢後才繼續執行,因此最後的結果能保證join線程的輸出先於主線程的輸出。
3、後臺線程
顧名思義,在後臺運行的線程,其目的是爲其他線程提供服務,也稱“守護線程”,JVM的垃圾回收線程就是典型的後臺線程,通過**t.setDaemon(true)**把一個線程設置爲後臺線程,如下:
public class DeamonThread extends Thread {
@Override
public void run() {
System.out.println(getName());
}
}
public static void main(String[] args) throws InterruptedException {
//主線程不是後臺線程,是前臺線程
DeamonThread deamonThread = new DeamonThread();
deamonThread.setDaemon(true);//設置子線程爲後臺線程
deamonThread.start();
//通過deamonThread.isDaemon()判斷是否是後臺線程
System.out.println(deamonThread.isDaemon());
}
輸出結果:true
後臺線程有以下幾個特點:
1、若所有的前臺線程死亡,後臺線程自動死亡,若前臺線程沒有結束,後臺線程是不會結束的。
2、前臺線程創建的線程默認是前臺線程,可以通過setDaemon(true)設置爲後臺線程,在後臺線程創建的新線程,新線程是後臺線程。
注意:**t.setDaemon(true)**方法必須在start方法前調用,否則會報IllegalMonitorStateException異常
4、線程禮讓
對靜態方法 Thread.yield() 的調用,聲明瞭當前線程已經完成了生命週期中最重要的部分,可以切換給其它線程來執行。如下:
public class YieldThread extends Thread {
@Override
public void run() {
System.out.println("已經完成重要部分,可以讓其他線程獲取CPU時間片");
Thread.yield();
}
}
該方法只是對線程調度器的一個建議,而且也只是建議具有相同優先級的其它線程可以運行。也就是說,就算你執行了這個方法,該線程還是有可能繼續運行下去。
5、線程組
java.lang.ThreadGroup類表示線程組,可以對一組線程進行集中管理,當用戶在創建線程對象時,可以通過構造器指定其所屬的線程組:Thread(ThreadGroup group, String name)。
如果A線程創建B線程,如果沒有設置B線程的分組,那麼B線程加入到A線程的線程組,一旦線程加入某個線程組,該線程就一直存在於該線程組中直到線程死亡,不能在中途修改線程的分組。
當java程序運行時,JVM會創建名爲main的線程組,在默認情況下,所以的線程都屬於該線程組。
結語
限於篇幅,還有ThreadLocal,線程池的知識點沒寫,就留到下一篇了。