JUC-读写锁ReadWriteLock(七)

一、读锁和写锁介绍

读锁(共享锁):可多条线程同时获取数据,多个线程可以同时占有

写锁(独占锁):只能单条线程写入,一次只能被一个线程占有

读锁和写锁是互斥的,加了读锁后,不能再加写锁,保证了读操作的数据原子性;同理,加了写锁不能再加读锁。

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:

线程进入读锁的前提条件:

  • 没有其他线程的写锁,
  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

ReentrantReadWriteLock支持以下功能:

1)支持公平和非公平的获取锁的方式;

2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;

3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;

4)读取锁和写入锁都支持锁获取期间的中断;

5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。

二、ReentrantReadWriteLock使用介绍

2.1 利用重入来执行升级缓存后的锁降级

class CachedData {
    Object data;
    volatile boolean cacheValid;    //缓存是否有效
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();    //获取读锁
        //如果缓存无效,更新cache;否则直接使用data
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            //获取写锁前须释放读锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            // Recheck state because another thread might have acquired
            //   write lock and changed state before we did.
            if (!cacheValid) {
                data = ...
                cacheValid = true;
            }
            // Downgrade by acquiring read lock before releasing write lock
            //锁降级,在释放写锁前获取读锁
            rwl.readLock().lock();
            rwl.writeLock().unlock(); // Unlock write, still hold read
        }

        use(data);
        rwl.readLock().unlock();    //释放读锁
    }
}

2.2 ReentrantReadWriteLock 提高并发性

通常在 collection 数据很多,读线程访问多于写线程并且 entail 操作的开销高于同步开销时尝试这么做。

class RWDictionary {
    private final Map<String, Data> m = new TreeMap<String, Data>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();    //读锁
    private final Lock w = rwl.writeLock();    //写锁

    public Data get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public String[] allKeys() {
        r.lock();
        try { return m.keySet().toArray(); }
        finally { r.unlock(); }
    }
    public Data put(String key, Data value) {
        w.lock();
        try { return m.put(key, value); }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
}

我们举个例子来比较ReentrantLock和ReentrantReadWriteLock的并发性能。

ReentrantLock例子:

class RWDictionary2 {
    private final Map<String, String> m = new TreeMap<String, String>();
    private final Lock lock = new ReentrantLock();

    public String get(String key) {
        lock.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return m.get(key);
        }
        finally { lock.unlock(); }
    }
    public int keySize() {
        lock.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return  m.size();
        }
        finally { lock.unlock(); }
    }

    public String put(String key, String value) {
        lock.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return m.put(key, value);
        }
        finally { lock.unlock(); }
    }

    public static void main(String[] args) {
        RWDictionary2 dictionary2 = new RWDictionary2();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary2.put(i + "", "put");
                System.out.println("put 用时:"+System.currentTimeMillis());
            }
        }, "put").start();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary2.get(i + "");
                System.out.println("get 用时:"+System.currentTimeMillis());
            }
        }, "get").start();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary2.keySize();
                System.out.println("keySize 用时:"+System.currentTimeMillis());
            }
        }, "allKeys").start();

    }
}

执行结果:

put 用时:1649216011387
get 用时:1649216014390
keySize 用时:1649216017401
put 用时:1649216020404
get 用时:1649216023414
keySize 用时:1649216026423
put 用时:1649216029425
get 用时:1649216032435
keySize 用时:1649216035449

解读代码:
上述代码,定义了两个读方法和一个写方法,而且三个方法都共用了一个ReentrantLock锁。因此结果也就可想而知,三个方法都是同步的,间隔三秒,并发性能严重受到影响。(此案例会出现数据错误,比如先执行了读再执行写,这里忽略,不关注数据正确性)

不给读方法加锁?不行,读方法不加锁,就随时可以执行,会出现脏读数据错误,甚至并发异常。

我们的目标是,保证写的时候不允许读,读的时候不允许写,可以多个线程一起读,仅能一个线程单独写。

ReentrantReadWriteLock 例子:

class RWDictionary {
    private final Map<String, String> m = new TreeMap<String, String>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();    //读锁
    private final Lock w = rwl.writeLock();    //写锁

    public String get(String key) {
        r.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return m.get(key);
        }
        finally { r.unlock(); }
    }
    public int keySize() {
        r.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return  m.size();
        }
        finally { r.unlock(); }
    }

    public String put(String key, String value) {
        w.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return m.put(key, value);
        }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            m.clear();
        }
        finally { w.unlock(); }
    }

    public static void main(String[] args) {
        RWDictionary dictionary = new RWDictionary();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary.put(i + "", "put");
                System.out.println("put 用时:"+System.currentTimeMillis());
            }
        }, "put").start();
        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary.put(i + "", "clear");
                System.out.println("clear 用时:"+System.currentTimeMillis());
            }
        }, "clear").start();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary.get(i + "");
                System.out.println("get 用时:"+System.currentTimeMillis());
            }
        }, "get").start();

        new Thread(() -> {
            for (int i = 1; i <= 50; i++) {
                dictionary.keySize();
                System.out.println("keySize 用时:"+System.currentTimeMillis());
            }
        }, "allKeys").start();

    }
}

执行结果:

put 用时:1649216720385
clear 用时:1649216723395
get 用时:1649216726402
keySize 用时:1649216726402
put 用时:1649216729415
clear 用时:1649216732427
keySize 用时:1649216735429
get 用时:1649216735429
put 用时:1649216738443
clear 用时:1649216741445
keySize 用时:1649216744452
get 用时:1649216744452

解读代码:
上述代码有两个写方法,两个读方法,两个写方法都只能单独执行并且间隔三秒,两个读方法可以并发执行间隔时间可忽略。如此就提高了读的并发性能。

三、ReentrantReadWriteLock 实现原理

暂时省略,后补

四、文章参考来源

https://www.cnblogs.com/zaizhoumo/p/7782941.html

https://www.cnblogs.com/xiaoxi/p/9140541.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值