一:共享锁
java中一般定义写锁为独享锁而读锁为共享锁,而java中读锁单独存在是没有意义的,原因如下:
- 读锁只是对资源的读取没有对资源的修改,那么效率会远小于没有锁的读。
java中只要读锁是依赖于写锁的,不同于写锁的所操作:获取锁然后操作(获取没有获取到锁进入等待阻赛队列)。读锁一般都是直接调用wait()方法进入线程阻赛的。其原因是,读锁一定保证线程中的写锁(至少是当前线程中的写锁)执行完毕。也就是说在一组读写锁中,写锁的优先级要大于读锁的。那么,总结一下读写锁的过程区别:
- 写锁:获取到锁,执行代码,释放锁,后续队列中的线程继续(或者获取不到锁,进入队列阻赛等待)
- 读锁:进入阻赛,等待所有写锁执行完毕,然后所有的线索同时被通知执行。
二:共享锁的过程
这里模拟一个比较复杂的读写锁过程吧:
- 独享锁A获取到锁,进入代码执行
- 独享线程B,C,D发现锁被占用,只好到FIFO队列等待被唤醒
- 共享锁E查询锁状态,若是锁没有被占用执行查询,否则继续执行一下步骤
- 共享锁E发现线程被占用,进入FIFO,并且发现前面有线程C,D
- 贡献锁线程F,G执行E的步骤
执行线程锁释放功能
- 线程A释放锁,通知B,B执行完毕通知C
- C执行完毕,通知E,E这时候联结F,G准备执行
- E,F,G同时执行
三:共享锁源码分析
- 共享锁源码分析,直接从线程wait()执行,这里的获取锁等方法就不讲了
- 这里使用Synchronized单独对读方法枷锁模拟,解析CountDownLatch
(1)结构
(2)CountDownLatch(int)
//实例化,同时锁存器+1
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//实现AQS的一个子类,初始化同步构造器
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
...
}
(2)await()
//线程等待
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//尝试获取到锁
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//响应中断
if (Thread.interrupted())
throw new InterruptedException();
//获取到锁(否则加入等待队列,至于如何封装成节点,加入FIF的就不用说了)
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
当同步状态state为0时,表示当前为无锁状态,处于await状态下的线程都可以执行。在读写锁的方法中,只有写锁能够修改state的状态值(毕竟读锁状态下只提供一个初始化的构造方法)。一旦写锁释放了锁,那么在处于等待队列的读锁都可以运行,直到下一个写锁方法再次上锁。
(3)countDown()
//释放锁存器
public void countDown() {
sync.releaseShared(1);
}
//尝试对锁存器操作
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这是调度线程之间关系的方法:如在一个读写方法中存在这样一个等待队列:写读读读读写。那么这个锁存器就记录了当前写到下一个写之间的所有读的个数,一旦当前写释放锁,通知后续的个数的读同时进行。相当于一个通知的功能。
四:功能简述
上面主要的方法讲述完成,下面我们来简要的说一下读写锁下的加锁/解锁方式:
- 写锁加锁执行
- 读锁调用构造方法,锁存器+1,进入等待队列,并响应中断(。。。)
- 写锁执行完毕,通知队列后的锁存器中个数的读锁进行执行,同时重置锁存器个数
写锁的核心就是:运行到写锁时,方法变成无锁方法。