Java并发(六)显示锁和StampedLock

显式锁

可以解决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的赋值,不能忘记赋值了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值