Java併發與多線程(2):線程、同步
一、線程
1、多線程原理
我們已經寫過一版多線程的代碼,那麼我們今天來體現一下多線程程序的執行原理。
代碼如下:
/**
* 自定義一個線程類extends Thread
* @author Mango
*/
public class MainThread extends Thread{
/**
* 利用繼承的特點將 線程名字 設置
* @param name
*/
MainThread(String name) {
super(name);
}
/**
* 重寫的run方法,線程要執行的語句
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//getName()來自Thread
System.out.println(getName() + ":正在執行!"+ i);
}
}
}
/**
* 測試類
* @author Mango
*/
public class Dome1 {
public static void main(String[] args) {
System.out.println("這裏是Main線程");
MainThread mainThread = new MainThread("新線程");
//開啓一個新線程
mainThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main線程" + i);
}
}
}
程序啓動運行main時候,JVM啓動一個進程,主線程main在main()調用時候被創建。隨着調用mainThread的對象的start方法,另外一個新的線程也啓動了,這樣,整個應用就在多線程下運行。
多線程的執行流程,那麼爲什麼可以完成併發執行呢?我們再來講一講原理。
多線程執行時,到底在內存中是如何運行的呢?以上個程序爲例,進行圖解說明:
多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。
當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。
2、Thread類
API中該類中定義了有關線程的一些方法,具體如下:
(1)構造方法:
public Thread() :分配一個新的線程對象。
public Thread(String name) :分配一個指定名字的新的線程對象。
public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象並指定名字。
(2)常用方法:
public String getName() :獲取當前線程名稱。
public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法。
public void run() :此線程要執行的任務在此處定義代碼。
public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
public static Thread currentThread() :返回對當前正在執行的線程對象的引用。
創建線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable接口方式。
方式一已經明白,接下來講解方式二實現的方式。
3、Runnable創建線程
採用java.lang.Runnable (通過實現runnable接口,來作爲參數傳遞)也是非常常見的一種,我們只需要重寫run方法即可。
/**
* 創建一個Runnable接口的實現類
* @author Mango
*/
public class DomeRunnable implements Runnable {
/**
* 重寫接口的run方法,設置線程任務
*/
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i );
}
}
}
/**
* 測試類
* @author Mango
*/
public class Dome1 {
public static void main(String[] args) {
//創建一個Runnable接口實現類對象
DomeRunnable runnable = new DomeRunnable();
//採用傳遞runnable的構造方法創建
Thread thread = new Thread(runnable);
//開啓新的線程run方法
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i );
}
}
}
通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。
在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
tips:Runnable對象僅僅作爲Thread對象的target,Runnable實現類裏包含的run()方法僅作爲線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。
4、Thread和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
實現Runnable接口比繼承Thread類所具有的優勢:
- 適合多個相同的程序代碼的線程去共享同一個資源。
- 可以避免java中的單繼承的侷限性。
- 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享(作參數被多個thread調用),代碼和線程獨立。
- 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
擴充:在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用
java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進
程。
二、線程安全
1、線程安全
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣
的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,實現多個窗口同時賣電影票。演示線程的安全問題:
(1)需要票,Runnable接口子類來模擬
/**
* 模擬票
* @author Mango
*/
public class Ticket implements Runnable {
//線程共享的數據
private int ticket = 100;
/**
* 賣票操作
*/
@Override
public void run() {
while(true) {
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
}
}
}
(2)需要窗口,採用線程對象來模擬;
/**
* 測試類
* @author Mango
*/
public class Dome1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//同時開啓線程,執行run方法
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票,是不存在的。
這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。
線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
2、線程同步
當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程併發訪問一個資源(ticket)的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制(synchronized) 來解決。
也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU
資源,完成對應的操作,保證了數據的同步性,解決了線程不安全的現象。爲了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。
有三種方式完成同步操作:
- 同步代碼塊。
- 同步方法。
- 鎖機制。
3、同步代碼塊
synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:
//同步代碼塊
synchronized (lock) {
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
}
同步鎖:
對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.
- 鎖對象 可以是任意類型。
2. 多個線程對象 要使用同一把鎖。
注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着(BLOCKED)。
使用同步代碼塊解決代碼:
/**
* 模擬票
* @author Mango
*/
public class Ticket implements Runnable {
/**
* 多線程共享數據
*/
private int ticket = 100;
//創建一個鎖對象
Object lock = new Object();
/**
* 賣票操作
*/
@Override
public void run() {
while(true) {
//同步代碼塊
synchronized (lock) {
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
}
}
}
}
4、同步方法
同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。
格式:
public synchronized void method(){
可能會產生線程安全問題的代碼
}
同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。
/**
* 模擬票
* @author Mango
*/
public class Ticket implements Runnable {
/**
* 多線程共享數據
*/
private int ticket = 100;
/**
* 賣票操作
*/
@Override
public void run() {
while(true) {
sellTicket();
}
}
/**
* 誰調用這個方法就是鎖對象
* 隱含鎖對象就是這個this
*/
public synchronized void sellTicket() {
//同步方法
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
}
}
5、Lock鎖(常用)
java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖 方法化了,如下:
public void lock() :加同步鎖。
public void unlock() :釋放同步鎖。
/**
* 模擬票
* @author Mango
*/
public class Ticket implements Runnable {
/**
* 多線程共享數據
*/
private int ticket = 100;
//創建一個lock鎖對象
Lock lock = new ReentrantLock();
/**
* 賣票操作
*/
@Override
public void run() {
while(true) {
lock.lock();
//同步方法
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
lock.unlock();
}
}
}
三、線程狀態
1、線程狀態概述
當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中這各個線程狀態發生的條件,下面將會對每種狀態進行詳細解析:
(1)Runnable(可運行)
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器(cpu)。
(2)Blocked(鎖阻塞)
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。
(3)Waiting(無限等待)
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
我們只需知道在做線程操作中存在這樣的狀態。那我們怎麼去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間的轉換問題。
2、Timed Waiting(計時等待)
Timed Waiting在API中的描述爲:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態。在我們寫賣票的案例中,爲了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。
sleep就是等待。
/**
* 賣票操作
*/
@Override
public void run() {
while(true) {
//開啓線程鎖
lock.lock();
if(ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣" + ticket--);
}
//關閉線程鎖
lock.unlock();
}
}
我們需要記住下面幾點:
- 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協
作關係。 - 爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程
中會睡眠 - sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態。
3、BLOCKED(鎖阻塞)
Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。
比如,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。
4、Waiting(無限等待)
一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。
當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,如果A獲取到鎖對象,那麼A線程喚醒後就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。
5、補充
線程狀態整體概括: