JAVA多线程之读写锁

生产案例

最近在做一个功能的时候,需要用到MCC码。然后看了一下前人写的代码,发现是用的读写锁ReadWriteLock去做的定时刷新。

读写锁原理

ReadWriteLock接口表示存在2把锁:一个读取锁,一个写入锁。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

基于AQS实现的ReentrantReadWriteLock中,单个AQS子类将同时管理读取加锁与写入加锁。ReentrantReadWriteLock使用了一个16位的状态表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数。在读取锁上的操作使用共享的获取方法与释放方法,在写入锁的操作使用独占的方法与释放方法。

AQS内部维护了一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。在ReentrantReadWriteLock中,当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会得到这个锁,如果位于队列头部的线程执行读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。

类图

从上面的类图可以发现,ReentrantReadWriteLock是基于AQS去实现的。里面读取锁是通过ReadLock控制的,写入锁是WriteLock控制的。

其中的Sync不且复用了AQS的能力,并且可以自主选择是否为公平锁:NonfairSync,FairSync

使用方式

以刚才的MCC为例,要完成类似以上功能需求,黑名单控制:

  • 根据用户手机号判断是否为黑名单,如果是则拒绝,反之则放过
  • 每天晚上定时更新一次黑名单列表

假设上述需求用读写锁去做,要怎么实现呢?

我们实现一个基本的,可能代码结构不一定标准,只是为说明使用方式

基础使用

public class ReadWriteLockTest {

    /**
     * 读锁测试
     */
    @Test
    public void testReadLock() {
        // 读写锁,控制并发
        ReadWriteLock lock = new ReentrantReadWriteLock();
        try {
            // 获取读锁
            lock.readLock().lock();

            System.out.println("check result is " + isBlackNum("13699001634"));
        } finally {
            // 释放读锁
            lock.readLock().unlock();
        }
    }

    /**
     * 写锁测试
     */
    @Test
    public void testWriteLock() {
        // 读写锁,控制并发
        ReadWriteLock lock = new ReentrantReadWriteLock();
        try {
            refreshBlackList();
            lock.writeLock().lock();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 是否命中黑名单
     * @param telephone 手机号
     * @return
     */
    public boolean isBlackNum(String telephone) {
        System.out.println("telephone" + telephone + " check");
        return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
    }

    /**
     * 返回黑名单列表
     * @return
     */
    public List<String> getBlackList() {
        return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
    }

    /**
     * 刷新黑名单 
     */
    public void refreshBlackList() {
        System.out.println("refresh blackList");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 读取锁:lock.readLock(),先lock,后面释放unlock
  • 写入锁:lock.writeLock(),也是先lock,后面释放

读取锁-共享

前面说了读取锁是共享的,也就是读取的时候多个线程可以同时读取信息,测试一下。

模拟3个线程同时请求,对判断方法的线程休眠3s。

public class ReadWriteLockTest {

    /**
     * 读取并发读取测试
     */
    @Test
    public void readLockShareTest() {
        ReadWriteLock lock = new ReentrantReadWriteLock();
        ExecutorService executor = Executors.newCachedThreadPool();
        List<Callable<Boolean>> tasks = Lists.newArrayList();
        for (int i = 0; i < 3; i++) {
            StringBuilder telephoneSb = new StringBuilder("1369900163");
            telephoneSb.append(i);
            Callable<Boolean> task = () -> {
                try {
                    lock.readLock().lock();
                    return isBlackNum(telephoneSb.toString());
                } finally {
                    lock.readLock().unlock();
                }
            };
            tasks.add(task);
        }
        long beginTime = System.currentTimeMillis();
        List<Future<Boolean>> futureLit = Lists.newArrayList();
        tasks.stream().forEach(task -> {
            Future<Boolean> isBlackTelephone = executor.submit(task);
            futureLit.add(isBlackTelephone);
        });

        futureLit.stream().forEach(booleanFuture -> {
            try {
                booleanFuture.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long endTime = System.currentTimeMillis();
        System.out.println("总共耗时:" + (endTime - beginTime));

    }

    /**
     * 是否命中黑名单
     * @param telephone 手机号
     * @return
     */
    public boolean isBlackNum(String telephone) {
        long beginTime = System.currentTimeMillis();
        System.out.println("收到获取黑名单列表请求:" + beginTime + ",telephone" + telephone + " check");
        return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
    }

    /**
     * 返回黑名单列表
     * @return
     */
    public List<String> getBlackList() {
        try {
            // 线程休眠3s,模拟远程获取数据
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
    }
}

打印结果:

收到获取黑名单列表请求:1586091816390,telephone13699001630 check
收到获取黑名单列表请求:1586091816390,telephone13699001631 check
收到获取黑名单列表请求:1586091816390,telephone13699001632 check
总共耗时:3023

从上面可以看出,3个线程同时请求黑名单方法,但是3个请求几乎同时到达,所以这个读取并没有阻塞,都获取了锁。如果是阻塞同步方法的话,正常应该是3s一次打印日志。

写入锁-独占

public class ReadWriteLockTest {

    /**
     * 读取并发写入测试
     */
    @Test
    public void readLockShareTest() {
        ReadWriteLock lock = new ReentrantReadWriteLock();
        ExecutorService executor = Executors.newCachedThreadPool();
        List<Callable<Boolean>> tasks = Lists.newArrayList();
        for (int i = 0; i < 3; i++) {
            StringBuilder telephoneSb = new StringBuilder("1369900163");
            telephoneSb.append(i);
            Callable<Boolean> task = () -> {
                try {
                    lock.writeLock().lock();
                    return isBlackNum(telephoneSb.toString());
                } finally {
                    lock.writeLock().unlock();
                }
            };
            tasks.add(task);
        }
        long beginTime = System.currentTimeMillis();
        List<Future<Boolean>> futureLit = Lists.newArrayList();
        tasks.stream().forEach(task -> {
            Future<Boolean> isBlackTelephone = executor.submit(task);
            futureLit.add(isBlackTelephone);
        });

        futureLit.stream().forEach(booleanFuture -> {
            try {
                booleanFuture.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long endTime = System.currentTimeMillis();
        System.out.println("总共耗时:" + (endTime - beginTime));

    }

    /**
     * 是否命中黑名单
     * @param telephone 手机号
     * @return
     */
    public boolean isBlackNum(String telephone) {
        long beginTime = System.currentTimeMillis();
        System.out.println("收到获取黑名单列表请求:" + beginTime + ",telephone" + telephone + " check");
        return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
    }

    /**
     * 返回黑名单列表
     * @return
     */
    public List<String> getBlackList() {
        try {
            // 线程休眠3s,模拟远程获取数据
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
    }
}

打印日志:收到获取黑名单列表请求:1586091930884,telephone13699001630 check
收到获取黑名单列表请求:1586091933894,telephone13699001631 check
收到获取黑名单列表请求:1586091936894,telephone13699001632 check
总共耗时:9019

发现总共耗时9s多,并且每个请求直接相隔3s,说明已经是串行执行的

源码剖析

应用场景

读写锁适用于

  • 读多写少的场景
  • 对实时性要求不是很高

比如黑白名单这种定时维护,能有效提高效率,并且防止并发

后续我们学习一下AQS的原理及源码分析,这样回头看一下这个理解会更加深刻

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值