面试加油站——第四期

ReentrantReadWriteLock和StampedLock

一、ReentrantReadWriteLock可重入读写锁

1、ReentrantReadWriteLock是什么?

一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;

其实是2把锁,它允许“读读”共享,但是“读写/写写”是互斥的(排他);

2、为什么会出现ReentrantReadWriteLock?

在一些业务场景中,大部分是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性,如果在读数据很频繁情况下,依然使用独占式锁,会大大降低性能。

所以在这种读多写少的情况下,Java提供另一个lock接口的实现子类ReentrantReadWriteLock(可重入读写锁)。

读写锁允许同一时刻被多个读线程访问,但是只要有写线程访问时,所有的读线程与其他的写线程均会阻塞。

3、ReentrantReadWriteLock有哪些特点?

  • 可重入性:

    可重入就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取(即自己可再次进入获取锁);

  • 读写分离:

    线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。

  • 允许锁降级(写权限高于读权限):

    如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

    45.jpg

锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性;

怎么理解呢?

比如我一个线程获取到“写锁”了,这个时候我写完数据,我可以直接获取到此资源的“读锁”,从而判断我数据已经修改成功了;

  • 不可锁升级

    很简单,因为“读权限”小于“写权限”,所以无法直接从“读锁”升级为“写锁”!


二、Demo演示读写锁的锁升级过程

1、先占“读锁”,后占“写锁”

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        //默认为false非公平锁,非公平锁效率高于公平锁
        //区别:公平锁要索淼队列,然后站到最后,唤醒最前;而非公平锁,直接就过来抢;
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(false);
        
        //先占读锁,后写锁,被阻塞,因为“读锁”无法升级为写锁
        rwLock.readLock().lock();
        System.out.println("我获取到读锁了");
        rwLock.writeLock().lock();
        System.out.println("我获取到写锁了");
    }
}

测试结果:

46.jpg

“读锁”无法升级为“写锁”,被阻塞!

2、先占“写锁”,后占“读锁”

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        //默认为false非公平锁,非公平锁效率高于公平锁
        //区别:公平锁要索淼队列,然后站到最后,唤醒最前;而非公平锁,直接就过来抢;
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(false);

        //先占写锁,后读锁,畅通,因为“写锁”可以降级为“读锁”
        rwLock.writeLock().lock();
        System.out.println("我获取到写锁了");
        rwLock.readLock().lock();
        System.out.println("我获取到读锁了");
        
    }
}

测试结果:

47.jpg

“写锁”可以顺利降级为“读锁”,线程不阻塞!


三、StampedLock——比ReentrantReadWriteLock更快的锁

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化

1、为什么会出现StampedLock?

ReentrantReadWriteLock已经对传统的锁进行了优化,实现了读写分类、锁降级等优化,但是还是会存在一定的问题:

  • “写锁”为“独占锁”,想要获取,必须其它锁都不存在了才可以,无论是“读锁”还是“写锁”,这是一个“悲观锁”;

  • 而且,当“读操作很多,写操作很少”时,线程有可能会遭遇“锁饥饿”问题;

锁饥饿:

ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,

假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了 

因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(╥﹏╥)o

缓解方案:

使用“公平”策略,即new ReentrantReadWriteLock(true)创建公平的“可重入读写锁”,大家FIFO,但是“公平”的代价是“牺牲系统吞吐量”!

2、StampedLock闪亮登场

StampedLock控制锁主要有三种模式(读锁、写锁、乐观读)

stampedLock.readLock();   //类似于原读锁
stampedLock.writeLock();  //类似于原写锁
stampedLock.tryOptimisticRead();   //乐观读,我觉得,我读取的时候,数据肯定不会被修改

StampedLock采取乐观获取读锁后,即使再有其他线程尝试获取写锁时也不会被阻塞,这其实是对读锁的优化

但是,为了准确,在获取乐观读锁结果后,还需要对结果进行校验;


四、代码验证StampedLock的乐观读模式

1、先验证“传统悲观读”

——读操作需耗时4s,我们让“写操作”在“读操作”开始后的1s内执行

public class StampedLockDemo {
    int number = 37;
    StampedLock stampedLock = new StampedLock();

    //1、先实现一个悲观读锁————“读锁”未释放前,“写锁”是不能介入的,只能被阻塞
    private void read(){
        long stamp = stampedLock.readLock();   //传统的悲观读模式
        System.out.println(Thread.currentThread().getName()+"\t 进入到悲观读代码块,共计需要4秒钟读完");
        for (int i = 1; i <=4; i++) {
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中..."+i);
        }
        try
        {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t 获得变量值result: "+result);
            System.out.println("写线程没有修改,不可介入。因为stampedLock.readLock(),不可以写入,读写互斥");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }

    //普通写
    private void write(){
        long stamp = stampedLock.writeLock();   //普通写锁
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 写线程开始修改");
            number = number + 13;
            System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> {
            resource.read();   //先悲观读
        }, "readthread").start();
        //暂停1秒钟线程,然后让写锁参与进来修改数据
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() ->{
            resource.write();   //“写操作”在“读操作”开始后的1秒钟,被启动
        },"writethread").start();
    }
}

执行结果:

48.jpg

结论:

写操作进入时,尝试获取“写锁”,但是被“悲观读锁”阻塞在外,只有等“悲观读锁”完全结束,释放锁之后,“写锁”才被获取到,执行写操作;

2、再验证“尝试乐观读”

public class StampedLockDemo {
    int number = 37;
    StampedLock stampedLock = new StampedLock();

    //尝试乐观读————“读锁”未释放前,如果有“写锁”想进入,也是可以的,直接进行修改操作,不需要被阻塞
    private void tryOptimisticRead(){
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //间隔4秒钟,我们很乐观的认为没有其他修改来修改,标志位不动,美好设想。
        System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 1; i <=4; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t正在读取中..."+i+"stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        }
        if (!stampedLock.validate(stamp)){   //需要验证一下,是否被串改
            System.out.println("-----有人动过数据了,只能重新来一次");
            stamp = stampedLock.readLock();
            result = number;
            System.out.println("重新读取一次最新值result: "+result);
            stampedLock.unlockRead(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t final result: "+result);
    }


    //普通写
    private void write(){
        long stamp = stampedLock.writeLock();   //普通写锁
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 写线程开始修改");
            number = number + 13;
            System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> {
            resource.tryOptimisticRead();
        }, "readthread").start();
        //暂停1秒钟线程,然后让写锁参与进来修改数据
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() ->{
            resource.write();   //“写操作”在“读操作”开始后的1秒钟,被启动
        },"writethread").start();
    }
}

执行结果:

49.jpg

结论:

即使“读锁”已经被占用,有“读操作”存在,但是由于是“乐观读”,有“写操作”需要获取“写锁”的时候,依然可以进入获取,并顺利修改了数据;

但是当“读锁”持有者真正准备读取数据的时候,先检验手上的stamp对应的锁是否被篡改,说过为false,则已经被篡改,则从新使用“传统readLock”读取一次;

——这种性能高的原因是,被篡改的可能性很小,毕竟,写操作很少;而当有“写操作”需要获取“写锁”时,也不需要阻塞,而是直接获取!

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐