1. 程式人生 > 其它 >Java多執行緒:執行緒同步

Java多執行緒:執行緒同步

執行緒同步

多個執行緒操作同一個資源

併發

併發:同一個物件被多個執行緒同時操作

執行緒同步是一種等待機制,選多個需要同時訪問的此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一執行緒再使用

佇列和鎖

同一程序的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也存在訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排他鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖。但存在以下問題:

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

執行緒不安全

執行緒的記憶體互不影響,是從同一個地方copy的

不安全的搶票

會發生搶到同一張票,搶到負數張票的問題

//不安全的買票
public class UnSafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket, "A").start();
        new Thread(buyTicket, "B").start();
        new Thread(buyTicket, "C").start();
    }
}

class BuyTicket implements Runnable{
    //票
    private int ticketNum = 10;
    boolean flag = true;


    @Override
    public void run() {
        while (flag){
            try {
                Thread.sleep(1000);
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //買票
    public void buy(){
        //判斷是否有票
        if (ticketNum <= 0){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "買了" + ticketNum--);
    }
}

不安全的取錢

//兩個人去銀行取錢,一個賬戶
public class UnSafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "A");

        Drawing me = new Drawing(account, 20, 0, "我");
        Drawing me2 = new Drawing(account, 100, 0, "還是我");

        me.start();
        me2.start();
    }
}

//賬戶
class Account{
    int money; //餘額
    String name; //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//銀行:模擬取款
//不設計多執行緒操作,繼承Thread
class Drawing extends Thread{
    Account account;//賬戶
    int drawingMoney; //取款數量
    int nowMoney; //現金

    public Drawing(Account account, int drawingMoney, int nowMoney, String name){
        super(name);//super()只能寫在構造方法的第一行
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.nowMoney = nowMoney;
    }

    //取錢
    @Override
    public void run() {
        //判斷餘額是否足夠
        if (account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName() + ":餘額不足");
            return;
        }

        //sleep可以放大問題的放生性
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新餘額
        account.money = account.money - drawingMoney;
        //現金
        nowMoney = nowMoney + drawingMoney;

        System.out.println(account.name + "的餘額為:" + account.money);
        //此處因為繼承Thread類,Thread.currentThread().getName() = this.getName()
        System.out.println(this.getName() + "的現金為:" + nowMoney);
    }
}

結果:

A的餘額為:-20
A的餘額為:-20
我的現金為:20
還是我的現金為:100

不安全的集合

//執行緒不安全的集合
public class UnSafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 9999; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        //防止部分run方法還沒跑完,沒來得及新增資料
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

同步方法

synchronized

  • 使用synchronized關鍵字控制物件的訪問,每個物件對應一把鎖,每個synchronized方法都必須呼叫改方法的物件的鎖才能執行,否則會阻塞。方法一旦執行,就獨佔鎖,直到該方法返回才釋放鎖,後續執行緒才能繼續執行。

  • synchronized方法和塊

    • 同步方法:同步監視器this
    //同步方法
    public synchronized void method(int args){}
    
    • 同步塊:obj稱為同步監視器
    synchronized(Obj){}
    
  • 若將大的方法申明為synchronized將會影響效率

    • 方法裡需要修改的內容才需要鎖,鎖的太多會浪費資源

同步方法

將不安全搶票中的buy()方法申明為synchronized

申明時synchronized預設鎖的是this:this是類的例項物件

public synchronized void buy(){
    //判斷是否有票
    if (ticketNum <= 0){
        flag = false;
        return;
    }
    System.out.println(Thread.currentThread().getName() + "買了" + ticketNum--);
}

同步塊

通過同步塊鎖上變化的量account

鎖的物件該是修改的物件

@Override
public void run() {
    synchronized (account){
        //判斷餘額是否足夠
        if (account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName() + ":餘額不足");
            return;
        }

        //sleep可以放大問題的放生性
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新餘額
        account.money = account.money - drawingMoney;
        //現金
        nowMoney = nowMoney + drawingMoney;

        System.out.println(account.name + "的餘額為:" + account.money);
        //此處因為繼承Thread類,Thread.currentThread().getName() = this.getName()
        System.out.println(this.getName() + "的現金為:" + nowMoney);
    }
}

JUC安全型別的集合

public class TestJUC {
    public static void main(String[] args) {
        //執行緒安全的list
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

死鎖

  • 多個執行緒各自佔有一些共享資源,並互相等待其他執行緒佔有的資源才能執行,在等待中都停止執行的情況
  • 某一個同步塊同時擁有兩個以上物件的鎖時,可能會發生死鎖的問題
  • 產生死鎖的必要條件:破壞其中一個或多個避免死鎖
    1. 互斥條件:一個資源只能被一個程序使用
    2. 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放
    3. 不剝奪條件:程序已獲得的資源,在未使用完前不能強行剝奪
    4. 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係
public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup = new Makeup(0,"0");
        Makeup makeup2 = new Makeup(1, "1");

        makeup.start();
        makeup2.start();
    }
}

//口紅
class Lipstick{}

//鏡子
class Mirror{}

//化妝
class Makeup extends Thread{

    //需要的資源只有一份,用static保證只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//選擇
    String name;//使用者

    Makeup(int choice, String name){
        this.choice = choice;
        this.name = name;
    }

    //化妝
    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝,互相持有對方的鎖,需要拿到對方的資源,
    private void makeup() throws InterruptedException {
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(this.name + "獲得口紅的鎖");
                Thread.sleep(1000);
            }
            //一秒後想拿鏡子,將鎖從鎖中拿出,解除死鎖
            synchronized (mirror){
                System.out.println(this.name + "獲得鏡子的鎖");
            }
        }else{
            synchronized (mirror){
                System.out.println(this.name + "獲得口紅的鎖");
                Thread.sleep(2000);
            }
            //2秒後拿口紅
            synchronized (lipstick){
                System.out.println(this.name + "獲得鏡子的鎖");
            }
        }
    }
}

Lock(鎖)

  • JDK5開始,同步鎖可以使用Lock物件充當
  • java.util.concurrent.locks.Lock介面控制多個執行緒對共享資源進行訪問。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源前需先獲得Lock物件
  • ReentrantLock(可重入鎖)類實現了Lock,與synchronized相似
class TestLock2 implements Runnable{
    int ticketNum = 10;

    //定義lock鎖
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //先加鎖後延時會造成只有一個人拿到票
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock.lock();//枷鎖,放在try外面防止無鎖釋放
            //try...finally避免同步程式碼存在異常
            try {
                if (ticketNum > 0){
                    System.out.println(Thread.currentThread().getName() + "拿到:" + ticketNum--);
                }else{
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

synchronized與Lock的對比

  • Lock是顯示鎖,需要手動開啟和關閉;synchronized是隱士鎖,處理作用域自動釋放
  • Lock只有程式碼塊鎖,synchronized由程式碼塊鎖和方法鎖
  • Lock鎖花費更少時間排程執行緒,效能更好,具有更好的擴充套件性(更多子類)
  • 優先使用順序:
    • Lock
    • 同步程式碼塊(已進入方法體,分配了相應自由
    • 同步方法(方法體外)