上面文中介绍的ReentrantLock是排他锁,同一时刻只允许一个线程访问。我们使用锁的目的是控制共享变量并发操作造成的线程安全问题。但有时共享变量提供读服务的是远超过写服务,为了提高并发时效率引入了读写锁。
在引入读写锁之前,所有获取锁的操作都需要上一个线程释放锁。
读写锁实现了读操作时获取读锁,写操作时获取写锁。
下表展示了读写锁的互斥关系。读锁实现共享,多个线程可以同时获得读写,使得线程不再阻塞在锁的获取上,提升并发效率。
1、A线程持有读锁时,B线程可以获取读锁,无法获取写锁。
2、A线程持有写锁时,B线程无法获取读锁,也无法获取写锁。
类型 | 读锁 | 写锁 |
---|---|---|
读锁 | 不互斥 | 互斥 |
写锁 | 互斥 | 互斥 |
读写锁同样也分公平锁和非公平锁,并且默认的是非公平锁;这里我们就会产生疑问了,若不断有获取读锁的线程(后统称读线程,同理写线程)来获取读锁,读锁就可能永远都不会完全释放,会不会也会造成写线程饿死呢?下面分别介绍读写锁中的公平锁和非公平锁。
公平锁:
读线程获取锁时,必须要同时满足下面情况才能得到锁,否则在CLH队列中等待:
1、当前线程是在CLH队列中等待最长的节点。
2、 上一线程持有的是读锁或者锁没有被任何线程获取。
写线程获取是,必须要满足下面情况才能得到锁,否则进入CLH队列中:
1、当前线程是在CLH队列中等待最长的节点。
2、锁没有被任何线程获取。
就是上面红色一小段逻辑不同,就能极大的提高并发读操作的性能。
非公平锁:
读线程获取锁时:
读线程在获取锁,若等待时间最长的线程是写线程,必须让渡给该写线程优先获取锁。避免出现写线程"饿死"。
写线程获取锁时:
最快的写线程获得锁,其他线程继续等待。
***读写锁的公平锁和非公平锁相比复杂一些,后面我们会再针对源码剖析,把它看得更清楚和透彻。
CLH这个概念会多次提到。后面源码分析时,会详细剖析CLH的结构。这里我们可以暂时理解为链表。
下面这段代码总觉得不够表达公平锁和非公平的特性。后续更深入了解后再补充!
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockClient {
// public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);//非公平锁
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);//公平锁
public static void main(String[] args) {
Integer threadCount = 10;
for (int i = 0; i < threadCount; i++) {
if (i % 3 == 0) {
Thread t = new Thread(new WriteWorker());
t.start();
} else {
Thread t = new Thread(new ReadWorker());
t.start();
}
}
}
static class ReadWorker implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-Read线程启动");
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "-Read线程acquired,当前已获取读线程总数:" + lock.getReadLockCount() + " 已重入总次数:" + lock.getReadHoldCount());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
// System.out.println(Thread.currentThread().getName() + "-读线程released");
}
}
static class WriteWorker implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-Write线程启动");
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "-Write线程acquired,已重入总次数:" + lock.getWriteHoldCount());
} finally {
lock.writeLock().unlock();
}
System.out.println(Thread.currentThread().getName() + "-Write线程released");
}
}
}
支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
读锁的重入性:
public class ReadWriteLockClient {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static Integer count = 5;
public static void main(String[] args) {
lockService();
}
public static void lockService() {
lock.readLock().lock();
while (--count > 0) {
System.out.println(Thread.currentThread().getName() + " ReadHoldCount:" + lock.getReadHoldCount() + " ReadLockCount:" + lock.getReadLockCount());
lockService();
}
lock.readLock().unlock();
}
}
写锁的重入性:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockClient {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static Integer count = 5;
public static void main(String[] args) {
lockService();
}
public static void lockService() {
lock.writeLock().lock();
while (--count > 0) {
System.out.println(Thread.currentThread().getName() + " WriteHoldCount:" + lock.getWriteHoldCount() + " ReadLockCount:" + lock.getReadLockCount());
lockService();
}
lock.writeLock().unlock();
}
}
ReentrantReadWriteLock的重入特性和ReentrantLock的重入特性相似,不再赘述。
锁降级指的是写锁降级成为读锁;
遵循获取写锁、获取读锁在释放写锁的顺序,写锁能够降级成为读锁。
但是,从读取锁升级到写入锁是不允许的;目的是为了保证数据的可见性。如果多个读锁获取了线程,其中任意线程成功获取写锁更新数据,对其他读线程来说是不可见的。
下面代码是对锁降级的一个简单示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockClient {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);//非公平锁
public static void main(String[] args) {
Thread t = new Thread(new Worker());
t.start();
}
static class Worker implements Runnable {
@Override
public void run() {
lock.writeLock().lock();
System.out.println("获取写锁...");
lock.readLock().lock();
System.out.println("获取读锁...");
lock.writeLock().unlock();
System.out.println("释放写锁,未释放读锁,完成了锁降级...");
System.out.println("此时是共享锁,其他线程可以获得读锁...");
lock.readLock().unlock();
System.out.println("释放读锁...");
}
}
}
getReadHoldCount():当前线程获取读锁的次数。
getReadLockCount():当前读锁被获取的次数(所有线程获取读锁的总数)。
附测试代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockClient {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
Thread t1 = new Thread(new Worker());
t1.start();
Thread t2 = new Thread(new Worker());
t2.start();
}
static class Worker implements Runnable {
@Override
public void run() {
lockService();
}
public void lockService() {
lock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " ReadHoldCount:" + lock.getReadHoldCount() + " ReadLockCount:" + lock.getReadLockCount());
lockService2();
lock.readLock().unlock();
}
public void lockService2() {
lock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " ReadHoldCount:" + lock.getReadHoldCount() + " ReadLockCount:" + lock.getReadLockCount());
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.readLock().unlock();
}
}
}