線程安全
如果有多個線程在同時運作,而這些線程可能會同時運作這段代碼。程式每次運作結果和單線程運作的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
我們通過一個案例,示範線程的安全問題:
電影院要賣票,我們模拟電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共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();
}
}
}