線程安全
假如Java程序中有多個線程在同時運行,而這些線程可能會同時運行一部分的代碼。如果說該Java程序每次運行的結果和單線程的運行結果是一樣的,並且其他的變量值也都是和預期的結果是一樣的,那么就可以說線程是安全的。
解析什么是線程安全:賣電影票案例
假如有一個電影院上映《葫蘆娃大戰奧特曼》,售票100張(1-100號),分三種情況賣票:

- 情況1:該電影院開設一個售票窗口,一個窗口賣一百張票,沒有問題。就如同單線程程序不會出現安全問題一樣。
- 情況2:該電影院開設n(n>1)個售票窗口,每個售票窗口售出指定號碼的票,也不會出現問題。就如同多線程程序,沒有訪問共享數據,不會產生問題。
- 情況3:該電影院開設n(n>1)個售票窗口,每個售票窗口出售的票都是沒有規定的(如:所有的窗口都可以出售1號票),這就會出現問題了,假如三個窗口同時在賣同一張票,或有的票已經售出,還有窗口還在出售。就如同多線程程序,訪問了共享數據,會產生線程安全問題。
賣100張電影票Java程序實現:出現情況3類似情況
public class MovieTicket01 implements Runnable {
/**
* 電影票數量
*/
private static int ticketNumber = 100;
/**
* 在實現類中重寫Runnable接口的run方法,並設置此線程要執行的任務
*/
@Override
public void run() {
// 設置此線程要執行的任務
while (ticketNumber > 0) {
// 提高程序安全的概率,讓程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 電影票出售
System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket01.ticketNumber + "號電影票");
ticketNumber --;
}
}
}
// 測試
public class Demo01MovieTicket {
public static void main(String[] args) {
// 創建一個 Runnable接口的實現類對象。
MovieTicket01 movieTicket = new MovieTicket01();
// 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象(三個窗口)。
Thread window0 = new Thread(movieTicket);
Thread window1 = new Thread(movieTicket);
Thread window2 = new Thread(movieTicket);
// 設置一下窗口名字,方便輸出確認
window0.setName("window0");
window1.setName("window1");
window2.setName("window2");
// 調用Threads類中的start方法,開啟新的線程執行run方法
window0.start();
window1.start();
window2.start();
}
}
控制台部分輸出:
售票窗口(window0)正在出售:100號電影票
售票窗口(window2)正在出售:99號電影票
售票窗口(window1)正在出售:100號電影票
售票窗口(window0)正在出售:97號電影票
售票窗口(window2)正在出售:97號電影票
售票窗口(window1)正在出售:97號電影票
售票窗口(window1)正在出售:94號電影票
售票窗口(window2)正在出售:94號電影票
.
.
.
.
.
.
售票窗口(window0)正在出售:7號電影票
售票窗口(window2)正在出售:4號電影票
售票窗口(window0)正在出售:4號電影票
售票窗口(window1)正在出售:2號電影票
售票窗口(window1)正在出售:1號電影票
售票窗口(window2)正在出售:0號電影票
售票窗口(window0)正在出售:-1號電影票
可以看到,三個窗口(線程)同時出售不指定號數的票(訪問共享數據),出現了賣票重復,和出售了不存在的票號數(0、-1)
Java程序中為什么會出現這種情況
-
在CPU線程的調度分類中,Java使用的是搶占式調度。
-
我們開啟了三個線程,3個線程一起在搶奪CPU的執行權,誰能搶到誰就可以被執行。

-
從輸出結果可以知道,剛開始搶奪CPU執行權的時候,線程0(window0窗口)先搶到,再到線程1(window1窗口)搶到,最后線程2(window2窗口)才搶到。

-
那么為什么100號票已經在0號窗口出售了,在1號窗口還會出售呢?其實很簡單,線程0先搶到CPU執行權,於是有了執行權后,他就開始囂張了,作為第一個它通過while判斷,很自豪的拿着ticketNumber = 100進入while里面開始執行

-
可線程0是萬萬沒有想到,這時候的線程1,在拿到執行權后,在線程0剛剛實現print語句還沒開始ticketNumber --的時候,線程1以ticketNumber = 100跑進了while里面。
-
線程2很遺憾,在線程0執行了ticketNumber --了才急匆匆的進入while里面,不過它也不甘落后,於是拼命追趕。終於,后來居上,在線程1還沒開始print的時候,他就開始print了。於是便出現了控制台的前三條輸出的情況。
售票窗口(window0)正在出售:100號電影票 售票窗口(window2)正在出售:99號電影票 售票窗口(window1)正在出售:100號電影票window0、window1、window2分別對應線程0、線程1、線程2
-
以此類推,直到最后程序執行完畢。
解決情況3的共享數據問題
通過線程的同步,來解決共享數據問題。有三種方式,分別是同步代碼塊、同步方法、鎖機制。
同步代碼塊
public class MovieTicket02 implements Runnable {
/**
* 電影票數量
*/
private static int ticketNumber = 100;
/**
* 創建鎖對象
*/
Object object = new Object();
/**
* 在實現類中重寫Runnable接口的run方法,並設置此線程要執行的任務
*/
@Override
public void run() {
// 設置此線程要執行的任務
synchronized (object) {
// 把訪問了共享數據的代碼放到同步代碼中
while (ticketNumber > 0) {
// 提高程序安全的概率,讓程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 電影票出售
System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket02.ticketNumber + "號電影票");
ticketNumber --;
}
}
}
}
// 進行測試
public class Demo02MovieTicket {
public static void main(String[] args) {
// 創建一個 Runnable接口的實現類對象。
MovieTicket02 movieTicket = new MovieTicket02();
// 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象(三個窗口)。
Thread window0 = new Thread(movieTicket);
Thread window1 = new Thread(movieTicket);
Thread window2 = new Thread(movieTicket);
// 設置一下窗口名字,方便輸出確認
window0.setName("window0");
window1.setName("window1");
window2.setName("window2");
// 調用Threads類中的start方法,開啟新的線程執行run方法
window0.start();
window1.start();
window2.start();
}
}
控制台輸出:
售票窗口(window0)正在出售:100號電影票
售票窗口(window0)正在出售:99號電影票
售票窗口(window0)正在出售:98號電影票
售票窗口(window0)正在出售:97號電影票
售票窗口(window0)正在出售:96號電影票
.
.
.
.
.
.
售票窗口(window0)正在出售:5號電影票
售票窗口(window0)正在出售:4號電影票
售票窗口(window0)正在出售:3號電影票
售票窗口(window0)正在出售:2號電影票
售票窗口(window0)正在出售:1號電影票
- 這時候,控制台不再出售不存在的電影號數以及重復的電影號數了。
- 通過代碼塊中的鎖對象,可以使用任意的對象。但是必須保證多個線程使用的鎖對象是同一。鎖對象作用:把同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行。
- 總結:同步中的線程,沒有執行完畢,不會釋放鎖,同步外的線程,沒有鎖,進不去同步。
同步方法
public class MovieTicket03 implements Runnable {
/**
* 電影票數量
*/
private static int ticketNumber = 100;
/**
* 創建鎖對象
*/
Object object = new Object();
/**
* 在實現類中重寫Runnable接口的run方法,並設置此線程要執行的任務
*/
@Override
public void run() {
// 設置此線程要執行的任務
ticket();
}
public synchronized void ticket() {
// 把訪問了共享數據的代碼放到同步代碼中
while (ticketNumber > 0) {
// 提高程序安全的概率,讓程序睡眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 電影票出售
System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket03.ticketNumber + "號電影票");
ticketNumber --;
}
}
}
測試與同步代碼塊一樣。
鎖機制(Lock鎖)
在Java中,Lock鎖機制又稱為同步鎖,加鎖public void lock(),釋放同步鎖public void unlock()。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MovieTicket05 implements Runnable {
/**
* 電影票數量
*/
private static int ticketNumber = 100;
Lock reentrantLock = new ReentrantLock();
/**
* 在實現類中重寫Runnable接口的run方法,並設置此線程要執行的任務
*/
@Override
public void run() {
// 設置此線程要執行的任務
while (ticketNumber > 0) {
reentrantLock.lock();
// 提高程序安全的概率,讓程序睡眠10毫秒
try {
Thread.sleep(10);
// 電影票出售
System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket05.ticketNumber + "號電影票");
ticketNumber --;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
}
// 測試
public class Demo05MovieTicket {
public static void main(String[] args) {
// 創建一個 Runnable接口的實現類對象。
MovieTicket05 movieTicket = new MovieTicket05();
// 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象(三個窗口)。
Thread window0 = new Thread(movieTicket);
Thread window1 = new Thread(movieTicket);
Thread window2 = new Thread(movieTicket);
// 設置一下窗口名字,方便輸出確認
window0.setName("window0");
window1.setName("window1");
window2.setName("window2");
// 調用Threads類中的start方法,開啟新的線程執行run方法
window0.start();
window1.start();
window2.start();
}
}
控制台部分輸出:
售票窗口(window0)正在出售:100號電影票
售票窗口(window0)正在出售:99號電影票
售票窗口(window0)正在出售:98號電影票
售票窗口(window0)正在出售:97號電影票
售票窗口(window0)正在出售:96號電影票
.
.
.
.
.
.
售票窗口(window1)正在出售:7號電影票
售票窗口(window1)正在出售:6號電影票
售票窗口(window1)正在出售:5號電影票
售票窗口(window1)正在出售:4號電影票
售票窗口(window1)正在出售:3號電影票
售票窗口(window2)正在出售:2號電影票
售票窗口(window1)正在出售:1號電影票
與前兩種方式不同,前兩種方式,只有線程0能夠進入同步機制執行代碼,Lock鎖機制,三個線程都可以進行執行,通過Lock鎖機制來解決共享數據問題。
