1. 首页
  2. 未分类

深入理解 Java 并发核心机制

“u003Cblockquoteu003Eu003Cpu003Eu003Cstrongu003E 本文以及示例源码已归档在 javacoreu003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Ch2 class=”heading”u003E一、J.U.C 简介u003Cu002Fh2u003Eu003Cpu003EJava 的 java.util.concurrent 包(简称 J.U.C)中提供了大量并发工具类,是 Java 并发能力的主要体现(注意,不是全部,有部分并发能力的支持在其他包中)。从功能上,大致可以分为:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003E原子类 – 如:AtomicInteger、AtomicIntegerArray、AtomicReference、AtomicStampedReference 等。u003Cu002Fliu003Eu003Cliu003E锁 – 如:ReentrantLock、ReentrantReadWriteLock 等。u003Cu002Fliu003Eu003Cliu003E并发容器 – 如:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等。u003Cu002Fliu003Eu003Cliu003E阻塞队列 – 如:ArrayBlockingQueue、LinkedBlockingQueue 等。u003Cu002Fliu003Eu003Cliu003E非阻塞队列 – 如: ConcurrentLinkedQueue 、LinkedTransferQueue 等。u003Cu002Fliu003Eu003Cliu003EExecutor 框架(线程池)- 如:ThreadPoolExecutor、Executors 等。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Cpu003E我个人理解,Java 并发框架可以分为以下层次。u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp1.pstatp.comu002Flargeu002Fpgc-imageu002F8ea19a2a2efd4eeebd85cb11227d2ec0″ img_width=”1280″ img_height=”956″ alt=”深入理解 Java 并发核心机制” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E由 Java 并发框架图不难看出,J.U.C 包中的工具类是基于 synchronized、volatile、CAS、ThreadLocal 这样的并发核心机制打造的。所以,要想深入理解 J.U.C 工具类的特性、为什么具有这样那样的特性,就必须先理解这些核心机制。u003Cu002Fpu003Eu003Ch2 class=”heading”u003E二、synchronizedu003Cu002Fh2u003Eu003Cblockquoteu003Eu003Cpu003Esynchronized 是 Java 中的关键字,是 u003Cstrongu003E利用锁的机制来实现互斥同步的u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003Esynchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Cpu003E如果不需要 Lock 、ReadWriteLock 所提供的高级同步特性,应该优先考虑使用 synchronized ,理由如下:u003Cu002Fpu003Eu003Cpu003EJava 1.6 以后,synchronized 做了大量的优化,其性能已经与 Lock 、ReadWriteLock 基本上持平。从趋势来看,Java 未来仍将继续优化 synchronized ,而不是 ReentrantLock 。 ReentrantLock 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 synchronized 是 JVM 的内置特性,所有 JDK 版本都提供支持。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Ch3 class=”heading”u003Esynchronized 的用法u003Cu002Fh3u003Eu003Cpu003Esynchronized 有 3 种应用方式:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003Eu003Cstrongu003E同步实例方法u003Cu002Fstrongu003E – 对于普通同步方法,锁是当前实例对象u003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E同步静态方法u003Cu002Fstrongu003E – 对于静态同步方法,锁是当前类的 Class 对象u003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E同步代码块u003Cu002Fstrongu003E – 对于同步方法块,锁是 synchonized 括号里配置的对象u003Cu002Fliu003Eu003Cu002Fulu003Eu003Cblockquoteu003Eu003Cpu003E说明:u003Cu002Fpu003Eu003Cpu003E类似 Vector、Hashtable 这类同步类,就是使用 synchonized 修饰其重要方法,来保证其线程安全。u003Cu002Fpu003Eu003Cpu003E事实上,这类同步容器也非绝对的线程安全,当执行迭代器遍历,根据条件删除元素这种场景下,就可能出现线程不安全的情况。此外,Java 1.6 针对 synchonized 进行优化前,由于阻塞,其性能不高。u003Cu002Fpu003Eu003Cpu003E综上,这类同步容器,在现代 Java 程序中,已经渐渐不用了。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Ch4 class=”heading”u003E同步实例方法u003Cu002Fh4u003Eu003Cpu003E❌ 错误示例 – 未同步的示例u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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++; }}u002Fu002F 输出结果: 小于 200000 的随机数字u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003EJava 实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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(); } } u002F** * synchronized 修饰普通方法 *u002F public synchronized void increase() { count++; }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Ch4 class=”heading”u003E同步静态方法u003Cu002Fh4u003Eu003Cpu003E静态方法的同步是指同步在该方法所在的类对象上。因为在 JVM 中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。u003Cu002Fpu003Eu003Cpu003E对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那个静态同步方法被调用,一个类只能由一个线程同时执行。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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(); } } u002F** * synchronized 修饰静态方法 *u002F public synchronized static void increase() { count++; }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Ch4 class=”heading”u003E同步代码块u003Cu002Fh4u003Eu003Cpu003E有时你不需要同步整个方法,而是同步方法中的一部分。Java 可以对方法的一部分进行同步。u003Cu002Fpu003Eu003Cpu003E注意 Java 同步块构造器用括号将对象括起来。在上例中,使用了 this,即为调用 add 方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。u003Cu002Fpu003Eu003Cpu003E一次只有一个线程能够在同步于同一个监视器对象的 Java 方法内执行。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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(); } } u002F** * synchronized 修饰代码块 *u002F public static void increase() { synchronized (SynchronizedDemo3.class) { count++; } }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Ch3 class=”heading”u003Esynchronized 的原理u003Cu002Fh3u003Eu003Cpu003Esynchronized 经过编译后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。如果 synchronized 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 synchronized 修饰的是实例方法还是静态方法,去对对应的对象实例或 Class 对象来作为锁对象。u003Cu002Fpu003Eu003Cpu003Esynchronized 同步块对同一线程来说是可重入的,不会出现锁死问题。u003Cu002Fpu003Eu003Cpu003Esynchronized 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E锁的机制u003Cu002Fh4u003Eu003Cpu003E锁具备以下两种特性:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003Eu003Cstrongu003E互斥性u003Cu002Fstrongu003E:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。u003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E可见性u003Cu002Fstrongu003E:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Ch4 class=”heading”u003E锁类型u003Cu002Fh4u003Eu003Cul class=””u003Eu003Cliu003Eu003Cstrongu003E对象锁u003Cu002Fstrongu003E – 在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。u003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E类锁u003Cu002Fstrongu003E – 在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Ch3 class=”heading”u003Esynchronized 的优化u003Cu002Fh3u003Eu003Cblockquoteu003Eu003Cpu003EJava 1.6 以后,synchronized 做了大量的优化,其性能已经与 Lock 、ReadWriteLock 基本上持平。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Ch4 class=”heading”u003E自旋锁u003Cu002Fh4u003Eu003Cpu003E互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。u003Cu002Fpu003Eu003Cpu003E自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。u003Cu002Fpu003Eu003Cpu003E在 Java 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E锁消除u003Cu002Fh4u003Eu003Cpu003Eu003Cstrongu003E锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Cpu003E锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。u003Cu002Fpu003Eu003Cpu003E对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3;}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003EString 是一个不可变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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();}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E锁粗化u003Cu002Fh4u003Eu003Cpu003E如果u003Cstrongu003E一系列的连续操作都对同一个对象反复加锁和解锁u003Cu002Fstrongu003E,频繁的加锁操作就会导致性能损耗。u003Cu002Fpu003Eu003Cpu003E上一节的示例代码中连续的 append() 方法就属于这类情况。如果u003Cstrongu003E虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部u003Cu002Fstrongu003E。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E轻量级锁u003Cu002Fh4u003Eu003Cpu003EJava 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003Eu003Cstrongu003E无锁状态(unlocked)u003Cu002Fstrongu003Eu003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E偏向锁状态(biasble)u003Cu002Fstrongu003Eu003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E轻量级锁状态(lightweight locked)u003Cu002Fstrongu003Eu003Cu002Fliu003Eu003Cliu003Eu003Cstrongu003E重量级锁状态(inflated)u003Cu002Fstrongu003Eu003Cu002Fliu003Eu003Cu002Fulu003Eu003Cpu003Eu003Cstrongu003E轻量级锁u003Cu002Fstrongu003E是相对于传统的重量级锁而言,它 u003Cstrongu003E使用 CAS 操作来避免重量级锁使用互斥量的开销u003Cu002Fstrongu003E。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。u003Cu002Fpu003Eu003Cpu003E当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E偏向锁u003Cu002Fh4u003Eu003Cpu003E偏向锁的思想是偏向于u003Cstrongu003E让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Ch2 class=”heading”u003E三、volatileu003Cu002Fh2u003Eu003Ch3 class=”heading”u003Evolatile 的要点u003Cu002Fh3u003Eu003Cpu003Evolatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。u003Cu002Fpu003Eu003Cpu003E可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。u003Cu002Fpu003Eu003Cpu003E一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:u003Cu002Fpu003Eu003Col start=”1″u003Eu003Cliu003E保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。u003Cu002Fliu003Eu003Cliu003E禁止进行指令重排序。u003Cu002Fliu003Eu003Cu002Folu003Eu003Cpu003E如果一个字段被声明成 volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的。u003Cu002Fpu003Eu003Ch3 class=”heading”u003Evolatile 的用法u003Cu002Fh3u003Eu003Cpu003E如果 volatile 变量修饰符使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。但是,volatile 无法替代 synchronized ,因为 volatile 无法保证操作的原子性。u003Cu002Fpu003Eu003Cpu003E通常来说,u003Cstrongu003E使用 volatile 必须具备以下 2 个条件u003Cu002Fstrongu003E:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003E对变量的写操作不依赖于当前值u003Cu002Fliu003Eu003Cliu003E该变量没有包含在具有其他变量的不变式中u003Cu002Fliu003Eu003Cu002Fulu003Eu003Cpu003E示例:状态标记量u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Evolatile boolean flag = false;while(!flag) { doSomething();}public void setFlag() { flag = true;}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E示例:双重锁实现线程安全的单例类u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Eclass 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; }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Ch3 class=”heading”u003Evolatile 的原理u003Cu002Fh3u003Eu003Cpu003E观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,u003Cstrongu003E加入 volatile 关键字时,会多出一个 lock 前缀指令u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003Elock 前缀指令实际上相当于一个内存屏障u003Cu002Fstrongu003E(也成内存栅栏),内存屏障会提供 3 个功能:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003E它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;u003Cu002Fliu003Eu003Cliu003E它会强制将对缓存的修改操作立即写入主存;u003Cu002Fliu003Eu003Cliu003E如果是写操作,它会导致其他 CPU 中对应的缓存行无效。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Ch2 class=”heading”u003E四、CASu003Cu002Fh2u003Eu003Ch3 class=”heading”u003ECAS 的要点u003Cu002Fh3u003Eu003Cpu003E互斥同步是最常见的并发正确性保障手段。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题u003Cu002Fstrongu003E,因此互斥同步也被称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。u003Cu002Fpu003Eu003Cpu003E随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。u003Cu002Fpu003Eu003Cpu003E为什么说乐观锁需要 u003Cstrongu003E硬件指令集的发展u003Cu002Fstrongu003E 才能进行?因为需要操作和冲突检测这两个步骤具备原子性。而这点是由硬件来完成,如果再使用互斥同步来保证就失去意义了。硬件支持的原子性操作最典型的是:CAS。u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003ECAS(Compare and Swap)u003Cu002Fstrongu003E,字面意思为u003Cstrongu003E比较并交换u003Cu002Fstrongu003E。CAS 有 3 个操作数,分别是:内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。u003Cu002Fpu003Eu003Ch3 class=”heading”u003ECAS 的原理u003Cu002Fh3u003Eu003Cpu003EJava 是如何实现 CAS ?u003Cu002Fpu003Eu003Cpu003EJava 主要利用 Unsafe 这个类提供的 CAS 操作。u003Cu002Fpu003Eu003Cpu003EUnsafe 的 CAS 依赖的是 JV M 针对不同的操作系统实现的 Atomic::cmpxchg 指令。u003Cu002Fpu003Eu003Cpu003EAtomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 CPU 提供的 lock 信号保证其原子性。u003Cu002Fpu003Eu003Ch3 class=”heading”u003ECAS 的应用u003Cu002Fh3u003Eu003Ch4 class=”heading”u003E原子类u003Cu002Fh4u003Eu003Cblockquoteu003Eu003Cpu003E原子类是 CAS 在 Java 中最典型的应用。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Cpu003E我们先来看一个常见的代码片段。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Eif(a==b) { a++;}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E如果 a++ 执行前, a 的值被修改了怎么办?还能得到预期值吗?出现该问题的原因是在并发环境下,以上代码片段不是原子操作,随时可能被其他线程所篡改。u003Cu002Fpu003Eu003Cpu003E解决这种问题的最经典方式是应用原子类的 incrementAndGet 方法。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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()); }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003EJ.U.C 包中提供了 AtomicBoolean、AtomicInteger、AtomicLong 分别针对 Boolean、Integer、Long 执行原子操作,操作和上面的示例大体相似,不做赘述。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E自旋锁u003Cu002Fh4u003Eu003Cpu003E利用原子类(本质上是 CAS),可以实现自旋锁。u003Cu002Fpu003Eu003Cpu003E所谓自旋锁,是指线程反复检查锁变量是否可用,直到成功为止。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。u003Cu002Fpu003Eu003Cpu003E示例:非线程安全示例u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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–; } } }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E输出结果:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epool-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 张票u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E很明显,出现了重复售票的情况。u003Cu002Fpu003Eu003Cpu003E示例:使用自旋锁来保证线程安全u003Cu002Fpu003Eu003Cpu003E可以通过自旋锁这种非阻塞同步来保证线程安全,下面使用 AtomicReference 来实现一个自旋锁。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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(); } } }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E输出结果:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epool-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 张票u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Ch3 class=”heading”u003ECAS 的问题u003Cu002Fh3u003Eu003Cpu003E一般情况下,CAS 比锁性能更高。因为 CAS 是一种非阻塞算法,所以其避免了线程阻塞和唤醒的等待时间。u003Cu002Fpu003Eu003Cpu003E但是,CAS 也有一些问题。u003Cu002Fpu003Eu003Ch4 class=”heading”u003EABA 问题u003Cu002Fh4u003Eu003Cpu003E如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。u003Cu002Fpu003Eu003Cpu003EJ.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E循环时间长开销大u003Cu002Fh4u003Eu003Cpu003E自旋 CAS (不断尝试,直到成功为止)如果长时间不成功,会给 CPU 带来非常大的执行开销。u003Cu002Fpu003Eu003Cpu003E如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003E它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。u003Cu002Fliu003Eu003Cliu003E它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Cpu003E比较花费 CPU 资源,即使没有任何用也会做一些无用功。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E只能保证一个共享变量的原子性u003Cu002Fh4u003Eu003Cpu003E当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。u003Cu002Fpu003Eu003Cpu003E或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i = 2, j = a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。u003Cu002Fpu003Eu003Ch2 class=”heading”u003E五、ThreadLocalu003Cu002Fh2u003Eu003Cblockquoteu003Eu003Cpu003Eu003Cstrongu003EThreadLocal 是一个存储线程本地副本的工具类u003Cu002Fstrongu003E。u003Cu002Fpu003Eu003Cpu003E要保证线程安全,不一定非要进行同步。同步只是保证共享数据争用时的正确性,如果一个方法本来就不涉及共享数据,那么自然无须同步。u003Cu002Fpu003Eu003Cpu003EJava 中的 u003Cstrongu003E无同步方案u003Cu002Fstrongu003E 有:u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E可重入代码u003Cu002Fstrongu003E – 也叫纯代码。如果一个方法,它的 u003Cstrongu003E返回结果是可以预测的u003Cu002Fstrongu003E,即只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性,当然也是线程安全的。 u003Cstrongu003E线程本地存储u003Cu002Fstrongu003E – 使用 u003Cstrongu003EThreadLocal 为共享变量在每个线程中都创建了一个本地副本u003Cu002Fstrongu003E,这个副本只能被当前线程访问,其他线程无法访问,那么自然是线程安全的。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Ch3 class=”heading”u003EThreadLocal 的用法u003Cu002Fh3u003Eu003Cpu003EThreadLocal 的方法:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic class ThreadLocal<T> { public T get() {} public void set(T value) {} public void remove() {} public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cblockquoteu003Eu003Cpu003E说明:u003Cu002Fpu003Eu003Cpu003Eget – 用于获取 ThreadLocal 在当前线程中保存的变量副本。 set – 用于设置当前线程中变量的副本。 remove – 用于删除当前线程中变量的副本。如果此线程局部变量随后被当前线程读取,则其值将通过调用其 initialValue 方法重新初始化,除非其值由中间线程中的当前线程设置。 这可能会导致当前线程中多次调用 initialValue 方法。 initialValue – 为 ThreadLocal 设置默认的 get 初始值,需要重写 initialValue 方法 。u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Cpu003EThreadLocal 常用于防止对可变的单例(Singleton)变量或全局变量进行共享。典型应用场景有:管理数据库连接、Session。u003Cu002Fpu003Eu003Cpu003E示例 – 数据库连接u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Eprivate static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override public Connection initialValue() { return DriverManager.getConnection(DB_URL); }};public static Connection getConnection() { return connectionHolder.get();}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E示例 – Session 管理u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Eprivate 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;}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E示例 – 完整使用示例u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Epublic 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); } }}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E全部输出 count = 10u003Cu002Fpu003Eu003Ch3 class=”heading”u003EThreadLocal 的原理u003Cu002Fh3u003Eu003Ch4 class=”heading”u003E存储结构u003Cu002Fh4u003Eu003Cpu003EThread 类中维护着一个 ThreadLocal.ThreadLocalMap 类型的成员 threadLocals。这个成员就是用来存储线程独占的变量副本。u003Cu002Fpu003Eu003Cpu003EThreadLocalMap 是 ThreadLocal 的内部类,它维护着一个 Entry 数组, Entry 用于保存键值对,其 key 是 ThreadLocal 对象,value 是传递进来的对象(变量副本)。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E如何解决 Hash 冲突u003Cu002Fh4u003Eu003Cpu003EThreadLocalMap 虽然是类似 Map 结构的数据结构,但它并没有实现 Map 接口。它不支持 Map 接口中的 next 方法,这意味着 ThreadLocalMap 中解决 Hash 冲突的方式并非 u003Cstrongu003E拉链表u003Cu002Fstrongu003E 方式。u003Cu002Fpu003Eu003Cpu003E实际上,u003Cstrongu003EThreadLocalMap 采用线性探测的方式来解决 Hash 冲突u003Cu002Fstrongu003E。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。u003Cu002Fpu003Eu003Ch4 class=”heading”u003E内存泄漏问题u003Cu002Fh4u003Eu003Cpu003EThreadLocalMap 的 Entry 继承了 WeakReference,所以它的 key (ThreadLocal 对象)是弱引用,而 value (变量副本)是强引用。u003Cu002Fpu003Eu003Cul class=””u003Eu003Cliu003E如果 ThreadLocal 对象没有外部强引用来引用它,那么 ThreadLocal 对象会在下次 GC 时被回收。u003Cu002Fliu003Eu003Cliu003E此时,Entry 中的 key 已经被回收,但是 value 由于是强引用不会被垃圾收集器回收。如果创建 ThreadLocal 的线程一直持续运行,那么 value 就会一直得不到回收,产生内存泄露。u003Cu002Fliu003Eu003Cu002Fulu003Eu003Cpu003E那么如何避免内存泄漏呢?方法就是:u003Cstrongu003E使用 ThreadLocal 的 set 方法后,显示的调用 remove 方法u003Cu002Fstrongu003E 。u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003EThreadLocal<String> threadLocal = new ThreadLocal();try { threadLocal.set("xxx"); u002Fu002F …} finally { threadLocal.remove();}u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003Eu003Cbru002Fu003Eu003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp9.pstatp.comu002Flargeu002Fpgc-imageu002F076c786047de4473a17f2ee0c1ccd9ba” img_width=”436″ img_height=”304″ alt=”深入理解 Java 并发核心机制” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003Eu003Cstrongu003E关注头条号u003Cu002Fstrongu003E《 u003Cstrong class=”highlight-text”u003EJAVA 后端架构u003Cu002Fstrongu003Eu003Cstrongu003E u003Cu002Fstrongu003E》 ,话痨技术,职场,招聘,在线面试,进阶提升。每天一篇技术分享u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E没有做不到的,只有想不到的。u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003Eu003Cbru002Fu003Eu003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F53a834ba85944b2f85ca536461e50550″ img_width=”1000″ img_height=”200″ alt=”深入理解 Java 并发核心机制” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003Eu003Cbru002Fu003Eu003Cu002Fpu003E”

原文始发于:深入理解 Java 并发核心机制

主题测试文章,只做测试使用。发布者:IT资讯科技,转转请注明出处:http://www.cxybcw.com/26208.html

联系我们

13687733322

在线咨询:点击这里给我发消息

邮件:1877088071@qq.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code