多線程(二)
在多線程(一)中,我們說到了第一種方式建立多線程的方式。
補充一點: 說到線程的排程方式,搶占式排程,那麼優先級是怎麼區分的呢,其實我們可以設定線程的優先級,在java中,一般是0-10.
建立線程的方式二
采用 java.lang.Runnable 也是非常常見的一種,我們隻需要重寫run方法即可。
步驟如下:
- 定義Runnable接口的實作類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- 建立Runnable實作類的執行個體,并以此執行個體作為Thread的target來建立Thread對象,該Thread對象才是真正 的線程對象。
- 調用線程對象的start()方法來啟動線程。
代碼如下:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//建立自定義類對象 線程任務對象
MyRunnable mr = new MyRunnable();
//建立線程對象
Thread t = new Thread(mr, "小強");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
通過實作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()方法。
Thread和Runnable的差別
如果一個類繼承Thread,則不适合資源共享。但是如果實作了Runable接口的話,則很容易的實作資源共享。
總結:
實作Runnable接口比繼承Thread類所具有的優勢:
- 适合多個相同的程式代碼的線程去共享同一個資源。
- 可以避免java中的單繼承的局限性。
- 增加程式的健壯性,實作解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
-
線程池隻能放入實作Runable或Callable類線程,不能直接放入繼承Thread的類。
注意
在java中,每次程式運作至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用 java指令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個進 程。
匿名内部類方式實作線程的建立
使用線程的内匿名内部類方式,可以友善的實作每個線程執行不同的線程任務操作。
使用匿名内部類的方式實作Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("張宇:"+i);
// }
// }
// };
//‐‐‐這個整體 相當于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("張宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("費玉清:"+i);
}
}
}
線程安全
如果有多個線程在同時運作,而這些線程可能會同時運作這段代碼。程式每次運作結果和單線程運作的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,示範線程的安全問題:
電影院要賣票,我們模拟電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個 (本場電影隻能賣100張票)。
我們來模拟電影院的售票視窗,實作多個視窗同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個視窗一起賣這100張票) 需要視窗,采用線程對象來模拟;需要票,Runnable接口子類來模拟 模拟票:
package com.example.demo;
public class Ticket implements Runnable{
private int ticket = 100;
public void run() {
//每個視窗賣票的操作
// 視窗 永遠開啟
while (true) {
if (ticket > 0) {
//有票 可以賣
// 出票操作
// 使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
}
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
public class DemoApplicationTests {
public static void main(String[] args) {
//建立線程任務對象
Ticket ticket = new Ticket();
//建立三個視窗對象
Thread t1 = new Thread(ticket, "視窗1");
Thread t2 = new Thread(ticket, "視窗2");
Thread t3 = new Thread(ticket, "視窗3");
//同時賣票
t1.start();
t2.start();
t3.start();
}
}
結果中有一部分這樣現象
發現程式出現了兩個問題:
- 相同的票數,比如5這張票被賣了兩回。
- 不存在的票,比如0票與-1票,是不存在的。
這種問題,幾個視窗(線程)票數不同步了,這種問題稱為線程不安全。
線程安全問題都是由全局變量及靜态變量引起的。若每個線程中對全局變量、靜态變量隻有讀操作,而無寫 操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步, 否則的話就可能影響線程安全。
線程同步
當我們使用多個線程通路同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程并發通路一個資源的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制 (synchronized)來解決。
根據案例簡述:
視窗1線程進入操作的時候,視窗2和視窗3線程隻能在外等着,視窗1操作結束,視窗1和視窗2和視窗3
才有機會進入代碼 去執行。也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待
修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,保證了資料的同步性,解決了線程不安全
的現象。
為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。
那麼怎麼去使用呢?有三種方式完成同步操作:
- 同步代碼塊。
- 同步方法。
- 鎖機制。
同步代碼塊
- 同步代碼塊:
關鍵字可以用于方法中的某個區塊中,表示隻對這個區塊的資源實行互斥通路。 格式:synchronized
synchronized(同步鎖){
需要同步操作的代碼
}
同步鎖:
對象的同步鎖隻是一個概念,可以想象為在對象上标記了一個鎖.
- 鎖對象 可以是任意類型。
- 多個線程對象 要使用同一把鎖。
注意:
在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程隻能在外等着 (BLOCKED)。
使用同步代碼塊解決代碼:
package com.example.demo;
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
public void run() {
//每個視窗賣票的操作
// 視窗 永遠開啟
while (true) {
synchronized (lock){
if (ticket > 0) {
//有票 可以賣
// 出票操作
// 使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
}
}
當使用了同步代碼塊後,上述的線程的安全問題,解決了。
同步方法
- **同步方法:**使用
synchronized
修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程隻能在方法外 等着。
格式:
public synchronized void method(){
可能會産生線程安全問題的代碼
}
同步鎖是誰?
對于非static方法,同步鎖就是this。
對于static方法,我們使用目前方法所在類的位元組碼對象(類名.class)。
使用同步方法代碼如下:
package com.example.demo;
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
public void run() {
//每個視窗賣票的操作
// 視窗 永遠開啟
while (true) {
sellTicket();
}
}
private synchronized void sellTicket() {
if (ticket > 0) {
//有票 可以賣
// 出票操作
// 使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
Lock鎖
java.util.concurrent.locks.Lock
機制提供了比
synchronized
代碼塊和
synchronized
方法更廣泛的鎖定操作, 同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更展現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
-
:加同步鎖。public void lock()
-
:釋放同步鎖public void unlock()
package com.example.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
public void run() {
//每個視窗賣票的操作
// 視窗 永遠開啟
while (true) {
lock.lock();
sellTicket();
lock.unlock();
}
}
private void sellTicket() {
if (ticket > 0) {
//有票 可以賣
// 出票操作
// 使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket--);
}
}
}
線程狀态
線程狀态概述
當線程被建立并啟動以後,它既不是一啟動就進入了執行狀态,也不是一直處于執行狀态。線上程的生命周期中, 有幾種狀态呢?在API中
java.lang.Thread.State
這個枚舉中給出了六種線程狀态:
/**
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* A thread that has exited is in this state.
* </li>
* </ul>
*
* <p>
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*
* @since 1.5
* @see #getState
*/
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
線程狀态 | 導緻狀态發生條件 |
---|---|
NEW(建立) | 線程剛被建立,但是并未啟動。還沒調用start方法。 |
Runnable(可運作) | 線程可以在java虛拟機中運作的狀态,可能正在運作自己代碼,也可能沒有,這取決于操 作系統處理器。 |
Blocked(鎖阻塞) | 當一個線程試圖擷取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 态;當該線程持有鎖時,該線程将變成Runnable狀态。 |
Waiting(無限等待) | 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀态。進入這個 狀态後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 |
Timed Waiting(計時等待) | 同waiting狀态,有幾個方法有逾時參數,調用他們将進入Timed Waiting狀态。這一狀态 将一直保持到逾時期滿或者接收到喚醒通知。帶有逾時參數的常用方法有Thread.sleep 、 Object.wait。 |
Teminated(被終止) | 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。 |
我們不需要去研究這幾種狀态的實作原理,我們隻需知道在做線程操作中存在這樣的狀态。那我們怎麼去了解這幾 個狀态呢,建立與被終止還是很容易了解的,我們就研究一下線程從Runnable(可運作)狀态與非運作狀态之間的轉換問題。
Timed Waiting(計時等待)
Timed Waiting在API中的描述為:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處于這一狀态。單獨 的去了解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀态了,在哪裡呢?
在我們寫賣票的案例中,為了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就 強制目前正在執行的線程休眠(暫停執行),以“減慢線程”。
其實當我們調用了sleep方法之後,目前執行的線程就進入到“休眠狀态”,其實就是所謂的Timed Waiting(計時等 待),那麼我們通過一個案例加深對該狀态的一個了解。
實作一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字元串
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
public class DemoApplicationTests extends Thread{
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("‐‐‐‐‐‐‐" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new DemoApplicationTests().start();
}
}
通過案例可以發現,sleep方法的使用還是很簡單的。我們需要記住下面幾點:
- 進入 TIMED_WAITING 狀态的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協 作關系。
- 為了讓其他線程有機會執行,可以将Thread.sleep()的調用**放線程run()**之内。這樣才能保證該線程執行過程中會睡眠。
-
sleep與鎖無關,線程睡眠到期自動蘇醒,并傳回到Runnable(可運作)狀态。
注意
sleep()中指定的時間是線程不會運作的最短時間。是以,sleep()方法不能保證該線程睡眠到期後就 開始立刻執行。
Timed Waiting 線程狀态圖:
BLOCKED(鎖阻塞)
Blocked狀态在API中的介紹為:一個正在阻塞等待一個螢幕鎖(鎖對象)的線程處于這一狀态。
我們已經學完同步機制,那麼這個狀态是非常好了解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲 取到鎖,線程A進入到Runnable狀态,那麼線程B就進入到Blocked鎖阻塞狀态。
這是由Runnable狀态進入Blocked狀态。除此Waiting以及Time Waiting狀态也會在某種情況下進入阻塞狀态,而 這部分内容作為擴充知識點帶領大家了解一下
Blocked 線程狀态圖
Waiting(無限等待)
Wating狀态在API中介紹為:一個正在無限期等待另一個線程執行一個特别的(喚醒)動作的線程處于這一狀态。
那麼我們之前遇到過這種狀态嗎?答案是并沒有,但并不妨礙我們進行一個簡單深入的了解。我們通過一段代碼來 學習一下
package com.example.demo;
public class WaitingTest {
public static Object obj = new Object();
public static void main(String[] args) {
// 示範waiting
new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (obj){
try {
System.out.println( Thread.currentThread().getName() +"=== 擷取到鎖對象,調用wait方法,進入waiting狀态,釋放鎖對象");
obj.wait(); //無限等待
//obj.wait(5000); //計時等待, 5秒 時間到,自動醒來
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "=== 從waiting狀 态醒來,擷取到鎖對象,繼續執行了");
}
}
}
},"等待線程").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) { //每隔3秒 喚醒一次
try {
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒鐘");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 擷取到鎖對 象,調用notify方法,釋放鎖對象");
obj.notify();
}
}
}
},"喚醒線程").start();
}
}
通過上述案例我們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的 Object.notify()方法 或 Object.notifyAll()方法。
其實waiting狀态并不是一個線程的操作,它展現的是多個線程間的通信,可以了解為多個線程之間的協作關系, 多個線程會争取鎖,同時互相之間又存在協作關系。就好比在公司裡你和你的同僚們,你們可能存在晉升時的競 争,但更多時候你們更多是一起合作以完成某些任務。
當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運作)狀态中調用了wait()方法那麼A線程就進入 了Waiting(無限等待)狀态,同時失去了同步鎖。假如這個時候B線程擷取到了同步鎖,在運作狀态中調用了 notify()方法,那麼就會将無限等待的A線程喚醒。注意是喚醒,如果擷取到鎖對象,那麼A線程喚醒後就進入 Runnable(可運作)狀态;如果沒有擷取鎖對象,那麼就進入到Blocked(鎖阻塞狀态)。
注意
我們在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀态聯系還是很緊密的, 比如Waiting(無限等待) 狀态中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。 這種帶參的方法,其實是一種倒計時操作,相當于我們生活中的小鬧鐘,我們設定好時間,到時通知,可是 如果提前得到(喚醒)通知,那麼設定好時間在通知也就顯得多此一舉了,那麼這種設計方案其實是一舉兩 得。如果沒有得到(喚醒)通知,那麼線程就處于Timed Waiting狀态,直到倒計時完畢自動醒來;如果在倒 計時期間得到(喚醒)通知,那麼線程從Timed Waiting狀态立刻喚醒。