JUC基础(三)—— Lock锁 及 AQS(1)

简介: JUC基础(三)—— Lock锁 及 AQS

前言

我们第一章提到了JUC的几大组成部分,其中就包含了“锁”,实际上Lock锁在业务上的使用频率恐怕是最高的,它弥补了很多synchronized关键字无法顾及的场景,灵活而强大。今天我们就来看一看这个强大的 Lock 以及它的核心类 AbstractQueuedSynchronizer (AQS)


一、Lock怎么用

1. 基础使用

我们先上一段代码,逻辑是用两个线程各为变量a加上10000次1。

    public static void main(String[] args) {
        SoutA soutA = new SoutA();
        new Thread(soutA).start();
        new Thread(soutA).start();
    }
    static class SoutA implements Runnable {
        static int a = 0;
        static Lock lock = new ReentrantLock();
        @Override
        public void run() {
            for (int i = 0 ; i < 10000; i++) {
                // lock.lock();
                a++;
                // lock.unlock();
            }
            System.out.println("Thread : " + Thread.currentThread().getName() + ", a = " + a);
        }
    }

我们都知道,在没有锁的情况下,结果是不固定的,两个线程结束后,a最终可能并没有达到20000

83c2435ae30f4d79b3d42051123c51b4.png


如果我们把lock的两行代码放开,结果就能恢复正常

8c08d93ab4cd449f93029e7d5db816d0.png

如果你有兴趣,可以考虑一下,如果此处不用锁,仅对变量a 加上 volatile修饰,最终结果能否输出20000?


此处我们可以看到lock的用法是要先 new 一个Lock实现类,然后使用lock() unlock()进行上锁、解锁。需要注意的是,当同步区域代码出现异常时,lock锁不会自动解锁,最好使用try catch finally 句式,这与sunchronized是有所不同的,所以lock锁的通常格式是这样的

        try {
            lock.lock();
            // 同步代码
            lock.unlock();
        } catch (Exception e) {
            // 异常处理
        } finally {
            lock.unlock();
        }

2. Lock提供的方法

我们上面显示了lock() unlock() 两个方法,我们来看看Lock接口还定义了哪些方法,以及这些方法的功能


5dac6b9b15424b5f8cb07e3122a86662.png

  • lock
  • 申请获取锁后立即返回,如果未获取到,则线程出于调度目的将被休眠,直到其获得锁

  • lockInterruptibly
  • 同lock,但如果在获取锁之前,或者休眠状态中,线程被设置了中断标志,将会向外抛出中断异常

  • newCondition
  • 为锁新建一个条件队列,并返回这个条件队列

  • tryLock
  • 尝试获取锁,无论是否成功,均立即返回,返回值就是是否成功的布尔值

  • tryLock(long time, TimeUnit unit)
  • 尝试获取锁,成功立即返回,即使不成功在指定时间后也返回,返回值就是是否成功的布尔值

  • unlock
  • 释放锁,只有锁的持有者才能释放锁


除了 newCondition() 大部分接口都非常简单,关于condition这个条件队列是什么,有什么用,我们后续会讲到


二、AQS

前面我们提到了ReentrantLock,但是其具体是怎么实现的,相信大家还有很多疑惑。其实ReentrantLock 的核心功能都是由AQS实现的,我们本章就先讲一讲AQS,AQS是整个Lock最核心的类,因此我们会详细去讲


1. AQS的作用

我们还是先看类图

b293e8164e5146ab82faf927a9b4145c.png

从名字和方法不难看出,与我们上面说的锁的模型是吻合的,锁其实是一堆线程在竞争资源,这堆线程分为两种:锁的持有线程、队列里等待着的线程。前者由抽象类AOS来操作,后者由AQS来操作,又因为AQS继承了AOS,因此AQS就拥有了管理所有线程的能力


AQS 一般被翻译成队列同步器,那么它的作用显而易见:实现一种同步模型,使得各个线程对资源的竞争能够有序进行。它定义了一个锁需要的所有基础方法 以及 部分实现,如果你想要自定义一种锁,那么直接继承AQS,或者包含一个继承AQS的实例,能大大减轻你的工作量


2. AQS的实现

以我的习惯,是更喜欢直接看源码的。但当我学习的时候,会发现看源码虽然深刻,但速度却十分缓慢。当时就想着,要是有个人能带着我把各个功能描述清楚。有我不懂的细节再去看源码,效率会大大提高,而我现在想做的就是这样事


我们回顾上述的图,AQS如果想实现这张图的功能,那么有几个方面是它必须考虑的

34867937d9d7462eabb939639cad0dcf.png

比如:锁是怎么获取的?没获取到锁线程会怎样?条件队列是怎么实现的~ 下面我们将分小节来详谈。


2.1 锁的竞争

AQS其实没有实现获取锁这部分方法,我们可以看到,这里是直接报错的,所以这段获取锁的逻辑交由其子类去实现,我们会在 ReentrantLock 里对这一部分做一些解析

eded3fcb16d840bb8618faed9c308e8f.png

但我们必须意识到一点,锁往往对应的是并发环境,所以锁的竞争必须是个原子操作,这一点上,几乎所有的锁都是一致的,大部分的锁都会使用一个变量作为标志,通过对变量进行CAS操作,根据结果判断是否获取成功。synchonized 底层是这样,AQS也是如此。


2.2 竞争成功的处理

竞争成功的线程,其实已经意味着线程是锁的持有者,如果锁是排他锁,则需要记录锁的持有线程,关这部分内容,是由AOS完成的,我们可以看看AOS的逻辑

422e140d885f400aa82b5b61bce1ecd3.png

可以看到,AOS其实就维护了一个线程属性,和它的get set方法。所以还是十分简单的,注意这里只有一个线程属性:意味着用于排他锁,无法适用于共享锁


2.3 竞争失败的处理

从我们前面的模型来看,竞争失败的线程会被放入条件队列,也叫等待队列,事实也确实如此

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

在AQS里,竞争失败的线程会被封装进一个 Node 节点,然后加入等待队列尾部,这个等待队列是一个双向链表结构。

90164f9315864b44a221ef28ff9a2070.png


进入同步队列后,仍然会在队列中继续尝试获取锁,即 acquireQueued 方法的含义。

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

如果在队列中还是没获取到锁,方法内会判断是否需要进行线程的park阻塞。当然,这里获取锁是在循环体内操作的,阻塞的线程被唤醒或被中断都会苏醒,醒来后又继续尝试获取锁。我们在这里需要注意两个点


  • 队列竞争:阻塞链表以头尾节点为锚进行链表节点的增删,为保证并发场景正确,头尾节点使用了volatile修饰,节点变动以CAS进行原子操作
  • head节点:head作为链表起始点,是个空节点,不包含线程。关于链表头是空节点的,在算法中经常被使用,能减少逻辑复杂度
  • 中断响应,因为 UNSAFE.park() 可以响应中断,唤醒线程,所以本方法也会被唤醒,但本方法响应中断后并没有退出循环,如果想中断就使线程退出竞争,可以使用doAcquireInterruptibly方法


目录
相关文章
|
2月前
|
安全 Java
JUC锁: ReentrantReadWriteLock详解
`ReentrantReadWriteLock` 主要用于实现高性能的并发读取,而在写操作相对较少的场景中表现尤为突出。它保证了数据的一致性和线程安全,在合适的场合合理使用 `ReentrantReadWriteLock`,可以实现更加细粒度的控制,并显著提升应用性能。然而,需要注意它的复杂度较一般的互斥锁高,因此在选择使用时要仔细考虑其适用场景。
38 1
|
4月前
|
存储 设计模式 安全
(五)深入剖析并发之AQS独占锁&重入锁(ReetrantLock)及Condition实现原理
在我们前面的文章《[深入理解Java并发编程之无锁CAS机制》中我们曾提到的CAS机制如果说是整个Java并发编程基础的话,那么本章跟大家所讲述的AQS则是整个Java JUC的核心。不过在学习AQS之前需要对于CAS机制有一定的知识储备,因为CAS在ReetrantLock及AQS中的实现随处可见。
|
4月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
88 0
|
5月前
|
监控 安全 Java
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
29 0
|
算法 Java
JUC--锁
简单介绍锁
|
Java
JUC基础(三)—— Lock锁 及 AQS(2)
JUC基础(三)—— Lock锁 及 AQS
94 0
|
安全 Java
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
95 0
并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
【JUC基础】04. Lock锁
java.util.concurrent.locks为锁定和等待条件提供一个框架的接口和类,说白了就是锁所在的包。
5519 0
AQS(abstractQueuedSynchronizer)锁实现原理详解
AQS(abstractQueuedSynchronizer)抽象队列同步器。其本身是一个抽象类,提供lock锁的实现。聚合大量的锁机制实现的共用方法。
151 0