1. 程式人生 > >淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖

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

tps 訪問限制 三種 ron 單線程 new t try end 必須

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

給指定對象加鎖,進入代碼前需要獲得對象的鎖


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);
    }
}

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


public static synchronized void add(){
            i++;
        }


重入鎖

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


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的功能。並且呢,我們可以看到使用沖入鎖是顯示的指定什麽時候加鎖什麽時候釋放的,這樣對於一些流程控制就會更加的有優勢。

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


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了。
來看代碼示例:


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

本文所有源碼https://github.com/shiyujun/syj-study-demo
![](https://s1.51cto.com/images/blog/201903/26/6218f25283ebe13378e87b0766148638.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

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