请你说一下自己对于 AQS 原理的理解(请你说一下你对AQS的理解)—java面试并发知识

        本文用于对AQS原理的概述。主要通过JavaGuide及两篇博客,在此基础上,作者进行补充说明、整理论述,使其能以一种更为逻辑地清晰地方式表达出“说一下你对AQS的原理”的理解,更多适应于java面试回答,亦可作对AQS原理和运作方式的简要了解。

目录

                一、AQS是什么

                二、AQS的设计思想

                三、AQS内部实现原理

                四、AQS实现方式

                五、通过实例演示,加深理解

                六、关于参照博客和源码分析


一、AQS是什么

        AQS英文全称是AbstractQueuedSynchronizer(抽象队列同步器),这个类在java.util.concurrent.locks包下。AQS 是一个用来构建锁和同步器的框架,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch都是基于AQS实现的。

二、AQS的设计思想

        AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

        补充说明:CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

原理图(具体细节不作赘述,仅供读者简单了解过程):

三、AQS内部实现原理

        AQS实现原理:AQS使用一个被volatile关键字修饰的int变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。状态信息通过procted类型的getState,setState。compareAndSet State进行操作。其中,state变量用于标记加锁(每次加可重入锁,state+1)和解锁(每次释放一把锁,state-1),当state为0时,即为真正释放所有的锁,类似于ReentrantLock可重入锁的原理。volatile保证state变量的可见性,每个线程在任何时刻访问的都是该变量的最新值。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

举例理解state变量: (关于独占式和共享式的详细介绍在下文,读者可先跳过该内容往下阅读)

①.独占式:

        以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

②.共享式

  再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

③.补充说明

        一般来说,自定义同步器要么是独占式,要么是共享式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

四、AQS实现方式

        在此之前,我们先了解AQS定义线程访问资源的两种方式:

  • 独占(Exclusive):至多只有一个线程能执行,如 ReentrantLock
  • 共享(Share):允许多个线程同时执行,如 CountDownLatchSemaphore。

         接着,我们继续了解:AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquiretryReleasetryAcquireSharedtryReleaseShared几个方法,来决定同步状态的释放和获取即可,将部分简单的、可由使用者决定的操作逻辑延迟到子类中去实现。至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法模式的应用 。

我们来看看AQS定义的这些可重写的方法:

protected boolean tryAcquire(int arg) :

        独占式获取同步状态,试着获取,成功返回true,反之为false

protected boolean tryRelease(int arg) :

        独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;

protected int tryAcquireShared(int arg) :

        共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;

protected boolean tryReleaseShared(int arg) :

        共享式释放同步状态,成功为true,失败为false

protected boolean isHeldExclusively() :

        是否在独占模式下被线程占用。


      最后, 具体实现:首先,我们需要去①.继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,②.在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。

五、通过实例演示,加深理解

       下面代码演示中, Mutex外部类为我们自定义实现的同步器类,内部类Sync继承了AbstractQueuedSynchronizer父类,然后重写了isHeldExclusively()、tryAcquire()、tryRelease()方法。此时,线程访问资源的方式是独占式。接着,在Mutex外部类里面,且在Sync内部类外面的范围中,我们的组件通过创建Sync类的实例对象去调用AQS中的模板方法(即上述的isHeldExclusively()、tryAcquire()、tryRelease() 方法),继而实现了加锁lock()方法、解锁unlock()方法和判断资源是否被占用的isLocked()方法,可类比ReentrantLock进行理解。

package juc;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * Created by chengxiao on 2017/3/28.
 */
public class Mutex implements java.io.Serializable {
    //静态内部类,继承AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        //是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        //当状态为0的时候获取锁,CAS操作成功,则state状态为1,
        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;
        }
    }
        //同步对象完成一系列复杂的操作,我们仅需指向它即可
        private final Sync sync = new Sync();
        //加锁操作,代理到acquire(模板方法)上就行,acquire会调用我们重写的tryAcquire方法
        public void lock() {
            sync.acquire(1);
        }
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
        //释放锁,代理到release(模板方法)上就行,release会调用我们重写的tryRelease方法。
        public void unlock() {
            sync.release(1);
        }
        public boolean isLocked() {
            return sync.isHeldExclusively();
        }
}

        下面演示,分别创建30个线程,每个线程 对int变量a增加10000次,在线程并发安全的情况下(利用上述自定义实现的独占式同步器Mutex类来保证并发安全),结果应该等于30 x 10000= 300000;在并发不安全的情况下(不加任何锁),该结果小于300000。

1 package juc;
 2 
 3 import java.util.concurrent.CyclicBarrier;
 4 
 5 /**
 6  * Created by chengxiao on 2017/7/16.
 7  */
 8 public class TestMutex {
 9     private static CyclicBarrier barrier = new CyclicBarrier(31);
10     private static int a = 0;
11     private static  Mutex mutex = new Mutex();
12 
13     public static void main(String []args) throws Exception {
14         //说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
15         //未加锁前
16         for(int i=0;i<30;i++){
17             Thread t = new Thread(new Runnable() {
18                 @Override
19                 public void run() {
20                     for(int i=0;i<10000;i++){
21                         increment1();//没有同步措施的a++;
22                     }
23                     try {
24                         barrier.await();//等30个线程累加完毕
25                     } catch (Exception e) {
26                         e.printStackTrace();
27                     }
28                 }
29             });
30             t.start();
31         }
32         barrier.await();
33         System.out.println("加锁前,a="+a);
34         //加锁后
35         barrier.reset();//重置CyclicBarrier
36         a=0;
37         for(int i=0;i<30;i++){
38             new Thread(new Runnable() {
39                 @Override
40                 public void run() {
41                     for(int i=0;i<10000;i++){
42                         increment2();//a++采用Mutex进行同步处理
43                     }
44                     try {
45                         barrier.await();//等30个线程累加完毕
46                     } catch (Exception e) {
47                         e.printStackTrace();
48                     }
49                 }
50             }).start();
51         }
52         barrier.await();
53         System.out.println("加锁后,a="+a);
54     }
55     /**
56      * 没有同步措施的a++
57      * @return
58      */
59     public static void increment1(){
60         a++;
61     }
62     /**
63      * 使用自定义的Mutex进行同步处理的a++
64      */
65     public static void increment2(){
66         mutex.lock();
67         a++;
68         mutex.unlock();
69     }
70 }

结果测试

        加锁前,279204

        加锁后,300000

        综上,介绍完毕。

六、关于参照博客和源码分析

        本文用于对AQS原理的概述。主要通过JavaGuide及两篇博客,在此基础上,作者进行补充说明、整理论述,使其能以一种更为逻辑地清晰地方式表达出“说一下你对AQS的原理”的理解,更多适应于java面试回答,亦可作对AQS原理和运作方式的简要了解。因此,此文不作源码分析,读者可从参照博客中进行深入阅读。如若本文有所帮助,请收藏点赞,谢谢你,朋友。

博客:

        Java并发包基石-AQS详解 - dreamcatcher-cx - 博客园

        Java并发之AQS详解 - waterystone - 博客园

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值