读写锁的出现是为了提高性能,思想是:读读不互斥,读写互斥,写写互斥。本文来了解一下读写锁的使用和锁降级的概念。
1. 锁的分类
- 排他锁:在同一时刻只允许一个线程进行访问,其他线程等待;
- 读写锁:在同一时刻允许多个读线程访问,但是当写线程访问,所有的写线程和读线程均被阻塞。读写锁维护了一个读锁加一个写锁,通过读写锁分离的模式来保证线程安全,性能高于一般的排他锁。
2. 读写锁
我们对数据的操作无非两种:“读”和“写”,试想一个这样的情景,当十个线程同时读取某个数据时,这个操作应不应该加同步。答案是没必要的。只有以下两种情况需要加同步:
- 这十个线程对这个公共数据既有读又有写
- 这十个线程对公共数据进行写操作
- 以上两点归结起来就一点就是有对数据进行改变的操作就需要同步
所以
java5提供了读写锁这种锁支持多线程读操作不互斥,多线程读写互斥,多线程写互斥。读操作不互斥这样有助于性能的提高,这点在java5以前没有。
3. java并发包提供的读写锁
java并发包提供了读写锁的具体实现ReentrantReadWriteLock
,它主要提供了一下特性:
- 公平性选择:支持公平和非公平(默认)两种获取锁的方式,非公平锁的吞吐量优于公平锁;
- 可重入:支持可重入,读线程在获取读锁之后能够再次获取读锁,写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁;
- 锁降级:线程获取锁的顺序遵循获取写锁,获取读锁,释放写锁,写锁可以降级成为读锁。
4. 先看个小例子
读取数据和写入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import java.util.HashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Demo { private HashMap<String,String> map = new HashMap<String,String>(); private ReadWriteLock rwl = new ReentrantReadWriteLock(); private Lock r = rwl.readLock(); private Lock w = rwl.writeLock(); public void get(String key){ r.lock(); System.out.println(Thread.currentThread().getName()+" 读操作开始执行"); try{ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(map.get(key)); }finally { r.unlock(); System.out.println(Thread.currentThread().getName()+" 读操作执行完毕"); } } public void put(String key,String value){ w.lock(); System.out.println(Thread.currentThread().getName()+" 写操作开始执行"); try{ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); }finally{ w.unlock(); System.out.println(Thread.currentThread().getName()+" 写操作执行完毕"); } } }
|
Main进行创建多线程测试:先来测试一下存在写的情况(只有写或者写读都有)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Main { public static void main(String[] args) { Demo demo = new Demo(); new Thread(new Runnable() { @Override public void run() { demo.put("key1", "value1"); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.get("key1"); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.put("key2", "value2"); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.put("key3", "value3"); } }).start(); } }
|
执行结果:
1 2 3 4 5 6 7 8 9
| Thread-0 写操作开始执行 Thread-0 写操作执行完毕 Thread-1 读操作开始执行 value1 Thread-1 读操作执行完毕 Thread-2 写操作开始执行 Thread-2 写操作执行完毕 Thread-3 写操作开始执行 Thread-3 写操作执行完毕
|
分析:
发现存在写的情况,那么就是一个同步等待的过程,即开始执行,然后等待3秒,执行完毕,符合第2个目录中提到的规则。
对只有读操作的情形进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class Main { public static void main(String[] args) { Demo demo = new Demo(); demo.put("key1", "value1"); demo.put("key2", "value2"); demo.put("key3", "value3"); new Thread(new Runnable() { @Override public void run() { demo.get("key1"); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.get("key2"); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.get("key3"); } }).start(); } }
|
运行结果:
1 2 3 4 5 6 7 8 9
| Thread-0 读操作开始执行 Thread-1 读操作开始执行 Thread-2 读操作开始执行 value1 Thread-0 读操作执行完毕 value2 Thread-1 读操作执行完毕 value3 Thread-2 读操作执行完毕
|
分析
在主线程中先put
进去几个数用于读的测试,下面开辟三个读线程,我们可以从执行结果中发现,其中一个线程进去之后,另外的线程能够立即再次进入,即这三把锁不是互斥的。
5. 锁降级
锁降级是指写锁将为读锁。
锁降级:从写锁变成读锁;锁升级:从读锁变成写锁。读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock
是不支持的。
1 2 3 4 5
| ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.readLock().lock(); System.out.println("get readLock."); rtLock.writeLock().lock(); System.out.println("blocking");
|
ReentrantReadWriteLock
支持锁降级,如下代码不会产生死锁。
1 2 3 4 5 6
| ReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.writeLock().lock(); System.out.println("writeLock"); rtLock.readLock().lock(); System.out.println("get read lock");
|
利用这个机制:同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock
是不支持的。
在写锁没有释放的时候,先获取到读锁,然后再释放写锁,保证后面读到的数据的一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private volatile boolean isUpdate;
public void readWrite(){ r.lock(); if(isUpdate){ r.unlock(); w.lock(); map.put("xxx","xxx"); r.lock(); w.unlock(); } String value = map.get("xxx"); System.out.println(value); r.unlock(); }
|