读写锁案例 + 小小总结
//读这篇文章的时候,建议先看一下,我并发专题中的 Lock.lock() 加锁的过程的文章
//我在写读写锁的时候,好多东西,好多理念都在lock()中体现
// 读读串行
// 读写串行
// 写写串行
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Integer data = 0;
public void get(){
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "ready read");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "数据 = " + data);
readWriteLock.readLock().unlock();
}
public void put(Integer data){
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "ready write");
try {
Thread.sleep(1000);
this.data = data;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写入的数据 = " + data);
readWriteLock.writeLock().unlock();
}
public static void main(String[] args) {
final ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(()->readWriteLockDemo.get()).start();
new Thread(()->readWriteLockDemo.put(new Random().nextInt(10))).start();
}
}
}
/**
put这个操作 加了写锁,写锁是排他锁,加上写锁后,不能在加其它锁,加上写锁后,读锁加上后不能加其它任何锁
get 读锁 可以允许很多读写进来的
Thread-11ready write
Thread-11写入的数据 = 8 这两个输出是不能分开的,write锁是排他锁
Thread-2ready read
Thread-6ready read
Thread-4ready read
读锁,这个是能分开的
为什么会这样呢? 下面源码解释一下
*/
其实,不管是lock也好,读写锁也罢,都是有一个状态state
这个state在lock中 0 初始状态 1、加锁状态 N(N > 1)重入锁
在读写锁中,也是有state这个状态的,但是,这个state是由32个bit位组成的,
0000 0000 0000 0000 0000 0000 0000 0000
前16个bit 读锁
后16个bit 写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
System.out.println("rrrrrrrrrrrrrr");
readWriteLock.writeLock().lock();
System.out.println("wwwwwwwwwwwwww");
//看到这个代码的时候,我有一个想法?
//首先,我是读锁,输出了 rrrrrrrrrrrrrr
//readWriteLock.writeLock().lock(); 我能否升级一下呢? 把读锁升级为写锁 当然是 不能
//为什么锁不能升级?
//因为,读锁可以并发,如果,现在有两个线程,t1 t2两个线程现在都是读锁,要升级写锁
//t1在等待t2释放锁,t2在等t1释放锁,死锁了
//读锁是共享锁,比如有100个线程同时占有这个锁,都在等待对方释放锁,无法保证100%释放锁。
//一旦写锁成功上锁, 写锁是排它锁,state从0变成1,只有一个写线程
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
System.out.println("wwwwwwwwwwwww");
readWriteLock.readLock().lock();
System.out.println("rrrrrrrrrrrrr");
//那,既然锁升级不能行,那锁降级呢? 其实是可以的
//锁降级就可以了,因为写锁是排他锁,可以保证让读100%占用到锁。
//锁降级,既然写锁是排它锁,为什么这个读锁不能等写锁释放再拿?
//因为,它一旦释放了锁,瞬间就会有很多线程来抢占这个state,你能否抢到就不一定了
写锁加锁过程
//这边和Lock.lock() 是一模一样的
public final void acquire(int arg) {
//tryAcquire(arg) 尝试加锁失败,和Lock.lock()一模一样,初始化同步队列
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static final int SHARED_SHIFT = 16;
//1 << SHARED_SHIFT 1 0000 0000 0000 0000
//EXCLUSIVE_MASK 0 1111 1111 1111 1111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread(); //拿到当前线程
//第一个线程进来,c肯定为0
int c = getState();
//把写的状态拿出来
int w = exclusiveCount(c);
//c != 0 证明已经上锁成功了 可能是写锁 也可能是读锁
if(c != 0){
//w == 0 证明加的不是写锁
//current != getExclusiveOwnerThread() 不是重入锁
if (w == 0 || current != getExclusiveOwnerThread()){
return false;
}
//这把锁能重入的最大次数
if (w + exclusiveCount(acquires) > MAX_COUNT){
throw new Error("Maximum lock count exceeded");
}
//这个是锁重入
setState(c + acquires);
return true;
}
//writerShouldBlock() 这个判断是 是否需要排队
//如果,不需要排队,看看能否加锁成功
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)){
return false;
}
//这边肯定是已经加锁成功了,设置当前线程
setExclusiveOwnerThread(current);
return true;
}
//这个和lock() 里面的那个东西是一模一样的
//就是看看 时候需要等待
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
读锁加锁过程
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
private transient HoldCounter cachedHoldCounter;
private transient ThreadLocalHoldCounter readHolds;
static final int SHARED_SHIFT = 16;
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0){
doAcquireShared(arg);
}
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount(c) != 0 这个是用来判断是否有写锁的 != 0 证明是有写锁的
//getExclusiveOwnerThread() != current 但是加锁线程不是当前线程
//因此,有写锁了,但是进来的又不是那个线程(不是重入锁),加锁失败,返回 -1
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current){
return -1;
}
//如果还没有加锁,c为0 右移16位,r为0
//如果已经加锁了,此时肯定是读锁,c不为0,右移16位,r不为0
//readerShouldBlock() 读锁是共享锁,为什么还要去判断有没有等待队列?
//就是为了防止,此时,有写锁在排队
//r < MAX_COUNT 不能达到最大值
//compareAndSetState(c, c + SHARED_UNIT)
// static final int SHARED_UNIT = (1 << SHARED_SHIFT);
int r = sharedCount(c);
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0 证明还没有加锁过,不管是读锁 还是 写锁 都没有
//firstReader == current 这个严格意义上来说不是重入锁,只算是firstReader的次数(下面图片解释)
//这边的步骤,建议你配置着下面的图,进行逻辑分析一下。
//第一个线程(读锁)就是上面的那个
//那其他的呢?HoldCounter rh = cachedHoldCounter; 这个HoldCounter里面维护了一个count
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
}else {
//其他线程,整了一个缓存数据结构 cachedHoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)){
//为当前线程初始化一个rh
//有个疑问?那么多线程,怎么区分呢? ThreadLocal
cachedHoldCounter = rh = readHolds.get();
//当前线程已经被初始化对象了
//count + 1 设置 rh对象
}else if (rh.count == 0){
readHolds.set(rh);
}
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
上面的图解释了,为什么上面说的 那个 严格意义上来说 不是重入锁?
在Lock.lock() 源码中,如果c为 > 1 就是锁的重入,这个读写锁就不一定是了。
读锁是共享锁,每次来一个读锁,都会把 r + 1 的 ,因此,这个r就是个总和。
如果说,想看看哪个线程是重入的,就是自己线程维护的 ReaderHoldCount 这个值就行了。