淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖

Java學習錄發表於2019-03-26

Java開發必須要掌握的知識點就包括如何使用鎖在多執行緒的環境下控制對資源的訪問限制


Synchronized

首先我們來看一段簡單的程式碼:

12345678910111213141516171819複製程式碼
public class NotSyncDemo {    public static int i=0;    static class ThreadDemo extends Thread {        @Override        public void run() {           for (int j=0;j<10000;j++){               i++;           }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1=new ThreadDemo();        ThreadDemo t2=new ThreadDemo();        t1.start();t2.start();        t1.join();        t2.join();        System.out.println(i);    }}複製程式碼

上方的程式碼使用了2個執行緒同時對靜態變數i進行++操作,理想中的結果最後輸出的i的值應該是20000才對,但是如果你執行這段程式碼的時候你會發現最後的結果始終是一個比20000小的數。這個就是由於JMM規定執行緒操作變數的時候只能先從主記憶體讀取到工作記憶體,操作完畢後在寫到主記憶體。而當多個執行緒併發操作一個變數時很可能就會有一個執行緒讀取到另外一個執行緒還沒有寫到主記憶體的值從而引起上方的現象。更多關於JMM的知識請參考此文章:Java多執行緒記憶體模型

想要避免這種多執行緒併發操作引起的資料異常問題一個簡單的解決方案就是加鎖。JDK提供的synchronize就是一個很好的選擇。
synchronize的作用就是實現執行緒間的同步,使用它加鎖的程式碼同一時刻只能有一個執行緒訪問,既然是單執行緒訪問那麼就肯定不存在併發操作了。
synchronize可以有多種用法,下面給出各個用法的示例程式碼。


Synchronized的三種使用方式

給指定物件加鎖,進入程式碼前需要獲得物件的鎖

1234567891011121314151617181920212223複製程式碼
public class SyncObjDemo {    public static Object obj = new Object();    public static int i = 0;    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                synchronized (obj) {                    i++;                }            }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1 = new ThreadDemo();        ThreadDemo t2 = new ThreadDemo();        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}複製程式碼

給方法加鎖,相當於給當前例項加鎖,進入程式碼前需要獲得當前例項的鎖

123456789101112131415161718192021222324複製程式碼
public class SyncMethodDemo {    public static int i = 0;    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                 add();            }        }        public synchronized void add(){            i++;        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo threadDemo=new ThreadDemo();        Thread t1 = new Thread(threadDemo);        Thread t2 = new Thread(threadDemo);        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}複製程式碼

給靜態方法加鎖,相當於給當前類加鎖,進入程式碼前需要獲得當前類的鎖。這種方式請慎用,都鎖住整個類了,那效率能高哪去

123複製程式碼
public static synchronized void add(){            i++;        }複製程式碼


重入鎖

在JDK6還沒有優化synchronize之前還有一個鎖比它表現的更為亮眼,這個鎖就是重入鎖。
我們來看一下一個簡單的使用重入鎖的案例:

12345678910111213141516171819202122232425262728複製程式碼
public class ReentrantLockDemo {    public static ReentrantLock lock = new ReentrantLock();    public static int i = 0;    static class ThreadDemo extends Thread {        @Override        public void run() {            for (int j = 0; j < 10000; j++) {                lock.lock();                 try {                     i++;                 }finally {                     lock.unlock();                 }            }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadDemo t1 = new ThreadDemo();        ThreadDemo t2 = new ThreadDemo();        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(i);    }}複製程式碼

上方程式碼使用重入鎖同樣實現了synchronize的功能。並且呢,我們可以看到使用衝入鎖是顯示的指定什麼時候加鎖什麼時候釋放的,這樣對於一些流程控制就會更加的有優勢。

再來看這個鎖為什麼叫做重入鎖呢,這是因為這種鎖是可以反覆進入的,比如說如下操作是允許的。

12345678複製程式碼
lock.lock();lock.lock();try {  i++;}finally {    lock.unlock();    lock.unlock();}複製程式碼

不過需要注意的是如果多次加鎖的話同樣也要記得多次釋放,否則資源是不能被其他執行緒使用的。

在之前的文章:多執行緒基本概念 中有提到過因為執行緒優先順序而導致的飢餓問題,重入鎖提供了一種公平鎖的功能,可以忽略執行緒的優先順序,讓所有執行緒公平競爭。使用公平鎖的方式只需要在重入鎖的構造方法傳入一個true就可以了。

1複製程式碼
public static ReentrantLock lock = new ReentrantLock(true);複製程式碼

重入鎖還提供了一些高階功能,例如中斷。
對於synchronize來說,如果一個執行緒獲取資源的時候要麼阻塞要麼就是獲取到資源,這樣的情況是無法解決死鎖問題的。而重入鎖則可以響應中斷,通過放棄資源而解決死鎖問題。
使用中斷的時候只需要把原先的lock.lock()改成lock.lockInterruptibly()就OK了。
來看程式碼示例:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253複製程式碼
public class ReentrantLockInterruptDemo {    public static ReentrantLock lock1 = new ReentrantLock();    public static ReentrantLock lock2 = new ReentrantLock();    static class ThreadDemo extends Thread {        int i = 0;        public ThreadDemo(int i) {            this.i = i;        }        @Override        public void run() {            try {                if (i == 1) {                    lock1.lockInterruptibly();                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    lock2.lockInterruptibly();                } else {                    lock2.lockInterruptibly();                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    lock1.lockInterruptibly();                }                System.out.println(Thread.currentThread().getName() + "完成任務");            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                if (lock1.isHeldByCurrentThread()) {                    lock1.unlock();                }                if (lock2.isHeldByCurrentThread()) {                    lock2.unlock();                }                System.out.println(Thread.currentThread().getName() + "退出");            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new ThreadDemo(1),"t1");        Thread t2 = new Thread(new ThreadDemo(2),"t2");        t1.start();        t2.start();        Thread.sleep(1500);        t1.interrupt();    }}複製程式碼

檢視上方程式碼我們可以看到,執行緒t1啟動後先佔有lock1,然後會在睡眠1秒之後試圖佔有lock2,而t2則先佔有lock2,然後試圖佔有lock1。這個過程則勢必會發生死鎖。而如果再這個時候我們給t1一箇中斷的訊號t1就會響應中斷從而放棄資源,繼而解決死鎖問題。

除了提供中斷解決死鎖以外,重入鎖還提供了限時等待功能來解決這個問題。
限時等待的使用方式是使用lock.tryLock(2,TimeUnit.SECONDS)
這個方法有兩個引數,前面是等待時長,後面是等待時長的計時單位,如果在等待時長範圍內獲取到了鎖就會返回true。

請看程式碼示例:

1234567891011121314151617181920212223242526272829303132複製程式碼
public class ReentrantLockTimeDemo {    public static ReentrantLock lock = new ReentrantLock();    static class ThreadDemo extends Thread {        @Override        public void run() {            try {                if (lock.tryLock(2, TimeUnit.SECONDS)) {                    try {                        System.out.println(Thread.currentThread().getName() + "獲取鎖成功");                        Thread.sleep(3000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                } else {                    System.out.println(Thread.currentThread().getName() + "獲取鎖失敗");                }            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                if (lock.isHeldByCurrentThread()) {                    lock.unlock();                }            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new ThreadDemo(), "t1");        Thread t2 = new Thread(new ThreadDemo(), "t2");        t1.start();        t2.start();    }}複製程式碼

同樣的tryLock也可以不帶引數,不帶引數的時候就是表示立即獲取,獲取不成功就直接返回false

我們知道synchronize配合wait和notify可以實現等待通知的功能,重入鎖同樣也提供了這種功能的實現。那就是condition。使用lock.newCondition()就可以獲得一個Condition物件。

下面請看使用Condition的程式碼示例:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647複製程式碼
public class ReentrantLockWaitNotifyThread {    public static ReentrantLock lock = new ReentrantLock();    public static Condition condition = lock.newCondition();    static class WaitThreadDemo extends Thread {        @Override        public void run() {            try {                System.out.println("WaitThread wait,time=" + System.currentTimeMillis());                lock.lock();                condition.await();            } catch (InterruptedException e) {                e.printStackTrace();            }finally {                lock.unlock();                System.out.println("WaitThread end,time=" + System.currentTimeMillis());            }        }    }    static class NotifyThreadDemo extends Thread {        @Override        public void run() {                lock.lock();                System.out.println("NotifyThread notify,time=" + System.currentTimeMillis());                condition.signal();                try {                    Thread.sleep(2000);                } catch (InterruptedException e) {                    e.printStackTrace();                }finally {                    lock.unlock();                    System.out.println("NotifyThread end,time=" + System.currentTimeMillis());                }            }    }    public static void main(String[] args) {        WaitThreadDemo waitThreadDemo = new WaitThreadDemo();        NotifyThreadDemo notifyThreadDemo = new NotifyThreadDemo();        waitThreadDemo.start();        try {            Thread.sleep(100);        } catch (InterruptedException e) {            e.printStackTrace();        }        notifyThreadDemo.start();    }}複製程式碼


讀寫鎖

通過上方的內容我們知道了為了解決執行緒安全問題,JDK提供了相當多的鎖來幫助我們。但是如果多執行緒併發讀的情況下是不會出現執行緒安全問題的,那麼有沒有一種鎖可以在讀的時候不控制,讀寫衝突的時候才會控制呢。答案是有的,JDK提供了讀寫分離鎖來實現讀寫分離的功能。

這裡給出使用讀寫鎖的一個程式碼示例

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152複製程式碼
public class ReadWriteLockDemo {    public static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();    public static Lock readLock = readWriteLock.readLock();    public static Lock writeLock = readWriteLock.writeLock();    public static void read(Lock lock) {        lock.lock();        try {            System.out.println("readTime:" + System.currentTimeMillis());            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public static void write(Lock lock) {        lock.lock();        try {            System.err.println("writeTime:" + System.currentTimeMillis());            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    static class ReadThread extends Thread {        @Override        public void run() {            read(readLock);        }    }    static class WriteThread extends Thread {        @Override        public void run() {            write(writeLock);        }    }    public static void main(String[] args) throws InterruptedException {        for (int i = 0; i < 10; i++) {            new ReadThread().start();        }        new WriteThread().start();        new WriteThread().start();        new WriteThread().start();    }}複製程式碼

上方程式碼模擬了10個執行緒併發讀,3個執行緒併發寫的狀況,如果我們使用synchronize或者重入鎖的時候我想上方最後的耗時應該是26秒多。但是如果你執行 一下上方的程式碼你就會發現僅僅只花費了6秒多。這就是讀寫鎖的魅力。

本文所有原始碼github.com/shiyujun/sy…

淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖


相關文章