Java中鎖的分類與使用方法

Lock和synchronized

  • 鎖是一種工具,用於控制對共享資源的訪問
  • Lock和synchronized,這兩個是最創建的鎖,他們都可以達到線程安全的目的,但是使用和功能上有較大不同
  • Lock不是完全替代synchronized的,而是當使用synchronized不合適或不足以滿足要求的時候,提供高級功能 
  • Lock 最常見的是ReentrantLock實現

為啥需要Lock

  1. syn效率低:鎖的釋放情況少,試圖獲得鎖時不能設定超時,不能中斷一個正在試圖獲得鎖的線程
  2. 不夠靈活,加鎖和釋放的時機單一,每個鎖僅有一個單一的條件(某個對象),可能是不夠的
  3. 無法知道是否成功獲取到鎖

主要方法

Lock();     

最普通的獲取鎖,最佳實踐是finally中釋放鎖,保證發生異常的時候鎖一定被釋放

    /**
     * 描述:Lock不會像syn一樣,異常的時候自動釋放鎖
     *      所以最佳實踐是finally中釋放鎖,保證發生異常的時候鎖一定被釋放
     */
    private static Lock lock = new ReentrantLock();
 
    public static void main(String[] args) {
        lock.lock();
        try {
            //獲取本鎖保護的資源
            System.out.println(Thread.currentThread().getName() + "開始執行任務");
        } finally {
            lock.unlock();
        }
    }

tryLock(long time,TimeUnit unit);超時就放棄

用來獲取鎖,如果當前鎖沒有被其它線程占用,則獲取成功,則返回true,否則返回false,代表獲取鎖失敗

/**
     * 描述:用TryLock避免死鎖
     */
    static class TryLockDeadlock implements Runnable {
 
        int flag = 1;
 
        static Lock lock1 = new ReentrantLock();
        static Lock lock2 = new ReentrantLock();
 
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (flag == 1) {
                    try {
                        if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                            try {
                                System.out.println("線程1獲取到瞭鎖1");
                                Thread.sleep(new Random().nextInt(1000));
                                if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
                                    try {
                                        System.out.println("線程1獲取到瞭鎖2");
                                        System.out.println("線程1成功獲取到瞭2把鎖");
                                        break;
                                    }finally {
                                        lock2.unlock();
                                    }
                                }else{
                                    System.out.println("線程1獲取鎖2失敗,已重試");
                                }
                            } finally {
                                lock1.unlock();
                                Thread.sleep(new Random().nextInt(1000));
                            }
                        } else {
                            System.out.println("線程1獲取鎖1失敗,已重試");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
 
                if (flag == 0) {
                    try {
                        if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            try {
                                System.out.println("線程2獲取到瞭鎖2");
                                Thread.sleep(new Random().nextInt(1000));
                                if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
                                    try {
                                        System.out.println("線程2獲取到瞭鎖1");
                                        System.out.println("線程2成功獲取到瞭2把鎖");
                                        break;
                                    }finally {
                                        lock1.unlock();
                                    }
                                }else{
                                    System.out.println("線程2獲取鎖1失敗,已重試");
                                }
                            } finally {
                                lock2.unlock();
                                Thread.sleep(new Random().nextInt(1000));
                            }
                        } else {
                            System.out.println("線程2獲取鎖2失敗,已經重試");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
 
        public static void main(String[] args) {
            TryLockDeadlock r1 = new TryLockDeadlock();
            TryLockDeadlock r2 = new TryLockDeadlock();
            r1.flag = 1;
            r2.flag = 0;
            new Thread(r1).start();
            new Thread(r2).start();
        }
    }
 
執行結果:
線程1獲取到瞭鎖1
線程2獲取到瞭鎖2
線程1獲取鎖2失敗,已重試
線程2獲取到瞭鎖1
線程2成功獲取到瞭2把鎖
線程1獲取到瞭鎖1
線程1獲取到瞭鎖2
線程1成功獲取到瞭2把鎖

lockInterruptibly(); 中斷

相當於tryLock(long time,TimeUnit unit) 把超時時間設置為無限,在等待鎖的過程中,線程可以被中斷

/**
     * 描述:獲取鎖的過程中,中斷瞭
     */
    static class LockInterruptibly implements Runnable {
 
        private Lock lock = new ReentrantLock();
 
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖");
            try {
                lock.lockInterruptibly();
                try {
                    System.out.println(Thread.currentThread().getName() + "獲取到瞭鎖");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + "睡眠中被中斷瞭");
                } finally {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + "釋放瞭鎖");
                }
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "等鎖期間被中斷瞭");
            }
        }
 
        public static void main(String[] args) {
            LockInterruptibly lockInterruptibly = new LockInterruptibly();
            Thread thread0 = new Thread(lockInterruptibly);
            Thread thread1 = new Thread(lockInterruptibly);
            thread0.start();
            thread1.start();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread0.interrupt();
        }
    }
 
執行結果:
Thread-0嘗試獲取鎖
Thread-1嘗試獲取鎖
Thread-0獲取到瞭鎖
Thread-0睡眠中被中斷瞭
Thread-0釋放瞭鎖
Thread-1獲取到瞭鎖
Thread-1釋放瞭鎖

Java鎖分類:

樂觀鎖和悲觀鎖:

樂觀鎖:

比較樂觀,認為自己在處理操作的時候,不會有其它線程來幹擾,所以並不會鎖住操作對象

  • 在更新的時候,去對比我修改期間的數據有沒有被改變過,如沒有,就正常的修改數據
  • 如果數據和我一開始拿到的不一樣瞭,說明其他人在這段時間內改過,會選擇放棄,報錯,重試等策略
  • 樂觀鎖的實現一般都是利用CAS算法來實現的

劣勢:

可能造成ABA問題,就是不知道是不是修改過

使用場景:

適合並發寫入少的情況,大部分是讀取的場景,不加鎖的能讓讀取的性能大幅提高

悲觀鎖:

比較悲觀,認為如果我不鎖住這個資源,別人就會來爭搶,就會造成數據結果錯誤,所以它會鎖住操作對象,Java中悲觀鎖的實現就是syn和Lock相關類

劣勢:

  • 阻塞和喚醒帶來的性能劣勢
  • 如果持有鎖的線程被永久阻塞,比如遇到瞭無限循環,死鎖等活躍性問題,那麼等待該線程釋放鎖的那幾個線程,永遠也得不到執行
  • 優先級反轉,優先級低的線程拿到鎖不釋放或釋放的比較慢,就會造成這個問題

使用場景:

適合並發寫入多的情況,適用於臨界區持鎖時間比較長的情況:

  1. 臨界區有IO操作
  2. 臨界區代碼復雜或者循環量大
  3. 臨界區競爭非常激烈

可重入鎖:

可重入就是說某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖

ReentrantLock 和 synchronized 都是可重入鎖

// 遞歸調用演示可重入鎖
    static class RecursionDemo{
 
        public static ReentrantLock lock = new ReentrantLock();
 
        private static void accessResource(){
            lock.lock();
            try {
                System.out.println("已經對資源處理瞭");
                if (lock.getHoldCount() < 5){
                    System.out.println("已經處理瞭"+lock.getHoldCount()+"次");
                    accessResource();
                }
            }finally {
                lock.unlock();
            }
        }
 
        public static void main(String[] args) {
            new RecursionDemo().accessResource();
        }
    }
 
 
執行結果:
已經對資源處理瞭
已經處理瞭1次
已經對資源處理瞭
已經處理瞭2次
已經對資源處理瞭
已經處理瞭3次
已經對資源處理瞭
已經處理瞭4次
已經對資源處理瞭

ReentrantLock的其它方法

  • isHeldByCurrentThread 可以看出鎖是否被當前線程持有
  • getQueueLength()可以返回當前正在等待這把鎖的隊列有多長,一般這兩個方法是開發和調試時候使用,上線後用到的不多

公平鎖和非公平鎖

  • 公平指的是按照線程請求的順序,來分配鎖;
  • 非公平指的是,不完全按照請求的順序,在一定情況下,可以插隊
  • 非公平鎖可以避免喚醒帶來的空檔期
/**
 * 描述:演示公平鎖和非公平鎖
 */
class FairLock{
 
    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread[] thread = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue));
        }
 
        for (int i = 0; i < 5; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
}
 
class Job implements Runnable{
 
    PrintQueue printQueue;
 
    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }
 
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"開始打印");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName()+"打印完成");
    }
}
 
class PrintQueue{    
    // true 公平,false是非公平
    private  Lock queueLock = new ReentrantLock(true);
    public void printJob(Object document){
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10)+1;
            System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
 
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10)+1;
            System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
 
    }
}
 
執行結果:
Thread-0開始打印
Thread-0正在打印,需要10秒
Thread-1開始打印
Thread-2開始打印
Thread-3開始打印
Thread-4開始打印
Thread-1正在打印,需要2秒
Thread-2正在打印,需要2秒
Thread-3正在打印,需要2秒
Thread-4正在打印,需要4秒
Thread-0正在打印,需要2秒
Thread-0打印完成
Thread-1正在打印,需要7秒
Thread-1打印完成
Thread-2正在打印,需要8秒
Thread-2打印完成
Thread-3正在打印,需要3秒
Thread-3打印完成
Thread-4正在打印,需要8秒
Thread-4打印完成
 
true改為false演示非公平鎖:
Lock queueLock = new ReentrantLock(false);
執行結果:
Thread-0正在打印,需要7秒
Thread-1開始打印
Thread-2開始打印
Thread-3開始打印
Thread-4開始打印
Thread-0正在打印,需要9秒
Thread-0打印完成
Thread-1正在打印,需要3秒
Thread-1正在打印,需要2秒
Thread-1打印完成
Thread-2正在打印,需要4秒
Thread-2正在打印,需要7秒
Thread-2打印完成
Thread-3正在打印,需要10秒
Thread-3正在打印,需要2秒
Thread-3打印完成
Thread-4正在打印,需要7秒
Thread-4正在打印,需要8秒
Thread-4打印完成

共享鎖和排它鎖:

  • 排它鎖,又稱為獨占鎖,獨享鎖
  • 共享鎖,又稱為讀鎖,獲得共享鎖之後,可以查看但無法修改和刪除數據,其他線程此時也可以獲取到共享鎖,也可以查看但無法修改和刪除數據
  • 共享鎖和排它鎖的典型是讀寫鎖 ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨享鎖

讀寫鎖的作用:

  • 在沒有讀寫鎖之前,我們假設使用ReentrantLock,那麼雖然我們保證瞭線程安全,但是也浪費瞭一定的資源:多個讀操作同時進行,並沒有線程安全問題
  • 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,提高瞭程序的執行效率

讀寫鎖的規則:

  1. 多個線程值申請讀鎖,都可以申請到
  2. 要麼一個或多個一起讀,要麼一個寫,兩者不會同時申請到,隻能存在一個寫鎖
/**
 * 描述:演示可以多個一起讀,隻能一個寫
 */
class CinemaReadWrite{
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
 
    private static void read(){
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到瞭讀鎖,正在讀取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放瞭讀鎖");
            readLock.unlock();
        }
    }
 
    private static void write(){
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到瞭寫鎖,正在寫入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放瞭寫鎖");
            writeLock.unlock();
        }
    }
 
    public static void main(String[] args) {
        new Thread(()-> read(),"Thrad1").start();
        new Thread(()-> read(),"Thrad2").start();
        new Thread(()-> write(),"Thrad3").start();
        new Thread(()-> write(),"Thrad4").start();
    }
}
 
執行結果:
Thrad1得到瞭讀鎖,正在讀取
Thrad2得到瞭讀鎖,正在讀取
Thrad2釋放瞭讀鎖
Thrad1釋放瞭讀鎖
Thrad3得到瞭寫鎖,正在寫入
Thrad3釋放瞭寫鎖
Thrad4得到瞭寫鎖,正在寫入
Thrad4釋放瞭寫鎖

讀鎖和寫鎖的交互方式:

讀鎖插隊策略:

  • 公平鎖:不允許插隊
  • 非公平鎖:寫鎖可以隨時插隊,讀鎖僅在等待隊列頭節點不是想獲取寫鎖線程的時候可以插隊

自旋鎖和阻塞鎖

  • 讓當前線程進行自旋,如果自旋完成後前面鎖定同步資源的線程已經釋放瞭鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
  • 阻塞鎖和自旋鎖相反,阻塞鎖如果遇到沒拿到鎖的情況,會直接把線程阻塞,知道被喚醒

自旋缺點:

  • 如果鎖被占用的時間很長,那麼自旋的線程隻會白浪費處理器資源
  • 在自旋的過程中,一直消耗cpu,所以雖然自旋鎖的起始開銷低於悲觀鎖,但是隨著自旋的時間增長,開銷也是線性增長的

原理:

  • 在Java1.5版本及以上的並發框架java.util.concurrent 的atmoic包下的類基本都是自旋鎖的實現
  • AtomicInteger的實現:自旋鎖的實現原理是CAS,AtomicInteger中調用unsafe 進行自增操作的源碼中的do-while循環就是一個自旋操作,如果修改過程中遇到其他線程競爭導致沒修改成功,就在while裡死循環直至修改成功
/**
 * 描述:自旋鎖演示
 */
class SpinLock{
    private AtomicReference<Thread> sign = new AtomicReference<>();
 
    public void lock(){
        Thread currentThread = Thread.currentThread();
        while (!sign.compareAndSet(null,currentThread)){
            System.out.println("自旋獲取失敗,再次嘗試");
        }
    }
 
    public void unLock(){
        Thread currentThread = Thread.currentThread();
        sign.compareAndSet(currentThread,null);
    }
 
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable(){
            @Override
            public void run(){
                System.out.println(Thread.currentThread().getName()+"開始嘗試自旋鎖");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName()+"獲取到瞭自旋鎖");
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    spinLock.unLock();
                    System.out.println(Thread.currentThread().getName()+"釋放瞭自旋鎖");
                }
            }
        };
 
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
 
 
執行結果:
Thread-0開始嘗試自旋鎖
Thread-0獲取到瞭自旋鎖
Thread-1開始嘗試自旋鎖
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
Thread-0釋放瞭自旋鎖
Thread-1獲取到瞭自旋鎖
Thread-1釋放瞭自旋鎖

使用場景:

  • 自旋鎖一般用於多核服務器,在並發度不是特別高的情況下,比阻塞鎖的效率要高
  • 另外,自旋鎖適用於臨界區比較短小的情況,否則如果臨界區很大(線程一旦拿到鎖,很久之後才會釋放),那也是不合適的

總結

到此這篇關於Java中鎖的分類與使用方法的文章就介紹到這瞭,更多相關Java中鎖使用內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀:

    None Found