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

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

Synchronized

首先咱們來看一段簡單的代碼:安全

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的三種使用方式

給指定對象加鎖,進入代碼前須要得到對象的鎖ide

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);
    }
}複製代碼

給方法加鎖,至關於給當前實例加鎖,進入代碼前須要得到當前實例的鎖優化

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);
    }
}複製代碼

給靜態方法加鎖,至關於給當前類加鎖,進入代碼前須要得到當前類的鎖。這種方式請慎用,都鎖住整個類了,那效率能高哪去this

public static synchronized void add(){
            i++;
        }複製代碼

重入鎖

在JDK6尚未優化synchronize以前還有一個鎖比它表現的更爲亮眼,這個鎖就是重入鎖。
咱們來看一下一個簡單的使用重入鎖的案例:spa

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的功能。而且呢,咱們能夠看到使用衝入鎖是顯示的指定何時加鎖何時釋放的,這樣對於一些流程控制就會更加的有優點。線程

再來看這個鎖爲何叫作重入鎖呢,這是由於這種鎖是能夠反覆進入的,好比說以下操做是容許的。code

lock.lock();
lock.lock();
try {
  i++;
}finally {
    lock.unlock();
    lock.unlock();
}複製代碼

不過須要注意的是若是屢次加鎖的話一樣也要記得屢次釋放,不然資源是不能被其餘線程使用的。

在以前的文章:多線程基本概念 中有提到過由於線程優先級而致使的飢餓問題,重入鎖提供了一種公平鎖的功能,能夠忽略線程的優先級,讓全部線程公平競爭。使用公平鎖的方式只須要在重入鎖的構造方法傳入一個true就能夠了。

public static ReentrantLock lock = new ReentrantLock(true);複製代碼

重入鎖還提供了一些高級功能,例如中斷。
對於synchronize來講,若是一個線程獲取資源的時候要麼阻塞要麼就是獲取到資源,這樣的狀況是沒法解決死鎖問題的。而重入鎖則能夠響應中斷,經過放棄資源而解決死鎖問題。
使用中斷的時候只須要把原先的lock.lock()改爲lock.lockInterruptibly()就OK了。
來看代碼示例:

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。

請看代碼示例:

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的代碼示例:

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提供了讀寫分離鎖來實現讀寫分離的功能。

這裏給出使用讀寫鎖的一個代碼示例

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秒多。這就是讀寫鎖的魅力。

最後

你們以爲不錯能夠點個贊在關注下,之後還會分享更多文章!

相關文章
相關標籤/搜索