十三、读锁和写锁

读读场景下不会出现线程安全问题,读写、写写场景下会出现线程安全问题。在没有写操作的时候,多个线程去读不会产生问题,但是当有一个线程想要去写操作时,就不应该有其他读线程和写线程。

读写锁顾名思义就是把一把锁分为读锁和写锁两部分,读锁允许多个线程共同获得,写锁只允许一个线程获得。

💡 问题:读操作为什么要加锁?

在读取数据的方法中如果要那这个数据做一些业务处理,此时没有加锁被其他线程给改变了,就会造成类似于“脏读”的现象。

(一)ReadWriteLock

内部维护了两个API规范,读锁和写锁。

(二)ReentrantReadWriteLock介绍

线程进入读锁的条件:

  • 没有其他线程的写锁

线程进入写锁的条件:

  • 没有其他线程的写锁
  • 没有其他线程的读锁

读写锁有以下三个重要特性:

  • 公平性选择:支持非公平锁和公平锁,默认非公平
  • 可重入:读锁和写锁都支持线程的重入,以读锁为例:读线程获取到锁后,能够再次获取读锁。写线程在获取写锁以后能够再次获取写锁以及读锁。
  • 可降级:遵循获取写锁,再获取读锁,最后再获取写锁的顺序,写锁能够降级为读锁。

总结:

当前线程如果获取到读锁,可以再次获取读锁。

当前线程如果获取到写锁,在写锁释放之前可以获取任意类型的锁。

2.1 内部结构

ReentrantReadWriteLock是可重入的读写搜实现类,内部维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。

2.2 如何使用

2.2.1 读锁的使用方式

注意:

  • 读锁不支持条件变量
  • 持有读锁的情况下不允许获取写锁(无法锁升级)
public static void main(String[] args) throws InterruptedException {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    CountDownLatch countDownLatch = new CountDownLatch(3);
    for (int i = 0; i < 3; i++) {
        //读锁线程
        new Thread(() -> {
            readLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到读锁");
                ThreadUtil.sleep(3000);
            }finally {
                readLock.unlock();
                countDownLatch.countDown();
            }
        },"【读】线程 " + i).start();
    }

    countDownLatch.await();
}

2.2.2 写锁的使用方式
public static void main(String[] args) throws InterruptedException {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    CountDownLatch countDownLatch = new CountDownLatch(3);
    for (int i = 0; i < 3; i++) {
        //写锁线程
        new Thread(() -> {
            writeLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到写锁");
                ThreadUtil.sleep(3000);
            }finally {
                writeLock.unlock();
                countDownLatch.countDown();
            }
        },"【写】线程 " + i).start();
    }
    countDownLatch.await();
}

2.3 锁降级

当前线程持有写锁的同时获取读锁,再把写锁给释放掉就成为锁的降级(又读锁转为写锁)

如果是当前线程持有写锁,又释放写锁后重新获取读锁则称为‘分段锁’

使用场景:

线程对同一个数据修改后立即进行读取,但是中间又不想被其他线程所干扰,就可以使用锁降级的特性。

例:方法中需要修改count变量的值,并且需要立即获取到修改后的值。

2.3.1 错误场景:使用分段锁
 private static volatile int count;

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        CountDownLatch countDownLatch = new CountDownLatch(2);
        for (int i = 0; i < 2; i++) {
            //场景1:先写锁再释放再读锁
            new Thread(() -> {
                writeLock.lock();
                try {
                    count ++;
                }finally {
                    writeLock.unlock();
                }

                ThreadUtil.sleep(2000);
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到的值:" + count);
                }finally {
                    readLock.unlock();
                }
            },"线程 " + i).start();
        }
        countDownLatch.await();
    }

2.3.2 正确场景:使用锁降级
 private static volatile int count;

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        CountDownLatch countDownLatch = new CountDownLatch(2);
        for (int i = 0; i < 2; i++) {
            //场景2:使用锁降级的方式
            new Thread(() -> {
                writeLock.lock();
                try {
                    count ++;
                    readLock.lock();
                    ThreadUtil.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + "获取到的值:" + count);
                }finally {
                    writeLock.unlock();
                    readLock.unlock();
                }
            },"线程 " + i).start();
        }
        countDownLatch.await();
    }


(三)StampedLock介绍

它不是基于AQS的锁,并且是不可重入的、不支持条件变量 Conditon

ReentrantReadWriteLock的读锁我们可以认为是一种悲观锁,因为它在有读锁的时候是不允许其他线程进行获取写锁的。

StampedLock的读锁我们可以认为是一种乐观锁,因为它在读取资源的时候不会加读锁,如果有其他线程进行修改操作会在访问资源后进行一次版本比较,如果版本不同则就是被修改过,此时要么加悲观读要么重试。

3.1 访问模式

  • 独占写锁:writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
  • 悲观读锁:writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
  • 乐观读锁:访问共享变量时不会进行加锁,

3.2 使用规范

  1. 获取当前版本号
  2. 读取资源
  3. 比对版本号
    1. 如果相同则没有被修改可以结束
    2. 如果不相同则资源被修改过要么加入悲观读锁重新获取,要么重试。
StampedLock lock = new StampedLock();
//获取版本号
long version = lock.tryOptimisticRead();
//比对版本号
if (!lock.validate(version)) {
    version = lock.readLock();
    try {
        System.out.println("资源被修改过,实际值:" + count);
    }finally {
        lock.unlock(version);
    }
}
  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值