第十四章: ReentrantLock、ReentrantReadWriteLock、StampedLock

相关面试题

  • 你说你用过读写锁,锁饥饿问题是什么?
  • 有没有比读写锁更快的锁?
  • StampedLock知道吗?(邮戳锁/票据锁)
  • ReentrantReadWriteLock有锁降级机制策略你知道吗?

锁的演变

无锁 ——> 独占锁 ——> 读写锁 ——> 邮戳锁

无锁: 不用多说,存在线程安全问题

独占锁: ==(synchronized,ReentrantLock)==同一时刻只能有一个线程访问,在读多写少的场景下,显然效率并不高。

读写锁:ReentrantReadWriteLock , 适应在多读写少的场景下,读锁可以被线程共享(共享锁),写锁只能有一个线程获取(排它锁)。读的时候不允许写,写的时候不允许读。

读写锁缺点

  • 写锁饥饿问题
  • 锁降级问题

邮戳锁StampedLock 由于读写锁写锁饥饿问题,出现了邮戳锁。下面详细讲解…

ReentrantReadWriteLock

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

想象成菜刀,刀刃切菜,刀背拍蒜,但是你不能同时拍蒜又切菜

image-20221207214625614

读写锁特点

  • 读写锁并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。

  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁(切菜还是拍蒜选一个)。

  • 也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。

只有在读多写少情境之下,读写锁才具有较高的性能体现

ReentrantReadWriteLock 架构图

实现了ReadWriteLock 接口,其中俩个方法,readLock获取读锁,writeLock 获取写锁

image-20221207220748126

代码演示

模仿多个线程进行读写的场景。演示独占锁、读写锁的区别

使用 ReentrantLock 独占锁

public class ReentrantReadWriteLockTest {
    public static void main(String[] args) {
        MapResource mapResource = new MapResource();
        for (int i = 1; i <= 10; i++) {
            //  lambda 表达式使用变量应为 final 类型
            final int finalI = i;
            new Thread(() -> {
                mapResource.put(finalI,finalI);
            },String.valueOf(i)).start();

            new Thread(() -> {
                mapResource.get(finalI);
            },String.valueOf(i)).start();
        }
    }
}

class MapResource {
    private  volatile Map<Integer,Integer> map = new HashMap<>();
    // 独占锁==synchronized
     ReentrantLock lock = new ReentrantLock();
    // 读写锁
     ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 写操作
    public  void put(Integer key,Integer value) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            TimeUnit.MILLISECONDS.sleep(200);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    // 读操作
    public  Integer get(Integer key) {
        lock.lock();
        Integer result = null ;
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            TimeUnit.MILLISECONDS.sleep(100);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读操作完成, 读取的值为: " + result);
            return  result ;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return  null;
        }finally {
            lock.unlock();
        }
    }
}

输出结果

无论是读还是写,都只允许一个线程进入。都是成双成对的出现。这样不仅效率降低 ,并且读操作没有达到共享

比如:电影院看电影,看电影相当于读操作,我们不能每一个电影只有一个人看,而是多个顾客都能够看,因此也就引入了读写锁

image-20221207222232228

演示读写锁

public class ReentrantReadWriteLockTest {
    public static void main(String[] args) {
        MapResource mapResource = new MapResource();
        for (int i = 1; i <= 10; i++) {
            //  lambda 表达式使用变量应为 final 类型
            final int finalI = i;
            new Thread(() -> {
                mapResource.put(finalI,finalI);
            },String.valueOf(i)).start();

            new Thread(() -> {
                mapResource.get(finalI);
            },String.valueOf(i)).start();
        }
    }
}

class MapResource {
    private  volatile Map<Integer,Integer> map = new HashMap<>();
    // 独占锁==synchronized
     ReentrantLock lock = new ReentrantLock();
    // 读写锁
     ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 写操作
    public  void put(Integer key,Integer value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            TimeUnit.MILLISECONDS.sleep(500);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 读操作
    public  Integer get(Integer key) {
        readWriteLock.readLock().lock();
        try {
            Integer result = null ;
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            TimeUnit.MILLISECONDS.sleep(100);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读操作完成, 读取的值为: " + result);
            return  result ;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return  null;
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}

输出结果

使用读写锁之后,读操作允许多个线程同时进入。

image-20221207222822207

注意点:在读操作没有完成之前,写锁无法获得。

新增加三个线程执行写操作,并延迟读操作的完成时间.

image-20221207225118676

image-20221207225135783

输出结果

只有当读操作完成后,写锁才会被获得。

在这里插入图片描述

锁降级

将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

image-20221208192236151

写锁的降级,降级成为了读锁

  • 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
  • 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
  • 如果释放了写锁,那么就完全转换为读锁。

获取写锁 ——> 获取读锁 ——> 释放写锁 ——> 释放读锁

案例演示一

演示锁降级: 写锁——> 读锁

main线程在获取写锁的同时,也能够获取读锁。

public class ReadWriteLockTest2 {
    public static void main(String[] args) {
        
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        writeLock.lock();
        System.out.println("写锁...");

        readLock.lock();
        System.out.println("读锁...");
        readLock.unlock();

        writeLock.unlock();
    }
}

结果

image-20221208202501179

案例演示二

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

public class ReadWriteLockTest2 {
    public static void main(String[] args) {

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();



        readLock.lock();
        System.out.println("读锁...");

        writeLock.lock();
        System.out.println("写锁...");
        
        readLock.unlock();
        writeLock.unlock();


    }
}

结果

写线程被阻塞

image-20221208202748092

总结

  • 线程获取读锁是不能直接升级为写入锁的。不可锁升级

image-20221208203039978

在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。
所以,需要释放所有读锁,才可获取写锁,

image-20221208203114064

即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,
也就是写入必须等待,这是一种悲观的读锁

为什么要有锁降级

锁降级 下面的示例代码摘自ReentrantWriteReadLock源码中:
ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。

image-20221208204622656

1 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。

2 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。

如果违背锁降级的步骤
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

如果遵循锁降级的步骤
线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。

邮戳锁 StampedLock

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。邮戳锁也叫票据锁。

邮戳锁解决了ReentrantReadWriteLock 的写锁饥饿问题。什么是写锁饥饿?

ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写.这就是写锁饥饿问题

如何缓解锁饥饿问题?

  • 使用公平锁,但是公平锁是以牺牲系统吞吐量为代价的。
  • 使用邮戳锁

ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统 的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发

StampedLock横空出世
ReentrantReadWriteLock 的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

邮戳锁的特点

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
  • 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的 Stamp一致;
  • StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁

邮戳锁的三种访问方式:

Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

官方描述

image-20221208211316754

案例演示一

使用传统读、写锁,演示: 读锁没有释放时,写锁无法获取

public class StampedLockTest {
    public static void main(String[] args) {
        MyResource myResource = new MyResource();
        new Thread(myResource::read, "readThread").start();
        try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
        new Thread(myResource::write, "writeThread").start();
    }
}

class MyResource {
    int num =  30 ;
    StampedLock stampedLock = new StampedLock();

    public void write() {

        long stamp = stampedLock.writeLock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            num = 30 + 20 ;
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } finally {
            // 通过邮戳释放锁
            stampedLock.unlock(stamp);
        }
    }

    public void read() {
        long stamp = stampedLock.readLock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            // 将读操作睡眠4s,为了演示出结果
            for (int i = 0; i < 4; i++) {
                System.out.println(Thread.currentThread().getName()  + " 正在读...");
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
            }
            System.out.println(Thread.currentThread().getName() + " 写操作完成....,读取的值: " + num);
        } finally {
            stampedLock.unlock(stamp);
        }

    }
}

结果

只有当读操作释放锁之后,写操作才会获取锁

image-20221208213703533

案例演示二

使用 StampedLock提供的乐观读 tryOptimisticRead , 此种方式允许在读的过程中获取写锁。

一般会通过 validate 方法判断 从获取 stamp 标记以来是否被其他线程获取,如果在读的时候有写操作介入,那么会返回 false,将乐观读 升级为 悲观读。

public class StampedLockTest {
    public static void main(String[] args) {
        MyResource myResource = new MyResource();
        new Thread(myResource::byTryOptimisticRead, "readThread").start();

        try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(() -> {
            System.out.println("writeThread want to come in ....");
            myResource.write();
        }, "writeThread").start();
    }
}

class MyResource {
    int num =  30 ;
    StampedLock stampedLock = new StampedLock();

    public void write() {

        long stamp = stampedLock.writeLock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行写操作....");
            num = 30 + 20 ;
            System.out.println(Thread.currentThread().getName() + " 写操作完成....");
        } finally {
            // 通过邮戳释放锁
            stampedLock.unlock(stamp);
        }
    }

    public void read() {
        long stamp = stampedLock.readLock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            // 将读操作睡眠4s,为了演示出结果
            for (int i = 0; i < 4; i++) {
                System.out.println(Thread.currentThread().getName()  + " 正在读...");
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
            }
            System.out.println(Thread.currentThread().getName() + " 写操作完成....,读取的值: " + num);
        } finally {
            stampedLock.unlock(stamp);
        }
    }

    // 乐观读
    public  void byTryOptimisticRead() {
        // 获取乐观读的邮戳
        long stamp = stampedLock.tryOptimisticRead();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在进行读操作....");
            for (int i = 0; i < 4; i++) {
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
                System.out.println(Thread.currentThread().getName()  + " 正在读..."+ "第 " + i + "秒, " + "stamp的状态: " + stampedLock.validate(stamp));
            }
            if (!stampedLock.validate(stamp)){
                // 返回 false 表示有线程获取锁,乐观锁 --> 悲观锁
                stamp = stampedLock.readLock();
                try {
                    System.out.println("乐观读 升级为 悲观读");
                    System.out.println("升级到悲观读,重新读取到的值: " + num);
                } finally {
                    stampedLock.unlock(stamp);
                }
            }

        } finally {
            System.out.println("最终读到的值: " + num);
        }
    }
}

结果:

首先读操作,沉睡4s,每过1s,都将 stamp 的状态输出。

当写操作沉睡2s中,stamp状态为true,没有线程获取写锁,唤醒后,stamp的状态为 false,表示写操作介入了,因此在 if 语句里 将乐观锁升级为了悲观锁,并重新读取值。

image-20221208220933525

StampedLock 缺点

  • StampedLock 不支持重入,没有Re开头
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
  • 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鲨瓜2号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值