并发学习(四)ReentrantReadWriteLock读写锁

诞生背景

前面提到的synchronized加锁基本都是排他锁,也就是说只要是多线程竞争情况下,就只能有一个线程获得锁,其余线程阻塞。虽然已经对synchronized进行性能优化了,但是这是从技术层面的优化,它的本质还是排他锁。
我们也可以从业务的角度出发,比如存在这种情况:多线程访问同步代码块时,大部分情况下都是读操作,少部分才是写操作。这样的话,如果还是将没获得锁的线程都阻塞起来,太影响性能了。因为读操作即使存在多个线程访问共享变量,但是不涉及修改也就不存在线程安全性问题了。
所以需要有个更加灵活的锁来处理这种读多写少的业务场景,于是乎,有了ReentrantReadWriteLock读写锁。

特点

读读(不互斥)
基于以上的业务场景,如果是多个线程同时读共享数据的话,应该是不需要阻塞的。下面来通过案例证明。

public class ReadWriteLockTest {

    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();
        demo.cacheMap.put("data", 1);
        // 线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.read();
                }
            }
        }).start();
        // 线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.read();
                }
            }
        }).start();
    }
}

class ReadWriteLockDemo {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock read = readWriteLock.readLock();
    // 共享数据
    Map cacheMap = new HashMap<>();

    public void read() {
        read.lock(); // 上读锁并且不释放
        System.out.println("线程:" + Thread.currentThread().getName() + "开始读数据...");
        System.out.println(Thread.currentThread().getName() + "读数据为:" + cacheMap.get("data"));
        System.out.println("线程:" + Thread.currentThread().getName() + "结束读数据!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

执行结果:在读锁没释放的情况下,线程1和线程2仍然可以同时读取数据,说明读读不存在互斥。

线程:Thread-1开始读数据...
Thread-1读数据为:1
线程:Thread-1结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!

读写(互斥)
这种既有读又要写的情况下,那么只能等读完再写或者写完再读,否则数据是不准确的。也就是说,读写之间存在互斥。下面通过案例证明。

public class ReadWriteLockTest02 {

    public static void main(String[] args) {
        ReadWriteLockDemo02 demo = new ReadWriteLockDemo02();
        demo.cacheMap.put("data", 1);
        // 线程1 读数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.read();
                }
            }
        }).start();
        // 线程2 写数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.write();
                }
            }
        }).start();
    }
}

class ReadWriteLockDemo02 {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock read = readWriteLock.readLock();
    Lock write=readWriteLock.writeLock();
    Map cacheMap = new HashMap<>();

    public void read() {
        read.lock(); // 获取读锁
        System.out.println("线程:" + Thread.currentThread().getName() + "开始读数据...");
        System.out.println(Thread.currentThread().getName() + "读数据为:" + cacheMap.get("data"));
        System.out.println("线程:" + Thread.currentThread().getName() + "结束读数据!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            read.unlock(); // 释放读锁
        }
    }

    public void write() {
        write.lock();// 获得写锁
        System.out.println(Thread.currentThread().getName() + "开始写数据...");
        double data = Math.random();
        cacheMap.put("data", data);
        System.out.println(Thread.currentThread().getName() + "写数据为:" + data);
        System.out.println(Thread.currentThread().getName() + "结束写数据!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            write.unlock();// 释放写锁
        }
    }

}

执行结果:线程1和线程2交替执行,说明此时互斥。

线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
Thread-1开始写数据...
Thread-1写数据为:0.1469638477677232
Thread-1结束写数据!
线程:Thread-0开始读数据...
Thread-0读数据为:0.1469638477677232
线程:Thread-0结束读数据!

假设将以上代码:read.unlock(); // 释放读锁注释掉,让读锁一直不释放。
执行结果:由于线程1读锁一直不释放,所以线程2写锁就一直阻塞,这也说明了读写是互斥的。

线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!

写写(互斥)
到了写锁,我们更加确定这是互斥的了。下面还是通过案例来证明。

public class ReadWriteLockTest03 {

    public static void main(String[] args) {
        ReadWriteLockDemo03 demo = new ReadWriteLockDemo03();
        demo.cacheMap.put("data", 1);
        // 线程1 写数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.write();
                }
            }
        }).start();
        // 线程2 写数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    demo.write();
                }
            }
        }).start();
    }
}

class ReadWriteLockDemo03 {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock write=readWriteLock.writeLock();
    Map cacheMap = new HashMap<>();

    public void write() {
        write.lock();// 获得写锁
        System.out.println(Thread.currentThread().getName() + "开始写数据...");
        double data = Math.random();
        cacheMap.put("data", data);
        System.out.println(Thread.currentThread().getName() + "写数据为:" + data);
        System.out.println(Thread.currentThread().getName() + "结束写数据!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
//            write.unlock();// 释放写锁
        }
    }

}

执行结果:第一个线程获得锁后,由于锁一直不释放,所以一直都是该线程写数据,其余线程只能阻塞。说明了写写是互斥的。如果将代码:write.unlock();注释取消,则线程1和线程2根据cpu调度来确定执行顺序(有了锁的释放,才有其他线程争抢锁的可能)。

读写锁实现缓存的案例

public class CacheTest {
    private Map<String, Object> cache = new HashMap<String, Object>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public Object getData(String key) {
        // 首先上读锁
        rwLock.readLock().lock();
        // 首先从缓存中获取
        Object value = null;
        try {
            Thread.sleep(1000);
            value = cache.get(key);
            if (value == null) {
                // 如果缓存中没有数据,那么就从数据库中获取
                // 但此时需要上写锁,只需要让一个进程进行写数据
                // 首先去除读锁,然后加上写锁
                rwLock.readLock().unlock();
                // 假设线程1、线程2、线程3都执行到了这里,线程1获得写锁继续往下执行,线程2和线程3阻塞
                rwLock.writeLock().lock();
                try {
                    // 注意防止多线程运行到上一步,某个线程写完数据后
                    // 别的线程就需要看是否有数据再决定是否进行写操作
                    // 在写之前再读一次,防止最开始的线程都进行写操作</span>
                    value = cache.get(key);
                    // 第一个线程写完后,防止后面的线程再次写数据
                    if (value == null) {
                        System.out.println("有线程写数据........");
                        value = "数据库中获取";
                        // 将数据放入缓存
                        cache.put(key, value);
                        System.out.println("数据写完了.......");
                        // 线程1修改完数据释放写锁,如果这里不加判断,则线程2和线程3还是会继续写入数据
                        // 加入判断后,线程2和线程3发现value已经有线程1之前写入的值了,就直接获得该值并返回
                    }
                } finally {
                    rwLock.readLock().lock();// 恢复读锁,锁的重入
                    rwLock.writeLock().unlock();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();// 解读锁
        }
        return value;
    }
}

以上案例参考:https://blog.csdn.net/u011535508/article/details/52527894

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值