(Java多線程系列二)線程間同步


Java多線程間同步

1、什么是線程安全

通過一個案例了解線程安全

案例:需求現在有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。

先來看一個線程不安全的例子

class SellTicketRunnable implements Runnable {

    public int count = 100;

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int index = 100 - count + 1;
            System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
            count--;
        }
    }
}

public class JavaSyncDemo {

    public static void main(String[] args) {
        SellTicketRunnable runnable = new SellTicketRunnable();
        Thread sellThread1 = new Thread(runnable);
        Thread sellThread2 = new Thread(runnable);
        sellThread1.start();
        sellThread2.start();
    }
}

線程不安全

可以看到兩個線程同時賣票的時候,會出現漏賣,多賣同一張票,還會出現超賣的問題,這就是線程不安全的問題。

當多個線程同時共享,同一個全局變量或靜態變量,做寫的操作時,可能會發生數據沖突問題,也就是線程安全問題。但是做讀操作是不會發生數據沖突問題。

2、線程安全問題的解決辦法

(1)使用同步代碼塊
class SellTicketRunnable implements Runnable {

    public int count = 100;

    private Object lock = new Object();

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                if (count > 0) {
                    int index = 100 - count + 1;
                    System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
                    count--;
                }
            }
        }
    }
}

public class JavaSyncDemo {

    public static void main(String[] args) {
        SellTicketRunnable runnable = new SellTicketRunnable();
        Thread sellThread1 = new Thread(runnable);
        Thread sellThread2 = new Thread(runnable);
        sellThread1.start();
        sellThread2.start();
    }
}

從上面的案例可以看出,使用synchronized同步代碼塊包裹住寫操作,每個線程在調用同步代碼塊中邏輯的時候,都需要先獲取同步鎖,所以避免了多線程寫操作數據的沖突問題。

(2)使用同步函數
class SellTicketRunnable01 implements Runnable {

    public int count = 100;

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.sale();
        }
    }

    synchronized void sale() {
        if (count > 0) {
            int index = 100 - count + 1;
            System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
            count--;
        }
    }
}

public class JavaSyncDemo01 {

    public static void main(String[] args) {
        SellTicketRunnable01 runnable = new SellTicketRunnable01();
        Thread sellThread1 = new Thread(runnable);
        Thread sellThread2 = new Thread(runnable);
        sellThread1.start();
        sellThread2.start();
    }
}

synchronized包裹的函數,其實就是給該函數塊添加了一把this鎖。

注意:synchronized 修飾靜態方法使用鎖是當前類的字節碼文件(即類名.class),同理,如果在靜態方法中添加個同步代碼塊,可以獲取類名.class為代碼塊加鎖

class SellTicketRunnable02 implements Runnable {

    public static int count = 100;

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            SellTicketRunnable02.sale();
        }
    }

    static void sale() {
        synchronized (SellTicketRunnable02.class) {
            if (count > 0) {
                int index = 100 - count + 1;
                System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
                count--;
            }
        }
    }
}

public class JavaSyncDemo02 {

    public static void main(String[] args) {
        SellTicketRunnable02 runnable = new SellTicketRunnable02();
        Thread sellThread1 = new Thread(runnable);
        Thread sellThread2 = new Thread(runnable);
        sellThread1.start();
        sellThread2.start();
    }
}
(3)使用lock鎖
class SellTicketRunnable03 implements Runnable {

    public int count = 100;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            if (count > 0) {
                int index = 100 - count + 1;
                System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
                count--;
            }
            lock.unlock();
        }
    }
}

public class JavaSyncDemo03 {

    public static void main(String[] args) {
        SellTicketRunnable03 runnable = new SellTicketRunnable03();
        Thread sellThread1 = new Thread(runnable);
        Thread sellThread2 = new Thread(runnable);
        sellThread1.start();
        sellThread2.start();
    }
}

lock和synchronized的區別

①lock在使用時需要手動的獲取鎖和釋放鎖;
②lock可以嘗試非阻塞的獲取鎖,如果這一時刻鎖沒有被其他線程獲取到,則成功獲取並持有鎖;
③lock鎖可以響應中斷,當獲取到鎖的線程被中斷時,中斷異常會被拋出,同時鎖被釋放;
④lock在指定截至時間之前獲取鎖,如果解釋時間到了依舊無法獲取鎖,就返回。

// lock鎖的安全使用方法
class lockDemo {
    Lock lock = new ReentrantLock();
    void demoFun() {
        lock.lock();
        try {
            // 可能出現線程安全的操作
        } finally {
            lock.unlock();
        }
    }
}
(4)使用Java原子類

java.util.concurrent.atomic.AtomicBoolean;
java.util.concurrent.atomic.AtomicInteger;
java.util.concurrent.atomic.AtomicLong;
java.util.concurrent.atomic.AtomicReference;

class SellTicketRunnable04 implements Runnable {

   public AtomicInteger count = new AtomicInteger(100);

   @Override
   public void run() {
       while (true) {
           try {
               Thread.sleep(50);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (count.get() > 0) {
               int index = 100 - count.getAndDecrement() + 1;
               System.out.println(Thread.currentThread().getName() + "賣出第" + index + "張票");
           }
       }
   }
}

public class JavaSyncDemo04 {

   public static void main(String[] args) {
       SellTicketRunnable04 runnable = new SellTicketRunnable04();
       Thread sellThread1 = new Thread(runnable);
       Thread sellThread2 = new Thread(runnable);
       sellThread1.start();
       sellThread2.start();
   }
}

3、死鎖

先看一個死鎖的示例

public class DeadLockDemo01 {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread() { //線程1
            public void run() {
                while (true) {
                    synchronized (lock1) {
                        System.out.println(this.getName() + ":獲取lock1鎖");
                        synchronized (lock2) {
                            System.out.println(this.getName() + ":獲取lock2鎖");
                        }
                    }
                }
            }
        }.start();

        new Thread() { //線程2
            public void run() {
                while (true) {
                    synchronized (lock2) {
                        System.out.println(this.getName() + ":獲取lock2鎖");
                        synchronized (lock1) {
                            System.out.println(this.getName() + "::獲取lock1鎖");
                        }
                    }
                }
            }
        }.start();
    }
}

運行上面的代碼,可以觀察到線程卡死,就是出現了死鎖

線程1先拿到lock1鎖,再拿到lock2鎖,執行完成后才能釋放所有鎖;
線程2先拿到lock2鎖,再拿到lock1鎖,執行完成后才能釋放所有鎖。
如果在線程1獲取到lock1鎖的時候,線程2獲取到lock2還沒釋放,線程1無法獲取lock2鎖,也就無法釋放lock2鎖,這時系統就會出現死鎖。

線程死鎖的避免辦法:不要在同步中嵌套同步

源碼地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM