专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  OpenAI ... ·  17 小时前  
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了! ·  昨天  
程序猿  ·  “我真的受够了Ubuntu!” ·  2 天前  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  3 天前  
程序员的那些事  ·  成人玩偶 + ... ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

深入 Java Lock 锁

SegmentFault思否  · 公众号  · 程序员  · 2019-10-27 10:30

正文

本文原载于 SegmentFault 社区专栏

作者:陈污龟



这篇文章讲的是 Java 的 Lock 锁,主要有以下知识点:


  • AQS
  • ReentrantLock

  • ReentrantReadWriteLock

  • Loc k 和 sy n chronized 的选择


AQS


在学习 Lock 锁之前,我们先来看看什么是 AQS?

  • AQS 其实就是一个可以给我们实现锁的框架,juc 包中很多可阻塞的类比如 ReentrantLock、 ReadWriteLock 都是基于 AQS 构建的。
  • 内部实现的关键是:先进先出的队列、state 状态
  • 在 AQS 中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现 (大量用到了模板代码)
  • AQS 同时提供了互斥模式 (exclusive) 和共享模式 (shared) 两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如 ReadWriteLock


注意:ReentrantLock 不是 AQS 的子类,其内部类 Sync 才是 AQS 的子类。



State 状态


AQS 维护了一个 volatile int 类型的 state 变量,用来表示当前同步状态。


volatile 虽然不能保证操作的原子性,但是保证了当前变量 state 的可见性。


compareAndSetState


compareAndSetState 用来修改 state 状态,它是一个原子操作,底层其实是调用系统的 CAS 算法,有关 CAS 可移步: CAS


protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }


请求资源


acquire


acquire(int arg) 以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。


 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }


如果 tryAcquire(int) 方法返回 true,则 acquire 直接返回,否则当前线程需要进入队列进行排队。

addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式。


ReentrantLock


学习 ReentrantLock 之前先来看看它实现的 Lock 接口


public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}



  • lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly() 是用来获取锁的。
  • unLock() 方法是用来释放锁的。
  • newCondition() 方法是创建一个条件对象,用来管理那些得到锁但是不能做有用工作的线程。


ReentrantLock ,意思是" 可重入锁 ",线程可以重复地获得已经持有的锁。ReentrantLock 是唯一实现了 Lock 接口的类。接下来我们来看看有关源码:


AQS子类


ReentrantLock 实现了三个内部类,分别是 Sync、NonfairSync 和FairSync。


abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync


这些内部类都是 AQS 的子类,这就印证了我们之前所说的:AQS 是 ReentrantLock 的基础,AQS 是构建锁的框架.


构造器

 


 public ReentrantLock() {
        sync = new NonfairSync();
    }

 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
   }     


默认实现的是非公平锁,传入 true 表示使用公平锁。


加锁


  • ReentrantLock 中加锁使用的是 lock 方法
  • 默认使用非公平锁的 lock 方法


加锁流程


首先会通过 CAS 方法,尝试将当前的 AQS 中的 State 字段改成从 0 改成 1,如果修改成功的话,说明原来的状态是 0,并没有线程占用锁,而且成功的获取了锁,只需要调用 setExclusiveOwnerThread 函将当前线程设置成持有锁的线程即可。否则, CAS 操作失败之后,和普通锁一样,调用父类 AQS 的 acquire(1) 函数尝试获取锁。



 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;


        final void lock() {
            if (compareAndSetState(01))//尝试获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else  //获取失败则调用AQS的acquire方法
                acquire(1);
        }


而在 AQS 的 acquire(1) 函数中,会判断 tryAcquire(1 以及 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,如果尝试获取失败并且添加队列成功的话,那么就会调用 se lfInt errupt 函数中断线程执行,说明已经加入到了 AQS 的队列中。


注意:AQS 的 tryAcquire(1) 是由子类 Sync (也就是 ReentrantLockd 的静态内部类) 自己实现的,也就是用到了模板方法,接下来我们去看看子类的实现。


tryAcquire 是在 NonfairSync 类中实现的,其中调用了 nonfairTryAcquire 函数。


 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;
        }


在 nonfairTryAcquire 函数中,会尝试让当前线程去获取锁:


  1. 获取当前线程,以及 AQS 的状态
  2. 如果当前 AQS 的状态为 0 的话,那么说明当前的锁没有被任何线程获取,则尝试做一次 CAS 操作,将当前的状态设置成 acquires ,如果设置成功了的话,那么则将当前线程设置成锁持有的线程,并且返回 true,表示获取成功。
  3. 如果当前的状态不为 0 的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,如果相同的话,则将当前的状态加上 acquires 重新将状态设置,并且返回 true,这也就是重入锁的原因。
  4. 如果当前线程没有获取到锁的话,那么就会返回 false,表示获取锁失败。


源码参考:ReentrantLock 中的 NonfairSync 加锁流程
链接:
https://www.jianshu.com/p/f7d05d06ef54


ReentrantReadWriteLock


概述


我们知道 synchronized 内置锁和 ReentrantLock 都是 互斥锁 (一次只能有一个线程进入到临界区(被锁定的区域))


而 ReentrantReadWriteLock 是一个 读写锁


  • 在读取数据的时候,可以多个线程同时进入到到临界区 (被锁定的区域)
  • 在写数据的时候,无论是读线程还是写线程都是互斥的


一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!

ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

接口只有两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作


public interface ReadWriteLock {  
    Lock readLock();
    Lock writeLock();
}


性质


  • 读锁不支持条件对象,写锁支持条件对象
  • 读锁不能升级为写锁,写锁可以降级为读锁
  • 读写锁也有公平和非公平模式
  • 读锁支持多个读线程进入临界区,写锁是互斥的


和 ReentrantLock 相比,ReentrantReadWriteLock 多了 ReadLock WriteLock 两个内部类。


public class ReentrantReadWriteLock implements ReadWriteLockjava.io






请到「今天看啥」查看全文