大佬,能给我讲讲什么是读写锁和锁的升级吗?

故事开端

在一个阳光灿烂的早晨,熊某突然从别人的口中听到读写锁锁的升级这个名词,脑海中第一反应就是——“What‘s this???”,锁还能分读写?锁还能升级?我的天,怎么感觉在升级打怪一样。

于是熊某带着满头的疑虑去请求公司的技术大佬,这位技术大佬很耐心的给我说:"熊兄弟,并发编程没你想得那么简单的,下面让我一一为你道来。"于是,担起小板凳,拿着扇子马上开讲!
在这里插入图片描述

何为“读写锁”?

读写锁是一个很多地方都使用的技术,基本上实现读写锁都要遵循三个原则:

  1. 允许多个线程同时读一个共享变量
  2. 只能有一个线程对共享变量执行写操作
  3. 当一个线程执行写操作的时候其它线程不能读共享变量

从以上三个原则可以得出,写锁和读锁是互斥的,写的时候不能读,写只能一个写,读可以多个读。

为什么要用读锁

这时候熊某略为不解,就问技术老大:“既然读锁能够运行多个线程访问共享变量,那直接不要锁就好了,为什么还要那么麻烦弄个读锁?“

技术老大耐心讲解:”熊兄弟这问题提的不错,为什么需要读锁?上面讲述的三个原则的第三点是答案问题的。’当一个线程执行写操作的时候其它线程不能读共享变量‘,就是说当有线程对共享变量进行更新的时候,其它线程是不能进行读操作的,只有当写操作完成后才能进行读,确保读到的数据是最新的。如果不加读锁,当数据更新的时候有其它线程还在读,就会出现读到的不是最新数据的情况“。

熊某:“哦!!!!”
在这里插入图片描述

读写锁的实现

“说了这么多,能给我看看是怎么实现的吗?”,熊某说到。
“没问题,上代码!”,技术大佬道

public class ReadWriteTest{

    private Map<String, Object> map = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    Object read(String key){
        readLock.lock();
        System.out.println("-------获得读锁");
        try {
            return map.get(key);
        }finally {
            System.out.println("-------释放读锁");
            readLock.unlock();
        }
    }

    void write(String key, Object value){
        writeLock.lock();
        System.out.println("-------获得写锁");
        try {
            map.put(key, value);
        }finally {
            System.out.println("-------释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        readWriteTest.write("test", "熊小哥好帅!");
        System.out.println(readWriteTest.read("test"));
    }
}

输出结果是:
-------获得写锁
-------释放写锁
-------获得读锁
-------释放读锁
熊小哥好帅!

代码这么看的话看不出什么,既然是并发编程就要加多几个线程来测试啦,下面是改良版:

public class ReadWriteTest {

    private Map<String, Object> map = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    Object read(String key) {
        System.out.println(Thread.currentThread().getName() + "-------尝试获得读锁");
        while (!readLock.tryLock())
            System.out.println(Thread.currentThread().getName() + "-------获得读锁失败");
        if (readLock.tryLock()) {
            System.out.println(Thread.currentThread().getName() + "-------获得读锁成功");
            try {
                return map.get(key);
            } finally {
                System.out.println(Thread.currentThread().getName() + "-------释放读锁");
                readLock.unlock();
            }
        }
        return null;

    }

    void write(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "-------尝试获得写锁");
        if (writeLock.tryLock()) {
            System.out.println(Thread.currentThread().getName() + "-------获得写锁成功");
            try {
                map.put(key, value);
            } finally {
                System.out.println(Thread.currentThread().getName() + "-------释放写锁");
                writeLock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "-------获得写锁失败");
        }

    }

    public static void main(String[] args) throws InterruptedException {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        Thread thread1 = new Thread(
                () -> {
                    readWriteTest.write("test", "熊小哥好帅");
                });
        Thread thread2 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        Thread thread3 = new Thread(
                () -> {
                    readWriteTest.write("test", "熊小哥好帅");
                });
        Thread thread4 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        thread1.setName("线程A");
        thread2.setName("线程B");
        thread3.setName("线程C");
        thread4.setName("线程D");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

}

这边为了得到加锁释放成功的结果,就用了tryLock(),运行结果为:
线程A-------尝试获得写锁
线程D-------尝试获得读锁
线程C-------尝试获得写锁
线程C-------获得写锁失败
线程B-------尝试获得读锁
。。。。。。。。省略了无数次的获得读锁失败
线程A-------释放写锁
线程D-------获得读锁失败
线程B-------获得读锁失败
线程D-------获得读锁成功
线程D-------释放读锁
线程B-------获得读锁成功
线程B-------释放读锁

线程AC负责获取写锁,线程BD负责获取读锁,看结果可以知道,当线程A获得写锁后,线程BD是不能获取读锁的,线程C也不能获取写锁。所以后面线程BD获取的数据是线程A修改后的数据。接下来我们看看多个线程能不能同时获取读锁,稍微改改测试代码:

public static void main(String[] args) throws InterruptedException {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        Thread thread2 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        Thread thread3 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        Thread thread4 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        thread2.setName("线程B");
        thread3.setName("线程C");
        thread4.setName("线程D");
        thread2.start();
        thread3.start();
        thread4.start();
    }

看看结果:
线程C-------尝试获得读锁
线程D-------尝试获得读锁
线程B-------尝试获得读锁
线程C-------获得读锁成功
线程C-------释放读锁
线程D-------获得读锁成功
线程B-------获得读锁成功
线程B-------释放读锁
线程D-------释放读锁

可以看到,多个线程可以同时获得读锁来访问变量。

值得注意的是,读写锁也是可重入锁
在这里插入图片描述

锁的升级

“读写锁的使用我基本明白,但是锁的升级呢?”
“熊兄弟莫要心急,本大佬马上给你解释。”
常说的锁升级,其实指的是从读锁升级为写锁,我们用上面的代码说下,这边改造下read()方法,为了方便,删除多余的代码:

void read(String key) {
        // 这里上读锁
        if (readLock.tryLock()) {
            try {
                if(Objects.isNull(map.get(key))){
                    try{
                        // 这里上写锁
                        writeLock.lock();
                        map.put(key, "123");
                    }finally {
                        // 这里释放写锁
                        writeLock.unlock();
                    }
                }
                map.get(key);
            } finally {
                // 这里释放读锁
                readLock.unlock();
            }
        }
    }

执行一下,发现程序永久没执行完。这是咋回事?
正当熊某有这个问题时,技术大佬心领神会的说出了答案,“读写锁(ReadWriteLock)是不支持锁的升级,因为当读锁不释放的时候,获取写锁的线程只能永久等待,虽然不支持锁的升级,但是锁的降级还是支持的(降级指的是从写锁转换成读锁),你看看代码”:

void read(String key) {
        // 这里上读锁
        if (readLock.tryLock()) {
            if(Objects.isNull(map.get(key))){
                // 发现为空释放读锁,然后上写锁
                readLock.unlock();
                writeLock.lock();
                //  再检测下是否有数据,避免重复存放数据,耗费资源
                try{
                    if(Objects.isNull(map.get(key))){
                        map.put(key, "熊小哥好帅");
                    }
                    // 这里降级为读锁
                    readLock.lock();
                }finally {
                    writeLock.unlock();
                }
                // 获取数据并释放读锁
                System.out.println(map.get(key));
                readLock.unlock();
            }

        }
    }

执行一下测试代码:

public static void main(String[] args) throws InterruptedException {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        Thread thread2 = new Thread(
                () -> {
                    readWriteTest.read("test");
                });
        thread2.setName("线程B");
        thread2.start();
    }

看结果为:熊小哥好帅

结尾

“oh,我都懂了,我应该要怎么感谢技术大佬您呢?”熊某兴奋地叫道。
“今晚带我去吃海底捞!!”,技术大佬不客气的喊道。
“好吧好吧!”

最后说一句,各位喜欢的话可以点个赞哦。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值