本系列文章:
多线程(一)线程与进程、Thread
多线程(二)Java内存模型、同步关键字
多线程(三)线程池
多线程(四)显式锁、队列同步器
多线程(五)可重入锁、读写锁
多线程(六)线程间通信机制
多线程(七)原子操作、阻塞队列
多线程(八)并发容器
多线程(九)并发工具类
多线程(十)多线程编程示例
一、ReentrantLock
ReentrantLock(可重入锁),主要利用CAS+AQS队列来实现,支持公平锁和非公平锁。
ReentrantLock使用示例:
private Lock lock = new ReentrantLock();
public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
}finally {
lock.unlock();
}
}
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
- 1、ReentrantLock对象是非公平锁
如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取; - 2、ReentrantLock对象是公平锁
如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
1.1 ReentrantLock的特点*
- 1、可重入锁
可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
关于可重入性,示例:
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
- 2、可中断锁
可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。 - 3、公平锁与非公平锁
公平锁是指多个线程同时尝试获取同一把锁时,锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO;而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。公平锁会影响性能。
ReentrantLock提供了两个构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认构造方法初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。
- 4、可以进行超时设置
1.2 重入性(特点1)
要支持重入性,就要解决两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
简单来说,ReentrantLock内部自定义了同步器Sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中。每次获取锁的时候,检查当前维护的那个线程ID和当前请求的线程ID是否 一致。如果一致,同步状态加1,表示锁被当前线程获取了多次。
1.2.1 获取锁
以非公平锁为例,要判断当前线程能否获得锁,核心方法为nonfairTryAcquire:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. 如果该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.若被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功,每次重新获取都会对同步状态进行加一的操作。
1.2.2 释放锁
还是以非公平锁为例,核心方法为tryRelease:
protected final boolean tryRelease(int releases) {
//1. 同步状态减1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//2. 只有当同步状态为0时,锁成功被释放,返回true
free = true;
setExclusiveOwnerThread(null);
}
// 3. 锁未被完全释放,返回false
setState(c);
return free;
}
需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。
1.2.3 重入锁使用示例
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
System.out.println("lock"+i);
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
System.out.println("unlock"+i);
}
}
}
结果:
lock1
lock2
lock3
unlock1
unlock2
unlock3
1.3 非公平锁和非公平锁(特点2)
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
- 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。
- Java中的synchronized是非公平锁,ReentrantLock默认的lock()方法采用的是非公平锁。
- 公平策略与非公平策略
简单来说,如果一个线程先申请锁,先获得锁,就表示使用了公平策略。如果某个线程后申请锁,却先获得了锁,就表示使用了非公平策略。
一般来说,非公平调度策略的吞吐率较高
。它的缺点是:从申请者个体的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较大,即有的线程很快就能申请到资源,而有的线程则要经历若干次暂停与唤醒才能成功申请到资源,极端情况下可能导致饥饿现象
。
公平调度策略的吞吐率较低,这是其维护资源独占权的授予顺序的开销比较大(主要是线程的暂停与唤醒所导致的上下文切换)的结果。其优点是,从个体申请者的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较小,即每个资源申请者申请到资源所需的时间基本相同,并且不会导致饥饿现象。
1.3.1 非公平锁
即NonfairSync。
- 1、获取锁
源码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
NonfairSync中lock方法的逻辑:用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的线程要去排队。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
如果有三个线程去竞争锁,假设线程A的CAS操作成功了,线程B和C就要执行AQS中的acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法的逻辑在AQS章节已介绍,不再赘述。
- 2、释放锁
即unlock()方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unlock()方法的流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。
tryRelease源码:
/**
* 释放当前线程占用的锁
* @param releases
* @return 是否释放成功
*/
protected final boolean tryRelease(int releases) {
// 计算释放后state值
int c = getState() - releases;
// 如果不是当前线程占用锁,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 锁被重入次数为0,表示释放成功
free = true;
// 清空独占线程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
1.3.2 公平锁
即FairSync。公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1)。fairSync的lock()源码:
final void lock() {
acquire(1);
}
acquire是AQS中的模板方法,会调用子类(FairSync)的tryAcquire(int acquires)的方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
这段代码的逻辑与nonfairTryAcquire基本一致,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁
。
1.3.3 非公平锁和公平锁的比较
先用一个例子测试一下:
public class TestDemo {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
公平锁的测试结果,可以看到线程几乎是轮流的获取到了锁:
再将上述代码改成非公平锁实现,可以看出线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题:
公平锁与非公平锁的比较:
- 1、
公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序;而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
。 - 2、公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
1.4 可以响应中断(特点3)
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
,该方法可以用来解决死锁问题。
接下来看个例子:两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。此时可以使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。示例:
public class TestDemo {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//该线程先获取锁1,再获取锁2
Thread thread = new Thread(new ThreadDemo(lock1, lock2));
//该线程先获取锁2,再获取锁1
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
thread.start();
thread1.start();
thread.interrupt();//是第一个线程中断
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果:
1.5 超时机制(特点4)
在ReetrantLock中,tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。
超时机制避免了线程无限期的等待锁释放
。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。
/**
* 在有限的时间内去竞争锁
* @return 是否获取成功
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 起始时间
long lastTime = System.nanoTime();
// 线程入队
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 又是自旋!
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 如果已经超时,返回false
if (nanosTimeout <= 0)
return false;
// 超时时间未到,且需要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞当前线程直到超时时间到期
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
// 更新nanosTimeout
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
//相应中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。
用超时机制解决死锁的例子:
public class TestDemo {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//该线程先获取锁1,再获取锁2
Thread thread = new Thread(new ThreadDemo(lock1, lock2));
//该线程先获取锁2,再获取锁1
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
thread.start();
thread1.start();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果:
Thread-0正常结束!
Thread-1正常结束!
1.6 tryLock、lock和lockInterruptibly的区别*
这3个方法都用来获取锁。
tryLock能获得锁就返回true,不能就立即返回false
,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。lock能获得锁就返回true,不能的话一直等待获得锁
。- lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,而lockInterruptibly会抛出异常。触发InterruptedException异常之后,线程的中断标志会被清空,即置为false。
1.7 ReentrantLock是如何实现可重入性的*
- 什么是可重入性
一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。 - ReentrantLock如何实现可重入性
ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数(可以理解为计数器),避免频繁的持有释放操作带来的线程问题。
当state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,这个判断是看当前持有锁的线程是不是自己,如果是自己,那么将state的值+1,表示重入返回即可。
二、ReentrantReadWriteLock
有这样的场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,可以用读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
。
2.1 ReentrantReadWriteLock的特点*
读写锁有以下三个重要的特性:
- 1、支持公平锁和非公平锁
支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。 - 2、可重入锁
读锁和写锁都支持线程重进入。 - 3、锁可以降级
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁
。
2.2 ReentrantReadWriteLock源码分析
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
public static class ReadLock implements Lock, java.io.Serializable {...}
public static class WriteLock implements Lock, java.io.Serializable {...}
//...
}
ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现。ReadWriteLock非常简单,只定义了两个接口:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock有五个内部类:
2.2.1 Sync
- Sync类的属性
abstract static class Sync extends AbstractQueuedSynchronizer {
//版本序列号
private static final long serialVersionUID = 6317671515068378041L;
//高16位为读锁,低16位为写锁
static final int SHARED_SHIFT = 16;
//读锁单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//写锁最大数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
//缓存的计数器
private transient HoldCounter cachedHoldCounter;
//第一个读线程
private transient Thread firstReader = null;
//第一个读线程的计数
private transient int firstReaderHoldCount;
}
- Sync类的构造函数
Sync() {
// 本地线程计数器
readHolds = new ThreadLocalHoldCounter();
// 设置AQS的状态
setState(getState());
}
- Sync类的两个内部类
Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter。
HoldCounter主要与读锁配套使用:
//计数器
static final class HoldCounter {
//表示某个读线程重入的次数,用来计数
int count = 0;
// 获取当前线程的TID属性的值
final long tid = getThreadId(Thread.currentThread());
}
ThreadLocalHoldCounter:
//本地线程计数器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
//重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
2.2.2 读写状态的设计
同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,在ReentrantLock中的state仅仅表示是否锁定,不用区分是读锁还是写锁。但读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
- 获取写状态:
S & 0x0000FFFF:将高16位全部抹去。- 获取读状态:
S>>>16:无符号补0,右移16位。- 写状态加1:
S+1。- 读状态加1:
S+(1<<16)即S + 0x00010000。
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
2.3 写锁的获取与释放
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
WriteLock类中的lock和unlock方法:
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
2.3.1 写锁的获取
即Sync类中的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
//当前线程
Thread current = Thread.currentThread();
//获取状态
int c = getState();
//写线程数量(即获取独占锁的重入数)
int w = exclusiveCount(c);
//当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
if (c != 0) {
// 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
// 如果写锁状态不为0且写锁没有被当前线程持有返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//判断同一线程获取写锁是否超过最大次数(65535),支持可重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//更新状态
//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
setState(c + acquires);
return true;
}
//到这里说明此时c=0,读锁和写锁都没有被获取
//writerShouldBlock表示是否阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置锁为当前线程所有
setExclusiveOwnerThread(current);
return true;
}
//返回占有写锁的线程数量
static int exclusiveCount(int c) {
//直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。
//这样计算是因为写锁数量由state的低十六位表示。
return c & EXCLUSIVE_MASK;
}
获取写锁的步骤:
- (1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
- (2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
- (3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
- (4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
- (5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
2.3.2 写锁的释放
即Sync类中的tryRelease方法:
protected final boolean tryRelease(int releases) {
//若锁的持有者不是当前线程,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//写锁的新线程数
int nextc = getState() - releases;
//如果独占模式重入数为0了,说明独占模式被释放
boolean free = exclusiveCount(nextc) == 0;
if (free)
//若写锁的新线程数为0,则将锁的持有者设置为null
setExclusiveOwnerThread(null);
//设置写锁的新线程数
//不管独占模式是否被释放,更新独占重入数
setState(nextc);
return free;
}
写锁的释放过程:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
2.4 读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。
2.4.1 读锁的获取
即Sync类中的tryAcquireShared方法:
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取状态
int c = getState();
//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁数量
int r = sharedCount(c);
/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) { // 读锁数量为0
// 设置第一个读线程
firstReader = current;
// 读线程占用的资源数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
// 占用资源数加1
firstReaderHoldCount++;
} else { // 读锁数量不为0并且不为当前线程
// 获取计数器
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程对应的计数器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 计数为0
//加入到readHolds中
readHolds.set(rh);
//计数+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
2.4.2 读锁的释放
即Sync类中的tryReleaseShared方法:
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
firstReader = null;
else // 减少占用的资源
firstReaderHoldCount--;
} else { // 当前线程不为第一个读线程
// 获取缓存的计数器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
// 获取当前线程对应的计数器
rh = readHolds.get();
// 获取计数
int count = rh.count;
if (count <= 1) { // 计数小于等于1
// 移除
readHolds.remove();
if (count <= 0) // 计数小于等于0,抛出异常
throw unmatchedUnlockException();
}
// 减少计数
--rh.count;
}
for (;;) { // 无限循环
// 获取状态
int c = getState();
// 获取状态
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) // 比较并进行设置
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。
2.5 读写锁的互斥性测试
- 1、基础代码
public class ReadWriteLockTest {
private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();
//获取写锁
public void getW(Thread thread) {
try {
rw1.writeLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 10){
System.out.println(thread.getName() + "正在写操作");
}
System.out.println(thread.getName() + "写操作完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rw1.writeLock().unlock();
}
}
//获取读锁
public void getR(Thread thread) {
try {
rw1.readLock().lock();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 10){
System.out.println(thread.getName() + "正在读操作");
}
System.out.println(thread.getName() + "读操作完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rw1.readLock().unlock();
}
}
}
- 2、并发读
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
}
结果:
Thread-1正在读操作
Thread-0正在读操作
Thread-1读操作完成
Thread-0读操作完成
可以看到读线程间是不用排队的。
- 3、并发写
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
}
结果:
可以看出写线程获取锁是互斥的。
- 4、并发读写
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(){
@Override
public void run() {
test.getR(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.getW(Thread.currentThread());
}
}.start();
}
结果:
可以看出读写线程获取锁也是互斥的。