java并发之AQS详解(待更)

一、为什么要用AQS同步框架?

开发者如果不了解JMM和多线程编程,就会写出很多线程不安全的程序,即使是经验丰富的程序员,并发编程也难免会出错。而对于java程序员来说,并发编程就变得容易得多了,因为并发编程大师Doug Lea为Java开发者提供了很多的并发容器和框架,而AQS就是java并发包下的一个核心框架。AQS是抽象队列同步器,是用来构建Lock锁和同步组件的基础框架,JUC包下的很多锁和同步组件都是基于AQS构建的,比如ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore。

为什么说AQS是构建Lock锁和同步组件的基础框架呢?

这里补充一点Lock和synchronized关键字的区别的知识:

  1. synchronized关键字是JVM层面上实现的,而Lock是JUC包下的一个接口
  2. synchronized让获取锁的线程执行完同步代码之后释放锁,线程执行发生异常的情况下,jvm会让线程释放锁,使用Lock的话,需要程序员显式地在finally块中释放锁,不然会造成死锁
  3. synchronized会让一直没获取到锁的线程阻塞等待,而Lock没有获取锁可以选择不用一直等待
  4. synchronized在发生异常的时候会自动释放占有的锁,不会出现死锁,Lock在发生异常的时候,不会主动释放占用的锁,必须手动unlock来释放锁,可能会引起死锁的发生
  5. synchronized无法判断锁的状态,而Lock可以判断
  6. synchronized是可重入锁,不响应中断,非公平的,而Lock是可重入锁,可判断,可公平的
  7. 在性能上,如果是并发量小的话,synchronized效率高,并发量高的话Lock高。Lock还可以提高多个线程读操作的效率(可以通过ReadWriteLock实现读写分离),ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步

设想一下,如果要实现线程的同步和锁机制,需要考虑哪些问题?

  • 如何去获取锁和释放锁?
  • 如何处理同步状态?
  • 竞争失败时线程如何处理?

而上述问题,AQS已经帮你解决了:

  • AQS实现了对同步状态的原子性管理
  • AQS实现了对线程阻塞和解除阻塞的管理
  • AQS实现了对同步队列的管理

Lock锁如何使用AQS?
AQS是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。以ReentrantLock为例,它内部聚合了一个同步器Sync,这个同步器继承了AQS
在这里插入图片描述
如何使用AQS实现线程同步?
AQS定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法实现中免不了对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证原子性的修改同步状态state字段。AQS既支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件。
在这里插入图片描述

二、什么是AQS同步框架?

AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量state表示同步状态,通过内置的FIFO双向虚拟队列来完成资源获取线程的排队工作,并发包的作者Doug Lea期望AQS能够成为实现大部分同步需求的基础。

AQS使用的设计模式

AQS的设计是基于 模板方法模式 的,使用AQS需要继承它并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

同步器可重写的方法有哪些?

在这里插入图片描述
在重写AQS指定的方法时,需要使用AQS提供的如下3个方法来访问或修改同步状态:

  • getState():获取当前的同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性(CAS机制实现)
AQS提供的模板方法有哪些?(部分)

在这里插入图片描述
AQS提供的模板方法基本分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程的情况。

三、AQS的底层原理(源码分析)

笔者打算从使用者的角度来分析AQS的底层,因此下面用AQS去实现一个简单的独占锁,独占锁就是在同一时刻只能有一个线程获取到锁,其他没有获取到锁的线程进入同步队列中等待。

用AQS实现一个简单的独占锁Mutex(代码来自AQS源码注释)
package com.demo.LockTest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class Mutex implements Lock {
    
    
    //静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    
    
        //是否被占用
        protected boolean isHeldExclusively() {
    
    
            return getState() == 1;
        }

        //当状态为0的时候获取锁
        public boolean tryAcquire(int acquires) {
    
    
            if (compareAndSetState(0, 1)) {
    
    
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
    
    
            //释放锁操作必须拥有锁,否则抛出异常
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //返回一个Condition,每个Condition都包含了一个condition队列
        Condition newCondition() {
    
    
            return new ConditionObject();
        }
    }

    /**
     * 然后仅仅需要将同步操作代理到Sync上
     */
    private final Sync sync = new Sync();

    @Override
    public void lock() {
    
    
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    
    
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
    
    
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    
    
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
    
    
        sync.release(1);
    }

    public boolean isLocked() {
    
    
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
    
    
        return sync.hasQueuedThreads();
    }

    @Override
    public Condition newCondition() {
    
    
        return sync.newCondition();
    }
}

上面的独占锁Mutex是一个自定义的同步组件,Mutex中定义了一个静态内部类,该内部类继承了AQS并实现了独占式获取和释放同步状态。在tryAcquire(int acquire)方法中,使用CAS机制设置同步状态为1,并将当前线程标记为拥有锁的线程,否则进入同步队列中等待。tryRelease(int releases)方法中只是将同步状态设置为0,然后将拥有锁的线程设置为null。

在Mutex的实现中,可以发现,用户使用这个类的时候不会直接和内部同步器交互(被设为private),而是通过调用Mutex提供的方法来使用独占锁。而Mutex的方法实现仅仅是去调用了同步器中的模板方法而已,这样就大大降低了开发一个可靠自定义同步组件的门槛

AQS是怎么实现线程同步的?

从实现的角度分析,线程同步主要包括:

  • 同步队列
  • 独占式同步状态的获取与释放
  • 共享式同步状态的获取与释放
  • 超时获取同步状态等同步器的核心数据结构与模板方法
1)同步队列

AQS依赖内部的同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息封装成一个结点(Node),并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会把首结点唤醒,使其再次尝试获取同步状态。

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
在这里插入图片描述

  • Node prev:前驱节点,当节点加入同步队列时被设置(尾部添加)
  • Node next:后继节点
  • Thread waiter:等待队列中的后继节点,若当前节点是共享的,那么这个字段将是一个SHARED常量,即节点类型(独占和共享)和等待队列中的后继节点共用同一个字段(?)。
  • int status:等待状态,包含CACELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、INITIAL(0),后续会详细介绍这些状态

节点是构成同步队列(或等待队列)的基础,AQS拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如下所示:
在这里插入图片描述
注意看,AQS包含了两个节点类型的引用,一个指向head节点,一个指向tail节点。

AQS将节点加入到同步队列

在这里插入图片描述

首结点的设置

同步队列遵循FIFO(先进先出)的,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态的时候,会去唤醒后继节点,而后继节点在获取到同步状态时就会将自己设置为首结点,过程如下:
在这里插入图片描述
请注意,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法不需要CAS来保证也是线程安全的,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

2)独占式同步状态的获取和释放

通过调用AQS的acquire(int arg)方法可以获取同步状态,这个方法不响应中断,即线程获取同步状态失败后进入同步队列中,后续对该线程进行中断操作的时候,这个线程也不会从同步队列中移除。

猜你喜欢

转载自blog.csdn.net/qq_56919740/article/details/133776495