背景介绍
最近转行做java后台了,对于java的并发完全不熟悉,于是到网上各种搜资料。
接到的一个任务是一个很简单从数据库里面读取一些表到内存里面,然后做一个缓存。
读数据库用的EJB3的Entity Bean; 服务是基于JBoss的MBean;访问接口是EJB3的stateless session bean;
因为session bean是多线程的方式访问Mbean的,所以要考虑内存数据的读写问题。
于是有个前辈就告诉我要用读写锁,也就是ReentrantReadWriteLock了,前辈还顺口提了一句说:“极端情况有可能写锁会一直被阻塞”
因为不是很清楚线程被阻塞时会发生什么,自己写了两个类来测当读锁被阻塞时会怎样。
一个测试类,使用读写锁保护一个int值。
package cfy.study.concurrency;
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class ReadWriteLockTest {
private static int data = 0;
private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private static ReadLock rLock = rwLock.readLock();
private static WriteLock wLock = rwLock.writeLock();
public static void read(){
String threadName = Thread.currentThread().getName();
System.out.println("\n"+threadName+" wait for ReadLock");
rLock.lock();
try{
System.out.println(threadName+"read data:"+data);
System.out.println("wait for awhile");
long time = System.currentTimeMillis();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
long duration = System.currentTimeMillis() - time;
System.out.println("\n I sleep for:"+duration+"(millis)");
}
finally{
System.out.println(threadName+"release read lock");
rLock.unlock();
}
}
public static void write(){
String threadName = Thread.currentThread().getName();
System.out.println("======== "+threadName+",wait for get write lock");
wLock.lock();
try{
System.out.println("=========== get write lock!");
Random rand = new Random();
data = rand.nextInt(10000);
System.out.println("=========== finish write, release lock");
}
finally{
wLock.unlock();
}
}
}
Junit测试类
package cfy.study.concurrency.test;
import org.junit.Test;
import cfy.study.concurrency.ReadWriteLockTest;
public class RWLockTest {
@Test
public void test() {
Thread[] threads = new Thread[4];
for(int i = 0; i<3;++i){
threads[i] = new Thread(){
public void run(){
ReadWriteLockTest.read();
}
};
threads[i].start();
}
threads[3] = new Thread(){
public void run(){
ReadWriteLockTest.write();
}
};
threads[3].start();
for(int i = 0;i<threads.length;++i){
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("parent threads exit!");
}
}
//某一次测的情况:
//Thread-2 wait for ReadLock
//Thread-2read data:0
//wait for awhile
//
//Thread-0 wait for ReadLock
//Thread-0read data:0
//wait for awhile
//
//Thread-1 wait for ReadLock 这里,线程1等待读锁
//======== Thread-3,wait for get write lock 线程3等待写锁
//
// I sleep for:500(millis)
//Thread-2release read lock
//
// I sleep for:500(millis)
//Thread-0release read lock
//=========== get write lock! //看起来线程1是被阻塞了,拿到写锁
//=========== finish write, release lock
//Thread-1read data:4984
//wait for awhile
//
// I sleep for:500(millis)
//Thread-1release read lock
//parent threads exit!
//从测的情况来看,当有写锁的申请的时候,有些时候是读锁先得到,小部分
//情况是读锁先得到,这估计应该是某种随机控制,
//避免出现写锁一直被读锁阻塞的情况
见上面的注释,其中某一次我发现并不是读锁就不会被阻塞,从理论上来说,读锁之间是不会被阻塞的,当出现
“Thread-1 wait for ReadLock ” 时,前面只有读锁,从上面的前辈告诉我的情况来说,下面是不应该先得到写锁的。 所以我估计应该是有某种随机性的机制来防止写锁一直被阻塞的这种情况的,一直猜下去也不是办法,还是去看看源代码比较靠谱,于是就在源代码里面看到这个部分:
ReentrantReadWriteLock代码片段
首先是构造函数的部分,默认的构造函数调用了一个带一个bool参数的构造函数,那个bool参数指定此类的sync是FairSync还是NonfairSync
/**
* Creates a new ReentrantReadWriteLock with 使用默认排序属性(不公平)来创建一个读写锁
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new ReentrantReadWriteLock with 使用传入的参数(公平策略)来创建一个读写锁
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = (fair)? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
望文生义猜一猜就是此锁的同步机制是公平的同步还是不公平的同步,什么叫做公平和不公平呢,不知道,往下找到上面两个东西的定义。
———-题外话:能够看到这个部分的读者想必比较有毅力了,这里推荐一篇文章点击打开链接,这里面提到了:
“事实上在ReentrantReadWriteLock里锁的实现是靠java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的。这个类看起来比较眼熟,实际上它是AQS的一个子类,这中类似的结构在CountDownLatch、ReentrantLock、Semaphore里面都存在。同样它也有两种实现:公平锁和非公平锁,也就是java.util.concurrent.locks.ReentrantReadWriteLock.FairSync和java.util.concurrent.locks.ReentrantReadWriteLock.NonfairSync”
看起来这两个东西的机制比较复杂,我就没有追下去,直接看注释了,也许后面有能力的话会把这些原理追下去。
/**
* Nonfair version of Sync 这是无参构造函数默认使用的同步方式,主要就看看这个吧
*/
final static class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock(Thread current) {
return false; // writers can always barge
}
final boolean readerShouldBlock(Thread current) {
/* As a heuristic to avoid indefinite writer starvation, 为了避免写者无限饥饿的问题
* block if the thread that momentarily appears to be head 当队列中只有一个写者的时候,把瞬间出现在队列头部的线程阻塞
* of queue, if one exists, is a waiting writer. This is (这里不太有把握,欢迎纠正)
* only a probablistic effect since a new reader will not 这个只是一种随机的效果。
* block if there is a waiting writer behind other enabled 因为当写线程的前面还有其他的读线程的时候,此时新进来的读线程
* readers that have not yet drained from the queue. 是不会被阻塞的
*/
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Fair version of Sync 公平锁
*/
final static class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock(Thread current) {
// only proceed if queue is empty or current thread at head 只有当队列为空或者当前线程在队列头部的时候
return !isFirst(current);
}
final boolean readerShouldBlock(Thread current) {
// only proceed if queue is empty or current thread at head
return !isFirst(current);
}
}
就望文生义的看一看吧(真是惭愧),大概证明了我的猜想,这里用了一种随机的机制来避免写线程的无限阻塞问题。