📦 本文以及示例源码已归档在 javacorehtml
Java 的 java.util.concurrent
包(简称 J.U.C)中提供了大量并发工具类,是 Java 并发能力的主要体现(注意,不是所有,有部分并发能力的支持在其余包中)。从功能上,大体能够分为:java
AtomicInteger
、AtomicIntegerArray
、AtomicReference
、AtomicStampedReference
等。ReentrantLock
、ReentrantReadWriteLock
等。ConcurrentHashMap
、CopyOnWriteArrayList
、CopyOnWriteArraySet
等。ArrayBlockingQueue
、LinkedBlockingQueue
等。ConcurrentLinkedQueue
、LinkedTransferQueue
等。Executor
框架(线程池)- 如:ThreadPoolExecutor
、Executors
等。我我的理解,Java 并发框架能够分为如下层次。git
由 Java 并发框架图不难看出,J.U.C 包中的工具类是基于 synchronized
、volatile
、CAS
、ThreadLocal
这样的并发核心机制打造的。因此,要想深刻理解 J.U.C 工具类的特性、为何具备这样那样的特性,就必须先理解这些核心机制。github
synchronized
是 Java 中的关键字,是 利用锁的机制来实现互斥同步的。算法
synchronized
能够保证在同一个时刻,只有一个线程能够执行某个方法或者某个代码块。数据库若是不须要
Lock
、ReadWriteLock
所提供的高级同步特性,应该优先考虑使用synchronized
,理由以下:编程
- Java 1.6 之后,
synchronized
作了大量的优化,其性能已经与Lock
、ReadWriteLock
基本上持平。从趋势来看,Java 将来仍将继续优化synchronized
,而不是ReentrantLock
。ReentrantLock
是 Oracle JDK 的 API,在其余版本的 JDK 中不必定支持;而synchronized
是 JVM 的内置特性,全部 JDK 版本都提供支持。
synchronized
有 3 种应用方式:数组
Class
对象synchonized
括号里配置的对象说明:缓存
相似
Vector
、Hashtable
这类同步类,就是使用synchonized
修饰其重要方法,来保证其线程安全。安全事实上,这类同步容器也非绝对的线程安全,当执行迭代器遍历,根据条件删除元素这种场景下,就可能出现线程不安全的状况。此外,Java 1.6 针对
synchonized
进行优化前,因为阻塞,其性能不高。综上,这类同步容器,在现代 Java 程序中,已经渐渐不用了。
❌ 错误示例 - 未同步的示例
public class NoSynchronizedDemo implements Runnable { public static final int MAX = 100000; private static int count = 0; public static void main(String[] args) throws InterruptedException { NoSynchronizedDemo instance = new NoSynchronizedDemo(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < MAX; i++) { increase(); } } public void increase() { count++; } } // 输出结果: 小于 200000 的随机数字
Java 实例方法同步是同步在拥有该方法的对象上。这样,每一个实例其方法同步都同步在不一样的对象上,即该方法所属的实例。只有一个线程可以在实例方法同步块中运行。若是有多个实例存在,那么一个线程一次能够在一个实例同步块中执行操做。一个实例一个线程。
public class SynchronizedDemo implements Runnable { private static final int MAX = 100000; private static int count = 0; public static void main(String[] args) throws InterruptedException { SynchronizedDemo instance = new SynchronizedDemo(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < MAX; i++) { increase(); } } /** * synchronized 修饰普通方法 */ public synchronized void increase() { count++; } }
静态方法的同步是指同步在该方法所在的类对象上。由于在 JVM 中一个类只能对应一个类对象,因此同时只容许一个线程执行同一个类中的静态同步方法。
对于不一样类中的静态同步方法,一个线程能够执行每一个类中的静态同步方法而无需等待。无论类中的那个静态同步方法被调用,一个类只能由一个线程同时执行。
public class SynchronizedDemo2 implements Runnable { private static final int MAX = 100000; private static int count = 0; public static void main(String[] args) throws InterruptedException { SynchronizedDemo2 instance = new SynchronizedDemo2(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < MAX; i++) { increase(); } } /** * synchronized 修饰静态方法 */ public synchronized static void increase() { count++; } }
有时你不须要同步整个方法,而是同步方法中的一部分。Java 能够对方法的一部分进行同步。
注意 Java 同步块构造器用括号将对象括起来。在上例中,使用了 this
,即为调用 add 方法的实例自己。在同步构造器中用括号括起来的对象叫作监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法自己的实例做为监视器对象。
一次只有一个线程可以在同步于同一个监视器对象的 Java 方法内执行。
public class SynchronizedDemo3 implements Runnable { private static final int MAX = 100000; private static int count = 0; public static void main(String[] args) throws InterruptedException { SynchronizedDemo3 instance = new SynchronizedDemo3(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } @Override public void run() { for (int i = 0; i < MAX; i++) { increase(); } } /** * synchronized 修饰代码块 */ public static void increase() { synchronized (SynchronizedDemo3.class) { count++; } } }
synchronized
通过编译后,会在同步块的先后分别造成 monitorenter
和 monitorexit
这两个字节码指令,这两个字节码指令都须要一个引用类型的参数来指明要锁定和解锁的对象。若是 synchronized
明确制定了对象参数,那就是这个对象的引用;若是没有明确指定,那就根据 synchronized
修饰的是实例方法仍是静态方法,去对对应的对象实例或 Class 对象来做为锁对象。
synchronized
同步块对同一线程来讲是可重入的,不会出现锁死问题。
synchronized
同步块是互斥的,即已进入的线程执行完成前,会阻塞其余试图进入的线程。
锁具有如下两种特性:
monitor
对象,这个对象其实就是 Java 对象的锁,一般会被称为“内置锁”或“对象锁”。类的对象能够有多个,因此每一个对象有其独立的对象锁,互不干扰。Java 1.6 之后,
synchronized
作了大量的优化,其性能已经与Lock
、ReadWriteLock
基本上持平。
互斥同步进入阻塞状态的开销都很大,应该尽可能避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,若是在这段时间内能得到锁,就能够避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减小开销,可是它须要进行忙循环操做占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 Java 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数再也不固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是经过逃逸分析来支持,若是堆上的共享数据不可能逃逸出去被其它线程访问到,那么就能够把它们当成私有数据对待,也就能够将它们的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了不少锁。例以下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 以前,会转化为 StringBuffer 对象的连续 append() 操做:
public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }
每一个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态做用域被限制在 concatString() 方法内部。也就是说,sb 的全部引用永远不会逃逸到 concatString() 方法以外,其余线程没法访问到它,所以能够进行消除。
若是一系列的连续操做都对同一个对象反复加锁和解锁,频繁的加锁操做就会致使性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类状况。若是虚拟机探测到由这样的一串零碎的操做都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操做序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操做以前直至最后一个 append() 操做以后,这样只须要加锁一次就能够了。
Java 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
轻量级锁是相对于传统的重量级锁而言,它 使用 CAS 操做来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,所以也就不须要都使用互斥量进行同步,能够先采用 CAS 操做进行同步,若是 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,若是锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中建立 Lock Record,而后使用 CAS 操做将对象的 Mark Word 更新为 Lock Record 指针。若是 CAS 操做成功了,那么线程就获取了该对象上的锁,而且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在以后获取该锁就再也不须要进行同步操做,甚至连 CAS 操做也再也不须要。
volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。
可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰以后,那么就具有了两层语义:
若是一个字段被声明成 volatile,Java 线程内存模型确保全部线程看到这个变量的值是一致的。
若是 volatile
变量修饰符使用恰当的话,它比 synchronized
的使用和执行成本更低,由于它不会引发线程上下文的切换和调度。可是,volatile
没法替代 synchronized
,由于 volatile
没法保证操做的原子性。
一般来讲,使用 volatile
必须具有如下 2 个条件:
示例:状态标记量
volatile boolean flag = false; while(!flag) { doSomething(); } public void setFlag() { flag = true; }
示例:双重锁实现线程安全的单例类
class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile
关键字时,会多出一个 lock
前缀指令。
lock
前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
互斥同步是最多见的并发正确性保障手段。
互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题,所以互斥同步也被称为阻塞同步。互斥同步属于一种悲观的并发策略,老是认为只要不去作正确的同步措施,那就确定会出现问题。不管共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分没必要要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要唤醒等操做。
随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略:先进行操做,若是没有其它线程争用共享数据,那操做就成功了,不然采起补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不须要将线程阻塞,所以这种同步操做称为非阻塞同步。
为何说乐观锁须要 硬件指令集的发展 才能进行?由于须要操做和冲突检测这两个步骤具有原子性。而这点是由硬件来完成,若是再使用互斥同步来保证就失去意义了。硬件支持的原子性操做最典型的是:CAS。
CAS(Compare and Swap),字面意思为比较并交换。CAS 有 3 个操做数,分别是:内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改成 B,不然什么都不作。
Java 是如何实现 CAS ?
Java 主要利用 Unsafe
这个类提供的 CAS 操做。
Unsafe
的 CAS 依赖的是 JV M 针对不一样的操做系统实现的 Atomic::cmpxchg
指令。
Atomic::cmpxchg
的实现使用了汇编的 CAS 操做,并使用 CPU 提供的 lock
信号保证其原子性。
原子类是 CAS 在 Java 中最典型的应用。
咱们先来看一个常见的代码片断。
if(a==b) { a++; }
若是 a++
执行前, a 的值被修改了怎么办?还能获得预期值吗?出现该问题的缘由是在并发环境下,以上代码片断不是原子操做,随时可能被其余线程所篡改。
解决这种问题的最经典方式是应用原子类的 incrementAndGet
方法。
public class AtomicIntegerDemo { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(3); final AtomicInteger count = new AtomicInteger(0); for (int i = 0; i < 10; i++) { executorService.execute(new Runnable() { @Override public void run() { count.incrementAndGet(); } }); } executorService.shutdown(); executorService.awaitTermination(3, TimeUnit.SECONDS); System.out.println("Final Count is : " + count.get()); } }
J.U.C 包中提供了 AtomicBoolean
、AtomicInteger
、AtomicLong
分别针对 Boolean
、Integer
、Long
执行原子操做,操做和上面的示例大致类似,不作赘述。
利用原子类(本质上是 CAS),能够实现自旋锁。
所谓自旋锁,是指线程反复检查锁变量是否可用,直到成功为止。因为线程在这一过程当中保持执行,所以是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
示例:非线程安全示例
public class AtomicReferenceDemo { private static int ticket = 10; public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.execute(new MyThread()); } executorService.shutdown(); } static class MyThread implements Runnable { @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); ticket--; } } } }
输出结果:
pool-1-thread-2 卖出了第 10 张票 pool-1-thread-1 卖出了第 10 张票 pool-1-thread-3 卖出了第 10 张票 pool-1-thread-1 卖出了第 8 张票 pool-1-thread-2 卖出了第 9 张票 pool-1-thread-1 卖出了第 6 张票 pool-1-thread-3 卖出了第 7 张票 pool-1-thread-1 卖出了第 4 张票 pool-1-thread-2 卖出了第 5 张票 pool-1-thread-1 卖出了第 2 张票 pool-1-thread-3 卖出了第 3 张票 pool-1-thread-2 卖出了第 1 张票
很明显,出现了重复售票的状况。
示例:使用自旋锁来保证线程安全
能够经过自旋锁这种非阻塞同步来保证线程安全,下面使用 AtomicReference
来实现一个自旋锁。
public class AtomicReferenceDemo2 { private static int ticket = 10; public static void main(String[] args) { threadSafeDemo(); } private static void threadSafeDemo() { SpinLock lock = new SpinLock(); ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.execute(new MyThread(lock)); } executorService.shutdown(); } static class SpinLock { private AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); while (!atomicReference.compareAndSet(null, current)) {} } public void unlock() { Thread current = Thread.currentThread(); atomicReference.compareAndSet(current, null); } } static class MyThread implements Runnable { private SpinLock lock; public MyThread(SpinLock lock) { this.lock = lock; } @Override public void run() { while (ticket > 0) { lock.lock(); if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); ticket--; } lock.unlock(); } } } }
输出结果:
pool-1-thread-2 卖出了第 10 张票 pool-1-thread-1 卖出了第 9 张票 pool-1-thread-3 卖出了第 8 张票 pool-1-thread-2 卖出了第 7 张票 pool-1-thread-3 卖出了第 6 张票 pool-1-thread-1 卖出了第 5 张票 pool-1-thread-2 卖出了第 4 张票 pool-1-thread-1 卖出了第 3 张票 pool-1-thread-3 卖出了第 2 张票 pool-1-thread-1 卖出了第 1 张票
通常状况下,CAS 比锁性能更高。由于 CAS 是一种非阻塞算法,因此其避免了线程阻塞和唤醒的等待时间。
可是,CAS 也有一些问题。
若是一个变量初次读取的时候是 A 值,它的值被改为了 B,后来又被改回为 A,那 CAS 操做就会误认为它历来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference
来解决这个问题,它能够经过控制变量值的版原本保证 CAS 的正确性。大部分状况下 ABA 问题不会影响程序并发的正确性,若是须要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
自旋 CAS (不断尝试,直到成功为止)若是长时间不成功,会给 CPU 带来很是大的执行开销。
若是 JVM 能支持处理器提供的 pause
指令那么效率会有必定的提高,pause
指令有两个做用:
比较花费 CPU 资源,即便没有任何用也会作一些无用功。
当对一个共享变量执行操做时,咱们可使用循环 CAS 的方式来保证原子操做,可是对多个共享变量操做时,循环 CAS 就没法保证操做的原子性,这个时候就能够用锁。
或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量 i = 2, j = a
,合并一下 ij=2a
,而后用 CAS 来操做 ij
。从 Java 1.5 开始 JDK 提供了 AtomicReference
类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操做。
ThreadLocal
是一个存储线程本地副本的工具类。要保证线程安全,不必定非要进行同步。同步只是保证共享数据争用时的正确性,若是一个方法原本就不涉及共享数据,那么天然无须同步。
Java 中的 无同步方案 有:
- 可重入代码 - 也叫纯代码。若是一个方法,它的 返回结果是能够预测的,即只要输入了相同的数据,就能返回相同的结果,那它就知足可重入性,固然也是线程安全的。
- 线程本地存储 - 使用
ThreadLocal
为共享变量在每一个线程中都建立了一个本地副本,这个副本只能被当前线程访问,其余线程没法访问,那么天然是线程安全的。
ThreadLocal
的方法:
public class ThreadLocal<T> { public T get() {} public void set(T value) {} public void remove() {} public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {} }
说明:
get
- 用于获取ThreadLocal
在当前线程中保存的变量副本。set
- 用于设置当前线程中变量的副本。remove
- 用于删除当前线程中变量的副本。若是此线程局部变量随后被当前线程读取,则其值将经过调用其initialValue
方法从新初始化,除非其值由中间线程中的当前线程设置。 这可能会致使当前线程中屡次调用initialValue
方法。initialValue
- 为 ThreadLocal 设置默认的get
初始值,须要重写initialValue
方法 。
ThreadLocal
经常使用于防止对可变的单例(Singleton)变量或全局变量进行共享。典型应用场景有:管理数据库链接、Session。
示例 - 数据库链接
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
示例 - Session 管理
private static final ThreadLocal<Session> sessionHolder = new ThreadLocal<>(); public static Session getSession() { Session session = (Session) sessionHolder.get(); try { if (session == null) { session = createSession(); sessionHolder.set(session); } } catch (Exception e) { e.printStackTrace(); } return session; }
示例 - 完整使用示例
public class ThreadLocalDemo { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(new MyThread()); } executorService.shutdown(); } static class MyThread implements Runnable { @Override public void run() { int count = threadLocal.get(); for (int i = 0; i < 10; i++) { try { count++; Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } threadLocal.set(count); threadLocal.remove(); System.out.println(Thread.currentThread().getName() + " : " + count); } } }
所有输出 count = 10
Thread
类中维护着一个 ThreadLocal.ThreadLocalMap
类型的成员 threadLocals
。这个成员就是用来存储线程独占的变量副本。
ThreadLocalMap
是 ThreadLocal
的内部类,它维护着一个 Entry
数组, Entry
用于保存键值对,其 key 是 ThreadLocal
对象,value 是传递进来的对象(变量副本)。
ThreadLocalMap
虽然是相似 Map
结构的数据结构,但它并无实现 Map
接口。它不支持 Map
接口中的 next
方法,这意味着 ThreadLocalMap
中解决 Hash 冲突的方式并不是 拉链表 方式。
实际上,ThreadLocalMap
采用线性探测的方式来解决 Hash 冲突。所谓线性探测,就是根据初始 key 的 hashcode 值肯定元素在 table 数组中的位置,若是发现这个位置上已经被其余的 key 值占用,则利用固定的算法寻找必定步长的下个位置,依次判断,直至找到可以存放的位置。
ThreadLocalMap 的 Entry
继承了 WeakReference
,因此它的 key (ThreadLocal
对象)是弱引用,而 value (变量副本)是强引用。
ThreadLocal
对象没有外部强引用来引用它,那么 ThreadLocal
对象会在下次 GC 时被回收。Entry
中的 key 已经被回收,可是 value 因为是强引用不会被垃圾收集器回收。若是建立 ThreadLocal
的线程一直持续运行,那么 value 就会一直得不到回收,产生内存泄露。那么如何避免内存泄漏呢?方法就是:使用 ThreadLocal
的 set
方法后,显示的调用 remove
方法 。
ThreadLocal<String> threadLocal = new ThreadLocal(); try { threadLocal.set("xxx"); // ... } finally { threadLocal.remove(); }