面试系列之-读写锁(JAVA基础)

2023-09-11 15:55:09 浏览数 (1)

读写锁的内部包含两把锁:一把是读(操作)锁,是一种共享锁;另一把是写(操作)锁,是一种独占锁。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是具有排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;进一步来说,如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。

读写锁的读写操作之间的互斥原则具体如下:

  • 读操作、读操作能共存,是相容的。
  • 读操作、写操作不能共存,是互斥的。
  • 写操作、写操作不能共存,是互斥的。

与单一的互斥锁相比,组合起来的读写锁允许对于共享数据进行更大程度的并发操作,虽然每次只能有一个写线程,但是同时可以有多个线程并发地读数据,读写锁适用于读多写少的并发情况。

代码语言:javascript复制
public interface ReadWriteLock {
    /**
    * 返回读锁
    */
    Lock readLock();
    /**
    * 返回写锁
    */
    Lock writeLock();
}
读写锁ReentrantReadWriteLock

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

读写锁升级与降级

锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。ReentrantReadWriteLock不支持读锁的升级,主要是避免死锁,例如两个线程A和B都占了读锁并且都需要升级成写锁,A升级要求B释放读锁,B升级要求A释放读锁,二者就会由于相互等待形成死锁。

代码语言:javascript复制
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 Object put(String key, String value){
        WRITE_LOCK.lock();
        try{
            Print.tco(DateUtil.getNowTime()
                        " 抢占了WRITE_LOCK,开始执行write操作");
            Thread.sleep(1000);
            String put = MAP.put(key, value);
            Print.tco( "尝试降级写锁为读锁");
            //写锁降级为读锁(成功)
            READ_LOCK.lock();
            Print.tco( "写锁降级为读锁成功");
            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);
            Print.tco( "尝试升级读锁为写锁");
            //读锁升级为写锁(失败)
            WRITE_LOCK.lock();
            Print.tco("读锁升级为写锁成功");
            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();
    }
}
StampedLock

StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。StampedLock的三种模式如下:

  1. 悲观读锁:与ReadWriteLock读锁类似,多个线程可同时获取悲观读锁,悲观读锁是一个共享锁。
  2. 乐观读锁:相当于直接操作数据,不加任何锁,连读锁都不要。
  3. 写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据。

StampedLock与ReentrantReadWriteLock语义类似,不同的是,StampedLock并没有实现ReadWriteLock接口,而是定义了自己的锁操作API。

代码语言:javascript复制
public class StampedLockTest{
    //创建一个Map,代表共享数据
    final static Map<String, String> MAP = new HashMap<String, String>();
    //创建一个印戳锁
    final static StampedLock STAMPED_LOCK = new StampedLock();
    //对共享数据的写操作
    public static Object put(String key, String value){
        long stamp = STAMPED_LOCK.writeLock(); //尝试获取写锁的印戳
        try{
            Print.tco(getNowTime()   " 抢占了WRITE_LOCK,开始执行write操作");
            Thread.sleep(1000);
            String put = MAP.put(key, value);
            return put;
        } catch (Exception e){
            e.printStackTrace();
        } finally{
            Print.tco(getNowTime()   " 释放了WRITE_LOCK");
            STAMPED_LOCK.unlockWrite(stamp); //释放写锁
        }
        return null;
    }
    //对共享数据的悲观读操作
    public static Object pessimisticRead(String key){
        Print.tco(getNowTime()   "LOCK进入过写模式,只能悲观读");
        //进入了写锁模式,只能获取悲观读锁
        long stamp = STAMPED_LOCK.readLock(); //尝试获取读锁的印戳
        try{
            //成功获取到读锁,并重新获取最新的变量值
            Print.tco(getNowTime()   " 抢占了READ_LOCK");
            String value = MAP.get(key);
            return value;
        } finally{
            Print.tco(getNowTime()   " 释放了READ_LOCK");
            STAMPED_LOCK.unlockRead(stamp); //释放读锁
        }
    }
    //对共享数据的乐观读操作
    public static Object optimisticRead(String key){
        String value = null;
        //尝试进行乐观读
        long stamp = STAMPED_LOCK.tryOptimisticRead();
        if (0 != stamp){
            Print.tco(getNowTime()   "乐观读的印戳值,获取成功");
            sleepSeconds(1); //模拟耗费时间1秒
            value = MAP.get(key);
        } else // 0 == stamp 表示当前为写锁模式 {
            Print.tco(getNowTime()   "乐观读的印戳值,获取失败");
            //LOCK已经进入写模式,使用悲观读方法
            return pessimisticRead(key);
        }
        //乐观读操作已经间隔了一段时间,期间可能发生写入
        //所以,需要验证乐观读的印戳值是否有效,即判断LOCK是否进入过写模式
        if (!STAMPED_LOCK.validate(stamp)){
            //乐观读的印戳值无效,表明写锁被占用过
            Print.tco(getNowTime()   " 乐观读的印戳值,已经过期");
            //写锁已经被抢占,进入了写锁模式,只能通过悲观读锁再一次读取最新值
            return pessimisticRead(key);
        } else {
            //乐观读的印戳值有效,表明写锁没有被占用过
            //不用加悲观读锁而直接读,减少了读锁的开销
            Print.tco(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();
    }
}

0 人点赞