【锁】——读写锁

401 阅读2分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

介绍

  • 只要读读可以共存(允许多个线程同时读),其他情况都会有线程安全问题。

  • 如果有一个线程想去写共享资源,就不应该在有其他线程可以对该资源进行读或写

  • 提供了写锁和读锁两种锁的操作机制

ReentranReadWriteLock

ReadWriteLock提供了readLock和writeLock两种锁的操作机制,一个是读锁,一个是写锁,而它的实现类就是ReentranReadWriteLock

代码Demo

  • 一个资源类MyCache,借用HashMap有put和get两个操作
  • 模拟5个线程读,5个线程写的场景
  • 在读写前加入延迟时间,便于观察是否有其他线程插入的场景
package com.lemon.lesson6_lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

public class WriteReadLockDemo {
    private ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> myCache.put(temp, temp), "write-" + temp).start();
        }
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp);

            }, "read-" + temp).start();
        }
    }
}

class MyCache {
    private Map<Object, Object> map = new HashMap<>();

    public void put(Object key, Object value) {
        System.out.println(Thread.currentThread().getName() + "\t开始写");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "\t写入完成");
    }

    public Object get(Object key) {
        System.out.println(Thread.currentThread().getName() + "\t开始读取");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object res = map.get(key);
        System.out.println("读取完成,读取结果为:" + res);
        return res;
    }
}

运行结果:

write-0	开始写
write-3	开始写
write-2	开始写
write-4	开始写
write-1	开始写
read-1	开始读取
read-2	开始读取
read-0	开始读取
read-3	开始读取
read-4	开始读取
write-0	写入完成
write-4	写入完成
write-2	写入完成
write-3	写入完成
write-1	写入完成
读取完成,读取结果为:3
读取完成,读取结果为:2
读取完成,读取结果为:0
读取完成,读取结果为:null
读取完成,读取结果为:null

可以看到由于在某一个线程写入的过程中被其他线程打断,所以数据出现问题

解决方案:当然可以使用synchronized或者ReentrantLock,当时这样虽然解决了线程安全的问题,但是降低了并发性(实际上在是可以允许多个线程同时读的),所以我们引入了 java.util.concurrent.locks.ReentrantReadWriteLock

修改后的资源类MyCache

  • 为了保证资源map的可见性和防止指令重排,使用volatile修饰
  • 对于put操作使用写锁
  • 对于get操作使用读锁
class MyCache {
    private volatile Map<Object, Object> map = new HashMap<>();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(Object key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t开始写");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t写入完成");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

    public Object get(Object key) {
        Object res = null;
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t开始读取");
            Thread.sleep(100);
            res = map.get(key);
            System.out.println("读取完成,读取结果为:" + res);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
        return res;
    }
}

运行结果

write-0	开始写
write-0	写入完成
write-1	开始写
write-1	写入完成
write-2	开始写
write-2	写入完成
write-3	开始写
write-3	写入完成
write-4	开始写
write-4	写入完成
read-1	开始读取
read-2	开始读取
read-0	开始读取
read-4	开始读取
read-3	开始读取
读取完成,读取结果为:1
读取完成,读取结果为:4
读取完成,读取结果为:0
读取完成,读取结果为:2
读取完成,读取结果为:3

适用场景

可以看到相比于 ReentrantLock 适用于一般场合,ReentrantReadWriteLock对于写相关的操作并没有比ReentrantLock表现的优秀,但是对于共享读的场景,进一步提高了并发性。所以说ReadWriteLock 适用读多写少的场景,合理使用可以进一步提高并发