JAVA | 線程(三)線程同步(重要)

一、線程安全問題(銀行取錢)

問題描述:
當兩個人同時對一個賬戶進行操作取錢的時候,可能會出現線程安全問題

//定義一個用戶類
public class Account {
    // 銀行賬戶
    private String accountNo;

    //餘額
    private int balance;

    public Account(String accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

  // get()  set()  方法
}
// 取錢的類
public class DrawThread extends Thread {
    // 取錢的賬戶
    private Account account;

    // 取錢的金額
    private int monny;

    DrawThread(String name,Account account,int monny){
        super(name);
        this.account = account;
        this.monny = monny;
    }

    @Override
    public void run() {
        super.run();

        if (account.getBalance() >= monny){
            Log.e("testthread",getName()+"取錢成功"+monny);
			 // 強制線程調度切換,這樣每次兩個用戶都能取到錢了
             Thread.sleep(1);
                
            account.setBalance(account.getBalance()-monny);

        }else {
            Log.e("testthread","餘額不足~~");
        }
    }
}

//取錢
mBntFun5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               Account account = new Account("admin",1000);

               DrawThread thread1 = new DrawThread("用戶1",account,800);
               thread1.start();

               DrawThread thread2 = new DrawThread("用戶2",account,800);
               thread2.start();
            }
        });

取錢成功取錢失敗
上面代碼很有可能會執行成功,如上圖1,或者如圖2;

要每次都出現圖1的異常情況,只需要將run()中的 Thread.sleep(1); 打開即可

二、同步代碼塊

圖一是因爲run()方法的方法體不具有同步安全性,可以用同步監視器解決這個問題,通用方法就是同步代碼塊,語法格式如下:

// obj 就是同步監視器
synchronized(obj){
   ...
   // 此處就是同步代碼塊
}

說明

  • 上面代碼的說明:就是在執行同步代碼塊之前,必須要對同步監視器進行鎖定
  • 任何時刻只能有一個線程可以獲得同步監視器的鎖定,當同步代碼塊執行完之後,就會釋放同步監視器的鎖定
  • 同步監視器 一般都是 可能被併發訪問的共享資源
  • 一般邏輯如下:
    加鎖–>修改–>釋放鎖

上面的代碼進行優化,如下:

@Override
    public void run() {
        super.run();
        // 符合加鎖-->修改-->釋放鎖的邏輯
        synchronized(account){
            if (account.getBalance() >= monny){
                Log.e("testthread",getName()+"取錢成功"+monny);
                try {
                    // 強制線程調度切換,這樣每次兩個用戶都能取到錢了
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance()-monny);
                Log.e("testthread","餘額:"+account.getBalance());
            }else {
                Log.e("testthread","餘額不足~~");
            }
        }
    }

在這裏插入圖片描述

三、同步方法

  • 用synchronized關鍵字修飾的方法就是同步方法,無需顯示指定同步監視器,它的同步監視器就是this,也就是該對象本身

  • 線程安全的類具有如下特點:
    (1)該類的對象可以被多個線程同時訪問
    (2)每個線程調用該類的方法後返回的都是正確結果
    (3)每個線程調用該對象的方法後,該對象的狀態仍然保持合理狀態

  • 不要對所有的方法都進行同步,只對共享資源進行同步

  • 如果可變類有兩種運行環境:單線程和多線程,則要爲它提供兩種版本:線程安全版本和線程不安全版本;

    • 單線程中使用線程不安全版本保證性能
    • 多線程中使用線程安全版本
public class Account {
    // 銀行賬戶
    private String accountNo;

    //餘額
    private int balance;

    public Account(String accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    
    public synchronized void draw(int monny){
        if (balance >= monny){
            Log.e("testthread",Thread.currentThread().getName()+"取錢成功:"+monny);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setBalance(balance - monny);

        }else {
            Log.e("testthread",Thread.currentThread().getName()+"取錢失敗");
        }
        Log.e("testthread","餘額:"+getBalance());
    }
}

@Override
    public void run() {
        super.run();
        account.draw(800);
    }

說明

  1. synchronized 要寫在返回值的前面
  2. 因爲draw()用synchronized修飾了,同步方法的同步監視器總是 this,而this指的是調用這個方法的對象。在上面的代碼中,調用draw()的是draw,因此多個線程併發修改account的時候,要先對account對象進行加鎖。

四、釋放同步監視器的鎖定

  • 以下幾種情況會釋放同步監視器:
  1. 當同步代碼塊或同步方法執行完了,會釋放;
  2. 當同步代碼塊或同步方法中遇到了break、return 進行終止,會釋放
  3. 當同步代碼塊或同步方法中有未處理的Error或Exception,導致了異常退出,會釋放
  4. 當執行了同步監視器的ewait()方法,當前線程會暫停,並釋放
  • 以下幾種情況不會釋放:
  1. 在同步代碼塊或同步方法執行的時候,程序調用了Thread.sleep()或Thread.yield()來暫停當前線程,則不會釋放
  2. 在同步代碼塊或同步方法執行的時候,程序調用了suspend使線程掛起了,則不會釋放。應該儘量避免使用suspend和resume來控制線程

五、同步鎖(Lock)

  • Lock 是sychronized的升級版,有跟廣泛的鎖定操作
  • Lock是控制多個線程對共享資源進行訪問的工具,提供了對共享資源的獨佔訪問
  • java提供了兩個根接口:
    • Lock---->實現類:ReentranLock(可重入鎖)
    • ReadWriteLock—>實現類:ReentrantReadWiteLock

可重用性
一個線程可以對已經加鎖的ReentranLock再次加鎖,線程每次調用lock()加鎖後,都必須顯示調用unlock()釋放鎖

使用格式:

class A{
ReentrantLock lock = new ReentrantLock();
    
    void fun(){
        //加鎖
        lock.lock();
        
        try {
            //需要保證線程安全的代碼
            // .....
        }
        finally {
            // 釋放鎖
            lock.unlock();
        }
    }
}

以上取錢的代碼進行優化:

public class Account {
    // 銀行賬戶
    private String accountNo;

    //餘額
    private int balance;

    ReentrantLock lock = new ReentrantLock();

    public void drawmonny(int monny){
        // 加鎖
        lock.lock();

        try {
            if (balance >= monny){
                Log.e("testthread",Thread.currentThread().getName()+"取錢成功:"+monny);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                setBalance(balance - monny);

            }else {
                Log.e("testthread",Thread.currentThread().getName()+"取錢失敗");
            }

            Log.e("testthread","餘額:"+getBalance());
        }
        finally {
            // 釋放鎖
            lock.unlock();
        }
    }
}

六、死鎖(互相等待釋放同步監視器)

當兩個線程相互等待對方釋放同步監視器的時候就會發生死鎖,死鎖發生的時候既不會發生異常,也不會給任何提示,所以要儘量避免死鎖。當系統中有多個同步監視器的時候就很容易發生死鎖。

public class A{
        public synchronized void funA(B b){
            Log.e("testthread",Thread.currentThread().getName()+"進入A的fun方法");

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.e("testthread",Thread.currentThread().getName()+"想進B的last方法");
            b.lashB();
        }

        public synchronized void lastA(){
            Log.e("testthread",Thread.currentThread().getName()+"進入A的last方法");
        }
    }

    public class B{
        public synchronized void funB(A a){
            Log.e("testthread",Thread.currentThread().getName()+"進入B的fun方法");

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Log.e("testthread",Thread.currentThread().getName()+"想進A的last方法");
            a.lastA();
        }

        public synchronized void lashB(){
            Log.e("testthread",Thread.currentThread().getName()+"進入B的last方法");
        }
    }

    public class DeadLock implements Runnable{
        A a = new A();
        B b = new B();

        void init(){
            Thread.currentThread().setName("主線程");
            a.funA(b);
        }

        @Override
        public void run() {
            b.funB(a);
        }
    }
mBntFun5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DeadLock lock = new DeadLock();
                new Thread(lock,"子線程").start();
                lock.init();
            }
        });

死鎖代碼解釋:
因爲funA()和funB()都是同步方法,當a和b調用他們的時候,就要對a和b加鎖。而A、B中各自sleep(200)後,開始繼續執行,這是A中要調用B的lastB()方法,這時候就要對B加鎖,但是這時候B的鎖並沒有釋放,同理B中也是這樣的情況,所以雙方就一直在等待,造成了死鎖。

說明
suspend很容易造成死鎖,儘量不要使用它來暫停線程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章