读写锁的基本使用

本文详细解读ReentrantReadWriteLock的读写锁原理,包括共享锁和排他锁的区别,读写锁的插队策略,以及升级降级机制。通过实例演示和策略分析,阐述了其在并发场景中的高效利用和避免死锁的方法。
摘要由CSDN通过智能技术生成

读写锁的基本使用

在【ReentrantLock锁详解】一文中讲到了java中锁的划分,本篇主要讲述共享锁和排他锁:ReentrantReadWriteLock

在这里插入图片描述

在ReentrantReadWriteLock中包含读锁和写锁,其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,在更改时候不允许其他线程操作。读写锁其实是一把锁,所以会有同一时刻不允许读写锁共存的规定。之所以要细分读锁和写锁也是为了提高效率,将读和写分离,对比ReentrantLock就可以发现,无论并发读还是写,它总会先锁住全部再说。

接着先来个代码演示下,读锁是共享锁,写锁是排他锁:

/**
 * ReentrantReadWriteLock读写锁示例
 **/
public class ReentrantReadWriteLockTest {
 
 private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
 private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
 private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
 
 public static void read() {
        readLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
        }
    }
 
 public static void write() {
        writeLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
        }
    }
 
 public static void main(String[] args) {
 new Thread(() -> read(), "Thread1").start();
 new Thread(() -> read(), "Thread2").start();
 new Thread(() -> write(), "Thread3").start();
 new Thread(() -> write(), "Thread4").start();
    }
}

输出结果如下,线程1和线程2可以同时获取读锁,而线程3和线程4只能依次获取写锁,因为线程4必须等待线程3释放写锁后才能获取到锁:

Thread1获取读锁,开始执行
Thread2获取读锁,开始执行
Thread1释放读锁
Thread2释放读锁
Thread3获取写锁,开始执行
Thread3释放写锁
Thread4获取写锁,开始执行
Thread4释放写锁

读锁的插队策略

设想如下场景:在非公平的ReentrantReadWriteLock锁中,线程2和线程4正在同时读取,线程3想要写入,拿不到锁(同一时刻是不允许读写锁共存的),于是进入等待队列,线程5不在队列里,现在过来想要读取,策略1是如果允许读插队,就是说线程5读先于线程3写操作执行,因为读锁是共享锁,不影响后面的线程3的写操作,这种策略可以提高一定的效率,却可能导致像线程3这样的线程一直在等待中,因为可能线程5读操作之后又来了n个线程也进行读操作,造成线程饥饿;策略2是不允许插队,即线程5的读操作必须排在线程3的写操作之后,放入队列中,排在线程3之后,这样能避免线程饥饿。

事实上ReentrantReadWriteLock在非公平情况下,读锁采用的就是策略2:不允许读锁插队,避免线程饥饿。更加确切的说是:在非公平锁情况下,允许写锁插队,也允许读锁插队,但是读锁插队的前提是队列中的头节点不能是想获取写锁的线程。

以上还在非公平ReentrantReadWriteLock锁中,在公平锁中,读写锁都是是不允许插队的,严格按照线程请求获取锁顺序执行。

下面用代码演示一下结论:在非公平锁情况下,允许写锁插队,也允许读锁插队,但是读锁插队的前提是队列中的头节点不能是想获取写锁的线程

/**
 * ReentrantReadWriteLock读写锁插队策略测试
 **/
public class ReentrantReadWriteLockTest {
 
 private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
 private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
 private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
 
 public static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
            Thread.sleep(20);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
        }
    }
 
 public static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
            Thread.sleep(40);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
 
 public static void main(String[] args) {
 new Thread(() -> write(), "Thread1").start();
 new Thread(() -> read(), "Thread2").start();
 new Thread(() -> read(), "Thread3").start();
 new Thread(() -> write(), "Thread4").start();
 new Thread(() -> read(), "Thread5").start();
 new Thread(() -> {
            Thread[] threads = new Thread[1000];
 for (int i = 0; i < 1000; i++) {
                threads[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
            }
 for (int i = 0; i < 1000; i++) {
                threads[i].start();
            }
        }).start();
    }
}

以上测试代码就演示了,在非公平锁时,其一:同一时刻读写锁不能同时存在, 其二,读锁非常容易插队,但前提是队列中的头结点不能是想获取写锁的线程。

锁的升降级

升降级是指读锁升级为写锁,写锁降级为度锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁,如下代码测试所示:

/**
 * ReentrantReadWriteLock锁升降级测试
 **/
public class ReentrantReadWriteLockTest {
 private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
 private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
 private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
 
 public static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
            Thread.sleep(20);
            System.out.println(Thread.currentThread().getName()+ "尝试升级读锁为写锁");
 //读锁升级为写锁(失败)
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() +"读锁升级为写锁成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
        }
    }
 
 public static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
 try {
            System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");            Thread.sleep(40);
            System.out.println(Thread.currentThread().getName() +"尝试降级写锁为读锁");
 //写锁降级为读锁(成功)
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+ "写锁降级为读锁成功");
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");           writeLock.unlock()           readLock.unlock();
        }
    }
 public static void main(String[] args) {new Thread(() -> write(), "Thread1").start();
 new Thread(() -> read(), "Thread2").start();
    }
}

运行控制台输出:

Thread1开始尝试获取写锁
Thread1获取写锁,开始执行
Thread1尝试降级写锁为读锁
Thread1写锁降级为读锁成功
Thread1释放写锁

Thread2开始尝试获取读锁
Thread2获取读锁,开始执行
Thread2尝试升级读锁为写锁
之所以ReentrantReadWriteLock不支持锁的升级(其它锁可以支持),主要是避免死锁,例如两个线程A和B都在读, A升级要求B释放读锁,B升级要求A释放读锁,互相等待形成死循环。如果能严格保证每次都只有一个线程升级那也是可以的。

总结

读写锁特点特点:读锁是共享锁,写锁是排他锁,读锁和写锁不能同时存在
插队策略:为了防止线程饥饿,读锁不能插队
升级策略:只能降级,不能升级
ReentrantReadWriteLock适合于读多写少的场合,可以提高并发效率,而ReentrantLock适合普通场合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值