显式锁
可以解决synchronized的限制
主要接口和类:
- 锁接口Lock,主要实现类是ReentrantLock
- 读写锁接口ReadWriteLock,主要实现类是ReentrantReadWriteLock
相比synchronized,显式锁支持以非阻塞方式获取锁、可以响应中断、可以限时,这使得它灵活的多
1 Lock
public interface Lock {
//获取锁和释放锁方法,lock()会阻塞直到成功
void lock();
void unlock();
//与lock()的不同是,它可以响应中断,如果被其他线程中断了,抛出InterruptedException。
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,立即返回,不阻塞,如果获取成功,返回true,否则返回false
boolean tryLock();
//先尝试获取锁,如果能成功则立即返回true,否则阻塞等待
//但等待的最长时间为指定的参数,在等待的同时响应中断
//如果发生了中断,抛出InterruptedException;如果在等待的时间内获得了锁,返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//新建一个条件,一个Lock可以关联多个条件
Condition newCondition();
}
1.synchronized不具有的特性:能中断、超时、非阻塞地获取锁
2.实现类原理:通过聚合了一个同步器的子类来完成线程访问控制,即AQS
2 ReentrantLock
2.1 知识点
1.基本机制:为lock/unlock方法,实现了与synchronized一样的语义,包括:
- 可重入,一个线程在持有一个锁的前提下,可以继续获得该锁
- 可以解决竞态条件问题
- 可以保证内存可见性
2.可以通过构造方法参数boolean fair来指定是否为公平,默认false
公平:等待时间最长的线程优先获得锁。保证公平会影响性能,一般也不需要,所以默认不保证,synchronized锁也是不保证公平的
3.基本用法:使用显式锁,一定要记得调用unlock,一般而言,应该将lock()之后的代码包装到try语句内,在finally语句内调用unlock()释放锁;不要将获取锁的过程(即lock.lock();)写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放
4.避免死锁:
使用tryLock(),可以避免死锁。在持有一个锁,获取另一个锁,获取不到的时候,可以释放已持有的锁,给其他线程机会获取锁,然后再重试获取所有锁
2.2 实现原理
在最底层,它依赖于CAS方法和类LockSupport中的一些方法。
内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值
2.2.1 LockSupport
作用:当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作
提供了最基本的线程阻塞和唤醒功能,成为构建同步组件的基础工具。
//使当前线程放弃CPU,进入等待状态(WAITING),操作系统不再对它进行调度
//不同于Thread.yield(),yield只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态,而park会放弃调度资格,使线程进入WAITING状态
//park是响应中断的,当有中断发生时,park会返回,线程的中断状态会被设置
//park可能会无缘无故的返回,程序应该重新检查park等待的条件是否满足
public static void park()
public static void unpark(Thread thread)//唤醒处于阻塞状态的线程thread
public static void parkNanos(long nanos)
public static void parkUntil(long deadline)
2.2.2 AQS的应用
AQS的三个内部类:
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync//fair为false时使用的类
static final class FairSync extends Sync//fire为true时使用的类
ReentrantLock类组合了Sync类
lock方法:
public void lock() {
sync.lock();
}
sync默认为NonfairSync,其lock方法实现如下:
//使用state表示是否被锁和持有数量,如果当前未被锁定,则立即获得锁,否则调用acquire(1)获得锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//AQS
public final void acquire(int arg) {
//如果tryAcquire返回false,即获取当前线程的锁失败,则调用acquireQueued来从等待队列中获取锁
//addWaiter会新建一个节点Node,代表当前线程,然后加入到内部的等待队列中
//放入等待队列后,调用acquireQueued尝试获得锁
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//设置中断标志位
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
//NonfairSync:获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//Sync:如果未被锁定,则使用CAS进行锁定,否则,如果已被当前线程锁定,则增加锁定次数
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//AQS:主体是一个死循环
//在每次循环中,首先检查当前节点是不是第一个等待的节点,如果是且能获得到锁,则将当前节点从等待队列中移除并返回;
//否则最终调用LockSupport.park放弃CPU,进入等待;被唤醒后,检查是否发生了中断,记录中断标志,在最终方法返回时返回中断标志。
//如果发生过中断,acquire方法最终会调用selfInterrupt方法设置中断标志位
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
总结:能获得锁就立即获得,否则加入等待队列,被唤醒后检查自己是否是第一个等待的线程,如果是且能获得锁,则返回,否则继续等待,这个过程中如果发生了中断,lock会记录中断标志位,但不会提前返回或抛出异常。
unlock实现如下:
public void unlock() {
sync.release(1);
}
//AQS:
public final boolean release(int arg) {
if (tryRelease(arg)) {//修改状态释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//调用LockSupport.unpark将第一个等待的线程唤醒
return true;
}
return false;
}
FairSync和NonfairSync的主要区别是:在获取锁时,即在tryAcquire方法中,如果当前未被锁定,即c==0,FairSync多个一个检查,如下
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;
}
}
...
保证公平整体性能比较低的原因:
不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起上下文切换,降低了整体的效率。
通常情况下,谁先运行关系不大,而且长时间运行,从统计角度而言,虽然不保证公平,也基本是公平的
2.3 对比synchronized
相比synchronized,ReentrantLock可以实现与synchronized相同的语义,但还支持以非阻塞方式获取锁、可以响应中断、可以限时等,更为灵活。
不过,synchronized的使用更为简单,写的代码更少,也更不容易出错。
synchronized代表一种声明式z编程,更多的是表达一种同步声明,由Java系统负责具体实现,程序员不知道其实现细节,显式锁代表一种命令式编程,程序员实现所有细节。
声明式编程的好处除了简单,还在于性能,在较新版本的JVM上,ReentrantLock和synchronized的性能是接近的,但Java编译器和虚拟机可以不断优化synchronized的实现,比如,自动分析synchronized的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。
总结:能用synchronized就用synchronized,不满足要求,再考虑ReentrantLock
3 显式条件Condition
显式锁与synchronzied相对应,而显式条件与wait/notify相对应。
wait/notify与synchronized配合使用,显式条件与显式锁配合使用
Condition即为条件变量,是一个接口,Lock#newCondition()可以返回Condition,其定义为:
public interface Condition {
//对应于Object的wait()
//与Object的wait方法一样,调用await方法前需要先获取锁
//如果没有锁,会抛出异常IllegalMonitorStateException。
//await在进入等待队列后,会释放锁,释放CPU,当其他线程将它唤醒后,或等待超时后,或发生中断异常后,它都需要重新获取锁,获取锁后,才会从await方法中退出
void await() throws InterruptedException;
//唯一一个不响应中断的等待方法
//该方法不会由于中断结束,但当它返回时,如果等待过程中发生了中断,中断标志位会被设置
void awaitUninterruptibly();
//等待时间是相对时间,如果由于等待超时返回,返回值为false,否则为true
boolean await(long time, TimeUnit unit) throws InterruptedException;
//等待时间也是相对时间,但参数单位是纳秒,返回值是nanosTimeout减去实际等待的时间
long awaitNanos(long nanosTimeout) throws InterruptedException;
//等待时间是绝对时间,如果由于等待超时返回,返回值为false,否则为true
boolean awaitUntil(Date deadline) throws InterruptedException;
//对应于notify/notifyAll,不过signal可以唤醒指定线程
//与notify/notifyAll一样,调用它们需要先获取锁
//如果没有锁,会抛出异常IllegalMonitorStateException。
//signal与notify一样,挑选一个线程进行唤醒
//signalAll与notifyAll一样,唤醒所有等待的线程
//但这些线程被唤醒后都需要重新竞争锁,获取锁后才会从await调用中返回
void signal();
void signalAll();
}
3.1 实现原理
Condition的实现是同步器AQS的内部类,因此每个Condition实例都能够访问同步器提供的方法
每个Condition对象都包含着一个队列(等待队列),该队列是Condition对象实现等待/通知功能的关键,其基本结构:
Object监视器模型拥有一个同步队列和一个等待队列;而Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列:
newCondition():
public Condition newCondition() {
return sync.newCondition();
}
//Sync
final ConditionObject newCondition() {
return new ConditionObject();
}
ConditionObject是AQS中定义的一个内部类
ConditionObject内部也有一个队列,表示条件等待队列,其成员声明为:
//条件队列的头节点
private transient Node firstWaiter;
//条件队列的尾节点
private transient Node lastWaiter;
ConditionObject是AQS的成员内部类,它可以直接访问AQS中的数据,比如AQS中定义的锁等待队列。
await:
public final void await() throws InterruptedException {
// 如果等待前中断标志位已被设置,直接抛异常
if (Thread.interrupted())
throw new InterruptedException();
// 1.为当前线程创建节点,加入条件等待队列
Node node = addConditionWaiter();
// 2.释放持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3.放弃CPU,进行等待,直到被中断或isOnSyncQueue变为true
// isOnSyncQueue为true表示节点被其他线程从条件等待队列移到了外部的锁等待队列,等待的条件已满足
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4.重新获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 5.处理中断,抛出异常或设置中断标志位
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
awaitNanos:与await的实现是基本类似的,区别主要是会限定等待的时间,如下所示:
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
long lastTime = System.nanoTime();
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
//等待超时,将节点从条件等待队列移到外部的锁等待队列
transferAfterCancelledWait(node);
break;
}
//限定等待的最长时间
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
long now = System.nanoTime();
//计算下次等待的最长时间
nanosTimeout -= now - lastTime;
lastTime = now;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return nanosTimeout - (System.nanoTime() - lastTime);
}
signal:
public final void signal() {
//验证当前线程持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//调用doSignal唤醒等待队列中第一个线程(因为一般都是配套使用,只有一个线程在等待,刚好就是配套的那个线程)
Node first = firstWaiter;
if (first != null)
//1.将节点从条件等待队列移到锁等待队列
//2.调用LockSupport.unpark将线程唤醒
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
StampedLock
1 知识点
StampedLock 支持三种模式:写锁、悲观读锁和乐观读
写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的;
不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个stamp(long类型变量);然后解锁的时候,需要传入这个 stamp。
性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式:
ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;
而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,即不是所有的写操作都被阻塞。
注意:用的是“乐观读”这个词,而不是“乐观读锁”,是因为乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能才会更好一些。
2 使用案例
//读锁模板
class Point {
private int x, y;
final StampedLock sl = new StampedLock();
// 计算到原点的距离
public int distanceFromOrigin() {
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读⼊局部变量
// 读的过程数据可能被修改
int curX = x, curY = y;
// 判断执⾏读操作期间,
// 是否存在写操作,如果存在,
// 则 sl.validate 返回 false
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
//读入方法局部变量
try {
curX = x;
curY = y;
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
//使⽤⽅法局部变量执⾏业务操作
return Math.sqrt(curX * curX + curY * curY);
}
}
乐观读原理类似数据库的乐观锁实现:
在表里增加一个数值型版本号字段 version,每次使用where version = x更新这个表某个数据的时候,都将 version 字段加 1;
如果更新的SQL 语句执行成功并且返回的条数等于 1,那么说明此SQL语句操作期间,没有其他人修改过这条数据;
因为如果这期间其他人修改过这条数据,那么版本号字段一定会大于x ;
version字段就类似stamp
//写锁模板
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
3 注意事项
StampedLock 的功能仅仅是 ReadWriteLock 的子集:
悲观读锁、写锁都不支持条件变量;
不支持重入;
如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock()上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升,因为内部实现里while循环里面对中断的处理有点问题
所以使用 StampedLock 一定不要调用中断操作;
如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。
//线程 T1获取写锁之后将自己阻塞,线程 T2 尝试获取悲观读锁,也会阻塞;
//如果此时调用线程 T2 的interrupt() 方法来中断线程 T2 的话,你会发现线程 T2 所在 CPU 会飙升到 100%
final StampedLock lock = new StampedLock();
Thread T1 = new Thread(()->{
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不释放写锁
LockSupport.park();
});
T1.start();
// 保证 T1 获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
// 阻塞在悲观读锁
lock.readLock()
);
T2.start();
// 保证 T2 阻塞在读锁
Thread.sleep(100);
// 中断线程 T2
// 会导致线程 T2 所在 CPU 飙升
T2.interrupt();
T2.join();
StampedLock 支持锁的降级(通过 tryConvertToReadLock() 方法实现)和升级(通过tryConvertToWriteLock() 方法实现),但是要慎重使用,注意stamp的赋值,不能忘记赋值了