《Java源码分析》:ReadWriteLock(第一部分)
对象的方法中一旦加入synchronized关键字修饰,则任何时刻都只能有一个线程能够访问synchronized修饰的方法,例如:在Hashtable类中就是所有方法都使用了synchronized关键字修饰,这虽然解决了线程安全的问题,但是也降低了程序的并发性能(吞吐量)。在Hashtable中,即使我们使用get方法也只能只有一个线程能够访问,而本质上get方法并不修改内容,即使多个线程一起访问get方法也不存在线程不安全。基于此,就有了ReadWriteLock类,ReadWriteLock解决了这个问题.
对于ReadWriteLock,当写操作时,其它线程无法读取或写入数据,而当读操作时,其它线程无法写数据,但却可以读取数据。
ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念。显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生。但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了。所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能否改造为读写锁。
读写锁(ReadWriteLock):分为读锁(ReadLock)和写锁(WriteLock),多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多线程同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读数据的时候上读锁,写数据的时候上写锁!
ReadWriteLock是一个接口,而ReentrantReadWriteLock是其的具体的实现类。
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁
线程进入读锁的前提条件 :
1、没有其他线程的写锁,
2、没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件 :
1、没有其他线程的读锁
2、没有其他线程的写锁
说到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。
然后就是总结这个锁机制的特性了:
(a). 重入方面 其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
(b).WriteLock可以 降级 为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a),呵呵.
(c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
(d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
(e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
(f):此锁最多支持 65535 个递归写入锁和 65535 个读取锁。试图超出这些限制将导致锁方法抛出 Error。
上面说的都是ReadWriteLock的理论知识。在分析ReentrantReadWriteLock的内部实现之前,我们先看ReentrantReadWriteLock一个例子。
public class ReadWriteLockDemo {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
int threadNum = 2;
ReadWriteLockDemo demo = new ReadWriteLockDemo();
RWMap rwMap = demo.new RWMap();
//开启threadNum个读线程
for(int i=0;i<threadNum;i++){
new Thread(){
@Override
public void run() {
int j = 0;
while(j++<2){
rwMap.get(j);
}
}
}.start();;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//threadNum个写线程
for(int i=0;i<threadNum;i++){
new Thread(){
@Override
public void run() {
int j = 0;
while(j++<2){
rwMap.put(j,j+"");
}
}
}.start();;
}
}
class RWMap{
private Map<Integer,String> map;
private ReadWriteLock rwLock ;
private Lock readLock ;
private Lock writeLock ;
public RWMap(){
map = new HashMap<Integer,String>();
rwLock = new ReentrantReadWriteLock();
readLock = rwLock.readLock();
writeLock = rwLock.writeLock();
initMap();
}
//初始化map
private void initMap() {
int len = 10;
for(int i= 0;i<len;i++){
map.put(i, i+"");
}
}
//get方法
public String get(int key){
readLock.lock();
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+"正在读map中key="+key+"的数据。。。");
try{
String value = map.get(key);
try {
Thread.sleep(1000);//一定时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+"读的数据内容为:"+value);
return value;
}finally{
readLock.unlock();
}
}
public void put(int key,String value){
writeLock.lock();
try{
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+"正在将键值对(key,value)=("+key+","+value+")写入Map中");
map.put(key, value);
try {
Thread.sleep(1000);//一定时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+"写数据结束");
}finally{
writeLock.unlock();
}
}
}
}
运行结果:
2016-08-03 17:34:06 Thread-1正在读map中key=1的数据。。。
2016-08-03 17:34:06 Thread-0正在读map中key=1的数据。。。
2016-08-03 17:34:07 Thread-1读的数据内容为:1
2016-08-03 17:34:07 Thread-1正在读map中key=2的数据。。。
2016-08-03 17:34:07 Thread-0读的数据内容为:1
2016-08-03 17:34:08 Thread-1读的数据内容为:2
2016-08-03 17:34:08 Thread-3正在将键值对(key,value)=(1,1)写入Map中
2016-08-03 17:34:09 Thread-3写数据结束
2016-08-03 17:34:09 Thread-2正在将键值对(key,value)=(1,1)写入Map中
2016-08-03 17:34:10 Thread-2写数据结束
2016-08-03 17:34:10 Thread-2正在将键值对(key,value)=(2,2)写入Map中
2016-08-03 17:34:11 Thread-2写数据结束
2016-08-03 17:34:11 Thread-0正在读map中key=2的数据。。。
2016-08-03 17:34:12 Thread-0读的数据内容为:2
2016-08-03 17:34:12 Thread-3正在将键值对(key,value)=(2,2)写入Map中
2016-08-03 17:34:13 Thread-3写数据结束
从运行结果可以看出两点:
1、读锁是可以被多线程拥有的,即允许多个线程同时读取数据。
2、写锁是互斥的,写锁只能被一个线程拥有。当写入数据时,其它线程是不能够读数据和写入数据的。
下篇博文就来看下ReentrantReadWriteLock类的具体实现。
小结
当多线程并发时,如果读操作远远要大于写操作,我们就可以使用ReadWriteLock来进行加锁控制实现线程安全。
如果读操作和写操作差不多,则使用ReadWriteLock就没有多大优势。
参考资料
1、http://zk1878.iteye.com/blog/1005160
2、http://www.cnblogs.com/liuling/archive/2013/08/21/2013-8-21-03.html
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。