专栏首页回顾[Java 并发]读锁/写锁

[Java 并发]读锁/写锁

这篇看一下JUC包提供的读写锁(共享锁/独占锁)。

之前我们都知道在一个变量被读或者写数据的时候每次只有一个线程可以执行,那么今天我们来看一下读写锁,读写两不误ReadWriteLock

这里有两个概念:

独占锁:

指该锁一次只能被一个线程所持有。(ReentrantLock和Synchronized都属于独占锁)。

共享锁:

指该锁可被多个线程所持有。

ReentrantReadWriteLock其读锁是共享锁,共写锁是独占锁。

读锁的共享锁可以保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

直接使用ReentrantReadWriteLock写段代码看一下:

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(Long value) {
        try {
            lock.writeLock().lock(); // 获取写锁
            System.out.println(Thread.currentThread().getName() + " \t 开始写入数据: \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 写入数据完成");
            lock.writeLock().unlock(); // 释放写锁
        }catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    public void get() {
        try {
            lock.readLock().lock(); // 获取读锁
            System.out.println(Thread.currentThread().getName() + " \t 开始读取数据");

            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
            String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

            System.out.println(Thread.currentThread().getName() + " \t 读取数据完成: " + collect);
            lock.readLock().unlock(); // 释放读锁
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }
}


public class ReadWriteLockDemo {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 5)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "写线程:" + i).start());

        IntStream.range(0, 5)
                .forEach(i -> new Thread(cacheMap::get,
                        "读线程:" + i).start());
    }
}

上方代码运行效果如下:

可以看到运行结果,红色圈住的地方我们可以看到当使用写锁的时候不管是哪个线程进来都会使其他线程在外等待,直到锁被释放才能拥有获取权限。而蓝色部分是使用了读锁,所有线程可以同时获取允许多个线程同时拥有锁。

注:

但是会出现写一个问题,就是写饥饿现象,上方我们是先运行了所有的写线程,读线程是在写线程后执行的,假如读线程的数量大于写线程数量的话,因锁的大概率都被读线程执行了,就会造成一种写饥饿现象,写线程无法满足大量读线程的读操作,因为写线程少的时候会抢不到锁。

然而在JDK1.8新增了一个锁叫做StampedLock锁,他是对ReadWriteLock的改进。

上边也说了ReadWrite锁可能会出现写饥饿,而StampedLock就是为了解决这个问题锁设计的,StampedLock可以选择使用乐观锁或悲观锁。

**乐观锁:**每次去拿数据的时候,并不是获取锁对象,而是为了判断标记为(stamp)是否又被修改,如果有修改就再去获取读一次。

**悲观锁:**每次拿数据的时候都去获取锁。

通过乐观锁,当写线程没有写数据的时候,标志位stamp并没有改变,所以即使有再多的读线程读数据,他都可以读取,而无需获取锁,这就不会使得写线程抢不到锁了。

stamp类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的stamp值。

通过代码来操作下看一看,先写一个出现写饥饿的情况,模拟19个读线程读取数据,1个写线程写数据。

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private StampedLock lock = new StampedLock();

    public void put(Long value) {
        long stamped = -1; // 设置标记位
        try {
            stamped = lock.writeLock(); // 获取写锁
            System.out.println(Thread.currentThread().getName() + " \t 开始写入数据: \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 写入数据完成");
        }catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockWrite(stamped); // 释放写锁
        }

    }

    public void get() {
        long stamped = -1; // 设置标记位
        try {
            stamped = lock.readLock(); // 获取读锁  -->这里是悲观锁实现  --> stamped重新赋值标记位
            System.out.println(Thread.currentThread().getName() + " \t 开始读取数据");

            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
            String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

            System.out.println(Thread.currentThread().getName() + " \t 读取数据完成: " + collect);
        } catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockRead(stamped); // 释放读锁 --> 这里我们放入一个标记位
        }

    }
}

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 19)
                .forEach(i -> new Thread(cacheMap::get,
                        "读线程:" + i).start());

        IntStream.range(0, 1)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "写线程:" + i).start());
    }
}

上边使用了StampedLock做了一个读锁悲观锁的实现,模拟了20个线程,假设了写线程因不能及时写入数据造成写饥饿现象。我们看一下运行结果。

可以看到结果,读锁都可以同时获取锁,就算写线程没有写入数据所有读线程还是在抢占锁,使用ReadWriteLock也是会出现同样的现象,写饥饿。

下面我们使用 乐观锁,每次判断标记位是否被修改,如果有被修改就再进行上锁然后重新读取。

class CacheList{
    private volatile ArrayList<Long> list = new ArrayList<>();

    private StampedLock lock = new StampedLock();

    public void put(Long value) {
        long stamped = -1; // 设置标记位
        try {
            stamped = lock.writeLock(); // 获取写锁
            System.out.println(Thread.currentThread().getName() + " \t 开始写入数据: \t" + value);

            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
            this.list.add(value);

            System.out.println(Thread.currentThread().getName() + " \t 写入数据完成");
        }catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.unlockWrite(stamped); // 释放写锁
        }

    }

    public void get() {
        // 这里使用了乐观锁,每次去判断标记位是否被改变,如果写线程有修改此值会被修改
        long stamped = lock.tryOptimisticRead();
        try {
            System.out.println(Thread.currentThread().getName() + " \t 开始读取数据");
            TimeUnit.SECONDS.sleep(2); // 阻塞两秒
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        // 读取值
        String collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));

        // 判断以下标记位是否被修改,被修改就会返回false,说明有写线程写入了新数据
        // 那么重新获取锁并去读取值,否则直接使用上面读取的值
        if (!lock.validate(stamped)){
            try {
                stamped = lock.readLock();
                collect = this.list.stream().map(String::valueOf).collect(Collectors.joining(","));
            }catch (Exception ex) {
                ex.printStackTrace();
            }finally {
                lock.unlockRead(stamped);
            }

        }

        System.out.println(Thread.currentThread().getName() + " \t 读取数据完成: " + collect);

    }
}

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        CacheList cacheMap = new CacheList();

        IntStream.range(0, 19)
                .forEach(i -> new Thread(cacheMap::get,
                        "读线程:" + i).start());

        IntStream.range(0, 1)
                .forEach(i -> new Thread(() ->  cacheMap.put(System.currentTimeMillis()),
                        "写线程:" + i).start());
    }
}

直接看运行结果:

主要看get方法,get方法开始调用StampedLocktryOptimisticRead方法来获取标志位stamp,获取乐观锁那块并不是真的去上锁**(所以不会阻塞写操作),然后直接去读数据。接着通过validate**方法来判断标志位是否被修改了,修改了就在进行获取锁进行读取,没被修改则会返回true直接使用上边获取到的值。

StampedLock解决了在没有新数据写入时,由于过多读操作抢夺锁而使得写操作一直获取不到锁无法写入新数据的问题。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ThreadLocal (上) 简介以及基本使用

    多线程访问同一个共享变量特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时进行适当的同步。如图所示

    YanL
  • ThreadLocal (下) 继承性问题解决,以及具体实现原理

    通过以上例子可以看到,同一个ThreadLocal变量在父线程中设置值后,在子线程是取不到的。根据上节的介绍,这应该是正常现象。因为子线程thread里面调用g...

    YanL
  • ThreadLocal (下) 继承性

    通过以上例子可以看到,同一个ThreadLocal变量在父线程中设置值后,在子线程是取不到的。根据上节的介绍,这应该是正常现象。因为子线程thread里面调用g...

    YanL
  • 详解synchronized与Lock的区别与使用

    在开始之前先把进程与线程进行区分一下,一个程序最少需要一个进程,而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最...

    Kevin_Zhang
  • 入门 | GPU是如何优化运行机器学习算法的?

    机器之心
  • redisson的MultiLock连锁

    Redis based distributed RedissonMultiLock object groups multiple RLock objects a...

    IT云清
  • 基于词典和朴素贝叶斯中文情感倾向分析算法

    每个句子分词 在每个句子分词的过程中,根据他的词性,去除停用词(做简单清洗),比如:专有名词、标点符好、时间(包含节假日)、数字、助词、语气词···· 得到如下...

    机器学习AI算法工程
  • 【趣学程序】Linux虚拟机安装

    通过阅读本文,你将了解到如何在windows上安装CentOS虚拟机。软件分享:VMWare软件及许可证:VMWare下载

    趣学程序-shaofeer
  • [Leetcode][python]Substring with Concatenation of All Words/与所有单词相关联的字串

    现有一组长度相等的字符串words,要在原字符串中找出正好包含words中所有字符串的子字符串的起始位置。 例子: 输入: s = “barfoothe...

    后端技术漫谈
  • mysql 隔离级别的实现

    本文探讨innodb如何使用mvcc和各种锁机制,保障mysql的四层隔离等级的。

    平凡的学生族

扫码关注云+社区

领取腾讯云代金券