前言
在前面的一篇文章里,我曾经讨论了volatile修饰变量的使用。当时,经过各种分析和比较,发现在如果只有一个单线程写但是有多个线程读数据的情况下,volatile变量是一个适用的选项。在这部分,我们可以探讨另外一个选项。那就是ReentrantReadWriteLock。
应用场景
记得以前在一些社区讨论的时候,就看到有人提出过这么一些让人觉得比较纠结的场景。比如说有两个线程,他们之间共享一块数据,在一个线程写数据和一个线程读数据的时候,怎么样保证数据和逻辑的正确性。是否需要同步和加锁呢?还是完全没有必要?实际上,在前面关于volatile的介绍里已经基本上解决了。对于有多个读取数据的线程和单个写数据线程的场景。在我们看来这是一个比较理想的情况,因为对于读操作来说它本身不会带来任何的副作用,我们希望所有的这种操作能够并行。而对于写操作来说,一旦它发生作用,那么其他读数据的线程必须和它互斥,这样才能保证程序的正确性。
在这种情况下,ReentrantReadWriteLock就算是一个比较理想的选择。它本身就定义了两个锁,一个读锁,一个写锁。在所有读数据的线程来说,他们都通过获取读锁来获得数据。这个锁对于读线程来说都是并行的,他们不会互斥。而对于写锁来说,一次只有一个线程能够获得。当它获得的时候,会和其他线程互斥。这样,我们在使用他们的时候,在需要读取数据的地方加上读锁,在需要写数据的地方加上写锁就能够满足基本的要求了。
下面是一个使用ReentrantReadWriteLock的示例:
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class PricesInfo { private double price1; private double price2; private ReadWriteLock lock; public PricesInfo() { price1 = 1.0; price2 = 2.0; lock = new ReentrantReadWriteLock(); } public double getPrice1() { lock.readLock().lock(); double value = price1; lock.readLock().unlock(); return value; } public double getPrice2() { lock.readLock().lock(); double value = price2; lock.readLock().unlock(); return value; } public void setPrices(double price1, double price2) { lock.writeLock().lock(); this.price1 = price1; this.price2 = price2; lock.writeLock().unlock(); } }
这部分是定义我们需要操作的数据对象,在getPrice的两个方法中,都添加了读锁。在setPrices方法里增加了写锁。
接着,我们再定义读数据的线程Reader和写数据的线程Writer:
public class Reader implements Runnable { private PricesInfo pricesInfo; public Reader(PricesInfo pricesInfo) { this.pricesInfo = pricesInfo; } @Override public void run() { for(int i = 0; i < 10; i++) { System.out.printf("%s: Price 1: %f\n", Thread.currentThread().getName(), pricesInfo.getPrice1()); System.out.printf("%s: Price 2: %f\n", Thread.currentThread().getName(), pricesInfo.getPrice2()); } } }
public class Writer implements Runnable { private PricesInfo pricesInfo; public Writer(PricesInfo pricesInfo) { this.pricesInfo = pricesInfo; } @Override public void run() { for(int i = 0; i < 3; i++) { System.out.printf("Writer: Attempt to modify the prices.\n"); pricesInfo.setPrices(Math.random() * 10, Math.random() * 8); System.out.printf("Writer: Prices have been modified.\n"); try { Thread.sleep(2); } catch(InterruptedException e) { e.printStackTrace(); } } } }
在测试程序里,我们定义了5个读线程和一个写线程,通过运行他们我们可以来查看运行的结果:
/** * Main class of the Example. Create and start two initialization tasks * and wait for their finish * */ public class Main { /** * Main method of the class. Create and star two initialization tasks * and wait for their finish * @param args */ public static void main(String[] args) { PricesInfo pricesInfo = new PricesInfo(); Reader[] readers = new Reader[5]; Thread[] threadsReader = new Thread[5]; for(int i = 0; i < 5; i++) { readers[i] = new Reader(pricesInfo); threadsReader[i] = new Thread(readers[i]); } Writer writer = new Writer(pricesInfo); Thread threadWriter = new Thread(writer); for(int i = 0; i < 5; i++) { threadsReader[i].start(); } threadWriter.start(); } }
如果我们去分析程序运行的结果,会发现只要是在执行写操作结束后,所有读线程获得的数据都会是一致的。这样就保证了正确性。
和volatile的比较
使用读写锁的方式在单个写线程加多个读线程的情况下,其实差别不大。在使用volatile变量的时候,因为每次操作修改的结果对于全局都是可见的。那么在只有一个写线程的情况下,只有这个线程可以唯一修改数据。不会存在有几个写线程而产生的竞争条件。对于有多个写线程的情况下,volatile变量就不能保证数据的一致性了。而ReentrantReadWriteLock却可以有锁的机制保证互斥。它同时也尽可能保证了足够大的并行性。
和synchronized的比较
synchronized的修饰一般限制这个区域是不可重入的。每次只有一个线程可以访问。这种强烈的互斥性使得每次不管是读数据还是写数据都只能有一个线程可以操作。在希望有多个读线程可以并行执行的情况下,它并不是一个理想的选择。
总结
ReentrantReadWriteLock是一个解决单线程写和多线程读的理想方法。它采用类似于读写分离的思路设定了读锁和写锁。对于这两个锁的访问保证尽可能大的读并行和写互斥。另外,在一定的条件下写锁可以转换成读锁,而读锁却不能转换成写锁。
参考资料
http://stackoverflow.com/questions/6637170/reentrantreadwritelock-vs-synchronized
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
相关推荐
inux 读写锁应用实例 /*使用读写锁实现四个线程读写一段程序的实例,共创建了四个新的线程,其中两个线程用来读取数据,另外两个线程用来写入数据。在任意时刻,如果有一个线程在写数据,将阻塞所有其他线程的任何...
读写锁实现例子
linux下用标准C写的线程池全部实现代码,其中包含有读写锁的实现和应用
各种锁汇总,乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁、行级锁等
读写锁 应用程序接口 创建 new( maxReaders, ?waitLogTimeout, ?waitLogger) 功能级别 read(func, ?wait) write(func, ?wait) 小鬼水平 // read prepareRead(?wait) releaseRead() // write prepareWrite(?wait) ...
目录面试三连什么是读写锁StampedLock横空出世StampedLock三种模式基本语法StampedLock完整的demo让StampedLock性能更上一楼的乐观读你了解乐观读的应用场景吗使用StampedLock的注意事项总结 面试三连 面试官:了解...
易语言读写锁源码,源码是多线程的应用例程,代码写得不错,想学习多线程的朋友可以下载看看了。强烈推荐。
易语言读写锁源码,源码是多线程的应用例程,代码写得不错,想学习多线程的朋友可以下载看看了。强烈推荐。
6.JUC并发工具类在大厂的应用场景详解 (1).pdf ...8、读写锁ReentrantReadWriteLock&StampLock详解.pdf 9、并发容器 (Map、List、Set) 实战及其原理.pdf 10、阻塞队列BlockingQueue 实战及其原理分析.pdf
SQLite实质上是将数据写入一个文件,通常情况下,在应用的包名下面都能找到xxx.db的文件,拥有root权限的手机,可以通过adb shell,看到data/data/packagename/databases/xxx.db这样的文件。我们可以得知SQLite是...
整数是一个受锁保护的简单计数器,该计数器标记缓冲区/队列中的最后一个可用插槽(客户端需要)。 如果缓冲区足够大(例如无限大),则简单的原子增量就足够了(例如fetch_and_add)。 每个“ OPS”数组的元素都是...
通过具体案例,我们理解了Zookeeper的加锁原理,包括并发问题处理、羊群效应、读写锁机制等。此外,文章还详细介绍了使用Zookeeper构建注册中心的步骤,包括服务注册、节点监听、自动发现和负载均衡等关键概念。这些...
3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高 读写锁也叫共享-独占锁。当...
(将抓记录卡插入到读写器中→单击“抓记录卡” →输入姓名→单击“确定” →取出卡片→插入要读取开门记录的那把锁→门锁的绿灯亮,待绿灯灭了之后→将卡取出→将抓记录卡插入读写器中→启动主画面下方“B级”栏→...
大致内容包括: Java内存模型 Java内存交互协议 Java的线程 Netty的并发编程分析 正确的使用锁 volatile的正确使用 CAS指令和原子类 线程安全类 读写锁的应用
Delphi开发读写ISO11784/11785低频RFID动物标签EM4205/4305、EM4469/4569,将EM4205/4305卡制作成ID门禁卡等。 EM4305/EM4205卡的EEPROM储存空间为512位,分为16个块,每个块32位,块1为UID块,块2为密码块,块4为...
四、开发一个BLE应用 4.1 准备 4.2 在清单文件中配置权限 4.3 检查设备 4.4 通过UUID扫描指定的设备 4.5 连接设备 4.6 获取GATT服务,进行读写通知操作 4.6.1 读取数据 4.6.2 写入数据 4.6.3 数据通知 4.7 断开连接 ...
本书从对Posix IPC和System V IPC的内部结构开始讨论,全面深入地介绍了4种IPC形式:消息传递(管道、FIFO、消息队列)、同步(互斥锁、条件变量、读写锁、文件与记录锁、信号量)、共享内存(匿名共享内存、具名...
本文实例讲述了Python使用文件锁实现...记录上锁是读写锁的一种扩展类型,它可用于有亲缘关系或无亲缘关系的进程间共享某个文件的读与写。被锁住的文件通过其描述字访问,执行上锁操作的函数是fcntl。这种类型的锁在内
本书从对Posix IPC和System V IPC的内部结构开始讨论,全面深入地介绍了4种IPC形式:消息传递(管道、FIFO、消息队列)、同步(互斥锁、条件变量、读写锁、文件与记录锁、信号量)、共享内存(匿名共享内存、具名...