ReadWriteLock和StampedLock

线程的安全问题都是因为共享且可变的对象被多个线程访问引起的,所以我们必须通过加锁来保证数据的一致性。

在实际的开发过程中,大部分的操作都是读操作,只有少部分是写操作,就算是少量的写操作也会带来线程不安全问题。

synchronized和ReentrantLock都是一种标准的互斥锁,每次都只能一个线程持有锁,但是这种加锁方式并不那么友好。

在读明显多于写的场景中放宽加锁的限制,只要保证每个线程都能取到最新的数据,并且在读取数据时不会有其他的线程修改数据,那么就不会发生数据不一致问题。因此伟大的Doug Lea在JDK1.5版本引入了读写锁类。

一个共享资源可以被多个读操作访问或者被一个写操作访问时,但是两者不同时进行此时就可以考虑使用读写锁。

今天勾勾就和大家一起了解下读写锁。

目录

ReadWriteLock读写锁用法

StampedLock读写锁用法

总结


ReadWriteLock读写锁用法

 

ReadWriteLock接口定义了两个Lock对象,其中一个用于读操作,一个用于写操作。

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

在读写锁的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作,即读读不冲突,读写、写写冲突。

ReentrantReadWriteLock实现了ReadWriteLock接口,为这两种锁都提供了可重入的加锁语义。与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平锁还是一个公平的锁,默认是非公平锁。

ReentrantReadWriteLock的构造函数:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    public ReentrantReadWriteLock() {
        this(false);
    }  
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
}

ReentrantReadWriteLock类派生出了ReadLock和WriteLock分别表示读锁和写锁。

ReadLock的加锁方法是基于AQS同步器的共享模式。

public void lock() {
    sync.acquireShared(1);
}

WriteLock的加锁方法是基于AQS同步器的独占模式。

public void lock() {
    sync.acquire(1);
}

我们用读写锁构建线程安全的Map来熟悉它的用法:

//新建ReentrantReadWriteLock对象
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//派生读锁和写锁
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
//共享资源
private final Map<String, String> map = new HashMap<>();

public void put(String k, String v) {
    writeLock.lock();
    try {
        map.put(k, v);
    }finally {
        writeLock.unlock();
    }
}

public String get(String k) {
    readLock.lock();
    try {
        return map.get(k);
    }finally {
        readLock.unlock();
    }
}

从上面代码我们可能觉着读写控制分开了,提高了读的共享控制是不是读写锁的性能要比独占锁要高呢?

但是从一些性能测试上来说,读写锁的性能并不好,而且使用不当还会引起饥饿写的问题。

饥饿写即在使用读写锁的时候,读线程的数量要远远大于写线程的数量,导致锁长期被读线程持有,写线程无法获取写操作权限而进入饥饿状态。

因此JDK1.8引入了StampedLock。

 

StampedLock读写锁用法

 

StampedLock在获取锁的时候会返回一个long型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为0则表示锁获取失败。

StampedLock是不可重入的,即使当前线程持有了锁再次获取锁还是会返回一个新的数据戳,所以要注意锁的释放参数,使用不小心可能会导致死锁。

StampedLock几乎具备了ReentrantReadWriteLock和ReentrantLock的功能,我们将上面的测试代码替换为StampedLock来熟悉下它的用法。

 //共享资源
    private final Map<String, String> map = new HashMap<>();
    //构建StampedLock锁
    private final StampedLock lock = new StampedLock();

    public void put(String k, String v) {
        long stamp = lock.writeLock();
        try {
            map.put(k, v);
        }finally {
            lock.unlockWrite(stamp);
        }
    }

    public String get(String k) {
        long stamp = lock.readLock();
        try {
            return map.get(k);
        }finally {
            lock.unlockRead(stamp);
        }
    }

StampedLock还提供了乐观读模式,使用tryOptimisticRead()方法获取一个非排他锁并且不会进入阻塞状态。

该方法会返回long型的数据戳,数据戳非0表示获取锁成功,如果为0表示获取锁失败。

我们把上面测试用例的读操作换成乐观读模式。

public String optimisticGet(String k) {
    //尝试获取读锁,返回获取的结果,线程不会进入阻塞
    long stamp = lock.tryOptimisticRead();
    //对上一步获取的结果进行验证
    //如果验证失败,则此时可能其他线程加了写锁,那么此时线程通过加读锁进入阻塞状态直到获取到读锁
    //如果验证成功,不进行任何加锁操作直接返回共享数据,这样的话就实现了无锁读的操作,提高了读访问性能。
    if (!lock.validate(stamp)) {
        stamp = lock.readLock();
        try {
            return map.get(k);
        }finally {
            lock.unlockRead(stamp);
        }
    }
    return map.get(k);                
}

 

总结

 

学到现在,勾勾已经掌握了好几种锁,之前就会一种的时候还好,用它就行,学的多了就不知道怎么选择了,结合知识的掌握和平时的开发实战,勾勾简单说下自己的理解。

synchronized和ReentrantLock是互斥锁只允许一个线程获取锁,如果是读写无法隔离的业务场景那就只能选择互斥锁了。如果读写能隔离但是读和写的线程数量不能明确,那就互斥锁吧。

勾勾在开发中用的最多的是互斥锁,读写锁至今还没有场景使用,但是多掌握一个知识准没错。万一以后的哪一天遇到了读写锁的场景我们还能找到一条好走的路。

那么什么时候选择synchronized,什么时候选择ReentrantLock呢?

如果你用到了ReentrantLock的高级性能,那么没办法只能ReentrantLock了。

如果不是高级性能可以优先选择synchronized,毕竟不用显示的加锁和解锁。但是我们需要考虑synchronized锁的升级不可逆的特点,如果线程并发存在比较明显的峰谷,则可以考虑选用ReentrantLock,毕竟重量级锁的性能确实不怎么好。

StampedLock的性能明显优于ReentrantReadWriteLock,且它还提供了乐观读的模式,优化了读操作的性能。StampedLock还提供了ReentrantLock的加锁解锁方式,因此如果遇到读写锁的场景那么可以考虑优先选择StampedLock。

2020年学习了很多知识但是发现自己懂的更少了,Java是一门神奇的语言,我用了这么多年却还是只知皮毛,前路漫漫,少年还需努力!!

我是勾勾,一直在努力的程序媛!感谢您的点赞、转发和关注!

参考资料:

《Java高并发编程详解》

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值