JUC AQS ReentrantReadWriteLock 读写锁

为什么标题存在 AQS?

因为在ReentrantReadWriteLock 底层也使用到了 AQS 这个同步器框架;

在这里插入图片描述

abstract static class Sync extends AbstractQueuedSynchronizer {
}

ReentrantReadWriteLock 读写锁

字面意思上来看,这是一个支持重入的读写锁;
在一些业务流程中,读的操作是大于写的操作的,因此设计这种读写锁,可以在一定程度上面提高程序运行的性能;

下面提供一个数据容器类,分别使用读锁以及写锁进行数据的操作;读数据使用 read() 方法;写锁保护数据的 write() 方法;

所谓的互斥与不互斥的理解:
互斥:多个线程之间不可以同时访问一个数据
不互斥:多个线程之间可以同时访问一个数据
// 读 读之间是不互斥的 两个线程之间可以同时访问;
// 读写之间是互斥的,就是两个线程不能同时访问;
// 写 写 之间也是互斥的
// 只有有写的线程,那么线程之间就要互斥
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReaderWriteLock {
    public static void main(String[] args) {
        DataContainer dataContainer = new DataContainer();

        new Thread(()->{
            dataContainer.read();
        },"t1").start();

        new Thread(()->{
            dataContainer.read();
        },"t2").start();
    }
}

@Slf4j(topic = "c.DataContainer")
class DataContainer {
    private Object data;

    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    public Object read() {
        log.debug("获取读锁");
        r.lock();
        try {
            log.debug("读取...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return data;
        } finally {
            log.debug("释放读锁");
            r.unlock();
        }
    }

    public void write() {
        log.debug("获取写锁");
        w.lock();
        try {
            log.debug("写数据");
        } finally {
            log.debug("释放写锁");

            w.unlock();
        }
    }
}

读写锁需要注意的地方

1、同一个线程,先获取到了读锁,然后想要获取写锁,是不支持的,会进入永久的等待中;不可以升级;
2、同一个线程,先获取到了写锁,然后获取读锁是可以的;可以降级;

源代码解读,关于 读锁以及写锁之间能不能升降级,怎么升降级的实现;
在这里插入图片描述

读写锁的应用

读写锁可以应用到数据库缓存的实现上,保证缓存与数据库之间的一致性;

在实际的业务场景中,因为如果每次都是同样的 sql ,并且执行的结果是一样的,每次都查数据库效率是比较低的,可以考虑使用缓存机制,提升查询效率;

使用缓存的时候,需要考虑数据的一致性以及性能的优良性;
不使用不使用读写锁实现的缓存,存在一定的缺陷,所以使用具有读写锁的方式,进行数据库缓存的实现;

注:只是数据库缓存实现的 demo ,不是真正的数据库缓存技术;


不使用读写锁的时候
先清理缓存:在这里插入图片描述先更新数据库:
在这里插入图片描述


上面的缓存策略存在的问题:
1、数据不一致
2、加锁可以保证数据的一致性但是对于性能的损耗太大了

使用读写锁解决上面的问题,可以保证缓存与数据库数据的一致性,也可以保证性能不会损失太多;

读写锁原理

读锁写锁的加锁原理

读写锁使用的是同一个 Sync 同步器,所以等待队列、state 也是同一个;

下面的实例中: t1 添加的是写锁, t2 添加的是读锁,进行分析;
state 分为两部分,一部分给读锁用, 一部分给写锁用;

在这里插入图片描述在这里插入图片描述
加的结点是 SHARED 结点,不是之前的EXCLUSIVE 结点
在这里插入图片描述
加入到队列中还是会再尝试一次,再次失败才会 park 进入阻塞;
在这里插入图片描述
再次新加进来两个线程,读锁使用的是 SHARED 状态,写锁使用的是EXCLUSIVE 状态,两者之间是存在区别的;

前面三个的 WaitStatus 是 -1 后面的 WaitStatus 是 0 ,需要加以区别;
-1 代表有职责唤醒它的后继节点
在这里插入图片描述


写锁的解锁原理

上面的是加锁的原理,下面展示的解锁的原理:
在这里插入图片描述在这里插入图片描述在这里插入图片描述因为读锁是可以共享的,所以可以出现 t2 t3 同时拿到一把锁的情况;
在这里插入图片描述两个读锁同时出来,state 里面的前面是 2 ,表示两个读了两次,读锁占用的时候,是不能添加写的;
在这里插入图片描述一个读节点,会将后面的读节点都唤醒,直到遇到写的结点了;


读锁的释放过程

在这里插入图片描述-1 改为 0 ,为了唤醒后面的写结点
在这里插入图片描述

StampedLock 优化读功能

读写并发的情况下面,会维护 state 的状态,不能使得读达到极致,所以产生了 StampedLock 进一步的优化读功能;
在这里插入图片描述在刚开始的时候不加锁,发现自己的数据被其他线程影响了之后,才会进行锁的升级;判断数据是不是被影响了是使用 stamp 这个戳来判断的; stamp 变了之后,就会进行锁的升级;

先乐观的不加锁进行读取,之后发现戳变了之后,锁升级,变为读锁;

Semaphere

信号量,用来限制能够同时访问资源的线程上限;

注意:控制的是线程数量的上限;

@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        // 创建线程执行

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.debug("running...");

                try {
                    Thread.sleep(1000);
                    log.debug("end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}
// 达到了在一定时间中,限制线程的连接数量,可以减少峰值使用

21:37:28.338 c.TestSemaphore [Thread-2] - running...
21:37:28.338 c.TestSemaphore [Thread-0] - running...
21:37:28.338 c.TestSemaphore [Thread-1] - running...
21:37:29.339 c.TestSemaphore [Thread-1] - end...
21:37:29.339 c.TestSemaphore [Thread-0] - end...
21:37:29.339 c.TestSemaphore [Thread-3] - running...
21:37:29.339 c.TestSemaphore [Thread-4] - running...
21:37:29.344 c.TestSemaphore [Thread-2] - end...
21:37:29.344 c.TestSemaphore [Thread-5] - running...
21:37:30.340 c.TestSemaphore [Thread-4] - end...
21:37:30.340 c.TestSemaphore [Thread-3] - end...
21:37:30.340 c.TestSemaphore [Thread-6] - running...
21:37:30.340 c.TestSemaphore [Thread-7] - running...
21:37:30.348 c.TestSemaphore [Thread-5] - end...
21:37:30.348 c.TestSemaphore [Thread-8] - running...
21:37:31.342 c.TestSemaphore [Thread-7] - end...
21:37:31.342 c.TestSemaphore [Thread-9] - running...
21:37:31.344 c.TestSemaphore [Thread-6] - end...
21:37:31.352 c.TestSemaphore [Thread-8] - end...
21:37:32.347 c.TestSemaphore [Thread-9] - end...

Semaphore 应用

限制的是线程数量,不是限制的资源数量;

数据库连接时可以使用 Semaphore 进行优化;

Semaphore 原理

在这里插入图片描述在这里插入图片描述在这里插入图片描述

CountDownLatch

用来协作线程同步,主线程等待所有线程完成之后,主线程才会继续执行;

使用方式:
使用构造参数初始化等待数值,
await() 用来等待计数归零;
countDown() 用来让计数减去 1 的操作;

join 与 CountLatch

join 是一种比较底层的 方式,使用起来比较繁琐,推荐使用 CountLatch
CountLatch 是一种高级的 API

一把使用的线程池,

CountLatch 的应用

在多人在线游戏中,需要等待所有玩家都加入到游戏中,游戏才可以开始,可以使用线程池与 CountLatch 结合使用,主线程等待所有线程都 countDown() 等到计数数值为 0 的时候,主线程不用等待了,开始执行;

CountLatch功能:主线程可以等待线程池中的线程完成操作之后,主线程继续执行;

线程之间没有返回结果的时候,使用 CountLatch 比较方便,但是存在线程之间有返回结果的时候,需要使用 Future 进行线程结果的获取;

在这里插入图片描述

CyclicBarrier

对于 CountDownLatch 的一种优化,不用多次的重复创建出来 CountDownLatch对象;
在这里插入图片描述

注意:线程池中的线程数量和循环栅栏 CyclicBarrier 里面的设置的初始值是需要一致的,否则可能导致执行结果的不正确性;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值