美文网首页
Java并发编程之显式锁

Java并发编程之显式锁

作者: 干天慈雨 | 来源:发表于2021-07-11 15:46 被阅读0次

    1.前言

           使用Java内置锁的时候,不需要通过Java代码显式地对同步对象的监视器(Monitor)进行抢占和释放,因为这些工作由JVM层面来完成。而且任何一个Java对象都能作为一个内置锁来使用,所以,Java的对象锁使用起来很方便。但是,Java内置锁的功能相对单一,不具备一些比较高级的锁功能:

    • 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃,不至于无限等下去
    • 中断抢锁:在抢锁时,外部线程给抢锁线程发出一个中断信号,就能唤起等待锁的线程,并且终止抢占过程。
    • 多个等待队列:为锁维持多个等待队列,以便提高锁的效率。比如在生产者消费者模式实现中,生产者和消费者共用一把锁,该锁上维持两个等待队列,一个生产者队列,一个消费者队列。

           除了以上的功能问题之外,Java对象锁还存在性能问题。在竞争稍微激烈的情况下,Java对象锁会膨胀为重量级锁(基于操作对象的Mutex Lock实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核状态和用户态之间来回切换,导致性能非常低。所以这个时候就需要引入一种新的锁。
           Java显式锁就是为了解决这些Java对象的功能问题、性能问题而生。Lock是Java代码级别的锁。为了和Java对象锁区分,Lock接口叫显式锁接口,其对象实例叫显式锁对象。

    2.Lock的实现

           Lock本质上是一个接口,它定义了释放锁和获得锁的抽象方法,实现Lock接口的类有很多,以下是几个常见的锁实现:
    ReentrantLock:表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计算器增加重入次数。
    ReentrantReadWriteLock:重入读写锁。它实现了ReadWriteLock接口。在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,它们分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥,读和写互斥,写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
    StampedLock:stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

    3. 显式锁的分类

           显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁与不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。

    3.1 可重入锁和不可重入锁

    从同一个线程是否可以重复占有同一个锁对象的角度分,显式锁可以分为可重入锁与不可重入锁。
    可重入锁,也叫做递归锁,指的一个线程可以多次抢占同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock 显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 依然可以抢到该 Lock 显式锁。
    不可重入锁与可重入锁相反,指的一个线程只能抢占一次同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 不可以抢到该 Lock 显式锁。除非,线程 A 提前释放了该 Lock 显式锁,才能第二次抢占该锁。
    JUC 的 ReentrantLock 类是可重入锁的一个标准实现类。

    3.2 悲观锁和乐观锁

    从线程进入临界区前是否锁住同步资源的角度来分,显式锁可以分为悲观锁和乐观锁。
    悲观锁是就是悲观思想,每次去入临界区操作数据的时候都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直 等到拿到锁。总体来说,悲观锁适用于写多读少的场景,遇到高并发写时性能高。
    Java 的 Synchronized 重量级锁是一种悲观锁。
    乐观锁是一种乐观思想,每次去拿数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。总体来说,乐观锁适用于读多写少的场景,遇到高并发写的可能性低。
    Java 中的乐观锁基本都是通过 CAS 自旋操作实现的。CAS 是一种更新原子操作,比较当前值跟传入值是否一样,是则更新,不是则失败。在争用激烈的场景下,CAS 自旋会出现大量的空自旋,会导致乐观锁性能大大降低。
    Java 的 Synchronized 轻量级锁是一种乐观锁。另外,JUC 中基于抽象队列同步器(AQS)实现的显式锁(如 ReentrantLock)都是乐观锁。

    3.3 公平锁和非公平锁

           “公平锁”是指“不同的线程抢占锁的机会是公平的、平等的”,从抢占时间上来说,先对锁进行抢占的线程一定被先满足,抢锁成功的次序体现为 FIFO(先进先出)顺序。简单来说,,公平锁就是保障了各个线程获取锁都是按照顺序来的,先到的线程先获取锁。
           使用公平锁,比如线程 A、B、C、D 依次去获取锁,线程 A 首先获取到了锁,然后它处理完成释放锁之后,会唤醒下一个线程 B 去获取锁。后续不断重复前面的过程,C、D 依次获取锁。
           “非公平锁”是指不同的线程抢占锁的机会是非公平的、不平等的,从抢占时间上来说,先对锁进行抢占的线程不一定被先满足,抢锁成功的次序不会体现为 FIFO(先进先出)顺序。
           使用公平锁,比如线程 A、B、C、D 依次去获取锁, 假如此时持有锁的是线程 A,然后线程B、C、D 尝试获取锁,就会进入一个等待队列。当线程 A 释放掉锁之后,会唤醒下一个线程 B 去获取锁。在唤醒线程 B 的这个过程中,如果有别的线程 E 尝试去请求锁,那么线程 E 是可以先获取到的,这就是插队。为什么可以线程 E 可以插队呢?因为 CPU 唤醒线程 B 需要进行线程的上下文切换,这个操作需要一定的时间,线程 E 可能与线程 A、B 不在同一个 CPU Core 执行,而是在其他的 Core 上执行,所以不需要进行线程的上下文切换。在线程 A 释放锁和线程 B 被唤醒的这段时间,锁是空闲的,其他 Core 上的线程 E 此时就能趁机获取非公平锁,这样做的目的主要是利用锁的空档期,提高其利用效率 。
           默认情况下 ReentrantLock 实例是非公平锁,但是,如果在实例构造时传入了参数 true,所得到就是公平锁。另外,ReentrantLock 的 tryLock()方法是一个特例,一但有线程释放了锁,那正在tryLock 的线程就能优先取到锁,即使已经有其他线程在等待队列中。

    3.4 可中断锁和不可中断锁

    什么是可中断锁?如果某一线程 A 正占有锁在执行临界区代码,另一线程 B 正在阻塞式抢占锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁。
    什么是不可中断锁?一旦这个锁被其他线程占有,如果自己还想抢占,自己只能选择等待或者阻塞,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么自己只能永远等下去,并且没有办法终止等待或阻塞。
    简单来说,在抢锁过程中能通过某些方法去终止抢占过程,那就是可中断锁,否则就是不可中断锁。
    Java 的 synchronized 内置锁就是一个不可中断锁,而 JUC 的显式锁(如 ReentrantLock) 是一个可中断锁。

    3.5 共享锁和独占锁

           “独占锁”指的是每次只能有一个线程能持有的锁。独占锁是一种悲观保守的加锁策略,它不必要地限制了读/读竞争,如果某个只读线程获取锁,则其他的读线程都只能等待,这种情况下就限制了读操作的并发性,因为读操作并不会影响数据的一致性。
    JUC 的 ReentrantLock 类,是一个标准的“独占锁”实现类。
    “共享锁”允许多个线程同时获取锁,容许线程并发进入临界区。与独占锁不同,共享锁则是一种乐观锁,它放宽了加锁策略,并不限制读/读竞争,允许多个执行读操作的线程同时访问共享资源。
           JUC 的 ReentrantReadWriteLock(读写锁)类,是一个“共享锁”实现类。使用该读写锁时,读操作可以有很多线程一起读,但是写操作只能有一个线程去写,而且在写入的时候,别的线程也不能进行读的操作。 用 ReentrantLock 锁替代 ReentrantReadWriteLock 锁虽然可以保证线程安全,但是也会浪费一部分资源,因为多个读操作并没有线程安全问题,所以在读的地方使用读锁,在写的地方用写锁,可以提高程序执行效率。

    4. ReentrantLock

    4.1 类继承层次

    相关类之间的继承层次,如下图所示:


    类继承层次

    4.2 ReentrantLock基本用法

    4.2.1 使用 lock( )方法抢锁的模板代码

    通常情况下,大家会使用 lock( )方法的进行阻塞式的锁抢占,其模板代码如下:

            Lock lock = new ReentrantLock();
            lock.lock(); //step1:抢占锁
            try {
                //step2:抢锁成功,执行临界区代码
            } finally {
                lock.unlock(); //step3:释放锁
            }
    

    以上抢锁模板代码,有以下几个需要注意的要点:
    (1)释放锁操作 lock.unlock() 必须在 try-catch 结构的 finally 块中执行,否则,如果临界区代码抛出异常,锁就有可能永远得不到释放。
    (2)抢占锁操作 lock.lock( ) 必须在 try 语句块之外,而不是放在 try 块之内。为什么呢?
    原因之一是 lock( ) 方法是没有申明抛出异常,所以可以不包含到 try 块中;
    原因之二是 lock( ) 方法并不是一定能够抢占锁成功,如果没有抢占成功,当然也就不需要释放锁,而且,在没有占有锁的情况下去释放锁,可能会导致运行时异常。
    (3)在抢占锁操作 lock.lock( )和 try 语句之间,不要插入任何代码,避免抛出异常而导致释
    放锁操作 lock.unlock() 执行不到,导致锁无法被释放。

    4.2.2 使用 tryLock( )方法非阻塞抢锁的模板代码

           lock( )是阻塞式抢占,在没有抢到锁的情况下,当前线程会阻塞。如果不希望线程阻塞,可以使用 tryLock()方法抢占锁。tryLock( ) 是非阻塞抢占,在没有抢到锁的情况下,当前线程会立即返回,不会别阻塞。
    使用 tryLock()方法非阻塞抢占锁,大致的模板代码如下:

            Lock lock = new ReentrantLock();
            if (lock.tryLock()) { //step1:尝试抢占锁
                try {
                    //step2:抢锁成功,执行临界区代码
                } finally {
                    lock.unlock(); //step3:释放锁
                }
            } else {
                //step4:抢锁失败,执行后备动作
            }
    
    

    4.2.3 使用 tryLock(long time, TimeUnit unit)方法抢锁的模板代码

           tryLock(long time, TimeUnit unit)方法用于限时抢锁,该方法在抢锁时会进行一段时间的阻塞等待,其 time 参数代表最大的阻塞时长,其 unit 参数为时长的单位(如秒)。
    使用 tryLock(long time, TimeUnit unit)方法限时抢锁,其大致的代码模板如下:

            Lock lock = new ReentrantLock();
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    //step2:抢锁成功,执行临界区代码
                } finally {
                    lock.unlock(); //step3:释放锁
                }
            } else {
                //step4:抢锁失败,执行后备动作
            }
    

    对 lock( )、tryLock( )、tryLock(long time, TimeUnit unit)的三个方法的总结如下:
    (1)lock( )方法用于阻塞抢锁,抢不到锁时线程会一直阻塞。
    (2)tryLock( )方法用于尝试抢锁,该方法有返回值,如果成功则返回 true,如果失败(即锁已被其他线程获取)则返回 false。此方法无论如何都会立即返回,在抢不到锁时,线程不会像使用 lock( )方法那样一直被阻塞。
    (3)tryLock(long time, TimeUnit unit)方法和 tryLock()方法是类似的,只不过这个方法在抢不到锁时时会阻塞一段时间。如果在阻塞期间获取到锁立即返回 true,超时则返回 false。

    5. 读写锁 ReentrantReadWriteLock

           通过 ReentrantReadWriteLock 类能获取其读锁和写锁,其读锁是可以多线程共享的共享锁,而其写锁是排他锁,在被占时候不允许其他线程再抢占操作。然而其读锁和写锁之间是有关系的:同一时刻不允许读锁和写锁同时被抢占,二者之间是互斥的。

    5.1 类继承层次

    ReadWriteLock是一个接口,内部由两个Lock接口组成。

    public interface ReadWriteLock {
        Lock readLock(); 
        Lock writeLock(); 
    }
    
    类图

    ReentrantReadWriteLock实现了该接口,使用方式如下:

        ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 
        Lock readLock = readWriteLock.readLock(); 
        readLock.lock(); 
        // 进行读取操作 
        readLock.unlock(); 
        
        Lock writeLock = readWriteLock.writeLock(); 
        writeLock.lock(); 
        // 进行写操作 
        writeLock.unlock();
    

    也就是说,当使用 ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock。

    5.2 ReentrantReadWriteLock的使用

           通过 ReentrantReadWriteLock 类能获取其读锁和写锁,其读锁是可以多线程共享的共享锁,而其写锁是排他锁,在被占时候不允许其他线程再抢占操作。然而其读锁和写锁之间是有关系的:同一时刻不允许读锁和写锁同时被抢占,二者之间是互斥的。
    接着先来个代码演示下,读锁是共享锁,写锁是排他锁:

    public class ReadWriteLockTest {
        //创建一个 Map,代表共享数据
        final static Map<String, String> MAP = new HashMap<String, String>();
        //创建一个读写锁
        final static ReentrantReadWriteLock LOCK = new
                ReentrantReadWriteLock();
        //获取读锁
        final static Lock READ_LOCK = LOCK.readLock();
        //获取写锁
        final static Lock WRITE_LOCK = LOCK.writeLock();
    
        //对共享数据的写操作
        public static Object put(String key, String value) {
            //抢写锁
            WRITE_LOCK.lock();
            try {
                System.out.println("[" + Thread.currentThread().getName() + "]" + getNowTime() + " 抢占了 WRITE_LOCK,开始执行 write 操作");
                Thread.sleep(1000);
                //写入共享数据
                String put = MAP.put(key, value);
                return put;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                WRITE_LOCK.unlock();
            }
            return null;
        }
    
        //对共享数据的读操作
        public static Object get(String key) {
            //抢占读锁
            READ_LOCK.lock();
            try {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() + " 抢占了 READ_LOCK,开始执行 read 操作");
                Thread.sleep(1000);
                //读取共享数据
                String value = MAP.get(key);
                return value;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放读锁
                READ_LOCK.unlock();
            }
            return null;
        }
    
        public static String getNowTime() {
            //HH表示用24小时制,如18;hh表示用12小时制
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            return sdf.format(System.currentTimeMillis());
        }
    
        public static void main(String[] args) {
            //创建 Runnable 异步可执行目标实例
            Runnable writeTarget = () -> put("key", "value");
            Runnable readTarget = () -> get("key");
            //创建 4 条读线程
            for (int i = 0; i < 4; i++) {
                new Thread(readTarget, "读线程" + i).start();
            }
            //创建 2 条写线程,并启动
            for (int i = 0; i < 2; i++) {
                new Thread(writeTarget, "写线程" + i).start();
            }
        }
    }
    
    

    运行程序,结果如下:

    [读线程 2]:09:33:20 抢占了 READ_LOCK,开始执行 read 操作
    [读线程 1]:09:33:20 抢占了 READ_LOCK,开始执行 read 操作
    [读线程 0]:09:33:20 抢占了 READ_LOCK,开始执行 read 操作
    [写线程 1]:09:33:21 抢占了 WRITE_LOCK,开始执行 write 操作
    [读线程 3]:09:33:22 抢占了 READ_LOCK,开始执行 read 操作
    [写线程 0]:09:33:23 抢占了 WRITE_LOCK,开始执行 write 操作
    

    从输出结果可以看出:
    (1)读线程 0、读线程 1、读线程 2 同时获取了读锁,说明可以同时进行共享数据的读操作。
    (2)写线程 1、写线程 0 只能依次获取写锁,说明共享数据的写操作不能同时进行。
    (3)读线程 3 必须等待写线程 1 释放写锁后才能获取到读锁,说明读写操作是互斥的。

    5.3 锁的升级与降级

           锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在 ReentrantReadWriteLock 读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。具体的演示代码如下:

    public class ReadWriteLockTest2 {
        //创建一个 Map,代表共享数据
        final static Map<String, String> MAP = new HashMap<String, String>();
        //创建一个读写锁
        final static ReentrantReadWriteLock LOCK = new
                ReentrantReadWriteLock();
        //获取读锁
        final static Lock READ_LOCK = LOCK.readLock();
        //获取写锁
        final static Lock WRITE_LOCK = LOCK.writeLock();
    
        public static String getNowTime() {
            //HH表示用24小时制,如18;hh表示用12小时制
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            return sdf.format(System.currentTimeMillis());
        }
    
        //对共享数据的写操作
        public static Object put(String key, String value) {
            WRITE_LOCK.lock();
            try {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  " 抢占了 WRITE_LOCK,开始执行 write 操作");
                Thread.sleep(1000);
                String put = MAP.put(key, value);
                System.out.println("[" + Thread.currentThread().getName() + "]" +  "尝试降级写锁为读锁");
                //写锁降级为读锁(成功)
                READ_LOCK.lock();
                System.out.println("[" + Thread.currentThread().getName() + "]" +  "写锁降级为读锁成功");
                return put;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                READ_LOCK.unlock();
                WRITE_LOCK.unlock();
            }
            return null;
        }
    
    
        public static Object get(String key)
        {
            READ_LOCK.lock();
            try
            {
                Print.tco(DateUtil.getNowTime()
                        + " 抢占了 READ_LOCK,开始执行 read 操作");
                Thread.sleep(1000);
                String value = MAP.get(key);
                System.out.println("[" + Thread.currentThread().getName() + "]" +  "尝试升级读锁为写锁");
                // 读锁升级为写锁(失败)
                WRITE_LOCK.lock();
                System.out.println("[" + Thread.currentThread().getName() + "]" +  "读锁升级为写锁成功");
                return value;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                WRITE_LOCK.unlock();
                READ_LOCK.unlock();
            }
            return null;
        }
    
        public static void main(String[] args) {
            //创建 Runnable 可执行实例
            Runnable writeTarget = () -> put("key", "value");
            Runnable readTarget = () -> get("key");
            //创建 1 条写线程,并启动
            new Thread(writeTarget, "写线程").start();
            //创建 1 条读线程
            new Thread(readTarget, "读线程").start();
        }
    }
    
    

    运行控制台输出:

    [写线程]:09:51:42 抢占了 WRITE_LOCK,开始执行 write 操作
    [写线程]:写线程尝试降级写锁为读锁
    [写线程]:写线程写锁降级为读锁成功
    [读线程]:09:51:43 抢占了 READ_LOCK,开始执行 read 操作
    [读线程]:读线程尝试升级读锁为写锁
    

           通过结果可以看出:ReentrantReadWriteLock 不支持读锁的升级,主要是避免死锁,例如两个线程 A 和 B 都占了读锁并且都需要升级成写锁, A 升级要求 B 释放读锁,B 升级要求 A 释放读锁,二者就会由于互相等待形成死锁。
    总结起来,与 ReentrantLock 相比,ReentrantReadWriteLock 更适合于读多写少的场景,可以
    提高并发读的效率;而 ReentrantLock 更适合于读写比例相差不大、或写比读多的场景。

    6. StampedLock

    StampedLock(印戳锁)其是对 ReentrantReadWriteLock 读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock 支持不用加读锁而是直接进行读操作,最大程度提升读的效率;只有在发生过写操作之后,再加读锁才能进行读操作。
    StampedLock 三种模式:
    (1)悲观读锁:与 ReadWriteLock 的读锁类似,多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁。
    (2)乐观读:相当于直接操作数据,不加任何锁,连读锁都不要。
    (3)写锁:与 ReadWriteLock 的写锁类似,写锁和悲观读锁是互斥的;虽然写锁与乐观读不会互斥,但是在数据被更新之后,之前通过乐观读所获得的数据,已经变成了脏数据。

    6.1 StampedLock 与 ReentrantReadWriteLock 对比

    StampedLock 与 ReentrantReadWriteLock 语义类似,不同的是,StampedLock 并没有实现ReadWriteLock 接口,而是定义了自己的锁操作 API,主要如下:
    (1)悲观读锁的获取与释放

    //获取普通读锁(悲观读锁),返回 long 类型的印戳值
    public long readLock()
    //释放普通读锁(悲观读锁),以取锁时的印戳值作为参数
    public void unlockRead(long stamp)
    

    (2)写锁的获取与释放

    //获取写锁,返回 long 类型的印戳值
    public long writeLock()
    //释放写锁,以获取写锁时的印戳值作为参数
    public void unlockWrite(long stamp)
    

    (3)乐观读的印戳获取与有效性判断

    //获取乐观读,返回 long 类型的印戳值,返回 0 表示当前锁处于写锁模式,不能乐观读
    public long tryOptimisticRead()
    //判断乐观读的印戳值是否有效,以 tryOptimisticRead 返回的印戳值作为参数
    public long tryOptimisticRead()
    

    6.2 StampedLock 的演示案例

    public class StampedLockTest {
        //创建一个 Map,代表共享数据
        final static Map<String, String> MAP = new HashMap<String, String>();
        //创建一个印戳锁
        final static StampedLock STAMPED_LOCK = new StampedLock();
    
        public static String getNowTime() {
            //HH表示用24小时制,如18;hh表示用12小时制
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            return sdf.format(System.currentTimeMillis());
        }
        public static void sleepSeconds(int second) {
            LockSupport.parkNanos(second * 1000L * 1000L * 1000L);
        }
    
        //对共享数据的写操作
        public static Object put(String key, String value) {
            //尝试获取写锁的印戳
            long stamp = STAMPED_LOCK.writeLock();
            try {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  " 抢占了 WRITE_LOCK,开始执行 write 操作");
                Thread.sleep(1000);
                String put = MAP.put(key, value);
                return put;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  "  释放了 WRITE_LOCK");
                //释放写锁
                STAMPED_LOCK.unlockWrite(stamp);
            }
            return null;
        }
    
        //对共享数据的悲观读操作
        public static Object pessimisticRead(String key) {
            System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  "LOCK 进入过写模式,只能悲观读");
            //进入了写锁模式,只能获取悲观读锁
            //尝试获取读锁的印戳
            long stamp = STAMPED_LOCK.readLock();
            try {
                //成功获取到读锁,并重新获取最新的变量值
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  " 抢占了 READ_LOCK");
                String value = MAP.get(key);
                return value;
            } finally {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  " 释放了 READ_LOCK");
                //释放读锁
                STAMPED_LOCK.unlockRead(stamp);
    
            }
        }
    
        //对共享数据的乐观读操作
        public static Object optimisticRead(String key) {
            String value = null;
            //尝试进行乐观读
            long stamp = STAMPED_LOCK.tryOptimisticRead();
            if (0 != stamp) {
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  "乐观读的印戳值,获取成功");
                //模拟耗费时间 1 秒
                sleepSeconds(1);
                value = MAP.get(key);
            } else {// 0 == stamp 表示当前为写锁模式
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  "乐观读的印戳值,获取失败");
                //LOCK 已经进入写模式,使用悲观读方法
                return pessimisticRead(key);
            }
            //乐观读操作已经间隔了一段时间,期间可能发生写入
            //所以,需要验证乐观读的印戳值是否有效,即判断 LOCK 是否进入过写模式
            if (!STAMPED_LOCK.validate(stamp)) {
                //乐观读的印戳值无效,表明写锁被占用过
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  "乐观读的印戳值,已经过期");
                //写锁已经被抢占,进入了写锁模式,只能通过悲观读锁,再一次读取最新值
                return pessimisticRead(key);
            } else {
                //乐观读的印戳值有效,表明写锁没有被占用过
                //不用加悲观读锁而直接读,减少了读锁的开销
                System.out.println("[" + Thread.currentThread().getName() + "]"  + getNowTime() +  " 乐观读的印戳值,没有过期");
                return value;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //创建 Runnable 可执行实例
            Runnable writeTarget = () -> put("key", "value");
            Runnable readTarget = () -> optimisticRead("key");
            //创建 1 条写线程,并启动
            new Thread(writeTarget, "写线程").start();
            //创建 1 条读线程
            new Thread(readTarget, "读线程").start();
        }
    }
    
    

    运行以上程序,结果如下:

    [写线程]:12:55:45 抢占了 WRITE_LOCK,开始执行 write 操作
    [读线程]:12:55:45 获取乐观读的印戳值,获取失败
    [读线程]:12:55:45 LOCK 进入过写模式,只能悲观读
    [写线程]:12:55:46 释放了 WRITE_LOCK
    [读线程]:12:55:46 抢占了 READ_LOCK
    [读线程]:12:55:46 释放了 READ_LOCK
    

    6.3 为什么引入StampedLock?

    StampedLock是在JDK8中新增的,有了读写锁,为什么还要引入StampedLock呢?

    并发度
    ReentrantLock 读读互斥,读写互斥,写写互斥
    ReentrantReadWriteLock 读读不互斥,读写互斥,写写互斥
    StampedLock 读读不互斥,读写不互斥,写写互斥

    可以看到,从ReentrantLock到StampedLock,并发度依次提高。
    另一方面,因为ReentrantReadWriteLock采用的是“悲观读”的策略,当第一个读线程拿到锁之后,第二个、第三个读线程还可以拿到锁,使得写线程一直拿不到锁,可能导致写线程“饿死”。虽然在其公平或非公平的实现中,都尽量避免这种情形,但还有可能发生。
           StampedLock引入了“乐观读”策略,读的时候不加读锁,读出来发现数据被修改了,再升级为“悲观读”,相当于降低了“读”的地位,把抢锁的天平往“写”的一方倾斜了一下,避免写线程被饿死。

    相关文章

      网友评论

          本文标题:Java并发编程之显式锁

          本文链接:https://www.haomeiwen.com/subject/jclcpltx.html