Java并发计数器的深入理解

前言

一提到线程安全的并发计数器,AtomicLong 必然是第一个被联想到的工具。Atomic* 一系列的原子类以及它们背后的 CAS 无锁算法,常常是高性能,高并发的代名词。本文将会阐释,在并发场景下,使用 AtomicLong 来充当并发计数器将会是一个糟糕的设计,实际上存在不少 AtomicLong 之外的计数器方案。近期我研究了一些 Jdk1.8 以及 JCTools 的优化方案,并将它们的对比与实现细节整理于此。

阅读本文前

本文相关的基准测试代码均可在博主的 github 中找到,测试方式全部采用 JMH,这篇文章可以帮助你入门 JMH。

AtomicLong 的前世今生

在 Java 中,Atomic* 是高效的,这得益于 sun.misc.Unsafe 提供的一系列底层 API,使得 Java 这样的高级语言能够直接和硬件层面的 CPU 指令打交道。并且在 Jdk1.7 中,这样的底层指令可以配合 CAS 操作,达到 Lock-Free。

在 Jdk1.7 中,AtomicLong 的关键代码如下:

public final long getAndIncrement() {
 while (true) {
 long current = get();
 long next = current + 1;
 if (compareAndSet(current, next))
  return current;
 }
}

public final boolean compareAndSet(long expect, long update) {
 return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
  1. get() 方法 volatile 读当前 long 值
  2. 自增
  3. 自旋判断新值与当前值
  4. 自旋成功,返回;否则返回 1

我们特别留意到 Jdk1.7 中 unsafe 使用的方法是 compareAndSwapLong,它与 x86 CPU 上的 LOCK CMPXCHG 指令对应,并且在应用层使用 while(true) 完成自旋,这个细节在 Jdk1.8 中发生了变化。

在 Jdk1.8 中,AtomicLong 的关键代码如下:

public final long getAndIncrement() {
 return unsafe.getAndAddLong(this, valueOffset, 1L);
}

Jdk1.7 的 CAS 操作已经不复存在了,转而使用了 getAndAddLong 方法,它与 x86 CPU 上的 LOCK XADD 指令对应,以原子方式返回当前值并递增(fetch and add)。

当问及 Atomic* 高效的原因,回答 CAS 是不够全面且不够严谨的,Jdk1.7 的 unsafe.compareAndSwapLong 以及 Jdk1.8 的 unsafe.getAndAddLong 才是关键,且 Jdk1.8 中不存在 CAS。

Jdk1.8 AtomicLong 相比 Jdk1.7 AtomicLong 的表现是要优秀的,这点我们将在后续的测评中见证。

AtomicLong 真的高效吗?

无论在 Jdk1.7 还是 Jdk1.8 中,Atomic* 的开销都是很大的,主要体现在:

  1. 高并发下,CAS 操作可能会频繁失败,真正更新成功的线程占少数。(Jdk1.7 独有的问题)
  2. 我之前的文章中介绍过“伪共享” (false sharing) 问题,但在 CAS 中,问题则表现的更为直接,这是“真共享”,与”伪共享“存在相同的问题:缓存行失效,缓存一致性开销变大。
  3. 底层指令的开销不见得很低,无论是 LOCK XADD 还是 LOCK CMPXCHG,想深究的朋友可以参考 instruction_tables ,(这一点可能有点钻牛角尖,但不失为一个角度去分析高并发下可行的优化)
  4. Atomic* 所做的,比我们的诉求可能更大,有时候我们只需要计数器具备线程安全地递增这样的特性,但 Atomic* 的相关操作每一次都伴随着值的返回。他是个带返回值的方法,而不是 void 方法,而多做了活大概率意味着额外的开销。

抛开上述导致 AtomicLong 慢的原因,AtomicLong 仍然具备优势:

  1. 上述的第 4 点换一个角度也是 AtomicLong 的有点,相比下面要介绍的其他计数器方案,AtomicLong 能够保证每次操作都精确的返回真实的递增值。你可以借助 AtomicLong 来做并发场景下的递增序列号方案,注意,本文主要讨论的是计数器方案,而不是序列号方案。
  2. 实现简单,回到那句话:“简单的架构通常性能不高,高性能的架构通常复杂度很高”,AtomicLong 属于性能相对较高,但实现极其简单的那种方案,因为大部分的复杂性,由 JMM 和 JNI 方法屏蔽了。相比下面要介绍的其他计数器实现,AtomicLong 真的太“简易”了。

看一组 AtomicLong 在不同并发量下的性能表现:

横向对比,写的性能相比读的性能要差很多,在 20 个线程下写性能比读性能差距了 4~5 倍。

纵向对比,主要关注并发写,线程竞争激烈的情况下,单次自增耗时从 22 ns 增长为了 488 ns,有明显的性能下降。

实际场景中,我们需要统计系统的 qps、接口调用次数,都需要使用到计数的功能,写才是关键,并不是每时每刻都需要关注自增后的返回值,而 AtomicLong 恰恰在核心的写性能上有所欠缺。由此引出其他计数器方案。

认识 LongAdder

Doug Lea 在 JDK1.8 中找到了一个上述问题的解决方案,他实现了一个 LongAdder 类。

@since 1.8
@author Doug Lea
public class LongAdder extends Striped64 implements Serializable {}

LongAdder 的 API 如下

LongAdder

你应当发现,LongAdder 和 AtomicLong 明显的区别在于,increment 是一个 void 方法。直接来看看 LongAdder 的性能表现如何。(LA = LongAdder, AL = AtomicLong, 单位 ns/op):

我们从中可以发现一些有意思的现象,网上不少很多文章没有从读写上对比二者,直接宣称 LongAdder 性能优于 AtomicLong,其实不太严谨。在单线程下,并发问题没有暴露,两者没有体现出差距;随着并发量加大,LongAdder 的 increment 操作更加优秀,而 AtomicLong 的 get 操作则更加优秀。鉴于在计数器场景下的特点—写多读少,所以写性能更高的 LongAdder 更加适合。

LongAdder 写速度快的背后

网上分析 LongAdder 源码的文章并不少,我不打算详细分析源码,而是挑选了一些必要的细节以及多数文章没有提及但我认为值得分析的内容。

1、Cell 设计减少并发修改时的冲突


LongAdder

在 LongAdder 的父类 Striped64 中存在一个 volatile Cell[] cells; 数组,其长度是 2 的幂次方,每个 Cell 都填充了一个 @Contended 的 Long 字段,为了避免伪共享问题。

@sun.misc.Contended static final class Cell {
 volatile long value;
 Cell(long x) { value = x; }
 // ... ignore
}

LongAdder 通过一系列算法,将计数结果分散在了多个 Cell 中,Cell 会随着并发量升高时发生扩容,最坏情况下 Cell == CPU core 的数量。Cell 也是 LongAdder 高效的关键,它将计数的总值分散在了各个 Cell 中,例如 5 = 3 + 2,下一刻,某个线程完成了 3 + (2 + 1) = 6 的操作,而不是在 5 的基础上完成直接相加操作。通过 LongAdder 的 sum() 方法可以直观的感受到这一点(LongAdder 不存在 get 方法)

public long sum() {
 Cell[] as = cells; Cell a;
 long sum = base;
 if (as != null) {
 for (int i = 0; i < as.length; ++i) {
  if ((a = as[i]) != null)
  sum += a.value;
 }
 }
 return sum;
}

这种惰性求值的思想,在 ConcurrentHashMap 中的 size() 中也存在,毕竟他们的作者都是 Doug Lea。

2、并发场景下高效获取随机数

LongAdder 内部算法需要获取随机数,而 Random 类在并发场景下也是可以优化的。

ThreadLocalRandom random = ThreadLocalRandom.current();
random.nextInt(5);

使用 ThreadLocalRandom 替代 Random,同样出现在了 LongAdder 的代码中。

3、longAccumulate

longAccumulate 方法是 LongAdder 的核心方法,内部存在大量的分支判断。首先和 Jdk1.7 的 AtomicLong 一样,它使用的是 UNSAFE.compareAndSwapLong 来完成自旋,不同之处在于,其在初次 cas 方式失败的情况下(说明多个线程同时想更新这个值),尝试将这个值分隔成多个 Cell,让这些竞争的线程只负责更新自己所属的 Cell,这样将竞争压力分散开。

LongAdder 的前世今生

其实在 Jdk1.7 时代,LongAdder 还未诞生时,就有一些人想着自己去实现一个高性能的计数器了,比如一款 Java 性能监控框架 dropwizard/metrics 就做了这样事,在早期版本中,其优化手段并没有 Jdk1.8 的 LongAdder 丰富,而在 metrics 的最新版本中,其已经使用 Jdk1.8 的 LongAdder 替换掉了自己的轮子。在最后的测评中,我们将 metrics 版本的 LongAdder 也作为一个参考对象。

JCTools 中的 ConcurrentAutoTable

并非只有 LongAdder 考虑到了并发场景下计数器的优化,大名鼎鼎的并发容器框架 JCTool 中也提供了和今天主题相关的实现,虽然其名称和 Counter 看似没有关系,但通过其 Java 文档和 API ,可以发现其设计意图考虑到了计数器的场景。

An auto-resizing table of longs, supporting low-contention CAS operations.Updates are done with CAS's to no particular table element.The intent is to support highly scalable counters , r/w locks, and other structures where the updates are associative, loss-free (no-brainer), and otherwise happen at such a high volume that the cache contention for CAS'ing a single word is unacceptable.


ConcurrentAutoTable

在最后的测评中,我们将 JCTools 的 ConcurrentAutoTable 也作为一个参考对象。

最终测评

Jdk1.7 的 AtomicLong,Jdk1.8 的 AtomicLong,Jdk 1.8 的 LongAdder,Metrics 的 LongAdder,JCTools 的 ConcurrentAutoTable,我对这五种类型的计数器使用 JMH 进行基准测试。

public interface Counter {
 void inc();
 long get();
}

将 5 个类都适配成 Counter 接口的实现类,采用 @State(Scope.Group),@Group 将各组测试用例进行隔离,尽可能地排除了互相之间的干扰,由于计数器场景的特性,我安排了 20 个线程进行并发写,1 个线程与之前的写线程共存,进行并发读。Mode=avgt 代表测试的是方法的耗时,越低代表性能越高。

Benchmark                      (counterType)  Mode  Cnt     Score       Error  Units
CounterBenchmark.rw                  Atomic7  avgt    3  1049.906 ±  2146.838  ns/op
CounterBenchmark.rw:get              Atomic7  avgt    3   143.352 ±   125.388  ns/op
CounterBenchmark.rw:inc              Atomic7  avgt    3  1095.234 ±  2247.913  ns/op
CounterBenchmark.rw                  Atomic8  avgt    3   441.837 ±   364.270  ns/op
CounterBenchmark.rw:get              Atomic8  avgt    3   149.817 ±    66.134  ns/op
CounterBenchmark.rw:inc              Atomic8  avgt    3   456.438 ±   384.646  ns/op
CounterBenchmark.rw      ConcurrentAutoTable  avgt    3   144.490 ±   577.390  ns/op
CounterBenchmark.rw:get  ConcurrentAutoTable  avgt    3  1243.494 ± 14313.764  ns/op
CounterBenchmark.rw:inc  ConcurrentAutoTable  avgt    3    89.540 ±   166.375  ns/op
CounterBenchmark.rw         LongAdderMetrics  avgt    3   105.736 ±   114.330  ns/op
CounterBenchmark.rw:get     LongAdderMetrics  avgt    3   313.087 ±   307.381  ns/op
CounterBenchmark.rw:inc     LongAdderMetrics  avgt    3    95.369 ±   132.379  ns/op
CounterBenchmark.rw               LongAdder8  avgt    3    98.338 ±    80.112  ns/op
CounterBenchmark.rw:get           LongAdder8  avgt    3   274.169 ±   113.247  ns/op
CounterBenchmark.rw:inc           LongAdder8  avgt    3    89.547 ±    78.720  ns/op

如果我们只关注 inc 即写性能,可以发现 jdk1.8 的 LongAdder 表现的最为优秀,ConcurrentAutoTable 以及两个版本的 LongAdder 在一个数量级之上;1.8 的 AtomicLong 相比 1.7 的 AtomicLong 优秀很多,可以得出这样的结论,1.7 的 CAS+LOCK CMPXCHG 方案的确不如 1.8 的 LOCK XADD 来的优秀,但如果与特地优化过的其他计数器方案来进行比较,便相形见绌了。

如果关注 get 性能,虽然这意义不大,但可以见得,AtomicLong 的 get 性能在高并发下表现依旧优秀,而 LongAdder 组合求值的特性,导致其性能必然存在一定下降,位列第二梯队,而 ConcurrentAutoTable 的并发读性能最差。

关注整体性能,CounterBenchmark.rw 是对一组场景的整合打分,可以发现,在我们模拟的高并发计数器场景下,1.8 的 LongAdder 获得整体最低的延迟 98 ns,相比性能最差的 Jdk1.7 AtomicLong 实现,高了整整 10 倍有余,并且,随着并发度提升,这个数值还会增大。

AtomicLong 可以被废弃吗?

既然 LongAdder 的性能高出 AtomicLong 这么多,我们还有理由使用 AtomicLong 吗?

本文重点讨论的角度还是比较局限的:单机场景下并发计数器的高效实现。AtomicLong 依然在很多场景下有其存在的价值,例如一个内存中的序列号生成器,AtomicLong 可以满足每次递增之后都精准的返回其递增值,而 LongAdder 并不具备这样的特性。LongAdder 为了性能而丧失了一部分功能,这体现了计算机的哲学,无处不在的 trade off。

高性能计数器总结

AtomicLong :并发场景下读性能优秀,写性能急剧下降,不适合作为高性能的计数器方案。内存需求量少。

LongAdder :并发场景下写性能优秀,读性能由于组合求值的原因,不如直接读值的方案,但由于计数器场景写多读少的缘故,整体性能在几个方案中最优,是高性能计数器的首选方案。由于 Cells 数组以及缓存行填充的缘故,占用内存较大。

ConcurrentAutoTable :拥有和 LongAdder 相近的写入性能,读性能则更加不如 LongAdder。它的使用需要引入 JCTools 依赖,相比 Jdk 自带的 LongAdder 并没有优势。但额外说明一点,ConcurrentAutoTable 的使用并非局限于计数器场景,其仍然存在很大的价值。

在前面提到的性能监控框架 Metrics,以及著名的熔断框架 Hystrix 中,都存在 LongAdder 的使用场景,有兴趣的朋友快去实践一下 LongAdder 吧。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解java并发编程(2) --Synchronized与Volatile区别

    1 Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同步方法,锁是当前类的class对象. 对于同步方法块,锁是synchronized括号里配置的对象. 当一个代码,方法或者类被synchronized修饰以后.当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁.那么这样做有什么好处呢? 它主要确保多个线程在同一

  • java并发之AtomicInteger源码分析

    问题 (1)什么是原子操作? (2)原子操作和数据库的ACID有啥关系? (3)AtomicInteger是怎么实现原子操作的? (4)AtomicInteger是有什么缺点? 简介 AtomicInteger是java并发包下面提供的原子类,主要操作的是int类型的整型,通过调用底层Unsafe的CAS等方法实现原子操作. 还记得Unsafe吗?点击链接直达[java Unsafe详细解析] 原子操作 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何

  • 浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一. CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西.大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成.虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GH

  • 通俗易懂学习java并发工具类-Semaphore,Exchanger

    1. 控制资源并发访问--Semaphore Semaphore可以理解为信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源. Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待.当执行完业务功能后,需要通过release()方法将许可证归还,以便其他线程能够获得许可证继续执行. Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接.假如有多个线程读取

  • java并发编程实例分析

    java并发编程是java程序设计语言的一块重点,在大部分的业务场景中都需要并发编程. 比如:并发的去处理http请求,这样就可以使得一台机器同时处理多个请求,大大提高业务的响应效率,从而使用用户体验更加流畅. java如何并发编程,要注意以下几个方面: 1.java语言中的多线程操作:创建和启动线程的几种方式. 2.共享变量的同步问题,要保证线程安全,辨别哪些变量是线程安全的.那些变量是线程不安全的,对于不安全的变量我们要想办法让其同步,一般也就是加锁. 3.线程锁:包括方法锁和synchro

  • 深入了解Java语言中的并发性选项有何不同

    前言 Java™ 工程师在努力让并发性容易为开发人员所用.尽管做了不少的改进,但并发性仍然是 Java 平台的一个复杂.容易出错的部分.一部分复杂之处在于理解语言本身中的并发性的低级抽象,这些抽象在您的代码中填满了同步的代码块.另一个复杂之处来自一些新库,比如 fork/join,这些库在某些场景中非常有用,但在其他场景中收效甚微.了解容易混乱的大量低级选项需要专业经验和时间. 脱离 Java 语言的优势之一是,能够改善和简化并发性等区域.每种 Java 下一代语言都为此问题提供了独特的答案,利

  • 浅谈java并发之计数器CountDownLatch

    CountDownLatch简介 CountDownLatch顾名思义,count + down + latch = 计数 + 减 + 门闩(这么拆分也是便于记忆=_=) 可以理解这个东西就是个计数器,只能减不能加,同时它还有个门闩的作用,当计数器不为0时,门闩是锁着的:当计数器减到0时,门闩就打开了. 如果你感到懵比的话,可以类比考生考试交卷,考生交一份试卷,计数器就减一.直到考生都交了试卷(计数器为0),监考老师(一个或多个)才能离开考场.至于考生是否做完试卷,监考老师并不关注.只要都交了试

  • 浅谈Java并发 J.U.C之AQS:CLH同步队列

    CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态. 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread).状态(waitStatus).前驱节点(prev).后继节点(next),其定义如下: static final class Node

  • Java并发计数器的深入理解

    前言 一提到线程安全的并发计数器,AtomicLong 必然是第一个被联想到的工具.Atomic* 一系列的原子类以及它们背后的 CAS 无锁算法,常常是高性能,高并发的代名词.本文将会阐释,在并发场景下,使用 AtomicLong 来充当并发计数器将会是一个糟糕的设计,实际上存在不少 AtomicLong 之外的计数器方案.近期我研究了一些 Jdk1.8 以及 JCTools 的优化方案,并将它们的对比与实现细节整理于此. 阅读本文前 本文相关的基准测试代码均可在博主的 github 中找到,

  • [java并发编程之深入理解]Synchronized的使用

    1.为什么要使用synchronized 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据.关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile. 2.实现原理 synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性 3.synchronized的三种应

  • java并发编程之深入理解Synchronized的使用

    1.为什么要使用synchronized 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据.关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile. 2.实现原理 synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性 3.synchronized的三种应

  • Java并发编程深入理解之Synchronized的使用及底层原理详解 上

    目录 一.线程安全问题 1.临界资源 2.线程安全问题 3.如何解决线程安全问题 二.synchronized使用介绍 三.synchronized实现原理 1.synchronized底层指令:monitorenter和monitorexit 2.Object Monitor(监视器锁)机制 一.线程安全问题 1.临界资源 多线程编程中,有可能会出现多个线程同时访问同一个共享.可变资源的情况,这个资源我们称之其为临界资源:这种资源可能是:对象.变量.文件等. 共享:资源可以由多个线程同时访问

  • 深入理解Java并发编程之LinkedBlockingQueue队列

    前面一篇文章我们介绍了使用CAS算法实现的非阻塞队列ConcurrentLinedQueue, 下面我们来介绍使用独占锁实现的阻塞队列LinkedBlockingQueue. LinkedBlockingQueue也是使用单向链表实现的,其也有两个Node,分别用来存放首.尾节点,并且还有一个初始值为0的原子变量count,用来记录队列元素个数.另外还有两个ReentrantLock的实例,分别用来控制元素入队和出队的原子性,其中takeLock用来控制同时只有一个线程可以从队列头获取元素,其他

  • Java利用Redis实现高并发计数器的示例代码

    业务需求中经常有需要用到计数器的场景:譬如一个手机号一天限制发送5条短信.一个接口一分钟限制多少请求.一个接口一天限制调用多少次等等.使用Redis的Incr自增命令可以轻松实现以上需求.以一个接口一天限制调用次数为例: /** * 是否拒绝服务 * @return */ private boolean denialOfService(String userId){ long count=JedisUtil.setIncr(DateUtil.getDate()+"&"+user

  • Java并发编程之Fork/Join框架的理解

    一.Fork/Join框架的理解 ForkJoinTask类属于java.util.concurrent 包下: ForkJoinTask类下有2个子类,分别为RecursiveTask和RecursiveAction类:(lz示例中使用RecursiveTask类进行重写compute()方法进行实现数值的累加计算) ForkJoinTask类 将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出. 二.Fork/Join框架使用示例 示例场景:对数值进

  • Java并发编程深入理解之Synchronized的使用及底层原理详解 下

    目录 一.synchronized锁优化 1.自旋锁与自适应自旋 2.锁消除 逃逸分析: 3.锁粗化 二.对象头内存布局 三.synchronized锁的膨胀升级过程 1.偏向锁 2.轻量级锁 3.重量级锁 4.各种锁的优缺点 接着上文<Java并发编程深入理解之Synchronized的使用及底层原理详解 上>继续介绍synchronized 一.synchronized锁优化 高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花费了大量的资源

  • 深入理解Java并发编程之ThreadLocal

    目录 ThreadLocal简介 ThreadLocal源码解析 实现原理 ThreadLocalMap源码分析 InheritableThreadLocal 参考资料 ThreadLocal简介 变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题. ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境

  • Java并发编程之Semaphore(信号量)详解及实例

    Java并发编程之Semaphore(信号量)详解及实例 概述 通常情况下,可能有多个线程同时访问数目很少的资源,如客户端建立了若干个线程同时访问同一数据库,这势必会造成服务端资源被耗尽的地步,那么怎样能够有效的来控制不可预知的接入量呢?及在同一时刻只能获得指定数目的数据库连接,在JDK1.5 java.util.concurrent 包中引入了Semaphore(信号量),信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器,通常用于限制可以访问某些资源(物

随机推荐