1. 程式人生 > 實用技巧 >從synchronized入手聊聊java鎖機制(一)

從synchronized入手聊聊java鎖機制(一)

  寫這篇文章之前,我去百度了一下啥叫鎖,百度百科上寫道:置於可啟閉的器物上,以鑰匙或暗碼開啟。確實我們一般理解的鎖就是門鎖,密碼鎖,但是在電腦科學中,鎖又是啥,說實話,這個問題我也思考了很久,也沒法很好的用一兩句話就讓人聽得明白,也不想有人看到我的文章,然後將我的結論當作答案,我覺得最好的答案還是在探索的過程中得到的,接下來我們就好好探索一番。

  作為一名java程式設計師,最開始接觸到的鎖就是synchronized,書本上是這麼寫的,老師也是這麼說的,至於為啥叫鎖,可能也沒多少人真的去思考過。不知道有沒有同學和我一樣,經歷過只知道用synchronized,後來逐漸的瞭解ReentrantLock,讀寫鎖,然後又瞭解了aqs,後來通過百度google,看一些部落格(這個我要吐槽一下,在學習過程中遇到過很多文章寫的有問題的,反而誤導了我),後面看了看synchronized的原始碼,最後對比synchronized和ReentrantLock才加深了對鎖的一些認知(說實話,作為一個剛畢業3年的非科班出身碼農,我也不敢保證自己寫的就一定對,算是學習過程中的一些感悟吧),那接下來我就按照學習順序來逐漸展開。

  先來一段簡單的synchronized使用程式碼:

public static void main(String[] args) {
        String s = new String();
        synchronized (s) {
            TestJni jni = new TestJni();
            jni.jniHello();
        }
    }

上面程式碼做的事情很簡單,如下圖所示,有A B C D E多個執行緒同時來到synchronized包含的程式碼塊,A先一步進來了,那麼BCDE都得等,等我A執行完他們才能進來執行。

  synchronized用起來確實很簡單,我們也可以放在方法上,但是其本質還是鎖的物件,這個我們後面分析原始碼一看就知道了。

  隨著開發時間越長,synchronized在有些複雜場景下(如需要可中斷,可控制時間搶鎖,需要多個等待佇列分別控制,讀寫鎖等場景的時候)無法滿足我們的需求,那麼就要用到Lock,下面我們先介紹一下Lock的簡單使用:

        Lock lock = new ReentrantLock();
        lock.lock();  
        try {
            System.out.println("執行緒:"+Thread.currentThread().getName()+ " 進來啦");
        }
finally { lock.unlock(); }

上面是一種最簡單的使用,和synchronized作用是一樣的,不過加鎖之後必須要解鎖,且必須緊跟try - finally塊解鎖,使用起來稍微複雜一點,容易出錯。

我們再介紹一種可中斷的使用方式:

public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                lock.lockInterruptibly();
            try {
                testLock();
            } finally {
                lock.unlock();
            }
            } catch (Exception e){
            }
        });
        thread.start();
        thread.interrupt();
    }

    public  static void testLock(){
        condition.signalAll();
        System.out.println("執行緒:"+Thread.currentThread().getName()+ " 進來啦");
    }

這種方式呢,在拿鎖被park住了,如果剛好這時候被打斷了,就會響應打斷退出搶鎖並丟擲異常,至於捕獲到異常開發者怎麼做,那就得根據業務來分別處理了。

而像可控制時間的其實就要稍微複雜一點,先看一下synchronized中的使用:

static TestHash s = new TestHash();
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            testLock();
        });
        Thread thread2 = new Thread(()->{
            synchronized (s) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                s.notify();
                testLock();
            }
        });
        thread1.start();
        thread2.start();
    }

    public static void testLock(){
        synchronized (s) {
            System.out.println("執行緒:"+Thread.currentThread().getName()+ " 進來啦");
            try {
                s.wait();
                System.out.println("執行緒:"+Thread.currentThread().getName()+ " 叫醒啦");
            } catch (InterruptedException e) {
                System.out.println("拋異常啦");
            }
        }
    }

這個例子看著要比前面幾個複雜一點,首先thread1會進入testLock方法,並拿到鎖,thread2等了1秒叫醒thread1(這裡就是簡單的wait/notify的使用),然後在拿到鎖的情況下,再次進入testLock方法並拿到鎖,由於沒人喚醒了,會一直卡在這裡(這裡證明了synchronized的可重入),結果我就不貼了,感興趣的可以拿著程式碼去試。

而ReentrantLock的使用也差不多,就是提前用lock去new一個Condition:

static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            testWaitSingal();
        },"thread1");
        Thread thread2 = new Thread(()->{
            lock.lock();
                try {
                    TimeUnit.SECONDS.sleep(1);
                    condition.signal();
                    testWaitSingal();
                } catch (InterruptedException e) {
                }finally {
                lock.unlock();
            }
        },"thread2");
        thread1.start();
        thread2.start();
    }

    public static void testWaitSingal(){
        lock.lock();
        try {
            System.out.println("執行緒:"+Thread.currentThread().getName()+ " 進來啦");
            condition.await();
            System.out.println("執行緒:"+Thread.currentThread().getName()+ " 叫醒啦");
        } catch (InterruptedException e) {
            System.out.println("拋異常啦");
        }finally {
            lock.unlock();
        }
    }

  可以看到兩種用法基本上是一致的,也就是將synchronized換成了lock,wait換成await,notify換成singal,

總結:

  基本上我們平時用到的synchronized關鍵字的用法也就這些,但lock鎖不一樣,它還支援如上述的中斷,更復雜的讀寫鎖,還可以在aqs的基礎上衍生出更多,如countDownLatch,cyclicBarrier等,可以支援我們做更多,但是不是lock就可以完全替代synchronized了呢,其實synchronized也有自己的優點,簡單,不易出錯,效能也不比lock差(有的書上寫道synchronized效能比lock好,但其實就算好也不會好太多,對於我們來說,基本上可以忽略),真要說選哪個,我的建議是優先選synchronized,如果有特殊業務特殊需求synchronized無法滿足,那當然是要用lock,不過,一定要記得釋放鎖哦。

  本來打算結合reentrant和synchronized直接串起來講的,但是確實有點多,這一篇就當作是後面的引子吧。