编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。多核时代摩尔定律告诉我们：当价格不变时，集成电路上可容纳的晶体管数目，约每隔 18 个月便会增加一倍，性能也将提升一倍。换言之，每一美元所能买到的电脑性能，将每隔 18 个月翻两倍以上。然而最近摩尔定律似乎遇到了麻烦，目前微处理器的集成度似乎到了极限，在目前的制造工艺和体系架构下很难再提高单个处理器的速度了，否则它就被烧坏了。所以现在的芯片制造商改变了策略，转而在一个电路板上集成更多的处理器，也就是我们现在常见的多核处理器。这就给软件行业带来麻烦（也可以说带来机会，比如说就业机会，呵呵）。原来的情况是：我买一台频率比原来快一倍的处理器，那么我的程序就比原来快一倍，软件工程师什么也不用干。现在不一样了，我买一台双核的处理器，我的程序和原来一样慢，当然这台机器同时处理的任务可以变多了，但是对于单个任务来说并没有帮助。在几年前，并发 (Concurrency) 和并行 (Parallelism) 程序设计还是在少量的地方使用，现在在个人的 PC 机上已经是很常见了。（Concurrency 和 Parallelism 的区别参考 这个帖子）造个诸葛亮的价钱远远高于造三个臭皮匠！多核是在一台机器上的并发，但是单机也是会到极限，所以分布式的计算也是类似的思路，用大量普通的机器协作完成一项任务。但是要想编写一个正确并且高效的能利用多核的多线程程序不是件容易的事，更别说分布式的情况（网络问题，机器故障，负载均衡，...）。现在的编译器没有办法把单线程的程序自动编译成一个多线程的版本（如果到了那一天，估计所有的程序员就失业了）。所以只能提供一些语言上的支持（比如 Scala/Erlang) 或者 MapReduce 这样的框架。Java 虽然没有提供 Scala 那样的基于消息的模型，但是也提供了丰富的 Concurrent 特性，并且屏蔽了平台的相关性（这不是件容易的事，比如多个处理器有自己的缓存，他们写的东西不会立刻被其它处理器看到），下面我们看看 Java 的内存模型 (JMM)。Java 内存模型 (JMM)并行程序有很多模型，比如共享内存模型，消息传递模型等等。这些模型或多或少的利用了平台相关的特性（在并行程序设计里很难回避平台的特性以便高效的通信），Java 抽象出了自己的内存模型，使得开发人员看不到平台的差异（这不是件容易的事），不过即使这样，和传统程序不同，我们还是不能完全不了解一些体系架构的细节问题，至少我们得了解一些。在共享内存的多处理器体系架构里（我们现在用的服务器甚至笔记本都是），每个处理器都有自己的局部缓存并定期的使之与内存同步。不同的处理器架构保证了不同程度的缓存一致性 (Cache Coherence)，所以操作系统，编译器和运行时环境必须一起努力来弥补平台的差异性。让每个处理器都知道其它处理器的状态的代价是非常昂贵的，所以大多数架构都不会保证一致性，这通常不会有什么问题：进程/线程直接并不共享信息，编译器可以调整代码执行顺序以便提高效率，我们都很开心。当然也有需要在线程之间进行同步的时候，比如某个线程要读取到另一个线程写入的信息，这个时候缓存里的数据就得同步到内存里才行。所以这些体系架构都提供了一些指令来完成数据的同步（当然这些指令是非常费时的，能不做就尽量不做）。这些指令一般叫做 Memory Barriers or Fences。当然只是很底层的一些东西，所幸 Java 提供了一些高层的抽象，让我们的生活变得容易一些。Sequential Consistency：我们假设一个线程执行（可能在多个处理器上切换），每个变量读取到的值都是最新的修改（也就是 Cache 里的立马生效），这样得到的结果是我们预期的。但是让我们意外的事情是：如果我们不做任何事情，那么很可能会出现错误，比如下面的这个例子：public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }我们在主线程里先让 number=42 (初始值是 0)，然后让 ready=true，而另一个线程不断检查是否 ready，如果 ready，那么读出 number。很自然的我们期望子线程打印出 42，但是很可能结果会令我们失望。编译器可能会调换 number=42 和 ready=true 的顺序（思考一下为什么它要这么干？为什么在单线程的情况下没有问题？），另外子线程可能永远在 while 里死循环。为什么？子线程会永远看不到 ready 的变化？这也许让很多人吃惊，事实确实如此，JSR 并不保证这一点（虽然大多数时候子线程能够退出），参考 这个帖子 和 JMM 的文章。volatile 和 synchronized (内置锁)volatile 关键字告诉编译器，一个线程对某个变量的修改立即对所有其它线程看见，加上这个能保证上面的程序不会死循环。但是不能保证读到 42，也就是保证 number=42 和 ready=true 的执行顺序，要保证这点就要用到 synchronized。synchronized 能够保证执行的顺序，除此之外，它也能保证可见性。public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { boolean r = false; while (true){ synchronized(NoVisibility.class){ r = ready; } if(r) break; else Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); synchronized(NoVisibility.class){ number = 42; ready = true; } } }synchronized(NoVisibility.class){ number = 42; ready = true; }这段代码保证了两个语句的执行顺序。synchronized(NoVisibility.class){ r = ready; }这保证子线程能看到 ready 的变化。注意他们必须 synchronized 同一个对象，如果是下面的代码，则不能有任何保障。为什么？试想任何 synchronized 里的变量必须立即对所有的可见，那么代价太大，比如我有这样的需求：我只要求两个语句顺序执行，它是否对别人可见我并不关心。synchronized(AnotherObject){ r = ready; }每个对象都有个 Monitor，所以 synchronized 也经常叫 Monitor Lock，另外这个锁是语言内置的，所以也叫 Intrinsic Lock。这两个关键字是 Java 1.5 之前就有了，在 Java 1.5 之后新引进了 java.util.concurrent 包，这里有我们需要关注的很多东西，这里我们只关心 Lock 相关的接口和类。不过 synchronized 来解决互斥不是很完美吗？我为什么要花力气搞这些新鲜东西呢？下面我们来看看 synchronized 解决不了（或者很难解决）的问题。银行转账的例子// Warning: deadlock-prone! public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAccount.debit(amount); toAccount.credit(amount); } } } }比如我要在两个用户之间转账，为了防止意外，我必须同时锁定两个账户。但是这可能造成死锁。比如：A: transferMoney(myAccount, yourAccount, 10); B: transferMoney(yourAccount, myAccount, 20);当线程 A 锁住 myAccount 时，B 锁住了 toAccount，这个时候 A 尝试锁住 toAccount，但是已经被 B 锁住，所以 A 不能继续运行，同理 B 也不能运行，造成死锁。怎么解决呢？你也许会想，我先锁住一个账户，然后"尝试"锁定另一个账户，如果“失败”，那么我释放所有的锁，“休息”一下再继续尝试，当然两个线程节拍一致的话，可能造成“活锁”。可惜 synchronized 不能提供这样的语义，它一旦尝试加锁，只能拿到锁，你不能控制它，比如你可能有这样的需求：尝试拿锁 30s，如果拿不到就算了，synchronized 是没办法满足这样的需求的。另外你使用“鸵鸟”策略来解决死锁：什么也不干，如果死锁了，kill 他们，重启他们。这种策略看起来很疯狂，不过如果死锁的概率很多，而避免死锁的算法很复杂，那这也是可以一试的策略（那一堆死锁发生的充分必要条件太麻烦了！！！）。下面我们仔细的来看看 Java 1.5 后提供的 Lock 接口及其相关类。Lock 接口Lock 的基本用法如下，为了防止异常退出时没有释放锁，一般都在拿到锁后立马 try，try 住所有临界区的代码，然后 finally 释放锁。主要和 synchronized 的区别，synchronized 里我们不用操心这些，如果 synchronized 保护的代码抛出异常，那么 JVM 会释放掉 Monitor Lock。Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }Lock.lock() 在锁定成功后释放锁之前，它所保护的代码段必须与使用 synchronized 保护的代码段有相同的语义（可见性，顺序性）。所以从这个角度来说，Lock 完全可以代替 synchronized，那么是否应该抛弃掉 synchronized 呢？答案是否定的。是否应该抛弃 synchronized？在 Java 5 引进 Lock 后，实现了 Lock 接口的类就是 ReentrantLock（待会再解释 Reentrant），因为 Java 5 之前 synchronized 的实现很烂，同样是为了实现互斥，ReentrantLock 会比 synchronized 速度上快很多，不过到了 JDK 6 之后就不是这样了，下面是一个测试结果：from book "Java Concurrency in Practice"横轴是线程数，纵轴是 ReentrantLock 的吞吐量/IntrinsicLock 的吞吐量。可以看出，JDK 5 中，ReentrantLock 快很多，但是到了 JDK 6，他们就没什么大的差别了。synchronized 的优点：锁的释放是语言内置的，不会出现忘记释放锁的情况，另外由于是语言内置的支持，调试时能很快知道锁被哪个线程持有，它加锁的次数。而 Lock 只是 util.concurrent 一个普通的类，所以调试器并不知道这个锁的任何信息，它只是一个普通的对象（当然你可以仔细观察每个线程的 stack frame 来看它在等待锁）。所以建议：如果只是为了实现互斥，那么使用 synchronized（扔掉 JDK 5 吧，现在都 Java 7 了），如果想用 Lock 附加的功能，那么才使用 Lock。下面回来继续看 Lock 接口。Lock 接口方法详解public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }void lock()尝试获取锁。如果锁被别人拿着，那么当前线程不在执行，也不能被调度，直到拿到锁为止。void lockInterruptibly() throws InterruptedException尝试获取锁，除非被 interrupted。如果锁可以获取，那么立刻返回。如果无法获取锁，那么线程停止执行，并且不能被再调度，直到：当前线程获得锁如果锁的实现支持 interruption，并且有其它线程 interrupt 当前线程。仔细阅读 javadoc 的第二个情况：Lock 接口并不要求 Lock 的实现支持 interruption，不过 Sun JDK 的实现都是支持的。这个函数在下面两个情况下抛出 InterruptedException：如果锁的实现支持 interruption，并且有其它线程 interrupt 当前线程。线程调用这个函数之前就被设置了 interrupted 状态位。可以发现这个方法并不区分这个 interrupted 状态位是之前就有的还是 lock 过程中产生的。不管如何，抛出异常后会清除 interrupted 标记。使用这个方法，我们可以中断某个等锁的线程，比如我们检测到了死锁，那么我们可以中断这个线程。boolean tryLock()尝试获取锁，如果可以，那么锁住对象然后返回 true，否则返回 false，不管怎么样，这个方法会立即返回。下面的例子展示了用这个方法来解决前面转账的死锁：public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { toAcct.lock.unlock(); } } } finally { fromAcct.lock.unlock(); } } if (System.nanoTime() < stopTime) return false; TimeUnit.NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }boolean tryLock(long time, TimeUnit unit) throws InterruptedException和 tryLock() 类似，不过不是立即返回，而是尝试一定时间后还拿不到锁就返回。unlock()释放锁。newCondition()暂且不管。ReentrantLock 类这是 Sun JDK（Open JDK）里唯一直接实现了 Lock 接口的类，所以如果你想用 Lock 的那些特性，比如 tryLock，那么就应该首先考虑它。首先我们解释一下 Reentrant。Reentrant 翻译成中文应该是“可重入”，对于锁来说，可重入是指如果一个线程已经拿到过一把锁，那么它可以再次拿到锁。听起来似乎没有什么意思，让我们来看看“不可重入”锁可能的一些问题和需要使用“可重入”锁的场景吧。public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } } Widget widget = new LoggingWidget(); widget.doSomething();设想这样一个应用场景：我们有一个图的数据结构，我们需要遍历所有节点，找到满足某些条件的节点，锁定所有这些节点，然后对他们进行一些操作。由于图的遍历可能重复访问某个节点，如果简单的锁定每个满足条件的节点，那么可能死锁。当然我们可以自己用程序记下哪些节点已经访问过了，不过也可以把这就事情交给 ReentrantLock，第二次锁定某个对象也会成功并立即返回。那么你可能会问，我释放锁的时候怎么记得它锁定过了多少次呢？如果释放少了，那么会死锁；释放多了，可能也会有问题（有些锁实现会抛出异常，但是 JMM 好像没有定义）。【上面的场景参考 http://stackoverflow.com/questions/1312259/what-is-the-re-entrant-lock-and-concept-in-general】不用担心，ReentrantLock 提供了 getHoldCount 方法，最后释放这么多次就好了。ReentrantLock 会记下当前拿锁的线程，已经拿锁的次数，每次 unlock 都会减一，如果为零了，那么释放锁，另一个线程拿到锁并且计数器值为一。ReentrantLock 的构造函数可以接受一个 fairness 的参数。如果为 true，那么它会倾向于把锁给等待时间最长的线程。但是这样的代价也是巨大的：横轴是并发线程数，参考方法是 ConcurrentHashMap，另外分别用 Nonfair Lock 和 Fair Lock 封装普通的 HashMap，可以看到，是否 fair 的差别是非常巨大的。正如前面所说的，ReentrantLock 是支持 Interrupted 的。ReadWriteLock 接口有的应用场景下，有两类角色：Reader 和 Writer。Reader 读取数据，Writer 更新数据。多个 Reader 同时读取是没有问题的，但是 Reader 们和 Writer 是互斥的，并且 Writer 和 Writer 也是互斥的。而且很多应用中，Reader 会很多，而 Writer 会比较少。这个接口就是为了解决这类特殊场景的。public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }用法：ReadWriteLock rwl = ...; // Reader threads void read(){ rwl.readLock().lock(); try{ // entering critical section } finally { rwl.readLock().unlock(); } } // Writer threads void write(){ rwl.writeLock().lock(); try{ // entering critical section } finally { rwl.writeLock().unlock(); } }ReentrantReadWriteLock 类这是 Sun JDK 里唯一实现 ReadWriteLock 接口的类。这个类的特性：获取锁的顺序这个类并不倾向 Reader 或者 Writer，不过有个 fairness 的策略。非公平模式（默认）如果很多 Reader 和 Writer 的话，很可能 Reader 一直能获取锁，而 Writer 可能会饥饿。公平模式这种模式下，会尽量以请求锁的顺序来保证公平性。当前锁释放以后，等待时间最长的 Writer 或者一组 Reader（Reader 是一伙的！）获取锁。如果锁被拿着，这时 Writer 来了，他会开始排队；如果 Reader 来了，如果它之前没有 Writer 并且当前拿锁的是 Reader，那么它直接就拿到锁，当然如果是 Writer 拿着，那么它也只能排队等锁。不过如果 Reader 拿着锁，Writer 排队，然后 Reader 排在 Writer 后，但是 Writer 放弃了排队（比如它用的是 tryLock 30s），那么 Reader 直接拿到锁而不用排队。还有就是 ReentrantReadWriteLock.ReadLock.tryLock() 和 ReentrantReadWriteLock.WriteLock.tryLock() 方法不管这些，一旦调用的时候能拿到锁，那么它们就会插队！！Reentrancy从名字就知道它支持可重入。以前拿过锁的 Reader 和 Writer 可以继续拿锁。另外拿到 WriteLock 的线程可以拿到 ReadLock，但是反之不然。Lock Downgrading拿到 WriteLock 的可以直接变成 ReadLock，不用释放 WriteLock 再从新请求 ReadLock（这样需要重新排队），实现的方法是先拿到 WriteLock，接着拿 ReadLock（上面的特性保证了不会死锁），然后释放 WriteLock，这样就得到一个 ReadLock 并立马持有。Interruption of lock acquisition支持。一个使用读写锁的例子：class CachedData { Object data; volatile boolean cacheValid; 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(); // 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(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }一个 Cache 数据的例子，读取数据时首先拿读锁，如果 cache 是有效的（volatile boolean cacheValid），直接使用数据。如果失效了，那么释放读锁，获取写锁【这个类不支持 upgrading】，然后 double check 一下是否 cache 有效，如果还是无效（说明它应该更新），那么更新数据，并且修改变量 cacheValid，让其它线程看到。臭名昭著的双重检查锁定 (Double-Checked Locking)前面提到了 double check，这里也顺便讨论一下：@NotThreadSafe public class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource(); } } return resource; } }很多"hacker"再提到延迟加载的时候都会提到它，上面的代码看起来没有什么问题：首先检查一下 resource，如果为空，那么加锁，因为检查 resource==null 没有加锁，所以可能同时两个线程进入 if 并且请求加锁，所以第一个拿到锁的初始化一次，第二次拿锁的会再次 check。这看起来很完美：大多数情况下 resource 不为空，很少的情况（刚开始时）resource 为空，那么再加锁，这比一上来就加锁要高效很多。不过千万别高兴地太早了，因为编译器对引用的赋值可能会做优化，可能这个对象还没有正确的构造好，值已经赋好了（为什么要这么做？也许构造对象需要 IO，IO 等待的时间把值赋好了能提高速度）。这个时候别的线程就惨了！另外很多讲延迟加载的文章都比较早（早于 JDK 6），那个年代 Java 的 synchronized 确实很不给力。如果你实在在乎这点性能的话，应该用 JVM 的静态类加载机制来实现：@ThreadSafe public class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }说明版本时效：本文内容主要基于 JDK 5 至 JDK 7 时期的技术背景。现代 JDK（如 JDK 17/21）在 synchronized 锁优化、并发工具类等方面已有显著改进，部分性能结论可能不再适用。链接有效性：文中部分外部链接及图片资源源自历史归档，可能已失效，请以官方文档为准。
NUMA 与 SMPSMP（Symmetric Multi-Processor，对称多处理器）结构指服务器中多个 CPU 对称工作，每个 CPU 访问内存地址所需时间相同。其主要特征是共享，包含对 CPU、内存、I/O 等资源进行共享。SMP 的优点是能够保证内存一致性；缺点是这些共享资源很可能成为性能瓶颈。随着 CPU 数量的增加，每个 CPU 都要访问相同的内存资源，可能导致内存访问冲突，进而造成 CPU 资源的浪费。常用的 PC 机就属于这种架构。NUMA（Non-Uniform Memory Access，非一致存储访问）将 CPU 分为多个 CPU 模块，每个模块由多个 CPU 组成，并且具有独立的本地内存、I/O 槽口等。模块之间可以通过互联模块相互访问，访问本地内存的速度将远远高于访问远地内存（系统内其它节点的内存）的速度，这也是“非一致存储访问”名称的由来。NUMA 的优点是可以较好地解决原来 SMP 系统的扩展问题；缺点是由于访问远地内存的延时远远超过本地内存，因此当 CPU 数量增加时，系统性能无法线性增加。CLH 算法实现CLH 队列中的结点 QNode 中含有一个 locked 字段：该字段若为 true，表示该线程需要获取锁且未释放锁；若为 false，表示线程已释放锁。结点之间通过隐式的链表相连。之所以称为“隐式链表”，是因为这些结点之间没有明显的 next 指针，而是通过 myPred 所指向结点的变化情况来影响 myNode 的行为。CLHLock 上还有一个尾指针 tail，始终指向队列的最后一个结点。CLHLock 的类图如下所示：当一个线程需要获取锁时，会创建一个新的 QNode，将其中的 locked 设置为 true 表示需要获取锁。然后线程对 tail 域调用 getAndSet 方法，使自己成为队列的尾部，同时获取一个指向其前驱的引用 myPred。随后，该线程就在前驱结点的 locked 字段上进行自旋（Spin），直到前驱结点释放锁。当一个线程需要释放锁时，将当前结点的 locked 域设置为 false，同时回收前驱结点。如下图所示，线程 A 需要获取锁，其 myNode 域为 true，此时 tail 指向线程 A 的结点；然后线程 B 也加入到线程 A 后面，tail 指向线程 B 的结点。接着线程 A 和 B 都在它的 myPred 域上自旋，一旦它的 myPred 结点的 locked 字段变为 false，它就可以获取锁并执行临界区代码。显然，线程 A 的 myPred locked 域为 false，此时线程 A 获取到了锁。整个 CLH 的代码如下。其中用到了 ThreadLocal 类，将 QNode 绑定到每一个线程上；同时用到了 AtomicReference，对尾指针的修改正是调用它的 getAndSet() 操作来实现的，它能够保证以原子方式更新对象引用。public class CLHLock implements Lock { AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode()); ThreadLocal<QNode> myPred; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } @Override public void lock() { QNode qnode = myNode.get(); qnode.locked = true; QNode pred = tail.getAndSet(qnode); myPred.set(pred); while (pred.locked) { // 自旋等待 } } @Override public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); } }从代码中可以看出，lock 方法中有一个 while 循环，这是在等待前驱结点的 locked 域变为 false，这是一个自旋等待的过程。unlock 方法很简单，只需要将自己的 locked 域设置为 false 即可，并将前驱结点回收供下次使用。CLH 优缺点CLH 队列锁的优点是空间复杂度低。如果有 n 个线程，L 个锁，每个线程每次只获取一个锁，那么需要的存储空间是 O(L+n)（n 个线程有 n 个 myNode，L 个锁有 L 个 tail）。CLH 的一种变体被应用在了 Java 并发框架中（例如 AQS 内部实现）。唯一的缺点是在 NUMA 系统结构下性能很差。在这种系统结构下，每个线程有自己的内存，如果前驱结点的内存位置比较远，自旋判断前驱结点的 locked 域，性能将大打折扣。但是在 SMP 系统结构下，该算法还是非常有效的。一种解决 NUMA 系统结构问题的思路是使用 MCS 队列锁。参考资料A Hierarchical CLH Queue Lock线程锁系列（1）：CLH LockThe Art of Multiprocessor Programming说明：本文内容主要基于 CLH 锁的理论模型与经典实现原理。在实际的 Java 并发包（java.util.concurrent）中，AQS（AbstractQueuedSynchronizer）使用了 CLH 队列的变体来实现锁机制，具体实现细节可能有所优化。
问题描述在 C/S（Client/Server）架构中，为了减少频繁建立连接的开销，我们有时会长时间保持一个连接。但同时，通常会设置一个超时时间：若在该时间内连接未发起任何请求，则将其断开，以减少服务端负载并节约资源。该机制一般建议在服务端实现。这是因为当客户端强制关闭或意外断开连接时，服务端往往无法立即感知。若将超时机制完全交由客户端实现，在上述异常情况下，该机制可能会失效。这一问题看似普通，往往容易被忽视。但最近在项目中观察到一种该机制的糟糕实现，故在此深入分析其潜在问题及优化方案。问题分析及解决方案服务端通常需要维护大量连接，因此普遍做法是创建一个定时器，定期检查所有连接中哪些已超时。此外，关键在于：当收到客户端发来的数据时，如何高效地刷新该连接的超时信息？初始实现与可见性问题最近看到的一种实现方式如下：public class Connection { private long lastTime; public void refresh() { lastTime = System.currentTimeMillis(); } public long getLastTime() { return lastTime; } // ...... }在每次收到客户端数据时，调用 refresh 方法更新最后活动时间。然后在定时器任务中，通过比较当前时间与每个连接的 getLastTime() 来判定是否超时：public class TimeoutTask extends TimerTask { public void run() { long now = System.currentTimeMillis(); for (Connection c : connections) { if (now - c.getLastTime() > TIMEOUT_THRESHOLD) { // timeout, do something } } } }熟悉并发编程的读者可能已经发现了问题：内存可见性。调用 refresh 方法的线程（通常是 IO 线程）与执行定时器的线程并非同一个线程。若 lastTime 未做同步处理，定时器线程读到的可能是旧值，导致将活跃连接误判为超时并断开。性能考量与测试有读者可能会想到使用 volatile 修饰 lastTime。这确实解决了可见性问题，但作为服务端，高性能往往是核心诉求。volatile 写操作以及频繁调用 System.currentTimeMillis() 是否存在性能瓶颈？以下是我本地环境的一组测试数据，代码供参考：public class PerformanceTest { private static long i; private volatile static long vt; private static final int TEST_SIZE = 10000000; public static void main(String[] args) { long time = System.nanoTime(); for (int n = 0; n < TEST_SIZE; n++) vt = System.currentTimeMillis(); System.out.println("volatile 写 + 取系统时间：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) i = System.currentTimeMillis(); System.out.println("普通写 + 取系统时间：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) synchronized (PerformanceTest.class) { } System.out.println("空的同步块（synchronized）：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) vt++; System.out.println("volatile 变量自增：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) vt = i; System.out.println("volatile 写：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) i = vt; System.out.println("volatile 读：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) i++; System.out.println("普通自增：" + (-time + (time = System.nanoTime()))); for (int n = 0; n < TEST_SIZE; n++) i = n; System.out.println("普通读写：" + (-time + (time = System.nanoTime()))); } }测试一千万次，结果如下（耗时单位：纳秒，包含循环本身的时间）：238,932,949 : volatile 写 + 取系统时间144,317,590 : 普通写 + 取系统时间135,596,135 : 空的同步块（synchronized）80,042,382 : volatile 变量自增15,875,140 : volatile 写6,548,994 : volatile 读2,722,555 : 普通自增2,949,571 : 普通读写从数据可以看出，volatile 写操作配合获取系统时间的耗时非常高，甚至接近一次无竞争的同步块操作。获取系统时间本身也是一项相对耗时的操作。在高负载场景下，若每次刷新都执行此操作，成本过高。优化方案针对上述问题，优化思路如下：同步问题：跨线程数据操作必须保证可见性。减少耗时操作：能否不在每次刷新时都获取系统时间？因为刷新调用在高负载下非常频繁。如果不直接在刷新时获取时间，该如何判定超时？我的方案是：将时间的掌控交给定时器，连接对象仅维护状态标志。具体实现如下：为每个连接维护一个计数器，并在连接对象中设置一个 volatile boolean 变量 reset。刷新时：仅设置 reset = true（成本极低）。定时检查时：若 reset 为 true，则计数器归零并将 reset 复位；否则计数器加一。若计数器超过阈值，则判定超时。由于计数器仅由定时器线程维护，无需额外的同步处理。从测试数据来看，普通变量的操作成本极低。public class Connection { int count = 0; volatile boolean reset = false; public void refresh() { // 仅在需要时写入，减少 volatile 写操作 if (!reset) { reset = true; } } }public class TimeoutTask extends TimerTask { public void run() { for (Connection c : connections) { if (c.reset) { c.reset = false; c.count = 0; } else if (++c.count >= TIMEOUT_COUNT) { // timeout, do something } } } }代码中的 TIMEOUT_COUNT 等于超时时间除以定时器的周期。周期大小既影响定时器的执行频率，也会影响实际超时时间的波动范围（这种波动在第一种方案中同样存在，难以完全避免，且通常不需要毫秒级的精确度）。方案分析可见性保证：reset 变量加上了 volatile 修饰，保证了多线程操作的可见性。虽然两个线程都可能对该变量进行写操作，但无论线程如何穿插执行，都不会影响逻辑的正确性。性能优化：在 refresh 方法中，我增加了一个条件判断 if (!reset)。这看似多了一次 volatile 读操作，实则有益。在高负载下，refresh 会被频繁调用，意味着 reset 长时间处于 true 状态。加上条件后，大部分情况下只会执行读操作，不会执行写操作。从测试数据来看，volatile 读操作的性能显著优于写操作。仅在 reset 为 false 时（即定时器的一个周期内最多一次）才会执行写操作。这对高负载情况下的性能优化更有意义。综上所述，该方案在保证了线程安全的前提下，显著降低了高频调用下的性能开销。说明：本文示例代码基于 Java 传统 Timer 机制编写。在实际生产环境中，建议优先使用 ScheduledExecutorService 替代 Timer，以获得更好的线程管理和异常处理能力。此外，涉及耗时统计时，System.nanoTime() 通常比 System.currentTimeMillis() 更适合用于测量时间间隔。
前言在 JDK 1.8 中，AbstractQueuedSynchronizer（简称 AQS）拥有众多子类，如下图所示：可以看到，公平锁与非公平锁（包括 ReentrantLock）都是基于 AQS 实现的。公平锁（Fair Lock）：线程抢占锁的顺序严格按照调用 lock 方法的先后顺序，依次获取锁。非公平锁（Unfair Lock）：线程抢占锁的顺序不定。线程尝试获取锁时，无论是否有其他线程在等待，都先尝试直接获取。若失败则加入等待队列。这意味着后来者可能“插队”成功（但因为抢占锁失败而加入到等待队列的线程不能参与下一次抢占，直到被 unpark）。AQS 核心结构AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作。如果当前线程获取同步状态失败（即获取锁失败），AQS 会将当前线程及等待状态等信息构造成一个节点（Node）并加入同步队列，同时阻塞当前线程。当同步状态释放时，则会把节点中的线程唤醒，使其再次尝试获取同步状态。类结构public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { protected AbstractQueuedSynchronizer() { } // 同步器队列头结点 private transient volatile Node head; // 同步器队列尾结点 private transient volatile Node tail; // 同步状态（当 state 为 0 时，无锁；当 state > 0 时，说明有锁） private volatile int state; // 获取锁状态 protected final int getState() { return state; } // 设置锁状态 protected final void setState(int newState) { state = newState; } // ... 其他方法 }通过 AQS 的类结构可以看到，其内部核心包含一个队列和一个 state 变量：队列：通过双向链表实现，用于存储等待获取锁的线程。state：锁的状态标志。volatile 关键字：head、tail 和 state 都是 volatile 类型的变量，保证了多线程环境下的内存可见性。同步队列的基本结构如下：Node 节点结构static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; // 表示当前的线程被取消 static final int CANCELLED = 1; // 表示当前节点的后继节点包含的线程需要运行，也就是 unpark static final int SIGNAL = -1; // 表示当前节点在等待 condition，也就是在 condition 队列中 static final int CONDITION = -2; // 表示当前场景下后续的 acquireShared 能够得以执行 static final int PROPAGATE = -3; // 表示节点的状态。默认为 0，表示当前节点在 sync 队列中，等待着获取锁 volatile int waitStatus; // 前驱节点 volatile Node prev; // 后继节点 volatile Node next; // 获取锁的线程 volatile Thread thread; // 存储 condition 队列中的后继节点 Node nextWaiter; }Node 节点中除了存储当前线程、节点类型、队列中前后元素的引用外，还有一个关键变量 waitStatus。该变量用于描述节点的状态。由于 AQS 队列中存在并发，队列中会存取一定数量的节点，每个节点代表了一个线程的状态。有的线程可能因超时或中断需要放弃竞争（取消），有的线程在等待条件满足（Condition）。因此需要一个变量来描述这些状态，waitStatus 主要有以下几种状态：static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;CANCELLED (1)：因为超时或者中断，节点会被设置为取消状态。被取消状态的节点不应再去竞争锁，只能保持取消状态不变，不能转换为其他状态。处于这种状态的节点会被踢出队列，最终被 GC 回收。SIGNAL (-1)：表示这个节点的后继节点被阻塞了。当自己释放锁或取消的时候，需要通知（unpark）后继节点。CONDITION (-2)：表示这个节点在条件队列中，因为等待某个条件而被阻塞。它不会被作为同步节点直到其状态被置为 0。PROPAGATE (-3)：使用在共享模式下，头结点有可能处于这种状态，表示锁的下一次获取可以无条件传播。0 (None)：新结点会处于这种状态，表示正常等待中。应用之——公平锁公平锁的获取锁公平锁的 lock 方法直接调用 AQS 的 acquire 方法：final void lock() { acquire(1); }进入 AQS 的 acquire 方法：public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); // 自己给自己设置了一个中断标志，只有之前被中断过，前面才会返回 true，这里相当于重新设置一个中断标志 }从方法语义上看，尝试获取一把锁。如果获取不到（tryAcquire 返回 false），则尝试将该线程封装成 Node 对象放入等待队列中（利用 && 实现了短路性质）。tryAcquire 方法在 AQS 中是留空的，留给子类实现：protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }设计理念：既然要给子类实现，通常应该用抽象方法。但是 Doug Lea 没有这么做，原因是 AQS 有两种功能（独占与共享），面向两种使用场景。如果都需要给子类定义的方法都是抽象方法，会导致子类无论如何都需要实现另外一种场景的抽象方法，这对子类来说是不友好的。以下是 FairSync（公平锁内部类）的 tryAcquire 方法实现：protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取当前线程 int c = getState(); // 获取父类 AQS 中的标志位 if (c == 0) { // 0 意味着锁空闲 if (!hasQueuedPredecessors() && // 如果前面没有已在队列中的线程！ compareAndSetState(0, acquires)) { // 修改一下状态位，注意：这里的 acquires 是在 lock 的时候传递来的，值为 1 setExclusiveOwnerThread(current); // 如果通过 CAS 操作将状态位更新成功，则代表当前线程获取锁，将当前线程设置到 AQS 的一个变量中 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果不为 0 意味着，锁已经被拿走了。因为 ReentrantLock 是重入锁，判断获取锁的线程是不是当前请求锁的线程 int nextc = c + acquires; // 如果是的，累加在 state 字段上就可以了 if (nextc < 0) // 重入次数太多，溢出了 throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }其中 hasQueuedPredecessors 方法用于判断队列中是否有前驱节点（保证公平性）：public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }到此，如果获取锁，tryAcquire 返回 true，反之返回 false。回到 AQS 的 acquire 方法，如果没有获取到锁，按照逻辑应将当前线程放到队列中去。在入队之前，需要做些包装：public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }addWaiter 方法用于创建节点并加入队列：/** * Creates and enqueues node for current thread and given mode. * 用当前线程去构造一个 Node 对象，mode 是一个表示 Node 类型的字段， * 仅仅表示这个节点是独占的，还是共享的。 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ private 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; }创建好节点后，将节点加入到队列尾部。此处，在队列不为空的时候，先尝试通过 CAS 方式修改尾节点为最新的节点。如果修改失败，意味着有并发，这个时候才会进入 enq 中死循环，“自旋”方式修改。private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }在解释 acquireQueued 之前，我们需要先看下 AQS 中队列的内存结构。队列由 Node 类型的节点组成，其中至少有两个变量，一个封装线程，一个封装节点类型。实际上，它的内存结构是这样的（第一次节点插入时，第一个节点是一个空节点，代表有一个线程已经获取锁，事实上，队列的第一个节点就是代表持有锁的节点）：黄色节点为队列默认的头节点，每次有线程竞争失败，进入队列后其实都是插入到队列的尾节点（tail 节点）后面。final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 前任节点 if (p == head && tryAcquire(arg)) { // 如果当前的节点的前一个节点是 head，说明他是队列中第一个“有效的”节点，因此尝试获取锁 setHead(node); // 成功后，将上图中的黄色节点移除，node 变成头节点 p.next = null; // help GC failed = false; return interrupted; } // 如果 p 节点不是头节点，或者 tryAcquire 返回 false，说明请求失败。 // 那么首先需要判断请求失败后 node 节点是否应该被阻塞，如果应该 // 被阻塞，那么阻塞 node 节点，并检测中断状态。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 如果需要，借助 JUC 包下的 LockSupport 类的静态方法 park 挂起当前线程，直到被唤醒 interrupted = true; } } finally { if (failed) // 如果有异常 cancelAcquire(node); // 取消请求，对应到队列操作，就是将当前节点从队列中移除 } }判断是否应该被挂起（当且仅当前一个节点是 SIGNAL 状态时，才会 return true，这之后才会执行挂起方法；若返回 false，则跳出上面的 if 判定，会继续自旋）：private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 如果这一步不满足，都会 return false /* * 前一节点已经设置了 signal 状态，释放锁后会通知后继节点 * 所以可以安全挂起 */ return true; if (ws > 0) { /* 判断如果前驱节点状态为 CANCELLED，那就一直往前找，直到找到最近一个正常等待的状态 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); // 放置在其后面 pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ // 如果前驱节点正常，则修改前驱节点状态为 SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }关于状态设置的疑问：为什么不关心是否成功却还要设置呢？如果设置失败，表示前驱已经被 signal 了。如果前驱是 head，说明有机会获取锁，所以返回 false 后还可以再次 tryAcquire。如果设置成功，表示前驱等待 signal。如果再次确认 pred.waitStatus 仍然是 Node.SIGNAL，则表明前驱等待释放锁的情况下必须阻塞当前线程，所以返回 true 后即被 park。private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); // 返回在这过程中是否被中断过，如果是，返回 true，同时将 interrupted 标志（acquireQueued 方法中）设为 true }在 acquireQueued 方法中，当前线程通过自旋的方式来尝试获取同步状态：当前节点的前驱节点为头节点才能尝试获得锁。如果获得成功，则把当前线程设置成头结点，把之前的头结点从队列中移除，等待垃圾回收（没有对象引用）。如果获取锁失败，则进入 shouldParkAfterFailedAcquire 方法中检测当前节点是否可以被安全的挂起（阻塞）。如果可以安全挂起，则进入 parkAndCheckInterrupt 方法，把当前线程挂起，并将 interrupted 置为 true。挂起的线程被唤醒后，继续在 acquireQueued 的 for 死循环中运行，直到获取锁后可以跳出循环。待验证结论：没有获得锁的线程，有可能在下次获得锁之前都不会被挂起。因为无法被安全挂起时，会把节点移到前驱节点为 SIGNAL 的节点后（这样下次就可以安全挂起了），然后再进行自旋。有可能在自旋的过程中，该节点已经成为了头结点后的节点，因此可以直接尝试获取锁而避免被挂起。公平锁的释放锁阶段（非公平锁调用的也是同一个方法）public void unlock() { sync.release(1); }public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; // 0 是初始值，不对应某个具体状态，所以不能为 0 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }unlock 方法调用了 AQS 的 release 方法，同样传入了参数 1。和获取锁相对应，获取一个锁，标志位 +1；释放一个锁，标志位 -1。protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果释放的线程和获取锁的线程不是同一个，抛出非法监视器状态异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 因为是重入的关系，不是每次释放锁 c 都等于 0，直到最后一次释放锁时，才通知 AQS 不需要再记录哪个线程正在获取锁，即当前锁是 free 状态 free = true; setExclusiveOwnerThread(null); } setState(c); return free; }释放锁成功后，找到 AQS 的头节点，并唤醒它即可：/** * Wakes up node's successor, if one exists. * * @param node the node */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ /* * 为什么不关心是否成功却还要设置呢？ * * 注意这里的 Node 实际就是 head * * 如果设置成功，即 head.waitStatus=0，则可以让这时即将被阻塞的线程有机会再次调用 tryAcquire 获取锁。 * 也就是让 shouldParkAfterFailedAcquire 方法里的 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 执行失败返回 false，这样就能再有机会再 tryAcquire 了 * * 如果设置失败，新跟随在 head 后面的线程被阻塞，但是没关系，下面的代码会立即将这个阻塞线程释放掉 */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; // 从后往前找，但仍是找离 node 最近的一个有效节点（也就是它的继承者） for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }应用之——非公平锁其实，非公平锁和公平锁的唯一区别就是获取锁的方式不同：一个是按前后顺序依次获取锁，一个是抢占式的获取锁。获取锁final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }非公平锁的 lock 方法处理方式是：在 lock 的时候先直接 CAS 修改一次 state 变量（尝试获取锁），成功就返回，不成功再排队，从而达到不排队直接抢占的目的。关于具体场景的解析，可参考：关于具体场景的解析当等待队列中，某个结点内的线程被唤醒后，它参与到锁竞争中。同它一起竞争的有可能是之前没有竞争失败过的线程。等待队列中的其他结点，不会参与到竞争中。说明本文基于 JDK 1.8 版本源码进行分析。文中代码片段为核心逻辑摘录，实际源码可能包含更多细节处理。AQS 是 Java 并发包（J.U.C）的基石，理解其原理有助于深入掌握 ReentrantLock、CountDownLatch 等同步工具。
本篇主要内容如下：帮你总结好的锁：序号锁名称应用1乐观锁CAS2悲观锁synchronized、Vector、Hashtable3自旋锁CAS4可重入锁synchronized、ReentrantLock、Lock5读写锁ReentrantReadWriteLock、CopyOnWriteArrayList、CopyOnWriteArraySet6公平锁ReentrantLock(true)7非公平锁synchronized、ReentrantLock(false)8共享锁ReentrantReadWriteLock 中读锁9独占锁synchronized、Vector、Hashtable、ReentrantReadWriteLock 中写锁10重量级锁synchronized11轻量级锁锁优化技术12偏向锁锁优化技术13分段锁ConcurrentHashMap14互斥锁synchronized15同步锁synchronized16死锁相互请求对方的资源17锁粗化锁优化技术18锁消除锁优化技术1. 乐观锁乐观锁（Optimistic Locking） 是一种基于乐观思想的并发控制机制。它假定当前环境是“读多写少”，发生并发冲突的概率较低。因此，在读数据时认为其他线程不会进行修改，所以不上锁。只有在写数据时，才会判断当前值与期望值是否相同：如果相同则进行更新（更新期间保证原子性），否则继续尝试 CAS 操作。Java 中的实现： 主要通过 CAS（Compare-And-Swap，比较并交换）实现。比较主内存中的当前值与线程工作内存中的预期值，若一致则更新，否则重试。如上图所示，乐观锁允许同时进行读操作，但在写操作时会检查冲突。2. 悲观锁悲观锁（Pessimistic Locking） 是一种基于悲观思想的并发控制机制。它认为写多读少，发生并发冲突的可能性高。因此，每次去拿数据的时候都认为其他线程会修改，所以每次读写数据时都会上锁。其他线程想要读写这个数据时，会被阻塞（Block），直到持有锁的线程释放锁。Java 中的实现： synchronized 修饰的方法和方法块、ReentrantLock 等。如上图所示，同一时刻只能有一个线程进行读操作或者写操作，其他线程的读写操作均被阻塞。3. 自旋锁自旋锁（Spin Lock） 是一种技术策略。为了让线程等待，不让线程立即进入阻塞状态，而是让线程执行一个忙循环（自旋），反复检查锁是否可用。现代个人电脑和服务器多为多核处理器系统。如果物理机器有一个以上的处理器核心，能让两个或以上的线程同时并行执行，就可以让后面请求锁的那个线程“稍等一会”，但不放弃处理器的执行时间，看看持有锁的线程是否很快就会释放锁。自旋锁 的优点： 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成，这些操作给 Java 虚拟机的并发性能带来了很大的压力。自旋锁 的缺点： 占用处理器的时间。如果占用的时间很长，会白白消耗处理器资源，而不会做任何有价值的工作，带来性能的浪费。因此自旋等待的时间必须有一定的限度，如果自旋超过了限定的次数仍然没有成功获得锁，就应当使用传统的方式去挂起线程。自旋次数默认值： 10 次，可以使用参数 -XX:PreBlockSpin 来自行更改（注：该参数在较新版本 JDK 中可能已废弃或调整）。自适应自旋： 意味着自旋的时间不再是固定的，而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋，随着程序运行时间的增长及性能监控信息的不断完善，虚拟机对程序锁的状态预测就会越来越精准。Java 中的实现： CAS 操作中的比较操作失败后的自旋等待。4. 可重入锁（递归锁）可重入锁（Reentrant Lock） 是一种技术特性。任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。可重入锁 的原理： 通过组合自定义同步器来实现锁的获取与释放。再次获取锁： 识别获取锁的线程是否为当前占据锁的线程，如果是，则再次成功获取。获取锁后，进行计数自增。释放锁： 释放锁时，进行计数自减。Java 中的实现： ReentrantLock、synchronized 修饰的方法或代码段。可重入锁 的作用： 避免死锁，支持递归调用。面试题 1： 可重入锁如果加了两把，但是只释放了一把会出现什么问题？答：程序卡死，线程不能出来。也就是说我们申请了几把锁，就需要释放几把锁。面试题 2： 如果只加了一把锁，释放两次会出现什么问题？答：会报错，java.lang.IllegalMonitorStateException。5. 读写锁读写锁（ReadWrite Lock） 是一种技术机制，通过 ReentrantReadWriteLock 类来实现。为了提高性能，Java 提供了读写锁，在读的地方使用读锁，在写的地方使用写锁，灵活控制。如果没有写锁的情况下，读是无阻塞的，在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁，多个读锁不互斥，读锁与写锁互斥，这是由 JVM 自己控制的。读锁： 允许多个线程获取读锁，同时访问同一个资源。写锁： 只允许一个线程获取写锁，不允许同时访问同一个资源。如何使用：/** * 创建一个读写锁 * 它是一个读写融为一体的锁，在使用的时候，需要转换 */ private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 获取读锁和释放读锁 // 获取读锁 rwLock.readLock().lock(); // 释放读锁 rwLock.readLock().unlock(); // 获取写锁和释放写锁 // 创建一个写锁 rwLock.writeLock().lock(); // 写锁释放 rwLock.writeLock().unlock(); Java 中的实现： ReentrantReadWriteLock。6. 公平锁公平锁（Fair Lock） 是一种思想策略。多个线程按照申请锁的顺序来获取锁。在并发环境中，每个线程会先查看此锁维护的等待队列，如果当前等待队列为空，则占有锁；如果等待队列不为空，则加入到等待队列的末尾，按照 FIFO（先进先出）的原则从队列中拿到线程，然后占有锁。7. 非公平锁非公平锁（Non-fair Lock） 是一种思想策略。线程尝试获取锁，如果获取不到，则再采用公平锁的方式。多个线程获取锁的顺序，不是按照先到先得的顺序，有可能后申请锁的线程比先申请的线程优先获取锁。优点： 非公平锁的性能高于公平锁（减少了线程唤醒的上下文切换开销）。缺点： 有可能造成线程饥饿（某个线程很长一段时间获取不到锁）。Java 中的实现： synchronized 是非公平锁；ReentrantLock 通过构造函数指定该锁是公平的还是非公平的，默认是非公平的。8. 共享锁共享锁（Shared Lock） 是一种思想。可以有多个线程获取读锁，以共享的方式持有锁。和乐观锁、读写锁中的读锁概念同义。Java 中用到的共享锁： ReentrantReadWriteLock 中的读锁。9. 独占锁独占锁（Exclusive Lock） 是一种思想。只能有一个线程获取锁，以独占的方式持有锁。和悲观锁、互斥锁同义。Java 中用到的独占锁： synchronized、ReentrantLock。10. 重量级锁重量级锁 是一种称谓。synchronized 是通过对象内部的一个叫做监视器锁（monitor）来实现的，监视器锁本身依赖底层的操作系统的 Mutex Lock 来实现。操作系统实现线程的切换需要从用户态切换到核心态，成本非常高。这种依赖于操作系统 Mutex Lock 来实现的锁称为重量级锁。为了优化 synchronized，JVM 引入了 轻量级锁 、 偏向锁。Java 中的重量级锁： synchronized（膨胀后）。11. 轻量级锁轻量级锁 是 JDK 6 时加入的一种锁优化机制。轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下，减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况，那轻量级锁将不会有效，必须膨胀为重量级锁。优点： 如果没有竞争，通过 CAS 操作成功避免了使用互斥量的开销。缺点： 如果存在竞争，除了互斥量本身的开销外，还额外产生了 CAS 操作的开销，因此在有竞争的情况下，轻量级锁比传统的重量级锁更慢。12. 偏向锁偏向锁 是 JDK 6 时加入的一种锁优化机制。在无竞争的情况下把整个同步都消除掉，连 CAS 操作都不去做了。偏是指偏心，它的意思是这个锁会偏向于第一个获得它的线程。如果在接下来的执行过程中，该锁一直没有被其他的线程获取，则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时，虚拟机都可以不再进行任何同步操作（例如加锁、解锁及对 Mark Word 的更新操作等）。优点： 把整个同步都消除掉，连 CAS 操作都不去做了，优于轻量级锁。缺点： 如果程序中大多数的锁都总是被多个不同的线程访问，那偏向锁就是多余的。13. 分段锁分段锁 是一种机制。最好的例子来说明分段锁是 ConcurrentHashMap。ConcurrentHashMap 原理（JDK 1.7）： 它内部细分了若干个小的 HashMap，称之为段（Segment）。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段，既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项 key-value，并不是将整个 HashMap 加锁，而是首先根据 hashcode 得到该 key-value 应该存放在哪个段中，然后对该段加锁，并完成 put 操作。在多线程环境中，如果多个线程同时进行 put 操作，只要被加入的 key-value 不存放在同一个段中，则线程间可以做到真正的并行。线程安全： ConcurrentHashMap 是一个 Segment 数组，Segment 通过继承 ReentrantLock 来进行加锁，所以每次需要加锁的操作锁住的是一个 segment，这样只要保证每个 Segment 是线程安全的，也就实现了全局的线程安全。14. 互斥锁互斥锁与悲观锁、独占锁同义，表示某个资源只能被一个线程访问，其他线程不能访问。读 - 读互斥读 - 写互斥写 - 读互斥写 - 写互斥Java 中的实现： synchronized。15. 同步锁同步锁与互斥锁同义，表示并发执行的多个线程，在同一时间内只允许一个线程访问共享数据。Java 中的实现： synchronized。16. 死锁死锁 是一种现象。如线程 A 持有资源 x，线程 B 持有资源 y，线程 A 等待线程 B 释放资源 y，线程 B 等待线程 A 释放资源 x，两个线程都不释放自己持有的资源，则两个线程都获取不到对方的资源，就会造成死锁。Java 中的死锁不能自行打破，所以线程死锁后，线程不能进行响应。所以一定要注意程序的并发场景，避免造成死锁。17. 锁粗化锁粗化（Lock Coarsening） 是一种优化技术。如果一系列的连续操作都对同一个对象反复加锁和解锁，甚至加锁操作都是出现在循环体之中，就算真的没有线程竞争，频繁地进行互斥同步操作将会导致不必要的性能损耗。所以就采取了一种方案：把加锁的范围扩展（粗化）到整个操作序列的外部，这样加锁解锁的频率就会大大降低，从而减少了性能损耗。18. 锁消除锁消除（Lock Elimination） 是一种优化技术，就是把锁干掉。当 Java 虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。那如何判断共享数据不会被线程竞争？利用 逃逸分析技术：分析对象的作用域，如果对象在 A 方法中定义后，被作为参数传递到 B 方法中，则称为方法逃逸；如果被其他线程访问，则称为线程逃逸。在堆上的某个数据不会逃逸出去被其他线程访问到，就可以把它当作栈上数据对待，认为它是线程私有的，同步加锁就不需要了。19. synchronizedsynchronized 是 Java 中的关键字，用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。作用于实例方法时，锁住的是对象的实例（this）；当作用于静态方法时，锁住的是 Class 类，相当于类的一个全局锁，会锁所有调用该方法的线程；synchronized 作用于一个非 NULL 的对象实例时，锁住的是所有以该对象为锁的代码块。它有多个队列，当多个线程一起访问某个对象监视器的时候，对象监视器会将这些线程存储在不同的容器中。每个对象都有个 monitor 对象，加锁就是在竞争 monitor 对象。代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的，方法加锁是通过一个标记位来判断的。20. Lock 和 synchronized 的区别Lock： 是 Java 中的接口，可重入锁、悲观锁、独占锁、互斥锁、同步锁（具体实现如 ReentrantLock）。区别对比：Lock 需要手动获取锁和释放锁。就好比自动挡和手动挡的区别。Lock 是一个接口，而 synchronized 是 Java 中的关键字，synchronized 是内置的语言实现。synchronized 在发生异常时，会自动释放线程占有的锁，因此不会导致死锁现象发生；而 Lock 在发生异常时，如果没有主动通过 unlock() 去释放锁，则很可能造成死锁现象，因此使用 Lock 时需要在 finally 块中释放锁。Lock 可以让等待锁的线程响应中断，而 synchronized 却不行，使用 synchronized 时，等待的线程会一直等待下去，不能够响应中断。通过 Lock 可以知道有没有成功获取锁，而 synchronized 却无法办到。Lock 可以通过实现读写锁提高多个线程进行读操作的效率。synchronized 的优势：足够清晰简单，只需要基础的同步功能时，用 synchronized。Lock 应该确保在 finally 块中释放锁。如果使用 synchronized，JVM 确保即使出现异常，锁也能被自动释放。使用 Lock 时，Java 虚拟机很难得知哪些锁对象是由特定线程锁持有的。21. ReentrantLock 和 synchronized 的区别ReentrantLock 是 Java 中的类： 继承了 Lock 接口，是可重入锁、悲观锁、独占锁、互斥锁、同步锁的具体实现。相同点：主要解决共享变量如何安全访问的问题。都是可重入锁，也叫做递归锁，同一线程可以多次获得同一个锁。保证了线程安全的两大特性：可见性、原子性。不同点：ReentrantLock 就像手动汽车，需要显式地调用 lock 和 unlock 方法，synchronized 隐式获得释放锁。ReentrantLock 可响应中断，synchronized 是不可以响应中断的，ReentrantLock 为处理锁的不可用性提供了更高的灵活性。ReentrantLock 是 API 级别的，synchronized 是 JVM 级别的。ReentrantLock 可以实现公平锁、非公平锁，默认非公平锁，synchronized 是非公平锁，且不可更改。ReentrantLock 通过 Condition 可以绑定多个条件。说明：文中关于 ConcurrentHashMap 分段锁（Segment）的描述主要基于 JDK 1.7 及之前版本；JDK 1.8 及之后版本已改为 CAS + synchronized 锁住链表/树节点的实现，不再使用 Segment 分段锁。文中关于锁优化（偏向锁、轻量级锁）及 PreBlockSpin 参数等细节可能随 JDK 版本（如 JDK 15+ 废弃偏向锁）有所调整，请以实际使用的 JDK 版本文档为准。
Java 单例对象同步问题探讨单例对象（Singleton）是一种常用的设计模式。在 Java 应用中，单例对象能保证在一个 JVM 中，该对象只有一个实例存在。正是由于这个特点，单例对象通常作为程序中存放配置信息的载体，因为它能保证其他对象读到一致的信息。例如在某个服务器程序中，服务器的配置信息可能存放在数据库或文件中，这些配置数据由某个单例对象统一读取。服务进程中的其他对象如果要获取这些配置信息，只需访问该单例对象即可。这种方式极大地简化了复杂环境下（尤其是多线程环境）的配置管理，但随着应用场景的不同，也可能带来一些同步问题。本文将探讨在多线程环境下，使用单例对象作配置信息管理时可能会带来的几个同步问题，并针对每个问题给出可选的解决办法。问题描述在多线程环境下，单例对象的同步问题主要体现在两个方面：单例对象的初始化和单例对象的属性更新。本文描述的方法基于如下假设：单例对象的属性（或成员变量）的获取，是通过单例对象的初始化实现的。即在单例对象初始化时，会从文件或数据库中读取最新的配置信息。其他对象不能直接改变单例对象的属性，单例对象属性的变化来源于配置文件或配置数据库数据的变化。1.1 单例对象的初始化首先讨论单例对象的初始化同步。单例模式的通常处理方式是：在对象中有一个静态成员变量，其类型就是单例类型本身；如果该变量为 null，则创建该单例类型的对象，并将该变量指向这个对象；如果该变量不为 null，则直接使用该变量。其过程如下面代码所示：public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } public static GlobalConfig getInstance() { if (instance == null) { instance = new GlobalConfig(); } return instance; } public Vector getProperties() { return properties; } }这种处理方式在单线程模式下可以很好地运行，但是在多线程模式下可能产生问题。如果第一个线程发现成员变量为 null 准备创建对象，此时第二个线程同时也发现成员变量为 null，也会创建新对象。这就会造成在一个 JVM 中有多个单例类型的实例。如果这个单例类型的成员变量在运行过程中变化，会造成多个单例类型实例的不一致，产生一些异常现象。例如，某服务进程通过检查单例对象的某个属性来停止多个线程服务，如果存在多个单例对象的实例，就会造成部分线程服务停止，部分线程服务不能停止的情况。1.2 单例对象的属性更新通常，为了实现配置信息的实时更新，会有一个线程持续检测配置文件或配置数据库的内容，一旦发现变化，就更新到单例对象的属性中。在更新这些信息的时候，很可能还会有其他线程正在读取这些信息，造成不可预知的后果。还是以通过单例对象属性停止线程服务为例，如果更新属性时读写不同步，可能访问该属性时这个属性正好为空（null），程序就会抛出异常。解决方案2.1 单例对象的初始化同步对于初始化的同步，可以通过如下代码所采用的方式解决：public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } }这种处理方式虽然引入了同步代码，但是因为这段同步代码只会在最开始的时候执行一次或多次，所以对整个系统的性能不会有影响。2.2 单例对象的属性更新同步为了解决属性更新的同步问题，有两种方法：参照读者/写者（Reader/Writer）的处理方式设置一个读计数器，每次读取配置信息前，将计数器加 1，读完后将计数器减 1。只有在读计数器为 0 时，才能更新数据，同时要阻塞所有读属性的调用。代码如下：public class GlobalConfig { private static GlobalConfig instance; private Vector properties = null; private boolean isUpdating = false; private int readCount = 0; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public synchronized void update(String p_data) { syncUpdateIn(); // Update properties } private synchronized void syncUpdateIn() { while (readCount > 0) { try { wait(); } catch (Exception e) { } } } private synchronized void syncReadIn() { readCount++; } private synchronized void syncReadOut() { readCount--; notifyAll(); } public Vector getProperties() { syncReadIn(); // Process data syncReadOut(); return properties; } }采用“影子实例”的办法具体说，就是在更新属性时，直接生成另一个单例对象实例。这个新生成的单例对象实例将从数据库或文件中读取最新的配置信息，然后将这些配置信息直接赋值给旧单例对象的属性。如下面代码所示：public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } private static synchronized void syncInit() { if (instance == null) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null) { syncInit(); } return instance; } public Vector getProperties() { return properties; } public void updateProperties() { // Load updated configuration information by new a GlobalConfig object GlobalConfig shadow = new GlobalConfig(); properties = shadow.getProperties(); } }注意：在更新方法中，通过生成新的 GlobalConfig 实例，从文件或数据库中得到最新配置信息，并存放到 properties 属性中。上面两个方法比较起来，第二个方法更好。首先，编程更简单；其次，没有那么多的同步操作，对性能的影响也不大。说明本文代码示例基于较早的 Java 版本编写（如使用 Vector 等 legacy 类）。在实际现代 Java 开发中，建议结合具体场景使用 java.util.concurrent 包下的并发工具类（如 ReentrantReadWriteLock）或遵循更新的单例实现最佳实践（如枚举单例、静态内部类持有等），以获得更好的性能与安全性。
JDK 7 中的 Fork/Join 模式介绍随着多核芯片逐渐成为主流，大多数软件开发人员不可避免地需要了解并行编程的知识。与此同时，主流编程语言正在将越来越多的并行特性合并到标准库或语言本身之中。Java 开发工具包（JDK）在这方面同样走在潮流的前方。在 JDK 5 中，由 Doug Lea 提供的并行框架成为了标准库的一部分（JSR-166）。随后，在 JDK 6 中，一些新的并行特性，例如并行 Collection 框架，被合并到了标准库中（JSR-166x）。在本文撰写时，尽管 Java SE 7 尚未正式发布，但一些并行相关的新特性已经出现在 JSR-166y 中：Fork/Join 模式；TransferQueue：继承自 BlockingQueue，能在队列满时阻塞“生产者”；ArrayTasks/ListTasks：用于并行执行某些数组/列表相关任务的类；IntTasks/LongTasks/DoubleTasks：用于并行处理数字类型数组的工具类，提供了排序、查找、求和、求最小值、求最大值等功能。其中，对 Fork/Join 模式 的支持可能是对开发并行软件来说最通用的新特性。在 JSR-166y 中，Doug Lea 实现 ArrayTasks、ListTasks 等类时就大量用到了 Fork/Join 模式。读者需要注意，因为本文基于 JDK 7 发布前的 JSR-166y 版本，涉及到的功能和最终发布版本可能存在差异。Fork/Join 模式有自己的适用范围。如果一个应用能被分解成多个子任务，并且组合多个子任务的结果就能够获得最终的答案，那么这个应用就适合用 Fork/Join 模式来解决。图 1 给出了一个 Fork/Join 模式的示意图，位于图上部的 Task 依赖于位于其下的 Task 的执行，只有当所有的子任务都完成之后，调用者才能获得 Task 0 的返回结果。图 1. Fork/Join 模式示意图可以说，Fork/Join 模式能够解决很多种类的并行问题。通过使用 Doug Lea 提供的 Fork/Join 框架，软件开发人员只需要关注任务的划分和中间结果的组合，就能充分利用并行平台的优良性能。其他和并行相关的诸多难于处理的问题，例如负载平衡、同步等，都可以由框架采用统一的方式解决。这样，我们就能够轻松地获得并行的好处，而避免了并行编程困难且容易出错的缺点。使用 Fork/Join 模式在开始尝试 Fork/Join 模式之前，我们需要从 Doug Lea 主持的 Concurrency JSR-166 Interest Site 上下载 JSR-166y 的源代码，并且需要安装最新版本的 JDK 6（下载网址请参阅 参考资源）。Fork/Join 模式的使用方式非常直观。首先，我们需要编写一个 ForkJoinTask 来完成子任务的分割、中间结果的合并等工作。随后，我们将这个 ForkJoinTask 交给 ForkJoinPool 来完成应用的执行。通常我们并不直接继承 ForkJoinTask，因为它包含了太多的抽象方法。针对特定的问题，我们可以选择 ForkJoinTask 的不同子类来完成任务。RecursiveAction 是 ForkJoinTask 的一个子类，它代表了一类最简单的 ForkJoinTask：不需要返回值，当子任务都执行完毕之后，不需要进行中间结果的组合。如果我们从 RecursiveAction 开始继承，那么我们只需要重载 protected void compute() 方法。下面，我们来看看怎么为快速排序算法建立一个 ForkJoinTask 的子类：清单 1. ForkJoinTask 的子类class SortTask extends RecursiveAction { final long[] array; final int lo; final int hi; private int THRESHOLD = 30; public SortTask(long[] array) { this.array = array; this.lo = 0; this.hi = array.length - 1; } public SortTask(long[] array, int lo, int hi) { this.array = array; this.lo = lo; this.hi = hi; } protected void compute() { if (hi - lo < THRESHOLD) sequentiallySort(array, lo, hi); else { int pivot = partition(array, lo, hi); coInvoke(new SortTask(array, lo, pivot - 1), new SortTask(array, pivot + 1, hi)); } } private int partition(long[] array, int lo, int hi) { long x = array[hi]; int i = lo - 1; for (int j = lo; j < hi; j++) { if (array[j] <= x) { i++; swap(array, i, j); } } swap(array, i + 1, hi); return i + 1; } private void swap(long[] array, int i, int j) { if (i != j) { long temp = array[i]; array[i] = array[j]; array[j] = temp; } } private void sequentiallySort(long[] array, int lo, int hi) { Arrays.sort(array, lo, hi + 1); } }在清单 1 中，SortTask 首先通过 partition() 方法将数组分成两个部分。随后，两个子任务将被生成并分别排序数组的两个部分。当子任务足够小时，再将其分割为更小的任务反而引起性能的降低。因此，这里我们使用一个 THRESHOLD，限定在子任务规模较小时，使用直接排序，而不是再将其分割成为更小的任务。其中，我们用到了 RecursiveAction 提供的方法 coInvoke()。它表示：启动所有的任务，并在所有任务都正常结束后返回。如果其中一个任务出现异常，则其它所有的任务都取消。coInvoke() 的参数还可以是任务的数组。现在剩下的工作就是将 SortTask 提交到 ForkJoinPool 了。ForkJoinPool() 默认建立具有与 CPU 可使用线程数相等线程个数的线程池。我们在一个 JUnit 的 test 方法中将 SortTask 提交给一个新建的 ForkJoinPool：清单 2. 新建的 ForkJoinPool@Test public void testSort() throws Exception { ForkJoinTask sort = new SortTask(array); ForkJoinPool fjpool = new ForkJoinPool(); fjpool.submit(sort); fjpool.shutdown(); fjpool.awaitTermination(30, TimeUnit.SECONDS); assertTrue(checkSorted(array)); }在上面的代码中，我们用到了 ForkJoinPool 提供的如下函数：submit()：将 ForkJoinTask 类的对象提交给 ForkJoinPool，ForkJoinPool 将立刻开始执行 ForkJoinTask。shutdown()：执行此方法之后，ForkJoinPool 不再接受新的任务，但是已经提交的任务可以继续执行。如果希望立刻停止所有的任务，可以尝试 shutdownNow() 方法。awaitTermination()：阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束。并行快速排序的完整代码如下所示：清单 3. 并行快速排序的完整代码package tests; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Random; import java.util.concurrent.TimeUnit; import jsr166y.forkjoin.ForkJoinPool; import jsr166y.forkjoin.ForkJoinTask; import jsr166y.forkjoin.RecursiveAction; import org.junit.Before; import org.junit.Test; class SortTask extends RecursiveAction { final long[] array; final int lo; final int hi; private int THRESHOLD = 0; // For demo only public SortTask(long[] array) { this.array = array; this.lo = 0; this.hi = array.length - 1; } public SortTask(long[] array, int lo, int hi) { this.array = array; this.lo = lo; this.hi = hi; } protected void compute() { if (hi - lo < THRESHOLD) sequentiallySort(array, lo, hi); else { int pivot = partition(array, lo, hi); System.out.println("\npivot = " + pivot + ", low = " + lo + ", high = " + hi); System.out.println("array" + Arrays.toString(array)); coInvoke(new SortTask(array, lo, pivot - 1), new SortTask(array, pivot + 1, hi)); } } private int partition(long[] array, int lo, int hi) { long x = array[hi]; int i = lo - 1; for (int j = lo; j < hi; j++) { if (array[j] <= x) { i++; swap(array, i, j); } } swap(array, i + 1, hi); return i + 1; } private void swap(long[] array, int i, int j) { if (i != j) { long temp = array[i]; array[i] = array[j]; array[j] = temp; } } private void sequentiallySort(long[] array, int lo, int hi) { Arrays.sort(array, lo, hi + 1); } } public class TestForkJoinSimple { private static final int NARRAY = 16; // For demo only long[] array = new long[NARRAY]; Random rand = new Random(); @Before public void setUp() { for (int i = 0; i < array.length; i++) { array[i] = rand.nextLong() % 100; // For demo only } System.out.println("Initial Array: " + Arrays.toString(array)); } @Test public void testSort() throws Exception { ForkJoinTask sort = new SortTask(array); ForkJoinPool fjpool = new ForkJoinPool(); fjpool.submit(sort); fjpool.shutdown(); fjpool.awaitTermination(30, TimeUnit.SECONDS); assertTrue(checkSorted(array)); } boolean checkSorted(long[] a) { for (int i = 0; i < a.length - 1; i++) { if (a[i] > (a[i + 1])) { return false; } } return true; } }运行以上代码，我们可以得到以下结果：Initial Array: [46, -12, 74, -67, 76, -13, -91, -96] pivot = 0, low = 0, high = 7 array[-96, -12, 74, -67, 76, -13, -91, 46] pivot = 5, low = 1, high = 7 array[-96, -12, -67, -13, -91, 46, 76, 74] pivot = 1, low = 1, high = 4 array[-96, -91, -67, -13, -12, 46, 74, 76] pivot = 4, low = 2, high = 4 array[-96, -91, -67, -13, -12, 46, 74, 76] pivot = 3, low = 2, high = 3 array[-96, -91, -67, -13, -12, 46, 74, 76] pivot = 2, low = 2, high = 2 array[-96, -91, -67, -13, -12, 46, 74, 76] pivot = 6, low = 6, high = 7 array[-96, -91, -67, -13, -12, 46, 74, 76] pivot = 7, low = 7, high = 7 array[-96, -91, -67, -13, -12, 46, 74, 76]Fork/Join 模式高级特性使用 RecursiveTask除了 RecursiveAction，Fork/Join 框架还提供了其他 ForkJoinTask 子类：带有返回值的 RecursiveTask，使用 finish() 方法显式中止的 AsyncAction 和 LinkedAsyncAction，以及可使用 TaskBarrier 为每个任务设置不同中止条件的 CyclicAction。从 RecursiveTask 继承的子类同样需要重载 protected void compute() 方法。与 RecursiveAction 稍有不同的是，它可使用泛型指定一个返回值的类型。下面，我们来看看如何使用 RecursiveTask 的子类：清单 4. RecursiveTask 的子类class Fibonacci extends RecursiveTask<Integer> { final int n; Fibonacci(int n) { this.n = n; } private int compute(int small) { final int[] results = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; return results[small]; } public Integer compute() { if (n <= 10) { return compute(n); } Fibonacci f1 = new Fibonacci(n - 1); Fibonacci f2 = new Fibonacci(n - 2); f1.fork(); f2.fork(); return f1.join() + f2.join(); } }在清单 4 中，Fibonacci 的返回值为 Integer 类型。其 compute() 函数首先建立两个子任务，启动子任务执行，阻塞以等待子任务的结果返回，相加后得到最终结果。同样，当子任务足够小时，通过查表得到其结果，以减小因过多地分割任务引起的性能降低。其中，我们用到了 RecursiveTask 提供的方法 fork() 和 join()。它们分别表示：子任务的异步执行和阻塞等待结果完成。现在剩下的工作就是将 Fibonacci 提交到 ForkJoinPool 了，我们在一个 JUnit 的 test 方法中作了如下处理：清单 5. 将 Fibonacci 提交到 ForkJoinPool@Test public void testFibonacci() throws InterruptedException, ExecutionException { ForkJoinTask<Integer> fjt = new Fibonacci(45); ForkJoinPool fjpool = new ForkJoinPool(); Future<Integer> result = fjpool.submit(fjt); // do something System.out.println(result.get()); }使用 CyclicAction 来处理循环任务CyclicAction 的用法稍微复杂一些。如果一个复杂任务需要几个线程协作完成，并且线程之间需要在某个点等待所有其他线程到达，那么我们就能方便地用 CyclicAction 和 TaskBarrier 来完成。图 2 描述了使用 CyclicAction 和 TaskBarrier 的一个典型场景。图 2. 使用 CyclicAction 和 TaskBarrier 执行多线程任务继承自 CyclicAction 的子类需要 TaskBarrier 为每个任务设置不同的中止条件。从 CyclicAction 继承的子类需要重载 protected void compute() 方法，定义在 barrier 的每个步骤需要执行的动作。compute() 方法将被反复执行直到 barrier 的 isTerminated() 方法返回 True。TaskBarrier 的行为类似于 CyclicBarrier。下面，我们来看看如何使用 CyclicAction 的子类：清单 6. 使用 CyclicAction 的子类class ConcurrentPrint extends RecursiveAction { protected void compute() { TaskBarrier b = new TaskBarrier() { protected boolean terminate(int cycle, int registeredParties) { System.out.println("Cycle is " + cycle + ";" + registeredParties + " parties"); return cycle >= 10; } }; int n = 3; CyclicAction[] actions = new CyclicAction[n]; for (int i = 0; i < n; ++i) { final int index = i; actions[i] = new CyclicAction(b) { protected void compute() { System.out.println("I'm working " + getCycle() + " " + index); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }; } for (int i = 0; i < n; ++i) actions[i].fork(); for (int i = 0; i < n; ++i) actions[i].join(); } }在清单 6 中，CyclicAction[] 数组建立了三个任务，打印各自的工作次数和序号。而在 b.terminate() 方法中，我们设置的中止条件表示重复 10 次计算后中止。现在剩下的工作就是将 ConcurrentPrint 提交到 ForkJoinPool 了。我们可以在 ForkJoinPool 的构造函数中指定需要的线程数目，例如 ForkJoinPool(4) 就表明线程池包含 4 个线程。我们在一个 JUnit 的 test 方法中运行 ConcurrentPrint 的这个循环任务：清单 7. 运行 ConcurrentPrint 循环任务@Test public void testBarrier() throws InterruptedException, ExecutionException { ForkJoinTask fjt = new ConcurrentPrint(); ForkJoinPool fjpool = new ForkJoinPool(4); fjpool.submit(fjt); fjpool.shutdown(); }RecursiveTask 和 CyclicAction 两个例子的完整代码如下所示：清单 8. RecursiveTask 和 CyclicAction 两个例子的完整代码package tests; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import jsr166y.forkjoin.CyclicAction; import jsr166y.forkjoin.ForkJoinPool; import jsr166y.forkjoin.ForkJoinTask; import jsr166y.forkjoin.RecursiveAction; import jsr166y.forkjoin.RecursiveTask; import jsr166y.forkjoin.TaskBarrier; import org.junit.Test; class Fibonacci extends RecursiveTask<Integer> { final int n; Fibonacci(int n) { this.n = n; } private int compute(int small) { final int[] results = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; return results[small]; } public Integer compute() { if (n <= 10) { return compute(n); } Fibonacci f1 = new Fibonacci(n - 1); Fibonacci f2 = new Fibonacci(n - 2); System.out.println("fork new thread for " + (n - 1)); f1.fork(); System.out.println("fork new thread for " + (n - 2)); f2.fork(); return f1.join() + f2.join(); } } class ConcurrentPrint extends RecursiveAction { protected void compute() { TaskBarrier b = new TaskBarrier() { protected boolean terminate(int cycle, int registeredParties) { System.out.println("Cycle is " + cycle + ";" + registeredParties + " parties"); return cycle >= 10; } }; int n = 3; CyclicAction[] actions = new CyclicAction[n]; for (int i = 0; i < n; ++i) { final int index = i; actions[i] = new CyclicAction(b) { protected void compute() { System.out.println("I'm working " + getCycle() + " " + index); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }; } for (int i = 0; i < n; ++i) actions[i].fork(); for (int i = 0; i < n; ++i) actions[i].join(); } } public class TestForkJoin { @Test public void testBarrier() throws InterruptedException, ExecutionException { System.out.println("\ntesting Task Barrier ..."); ForkJoinTask fjt = new ConcurrentPrint(); ForkJoinPool fjpool = new ForkJoinPool(4); fjpool.submit(fjt); fjpool.shutdown(); } @Test public void testFibonacci() throws InterruptedException, ExecutionException { System.out.println("\ntesting Fibonacci ..."); final int num = 14; // For demo only ForkJoinTask<Integer> fjt = new Fibonacci(num); ForkJoinPool fjpool = new ForkJoinPool(); Future<Integer> result = fjpool.submit(fjt); // do something System.out.println("Fibonacci(" + num + ") = " + result.get()); } }运行以上代码，我们可以得到以下结果：testing Task Barrier ... I'm working 0 2 I'm working 0 0 I'm working 0 1 Cycle is 0; 3 parties I'm working 1 2 I'm working 1 0 I'm working 1 1 Cycle is 1; 3 parties I'm working 2 0 I'm working 2 1 I'm working 2 2 Cycle is 2; 3 parties I'm working 3 0 I'm working 3 2 I'm working 3 1 Cycle is 3; 3 parties I'm working 4 2 I'm working 4 0 I'm working 4 1 Cycle is 4; 3 parties I'm working 5 1 I'm working 5 0 I'm working 5 2 Cycle is 5; 3 parties I'm working 6 0 I'm working 6 2 I'm working 6 1 Cycle is 6; 3 parties I'm working 7 2 I'm working 7 0 I'm working 7 1 Cycle is 7; 3 parties I'm working 8 1 I'm working 8 0 I'm working 8 2 Cycle is 8; 3 parties I'm working 9 0 I'm working 9 2 testing Fibonacci ... fork new thread for 13 fork new thread for 12 fork new thread for 11 fork new thread for 10 fork new thread for 12 fork new thread for 11 fork new thread for 10 fork new thread for 9 fork new thread for 10 fork new thread for 9 fork new thread for 11 fork new thread for 10 fork new thread for 10 fork new thread for 9 Fibonacci(14) = 610结论从以上的例子中可以看到，通过使用 Fork/Join 模式，软件开发人员能够方便地利用多核平台的计算能力。尽管还没有做到对软件开发人员完全透明，Fork/Join 模式已经极大地简化了编写并发程序的琐碎工作。对于符合 Fork/Join 模式的应用，软件开发人员不再需要处理各种并行相关事务，例如同步、通信等，以难以调试而闻名的死锁和 data race 等错误也就不会出现，提升了思考问题的层次。你可以把 Fork/Join 模式看作并行版本的 Divide and Conquer（分治）策略，仅仅关注如何划分任务和组合中间结果，将剩下的事情丢给 Fork/Join 框架。在实际工作中利用 Fork/Join 模式，可以充分享受多核平台为应用带来的免费午餐。说明版本时效性说明：本文基于 JDK 7 发布前的 JSR-166y 实验性版本编写。在正式发布的 Java 7 及后续版本中，Fork/Join 框架已纳入标准库 java.util.concurrent.forkjoin 包（Java 8 后部分类有所调整）。文中提到的 CyclicAction、TaskBarrier 及 coInvoke 等方法属于早期实验性 API，并未全部保留在标准 JDK 中。现代开发建议直接使用 java.util.concurrent.ForkJoinPool、RecursiveAction 和 RecursiveTask 标准类。相关主题阅读文章"The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software"：了解为什么从现在开始每个严肃的软件工作者都应该了解并行编程方法。阅读 Doug Lea 的文章"A Java Fork/Join Framework"：了解 Fork/Join 模式的实现机制和执行性能。阅读 developerWorks 文章"驯服 Tiger：并发集合"：了解如何使用并行 Collection 库。阅读 developerWorks 文章"Java 理论与实践：非阻塞算法简介"：介绍了 JDK 5 在并行方面的重要增强以及在 JDK 5 平台上如何实现非阻塞算法的一般介绍。书籍"Java Concurrency in Practice"：介绍了大量的并行编程技巧、反模式、可行的解决方案等，它对于 JDK 5 中的新特性也有详尽的介绍。访问 Doug Lea 的 JSR 166 站点获得最新的源代码。从 Sun 公司 网站下载 Java SE 6。
监视器Java 中的同步是通过监视器（Monitor）模型来实现的。Java 中的监视器实质是一个代码块，这段代码块在同一时刻只允许被一个线程执行。线程要想执行这段代码块，唯一的方式是获得监视器。监视器支持两种同步方式：互斥与协作。互斥：多线程环境下，如果线程之间需要共享数据，必须解决互斥访问的问题。监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。协作：某些场景需要线程间协作。例如，一个线程向缓冲区写数据，另一个线程从缓冲区读数据。如果读线程发现缓冲区为空就会等待，当写线程向缓冲区写入数据后，会唤醒读线程。这里读线程和写线程就是一种合作关系。JVM 通过 Object 类的 wait 方法来使线程等待。在调用 wait 方法后，该线程会释放它持有的监视器，直到其他线程通知它才有执行的机会。一个线程调用 notify 方法通知正在等待的线程，但被唤醒的线程并不会马上执行，而是要等到通知线程释放监视器后，它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占，那么这个线程会继续等待。Object 类中的 notifyAll 方法可以解决这个问题，它可以唤醒所有等待的线程，总有一个线程能获得执行机会。 如上图所示，一个线程通过 1 号门进入 Entry Set（入口区）。如果在入口区没有线程等待，那么这个线程就会获取监视器成为监视器的 Owner，然后执行监视区域的代码。如果在入口区中有其它线程在等待，那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中，有两个选择：正常执行监视器区域的代码，释放监视器，通过 5 号门退出监视器。等待某个条件的出现，于是它会通过 3 号门到 Wait Set（等待区） 休息，直到相应的条件满足后再通过 4 号门进入，重新获取监视器再执行。注意：当一个线程释放监视器时，在入口区和等待区的等待线程都会去竞争监视器。如果入口区的线程赢了，会从 2 号门进入；如果等待区的线程赢了，会从 4 号门进入。只有通过 3 号门才能进入等待区，在等待区中的线程只有通过 4 门才能退出等待区。也就是说，一个线程只有在持有监视器时才能执行 wait 操作，处于等待的线程只有再次获得监视器才能退出等待状态。对象锁JVM 中的一些数据，比如堆和方法区，会被所有线程共享。Java 中每个对象和类实际上都有一把锁与之相关联：对于对象来说，监视的是这个对象的实例变量。对于类来说，监视的是类变量。如果一个对象没有实例变量，就什么也不监视。当虚拟机装载类时，会创建一个 Class 类的实例，锁住一个类实际上锁住的是这个类对应的 Class 类的实例。对象锁是可重入的，也就是说对一个对象或者类上的锁可以累加。在 Java 中有两种监视区域：同步方法和同步块。这两种监视区域都和一个引用对象相关联。当到达这个监视区域时，JVM 就会锁住这个引用对象；不论它是怎么离开的，都会释放这个引用对象上的锁。Java 程序员不能自己手动加对象锁，对象锁是 JVM 内部机制，只需要编写同步方法或者同步块即可，操作监视区域时 JVM 会自动帮你上锁或者释放锁。同步语句要建立一个同步语句，只需要在相关语句加上 synchronized 关键字就可以。例如下面的 incr 方法，如果没有获得当前对象（this）的锁，在同步块内的语句是不会执行的。如果不是 this 引用，而是用另一个对象的引用，需要获得对应对象的锁同步块才会执行；如果用表达式获得对 Class 对象实例的引用，就需要锁住那个类。void incr() { synchronized (this) { i++; } }以下是 incr 方法生成的字节码序列： void incr(); Code: 0: aload_0 // 将 this 引用压栈 1: dup // 复制栈顶元素 2: astore_1 // 出栈并将 this 引用存放在局部变量 1 中 3: monitorenter // 出栈并获取对象锁 4: aload_0 // 将 this 引用压栈 5: dup // 复制栈顶元素 6: getfield #17 // 获取 i 的值 9: iconst_1 // 常数 1 入栈 10: iadd // 将 i+1 的结果入栈 11: putfield #17 // 将 i 的值存入 this 中 14: aload_1 // 将 this 引用压栈 15: monitorexit // 弹出 this 引用释放对象锁 16: goto 22 // 返回 19: aload_1 // 19-22 如果抛出，释放对象锁 20: monitorexit 21: athrow 22: return Exception table: from to target type 4 16 19 any 19 21 19 any字节码的第 3 行从栈顶中获取对象锁，对象锁获取成功后才执行后面的 add 操作，第 15 行释放获取的对象锁。注意：字节码中出现了异常表，是用于确保加锁的对象被释放。即使从同步语句块中抛出异常，也会释放对象锁，否则有可能导致死锁。同步方法要建立同步方法，只需要在方法修饰符前加上 synchronized 关键字，类似代码如下：synchronized void incr() { i++; }生成的字节码序列如下：synchronized void incr(); Code: 0: aload_0 // this 引用压栈 1: dup // 复制栈顶元素 2: getfield #2 // 获取 i 的值 5: iconst_1 // 将常量 1 入栈 6: iadd // i+1 入栈 7: putfield #2 // 将 i 的值存入 this 中 10: return // 返回可见，JVM 并没有使用 monitorenter 和 monitorexit 等指令。查看 class 文件在方法表中可以看到有 0020 出现，这是 incr 方法的访问标志（access flag）：ACC_SYNCHRONIZED。顾名思义，这说明 incr 是一个线程同步方法。当 JVM 发现这是一个同步方法时，就会在这个对象或者类上获取锁，退出方法时会释放这个锁。两段字节码除了调用指令不同，还有一个区别是同步方法可以没有异常表，实际上 JVM 隐式地做了异常处理。优缺点synchronized 是通过软件（JVM）实现的，简单易用。即使在 JDK 5 之后有了 Lock，synchronized 仍然被广泛地使用。优势简单易用：无需手动管理锁的获取与释放。防止饥饿：synchronized 实际上是非公平的，新来的线程有可能立即获得监视器，而在等待区中等候已久的线程可能再次等待。不过这种抢占的方式在一定程度上可以预防饥饿。局限性灵活性不足：synchronized 只有锁只与一个条件（是否获取锁）相关联。后来 Condition 与 Lock 的结合解决了这个问题。无法响应中断：多线程竞争一个锁时，其余未得到锁的线程只能不停地尝试获得锁，而不能中断。高并发的情况下会导致性能下降。ReentrantLock 的 lockInterruptibly() 方法可以优先考虑响应中断。一个线程等待时间过长，它可以中断自己，然后 ReentrantLock 响应这个中断，不再让这个线程继续等待。有了这个机制，使用 ReentrantLock 时就不会像 synchronized 那样容易产生死锁。参考资料Java 多线程总结之由 synchronized 说开去深入 JVM 锁机制 1-synchronized深入 JVM 锁机制 2-LockThe JavaTM Virtual Machine SpecificationInside the Java Virtual Machine说明：本文内容基于 JDK 5/6 时期的锁机制原理整理。自 JDK 6 起，synchronized 引入了偏向锁、轻量级锁等优化机制，性能已有显著提升，但底层监视器模型基本原理保持一致。
两阶段终止模式 (Two-Phase Termination Pattern)两阶段终止模式 (Two-Phase Termination Pattern) 指的是当希望结束一个线程时，发出终止请求后线程不会立即停止，而是先完成当前的刷新或清理工作。线程进入“终止处理中”状态，在该状态下不再执行日常工作任务，而是专注于执行终止相关的操作。设计考量采用该模式主要基于以下三个关键因素：安全结束线程必须确保线程能够安全地结束。直接使用 Thread.stop() 方法存在风险，因为它会强制停止线程，无论线程执行到哪一行代码，都无法保证资源释放或数据一致性，可能导致对象处于不一致状态。正常的终止处理必须确保能够进行正常的收尾工作。在 Java 中，这一点通常可以使用 finally 代码块来实现，确保无论何种情况下退出循环，清理逻辑都能被执行。高响应性的终止收到终止请求后，如果线程正处于 wait、sleep 或 join 等阻塞状态，不应等到超时才终止，而应立即中断这些状态，进而执行终止操作。工作流程当一个线程正在执行周期性工作时，若在“作业阶段”收到了停止请求，该线程不应立即离开，而应先完成本次周期内部的工作，然后进入“善后阶段”完成清理工作。所谓的两阶段终止，即中止“运作阶段”，并完成“善后阶段”，从而完整地结束线程的工作生命周期。代码实现两阶段终止线程的架构模式如下：public class WorkerTerminalThread extends Thread { // 已经送出终止请求为 true，初始化的时候为 false // 由于该字段可能会被多个线程访问修改，为了保护就使用 volatile private volatile boolean shutdownRequested = false; // 终止请求 public void shutdownRequest() { shutdownRequested = true; interrupt(); } public boolean isShutdownRequest() { return shutdownRequested; } // 具体动作 public final void run() { try { while (!shutdownRequested) { doWork(); } } catch (InterruptedException e) { // 捕获中断异常，配合标志位退出循环 } finally { // 终止处理中的工作，不会进行平常操作，但是会进行终止处理 doShutdown(); } } // 具体工作操作 private void doWork() throws InterruptedException { // 模拟周期性工作 } // 终止后进行善后处理 private void doShutdown() { // 清理资源 } }原理解析Volatile 的作用利用 volatile 修饰 shutdownRequested 字段，是因为该字段可能会被多个线程使用及修改。为了保证该字段的可见性，可以使用同步方法、同步代码块或 volatile 关键字。使用 volatile 修饰的字段，强制该成员变量在每次被线程访问时，都从共享内存中重读该成员变量的值；而且当成员变量发生变化时，强迫线程将变化值回写到共享内存。这样在任何时刻，两个不同的线程总是看到某个成员变量的同一个值。标识位与中断状态的结合这里运用了“标识位”和“中断状态”组合的方式来终止线程，之所以不单独使用其中一种，原因如下：仅利用标识位：无法使那些处于 wait、sleep 或者 join 中的线程马上停止，响应性会很差。仅利用中断 (interrupt)：interrupt 仅仅对于 wait、sleep 或 join 处抛出异常。如果工作代码执行在 catch 里捕获了 InterruptedException 后未做处理，则此时中断标志可能被清除，导致中断不起作用。组合使用：加入 interrupt 后可以立刻使得处于阻塞状态的线程中断并抛出异常；配合 shutdownRequested 标识位，确保即使在异常被捕获的情况下，也能通过检查标志位正常退出循环。说明：本文示例基于经典 Java 多线程模型编写。在现代 Java 并发编程中，更推荐使用 ExecutorService 配合 Runnable 或 Callable 任务，并通过 shutdown() 或 Future.cancel() 来管理线程生命周期，而非直接继承 Thread 类。但两阶段终止的核心思想（请求 + 清理）依然适用。
编者注：本文为历史博文归档，内容涉及 JDK、框架与工具链版本请以当前官方文档为准。文中引用外链图片可能失效，阅读时请注意时效性。1. JUC 概况以下是 Java JUC（java.util.concurrent）包的主体结构：Atomic：AtomicInteger 等原子类Locks：Lock, Condition, ReadWriteLockCollections：Queue, ConcurrentMap 等并发集合Executors 框架：Future, Callable, ExecutorTools：CountDownLatch, CyclicBarrier, Semaphore2. 原子操作当多个线程执行一个操作时，如果其中任何一个线程要么完全执行完此操作，要么没有执行此操作的任何步骤，那么这个操作就是原子的（Atomic）。引入原子操作的主要原因是为了降低 synchronized 带来的性能开销。以下以 AtomicInteger 为例，介绍其常用方法：int addAndGet(int delta)：以原子方式将给定值与当前值相加。实际上等于线程安全版本的 i = i + delta 操作。boolean compareAndSet(int expect, int update)：如果当前值 == 预期值，则以原子方式将该值设置为给定的更新值。如果成功返回 true，否则返回 false 且不修改原值。int decrementAndGet()：以原子方式将当前值减 1。相当于线程安全版本的 --i 操作。int getAndAdd(int delta)：以原子方式将给定值与当前值相加。相当于线程安全版本的 t=i; i+=delta; return t; 操作。int getAndDecrement()：以原子方式将当前值减 1。相当于线程安全版本的 i-- 操作。int getAndIncrement()：以原子方式将当前值加 1。相当于线程安全版本的 i++ 操作。int getAndSet(int newValue)：以原子方式设置为给定值，并返回旧值。相当于线程安全版本的 t=i; i=newValue; return t; 操作。int incrementAndGet()：以原子方式将当前值加 1。相当于线程安全版本的 ++i 操作。3. 指令重排程序的执行顺序并不能总是保证符合代码编写的顺序，CPU 为了优化性能可能会进行指令重排。要保证程序的最终结果等同于它在严格的顺序化环境下的结果，指令的执行顺序就可能与代码的顺序不一致。在多核 CPU 高压力下，两个线程交替执行，x，y 的输出结果不确定。可能的结果如下：序号可能结果1x = 0，y = 12x = 1，y = 13x = 1，y = 04x = 0，y = 04. Happens-before 法则（Java 内存模型）如果动作 B 要看到动作 A 的执行结果（无论 A/B 是否在同一个线程里面执行），那么 A/B 就需要满足 happens-before 关系。Happens-before 规则Program order rule：同一个线程中的每个 Action 都 happens-before 于出现在其后的任何一个 Action。Monitor lock rule：对一个监视器的解锁 happens-before 于每一个后续对同一个监视器的加锁。Volatile variable rule：对 volatile 字段的写入操作 happens-before 于每一个后续的同一个字段的读操作。Thread start rule：Thread.start() 的调用会 happens-before 于启动线程里面的动作。Thread termination rule：Thread 中的所有动作都 happens-before 于其他线程检查到此线程结束，或者 Thread.join() 中返回，或者 Thread.isAlive() == false。Interruption rule：一个线程 A 调用另一个线程 B 的 interrupt() 都 happens-before 于线程 A 发现 B 被 A 中断（B 抛出异常或者 A 检测到 B 的 isInterrupted() 或者 interrupted()）。Finalizer rule：一个对象构造函数的结束 happens-before 与该对象的 finalizer 的开始。Transitivity：如果 A 动作 happens-before 于 B 动作，而 B 动作 happens-before 于 C 动作，那么 A 动作 happens-before 于 C 动作。因为 CPU 可以不按我们写代码的顺序执行内存的存取过程（即指令乱序或并行运行），只有上面的 happens-before 所规定的情况下，才保证顺序性。JMM 的特性多个 CPU 之间的缓存也不保证实时同步。JMM 不保证创建过程的原子性，读写并发时，可能看到不完整的对象（因此涉及双重检查锁定 DCL 时需注意）。volatile 语义volatile 实现了类似 synchronized 的语义，却又没有锁机制。它确保对 volatile 字段的更新以可预见的方式告知其他的线程。禁止重排序：Java 存储模型不会对 volatile 指令的操作进行重排序，保证对 volatile 变量的操作是按照指令的出现顺序执行的。内存可见性：volatile 变量不会被缓存在寄存器中（只拥有线程可见性），每次总是从主存中读取 volatile 变量的结果。注意：volatile 并不能保证线程安全，也就是说 volatile 字段的操作不是原子性的，volatile 变量只能保证可见性。5. CAS 操作CAS (Compare and Swap) 是一种乐观锁机制。CAS 有 3 个操作数：内存值 V，旧的预期值 A，要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时，将内存值 V 修改为 B，否则什么都不做。实现简单的非阻塞算法示例：private volatile int value; // 借助 volatile 原语，保证线程间的数据是可见的 public final int get() { return value; } public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } // Spin 自旋等待直到返回为止 }整个 J.U.C 包都是建立在 CAS 之上的。对于 synchronized 阻塞算法，J.U.C 在性能上有了很大的提升。但 CAS 会出现所谓的 "ABA" 问题。6. Lock 锁Synchronized 属于独占锁，高并发时性能不高。JDK 5 以后开始用 JNI 实现更高效的锁操作。锁类层次结构：LockReentrantLockReentrantReadWriteLockReadLockWriteLockLockSupportConditionLock 接口主要方法：方法名称作用void lock()获取锁。如果锁不可用，出于线程调度目的，将禁用当前线程，并且在获得锁之前，该线程将一直处于休眠状态。void lockInterruptibly() throws InterruptedException如果当前线程未被中断，则获取锁。如果锁可用，则获取锁，并立即返回。Condition newCondition()返回绑定到此 Lock 实例的新 Condition 实例。boolean tryLock()仅在调用时锁为空闲状态才获取该锁。boolean tryLock(long time, TimeUnit unit) throws InterruptedException如果锁在给定的等待时间内空闲，并且当前线程未被中断，则获取锁。void unlock()释放锁。最佳实践：一般来说，获取锁和释放锁是成对的操作，这样可以避免死锁和资源的浪费。务必在 finally 块中执行释放锁的操作。7. AQS (AbstractQueuedSynchronizer)AQS 是锁机制实现的核心所在，是 Lock/Executor 实现的前提。AQS 实现原理基本的思想是表现为一个同步器，AQS 支持下面两个核心操作：acquire (获取同步状态):while (synchronization state does not allow acquire) { enqueue current thread if not already queued; possibly block current thread; } dequeue current thread if it was queued;release (释放同步状态):update synchronization state; if (state may permit a blocked thread to acquire) unpark one or more queued threads;要支持这两个操作，需要实现三个条件：Atomically managing synchronization state：原子性操作同步器的状态位。Blocking and unblocking threads：阻塞和唤醒线程。Maintaining queues：维护一个有序的队列。1. 原子性地管理同步状态使用一个 32 位整数来描述状态位：private volatile int state; 对其进行 CAS 操作，确保值的正确性。2. 阻塞和唤醒线程JDK 5.0 以后利用 JNI 在 LockSupport 类中实现了线程的阻塞和唤醒。LockSupport.park()：在当前线程中调用，导致线程阻塞。LockSupport.park(Object blocker)LockSupport.unpark(Thread thread)3. 维护队列在 AQS 中采用 CLH 队列 来解决有序队列的问题（CLH = Craig, Landin, and Hagersten）。Node 里面是什么结构？WaitStatus –> 节点的等待状态，一个节点可能位于以下几种状态：CANCELLED = 1：节点操作因为超时或者对应的线程被 interrupt。节点不应该留在此状态，一旦达到此状态将从 CLH 队列中踢出。SIGNAL = -1：节点的继任节点是（或者将要成为）BLOCKED 状态（例如通过 LockSupport.park() 操作），因此一个节点一旦被释放（解锁）或者取消就需要唤醒（LockSupport.unpark()）它的继任节点。CONDITION = -2：表明节点对应的线程因为不满足一个条件（Condition）而被阻塞。0：正常状态，新生的非 CONDITION 节点都是此状态。非负值标识节点不需要被通知（唤醒）。队列管理操作：入队 (enqueue):采用 CAS 操作，每次比较尾结点是否一致，然后插入到尾结点中。do { pred = tail; } while (!compareAndSet(pred, tail, node));出队 (dequeue):while (pred.status != RELEASED); head = node;加锁操作：public final void acquire(int arg) { if (!tryAcquire(arg)) acquireQueued(addWaiter(Node.EXCLUSIVE), arg); selfInterrupt(); }释放操作：public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }"The synchronizer framework provides a ConditionObject class for use by synchronizers that maintain exclusive synchronization and conform to the Lock interface." —— Doug Lea《The java.util.concurrent Synchronizer Framework》以下是 AQS 队列和 Condition 队列的出入结点的示意图，可以通过这几张图看出线程结点在两个队列中的出入关系和条件。说明：本文内容基于 JDK 5/6 时期的技术背景整理，部分实现细节在后续 JDK 版本中可能有所优化或调整，具体请以最新官方文档为准。