JAVA多執行緒詳解(3)執行緒同步和鎖

pmc0_0發表於2020-12-04

佇列

處理多執行緒問題時,多個執行緒訪問一個物件並修改資料庫時,可能破壞事務的四大特性(原子性、一致性、隔離性、永續性),因此我們要採取佇列和鎖(缺一不可),就好像上圖廁所排隊,請問你怎麼才能安全和安心的上一個廁所?這時候首先得有序排隊(佇列)避免插隊衝突,第二 人進廁所得上鎖(加鎖)避免在你未完成的情況下別人進去干擾你


執行緒同步(保證執行緒安全)

當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用完成後釋放鎖即可,但會引起以下問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
  • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題
  • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖 會導致效能慢

同步方法

  • 我們通過Private 關鍵字來保證資料物件只能被方法訪問,所以我們只需要針對方法提供機制,也就是synchronized 關鍵字,它包括兩種用法:synchronized方法和synchronized塊
  • synchronized方法控制對 物件 的訪問,每一個物件對應一把鎖,每一個synchronized方法都必須獲得呼叫該方法物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,繼續執行

不安全案例程式碼

public class TestLock {

    public static void main(String[] args) {
        User user = new User(1000,"小明");
        Bank bank = new Bank(user,300);
        //四個人去銀行取錢
        new Thread(bank,"小明").start();
        new Thread(bank,"小明老婆").start();
        new Thread(bank,"小明媽媽").start();
        new Thread(bank,"小明爸爸").start();

    }
}


//個人賬戶
class User{
    //賬戶的錢和名字
    int totalMoney;
    String name;

    public User(int totalMoney, String name){
        this.totalMoney = totalMoney;
        this.name = name;
    }

}

//銀行取款
class Bank implements Runnable{
    User user;
    //要取的錢和個人擁有的現金
    int getMoney;
    int money;

    public Bank(User user,int getMoney){
        this.user = user;
        this.getMoney = getMoney;
    }

    @Override
    public void run() {
        if(getMoney> user.totalMoney){
            System.out.println("卡里餘額不足");
            return;
        }
        //設定延時是為了滿足四個人在知道卡里有錢的情況下同時取錢
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        money = getMoney;
        user.totalMoney = user.totalMoney-getMoney;
        System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡餘額為:"+user.totalMoney);
    }
}

執行結果:

小明老婆取了300
小明的卡餘額為:400
小明取了300
小明的卡餘額為:400
小明媽媽取了300
小明的卡餘額為:400
小明爸爸取了300
小明的卡餘額為:400

!結果很明顯是銀行虧大了

1.synchronized方法保證執行緒安全

  • 同步方法無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個物件本身,也可以或者是class(反射)
    @Override
    public synchronized void run() {
        if(getMoney> user.totalMoney){
            System.out.println("卡里餘額不足");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        money = getMoney;
        user.totalMoney = user.totalMoney-getMoney;
        System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡餘額為:"+user.totalMoney);
    }

2.synchronized塊保證執行緒安全

  • 同步塊 synchronized(Obj){…}
  • Obj可以稱為同步監視器
    1.Obj可以是任何物件,推薦使用共享資源作為同步監視器
  • 同步監視器的執行過程
    1.第一個執行緒訪問,鎖定同步監視器,執行其中程式碼
    2.第二執行緒訪問,發現同步監視器被鎖定,無法訪問
    3.第一個執行緒訪問結束,解鎖同步監視器
    4.第二個執行緒訪問,發現同步監視器沒有鎖,然後鎖定訪問
    @Override
    public void run() {
        synchronized (user){
            if(getMoney> user.totalMoney){
                System.out.println("卡里餘額不足");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money = getMoney;
            user.totalMoney = user.totalMoney-getMoney;
            System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡餘額為:"+user.totalMoney);
        }
    }

如果Bank繼承的是Thread類,採用synchronized方法(在修飾run方法加synchronized)是不能保證執行緒安全的,因為建立多執行緒時,synchronized方法鎖的是Bank,而操作物件增刪改的是User,synchronized方法鎖的是當前例項(this)。而對於上述實現Runnable介面的Bank類,採取synchronized方法鎖的雖然是Bank,但不會出現問題,因為代理物件操作的同一資源(進同一銀行得排隊),沒有代理物件的話是多個銀行取同一資源(賬戶的錢),鎖Bank是解決不了問題的,因為其他銀行操作不需要另一個銀行的鎖,只需要User的鎖


死鎖問題的導致

執行緒死鎖是指由於兩個或者多個執行緒互相持有對方所需要的資源,導致這些執行緒處於等待狀態,無法前往執行。當執行緒進入物件的synchronized程式碼塊時,便佔有了資源,直到它退出該程式碼塊或者呼叫wait方法,才釋放資源,在此期間,其他執行緒將不能進入該程式碼塊。當執行緒互相持有對方所需要的資源時,會互相等待對方釋放資源,如果執行緒都不主動釋放所佔有的資源,將產生死鎖。

案例程式碼

public class TestDeadLock {
    public static void main(String[] args) {
        PlayGame playGame = new PlayGame();
        new Thread(playGame,"a").start();
        new Thread(playGame,"b").start();
    }
}

class Lol{}

class Dnf{}

class PlayGame implements Runnable{
    //可以理解為資源只有一份(只有一個遊戲賬戶)
    static Lol lol = new Lol();
    static Dnf dnf = new Dnf();

    @Override
    public void run() {
        try {
            playGame();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    private void playGame() throws InterruptedException {
        if(Thread.currentThread().getName()=="a"){
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"獲得LOL的鎖");
                Thread.sleep(1000);
                synchronized (dnf){
                    System.out.println(Thread.currentThread().getName()+"同時獲得DNF的鎖");
                }
            }
        }else {
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"獲得DNF的鎖");
                Thread.sleep(2000);
                synchronized (lol){
                    System.out.println(Thread.currentThread().getName()+"同時獲得LOL的鎖");
                }
            }
        }
    }
}

a執行緒訪問鎖定 lol 同步監視器,b執行緒訪問鎖定 dnf 同步監視器,接下來a沒執行完synchronized程式碼塊(相當於lol沒下線還玩著)還想同時玩 dnf,但dnf 同步監視器已經被 b 執行緒鎖定了(導致無法登入),於是等待b釋放鎖。但b也沒執行完synchronized程式碼塊(相當於dnf沒下線還玩著)還想同時玩 lol,但lol同步監視器已經被 a 執行緒鎖定了(導致無法登入),於是雙方等待對方釋放鎖資源(但對方都很倔強,如果a沒等到b釋放dnf的鎖死都不會釋放lol的,b也和a有著一樣的想法),最終宇宙毀滅那時候雙方都只玩過其中一個遊戲,這就是死鎖問題的發生。

死鎖問題的解決

    private void playGame() throws InterruptedException {
        if(Thread.currentThread().getName()=="a"){
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"獲得LOL的鎖");
                Thread.sleep(1000);
            }
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"獲得DNF的鎖");
            }
        }else {
            synchronized (dnf){
                System.out.println(Thread.currentThread().getName()+"獲得DNF的鎖");
                Thread.sleep(2000);
            }
            synchronized (lol){
                System.out.println(Thread.currentThread().getName()+"獲得LOL的鎖");
            }
        }
    }

不要鎖中加鎖,a釋放 lol 同步資源器的條件是你已經玩完了,而不是要等 dnf 的同步資源器可以訪問才釋放lol的鎖,b的思想也一樣。


ReentrantLock(可重入鎖)

ReentrantLock類實現了Lock介面,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常見的是ReentrantLock,可以顯式加鎖、釋放鎖。

不安全案例程式碼

public class TestReentrantLock {

    public static void main(String[] args) {
        SaleTicket saleTicket = new SaleTicket();
        new Thread(saleTicket,"小明").start();
        new Thread(saleTicket,"小胖").start();
    }
}

class SaleTicket implements Runnable{
    private int ticket = 10;

    @Override
    public void run() {
        try {
            while (true) {
                if(ticket>0){
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"買了第"+ticket--+"票");
                }else{
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ReentrantLock(可重入鎖)解決方法

class SaleTicket implements Runnable {
    private int ticket = 10;

    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        try {
            while (true) {
                reentrantLock.lock();//加鎖
                if (ticket > 0) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "買了第" + ticket-- + "票");
                } else {
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();//解鎖
        }
    }
}

這時候就是小明買票沒完成,;另外一個是不能買票的,在實際的業務程式碼裡面,小明不可能一直買票直到票沒有,買了一張就釋放鎖了,有興趣的朋友可以自己寫程式碼測試

相關文章