線程安全同步鎖之對象鎖

線程的相關理解就不多說了,直接進入主題:通過同步鎖實現線程安全

在多線程,爲了避免多個線程同時操作某個共享變量導致數據錯亂,採用了同步鎖機制,保證了操作的原子性,數據的一致性及線程安全。可以結合數據庫的事務操作理解。

同步鎖又可以分爲對象鎖,實例鎖,類鎖。

對象鎖,鎖住某個對象;實例鎖,鎖住某個實例,類鎖,鎖住某個類。

哈哈哈,佛了,有點抽象,不知道怎麼解釋,很尷尬。不急,可以根據下面的應用場景及代碼來理解。

假定一個應用場景,就用經典的火車票售票系統爲例:

假設有10張票,編號爲1~10,有四個售票窗口(1~3)在賣,即3個窗口共享這10張票,總共賣出的票不能超出10張,也不能有相同號的票被賣出。

未加鎖,線程不安全,代碼如下:

/**
 * 對象鎖
 */
public class SyncObject {

    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(),"窗口1");
        Thread t2 = new Thread(new MyRunnable(),"窗口2");
        Thread t3 = new Thread(new MyRunnable(),"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
    private static class  MyRunnable implements  Runnable{
        static int a = 10;  //類變量,多個實例共享,數據存在線程安全問題
        @Override
        public void run() {
            sale0();
        }
        //無鎖,線程不安全
        public static void sale0(){
            while (a>0){    //有票
                try{
                    Thread.sleep(500);  //模擬網絡延遲,更能看出問題,多個線程在此處等待開始競爭
                    System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
                }catch (Exception e){
                }
            }
        }
    }
}

運行結果:

窗口3賣出10號票
窗口2賣出9號票
窗口1賣出8號票
窗口3賣出7號票
窗口2賣出7號票
窗口1賣出6號票
窗口3賣出5號票
窗口2賣出5號票
窗口1賣出4號票
窗口3賣出3號票
窗口2賣出2號票
窗口1賣出1號票
窗口3賣出-1號票
窗口2賣出0號票

可見7號票,5號票賣了兩次,只有10張票,3個窗口總共賣了14張票,數據錯亂,線程不安全。

原因分析:

當a=1時,t1,t2,t3 都跑通了while(a>0),經過0.5s的sleep後,t1率先執行了

System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");

此時,a=0,然後t2接着執行,a=-1,最後t3執行,即出現了超賣現象;

同理,當a=7時,線程t3率先執行了

System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");

輸出賣了7號票,還沒來得及將a--賦值給a,這時,線程t2來了,也執行了:

System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");

即,窗口2和3同時賣出了7號票。

這兩種情況都是不允許發生的,於是對程序進行了改進,加入同步鎖,保證線程安全:

/**
 * 對象鎖
 */
public class SyncObject {

    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(),"窗口1");
        Thread t2 = new Thread(new MyRunnable(),"窗口2");
        Thread t3 = new Thread(new MyRunnable(),"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }


    private static class  MyRunnable implements  Runnable{

        static int a = 10;  //共享變量,數據存在線程安全問題
        @Override
        public void run() {
           sale1();
        }
        //線程安全,但不合理
        //當某線程進入sale方法,獲取到鎖後,運行synchronized中代碼塊,while()循環,直到a爲0,才運行完才釋放鎖
        //這期間其他售票窗口無法進入,無法賣票,當a=0時,其他窗口才獲取到該鎖,才能執行synchronized代碼塊,
        // 而此時已沒票
        public static void sale1(){
            synchronized (lock){   //同步代碼塊,某一時刻只允許一個線程進來,運行完釋放鎖
                while (a>0){
                    try{
                        Thread.sleep(500); //模擬買票網絡延遲
                        System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
                    }catch (Exception e){
                    }
                }
                System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
            }
        }
    }
}

運行結果如下:

窗口1賣出10號票
窗口1賣出9號票
窗口1賣出8號票
窗口1賣出7號票
窗口1賣出6號票
窗口1賣出5號票
窗口1賣出4號票
窗口1賣出3號票
窗口1賣出2號票
窗口1賣出1號票
窗口1票已賣完。。。
窗口3票已賣完。。。
窗口2票已賣完。。。

多次運行後發現,雖然沒有出現同號票,或多出的票,但是10張票都是隻由一個窗口(假設1)賣完,其他窗口無票可賣。

導致的情況是,若窗口1票賣不完,而其他窗口想買買不到,造成了資源不能合理充分利用。

雖然加了sync同步鎖,某個時刻只能有一個線程執行sync中的代碼塊,保證了變量a操作的原子性,數據的一致性。

但是由於while()循環,只要a>0有票,持有鎖的線程就會一直執行,不釋放鎖,則其他線程只能等待,無法進行售票。

直到該線程執行完sync中代碼塊,才釋放鎖,其他線程才能搶到鎖,但a已經爲0,已無票可售。雖然保證了線程安全,

但是不能滿足現實業務需求,我們需要的是在有票的情況下,各個窗口都有機會搶到票賣,既不能出現重複票,也不能

超賣。改進如下:

package com.knowsight.test.Thread.synclock;

/**
 * 對象鎖
 */
public class SyncObject {

    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(),"窗口1");
        Thread t2 = new Thread(new MyRunnable(),"窗口2");
        Thread t3 = new Thread(new MyRunnable(),"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }


    private static class  MyRunnable implements  Runnable{

        static int a = 10;  //共享變量,數據存在線程安全問題
        @Override
        public void run() {
           //sale1();
            sale2();
            //sale0();
        }

        //無鎖,線程不安全
        public static void sale0(){
            while (a>0){
                try{
                    Thread.sleep(500);  //模擬網絡延遲,更能看出問題,多個線程在此處等待開始競爭
                    System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
                }catch (Exception e){
                }
            }
        }

        //線程安全,但不合理
        //當某線程進入sale方法,獲取到鎖後,運行synchronized中代碼塊,while()循環,直到a爲0,才運行完才釋放鎖
        //這期間其他售票窗口無法進入,無法賣票,當a=0時,其他窗口才獲取到該鎖,才能執行synchronized代碼塊,
        // 而此時已沒票
        public static void sale1(){
            synchronized (lock){   //同步代碼塊,某一時刻只允許一個線程進來,運行完釋放鎖
                while (a>0){
                    try{
                        Thread.sleep(500); //模擬買票網絡延遲
                        System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
                    }catch (Exception e){
                    }
                }
                System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
            }
        }

        //線程安全,較爲合理
        public static void sale2(){
            while (a>0){   //有票,無鎖,多個線程(售票窗口)可以同時進入
                synchronized (lock){   //同步代碼塊,只能一個線程進入,運行完釋放鎖
                    if(a>0){       //因爲當a=1時,可能會有多個線程跑通了a>0,在sync外等待,但只能給其中一個,需要再次判斷是否有票
                        try{
                            Thread.sleep(500); //模擬買票網絡延遲
                            System.out.println(Thread.currentThread().getName()+"賣出"+(a--)+"號票");
                        }catch (Exception e){
                        }
                    }else {
                        System.out.println(Thread.currentThread().getName()+"票已賣完。。。");
                    }
                }
            }
        }
    }
}

代碼較簡單,也有註釋,就不多嘮叨。

關於多線程,個人感覺比較抽象,每個人都有各自的理解。以上是個人以火車票售票系統作爲應用場景,及代碼的具體實現,來理解多線程,及線程安全,同步鎖等,如有不妥之處,望多多指教!

 

 

 

 

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