【JUC】读写锁+邮戳锁

15 篇文章 1 订阅

关于锁的面试题

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

简单聊聊ReentrantReadWriteLock

ReadWriteLock和Lock一样,都是一个接口,ReentrantReadWriteLock是其 实现类

在这里插入图片描述

在这里插入图片描述

底层也是AQS

在这里插入图片描述

是什么?

读写锁定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程**(读写互斥、写写互斥、读读共享)**

  • 读写锁意义和特点
    • 实际上,大多场景是“读/读”,线程间不存在互斥(电影院里面多个人看一块屏幕),只有”读/写“线程或者”写/写“线程间的操作是需要互斥的,因此引入了 ReentrantReadWriteLock
    • 一个ReentrantReadWriteLock只能存在一个写锁,可以同时存在多个读锁,不能同时存在写锁和读锁。也即资源可以被同时多个读操作访问,或一个写操作访问,两者不能同时进行
    • 只有在读多写少的场景之下,读写锁才具有较高的性能体现

锁的演变

无锁无序->加锁->读写锁->邮戳锁

在这里插入图片描述

读写锁有两个缺点,因此才诞生了youchuosuo

  • 写锁饥饿问题:如有9999个读请求,一个写请求,因为读写互斥,可能出现写操作长期抢不到锁
  • 锁降级

读写锁案例

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 定义一个资源类,模拟简单的缓存
 */
class MyResource {
    Map<String, String> map = new HashMap<>();
    // =====ReentrantLock 等价于 =====synchronized,之前讲解过
    Lock lock = new ReentrantLock();
    // =====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
            map.put(key, value);
            // 暂停毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "完成写入");
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
            String result = map.get(key);
            // 暂停200毫秒
            // try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

            // 暂停2000毫秒,演示读锁没有完成之前,写锁无法获得
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "完成读取" + "\t" + result);
        } finally {
            rwLock.readLock().unlock();
        }
    }


}

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyResource myResource = new MyResource();

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI + "");
            }, String.valueOf(i)).start();
        }

        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 1; i <= 3; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, "新写锁线程->" + String.valueOf(i)).start();
        }
    }
}

读写锁特点

读写互斥,读读共享,读没有完成的时候其他线程无法获得写锁

在这里插入图片描述

锁降级

  • 锁降级:将写锁降级为读锁(类似Linux文件读写权限,写权限高于读权限,锁的严苛程度变强叫做升级,反之叫做降级)
  • 如果一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁
  • 在释放写锁那一步之后,完全转换为了读锁
  • 如果有线程在读,那么写线程是无法获取写锁的(悲观锁的策略)

在这里插入图片描述

案例演示

【写完马上去读:锁降级演示】

public static void main(String[] args) {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

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

    // 正常 A B两个线程
    // A
    /*readLock.lock();
    System.out.println("----读取");
    readLock.unlock();

    // B
    writeLock.lock();
    System.out.println("----写入");
    writeLock.unlock();*/


    // 本例,只有一个线程,即同一个线程
    writeLock.lock();
    System.out.println("----写入");

    readLock.lock();
    System.out.println("----读取");

    writeLock.unlock();
    readLock.unlock();
}

写后读

在这里插入图片描述

在这里插入图片描述

【读后写:锁升级失败】

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("----写入");

    writeLock.unlock();
    readLock.unlock();
}

读完之后,阻塞了,因为writeLock.lock()执行的时候,readLock.unlock()还没有释放锁,锁不能升级
在这里插入图片描述

在这里插入图片描述

这是一种悲观的读锁,读的时候,不让写锁进来。

【StampedLock的改进之处】

读的过程中也允许获取写锁介入(读和写两个操作也能“共享”(注意引号)),这样会导致我们读的数据就可能不一致,所以需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁。显然乐观锁的并发效率更高,但会有小概率的写入导致读取的数据不一致,需要能检测出来(邮戳锁帮我们搞定了检测这件事),再读一遍

为什么设计锁降级(源码分析)

在这里插入图片描述

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

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

总结:同一个线程自己持有写锁时再去拿读锁,其本质相当于重入

  • 如果违背锁降级的步骤:当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么线程C无法感知到数据已被修改,则数据出现错误,自己读出来的数据和自己写的数据不同
  • 如果遵循锁降级的步骤:线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的
  • 用 锁降级 替代 释放写锁再获取读锁,避免数据出错

邮戳锁StampedLock(比读写锁更快的锁)

邮戳锁是什么?

  • StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
  • 邮戳锁也称为票据锁
  • stamp(Long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。当释放锁或者转换锁的时候,都要传入最初获取的stamp值(类似于有一个流水号,加锁要用这个流水号,解锁也要用这个流水号)

它是由饥饿问题引出

  • 锁饥饿问题:
    • ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。——>引出一个问题,如何在读的时候也能写呢?
  • 如何解决锁饥饿问题:
    • 使用“公平”策略可以一定程度上缓解这个问题
    • 使用“公平”策略是以牺牲系统吞吐量为代价的
    • StampedLock类的乐观读锁方式:采取乐观获取读锁,其他线程尝试获取写锁时不会被阻塞,其实是对读锁的优化。在获取乐观读锁后,还需要对结果进行校验(很乐观,认为读的时候不会有写锁进来,就算有写锁进来,我读的时候进行对比,如果发现数据被改过,本次读作废,重新读一遍)

StampedLock的特点

  • 所有获取锁的方法,都返回一个邮戳,stamp为零表示失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳,这个stamp必须是和成功获取锁时得到的stamp一致
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话会造成死锁)
  • StampedLock有三种访问模式:
    • Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
    • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
    • Optimistic reading(乐观读模式):无锁机制,类似与数据库中的乐观锁,支持读写并发,很乐观认为读时没人修改,假如被修改在实现升级为悲观读模式
  • 一句话总结:读的过程中也允许写锁介入

乐观读模式Code演示

传统的读写锁模式----读的时候写锁不能获取

package com.bilibili.juc.rwlock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

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

    public void write() {
        // 返回一个戳记
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    /**
     * 悲观读,读没有完成时候写锁无法获得锁
     */
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + " come in readlock code block,4 seconds continue...");

        // 模拟读取4秒
        for (int i = 0; i < 4; i++) {
            // 暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取中......");
        }

        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + " 获得成员变量值result:" + result);
            System.out.println("写线程没有修改成功,加读锁时候,写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 读的过程中,开始写
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "----come in");
            resource.write();
        }, "writeThread").start();

        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t" + "number:" + number);
    }
}

在这里插入图片描述

乐观读模式----读的过程中也允许写锁介入

package com.bilibili.juc.rwlock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

/**
 * @auther zzyy
 * <p>
 * StampedLock = ReentrantReadWriteLock + 读的过程中也允许获取写锁介入
 */
public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        // 返回一个戳记
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    /**
     * 乐观读,读的过程中也允许获取写锁介入
     */
    public void tryOptimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        // 故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
        System.out.println("4秒前stampedLock.validate方法值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 0; 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();
            try {
                System.out.println("从乐观读 升级为 悲观读");
                result = number;
                System.out.println("重新悲观读后result:" + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + " finally value: " + result);
    }


    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.tryOptimisticRead();
        }, "readThread").start();

        //暂停2秒钟线程,读过程可以写介入,演示
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "----come in");
            resource.write();
        }, "writeThread").start();
    }
}

在这里插入图片描述

StampedLock的缺点

  • StampedLock不支持重入(没有Re开头)
  • StampedLock的悲观读锁和写锁都不支持条件变量,这个需要注意
  • 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法(会影响性能,或者产生意外的情况)

条件变量是什么?

条件变量是多线程编程中常用的一种同步机制,它允许线程等待某个条件成立后继续执行。条件变量通常与锁一起使用,以便在线程间同步共享资源的访问

条件变量的主要用途包括:
  1. 线程等待:当某个条件不满足时,线程会释放锁并进入等待状态,直到条件满足
  2. 线程唤醒:当某个线程改变了条件,并使得条件满足时,它可以唤醒一个或多个等待该条件的线程
Java 中条件变量的实现

在 Java 中,条件变量通常通过 ReentrantLock 类的 newCondition() 方法来创建。以下是一个简单的示例,展示如何使用条件变量来实现生产者-消费者模式:

案例

【缓冲区】

package com.dam.juc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author dam
 * @create 2024/8/14 17:25
 */
public class Buffer {
    private int data;
    private boolean empty = true;
    private final ReentrantLock lock = new ReentrantLock();
    // 条件变量
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    public void put(int value) {
        lock.lock();
        try {
            while (!empty) {
                notFull.await(); // 缓冲区已满,等待
            }
            data = value;
            empty = false;
            notEmpty.signal(); // 通知消费者缓冲区已非空
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public int take() {
        lock.lock();
        try {
            while (empty) {
                notEmpty.await(); // 缓冲区为空,等待
            }
            empty = true;
            notFull.signal(); // 通知生产者缓冲区已空
            return data;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
        return -1; // 应该不会到达这里
    }

}

在这里插入图片描述

【生产者】

package com.dam.juc;

/**
 * @Author dam
 * @create 2024/8/14 17:25
 */
class Producer implements Runnable {
    private final Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Produced: " + i);
            buffer.put(i);
        }
    }
}

【消费者】

package com.dam.juc;

/**
 * @Author dam
 * @create 2024/8/14 17:25
 */
class Consumer implements Runnable {
    private final Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            int data = buffer.take();
            System.out.println("Consumed: " + data);
        }
    }
}

【测试】

package com.dam.juc;

/**
 * @Author dam
 * @create 2024/8/14 17:26
 */
public class BufferTest {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        Thread producer = new Thread(new Producer(buffer));
        Thread consumer = new Thread(new Consumer(buffer));

        producer.start();
        consumer.start();

        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

文章说明

该文章是本人学习 尚硅谷 的学习笔记,文章中大部分内容来源于 尚硅谷 的视频尚硅谷JUC并发编程(对标阿里P6-P7),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 尚硅谷 的优质课程表示感谢。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Hello Dam

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

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

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

打赏作者

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

抵扣说明:

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

余额充值