Java基礎 第四節 第二課
技術標籤:# Java 基礎第四節我要偷偷學 Java, 然後驚呆所有人
執行緒安全
概述
如果有過個執行緒在同時執行, 而這些執行緒可能會勇士執行這段程式碼. 程式每次執行結果和單執行緒執行的結果是一樣的, 而其他的變數的值也和預期的是一樣的, 就是執行緒安全的.
案例
我們通過一個案例, 演示執行緒的安全問題:
電影院要賣票, 我們模擬電影院的賣過程. 假設要播放的電影是 “郭德綱和他嫂子的愛情故事”. 本次電影的座位共有 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) { e.printStackTrace(); } // 獲取當前執行緒物件的名字 String name = Thread.currentThread().getName(); System.out.println(name + "正在賣: " + ticket--); } } } }
測試類
public class Test51 { 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正在賣: 99 視窗3正在賣: 98 視窗2正在賣: 100 視窗2正在賣: 97 視窗1正在賣: 96 視窗3正在賣: 95 視窗2正在賣: 94 視窗1正在賣: 93 視窗3正在賣: 92 視窗3正在賣: 91 視窗1正在賣: 90 視窗2正在賣: 89 視窗2正在賣: 88 視窗3正在賣: 86 視窗1正在賣: 87 視窗1正在賣: 85 視窗2正在賣: 84 視窗3正在賣: 83 視窗1正在賣: 82 視窗2正在賣: 81 視窗3正在賣: 80 視窗3正在賣: 79 視窗2正在賣: 78 視窗1正在賣: 77 視窗3正在賣: 76 視窗2正在賣: 75 視窗1正在賣: 74 視窗1正在賣: 73 視窗2正在賣: 72 視窗3正在賣: 71 視窗1正在賣: 70 視窗2正在賣: 69 視窗3正在賣: 68 視窗3正在賣: 67 視窗1正在賣: 65 視窗2正在賣: 66 視窗1正在賣: 64 視窗2正在賣: 62 視窗3正在賣: 63 視窗3正在賣: 61 視窗1正在賣: 60 視窗2正在賣: 59 視窗2正在賣: 58 視窗1正在賣: 57 視窗3正在賣: 56 視窗3正在賣: 55 視窗2正在賣: 53 視窗1正在賣: 54 視窗1正在賣: 52 視窗3正在賣: 51 視窗2正在賣: 50 視窗1正在賣: 49 視窗2正在賣: 48 視窗3正在賣: 47 視窗2正在賣: 46 視窗3正在賣: 45 視窗1正在賣: 44 視窗1正在賣: 43 視窗3正在賣: 42 視窗2正在賣: 41 視窗2正在賣: 40 視窗3正在賣: 39 視窗1正在賣: 38 視窗1正在賣: 37 視窗3正在賣: 36 視窗2正在賣: 35 視窗1正在賣: 34 視窗3正在賣: 33 視窗2正在賣: 32 視窗2正在賣: 31 視窗3正在賣: 30 視窗1正在賣: 29 視窗3正在賣: 28 視窗1正在賣: 27 視窗2正在賣: 26 視窗2正在賣: 25 視窗3正在賣: 24 視窗1正在賣: 23 視窗3正在賣: 22 視窗2正在賣: 21 視窗1正在賣: 22 視窗2正在賣: 20 視窗1正在賣: 19 視窗3正在賣: 18 視窗3正在賣: 17 視窗2正在賣: 16 視窗1正在賣: 15 視窗3正在賣: 14 視窗2正在賣: 13 視窗1正在賣: 12 視窗1正在賣: 11 視窗2正在賣: 10 視窗3正在賣: 9 視窗3正在賣: 8 視窗2正在賣: 7 視窗1正在賣: 6 視窗2正在賣: 5 視窗1正在賣: 5 視窗3正在賣: 4 視窗2正在賣: 3 視窗1正在賣: 2 視窗3正在賣: 1 視窗2正在賣: 0 視窗1正在賣: -1
發現程式出現了兩個問題:
- 相同的票數, 比如 5 這張票被賣了兩回
- 不存在的票, 比如 0 票與 -1, 是不存在的
這種問題, 幾個視窗 (執行緒)票數不同了, 這種問題成為執行緒不安全.
執行緒安全問題都是由全域性變數及靜態變數引起的. 若每個執行緒中對全域性變數, 靜態變數只有讀操作, 而無寫操作, 一般來說, 這個全域性變數是執行緒安全的. 若有多個執行緒同時執行操作, 一般都需要考慮執行緒同步, 否則的話就可能影響執行緒安全.
執行緒同步
當我們使用多個執行緒訪問同一資源的時候, 且多個執行緒中對資源有寫的操作, 就容易出現執行緒安全問題.
要解決上述多執行緒併發訪問一個資源的安全性問題. 也就是解決重複票與不存在票問題. Java 中提供了 (synchronized) 來解決.
根據案例描述:
視窗 1 執行緒進入操作的時候, 視窗 2 和視窗 3
執行緒只能在外等著. 視窗 1 操作結束, 視窗 1 和視窗 3有機會去執行. 也就是說在某個執行緒修改共享資源的時候, 其他執行緒不能去修改該資源, 等待修改完畢同步之後,才能去搶奪 CPU 資源, 完成對應的操作, 保證了資料的同步性, 解決了執行緒不安全的現象.
為了保證每個執行緒都能正常秩序原子操作 Java 引入了執行緒同步機制.
那麼怎麼去使用呢? 有三種方式完成同步操作:
- 同步程式碼塊
- 同步方法
- 鎖機制
同步程式碼塊
同步程式碼塊: synchronized 關鍵字可以用於方法中的某個區塊中. 表示只對這個區塊的資源實行互斥訪問.
格式
synchronized(同步鎖){
需要同步操作的程式碼
}
同步鎖
物件的同步鎖只是一個概念, 可以想象為在物件上標記了一個鎖:
- 鎖物件, 可也是任意型別
- 多個執行緒物件, 要使用同一把鎖
注: 在任何時候, 最多允許一個執行緒擁有同步鎖. 誰拿到所就進入程式碼塊. 其他的執行緒只能在外面等著. (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(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)
程式碼
public class Ticket implements Runnable {
private int ticket = 100;
/**
* 執行賣票操作
*/
@Override
public void run() {
// 每個視窗賣票的操作
// 視窗永遠開啟
while (true){
}
}
/**
* 鎖物件是誰呼叫這個方法就是誰
* 隱含鎖物件就是this
*/
public 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()
: 釋放同步鎖
使用如下:
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();
/**
* 執行賣票操作
*/
@Override
public void run() {
// 每個視窗賣票的操作
// 視窗永遠開啟
while (true){
lock.lock();
if(ticket > 0){ // 有票可以賣
// 出票操作
// 使用sleep模擬一下出票時間
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 獲取當前執行緒物件的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在賣: " + ticket--);
}
}
}
}