多线程读取并修改一个资源时,我们过去通常使用synchronized同步锁,这个是有性能损失的,很多情况下,资源对象总是被大量并发读取,偶尔有一个线程进行修改,也就是说:以读为主,修改不是很频繁。在JDK5.0版本以后,增加了Lock家族用于增强和扩展锁的机制,ReEntranceLock基本上可以替代原有的synchronized同步锁,而其中用ReentrantReadWriteLock更是可以获得比synchronized更高并发性能。高并发性能被认为是JDK5带来的最重要改进。
ReentrantReadWriteLock被大量使用在缓存中,因为缓存中的对象总是被共享大量读操作,偶尔修改这个对象或者其中的子对象,比如状态,那么只要通过ReentrantReadWriteLock来更新对象就可以了,这就实现了并发中对原子性的要求。
最近基于应用中的适配问题,研究配置文件的应用,其中涉及到一个重要编码场景,即配置信息的初始化,此场景即面临着配置数据在缓存中读写并发问题:
l 通过异步任务将配置数据从文件读取,缓存到内存;
l 同时接受各个线程中对配置属性的同步读取;
l 各处读取接口需要共享并发锁,高效读取。
因此,需要对该缓存对象展开高性能读写实践。
JAVA并发、读写互斥、读锁重入
配置文件初始化过程中,缓存配置对象由一个线程作数据异步写入和多线程数据同步读取。
该问题需要统筹解决三个主要细节:
l 读写在不同线程,尽管要求尽早init写入,但实际上由于写入为耗时线程,面临读并发;
l 要求所有读操作要等待写完后再读,不可直接返回空值;
l 写操作与读操作互斥,但是所有的读操作无需互斥,并需要高效率并发读取;
技术思路:
l 读操作与写操作进行同步互斥;
l 读操作并发不互斥;
l 数据未写入时,所有读操作wait;
l 写入完成时,所有读操作重入;
根据以上需求,考察了Java中的常见并发编程实践,伪代码示例小结如下。
Objectobj = new Object(); public (synchronized) void myMethod(){ syschronized(obj){ if(!(ok to process)) obj.await(); //add your code here obj.notifyAll(); } }
Lock mlock = new ReentrantLock(); public void myMethod(){ mlock.lock(); try{ //add your code here } finally{ mlock.unlock(); } }
唤醒:
Lock mlock = new ReentrantLock(); Condition mCondition = mlock.newCondition(); public void myMethod(){ mlock.lock(); try{ if(!(ok to process)) mConditon.await(); //add your code here mConditon.signalAll(); } finally{ mlock.unlock(); } }
官方说明,适用于很多线程从一个数据结构读取数据而很少的线程修改其中数据
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //该锁可以被多个读操作共用的读锁,但会排斥写操作 Lock readLock = rwl.readLock(); //该锁排斥所有其他的读操作和写操作 Lock writeLock = rwl.writeLock(); public int getNumber(){ readLock.lock(); try{ //add your code here } finally{ readLock.unlock(); } } public void setNumber(){ writeLock.lock(); try{ //add your code here } finally{ writeLock.unlock(); } }
唤醒:
该机制的Condition条件与唤醒的使用较为复杂:
l 首先只能针对写锁进行条件await和signal,读锁不能使用Condition,否则抛出异常;
l 因为其写锁可以降级为读锁,而读锁不能升级为写锁,因此在读操作中需要手工切换到写锁后,再唤醒后切回读锁。
写锁中:
writeLock.lock(); //locks all readers and writers // do write data hasData.signalAll(); writeLock.unlock();
读锁中:
readLock.lock(); //blocks writers only try{ if(!checkData()) //check if there's data, don't modify shared variables { readLock.unlock(); writeLock.lock(); // need to lock the writeLock to allow to use the condition. // only one reader will get the lock, other readers will wait here try{ while(!checkData()) // check if there' still no data { hasData.await();//will unlock and re-lock after writer has signalled and unlocked. } readLock.lock(); // continue blocking writer } finally { writeLock.unlock();//let other readers in } } //there should be data now readData(); // don't modify variables shared by readers. }finally{ readlock.unlock(); //let writers in }
保证读取到的值是当前最新的;用该关键字声明的变量具备可见性;
为并发字段增加此修饰,屏蔽jdk工作内存的切回,使各个线程实时共享最新的数据变化;
l 非公平锁(默认)
这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。
l 重入性
延续了原有Sync锁的特点,读写锁允许读线程和写线程重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
l 锁降级
写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
l 锁获取中断
读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
l 条件变量
写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个UnsupportedOperationException异常。
根据以上分析,ReentrantReadWriteLock非常适合本场景的需求,读写互斥,写入之后,读取能共享锁,使得少量写入和大量读取取得最高并发性能。
① 启动初始化,同时启动多个读操作
// async init FitApi.initFit(MyApplication.getContext(), mParseListener); // sync get for (int i =0; i<5; i++) {
// 线程示例演示需要,非推荐写法 new Thread(new Runnable() { @Override public void run() { FitApi.getFitData(); } }).start(); } Response response = FitApi.getFitData(); tv_content.setText(response.toString());
② 写入操作和唤醒操作
@Override protected T doInBackground(V... params) { LogUtil.e("Parse wLock.lock() -->"); FitHelp.wLock.lock(); try { // write operation ... mFitInterR.get().onDataParsed((Response) result1.response); } return result; } finally { // 唤醒 Condition.signalAll() FitHelp.initCondition.signalAll();
if (FitHelp.wLock.isHeldByCurrentThread()) { LogUtil.e("Parse wLock.unlock() <--"); FitHelp.wLock.unlock(); } } }
③ 读操作和被唤醒
public Response getFitData() { // wait until the mResponse has been created. LogUtil.e("rLock.lock() -->"); rLock.lock(); try { if (!hasData()) { LogUtil.e("rLock.unlock() <--"); rLock.unlock(); LogUtil.e("wLock.lock() -->"); wLock.lock(); // need to lock the writeLock to allow to use the condition. try { while (!hasData()) { LogUtil.e("mResponse == null --> Condition.await()"); initCondition.await(); // will unlock and re-lock after writer has signalled and unlocked. } LogUtil.e("rLock.lock() -->"); rLock.lock(); // continue blocking this reader } finally { LogUtil.e("wLock.unlock() <--"); wLock.unlock(); // let other readers in } } // there should be data now return readData(); } catch (InterruptedException e) { e.printStackTrace(); } finally { LogUtil.e("readData done -- sleep(1000)" , Thread.currentThread().getName()); SystemClock.sleep(1000); LogUtil.e("rLock.unlock() <--"); rLock.unlock(); } return new Response(); }
④ 并发日志
写入唤醒后,6个读操作并发读取,并没有受到各自sleep(1000)的影响。