Web全堆疊~32.深入理解synchronized
上一期
基本用法和原理
銀行案例
synchronized可以用于修飾類的實體方法、靜態方法和代碼塊,當兩個或兩個以上執行緒訪問同一資源時,需要某種方式來確保資源在某一時刻只被一個執行緒使用
假設場景
當多個用戶同時操作一個銀行賬戶,每次取款400元,取款前先檢查余額是否足夠,如果不夠,放棄取款
很顯然這個問題我們可以使用多執行緒的思維去做,但結果往往不是理想中的那樣
package com.alvin.test;
import java.util.ArrayList;
import java.util.List;
public class Test extends Thread{
public static void main(String[] args) {
//創建兩個執行緒
Runnable runnable = new AccountRunnable();
Thread zhangsanThread = new Thread(runnable);
Thread lisi =new Thread(runnable,"李四");
zhangsanThread.setName("張三");
//啟動兩個執行緒
zhangsanThread.start();
lisi.start();
}
}
class Account {
//設定余額600元
private int balance = 600;
//取款
public void withDraw(int money){
this.balance = this.balance - money;
}
//查看余額
public int getBalance(){
return balance;
}
}
class AccountRunnable implements Runnable {
private Account account = new Account();
@Override
public void run() {
//判斷余額是否足夠,夠,取之;不夠,不取之;
if(account.getBalance()>=400){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取之
account.withDraw(400);
//輸出資訊
System.out.println(Thread.currentThread().getName()+
"取款成功,現在的余額是"+account.getBalance());
}else{
System.out.println("余額不足,"+Thread.currentThread().getName()
+"取款失敗,現在的余額是" +account.getBalance());
}
}
}

怎么會這樣呢?因為使用Thread.sleep()的目的在于模擬執行緒切換,在一個執行緒判斷完余額后,不是立刻取款,而是讓出CPU,這樣另外一個執行緒獲取CPU,并且進行余額的判斷,執行緒安全問題就這么產生了,如果保證安全,必須判斷余額和取款的陳述句必須被一個執行緒執行完才能讓另外一個執行緒執行,
修改代碼
public void run() {
synchronized(account) {
//判斷余額是否足夠,夠,取之;不夠,不取之;
if (account.getBalance() >= 400) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取之
account.withDraw(400);
//輸出資訊
System.out.println(Thread.currentThread().getName() +
"取款成功,現在的余額是" + account.getBalance());
} else {
System.out.println("余額不足," + Thread.currentThread().getName()
+ "取款失敗,現在的余額是" + account.getBalance());
}
}
}

synchronized實體方法實際保護的是同一個物件的方法呼叫,確保同時只能有一個執行緒執行,再具體來說,synchronized實體方法保護的是當前實體物件,即this,this物件有一個鎖和一個等待佇列,鎖只能被一個執行緒持有,其他試圖獲得同樣鎖的執行緒需要等待,
synchronized必須是參考資料型別,不能是基本資料型別,在同步代碼塊中可以改變同步監視器物件的值,不能改變其參考,并且盡量不要使用String和包裝類Integer做同步監視器,如果使用了,只要保證代碼塊中不對其進行任何操作也沒有關系 ,一般使用共享資源做同步監視器即可,也可以創建一個專門的同步監視器,沒有任何業務含義
同步代碼塊的執行程序
第一個執行緒來到同步代碼塊,發現同步監視器open狀態,需要close,然后執行其中的代碼
第一個執行緒執行程序中,發生了執行緒切換(阻塞 就緒),第一個執行緒失去了cpu,但是沒有開鎖open
第二個執行緒獲取了cpu,來到了同步代碼塊,發現同步監視器close狀態,無法執行其中的代碼,第二個執行緒也進入阻塞狀態
第一個執行緒再次獲取CPU,接著執行后續的代碼;同步代碼塊執行完畢,釋放鎖open
第二個執行緒也再次獲取cpu,來到了同步代碼塊,發現同步監視器open狀態,重復第一個執行緒的處理程序(加鎖)
執行緒同步雖然安全但效率低下 可能出現死鎖
進一步理解synchronized
可重入性
synchronized有一個重要的特征,它是可重入的,也就是說,對同一個執行執行緒,它在獲得了鎖之后,在呼叫其他需要同樣鎖的代碼時,可以直接呼叫,比如,在一個syn-chronized實體方法內,可以直接呼叫其他synchronized實體方法,可重入是一個非常自然的屬性,應該是很容易理解的,之所以強調,是因為并不是所有鎖都是可重入的,后續章節我們會看到不可重入的鎖,
可重入是通過記錄鎖的持有執行緒和持有數量來實作的,當呼叫被synchronized保護的代碼時,檢查物件是否已被鎖,如果是,再檢查是否被當前執行緒鎖定,如果是,增加持有數量,如果不是被當前執行緒鎖定,才加入等待佇列,當釋放鎖時,減少持有數量,當數量變為0時才釋放整個鎖,
記憶體可見性
在釋放鎖時,所有寫入都會寫回記憶體,而獲得鎖后,都會從記憶體中讀最新資料,不過,如果只是為了保證記憶體可見性,使用synchronized的成本有點高,有一個更輕量級的方式,那就是給變數加修飾符volatile
private volatile boolean on;
public boolean getOn() {
return on;
}
public void setOn(boolean on) {
this.on = on;
}
加了volatile之后,Java會在操作對應變數時插入特殊的指令,保證讀寫到記憶體最新值,而非快取的值,
死鎖
使用synchronized或者其他鎖,要注意死鎖,所謂死鎖就是類似這種現象,比如,有a、b兩個執行緒,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A,a和b陷入了互相等待,最后誰都執行不下去,
public class Test extends Thread{
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {
Thread aThread = new Thread() {
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockB) {
System.out.println("lockB");
}
}
}
};
aThread.start();
}
private static void startThreadB() {
Thread bThread = new Thread() {
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockA) {
System.out.println("lockA");
}
}
}
};
bThread.start();
}
public static void main(String[]args) {
startThreadA();
startThreadB();
}
}
出現死鎖的時候Java并不會主動處理,不過,借助一些工具,我們可以發現運行中的死鎖,比如,Java自帶的jstack命令會報告發現的死鎖,
同步容器及其注意事項
類Collections中有一些方法,可以回傳執行緒安全的同步容器
public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它們是給所有容器方法都加上synchronized來實作安全的,比如synchronized-Collection,里面有部分代碼是這樣的


這里執行緒安全針對的是容器物件,指的是當多個執行緒并發訪問同一個容器物件時,不需要額外的同步操作,也不會出現錯誤的結果,加了synchronized,所有方法呼叫變成了原子操作,當然,在客戶端呼叫的時候也有以下情況需要注意,
復合操作
class EnbancedMap<K,V>{
Map<K,V> map;
public EnbancedMap(Map<K,V>map){
this.map = Collections.synchronizedMap(map);
}
public V putIfAbsent(K key, V value){
V old = map.get(key);
if(old != null){
return old;
}
return map.put(key,value);
}
public V put(K key,V value){
return map.put(key, value);
}
}
EnhancedMap是一個裝飾類,接受一個Map物件,呼叫synchronizedMap轉換為了同步容器物件map,增加了一個方法putIfAbsent,該方法只有在原Map中沒有對應鍵的時候才添加
map的每個方法都是安全的,但這個復合方法putIfAbsent并不一定安全,這是一個檢查然后再更新的復合操作,在多執行緒的情況下,可能有多個執行緒都執行完了檢查這一步,都發現Map中沒有對應的鍵,然后就會都呼叫put,這就破壞了putIf-Absent方法期望保持的語意,
偽同步
給剛剛那個方法加上synchronized?
public synchronized V putIfAbsent(K key, V value){
V old = map.get(key);
if(old != null){
return old;
}
return map.put(key,value);
}
很明顯這樣同步錯物件了,putIfAbsent同步使用的是EnhancedMap物件,而其他方法(如代碼中的put方法)使用的是Collections.synchronizedMap回傳的物件map,兩者是不同的物件,要解決這個問題,所有方法必須使用相同的鎖,可以使用EnhancedMap的物件鎖,也可以使用map,使用EnhancedMap物件作為鎖,則Enhanced-Map中的所有方法都需要加上synchronized,使用map作為鎖
public V putIfAbsent(K key, V value){
synchronized(map){
V old = map.get(key);
if(old != null){
return old;
}
}
return map.put(key,value);
}
執行緒的協作機制
多執行緒之間除了競爭訪問同一個資源以外,也經常需要相互協作,
生產者/消費者協作模式:這是一種常見的協作模式,生產者執行緒和消費者執行緒通過共享佇列進行協作,生產者將資料或任務放到佇列上,而消費者從佇列上取資料或任務,如果佇列長度有限,在佇列滿的時候,生產者需要等待,而在佇列為空的時候,消費者需要等待,
應用場景:生產者和消費者問題
假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費
如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產并等待,直到倉庫中的產品被消費者取走為止
如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費并等待,直到倉庫中再次放入產品為止
分析
這是一個執行緒同步問題,生產者和消費者共享同一個資源,并且生產者和消費者之間相互依賴,互為條件
對于生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之后,又需要馬上通知消費者消費
對于消費者,在消費之后,要通知生產者已經消費結束,需要繼續生產新產品以供消費
在生產者消費者問題中,僅有synchronized是不夠的
synchronized可阻止并發更新同一個共享資源,實作了同步
synchronized不能用來實作不同執行緒之間的訊息傳遞(通信)
| 方法名 | 作用 |
| final void wait() | 表示執行緒一直等待,直到其它執行緒通知 |
| void wait(long timeout) | 執行緒等待指定毫秒引數的時間 |
| final void wait(long timeout,int nanos) | 執行緒等待指定毫秒、微妙的時間 |
| final void notify() | 喚醒一個處于等待狀態的執行緒 |
| final void notifyAll() | 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先運行 |
消費者,生產者執行緒代碼實作
package com.alvin.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args){
//創建共享資源物件
Shop shop = new Shop();
//創建生產者
Boss boss = new Boss(shop);
//創建消費者
Customer customer = new Customer(shop);
//啟動執行緒
new Thread(boss).start();
new Thread(customer).start();
}
}
//生產者類
class Boss implements Runnable{
private Shop shop;
public Boss(Shop shop){
this.shop = shop;
}
@Override
public void run() {
for(int i = 1; i < 100; i++){
if(i % 2 == 0){
shop.Set("OPPO","reno 3 Pro");
}else{
shop.Set("HUWEI","Mate 30 Pro");
}
}
}
}
//消費者類
class Customer implements Runnable{
private Shop shop;
public Customer(Shop shop){
this.shop = shop;
}
@Override
public void run() {
for(int i = 1; i < 100; i++){
shop.Get();
}
}
}
//商品類
class Shop{
private String brand; //商品品牌
private String name; //商品名稱
private boolean flag = false; //默認沒有商品
// 撰寫一個賦值的方法 同步監視器為Shop類的物件
public synchronized void Set(String brand, String name){
//先判斷下有沒有商品
if(flag){
try {
//生產者執行緒等待
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//當生產者執行緒被喚醒后從wait()之后的代碼開始執行
//生產商品
this.brand = brand;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
System.out.println("老板: 今天廠里生產了 " + brand + " " + name + " 歡迎下次光臨!----------");
//通知消費者
super.notify();
flag = true;
}
//撰寫一個取值的方法
public synchronized void Get(){
//先判斷下有沒有商品
if(!flag){
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("----------顧客: " + brand + " " + name + " 已經買到手,謝謝老板!");
//通知生產者
super.notify();
flag = false;
}
}
執行緒同步的細節
進行執行緒通信的多個執行緒,要使用同一個同步監視器(product),還必須要呼叫該同步監視器的wait()、notify()、notifyAll();
wait()等待
在【其他執行緒】呼叫【此物件】的 notify() 方法或 notifyAll() 方法前,導致當前執行緒等待,換句話說,此方法的行為就好像它僅執行 wait(0) 呼叫一樣,當前執行緒必須擁有此物件監視器
wait(time) 等待
在其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量前,導致當前執行緒等待, 當前執行緒必須擁有此物件監視器,
notify() 通知 喚醒
喚醒在【此物件監視器】上等待的【單個】執行緒,如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒,【選擇是任意性的】,并在對實作做出決定時發生
notifyAll() 通知所有 喚醒所有
喚醒在【此物件監視器】上等待的【所有】執行緒,被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒【進行競爭】;
完整的執行緒生命周期
阻塞狀態有三種
普通的阻塞 sleep,join,Scanner input.next()
同步阻塞(鎖池佇列) 沒有獲取同步監視器的執行緒的佇列
等待阻塞(阻塞佇列) 被呼叫了wait()后釋放鎖,然后進行該佇列
sleep()和wait()的區別
區別1:sleep() 執行緒會讓出CPU進入阻塞狀態,但不會釋放物件鎖 wait() 執行緒會讓出CPU進入阻塞狀態, 【也會放棄物件鎖】,進入等待【此物件】的等待鎖定池
區別2: 進入的阻塞狀態也是不同的佇列
區別3:wait只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/258145.html
標籤:其他
上一篇:自定義負載均衡輪詢演算法——基于Ribbon的輪詢演算法
下一篇:35歲的程式員:第26章,回家
