对于一个读写锁来说,同一时刻,可以有多个线程拿到读锁,只有一个线程拿到写锁。一旦一个线程拿到写锁,他们任何想要获取读锁或者写锁的线程,都必须等待。
考虑下面这种情况
Thread1: A.readlock -> ... (已经拿到读锁)
Thread2: A.readlock->... (Thread1拿到读锁之后,Thread2也去请求读锁)
很简单,这种情况Thread2也可以顺利拿到读锁,没有任何问题。
如果这时候有个Thread3,他在Thread1拿到读锁之后,Thread2请求读锁之前,去请求写锁。
Thread1: A.readlock -> ... (已经拿到读锁)
Thread3: A.writelock->...(请求写锁)
Thread2: A.readlock->...
那么这种情况下,Thread2和Thread3会继续往下执行么?
Thread3显然是要等待的。Thread2呢?答案是:不一定。
这要取决于读写锁的实现方法。
linux内核的rwlock是读写锁的最简单的参考实现。它用一个整数counter代表一个rwlock。0代表没有人占有锁,-1代表有一个线程持有着写锁, 正整数n代表有n个线程持有读锁。要拿读锁时,如果counter小于0, 则继续循环测试,直到counter非负。然后给counter加1,拿锁成功。(当然,得保证“在counter非负的情况下加1”这个操作的原子性,一般通过spinlock或者bit spinlock实现)。可见,如果已经有一个线程拿着读锁还未释放,另一个线程获取读锁会立即成功。
这个实现很简单,但是存在公平性的问题:写者可能会被饿死。 如果有很多线程相续拿到读锁然后释放读锁,保持counter的值始终大于0,那写者就一直拿不到写锁。http://lwn.net/Articles/364583/
一个办法是在rwlock元数据中增加一个标记,代表是否有写者在等待读者。读者要拿读锁时,先要等待这个标记的清除。笔者曾经在嵌入式环境中,使用和修改过这样的读写锁。更加先进的方法,是让等待者排一个FIFO队列,比较著名的是MCS lock和CLH lock。
Java的ReentrantReadWriteLock,就是基于CLH算法。
正是由于这个排队算法,由于Thread2在Thread3之前,因此Thread2必须等Thread3拿到锁,做完事情,并且释放,才能获得读锁。
下面是一个简单的实验的代码
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main {
public static void main(String[] args) {
final ReentrantReadWriteLock.ReadLock readLock;
final ReentrantReadWriteLock.WriteLock writeLock;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
readLock = lock.readLock();
writeLock = lock.writeLock();
System.out.println("main: before readLock.lock()");
readLock.lock();
System.out.println("main: after readLock.lock()");
Thread tw = new Thread() {
@Override
public void run() {
System.out.println("tw: before writeLock.lock()");
writeLock.lock();
System.out.println("tw: after writeLock.lock()");
}
};
Thread tr = new Thread() {
@Override
public void run() {
System.out.println("tr: before readLock.lock()");
readLock.lock();
System.out.println("tr: after readLock.lock()");
}
};
try {
tw.start();
Thread.sleep(1000);
tr.start();
tw.join();
tr.join();
} catch (InterruptedException ie) {
}
}
}