天天看點

多線程(三) 線程安全、線程同步、案例:模拟電影院買票線程安全線程同步

線程安全

如果有多個線程在同時運作,而這些線程可能會同時運作這段代碼。程式每次運作結果和單線程運作的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,示範線程的安全問題:

電影院要賣票,我們模拟電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個 (本場電影隻能賣100張票)。 我們來模拟電影院的售票視窗,實作多個視窗同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個視窗一起賣這100張票) 需要視窗,采用線程對象來模拟;需要票,Runnable接口子類來模拟

模拟票:

public class Ticket implements Runnable {
private int ticket = 100;
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個視窗賣票的操作
//視窗 永遠開啟
while (true) {
if (ticket > 0) {//有票 可以賣
//出票操作
//使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣:" + ticket‐‐);
}
}
}
}
           

測試類:

public class Demo {
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();
}
}
           

結果中有一部分這樣現象:

多線程(三) 線程安全、線程同步、案例:模拟電影院買票線程安全線程同步

發現程式出現了兩個問題:

    1. 相同的票數,比如5這張票被賣了兩回。

    2. 不存在的票,比如0票與-1票,是不存在的。

這種問題,幾個視窗(線程)票數不同步了,這種問題稱為線程不安全。

線程安全問題都是由全局變量及靜态變量引起的。若每個線程中對全局變量、靜态變量隻有讀操作,而無寫
操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,
否則的話就可能影響線程安全。
           

線程同步

當我們使用多個線程通路同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。 要解決上述多線程并發通路一個資源的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制 (synchronized)來解決。

根據案例簡述:

視窗1線程進入操作的時候,視窗2和視窗3線程隻能在外等着,視窗1操作結束,視窗1和視窗2和視窗3才有機會進入代碼 去執行。也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,保證了資料的同步性,解決了線程不安全的現象。

為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。

那麼怎麼去使用呢?有三種方式完成同步操作:

1. 同步代碼塊。 2. 同步方法。 3. 鎖機制。

同步代碼塊

同步代碼塊: synchronized 關鍵字可以用于方法中的某個區塊中,表示隻對這個區塊的資源實行互斥通路。

格式:

synchronized(同步鎖){
    需要同步操作的代碼
}
           

同步鎖:

對象的同步鎖隻是一個概念,可以想象為在對象上标記了一個鎖.

    1. 鎖對象 可以是任意類型。

    2. 多個線程對象 要使用同一把鎖

注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程隻能在外等着
(BLOCKED)。
           

使用同步代碼塊解決代碼:

public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個視窗賣票的操作
//視窗 永遠開啟
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以賣
//出票操作
//使用sleep模拟一下出票時間
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在賣:"+ticket‐‐);
}
}
}
}
}
           

當使用了同步代碼塊後,上述的線程的安全問題,解決了。

同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程隻能在方法外 等着。

格式:

public synchronized void method(){
可能會産生線程安全問題的代碼
}
           
同步鎖是誰?
對于非static方法,同步鎖就是this。
對于static方法,我們使用目前方法所在類的位元組碼對象(類名.class)
           

使用同步方法代碼如下:

public class Ticket implements Runnable{
private int ticket = 100;
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個視窗賣票的操作
//視窗 永遠開啟
while(true){
sellTicket();
}
}
/*
* 鎖對象 是 誰調用這個方法 就是誰
* 隐含 鎖對象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以賣
//出票操作
//使用sleep模拟一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
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() :釋放同步鎖。

使用如下:

public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 執行賣票操作
*/
@Override
public void run() {
//每個視窗賣票的操作
//視窗 永遠開啟
while(true){
lock.lock();
if(ticket>0){//有票 可以賣
//出票操作
//使用sleep模拟一下出票時間
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//擷取目前線程對象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在賣:"+ticket‐‐);
}
lock.unlock();
}
}
}
           

繼續閱讀