-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 130 KB
/
content.json
1
{"meta":{"title":"闲情记趣","subtitle":"Not a porter but a programmer","description":"","author":"BlueRhino","url":"http://bluerhino.github.io"},"pages":[{"title":"关于","date":"2017-12-17T10:58:02.000Z","updated":"2017-12-20T13:15:38.000Z","comments":false,"path":"about/index.html","permalink":"http://bluerhino.github.io/about/index.html","excerpt":"","text":"今天的风儿甚是喧嚣啊 风中似有略略欲泣 不祥的东西跟随风的足迹飘到了镇子里 快走吧,在风停止之前 然而猴王早已看穿了一切 那边的薯片半价啊!"},{"title":"categories","date":"2017-12-17T10:52:25.000Z","updated":"2017-12-17T11:59:34.000Z","comments":false,"path":"categories/index.html","permalink":"http://bluerhino.github.io/categories/index.html","excerpt":"","text":""},{"title":"标签","date":"2017-12-17T11:59:01.000Z","updated":"2017-12-17T11:59:29.000Z","comments":false,"path":"tags/index.html","permalink":"http://bluerhino.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"迁移segmentfault","slug":"迁移segmentfault","date":"2018-12-02T13:28:46.000Z","updated":"2018-12-02T13:28:46.000Z","comments":true,"path":"2018/12/02/迁移segmentfault/","link":"","permalink":"http://bluerhino.github.io/2018/12/02/迁移segmentfault/","excerpt":"","text":"由于维护及SEO\b等关系,当前博客网站停止更新,后续文章暂定发布到segmentfault。有兴趣的同学请点击","categories":[],"tags":[]},{"title":"Java线程池简单总结","slug":"Java线程池简单总结","date":"2018-09-19T14:04:44.000Z","updated":"2018-09-19T14:10:45.000Z","comments":true,"path":"2018/09/19/Java线程池简单总结/","link":"","permalink":"http://bluerhino.github.io/2018/09/19/Java线程池简单总结/","excerpt":"概述\b线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操作系统一般使用分时的方式实现多线程;在多核处理器架构下,多个线程能够做到真正的在不同处理核心并行处理。无论使用何种方式实现多线程,正确使用\b多线程都可以提高程序性能,\b或是吞吐量,或是响应时间,甚至两者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文无法详细展开,可参考如《Programming Concurrency on the JVM》等书籍。本文主要内容为简单总结Java中线程池的相关\b信息。","text":"概述\b线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操作系统一般使用分时的方式实现多线程;在多核处理器架构下,多个线程能够做到真正的在不同处理核心并行处理。无论使用何种方式实现多线程,正确使用\b多线程都可以提高程序性能,\b或是吞吐量,或是响应时间,甚至两者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文无法详细展开,可参考如《Programming Concurrency on the JVM》等书籍。本文主要内容为简单总结Java中线程池的相关\b信息。 Java线程使用及特点\bJava中提供Thread作为线程实现,一般有两种方式: 直接集成Thread类: 1234567891011121314151617181920212223242526272829303132333435363738class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . }}class Starter{ public static void main(){ PrimeThread p = new PrimeThread(143); p.start(); }}``` 2. 实现`Runnable` 接口:```Javaclass PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . }}class Starter{ public static void main(){ PrimeRun p = new PrimeRun(143); new Thread(p).start(); }} 线程是属于操作系统的\b概念,Java中的多线线程实现一定会依托于操作系统支持。HotSpot虚拟机中对多线程的实现实际上是使用了一对一的映射模型,即一个Java进程映射到一个轻量级进程(LWP)之中。\b在使用Thread的start方法后,HotSpot创建本地线程并与Java线程关联。在此过程之中虚拟机需要创建多个对象(如OSThread等)用于跟踪线程状态,\b后续需要进行线程初始化工作(如初始换ThreadLocalAllocBuffer对象等),最后启动线程调用上文实现的run方法。由此可见创建线程的成本较高,如果线程中run函数中业务代码执行时间非常短且消耗资源较少的情况下,可能出现创建线程成本大于执行真正业务代码的成本,这样难以达到提升程序性能的目的。由于创建线程成本较大,很容易想到通过复用已创建的线程已达到减少线程创建成本的方法,此时线程池就可以发挥作用。 Java线程池\bJava线程池主要核心类(接口)为Executor,ExecutorService,Executors等,具体关系如下\b图所示: Executor接口由以上类图可见在线程池类结构体系中Executor作为最初始的接口,该接口仅仅规定了一个方法void execute(Runnable command),此接口作用为规定线程池需要实现的最基本方法为可运行实现了Runnable接口的任务,并且开发人员不需要关心具体的线程池实现(在实际使用过程中,仍需要根据不同任务特点选择不同的线程池实现),将\b客户端代码与\b运行客户端代码的线程池解耦。 ExecutorService接口Executor接口虽然完成了业务代码与线程池的解耦,但没有提供任何与线程池交互的方法,并且仅仅支持没有任何返回值的Runnable任务的提交,在实际\b业务实现中\b\b功能略显不足。为了解决以上问题,JDK中增加了扩展Executor接口的子接口ExecutorService。ExecutorService接口主要在两方面扩展了Executor接口: 提供针对线程池的多个管理方法,主要包括停止任务提交、停止线程池运行、\b判断线程池是否停止运行及线程池中任务是否运行完成; 增加submit的多个重载方法,该方法可在提交运行任务时,返回给提交任务的线程一个Future对象,可通过该对象对提交的任务进行控制,如取消任务或获取任务结果等(Future对象如何实现此功能另行讨论)。 Executors工具类Executors是主要为了简化线程池的创建而提供的工具类,通过调用各静态工具方法返回响应的线程池实现。通过对其方法\b的观察可将其\b提供的工具方法归为如下几类: 创建ExecutorService对象的工具:又可细分为创建FixedThreadPool、SingleThreadPool、CachedThreadPool、WorkStealingPool、UnconfigurableExecutorService、SingleThreadScheduledExecutor及ThreadScheduledExecutor; 创建ThreadFactory对象; 将Runnable等对象封装为Callable对象。以上各工具方法中使用最广泛的为newCachedThreadPool、newFixedThreadPool及newSingleThreadExecutor,这三个方法创建的ExecutorService对象均是其子类ThreadPoolExecutor(严格来说newSingleThreadExecutor方法返回的\b是FinalizableDelegatedExecutorService对象,其封装了ThreadPoolExecutor,为何如此实现后文在做分析),下文着重分析ThreadPoolExecutor类。至于其他ExecutorService实现类,如ThreadScheduledExecutor\b本文不做详细分析。 ThreadPoolExecutor类ThreadPoolExecutor类是线程池ExecutorService的重要实现类,在工具类Executors中构建的线程池对象,有大部分均是ThreadPoolExecutor实现。ThreadPoolExecutor类提供多个构造参数对线程池进行配置,代码如下:1234567public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 现在对各个参数作用进行总结: \b参数名称 参数类型 参数用途 corePoolSize int 核心线程数,线程池中会一直保持该数量的线程,即使这些线程是空闲的状态,如果设置allowCoreThreadTimeOut属性(默认为false)为true,则空闲超过超时时间的核心线程可以被回收 maximumPoolSize int 最大线程数,当前线程池中可存在的最大线程数 keepAliveTime long 线程存活时间,当当前线程池中线程数大于核心线程数时,空闲线程等待新任务的时间,超过该时间\b则停止空闲线程 unit TimeUnit 时间单位,keepAliveTime属性的时间单位 workQueue BlockingQueue\\ 等待队列,存储待执行的任务 threadFactory ThreadFactory 线程工厂,\b线程池创建线程时s使用 handler RejectedExecutionHandler 拒绝执行处理器,当提交任务被拒绝(当等待队列满,且线程达到最大限制后)时调用 在使用该线程池时有一个重要的参数起效顺序: 提交任务时,当当前运行的线程数小于核心线程时,则启动新的线程执行任务; 提交任务时,当前运行线程数大于等于核心线程数,将当前任务加入等待队列\b中; 将任务添加到等待队列失败时(如队列满),尝试新建线程运行任务; 新建线程时,线程池关闭或达到最大线程数,则拒绝任务,调用handler进行处理。 ThreadFactory有默认的实现为Executors.DefaultThreadFactory,其创建线程主要额外工作为将新建的线程加入当前线程组,并且将线程的名称置为pool-x-thread-y的形式。 ThreadPoolExecutor类通过内部类的\b形式提供了四种任务被拒绝时的处理器:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy及DiscardPolicy。 \b\b拒绝策略类 具体操作 AbortPolicy 抛出RejectedExecutionException异常,拒绝执行任务 CallerRunsPolicy 在提交任务的线程执行当前任务,即在调用函数execute或submit的线程直接运行任务 DiscardOldestPolicy 直接取消当前等待队列中最早的任务 DiscardPolicy 以静默方式丢弃任务 \bThreadPoolExecutor默认使用的是AbortPolicy处理策略,用户可自行实现RejectedExecutionHandler接口自定义处理策略,本处不在赘述。 Executors对于ThreadPoolExecutor的创建根据上文描述,Executors类提供了较多的关于创建或使用线程池的工具方法,此节重点总结其在创建ThreadPoolExecutor线程池的各方法。 newCachedThreadPool\b方法\b\b\b簇newCachedThreadPool\b方法\b\b\b簇用于创建可缓存任务的ThreadPoolExecutor线程池。包括两个\b重构方法:1234567891011public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());}public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory);} 结合上文分析的ThreadPoolExecutor各构造参数,可总结如下: 核心线程数为0:没有核心线程,即在没有任务运行时所有线程均会被回收; 最大线程数为Integer.MAX_VALUE,即线程池中最大可存在的线程为Integer.MAX_VALUE,由于此值在通常情况下远远大于系统可新建的线程数,可简单理解为此线程池不限制最大可建的线程数,此处可出现逻辑风险,在提交任务\b时可能由于超过系统处理能力造成无法再新建线程时会出现OOM异常,提示无法创建新的线程; \b存活时间60秒:线程数量超过核心线程后,空闲60秒的线程将会被回收,根据第一条可知核心线程数为0,则本条表示所有线程空闲超过60秒均会被回收; 等待队列SynchronousQueue:构建CachedThreadPool时,使用的等待队列为SynchronousQueue类型,此类型的等待队列较为特殊,可认为\b这是一个\b容量为0的阻塞队列,在调用其offer方法时,如当前有消费者正在等待获取\b元素,则返回true,否则返回false。使用此等待队列可做到快速提交任务到空闲线程,没有空闲线程时触发新建线程; ThreadFactory参数:默认为DefaultThreadFactory,也可通过构造函数设置。 newFixedThreadPool方法簇newFixedThreadPool方法簇用于创建固定线程数的ThreadPoolExecutor线程池。包括两个构造方法:1234567891011public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);} 各构造参数总结: 核心线程数与最大线程数nThreads:构建的ThreadPoolExecutor核心线程数与最大线程数相等且均为nThreads,这\b说明当前线程池不会存在非核心线程,即不会存在线程的回收(allowCoreThreadTimeOut默认为false),随着任务的提交,线程数增加到nThreads个后就不会变化; 存活时间为0:线程存在非核心线程,该时间没有特殊效果; 等待队列LinkedBlockingQueue:该等待队列为LinkedBlockingQueue类型,没有长度限制; ThreadFactory参数:默认为DefaultThreadFactory,也可通过构造函数设置。 newSingleThreadExecutor方法\b簇newSingleThreadExecutor方法\b簇用于创建只包含一个线程的线程\b池。包括两个构造方法:12345678910111213public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));}public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));} 结合上文分析的ThreadPoolExecutor各构造参数,可总结如下: 核心线程数与最大线程数1:当前线程池中有且仅有一个核心线程; 存活时间为0:当前线程池不存在非核心线程,不会存在线程的超时回收; 等待\b队列LinkedBlockingQueue:该等待队列为LinkedBlockingQueue类型,没有长度限制; ThreadFactory参数:默认为DefaultThreadFactory,也可通过构造函数设置。特殊说明,函数实际返回的对象类型并不是ThreadPoolExecutor而是FinalizableDelegatedExecutorService类型,为何如此设计\b在后文统一讨论。 三种常见线程池的对比上文总结了Executors工具类创建常见线程池的方法,\b现对三种线程池区别进行比较。 线程池类型 CachedThreadPool FixedThreadPool SingleThreadExecutor 核心线程数 0 nThreads(用户设定) 1 最大线程数 Integer.MAX_VALUE nThreads(用户设定) 1 非核心线程\b存活时间 60s 无非核心线程 无非核心线程 等待队列最大长度 1 无限制 无限制 特点 提交任务优先复用空闲线程,没有空闲\b线程则创建新线程 \b固定线程数,等待运行的任务均放入等待队列 有且仅有一个线程在运行,\b等待运行任务放入等待队列,可保证任务\b运行顺序与提交顺序一直 内存溢出 大量提交任务后,可能出现无法创建线程的OOM 大量提交任务后,可能出现内存不足的OOM 大量提交任务后,可能出现内存不足的OOM \b三种类型的线程池与GC关系原理说明\b一般情况下JVM中的GC\b根据可达性分析确认一个对象是否可被回收(eligible for GC),而在运行的线程\b\b被视为‘GCRoot’。因此被在运行的线程引用的对象是不会被GC回收的。在ThreadPoolExecutor类中具有f非静态内部类Worker,用于\b表示x当前线程池中的线程,并且根据Java语言规范An instance i of a direct inner class C of a class or interface O is associated with an instance of O, known as the immediately enclosing instance of i. The immediately enclosing instance of an object, if any, is determined when the object is created (§15.9.2).可知非静态内部类对象具有外部包装\b类对象的引用(此处也可\b通过查看字节码来验证),因此Worker类的对象即作为线程对象(‘GCRoot’)有持有外部类ThreadPoolExecutor对象的引用,则在其运行结束之前,外部内不会被Gc回收。根据以上分析,再次观察以上三个线程池: CachedThreadPool:没有核心线程,且线程具有超时时间,可见在其引用消失后\b,等待任务运行结束且所有线程空闲回收后,GC开始回收此线程池对象; FixedThreadPool:核心\b线程数及最大线程数均为nThreads,并且在默认allowCoreThreadTimeOut为false的情况下,其引用消失后,核心线程即使空闲也不会被回收,故GC不会回收该线程池; SingleThreadExecutor:默认与FixedThreadPool\b\b情况一致,但由于其语义为单线程线程池,JDK开发人员为其提供了FinalizableDelegatedExecutorService包装类,在创建FixedThreadPool对象时实际返回的是FinalizableDelegatedExecutorService对象,该对象持有FixedThreadPool对象的引用,但FixedThreadPool对象并不引用FinalizableDelegatedExecutorService对象,这使得在FinalizableDelegatedExecutorService对象的外部引用消失后,GC将会对其进行回收,触发finalize函数,而该函数仅仅简单的调用shutdown函数关闭线程,是的所有当前的任务执行完成后,回收线程池中线程,则GC可回收线程池对象。因此可得出结论,CachedThreadPool及SingleThreadExecutor的对象在不显式调用shutdown函数(或shutdownNow函数),且其对象引用消失的情况下,可以\b被GC回收;FixedThreadPool对象在不显式调用shutdown函数(或shutdownNow函数),且其对象引用消失的情况下不会被GC回收,会出现内存泄露。 实验验证以上结论可使用实验验证:12345678910111213public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); //ExecutorService executorService = Executors.newFixedThreadPool(1); //ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(() -> System.out.println(Thread.currentThread().getName())); //线程引用置空 executorService = null; Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(\"Shutdown.\"))); //等待线程\b超时,主要对CachedThreadPool有效 Thread.sleep(100000); //手动触发GC System.gc();} 使用以上代码,分别创建三种不同的线程池,可发现最终FixedThreadPool\b不会打印出‘Shutdown.’,\bJVM没有退出。另外两种线程池均能退出JVM。因此\b无论使用什么线程池线程池使用完毕后均调用shutdown以保证其最终会被GC回收是一个较为安全的编程习惯。 猜想及踩坑代码示例根据以上的原理及\b代码分析,很容易提出如下问题:既然SingleThreadExecutor的实现方式可以自动完成线程池的关闭,为何不使用同样的方式实现FixedThreadPool呢?目前作者没有找到确切的原因,此处引用两个对此有所讨论的两个网址:王智超-理解SingleThreadExecutor及Why doesn’t all Executors factory methods wrap in a FinalizableDelegatedExecutorService? 有兴趣的同学可以参考。\b作者当前提出一种不保证正确的可能性:JDK开发人员可能重语义方面考虑将FixedThreadPool定义为可重新配置的线程池,SingleThreadExecutor定义为不可重新配置的线程池。因此没有使用FinalizableDelegatedExecutorService对象包装FixedThreadPool对象,将其控制权放到了程序员手中。最后再分享一个关于SingleThreadExecutor的踩坑代码,改代码在编程过程中一般不会出现,\b但其中涉及较多知识点,不失为一个好的学习示例:1234567891011121314151617181920import java.util.concurrent.Callable;import java.util.concurrent.Executors;class Prog { public static void main(String[] args) { Callable<Long> callable = new Callable<Long>() { public Long call() throws Exception { // Allocate, to create some memory pressure. byte[][] bytes = new byte[1024][]; for (int i = 0; i < 1024; i++) { bytes[i] = new byte[1024]; } return 42L; } }; for (;;) { Executors.newSingleThreadExecutor().submit(callable); } }} 以上代码在设置-Xmx128m的虚拟机进行运行,大概率会抛出RejectedExecutionException异常,其原理与上文分析的GC回收有关,详细分析可参考Learning from bad code 此处不再展开。 Executors对于ThreadPoolExecutor的创建的最佳实践以上总结了使用Executors创建常见线程池的方法,在简单的使用中的确方便使用且减少的手动创建线程池的代码量,但在真正开发高并发程序时,其默认创建的线程由于\b屏蔽了底层参数,程序员难以真正理解其中可能出现的细节问题,包括内存溢出及拒绝策略等,故在使用中t推荐使用ThreadPoolExecutor等方式直接创建。此处可以参考《阿里巴巴Java开发手册终极版v1.3.0》(六)并发处理的第4点。 总结本文简单总结了Java线程及常用线程池的使用,对比常见线程池的特点。由于本文侧重于分析使用层面,并没有深入探究各线程池具体的代码实现,此项可留后续继续补充。","categories":[{"name":"Javav","slug":"Javav","permalink":"http://bluerhino.github.io/categories/Javav/"},{"name":"多线程","slug":"Javav/多线程","permalink":"http://bluerhino.github.io/categories/Javav/多线程/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"多线程","slug":"多线程","permalink":"http://bluerhino.github.io/tags/多线程/"},{"name":"线程池","slug":"线程池","permalink":"http://bluerhino.github.io/tags/线程池/"}]},{"title":"限流器及Guava实现分析","slug":"限流器及Guava实现分析","date":"2018-09-02T08:51:54.000Z","updated":"2018-09-02T09:02:36.000Z","comments":true,"path":"2018/09/02/限流器及Guava实现分析/","link":"","permalink":"http://bluerhino.github.io/2018/09/02/限流器及Guava实现分析/","excerpt":"限流限流\b一词常用于计算机网络之中,定义如下: In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks. 通过控制数据的网络数据的发送或接收速率来防止可能出现的DOS攻击。\b而实际的软件服务过程中,限流也可用于API服务的保护。由于提供服务的计算机资源(包括CPU、内存、磁盘及网络带宽等)是有限的,则其提供的API服务的QPS也是有限的,限流工具就是通过\b限流算法对API访问进行限制,保证服务不会超过其能承受的负载压力。本文主要涉及内容包括: 常用限流算法的简单介绍及比较 Guava包中限流工具的实现分析","text":"限流限流\b一词常用于计算机网络之中,定义如下: In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks. 通过控制数据的网络数据的发送或接收速率来防止可能出现的DOS攻击。\b而实际的软件服务过程中,限流也可用于API服务的保护。由于提供服务的计算机资源(包括CPU、内存、磁盘及网络带宽等)是有限的,则其提供的API服务的QPS也是有限的,限流工具就是通过\b限流算法对API访问进行限制,保证服务不会超过其能承受的负载压力。本文主要涉及内容包括: 常用限流算法的简单介绍及比较 Guava包中限流工具的实现分析 常用限流算法援引wiki中关于限流\b的Algorithms一小节的说明,常用的限流算法主要包括: \bToken bucket-令牌桶 Leaky bucket-漏桶 Fixed window counter-固定窗口计数 Sliding window log-滑动窗口日志 Sliding window counter-滑动窗口计数\b\b以上几种方式其实可以简单的分为计数算法、漏桶算法和令牌桶算法。 计数限流算法无论固定窗口还是滑动窗口核心均是对\b请求进行计数,区别仅仅在于对于计数时间区间的处理。 固定窗口计数实现原理固定窗口计数法\b思想比较简单,只需要确定两个参数:计数周期\bT及周期内最大访问(调用)数N。请求到达时使用以下流程进行操作: 固定窗口计数实现简单,并且只需要记录上一个周期起始时间与周期内访问总数,几乎不消耗额外的存储空间。 算法缺陷固定窗口计数缺点也非常明显,\b在进行周期切换时,上一个周期的访问总数会立即置为0,这可能导致在进行周期切换时可能出现流量突发,如下图所示简化模型,假设在两个周期T0中a时刻有n1个访问同时到达,周期T1中b时刻有n2个访问同时到达,且n1和n2均小于设定的最高访问次数N(否则会触发限流)。根据以上假设可以推断,限流器不会限流,n1+n2次访问均可以通过。现假设\ba,b两时刻之间时间差为t,\b则可以得出以下关系: \\left\\{ \\begin{aligned} n1 \\le N \\\\ n2 \\le N \\\\ (n1+n2) \\le 2N \\\\ \\end{aligned} \\right.根据观察可发现,在$t$的时间内,出现了$n1+n2$次请求,且$n1+n2$是可能大于$N$的,所以在实际使用过程中,固定窗口计数器存在突破限额N的可能。举例,限制QPS为10,某用户在周期切换的前后\b的0.1秒内,分两次发送10次请求,根据算法规则此20次请求可通过限流器,则0.1面\b\b秒请求数20,超过每秒最多10次请求的限制。 滑动窗口计数为解决固定窗口计数带来的周期切换处流量突发问题,可以使用滑动窗口计数。滑动窗口计算本质上也是固定窗口计数,区别在于将计数周期进行细化。 实现原理滑动窗口计数法与股固定窗口计数法相比较,除了计数周期\bT及周期内最大访问(调用)数N两个参数,增加一个参数M,用于设置周期T内的滑动窗口数。限流流程如下:滑动窗口计数在固定窗口计数记录数据基础上,需要增加一个长度为M的计数数组,用于记录在窗口\b滑动过程中各窗口访问数据。其流程示例如下: 周期切换问题滑动窗口针对周期进行了细分,不存在周期到后计数直接重置为0的情况,故不会出现跨周期的流量限制问题。 非计数限流法漏桶限流实现原理漏桶\b限流算法的实现原理在wiki有详细说明,引用其原理图:简单说明为:\b人为设定漏桶流出速度及漏桶的总容量,在请求到达时判断当前漏桶容量是否已满,不满则可将请求存入桶中,否则抛弃请求。程序以设定的速率取出请求进行处理。根据描述,需要确定参数为漏桶流出速度r及漏桶容量N,流程如下: 算法特点漏桶算法主要特点在于可以保证无论\b收到请求的速率如何,真正抵达服务方接口的请求速率最大为r,\b能够对输入的请求进行平滑处理。漏桶算法的\b缺点也非常明显,由于其只能以特定速率处理请求,则如何确定该速率就是\b核心问题,如果速率设置太小则会浪费性能资源,设置太大则会造成资源不足。并且由于速率的设置,无论输入速率如何波动,均不会体现在服务端,即使资源有空余,对于突发请求也无法及时处理,故对有突发请求处理需求时,不宜选择该方法。 令牌桶限流实现原理令牌桶限流的实现原理在wiki有详细说明。简单总结为:\b设定令牌桶中添加令牌的速率,并且设置桶中最大可存储的令牌,当请求到达时,向桶中请求令牌(根据应用需求,可能为1个或多个),若令牌数量满足要求,则删除对应数量的令牌并\b通过当前请求,若桶中令牌数不足则触发限流规则。根据描述需要设置的参数为,令牌添加速率r,令牌桶中最大容量N,流程如下: 算法特点令牌桶算法通过设置令牌放入速率可以控制请求通过的平均速度,且由于设置的容量为N的桶对令牌进行缓存,可以容忍一定流量的突发。 算法比较以上提到四种算法,本小节主要对四种算法做简单比较算法进行对比。 table th { width: 100px; } \b\u001c算法名称 需要确定参数 实现简介 空间复杂度 说明 固定窗口计数 计数周期T周期内最大访问数N 使用计数器在周期内累加访问次数,达到最大次数后出发限流策略 O(1),仅需要记录周期内访问次数及周期开始时间 周期切换时可能出现访问次数超过限定值 滑动窗口计数 计数周期T周期内最大访问数N滑动窗口数M \b将时间周期分为M个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期 O(M),需要记录每个小周期中的访问数量 解决固定窗口算法周期切换时的访问突发问题 漏桶算法 漏桶流出速度r漏桶容量N 服务到达时直接放入漏桶,如当前容量达到N,则触发限流侧率,程序以r的速度在漏桶中获取访问请求,知道漏桶为空 O(1),仅需要记录当前漏桶中容量 平滑流量,保证服务请求到达服务方的速度恒定 令牌桶算法 令牌产生速度r令牌桶容量N 程序以r的速度向令牌桶中增加令牌,\b直到令牌桶满,请求到达时向令牌桶请求令牌,如有满足需求的令牌则通过请求,否则触发限流策略 O(1),仅需要记录当前令牌桶中令牌数 能够在限流的基础上,处理一定量的突发请求 Guava包中限流工具的实现分析概览上文简单介绍了常用的限流算法,在JAVA软件开发过程中可使用Guava包中的限流工具进行服务限流。Guava包中限流工具类\b图如下所示:其中\bRateLimiter类为限流的核心类,其为public的抽象类,RateLimiter有一个实现类SmoothRateLimiter,根据不同\b消耗令牌的策略SmoothRateLimiter\b又有两个具体实现类SmoothBursty和SmoothWarmingUp。在实际使用过程中一般直接使用RateLimiter类,其他类对用户是透明的。RateLimiter类的设计使用了类似BUILDER模式的小技巧,\b并做了一定的调整。通过RateLimiter类图可见,RateLimiter类不仅承担了具体实现类的创建职责,同时也确定了被创建出的\b实际类可提供的方法。标准创建者模式UML图如下所示(引用自百度百科)RateLimiter类即承担了builder的职责,也承担了Product的职责。 简单使用示例在实际的代码编写过程中,对GUAVA包限流工具的使用参考以下代码:1234567final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is \"2 permits per second\"void submitTasks(List<Runnable> tasks, Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); // may wait executor.execute(task); }} 以上代码摘自GUAVA包RateLimiter类的说明文档,\b首先使用create函数创建限流器,指定每秒生成2个令牌,在需要调用服务时使用acquire函数或取令牌。 RateLimiter实现分析根据代码示例,抽象类RateLimiter由于承担了Product的职责,\b其已经确定了暴露给编程人员使用的API函数,其中主要实现的核心函数为create及acquire。因此\b由此\b为入口进行分析。 create函数分析create函数具有两个个重载,根据不同的重载可能创建不同的RateLimiter具体实现子类。目前可返回的实现子类包括SmoothBursty及SmoothWarmingUp两种,具体不同下文详细分析。 acquire函数分析acquire函数也具有两个重载类,但\b分析过程仅仅需要关系具有整形参数的函数重载即可,无参数的函数仅仅是acquire(1)的简便写法。在acquire(int permits)函数中主要完成三件事: 预分配授权数量,此函数返回需要等待的时间,可能为0; 根据等待时间进行休眠; 以秒为单位,返回获取授权消耗的时间。 完成以上工作的过程中,RateLimiter类确定了\b获取授权的过程骨架并且实现了一些通用的方法,这些通用方法中会调用为实现的抽象方法,\b开发人员根据不同的算法需求可实现特定子类对抽象方法进行覆盖。其调用流程如下图:其中橙色块中reserveEarliestAvailable方法即为需要子类进行实现的,下文以该函数为核心,分析RateLimiter类的子类是如何实现该方法的。 子类实现分析代码\b分析根据上文的类图可见,RateLimiter类在GUAVA包中的\b直接子类仅有SmoothRateLimiter,故\u001c\b以reserveEarliestAvailable函数为入口研究其具体实现,\b而在代码实现的过程中需要使用SmoothRateLimiter类中的属性,现将类中各属性罗列出来: 序号 属性名称 属性说明 是否为静态属性 1 storedPermits 当前令牌桶中令牌数 否 2 maxPermits 令牌桶中最大令牌数 否 3 stableIntervalMicros 两个令牌产生的时间间隔 否 4 nextFreeTicketMicros 下一次\b有空闲令牌产生的时刻 否 reserveEarliestAvailable源码如下:1234567891011121314@Overridefinal long reserveEarliestAvailable(int requiredPermits, long nowMicros) { resync(nowMicros); long returnValue = nextFreeTicketMicros; double storedPermitsToSpend = min(requiredPermits, this.storedPermits); double freshPermits = requiredPermits - storedPermitsToSpend; long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros); this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); this.storedPermits -= storedPermitsToSpend; return returnValue;} 通过reserveEarliestAvailable的\b函数名称可以知道,该函数能够返回令牌可用的最早时间。函数需要的输入参数有需求的令牌数requiredPermits,当前时刻nowMicros。进入函数后,首先调用名为resync的函数:12345678void resync(long nowMicros) { // if nextFreeTicket is in the past, resync to now if (nowMicros > nextFreeTicketMicros) { double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); storedPermits = min(maxPermits, storedPermits + newPermits); nextFreeTicketMicros = nowMicros; }} 函数逻辑比较简单,首先获取nextFreeTicketMicros,该值在上表中已经说明,表示下一次\b有空闲令牌产生的时刻,如果当前时刻小于等于nextFreeTicketMicros,说明在当前时刻不可能有新的令牌产生,则直接返回。若当前时刻大于nextFreeTicketMicros,则完成以下工作: 计算到当前时刻新产生的令牌数,其中涉及一个名为coolDownIntervalMicros 的\b函数,该函数返回创建一个新令牌需要的冷却时间(\b注:该函数在当前类中并未实现,具体实现下文\b说明); 更新storedPermits属性,取产生的令牌和最大可存储令牌之间的最小值; 将nextFreeTicketMicros属性置为当前时刻。 可见,resync函数主要功能在于计算新产生的令牌数,并更新nextFreeTicketMicros属性,nextFreeTicketMicros属性取值是当前时刻\b和nextFreeTicketMicros属性的原始值中最大的一个。完成resync函数的调用后,使用returnValue变量记录更新令牌后的最近可用时间(即上文更新后的nextFreeTicketMicros属性)。使用storedPermitsToSpend变量记录需要消耗以存储的令牌数,其取值为请求令牌数和当前存储令牌数之间的最小值。使用freshPermits变量记录需要刷新的令牌数,其实现是用需求的令牌数减去之前计算的storedPermitsToSpend变量,可见freshPermits变量取值为需求令牌数与已存储令牌数之间的差值,当需求令牌数小于已存储令牌数是则为0。后续为该函数核心,计算需要等待的时间,计算等待时间主要分为两个部分:消耗已经存储的令牌需要的时间及生成新令牌的时间,其中storedPermitsToWaitTime函数用于计算消耗已经存储的令牌需要的时间,该函数也是抽象函数,后文具体分析子类实现。完成等待时间的计算后,程序更新nextFreeTicketMicros属性,将最近可用时间与需要等待的时间相加。最后在更新存储的令牌数,将需要消耗额\b令牌数减去。 实现逻辑特点根据以上的代码分析可以发现,GUAVA对于令牌桶的实现跟理论有一点点小的区别。其当前一次的请求消耗的令牌数\b并不会影响本次请求的等待时间,而是会影响下一次请求的等待时间。根据以上分析,当一次请求到达,\b最近可用时间返回当前时间和上一次请求计算的最近可用时间的最大值,而本次请求需要的令牌数会更新下一次的最近可用时间。在这样的设计下,如果每秒产生一个令牌,第一请求需求10个令牌,则当第一次请求调用acquire方法时能够立即返回,而下一次请求(无论需要多少令牌)均需要等待到第一个请求之后的10秒以后,第三次请求等待时间则取决于第二次需求了多少令牌。这也是函数名称中“reserve”的含义。 抽象函数分析在以上文代码分析中出现了两个抽象函数coolDownIntervalMicros及storedPermitsToWaitTime,现分析这两个抽象函数。 coolDownIntervalMicros函数coolDownIntervalMicros函数在代码中已经有说明:1Returns the number of microseconds during cool down that we have to wait to get a new permit. 主要含义为生成一个令牌需要消耗的时间,\b该函数主要应用于计算当前时间可产生的令牌数。根据上文的UML图SmoothRateLimiter类有两个子类SmoothBursty及SmoothWarmingUp。SmoothBursty类中对于coolDownIntervalMicros函数的实现如下:1234@Overridedouble coolDownIntervalMicros() { return stableIntervalMicros;} 可见实现非常简单,仅仅只是返回stableIntervalMicros属性\b,即产生两个令牌需要的时间间隔。SmoothWarmingUp类中对于coolDownIntervalMicros函数的实现如下:1234@Overridedouble coolDownIntervalMicros() { return warmupPeriodMicros / maxPermits;} 其中maxPermits属性上文已经出现过,表示当前令牌桶的最大容量。warmupPeriodMicros属性属于SmoothWarmingUp类的特有属性,表示令牌桶中令牌从0到maxPermits需要经过的时间,故warmupPeriodMicros / maxPermits表示在令牌数量达到maxPermits之前的令牌产生时间间隔。 storedPermitsToWaitTime函数storedPermitsToWaitTime函数在代码中已经有说明:123Translates a specified portion of our currently stored permits which we want to spend/acquire,into a throttling time. Conceptually, this evaluates the integral of the underlying function weuse, for the range of [(storedPermits - permitsToTake), storedPermits] 主要表示消耗存储在令牌桶中的令牌需要的时间。SmoothBursty类中对于storedPermitsToWaitTime函数的实现如下:1234@Overridelong storedPermitsToWaitTime(double storedPermits, double permitsToTake) { return 0L;} 直接返回0,表示消耗令牌不需要时间。SmoothBursty类中对于storedPermitsToWaitTime函数的实现如下:123456789101112131415161718@Overridelong storedPermitsToWaitTime(double storedPermits, double permitsToTake) { double availablePermitsAboveThreshold = storedPermits - thresholdPermits; long micros = 0; // measuring the integral on the right part of the function (the climbing line) if (availablePermitsAboveThreshold > 0.0) { double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake); // TODO(cpovirk): Figure out a good name for this variable. double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); micros = (long) (permitsAboveThresholdToTake * length / 2.0); permitsToTake -= permitsAboveThresholdToTake; } // measuring the integral on the left part of the function (the horizontal line) micros += (long) (stableIntervalMicros * permitsToTake); return micros;} 实现较为复杂,其核心思想在于计算消耗当前存储令牌时需要根据预热设置区别对待。其中涉及到新变量thresholdPermits,该变量为令牌阈值,当当前存储的令牌数大于该值时,消耗(storedPermits-thresholdPermits)范围的令牌需要有预热的过程(即消耗每个令牌的间隔时间慢慢减小),而消耗0~thresholdPermits个数的以存储令牌,每个令牌消耗时间为固定值,即stableIntervalMicros。而thresholdPermits取值需要考虑预热时间及令牌产生速度两个属性,即thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;。可见阈值为预热时间中能够产生的令牌数的一半,并且根据注释计算消耗阈值以上的令牌的时间可以转换为计算预热图的梯形面积(实际为积分),本处不详细展开。使用此种设计可以保证在上次请求间隔时间较长时,令牌桶中存储了较多的令牌,当消耗这些令牌时,最开始的令牌消耗时间较长,后续时间慢慢缩短直到达到stableIntervalMicros的状态,\b产生预热的效果。 GUAVA限流器实现总结以上分析GUAVA限流器实现,其使用了两个抽象类及两个具体子类完成了限流器实现,其中使用顶层抽象类承担了\b创建者角色,将所有子类进行了透明化,减少了程序员在使用工具过程中需要了解的类的数量。在实现限流器的过程中,基于令牌桶的思想,并且增加了带有预热器的令牌桶限流器实现。被限流的\b线程使用其自带的SleepingStopwatch工具类,最终使用的是Thread.sleep(ms, ns);方法,而线程使用sleep休眠时其持有的锁并不会释放,在多线程编程时此处需要注意。最后,GUAVA的限流器触发算法采用的是预定令牌的方式,即当前请求需要的令牌数不会对当前请求的等待时间造成影响,而是会影响下一次请求的等待时间。 总结本文主要\b总结了当前常用的服务限流算法,对比个各算法特点,\b最后\b分析GUAVA包中对于限流器的实现的核心方法。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"},{"name":"开源工具","slug":"Java/开源工具","permalink":"http://bluerhino.github.io/categories/Java/开源工具/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"Guava","slug":"Guava","permalink":"http://bluerhino.github.io/tags/Guava/"},{"name":"限流器","slug":"限流器","permalink":"http://bluerhino.github.io/tags/限流器/"}]},{"title":"图解ReentrantReadWriteLock实现分析","slug":"ReentrantReadWriteLock实现分析","date":"2018-06-20T14:48:35.000Z","updated":"2018-08-19T10:59:36.000Z","comments":true,"path":"2018/06/20/ReentrantReadWriteLock实现分析/","link":"","permalink":"http://bluerhino.github.io/2018/06/20/ReentrantReadWriteLock实现分析/","excerpt":"概述本文主要分析JCU包中读写锁接口(ReadWriteLock)的重要实现类ReentrantReadWriteLock。主要实现读共享,写互斥功能,对比单纯的互斥锁在共享资源使用场景为频繁读取及少量修改的情况下可以较好的提高性能。","text":"概述本文主要分析JCU包中读写锁接口(ReadWriteLock)的重要实现类ReentrantReadWriteLock。主要实现读共享,写互斥功能,对比单纯的互斥锁在共享资源使用场景为频繁读取及少量修改的情况下可以较好的提高性能。 ReadWriteLock接口简单说明ReadWriteLock接口只定义了两个方法:123456789101112131415public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock();} 通过调用相应方法获取读锁或\b写锁,获取的读锁及写锁都是Lock接口的实现,可以如同使用Lock接口一样使用(其实也有一些特性是不支持的)。 ReentrantReadWriteLock使用示例读写锁的使用并不复杂,可以参考以下使用示例:123456789101112131415161718192021222324252627 class RWDictionary { private final Map<String, Data> m = new TreeMap<String, Data>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); public Data get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); } } public Data put(String key, Data value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } }} 与普通重入锁使用的主要区别在于需要使用不同的锁\b对象引用读写锁,并且在读写时分别调用对应的锁。 ReentrantReadWriteLock锁实现\b分析本节通过学习源码\b分析可重入读写锁的实现。 图解重要函数及对象关系根据示例代码可以发现,读写锁需要关注的重点函数为获取读锁及写锁的函数,对于读锁及写锁对象则主要关注加锁和解锁函数,这几个函数及对象关系如下图:从图中可见读写锁的\b加锁\b解锁操作最终都是调用ReentrantReadWriteLock类的\b内部类Sync提供的方法。与细谈重入锁ReentrantLock一文中描述相似,Sync对象通过继承AbstractQueuedSynchronizer进行实现,故后续分析主要基于Sync类进行。 读写锁Sync结构分析Sync继承于AbstractQueuedSynchronizer,其中主要功能均在AbstractQueuedSynchronizer中完成,其中最重要功能为控制线程获取锁失败后转换为等待状态及在\b满足一定条件后唤醒等待状态的线程。先对AbstractQueuedSynchronizer进行观察。 AbstractQueuedSynchronizer图解为了更好理解AbstractQueuedSynchronizer的运行机制,可以首先研究其内部数据结构,如下图:图中展示AQS类较为重要的数据结构,包括int类型变量state用于记录锁的状态,继承自AbstractOwnableSynchronizer类的Thread类型变量exclusiveOwnerThread用于指向当前排他的获取锁的线程,AbstractQueuedSynchronizer.Node类型的变量head及tail。其中Node对象表示当前等待锁的节点,Node中thread变量指向等待的线程,waitStatus表示当前等待节点状态,mode为节点类型。多个节点之间使用prev及next组成双向链表,参考CLH锁队列的方式进行锁的获取,但其中与CLH队列的重要区别在于CLH队列中后续节点需要自旋轮询前节点状态以确定前置节点是否已经释放锁,期间不释放CPU资源,而AQS中Node节点指向的线程在获取锁失败后调用LockSupport.park函数使其进入阻塞状态,让出CPU资源,故在前置节点释放锁时需要调用unparkSuccessor函数唤醒后继节点。 根据以上说明可得知此上图图主要表现当前thread0线程获取了锁,thread1线程正在等待。 读写锁Sync对于AQS使用读写锁中Sync类是继承于AQS,并且主要使用上文介绍的数据结构中的state及waitStatus变量进行实现。实现读写锁与实现普通\b互斥锁的主要区别在于需要分别记录读锁状态及写锁状态,并且等待队列中需要区别处理两种加锁操作。Sync使用state变量同时记录读锁与写锁状态,将int类型的state变量分为高16位与第16位,高16位记录读锁状态,低16位记录写锁状态,如下图所示:Sync使用不同的mode描述等待队列中的节点\b以区分读锁等待节点和写锁等待节点。mode取值包括SHARED及EXCLUSIVE两种,分别代表当前等待节点为读锁和写锁。 读写锁Sync代码过程分析写锁加锁通过\b对于重要函数关系的分析,写锁加锁最终调用Sync类的acquire函数(继承自AQS)12345public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 现在分情况图解分析 无锁状态无锁状态AQS内部数据结构如下图所示:其中state变量为0,表示高位地位地位均为0,没有任何锁,且等待节点的首尾均指向空(此处特指head节点没有初始化时),锁的所有者线程也为空。在无锁状态进行加锁操作,线程调用acquire 函数,首先使用tryAcquire函数判断锁是否可获取成功,由于当前是\b无锁状态必然成功获取锁(如果多个线程同时进入此函数,则有且只有一个线程可调用compareAndSetState成功,其他线程转入获取锁失败的流程)。获取锁成功后AQS状态为: 有锁状态在加写锁时如果当前AQS已经是有锁状态,则需要进一步处理。有锁状态主要分为已有写锁和已有读锁状态,并且根据最终当前线程是否可直接获取锁\b分为两种情况: \b非重入:如果满足一下两个条件之一,当前线程必须加入等待队列(暂不考虑非公平锁抢占情况)a. 已有读锁;b. 有写锁且获取写锁的线程不为当前请求锁的线程。 重入:有写锁且当前获取写锁的线程与当前请求锁的线程为同一线程,则直接获取锁并将写锁状态值加1。写锁重入状态如图:写锁非重入等待状态如图:在非重入状态,当前线程创建等待节点追加到等待队列队尾,如果当前头结点为空,则需要创建一个默认的头结点。之后再当前获取锁的线程释放锁后,会唤醒等待中的节点,即为thread1。如果当前等待队列存在多个等待节点,由于thread1等待节点为EXCLUSIVE模式,则只会唤醒当前一个节点,不会传播唤醒信号。 读锁加锁通过\b对于重要函数关系的分析,写锁加锁最终调用Sync类的acquireShared函数(继承自AQS): 1234public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } 同上文,现在分情况图解分析 无锁状态无所状态AQS内部数据状态图与写加锁是无锁状态一致:在无锁状态进行加锁操作,线程调用acquireShared 函数,首先使用tryAcquireShared函数判断共享锁是否可获取成功,由于当前为无锁状态则获取锁一定成功(如果同时多个线程在读锁进行竞争,则只有一个线程能够直接获取读锁,其他线程需要进入fullTryAcquireShared函数继续进行锁的获取,该函数在后文说明)。当前线程获取读锁成功后,AQS内部结构如图所示:其中有两个新的变量:firstReader及firstReaderHoldCount。firstReader指向在无锁状态下第一个获取读锁的线程,firstReaderHoldCount记录第一个获取读锁的线程持有当前锁的\b计数(主要用于重入)。 有锁状态无锁状态获取读锁比较简单,在有锁状态则需要分情况讨论。其中需要分当前被持有的锁是读锁还是写锁,并且每种情况需要区分等待队列中是否有等待节点。 \b已有读锁且等待队列为空此状态比较简单,图示如:此时线程申请读锁,首先调用readerShouldBlock函数进行判断,该函数根据当前锁是否为公平锁判断规则稍有不同。如果为非公平锁,则只需要当前第一个等待节点不是写锁就可以尝试获取锁(考虑第一点为写锁主要为了方式写锁“饿死”);如果是公平锁则只要有等待节点且当前锁不为重入就需要等待。由于本节的\b前提是等待队列为空的情况,故readerShouldBlock函数一定返回false,则当前线程使用CAS对读锁计数进行增加(同上文,如果同时多个线程在读锁进行竞争,则只有一个线程能够直接获取读锁,其他线程需要进入fullTryAcquireShared函数继续进行锁的获取)。在成功对读锁计数器进行\b增加后,当前线程需要继续对当前线程持有读锁的计数进行增加。此时分为两种情况: 当前线程是第一个获取读锁的线程,此时由于第一个获取读锁的线程已经通过firstReader及firstReaderHoldCount两个变量进行\b存储,则仅仅需要将firstReaderHoldCount加1\b即可; 当前线程不是第一个获取读锁的线程,则需要使用readHolds\b\b进行存储,readHolds是ThreadLocal的子类,通过readHolds可获取当前线程对应的HoldCounter类的对象,该对象保存了当前线程获取读锁的计数。考虑程序的局部性原理,又使用cachedHoldCounter缓存\b最近使用的HoldCounter类的对象,\b如在一段时间内只有一个线程请求读锁则可加速对读锁获取的计数。第一个读锁线程重入如图:非首节点获取读锁根据上图所示,thread0为首节点,thread1线程继续申请读锁,获取成功后使用ThreadLocal链接</subp>的方式进行存储计数对象,并且由于其为最近获取读锁的线程,则cachedHoldCounter对象设置指向thread1\b对应的计数对象。\b已有读锁且等待队列不为空在当前锁已经被读锁获取,且等待队列不为空的情况下 ,可知等待队列的头结点一定为写锁获取等待,这是由于在读写锁实现过程中,如果\b某线程获取了读锁,则会唤醒当前等到节点之后的所有等待模式为SHARED的节点,直到队尾或遇到EXCLUSIVE\b模式的等待节点(具体实现函数为setHeadAndPropagate后续还会遇到)。所以可以确定当前\b为读锁状态其有等待节点情况下,首节点一定是写锁等待。如图所示:上图展示当前thread0与thread1线程获取读锁,thread0为首个获取读锁的节点,并且thread2线程在等待获取写锁。\b在上图显示的状态下,无论公平锁还是非公平锁的实现,新的读锁加锁一定会进行排队,添加等待节点在写锁等待节点之后,这样可以防止写操作的饿死。\b申请读锁后的状态如图所示:如图所示,在当前锁被为读锁且有等待队列情况下,thread3及thread4线程申请读锁,则被封装为等待节点追加到当前等待队列后,节点模式为SHARED,线程使用LockSupport.park函数进入\b阻塞状态,让出CPU资源,直到前驱的等待节点完成锁的获取和释放后进行唤醒。已有写锁\b被获取当前线程申请读锁时发现写锁已经被获取,则无论等待队列是否为空,线程一定会需要加入等待队列(注意在非公平锁实现且前序没有写锁申请的等待,线程有机会抢占获取锁而不进入等待队列)。写锁被获取的情况下,AQS状态为如下状态在两种情况下,读锁获取都会进入等待队列等待前序节点唤醒,这里不再赘述。读等待节点被唤醒读写锁与单纯的排他锁主要区别在于读锁的共享性,在读写锁实现中保证读锁能够共享的其中一个机制就在于,如果一个读锁等待节点被唤醒后其会继续唤醒拍在当前唤醒节点之后的SHARED模式等待节点。\b查看源码:12345678910111213141516171819202122232425262728private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { //注意看这里 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } 在for循环中,线程如果获取读锁成功后,需要调用setHeadAndPropagate方法。查看其源码: 12345678910private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } } 在满足传播条件情况下,获取读锁后继续唤醒后续节点,所以如果当前锁是读锁状态则等待节点第一个节点一定是写锁等待节点。 锁降级锁降级算是获取读锁的特例,如在t0线程已经获取写锁的情况下,再调取读锁加锁函数则可以直接获取读锁,但此时其他线程仍然无法获取读锁或写锁,在t0线程释放写锁后,如果有节点等待则会唤醒后续节点,后续节点可见的状态为目前有t0线程获取了读锁。所降级有什么应用场景呢?引用读写锁中使用示例代码 1234567891011121314151617181920212223242526272829303132 class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } }} 其中针对变量cacheValid的使用主要过程为加读锁、读取、释放读锁、加写锁、修改值、加读锁、释放写锁、使用数据、释放读锁。其中后续几步(加写锁、修改值、加读锁、释放写锁、使用数据、释放读锁)为典型的锁降级。如果不使用锁降级,则过程可能有三种情况: 第一种:加写锁、修改值、释放写锁、使用数据,即\b使用写锁修改数据后直接使用刚修改的数据,这样可能\b有数据的不一致,如当前线程释放写锁的同时其他线程(如t0)获取写锁准备修改(还没有改)cacheValid变量,而当前线程却继续运行,则当前线程读到的cacheValid变量的值为t0修改前的老数据; 第二种:加写锁、修改值、使用数据、释放写锁,即将修改数据与再次使用数据合二为一,这样不会有数据的不一致,但是由于混用了读写两个过程,以排它锁的方式使用读写锁,减弱了读写锁读共享的优势,增加了写锁(独占锁)的占用时间; 第三种:加写锁、修改值、释放写锁、加读锁、使用数据、释放读锁,即使用写锁修改数据后再请求读锁来使用数据,这是时数据的一致性是可以得到保证的,但是由于\b释放写锁和获取读锁之间存在时间差,则当前想成可能会需要进入等待队列进行等待,可能造成线程的阻塞降低吞吐量。 因此针对以上情况提供了锁的\b降级功能,可以在完成数据修改后尽快读取最新的值,且能够减少写锁占用时间。最后注意,读写锁不支持锁升级,即获取读锁、读数据、获取写锁、释放读锁、释放写锁这个过程,因为读锁为共享锁,如同时有多个线程获取了读锁后有一个线程进行锁升级获取了写锁,这会造成同时有读锁(其他线程)和写锁的情况,造成其他线程可能无法感知新修改的数据(此为逻辑性错误),并且在JAVA读写锁实现上由于当前线程获取了读锁,再次请求写锁时必然会阻塞而导致后续释放读锁的方法无法执行,这回造成死锁(此为功能性错误)。 写锁释放锁过程了解了加锁过程后解锁过程就非常简单,每次调用解锁方法都会减少重入计数次数,直到减为0则唤醒后续第一个等待节点,如唤醒的后续\b节点为读等待节点,则后续节点会继续传播唤醒\b状态。 读锁释放过程读锁释放过比写锁稍微复杂,因为是\b共享锁,所以可能会有多个线程同时获取读锁,故在解锁时需要做两件事: 获取当前线程对应的重入计数,并进行减1,此处\b天生为线程安全的,不需要特殊处理; \b当前读锁获取次数减1,此处由于可能存在多线程竞争,故使用自旋CAS进行设置。完成以上两步后,如读状态为0,则唤醒后续等待节点。 总结根据以上分析,本文主要展示了读写锁的场景及方式,并分析读写锁核心功能(加解锁)的代码实现。Java读写锁同时附带了更多其他方法,包括锁状态监控和带超时机制的加锁方法等,本文不在赘述。并且读写锁中写锁可使用Conditon机制也不在详细说明。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"},{"name":"并发","slug":"Java/并发","permalink":"http://bluerhino.github.io/categories/Java/并发/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"并发","slug":"并发","permalink":"http://bluerhino.github.io/tags/并发/"},{"name":"重入锁","slug":"重入锁","permalink":"http://bluerhino.github.io/tags/重入锁/"},{"name":"读写锁","slug":"读写锁","permalink":"http://bluerhino.github.io/tags/读写锁/"},{"name":"图解","slug":"图解","permalink":"http://bluerhino.github.io/tags/图解/"}]},{"title":"细谈重入锁ReentrantLock","slug":"细谈重入锁ReentrantLock","date":"2018-04-26T14:26:54.000Z","updated":"2018-08-19T10:59:43.000Z","comments":true,"path":"2018/04/26/细谈重入锁ReentrantLock/","link":"","permalink":"http://bluerhino.github.io/2018/04/26/细谈重入锁ReentrantLock/","excerpt":"概述在java.util.concurrent.locks.Lock详解一文中简单的描述了JDK中JUC包对于Lock接口的定义,并且简单的对比了Lock接口及Java关键字synchronized的异同。本文主要研究Lock接口的常用实现ReentrantLock,本文主要分为以下几部分: 什么是重入? ReentrantLock的实现分析; Condition对象介绍; ReentrantLock性能分析; ReentrantLock使用场景; 总结。","text":"概述在java.util.concurrent.locks.Lock详解一文中简单的描述了JDK中JUC包对于Lock接口的定义,并且简单的对比了Lock接口及Java关键字synchronized的异同。本文主要研究Lock接口的常用实现ReentrantLock,本文主要分为以下几部分: 什么是重入? ReentrantLock的实现分析; Condition对象介绍; ReentrantLock性能分析; ReentrantLock使用场景; 总结。 什么是重入?ReentrantLock通过类名称可顾名思义,由Reentrant及Lock两部分组成。Lock略去不谈,观察Reentrant单词可以简单将其拆为前缀re及主体entrant,即re:再,entrant:进入。显而易见重入就是再次进入的意思,在并发编程中具体指某线程已经获取了一个锁后,再次请求这个锁时可以获得该锁而不会阻塞。 具体示例在某线程递归调用某函数时较容易观察重入锁与非重入锁区别,如以下代码:123456789101112131415161718192021public class ReentrantLockTest { private volatile int i = 20; public synchronized void callBack() { if (i > 0) { i--; System.out.println(Thread.currentThread().getName() + \":\" + i); callBack(); } else{ //将i重置 i = 20; } } public static void main(String[] args) { ReentrantLockTest reentrantLockTest = new ReentrantLockTest(); reentrantLockTest.callBack(); }} callBack函数使用synchronized关键字进行同步控制,synchronized关键字可以确保同时只有一个线程可进入临界区,并且由于synchronized支持重入,故上面的代码可以正常运行(synchronized关键字实现原理以后再做补充)。 简单非重入锁现为Lock接口编写一个简单实现:12345678910111213141516171819202122232425262728293031public class BlueRhinoLock implements Lock { private BlockingQueue<Object> blockingQueue = new LinkedBlockingQueue<Object>(1); public BlueRhinoLock() { try { this.blockingQueue.put(new Object()); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void lock() { try { blockingQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void unlock() { try { blockingQueue.put(new Object()); } catch (InterruptedException e) { e.printStackTrace(); } } //其他接口函数略 } 简单起见,该锁基于BlockingQueue将capacity取值为1,实现了一个不可重入的排它锁。如以下代码:123456789101112131415161718192021222324public class ReentrantLockTest { private volatile int i = 5; BlueRhinoLock blueRhinoLock = new BlueRhinoLock(); public void callBack() { blueRhinoLock.lock(); if (i > 0) { i--; System.out.println(Thread.currentThread().getName() + \":\" + i); callBack(); }else{ i = 5; } blueRhinoLock.unlock(); } public static void main(String[] args) { ReentrantLockTest reentrantLockTest = new ReentrantLockTest(); for(int i =0 ;i<2;i++){ new Thread(() -> reentrantLockTest.callBack()).start(); } }} 此段代码不使用synchronized关键字而使用BlueRhinoLock进行同步控制,在任一线程运行到blueRhinoLock.lock();语句后获取锁,则所有其他线程需要在此等待。但由于本段代码中存在回调,同一线程会在回调callBack时再次运行到blueRhinoLock.lock();代码行时则由于无法获取锁进入等待,则形成由于锁不支持重入而形成死锁。运行结果为:12Thread-0:4//无法继续打印 简单重入锁为了解决以上死锁问题,需要对以上的简单锁进行改进使其支持重入。主要需要完成对于同一线程请求锁时,若当前线程已经拥有当前锁且没有释放的情况下,直接继续执行,根据以上思路配合使用ThreadLocall可以将代码修改为:123456789101112131415161718192021222324252627282930313233343536373839public static class BlueRhinoReentrantLock implements Lock { private BlockingQueue<Object> blockingQueue = new LinkedBlockingQueue<Object>(1); private ThreadLocal<Object> threadLocal = new ThreadLocal<>(); public BlueRhinoReentrantLock() { try { this.blockingQueue.put(new Object()); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void lock() { if(threadLocal.get() != null){ return ; } try { Object o = blockingQueue.take(); threadLocal.set(o); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void unlock() { Object o = threadLocal.get(); if(o == null){ return; } try { blockingQueue.put(o); } catch (InterruptedException e) { e.printStackTrace(); } } } 注意以上为测试代码,没有充分考虑安全性问题 以上代码使用ThreadLocal记录当前线程是否已经获取锁,解决了重入问题。调用代码为:123456789101112131415161718192021222324public class ReentrantLockTest { private volatile int i = 5; BlueRhinoReentrantLock blueRhinoLock = new BlueRhinoReentrantLock(); public void callBack() { blueRhinoLock.lock(); if (i > 0) { i--; System.out.println(Thread.currentThread().getName() + \":\" + i); callBack(); }else{ i = 5; } blueRhinoLock.unlock(); } public static void main(String[] args) { ReentrantLockTest reentrantLockTest = new ReentrantLockTest(); for(int i =0 ;i<2;i++){ new Thread(() -> reentrantLockTest.callBack()).start(); } }} 运行结果为:12345678910Thread-0:4Thread-0:3Thread-0:2Thread-0:1Thread-0:0Thread-1:4Thread-1:3Thread-1:2Thread-1:1Thread-1:0 ReentrantLock的实现分析总览以上代码简单演示了重入锁与非重入锁的区别,但是代码实现非常粗糙,本节主要深入到JDK8中ReentrantLock类的实现,学习生产级别的代码如何实现重入锁。JDK8中ReentrantLock代码包括注释为763行,作者为大神Doug Lea。其UML图为:通过UML可以看出与ReentrantLock与其相关类(接口)之间的重要关系: ReentrantLock实现了Lock接口(应该说是废话); 抽象类Sync是ReentrantLock重要组成部分; FairSync及NonFairSync均继承于Sync; Sync继承抽象类了AbstractQueuedSynchronizer; AbstractQueuedSynchronizer继承了抽象类AbstractOwnableSynchronizer。 下文从一些重要的函数入手开始研究其实现。 重要函数构造函数ReentrantLock只有两个构造函数1234567public ReentrantLock() { sync = new NonfairSync();}public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 构造函数均比较简单,都创建了一个Sync类型的实例变量,由于Sync是抽象类,实际创建的为其子类,默认为NonfairSync,在带boolean参数的构造函数中可以使用参数值指定创建NonfairSync或FairSync对象。源码中对于Sync的介绍为: Base of synchronization control for this lock. Subclassed into fair and nonfair versions below. Uses AQS state to represent the number of holds on the lock. 重点在于使用AQS(AbstractQueuedSynchronizer)记录锁的获取数量,由此可见AbstractQueuedSynchronizer为ReentrantLock实现提供了重要支持。在前面的文章深入AQS介绍了AQS相关技术。 lock函数lock函数为Lock接口的核心函数之一,在Lock接口中对于该函数的注释为: Acquires the lock.If the lock is not available then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired. 在ReentrantLock的实现中对于Lock函数也有注释: Acquires the lock.Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.If the current thread already holds the lock then the hold count is incremented by one and the method returns immediately.If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired, at which time the lock hold count is set to one. 对比两段注释可见,都表明该函数主要功能为“获取锁”。在Lock接口中仅仅规定如果当前线程无法获取锁则进入无法调度的休眠状态直到获得锁。在ReentrantLock实现时细化了其实现细节,说明在当前线程获得锁后,将其锁计数器置为1,以后持有该锁的线程再次申请锁则将计数器加1并且立即返回,如当前线程无法获取锁则进入无法调度的休眠状态直道获取锁,并且在获取锁的同时将锁计数器置为1。现在具体看实现代码:123public void lock() { sync.lock(); } 可见直接调用了sync的lock函数,再进入lock函数进行研究。1abstract void lock(); sync的lock函数是一个抽象函数,在两个子类NonfairSync、FairSync中进行了不同实现,上一节构造函数中已经看见过这两个类了,分别代表非公平算法实现的同步器及公平算法实现的同步器,无参数构造函数默认创建的是非公平的同步器。 NonfairSync中的lock先考察NonfairSync中的lock函数:123456final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } 进入函数首先使用compareAndSetState函数进行设置,该函数实际是从AbstractQueuedSynchronizer类继承而来,其功能为使用CAS操作原子的设置对象中一个名为state的int型变量。如果实际值与期望值相等均为0,说明当前锁没有被持有,则直接将state设置为1,继续调用setExclusiveOwnerThread函数将当前锁的所有者设置为当前线程。如果实际值与期望值0不相等则说明当前锁已经被别的线程获取,需要使用acquire函数进行锁的获取,该函数仍然是从AbstractQueuedSynchronizer类继承而来其过程如下:根据流程图继续进行分析,进入acquire函数首先调用tryAcquire再次尝试获取锁,这是因为在线程从使用compareAndSetState设置state失败到当前时刻,原来占用锁的的线程可能已经释放了锁,如果这次尝试成功可以较大的减少将线程加入等待队列的性能消耗。根据前文深入AQS的介绍,tryAcquire函数是AQS类的成员函数,使用继承AQS类的方式实现同步器时需要在子类覆盖tryAcquire方法。在ReentrantLock中对于tryAcquire调用会根据当前锁是否公平锁,而最终调用NonfairSync或FairSync对象的tryAcquire方法。由于本处分析NonfairSync的lock函数,则继续查看NonfairSync的tryAcquire函数:123protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } 其最终调用nonfairTryAcquire函数:123456789101112131415161718final 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; } 该函数主要完成如下逻辑: 获取当前线程对象及当前锁的状态值; 如果当前锁的状态值为0,则说明当前没有线程获取锁,使用CAS方式设置锁的状态为acquires(根据上文参数,此处为1);如果不为0说明已有线程获取了锁,直接跳转到第5步; 如果设置成功,说明获取锁成功,则直接将当前锁的排他所有者线程设置为当前线程(由于第二步CAS设置只可能有一个线程成功,此处代码不需要作临界区保护); 如果设置不成功,说明同一时刻有其他线程调用compareAndSetState,并且获得成功,则当前线程竞争锁失败直接返回false; 当前状态值不为0,需要继续判断以获取锁的线程是不是当前线程: 如果是,说明当前是同一线程重入的获取锁,则将当前锁状态加acquires(根据上文参数,此处为1),之后锁状态如果溢出,抛出异常,否则将锁的状态设置为新的值,返回true; 如果不是,尝试获取锁失败,直接返回false 以上第5.1步逻辑是实现重入的关键代码。完成tryAcquire调用后,如果获取锁失败,则将当前线程封装为等待节点加入等待队列中。后续详细操作主要由AQS完成,可参考深入AQS。 FairSync中的lock继续研究FairSync中的lock123final void lock() { acquire(1); } 为方便比较将NonfairSync的lock拷贝下来:123456final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } 对比代码可见公平锁实现较非公平锁简单,其直接调用acquire函数,第一次放弃插队的机会。而acquire函数如上文分析,最开始会调用FairSync类实现的tryAcquire函数,其代码如下:12345678910111213141516171819protected 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; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error(\"Maximum lock count exceeded\"); setState(nextc); return true; } return false; } 对比非公平锁代码,其实现最大逻辑区别在于第五行,公平锁实现时,在当前锁没有被其他线程获取时,会判断当前等待队列是否有等待锁的线程,如果没有才会获取锁,否则直接返回失败,在此处第二次放弃插队机会,由此保证线程获取锁的顺序一定与申请锁的等待时间相同。 lockInterruptibly函数lockInterruptibly函数是前文lock函数的可中断版本,查看源代码123public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } 该函数通过调用AQS类的acquireInterruptibly函数实现,继续查看源码:1234567public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg);} 该函数实现逻辑与lock函数调用的AQS类的lock函数基本相同,主要增加了对线程中断的判断。进入函数首先判断当前线程是否已经中断,中断后则直接抛出中断异常,停止获取锁。如果没有被中断,则调用tryAcquire尝试获取锁,如果获取锁失败则进入doAcquireInterruptibly函数,该函数代码如下:12345678910111213141516171819202122private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } 对比lock函数调用的acquireQueued函数,主要区别在于第16行,如果在阻塞过程中被中断直接抛出中断异常到上层函数,并且取消当前等待节点。 tryLock函数tryLock函数尝试获取锁,与lock函数不同在于,该函数不阻塞,如果函数锁成功则返回true,否则直接返回false。 tryLock(long timeout, TimeUnit unit)函数该函数与lock函数逻辑相似,主要区别在于调用LockSupport工具类时使用parkNanos函数,指定等待时间。该函数在获取到锁或者达到过期时间时返回,获取锁则返回true,否则返回false。 unlock函数unlock函数用于释放锁,其调用syn.release函数,代码在AQS中实现:123456789public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 其逻辑为调用tryRelease函数释放锁,在释放成功后唤醒当前头结点的后续等待节点。tryRelease在AQS中没有具体实现,在ReentrantLock类的内部工具类Syn中实现,代码为:123456789101112protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } 逻辑为: 获取当前加锁状态,并将其减少releases(此处为1); 判断当前线程是否是获取锁的线程,如果当前线程没有获取锁则直接抛出异常; 若没有异常则需判断当前的加锁状态是否为0,此步与每次重入加锁状态加1对应,如果当前加锁状态已经为0,表示当前线程已经释放锁,则重新设置锁的持有线程为空,返回true,上文release函数不唤醒后续节点; 若加锁状态不为0,则更新加锁状态,返回false,上文release函数不唤醒后续节点。 newCondition函数该函数用于返回一个Condition接口实例,该类在AQS中有相关实现ConditionObject,由于ConditionObject为AQS非静态内部类,故通过调用newCondition函数创建的Condition接口实例会与当前的AQS对象自动关联起来(此处可参考非静态内部类特点)。对于Condition接口下一节再详细介绍。 Condition对象介绍Condition接口主要用于多线程加锁环境下,不同线程之间的协作。提供的核心方法为await及signal,语义为等待和通知。参考使用代码为:12345678910111213141516171819202122232425262728293031323334353637class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } } 以上代码引用自JDK8的Condition注释,实现了一个简单的有界缓存。主要包括存和取两个函数,由于可能使用与多线程环境,类中定义了一个重入锁进行对象属性的包括,保证同一时刻最多仅有一个线程可对有界缓存进行操作。但在缓存使用过程中,可能出现存入对象时有界队列已满,或取对象时队列还是空这两种情况。当缓存在插入对象时,如队列已满则阻塞等待缓存有空间后继续插入;在读取对象时,如缓存为空则阻塞等待直到缓存有新的元素插入。为实现以上功能,该段代码使用了使用lock.newCondition()创建notFull,notEmpty两个Condition对象,分别表示当前缓存非满和非空,在线程满时调用notFull.await()阻塞等待,直到缓存有空间后调用notFull.signal()唤醒等待线程,notEmpty用法类似。那Condition对象是如何完成以上功能?与Object提供的wait及notify函数又有何区别? Condition对象实现通过以上示例可以学习到Condition接口的两个主要函数的用法,现在通过观察其代码实现学习具体实现逻辑。 await函数实现本处查看AQS类中Condition接口的实现类ConditionObject中await函数的实现代码:123456789101112131415161718public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } 通过以上代码发现该函数主要逻辑为:其中释放锁时相当于将当前节点从AQS的头节点移除。使用await需要注意需要在当前线程获取锁后再调用,否则在释放锁时会抛出IllegalMonitorStateException异常,且在增加等待队列时会调用unlinkCancelledWaiters函数,该函数并不是线程安全的,这也要求调用该函数时需要首先获取锁。另外在中断方面可以发现如果在Condition对象的等待队列中被中断,当前等待节点会从Condition对象的等待队列中被移除,但是仍然会将自己添加到AQS的等待队列中继续尝试获取锁。所以调用await函数后,即使等待线程被中断也不会立即抛出中断异常,仍然需要等到其获取到锁后才能根据不同中断处理模式进行中断处理。 signal函数实现此处仍然查看AQS类中ConditionObject中signal的实现代码1234567public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } 代码首先调用isHeldExclusively函数判断当前线程是否排他的持有当前锁,如果不存在则直接抛出IllegalMonitorStateException异常。如果持有锁则调用doSignal函数:12345678private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); } 此段代码逻辑也比较简单,首先将ConditionObject的等待节点的头结点设置为待转移节点的后续节点,调用transferForSignal方法将待转移节点附到AQS等待队列的队尾。这里需要注意的是transferForSignal函数的实现:12345678910111213141516171819final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; /* * Splice onto queue and try to set waitStatus of predecessor to * indicate that thread is (probably) waiting. If cancelled or * attempt to set waitStatus fails, wake up to resync (in which * case the waitStatus can be transiently and harmlessly wrong). */ Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true;} 该函数首先将节点状态从Node.CONDITION设置为无状态,如果当前状态不为Node.CONDITION则返回上层函数,继续设置下一个节点。将节点设置为无状态后将其加入AQS等待队列,并且判断前置节点状态,如果前置节点已经被取消,则唤醒当前节点中阻塞的线程。根据Condition实现可知,当前节点阻塞的线程必然在await函数的LockSupport.park(this)行阻塞,如果发现其前驱节点已经取消则直接唤醒该线程,此处需要注意唤醒此线程并不会让该线程直接获取锁,该线程仍然需要等待获取锁。对于Condition对象的其他函数核心实现逻辑相似,不在赘述。 对比Object的wait及notify函数根据上文分析Condition类的await及signal函数与Object类的wait及notify功能似乎非常类似,此处进行两种实现的对比。 函数名称 所属类或接口 功能 使用条件 wait Object 阻塞等待其他线程调用notify 需要先使用synchronized获取调用对象的锁 notify Object 使被wait阻塞的线程进度调度 需要先使用synchronized获取调用对象的锁 await Condition 阻塞等待其他线程调用signal 需要先获取Condition对象关联的锁对象代表的锁 signal Condition 使被await阻塞的线程进度调度 需要先获取Condition对象关联的锁对象代表的锁 直接查看发现以上两种线程间交互方式似乎没有明显不同,但在使用中await及notify方式明细较为灵活,因为一个Lock对象可以创建多个Condition对象用于说明在同一互斥资源上不同的等待条件,如上文BoundedBuffer缓存示例代码,如果使用wait配合synchronized进行实现,则必须同时唤醒插入及读取队列,并且根据判断,总有一个函数需要继续阻塞等待,效率及灵活性都较差。 ReentrantLock性能分析本文没有针对重入锁性能进行深入测试,参考《Java并发编程实战》一书总结如下:ReentrantLock与内置锁synchronized在JAVA6之后性能差距不大。ReentrantLock锁的公平实现高并发性能明显低于非公平实现,针对代码分析,造成这种原因在于公平锁在高度竞争条件下几乎所有线程都会经过排队及上下文切换(即经过park函数阻塞及unpark函数唤醒),而非公平锁由于可能存在插队的情况,线程上下文切换次数明显少于公平锁,这是是的非公平锁性能更好的主要原因。 ReentrantLock使用场景ReentrantLock提供比内置锁更灵活的加锁方式,在JCU包中有较多应用。随着synchronized的性能优化,在简单加锁情况下,还是优选synchronized关键字,其使用简单且不需要显示释放,使用出错的几率较低。如果需要灵活的加锁策略(如上文有界缓存示例类)或需要超时机制等可以考虑使用重入锁,当需要尤其注意需要释放锁,推荐在finally代码块中释放锁,保证正常异常情况都能成功释放。 总结本文在代码层面详细分析了重入锁主要函数实现,ReentrantLock实现了较为灵活的重入锁,对比内置synchronized关键字,不仅提供常规的加锁解锁操作,也提供了非阻塞获取锁及超时时间内获取锁的方法,并且通过Conditon对象提供灵活的线程间通信方式。但其使用比synchronized关键字复杂,且不能自动释放,且随着synchronized关键字性能的提升,故在只需要简单加锁时仍推荐使用synchronized关键字,在需要灵活的加锁策略时考虑使用ReentrantLock","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"},{"name":"并发","slug":"Java/并发","permalink":"http://bluerhino.github.io/categories/Java/并发/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"并发","slug":"并发","permalink":"http://bluerhino.github.io/tags/并发/"},{"name":"重入锁","slug":"重入锁","permalink":"http://bluerhino.github.io/tags/重入锁/"}]},{"title":"深入AQS","slug":"深入AQS","date":"2018-04-07T03:31:54.000Z","updated":"2018-08-19T10:59:48.000Z","comments":true,"path":"2018/04/07/深入AQS/","link":"","permalink":"http://bluerhino.github.io/2018/04/07/深入AQS/","excerpt":"简介AbstractQueuedSynchronizer一般简称AQS,是位于JUC包中的重要工具类,包括ReentrantLock、CountDownLatch等众多提供阻塞方法的类都是基于AQS进行编写的,理解AQS对于理解JUC包中许多类的实现都有极大的帮助。本文通过阅读AQS源码的方式,学习AQS实现的原理及技巧。下文主要包括以下几方面: AQS总览; AQS主要功能; 使用示例-排它锁","text":"简介AbstractQueuedSynchronizer一般简称AQS,是位于JUC包中的重要工具类,包括ReentrantLock、CountDownLatch等众多提供阻塞方法的类都是基于AQS进行编写的,理解AQS对于理解JUC包中许多类的实现都有极大的帮助。本文通过阅读AQS源码的方式,学习AQS实现的原理及技巧。下文主要包括以下几方面: AQS总览; AQS主要功能; 使用示例-排它锁 AQS总览AQS在java.util.concurrent.locks包中,其UML图为:可见AQS继承于AbstractOwnableSynchronizer,并且其实现中包含了Node及ConditionObject两个内部类。AbstractOwnableSynchronizer类功能较为简单,不包括构造函数外只提供了两个方法:setExclusiveOwnerThread及getExclusiveOwnerThread,用于设置和获取当前对象所属的线程,需要注意在设置及获取当前对象所属线程时,该类并没有加锁使用时需要调用人员自行保证数据的一致性。 AQS主要功能AQS类的文档中说明其主要作用为: Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. 简单说来就是提供一个实现基于FIFO队列的阻塞锁的框架,在使用中一般推荐继承AQS并实现以下几个方法完成同步逻辑: tryAcquire:线程尝试以排他方式获取锁; tryRelease:线程调用该方法释放排他锁; tryAcquireShared:线程尝试以共享方式获取锁; tryReleaseShared:线程调用该方法释放共享锁; isHeldExclusively:锁是否被排他方式占用 以上几个方法在AQS中均为进行实现,直接调用会抛出UnsupportedOperationException异常,需要子类根据自己的需求实现对应方法。文字描述较为抽象,通过学习AQS文档中自带的代码深入学习 使用示例-排它锁以下代码引用自JDK8中的AQS类注释 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556class Mutex implements Lock, java.io.Serializable { // Our internal helper class private static class Sync extends AbstractQueuedSynchronizer { // Reports whether in locked state protected boolean isHeldExclusively() { return getState() == 1; } // Acquires the lock if state is zero public boolean tryAcquire(int acquires) { assert acquires == 1; // Otherwise unused if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // Releases the lock by setting state to zero protected boolean tryRelease(int releases) { assert releases == 1; // Otherwise unused if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // Provides a Condition Condition newCondition() { return new ConditionObject(); } // Deserializes properly private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } } // The sync object does all the hard work. We just forward to it. private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } } 该类实现了一个互斥锁,实现了Lock接口,在实现过程中主要通过调用内部类Sync的对象sync完成相关工作。重点观察Sync,Sync继承了了AbstractQueuedSynchronizer,并实现了上文提到的5个方法中的3个,即isHeldExclusively、tryAcquire及tryRelease三个方法。 加锁过程当前线程调用lock方法请求锁时,实际上是调用了AQS的acquire函数,该函数代码为: 12345public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 而该函数流程为: tryAcquire函数实现其中tryAcquire函数为示例类Mutex实现, 根据以上代码该函数实现时调用compareAndSetState函数。其功能为使用CAS操作原子的设置对象中一个名为state的int型变量,传入两个int类型参数,分别为预期值及设置后的值,流程如下: compareAndSetState是AQS关键函数之一,查看compareAndSetState源码:1234protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } 从代码可以发现compareAndSetState最终调用了sun.misc.Unsafe类的compareAndSwapInt方法,该函数可以原子性的设置属性,保证线程安全,该方法前两个参数分别为对象地址及偏移量,对象地址执行需要修改的对象,偏移量指定需要修改的整数类型数据在该对象中的内存地址,该函数实现是在Native方法中实现,查看该方法在OpenJdk9中的实现为(代码在:hotspot/src/share/vm/primsunsafe.cpp中): 123456UNSAFE_ENTRY(jint, Unsafe_CompareAndExchangeInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) { oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e));} UNSAFE_END 其中主要为调用Atomic::cmpxchg方法,该方法在不同平台实现略有不同,以下为mac平台实现: 12345678inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value, cmpxchg_memory_order order) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) \"cmpxchgl %1,(%3)\" : \"=a\" (exchange_value) : \"r\" (exchange_value), \"a\" (compare_value), \"r\" (dest), \"r\" (mp) : \"cc\", \"memory\"); return exchange_value;} 以下为windows实现: 1234567891011inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value, cmpxchg_memory_order order) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx }} 可见其中主要使用了c++代码中嵌入汇编代码的方式实现,并且使用宏LOCK_IF_MP判断是否为多核处理器,在是多核处理时会增加lock操作。到当前代码依然可以使用汇编的视角进行语句分析,本文在此就略去,不过有一点小细节可见在windows实现时使用了dword关键字,可见Java整形数据在内存中的确使用了32位进行存储。返回对于acquire函数的研究,调用了tryAcquire后,具有两种情况: 获取锁成功(将值从0设置为1):tryAcquire函数继续调用setExclusiveOwnerThread函数将锁的所有者线程设置为当前线程,并返回true,注意此处有之前调用compareAndSetState函数,表明只可能有设置值成功的线程可调用setExclusiveOwnerThread函数,故可保证线程安全。 获取锁失败(期望设置的值不为0或设置时失败):tryAcquire函数直接返回false。 在tryAcquire函数直接返回true后acquire函数不继续判断直接返回,则最上层的lock函数执行完毕,线程完成锁的申请,继续执行后续代码。tryAcquire函数直接返回false后,继续执行acquireQueued函数。 acquireQueued函数tryAcquire函数核心为尝试将state充0设置为1,在两种情况下可能返回失败: state已经不为0:已有其他线程获取锁; state为0:但同时多个线程同时调用compareAndSwapInt,当前线程竞争失败。 无论以上何种失败,在tryAcquire函数无法获取锁的情况下,继续进行acquireQueued函数函数的调用。在调用acquireQueued之前,函数会调用addWaiter将当前线程封装为节点对象,由于调用acquire函数获取的是排它锁,故调用addWaiter函数时传入创建节点类型为Node.EXCLUSIVE,addWaiter代码如下:1234567891011121314private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } 完成如下功能: 将当前线程封装到Node对象中; 获取当前AQS对象中等待队列的尾节点; 如果当前尾节点不为空:a. 将新建节点的前置节点指向当前尾节点;b. 尝试使用compareAndSetTail函数原子性的将当前AQS对象的尾节点指向新建节点,若指向成功,将原尾节点的后续节点指向新建节点,返回新建节点对象,由于compareAndSetTail函数保证有且只有一个线程能够设置成功,则后续pred.next = node语句能在逻辑上保证线程安全;c. 若设置失败,继续调用enq(node)函数进行自旋. 如当前尾节点不存在或希望更新尾节点时失败,继续调用enq(node)函数。 enq(node)函数主要使用自旋循环CAS方式更新当前队列的尾节点(在首次初始化时会将新建一不携带任何逻辑信息的头结点),此处不再详细展开。完成addWaiter代码的调用后,保证当前封装当前线程的等待节点已经插入等待队列队尾,此时开始真正调用acquireQueued函数,其代码为:123456789101112131415161718192021final 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); }} 根据代码分析: 取出刚加入队列的节点的前置节点,若前置节点为头结点则再次尝试获取锁,这么做的原因是在刚才获取锁失败到当前时刻,排在现在节点之前的等待节点可能被其他线程移除; 如获取锁成功,将当前节点设置为头结点,在设置时,需要将当前节点指向的线程及前置节点的属性都置为null,以帮助GC有机会快速收回不可达对象,之后进入finally块,最后返回false; 如果获取锁失败开始调用shouldParkAfterFailedAcquire函数 调用shouldParkAfterFailedAcquire函数需要传入参数包括当前节点及当前节点的前置结点,其逻辑如下:shouldParkAfterFailedAcquire函数在前置节点状态不为Node.SIGNAL的状态下主要完成两件事:移除队列中所有状态值大于0的节点(如被取消的节点);将前置节点状态设置为Node.SIGNAL。继续acquireQueued函数的分析,在调用shouldParkAfterFailedAcquire函数后,若shouldParkAfterFailedAcquire返回false,则继续刚才的循环,知道获取锁或shouldParkAfterFailedAcquire函数返回true。由于前一次的函数调用将前置节点状态置为了Node.SIGNAL,再次调用shouldParkAfterFailedAcquire大概率情况下会返回true。当shouldParkAfterFailedAcquire函数返回true后,acquireQueued函数继续调用parkAndCheckInterrupt函数。1234private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } 该函数使用到了Jdk(从1.6开始)中提供的工具类方法LockSupport.park(this),代码为:123456public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); } 该函数作用设置当前线程的阻塞对象,并且调用UNSAFE.park(false, 0L)函数将当前线程设置为阻塞状态,让出CPU。UNSAFE.park函数在JVM的native方法中实现,查看JVM源码在Linux平台该函数最终主要使用了pthread_cond_wait及pthread_cond_signal两个函数进行线程的阻塞和唤醒,其中具体实现本文不再赘述。此处需要注意理论上pthread_cond_signal函数的调用仅仅会唤醒一个线程,不会出现惊群现象(即同时唤醒多个线程),但是在某些平台可能无法完全保证,所以LockSupport.park函数说明了三种停止阻塞并继续执行的情况: Some other thread invokes unpark with the current thread as the target; Some other thread interrupts the current thread; The call spuriously (that is, for no reason) returns. 其中第三种情况就可能是由惊群现象所引起。但由于本文中acquireQueued函数使用死循环的方式进行了判断,即使线程被意外唤醒,也可以再次判断其锁的状态,在无法获取锁的情况下会再次阻塞,不会出现逻辑问题。在执行了UNSAFE.park函数后,当前线程进入阻塞状态,不再占用CPU资源(此处的线程调度可参考操作系统相关实现)。当前当前线程被唤醒后从LockSupport.park函数返回,并且通过Thread.interrupted()返回当前线程中断状态。之后继续在循环中继续判断是获取锁还是继续阻塞。这里需要注意由于acquireQueued并没有处理中断,其只会在正常获取锁后返回当前线程是否是由于中断被唤醒的,所以使用acquireQueued获取锁的线程不会因为中断而停止锁的获取。最后如果在获取锁的过程出现异常,则会调用cancelAcquire(node)函数,cancelAcquire函数执行以下逻辑: 将当前节点指向的线程置为空; 设置指向前置节点的引用,跳过所有已经被取消的前驱节点; 将当前节点的状态置为已取消状态; 如果当前节点是尾节点,原子性的设置当前节点的前置节点为尾节点,并且将前置节点的后驱节点置为空; 如果当前节点不是尾节点,则判断当前节点的前置节点是不是头结点且前置节点状态为Node.SIGNAL则将前置节点的后置节点置为当前节点的后置置节点,否则调用unparkSuccessor函数唤醒后置节点。 锁获取总结到此为止通过分析acquire函数的实现,完成了Mutex类lock函数的实现的分析。在获取锁的过程中主要使用AQS相关功能,通过维护等待队列的方式完成线程的阻塞及唤醒。Mutex类是AQS注释中的示例类,不能应用在实际生产环境中,如以上加锁过程没有考虑重入问题,生产环境使用可能出现递归调用死锁问题 解锁过程在线程执行完成临界区代码(或出现异常退出临界区时)需要调用unlock函数进行解锁以让出排他锁。通过对于示例代码的分析,解锁函数实际使用sync.release函数进行,该函数代码为:123456789public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 为了方便观察,将tryRelease函数也展示出来: 1234567protected boolean tryRelease(int releases) { assert releases == 1; // Otherwise unused if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true;} 解锁函数较为简单,当前的Mutex类假设使用锁的代码都是按照先加锁再解锁的顺序进行执行,所以在此情况下能保证同一时刻有且只有一个线程能够执行unlock函数,故解锁过程非常简单且不需要同步: 获取锁的状态,如果锁的状态为0,则表示目前没有加锁,抛出异常; 将当前锁记录的拥有锁的线程置为空; 将锁的状态置为0; 获取当前等待队列头结点,如头结点不为空且其等待状态不为0则唤醒头结点的后置节点 到此则释放锁的过程结束。Mutex类是AQS注释中的示例类,不能应用在实际生产环境中,如以上解锁过程就没有判断当前解锁的线程是不是已经得到锁的线程,这在生产环境中可能出现未知的问题 排它锁总结本节分析了使用AQS进行实现互斥锁的方法,并深入AQS代码分析其实现原理。重点分析其加锁及解锁过程,其他锁方法类似,本处不在赘述。针对其他重要方法、共享锁及Condition类的分析后续结合JCU包中关联类进行。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"},{"name":"并发","slug":"Java/并发","permalink":"http://bluerhino.github.io/categories/Java/并发/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"并发","slug":"并发","permalink":"http://bluerhino.github.io/tags/并发/"},{"name":"AbstractQueuedSynchronizer","slug":"AbstractQueuedSynchronizer","permalink":"http://bluerhino.github.io/tags/AbstractQueuedSynchronizer/"},{"name":"AQS","slug":"AQS","permalink":"http://bluerhino.github.io/tags/AQS/"}]},{"title":"微信小程序采坑记录","slug":"微信小程序采坑记录","date":"2018-03-04T03:40:01.000Z","updated":"2018-08-19T10:59:38.000Z","comments":true,"path":"2018/03/04/微信小程序采坑记录/","link":"","permalink":"http://bluerhino.github.io/2018/03/04/微信小程序采坑记录/","excerpt":"微信小程序采坑记录最近在学习微信小程序开发,本文用于记录开发中踩得坑","text":"微信小程序采坑记录最近在学习微信小程序开发,本文用于记录开发中踩得坑 scroll-view删除坑情景重现在列表中使用scroll-view,使用wx:for指令进行渲染,每一行为一个scroll-view,在右侧增加删除功能,日常显示如下:使用向左滑动后展示如下:这是点击删除按钮,js使用setData更新列表数据,预期为删除本行,下三行自动上移,但实际情况却是下三行自动上移,但其中第一行却是滑动后状态,具体如图: 问题解决(临时措施)在scroll-view中有一个scroll-left属性可以设置,目前解决方案为每次更新重新设置scroll-left为0。","categories":[{"name":"微信小程序","slug":"微信小程序","permalink":"http://bluerhino.github.io/categories/微信小程序/"}],"tags":[{"name":"微信小程序","slug":"微信小程序","permalink":"http://bluerhino.github.io/tags/微信小程序/"},{"name":"踩坑","slug":"踩坑","permalink":"http://bluerhino.github.io/tags/踩坑/"}]},{"title":"JAVA栈溢出","slug":"JAVA栈溢出","date":"2018-01-14T12:01:21.000Z","updated":"2018-08-19T10:59:33.000Z","comments":true,"path":"2018/01/14/JAVA栈溢出/","link":"","permalink":"http://bluerhino.github.io/2018/01/14/JAVA栈溢出/","excerpt":"Java栈溢出小记今天偶然有人问起如何在编写Java代码使其在运行时抛出栈溢出异常,看似简单的问题涉及到了Java虚拟机的知识,特记录于此文。","text":"Java栈溢出小记今天偶然有人问起如何在编写Java代码使其在运行时抛出栈溢出异常,看似简单的问题涉及到了Java虚拟机的知识,特记录于此文。 Java虚拟机结构简介根据《Java虚拟机规范》(The Java Virtual Machine Specification)对于Java虚拟机运行时数据区域(Run-Time Data Areas)的描述,虚拟机运行时的描述,其构成图如下所示:图中,PC寄存器、Java虚拟机栈及本地方法栈为各线程私有,方法区(包括运行时常量取)及堆为线程间共享的存储空间。针对问题提出的栈溢出,有两个区域与其相关,包括Java虚拟机栈及本地方法栈。查阅《Java虚拟机规范》,针对栈溢出有如下两段描述:对于Java虚拟机栈 The following exceptional conditions are associated with Java Virtual Machine stacks: If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError. If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError. 对于本地方法栈 The following exceptional conditions are associated with native method stacks: If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError. If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError. 由此可见对于Java虚拟机栈与本地方法栈都定义了相似的两种溢出: 线程请求栈上分配内存时,内存不足:此溢出一般出现在线程递归调用方法时。在线程调用方法时虚拟机创建栈帧保存方法调用信息,在方法调用完成后销毁栈帧释放存储,如果在方法调用过程中无法创建栈帧则会报出StackOverflowError异常。 动态扩展栈或线程创建时无法分配足够内存:此溢出一般出现在创建新的线程时。创建新的线程,需要在栈上为其分配存储,如果此时栈上存储不足以分配则会报出OutOfMemoryError异常。 代码实现以下代码在Mac版JDK8中实现及运行,由于HotSpot实现中没有分Java虚拟机栈及本地方法栈[1],故以下代码只针对Java虚拟机栈。Hotspot中设置栈容量的参数为-Xss,后续实验均设置-Xss1M,使用Junit4进行测试 分配栈帧失败(StackOverflowError)代码为:12345678910111213141516public class StackOverflow { public void callMyself(int depth) { System.out.println(depth); callMyself(++depth); }}public class StackOverflowTest { @Test public void callMyself() throws Exception { StackOverflow overflow = new StackOverflow(); overflow.callMyself(0); }} 最终会抛出java.lang.StackOverflowError,且最终能够达到的栈深度主要与栈内存最大大小与栈帧中局部变量占用的空间有关。使用如下代码最大深度会明显变小12345678public class StackOverflow { public void callMyself(int depth) { int a,b,c,d,e,f,g,h,i,j,k; System.out.println(depth+\"|\"); callMyself(++depth); }} 为线程分配栈上内存失败(OutOfMemoryError)代码为:1234567891011121314151617181920public class OutOfMemory { public void createThread() { while (true) { Thread t = new Thread(() -> { while (true) { System.out.println(System.currentTimeMillis()); } }); t.start(); } }}public class OutOfMemoryTest { @Test public void createThread() throws Exception { OutOfMemory outOfMemory = new OutOfMemory(); outOfMemory.createThread(); }} 最终会抛出OutOfMemoryError。 针对于OutOfMemoryError的补充在HotSpot虚拟机实现中,对于Java线程的创建是映射到操作系统线程中的,如果无法创建操作系统线程也会抛出异常,具体为:java.lang.OutOfMemoryError: unable to create new native thread。通过实验在MacOS中,一般小于2048(多次测试为2023),因为默认Mac每个进程最多分配的线程数为2048。可使用sysctl kern.num_taskthreads命令进行查询。如果需要突破限制可以参考官方解决方案。在centOS中实验发现max_user_processes及stack size参数都会限制各进程的线程数量。 参考文献[1] 周志明.深入理解Java虚拟机[M].北京:机械工业出版社,53","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"}],"tags":[{"name":"JAVA","slug":"JAVA","permalink":"http://bluerhino.github.io/tags/JAVA/"},{"name":"JVM","slug":"JVM","permalink":"http://bluerhino.github.io/tags/JVM/"},{"name":"JAVA虚拟机","slug":"JAVA虚拟机","permalink":"http://bluerhino.github.io/tags/JAVA虚拟机/"},{"name":"栈溢出","slug":"栈溢出","permalink":"http://bluerhino.github.io/tags/栈溢出/"}]},{"title":"感知机对偶形式学习","slug":"感知机对偶形式学习","date":"2017-12-21T14:01:56.000Z","updated":"2018-12-02T13:40:33.000Z","comments":true,"path":"2017/12/21/感知机对偶形式学习/","link":"","permalink":"http://bluerhino.github.io/2017/12/21/感知机对偶形式学习/","excerpt":"总结感知机及其对偶形式","text":"总结感知机及其对偶形式 问题背景实际应用中常出现二元分类问题,如引用台湾大学机器学习基石课程[1]的信用卡案例:如有用户申请信用卡,其个人信息如下: 特征 数据 年龄 23 性别 女 年收入 1,000,000 居住年限 1 工作年限 0.5 负债 200,000 银行具有原来的信用卡申请记录(包括申请用户信息及审批结果),如何根据原来的记录判断当前申请是否能够批准就是一个典型的二分问题。输入数据为个人信息,训练数据为历史申请及审批记录,输出数据为是否同意申请。假设银行根据申请用户的各项信息(特征)为用户打分,并且设定一个阈值,在用户得分超过该阈值则同意信用卡申请,否则拒绝申请。故假设用户具m个特征为 x=({x_1,x_2,\\cdots,x_m})得分阈值为$d$。每个特征对于最终的用户得分有不同的重要程度,所以为每一个特征增加入不同的权值$w=({w_1,w_2,\\cdots,w_m})$,用户最终得分为 w_1x_1+w_2x_2+ \\cdots +w_mx_m=\\sum_{i=1}^{m}w_ix_i最后只需要比较 $\\sum_{i=1}^{m}w_ix_i$与阈值$d$的大小就可以得出结果。观察上文公式,如何确定每一个信息的权值$w$及同意申请的阈值$d$就是解决问题的关键,由于具有历史数据,使用历史数据确定参数的想法就水到渠成。 问题抽象根据上文背景问题的提出,可进行抽象,银行n笔已知的数据可以抽象为训练数据集 T=\\{(x_1,y_1),(x_2,y_2),\\cdots,(x_n,y_n)\\}其中$x\\in\\chi ,\\chi\\subseteq R^m$,$y\\in\\mathcal{Y}=\\{+1,-1\\}$。设上文求和公式及阈值之差为函数 h(x)=\\sum_{i=1}^{m}w_ix_i-d为简化公式取$b=-d$,则公式变化为: h(x)=\\sum_{i=1}^{m}w_ix_i+b改写为向量形式为: h(x)=w\\cdot x+b当$h(x)>0$时,发放信用卡,否则拒绝发放信用卡使用取符号的函数$sign$,得到函数 f(x)=sign(w\\cdot x+b)此函数为感知机算法需要学习得到的最终函数。 感知机介绍感知机1957年由Rosenblatt提出,是支持向量机及神经网络基础算法。感知机主要通过训练数据集学习函数: f(x)=sign(w\\cdot x+b)中的模型参数$w$及$b$,其中$x\\in\\chi ,\\chi\\subseteq R^m,w\\in R^m,b\\in R$。感知机算法要求训练集是线性可分,当训练集线性可分时可以证明感知机算法可以通过有限次的搜索找到将训练集完全区分的超平面,否则感知机算法将不会收敛[2]。 未完待续 参考文献[1] 林轩田.机器学习基石[R].台湾:台湾大学.[2] 李航.统计学习方法[M].北京:清华大学出版社,2012:26-33.","categories":[{"name":"机器学习","slug":"机器学习","permalink":"http://bluerhino.github.io/categories/机器学习/"}],"tags":[{"name":"机器学习","slug":"机器学习","permalink":"http://bluerhino.github.io/tags/机器学习/"},{"name":"感知机","slug":"感知机","permalink":"http://bluerhino.github.io/tags/感知机/"},{"name":"统计学习方法","slug":"统计学习方法","permalink":"http://bluerhino.github.io/tags/统计学习方法/"},{"name":"李航","slug":"李航","permalink":"http://bluerhino.github.io/tags/李航/"}]},{"title":"java.util.concurrent.locks.Lock详解","slug":"java-util-concurrent-locks-Lock详解","date":"2017-10-23T11:39:22.000Z","updated":"2018-12-02T13:40:31.000Z","comments":true,"path":"2017/10/23/java-util-concurrent-locks-Lock详解/","link":"","permalink":"http://bluerhino.github.io/2017/10/23/java-util-concurrent-locks-Lock详解/","excerpt":"简介java.util.concurrent.locks.Lock接口(以下简称Lock)作者Doug Lea,对比使用synchronized关键字进行并发控制,Lock接口的实现类可以完成更灵活的加锁操作。本文以下会详细介绍Lock接口相关功能,并对比Lock接口及synchronized关键字在功能上(非性能对比)的异同。","text":"简介java.util.concurrent.locks.Lock接口(以下简称Lock)作者Doug Lea,对比使用synchronized关键字进行并发控制,Lock接口的实现类可以完成更灵活的加锁操作。本文以下会详细介绍Lock接口相关功能,并对比Lock接口及synchronized关键字在功能上(非性能对比)的异同。 Lock接口相关功能介绍Lock接口定义6个方法: lock:尝试获取锁,如当前线程无法获取锁,则当前线程进入无法调度的休眠状态,直到获取锁。 lockInterruptibly: 与 lock相似,主要区别在于调用该方法后,如无法获取锁,则当前线程进入无法调度的休眠状态,直到出现以下两种情况: . 获取锁; . 当前线程被其他线程中断。 tryLock: 尝试获取锁,但与lock不同,不会阻塞线程调用后立刻返回,能够获取锁返回true,否则返回false tryLock(long,TimeUnit): 尝试获取锁,可传入超时参数,在以下3种情况下返回: 在等待时间内获取锁; 在等待时间被其他线程中断,抛出中断异常; 等待时间结束,返回false unlock:释放锁 newCondition:创建一个Condition对象,此对象另开专题讨论。 Lock接口及synchronized关键字对比synchronized作为Java语言的并发控制关键字,在多线程编程中十分常用,主要有三种使用方式: 修饰对象方法; 修饰静态方法; 修饰同步方法快。 使用synchronized关键字可以完成对访问共享资源的代码块的显示加锁和隐式释放锁。在代码离开synchronized关键字修饰的区域后,synchronized自动释放锁定,其优势主要为: JVM内置锁结构,使用简单; 不需要手动释放,不会出现程序员忘记释放的认为失误; 可以受到JVM厂商的底层优化。其主要缺点为: 性能一般低于Lock接口的实现类(随着优化的进行,这个性能差距正在极大的缩小); 灵活性不足,对于较为复杂的加锁操作无法进行支持。 Lock接口将加锁及解锁操作控制权都交给程序员,这增加了锁的灵活性却也增加了出现逻辑问题的可能性。由此可见,在由于当前synchronized关键字不断得到JVM优化,在仅仅需求简单同步操作时可以优先考虑synchronized关键字,在有灵活加锁操作的需求时再考虑使用Lock接口的各实现类,并且需要谨慎使用,尤其不能忘记锁的释放。 总结本文主要介绍了Lock接口的各方法含义,并且简单对比了Lock及synchronized关键字的异同。后续计划继续对于synchronized关键字的实现的研究,及Lock接口各典型实现的研究。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"并发","slug":"并发","permalink":"http://bluerhino.github.io/tags/并发/"}]},{"title":"CopyOnWriteArrayList学习总结","slug":"CopyOnWriteArrayList学习总结","date":"2017-10-17T12:28:00.000Z","updated":"2017-12-23T03:09:30.000Z","comments":true,"path":"2017/10/17/CopyOnWriteArrayList学习总结/","link":"","permalink":"http://bluerhino.github.io/2017/10/17/CopyOnWriteArrayList学习总结/","excerpt":"简介CopyOnWriteArrayList类位于java.util.concurrent包,JDK1.5引入,作者为Doug Lea。CopyOnWriteArrayList是一个线程安全的List,使用“写时复制”的思想在进行所有写入操作(增加、删除等)是都会进行内部存储元素数组的复制。","text":"简介CopyOnWriteArrayList类位于java.util.concurrent包,JDK1.5引入,作者为Doug Lea。CopyOnWriteArrayList是一个线程安全的List,使用“写时复制”的思想在进行所有写入操作(增加、删除等)是都会进行内部存储元素数组的复制。 代码分析写时过程由于采用写时复制的策略,其主要作用过程在于对存储数据进行修改时。以add函数为例。一下为JDK8中实现源码:1234567891011121314public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); }} 进入函数首先获取ReentrantLock重入锁; 获取当前对象中实际存储数据的Objecte数组; 复制当前数组并且将数组长度扩展1; 将新数据插入当前数组的最后一位; 将指向原数组的变量置为指向新数组,释放锁,完成工作。 对比ArrayList增加元素代码:12345public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 通过查看代码可知,ArrayList在写入时是没有加锁的,所以多线程情况下使用并不安全,需要程序员手动进行并发控制。引用源码中说明为: Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be “wrapped” using the Collections.synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:List list = Collections.synchronizedList(new ArrayList(…)); 验证ArrayList线程不安全可使用以下代码12345678910111213141516171819202122232425262728293031323334public class Main { static class AddThread implements Runnable { private List<String> list; private CountDownLatch countDownLatch; public AddThread(List<String> list, CountDownLatch countDownLatch) { this.list = list; this.countDownLatch = countDownLatch; } @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); for(int i=0;i<500;i++){ list.add(\"1\"); } } } public static void main(String[] args) { CountDownLatch countDownLatch = new CountDownLatch(1); List<String> list = new ArrayList<>(); for (int i = 0; i < 200; i++) { new Thread(new AddThread(list, countDownLatch)).start(); } countDownLatch.countDown(); }} 运行过程中大概率会出现Java.lang.ArrayIndexOutOfBoundsException错误 优点使用CopyOnWriteArrayList可以不用进行手动同步控制,并且由于使用了读写分离的措施,在进行写入时可以同时进行数据的读取,并且数据读取过程中不需要加锁。 缺点根据其实现代码,CopyOnWriteArrayList的缺点同样明显。首先、其每次写入都需要进行数组的复制,所以对于写入操作非常消耗系统资源,尤其是内存资源,这种情况在处理存储较多数据的List时尤其明显。对于gc也造成了较大压力。其次,由于其在读取时不需要获取锁,造成如写入数据同时进行读取可能造成读取到的数据为旧数组中的数据,可能造成数据的不一致。 使用场景由此可见,CopyOnWriteArrayList比较适合用在大量读取小量写入的场景,并且要求系统对于数据的不一致宽容度较高。针对写时复制的策略,在必须要进行写入时尽量将多次写入合并为一次写入,减少数组复制次数。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/tags/Java/"},{"name":"同步","slug":"同步","permalink":"http://bluerhino.github.io/tags/同步/"}]},{"title":"Spring @Autowired+@Qualifier与@Resource的区别","slug":"Spring @Autowired+@Qualifier与@Resource的区别","date":"2016-06-02T13:37:17.000Z","updated":"2017-12-23T03:04:58.000Z","comments":true,"path":"2016/06/02/Spring @Autowired+@Qualifier与@Resource的区别/","link":"","permalink":"http://bluerhino.github.io/2016/06/02/Spring @Autowired+@Qualifier与@Resource的区别/","excerpt":"最近由于希望使用Spring在XML文件中定义List的bean,并使用@Autowired进行注入到对象中使用,遇到到一些坑,记录一下作为备忘。","text":"最近由于希望使用Spring在XML文件中定义List的bean,并使用@Autowired进行注入到对象中使用,遇到到一些坑,记录一下作为备忘。 @Autowired@Autowired是Spring定义的注解,是根据类型进行自动装配的。如果当spring上下文中存在不止一个存在一个需要装配类型的bean时,就会抛出BeanCreationException异常;这时我们可以使用@Qualifier配合@Autowired来解决问题。 @Resource@Resource是JSR-250规定的注解,主要有两种类型的属性type及name,所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略 。@Resource装配顺序: 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配; 使用区别使用Spring注入List的时候在XML中定义: 1234<util:list id=\"myList\"> <value>10.1.200.104</value> <value>10.1.200.205</value></util:list> 之后使用注解注入,代码1为: 12345@Componentpublic class App { @Autowired List list;} 注入失败。修改,代码2为: 12345@Componentpublic class App { @Autowired ArrayList<String> strings;} 注入仍然失败。修改,代码3为: 123456@Componentpublic class App { @Autowired @Qualifier('myList') ArrayList<String> strings;} 仍然失败。主要原因在于,使用Autowired注入,Spring默认使用按类型方式注入,而对于List集合类型Spring会读取其中的泛型类型进行注入,上面代码2的含义为注入当前bean中类型为String的对象,代码3的含义为注入当前bean中类型为String且qualifier是myList的对象。这两种含义都不能完成正确的注入。 正确使用使用@Resource注入,引用stackoverflow上的解答使用正确方式为: 1234567@Componentpublic class App { @Resource(name = \"myList\") ArrayList<Object> list;} 或者 1234567@Componentpublic class App { @Resource(name = \"myList\") List<Object> list;} 因为Resource设置了name属性,Spring直接寻找id为myList的对象进行注入,可以注入成功。","categories":[{"name":"Java","slug":"Java","permalink":"http://bluerhino.github.io/categories/Java/"}],"tags":[{"name":"Spring","slug":"Spring","permalink":"http://bluerhino.github.io/tags/Spring/"},{"name":"@Autowired","slug":"Autowired","permalink":"http://bluerhino.github.io/tags/Autowired/"},{"name":"@Qualifier","slug":"Qualifier","permalink":"http://bluerhino.github.io/tags/Qualifier/"},{"name":"@Resource List","slug":"Resource-List","permalink":"http://bluerhino.github.io/tags/Resource-List/"}]}]}