Java學習日誌(十一): 線程安全,線程同步

JavaEE學習日誌持續更新----> 必看!JavaEE學習路線(文章總彙)

線程安全

產生原理

若有三個電影院同時上映了戰狼3,每個電影院要賣100張不同號碼的電影票,三位電影院老闆有了三種不同的賣票思路

第一位老闆:設置了一個售賣窗口,這個窗口賣1-100號碼的票,則不會出現問題,此時則爲單線程程序,不會出現線程安全問題。
在這裏插入圖片描述
第二位老闆:設置了三個售賣窗口,第一個窗口賣1-33號的票,第二個窗口賣34-67號的票,第三個窗口賣68-100號的票,則不會出現問題,此時則爲多線程程序,但不訪問共享資源,不會出現線程安全問題。
在這裏插入圖片描述
第三位老闆:設置了三個售賣窗口,三個窗口同時賣1-100號的票,則出現了問題,此時則爲多線程程序,訪問了共享資源,會出現線程安全問題。

在這裏插入圖片描述

代碼模擬:

/*
    賣票案例
 */
public class RunnableImpl implements Runnable {
    //定義共享票源
    private int ticket = 100;
    //線程任務:賣票
    @Override
    public void run() {
        //賣票重複執行
        while(true){
            //增加一個判斷,票大於0
            if(ticket>0){
            	//添加sleep方法,增加線程安全問題出現的機率
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //賣票操作
                System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票!");
                ticket--;
            } else {
                break;
            }
        }
    }
}
/*
    開啓三個線程,同時進行賣票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl r = new RunnableImpl();
        //創建三個線程
        Thread t0 = new Thread(r,"售票員A");
        Thread t1 = new Thread(r,"售票員B");
        Thread t2 = new Thread(r,"售票員C");
        //開啓新的線程
        t0.start();
        t1.start();
        t2.start();
    }
}

結果:出現安全問題

...
售票員C正在賣第16張票!
售票員A正在賣第16張票!
售票員B正在賣第14張票!
售票員A正在賣第13張票!
售票員C正在賣第12張票!
售票員B正在賣第11張票!
售票員A正在賣第10張票!
售票員C正在賣第10張票!
售票員B正在賣第8張票!
售票員A正在賣第7張票!
售票員C正在賣第7張票!
售票員B正在賣第5張 票!
售票員A正在賣第4張票!
售票員C正在賣第4張票!
售票員B正在賣第2張票!
售票員C正在賣第1張票!
售票員A正在賣第0張票!
售票員B正在賣第-1張票!

線程安全問題產生的原因

售票員C正在賣第1張票!
售票員A正在賣第0張票!
售票員B正在賣第-1張票!

第一種安全問題:出現-1和0張票的原因(出現超出if語句範圍的數值)
售票員C線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
售票員A線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
售票員B線程搶到了cpu的執行權,進入到run中執行,執行到了if語句中,碰到sleep,睡眠,失去cpu的執行權。
④售票員C線程睡醒了,繼續執行程序進行賣票。

售票員C正在賣第1張票!
ticket--;ticket = 0;

`⑤售票員A線程睡醒了,繼續執行程序進行賣票。

售票員A正在賣第0張票
ticket--;ticket = -1;

`⑥售票員B線程睡醒了,繼續執行程序進行賣票。

售票員B正在賣第-1張票!
ticket--;ticket = -2;

第二種安全問題:出現重複賣票的原因

售票員A正在賣第10張票!
售票員C正在賣第10張票!
售票員B正在賣第8張票!

售票員A和售貨員C兩個線程都在打印正在賣第10張票,這時候ticket還沒有進行自減操作。之後ticket自減兩次,所以第9張票消失,售票員B只能賣第8張票。

線程同步

解決線程安全的第一種方式:同步代碼塊

格式:

 synchronized(鎖對象){
        出現安全問題的代碼(訪問了共享數據的代碼)
 }

注意事項

  1. 鎖對象可以是任意對象 new Person,new Student
  2. 鎖對象必須保證多個線程使用的是同一個鎖對象
  3. 鎖對象的作用:把括號中的代碼鎖住,只讓一個線程進去執行

示例:賣票案例

public class RunnableImpl implements Runnable {
    private int ticket = 100;
    //在成員位置創建一個鎖對象
    Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized (obj){
                //訪問了共享數據的代碼
                if(ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票!");
                    ticket--;
                }
            }
        }
    }
}
public class Demo01PayTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl r = new RunnableImpl();
        //創建三個線程
        Thread t0 = new Thread(r,"售票員A");
        Thread t1 = new Thread(r,"售票員B");
        Thread t2 = new Thread(r,"售票員C");
        //開啓新的線程
        t0.start();
        t1.start();
        t2.start();
    }
}

同步技術的原理:使用了一個鎖對象,這個鎖對象也叫同步鎖,還叫對象監視器

當線程執行到同步代碼塊的時候,會判斷同步代碼中,是否有鎖對象
如果有,那麼這個線程就會獲取鎖對象,進入到同步中執行,在執行過程中,無論是否失去了CPU的執行權,都不會釋放鎖對象,線程執行完同步中的代碼,纔會把鎖對象歸還給同步代碼。
如果沒有,那麼這個線程就會進入阻塞狀態,在同步外邊一直等待同步中的線程歸還鎖對象,直到同步中的線程把鎖對象歸還,才能獲取鎖對象進入到同步中執行。

總結

  1. 沒有鎖對象,進不去同步
  2. 同步中的對象沒有執行完,也不會歸還鎖

這樣就保證只有一個線程在同步中執行,代碼就安全了

問題:程序頻繁判斷鎖,獲取鎖,歸還鎖,效率就會降低

解決線程安全的第二種方式:同步方法

格式

 修飾符 synchronized 返回值類型 方法名(參數列表){
        出現安全問題的代碼
 }

使用步驟

  1. 創建一個方法,方法的修飾符添加上synchronized
  2. 把訪問了共享數據的代碼放入到方法中
  3. 調用同步方法

注意:同步方法的鎖對象就是this(本類對象 new RunnableImpl)

示例:賣票案例

public class RunnableImpl implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //3.調用同步方法
            payTicket();
        }
    }

    /*
        定義一個方法
        1.創建一個方法,方法的修飾符添加上synchronized

     */
    public synchronized void payTicket() {
        //2.把訪問了共享數據的代碼放入到方法中
        //訪問了共享數據的代碼
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
            ticket--;
        }
    }
}
public class Demo01PayTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl r = new RunnableImpl();
        //創建三個線程
        Thread t0 = new Thread(r,"售票員A");
        Thread t1 = new Thread(r,"售票員B");
        Thread t2 = new Thread(r,"售票員C");
        //開啓新的線程
        t0.start();
        t1.start();
        t2.start();
    }
}

擴展:靜態同步方法
靜態同步方法(優先於對象加載到內存中):鎖對象是的class文件對象(反射)RunnableImpl.class–>唯一

示例:賣票案例

public class RunnableImpl implements Runnable {
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //3.調用同步方法
            payTicket();
        }
    }

    /*
        定義一個方法
        1.創建一個方法,方法的修飾符添加上synchronized

     */
    //靜態同步方法(優先於對象加載到內存中):鎖對象是的class文件對象(反射)RunnableImpl.class-->唯一
    public static synchronized void payTicket() {
        //2.把訪問了共享數據的代碼放入到方法中
        //訪問了共享數據的代碼
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
            ticket--;
        }
    }
}
public class Demo01PayTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl r = new RunnableImpl();
        //創建三個線程
        Thread t0 = new Thread(r,"售票員A");
        Thread t1 = new Thread(r,"售票員B");
        Thread t2 = new Thread(r,"售票員C");
        //開啓新的線程
        t0.start();
        t1.start();
        t2.start();
    }
}

解決線程安全的第三種方式:使用Lock鎖

java.util.concurrent.locks.Lock接口
JDK1.5之後的新特性,Lock實現提供了比使用synchronized方法和語句可以獲得的更廣泛的鎖定操作。

Lock接口中的方法:

  • void lock() 獲得鎖。
  • void unlock() 釋放鎖。

實現類:java.util.concurrent.locks.ReentrantLock implements Lock

使用步驟:

  1. 在成員位置創建一個Lock接口的實現類對象ReentrantLock
  2. 在可能會出現安全問題的代碼前,調用Lock方法獲取鎖對象
  3. 在可能會出現安全問題的代碼後,調用Lock方法釋放鎖對象

示例:賣票案例

public class RunnableImpl implements Runnable {
    //1.在成員位置創建一個Lock接口的實現類對象ReentrantLock
    Lock l = new ReentrantLock();

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //2.在可能會出現安全問題的代碼前,調用Lock方法獲取鎖對象
            l.lock();
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票!");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //無論程序是否異常,都會把鎖對象釋放,節約內存,提高程序效率
                    //3.在可能會出現安全問題的代碼後,調用Lock方法釋放鎖對象
                    l.unlock();
                }
            }
        }
    }
}
public class Demo01PayTicket {
    public static void main(String[] args) {
        //創建Runnable接口的實現類對象
        RunnableImpl r = new RunnableImpl();
        //創建三個線程
        Thread t0 = new Thread(r,"售票員A");
        Thread t1 = new Thread(r,"售票員B");
        Thread t2 = new Thread(r,"售票員C");
        //開啓新的線程
        t0.start();
        t1.start();
        t2.start();
    }
}
發佈了25 篇原創文章 · 獲贊 33 · 訪問量 5975
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章