读写锁的基本使用
在【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适合普通场合