您当前的位置:首页 > 计算机 > 编程开发 > Java

11.5. Java concurrent 包的实现

时间:12-14来源:作者:点击数:
城东书院 www.cdsy.xyz

由于 java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面四种方式:

  1. A线程写 volatile 变量,随后B线程读这个 volatile 变量。
  2. A线程写 volatile 变量,随后B线程用 CAS 更新这个 volatile 变量。
  3. A线程用 CAS 更新一个 volatile 变量,随后B线程用 CAS 更新这个 volatile 变量。
  4. A线程用 CAS 更新一个 volatile 变量,随后B线程读这个 volatile 变量。

Java 的 CAS 会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行 读-改-写 操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。

同时,volatile 变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。如果我们仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent 包的实现示意图如下:

JVM 中的 CAS(堆中对象的分配)

Java 调用 new object() 会创建一个对象,这个对象会被分配到JVM的堆中。那么这个对象到底是怎么在堆中保存的呢?

首先,new object() 执行的时候,这个对象需要多大的空间,其实是已经确定的,因为 java 中的各种数据类型,占用多大的空间都是固定的(对其原理不清楚的请自行Google)。那么接下来的工作就是在堆中找出那么一块空间用于存放这个对象。

在单线程的情况下,一般有两种分配策略:

  1. 指针碰撞:这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工作只是将指针像空闲内存一侧移动对象大小的距离即可。
  2. 空闲列表:这种适用于内存非规整的情况,这种情况下 JVM 会维护一个内存列表,记录哪些内存区域是空闲的,大小是多少。给对象分配空间的时候去空闲列表里查询到合适的区域然后进行分配即可。

但是JVM不可能一直在单线程状态下运行,那样效率太差了。由于在给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。

解决并发时的安全问题也有两种策略:

  1. CAS:实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。
  2. TLAB:如果使用CAS其实对性能还是会有影响的,所以 JVM 又提出了一种更高级的优化策略:每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在 TLAB 上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行 CAS 操作分配更大的内存空间。

虚拟机是否使用 TLAB,可以通过 -XX:+/-UseTLAB 参数来进行配置(jdk 5 及以后的版本默认是启用 TLAB 的)。

城东书院 www.cdsy.xyz
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐