volatile 伪共享问题

JDK 内置并发队列

JDK 内置并发队列按照实现方式可以分为阻塞队列和非阻塞队列两种类型,阻塞队列是基于锁实现的,非阻塞队列是基于 CAS 操作实现的。JDK 中包含多种阻塞和非阻塞的队列实现,如下图所示。

图片

队列是一种 FIFO(先进先出)的数据结构,JDK 中定义了 java.util.Queue 的队列接口,与 List、Set 接口类似,java.util.Queue 也继承于 Collection 集合接口。

此外,JDK 还提供了一种双端队列接口 java.util.Deque,我们最常用的 LinkedList 就是实现了 Deque 接口。

下面我们简单说说上图中的每个队列的特点,并给出一些对比和总结。

阻塞队列

阻塞队列在队列为空或者队列满时,都会发生阻塞。阻塞队列自身是线程安全的,使用者无需关心线程安全问题,降低了多线程开发难度。

阻塞队列主要分为以下几种:

  • ArrayBlockingQueue

    最基础且开发中最常用的阻塞队列,底层采用数组实现的有界队列,初始化需要指定队列的容量。ArrayBlockingQueue 是如何保证线程安全的呢?

    它内部是使用了一个重入锁 ReentrantLock,并搭配 notEmpty、notFull 两个条件变量 Condition 来控制并发访问。

    从队列读取数据时,如果队列为空,那么会阻塞等待,直到队列有数据了才会被唤醒。

    如果队列已经满了,也同样会进入阻塞状态,直到队列有空闲才会被唤醒。

  • LinkedBlockingQueue

    内部采用的数据结构是链表,队列的长度可以是有界或者无界的,初始化不需要指定队列长度,默认是 Integer.MAX_VALUE。

    LinkedBlockingQueue 内部使用了 takeLock、putLock两个重入锁 ReentrantLock,以及 notEmpty、notFull 两个条件变量 Condition 来控制并发访问。

    采用读锁和写锁的好处是可以避免读写时相互竞争锁的现象,所以相比于 ArrayBlockingQueue,LinkedBlockingQueue 的性能要更好。

  • PriorityBlockingQueue

    采用最小堆实现的优先级队列,队列中的元素按照优先级进行排列,每次出队都是返回优先级最高的元素。PriorityBlockingQueue 内部是使用了一个 ReentrantLock 以及一个条件变量 Condition notEmpty 来控制并发访问,

    因为 PriorityBlockingQueue 是无界队列,所以不需要 notFull ,每次 put 都不会发生阻塞。PriorityBlockingQueue 底层的最小堆是采用数组实现的,当元素个数大于等于最大容量时会触发扩容,

    在扩容时会先释放锁,保证其他元素可以正常出队,然后使用 CAS 操作确保只有一个线程可以执行扩容逻辑。

  • DelayQueue

    一种支持延迟获取元素的阻塞队列,常用于缓存、定时任务调度等场景。

    DelayQueue 内部是采用优先级队列 PriorityQueue 存储对象。

    DelayQueue 中的每个对象都必须实现 Delayed 接口,并重写 compareTo 和 getDelay 方法。向队列中存放元素的时候必须指定延迟时间,只有延迟时间已满的元素才能从队列中取出。

  • SynchronizedQueue

    又称无缓冲队列。

    比较特别的是 SynchronizedQueue 内部不会存储元素。与 ArrayBlockingQueue、LinkedBlockingQueue 不同,SynchronizedQueue 直接使用 CAS 操作控制线程的安全访问。

    其中 put 和 take 操作都是阻塞的,每一个 put 操作都必须阻塞等待一个 take 操作,反之亦然。

    所以 SynchronizedQueue 可以理解为生产者和消费者配对的场景,双方必须互相等待,直至配对成功。

    在 JDK 的线程池 Executors.newCachedThreadPool 中就存在 SynchronousQueue 的运用,对于新提交的任务,如果有空闲线程,将重复利用空闲线程处理任务,否则将新建线程进行处理。

  • LinkedTransferQueue

    一种特殊的无界阻塞队列,可以看作 LinkedBlockingQueues、SynchronousQueue(公平模式)、ConcurrentLinkedQueue 的合体。

    与 SynchronousQueue 不同的是,LinkedTransferQueue 内部可以存储实际的数据,当执行 put 操作时,如果有等待线程,那么直接将数据交给对方,否则放入队列中。与 LinkedBlockingQueues 相比,LinkedTransferQueue 使用 CAS 无锁操作进一步提升了性能。

非阻塞队列

说完阻塞队列,我们再来看下非阻塞队列。非阻塞队列不需要通过加锁的方式对线程阻塞,并发性能更好。

JDK 中常用的非阻塞队列有以下几种:

  • ConcurrentLinkedQueue

    它是一个采用双向链表实现的无界并发非阻塞队列,它属于 LinkedQueue 的安全版本。ConcurrentLinkedQueue 内部采用 CAS 操作保证线程安全,这是非阻塞队列实现的基础,

    相比 ArrayBlockingQueue、LinkedBlockingQueue 具备较高的性能。

  • ConcurrentLinkedDeque

    也是一种采用双向链表结构的无界并发非阻塞队列。

    与 ConcurrentLinkedQueue 不同的是,ConcurrentLinkedDeque 属于双端队列,

    它同时支持 FIFO 和 FILO 两种模式,可以从队列的头部插入和删除数据,也可以从队列尾部插入和删除数据,适用于多生产者和多消费者的场景。

BlockingQueue阻塞队列超级接口

平时开发中使用频率最高的是 BlockingQueue。实现一个阻塞队列需要具备哪些基本功能呢?

下面看 BlockingQueue 的接口继承关系,如下图所示。

图片

下面看 BlockingQueue 的接口的各种实现子类,如下图所示。

图片

BlockingQueue 实现子类太多,图里放不下,还请大家通过IDEA工具,自行查看。

下面看 BlockingQueue 的接口的 抽象方法,如下图所示。

图片

我们可以通过下面一张表格,对上述 BlockingQueue 接口的具体行为进行归类。

图片

Java内置队列的问题

队列 有界性 数据结构
ArrayBlockingQueue bounded 加锁 arraylist
LinkedBlockingQueue optionally-bounded 加锁 linkedlist
ConcurrentLinkedQueue unbounded 无锁 linkedlist
LinkedTransferQueue unbounded 无锁 linkedlist
<think> 首先,用户的问题是:“我想了解Java中共享的概念以及避免方法 请问Java中共享的概念及避免方法”。系统指令要求我使用中文回答,并遵循特定的格式规则。 关键指令: - 行内数学表达式用 `$...$`,但在这个问题中,可能没有数学表达式,所以我需要检查是否需要使用。 - 独立公式用 `$$...$$` 并单独成段。 - LaTeX语法正确:确保任何数学表达正确。 - 使用中文回答。 - 生成相关问题:在回答末尾生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 内容基于提供的引用: - 引用[1]:共享是由于多个变量共享同一缓存行导致的性能问题volatile关键字保证可见性等,但不能保证原子性;@Contended注解可解决。 - 引用[2]:缓存行大小和布局依赖硬件和JVM,填充方法减少共享可能性,但不保证有效。 - 引用[3]:JDK8提供@sun.misc.Contended注解来解决共享。 - 引用[4]:共享定义——CPU缓存中以缓存行为单位存储;当多个线程修改共享同一缓存行的变量时,无意影响性能。 - 引用[5]:详细解释共享原理——当a或b变量被修改,整个缓存行需要刷新,导致原本独立的变量互相影响。 用户问题焦点:Java中共享的概念和避免方法。 结构化回答: 1. **概念解释**:先定义共享False Sharing),使用引用[4]和[5]的内容。 2. **原因和影响**:解释为什么发生,基于缓存行机制。 3. **避免方法**:讨论如何避免或缓解: - 使用填充(padding)来隔离变量,确保它们不在同一缓存行(引用[2])。 - 使用@Contended注解(引用[1]和[3])。 - 注意volatile关键字的作用(引用[1])。 4. **添加引用标识**:在相关内容后添加 [^数字]。 生成相关问题:在末尾,基于回答生成2-3个相关问题。 回答结构: - 清晰、逐步解决问题。 - 使用段落和列表使结构清晰。 - 确保语言是中文。 最终输出: - 回答部分。 - 相关问题部分,标记为§§相关问题§§。 现在,大纲回答: 1. **共享的概念**: - 定义:共享是指多个线程访问共享同一缓存行的不同变量时,由于缓存一致性协议,导致不必要的缓存行刷新,从而降低性能的现象。 - 原因:CPU缓存以缓存行为单位(通常是64字节),当任何变量在缓存行中被修改,整个缓存行都无效化。 - 引用[4]和[5]。 2. **如何避免共享**: - 填充方法:添加额外的字节使变量占据整个缓存行,避免共享。 - 使用@Contended注解:在JDK8及以后,使用此注解自动处理填充。 - 注意事项:填充方法不保证在所有硬件有效,依赖JVM实现;@Contended需要特定标志启用。 - 引用[1]、[2]、[3]。 3. **volatile关键字的作用**:保证变量的可见性和有序性,但不解决共享问题;它可能加剧共享,因为volatile变量更频繁地被访问。 相关问题: 1. 共享在不同CPU架构下的影响如何? 2. 除了填充和@Contended,还有其他方法避免共享吗? 3. 共享在实际应用中的性能影响有多大? </think> ### Java中共享的概念及避免方法 在Java多线程编程中,共享False Sharing)是一个重要的性能问题,它源于CPU缓存系统的机制。下面将从概念、原因、影响和避免方法逐步解释,基于相关引用内容。 #### 1. **共享的概念** 共享是指当多个线程访问共享同一缓存行(cache line)的不同变量时,由于CPU缓存一致性协议(如MESI协议),即使这些变量是独立的,对一个变量的修改也会导致整个缓存行无效化,从而迫使其他线程重新从内存加载数据,引发不必要的性能开销[^4]。 - **核心原理**: - CPU缓存系统以缓存行为单位存储数据,主流缓存行大小为64字节(Bytes)。 - 如果多个变量(如变量a和b)存储在同一个缓存行中,当线程A修改变量a时,线程B访问变量b也会受到波及:线程A需要将整个缓存行同步到内存,线程B必须重新加载整个缓存行,尽管b本身未被修改。 - 这导致原本独立的变量互相影响,降低程序性能,尤其在并行计算中表现为显著的吞吐量下降[^5]。 - **示例场景**: 假设在Java中定义了两个变量:`volatile long a` 和 `volatile long b`,它们被存储在同一个缓存行中。线程A修改`a`后,线程B读取`b`时,由于缓存行无效化,整个操作会变得更慢。这种现象被称为“共享,因为它不是真正的共享变量冲突,而是缓存行共享带来的副作用[^1][^5]。 共享在高并发场景下尤为严重,可能导致程序性能下降数十倍[^4]。 #### 2. **共享的避免方法** 为避免共享,Java开发者可以采用以下策略,这些方法的目标是利用填充或注解来隔离变量,确保它们不共享同一缓存行。 - **填充(Padding)方法**: - **原理**:在变量周围添加额外的无用字段(padding),使每个变量独占一个缓存行。 - **实现方式**: ```java public class FalseSharingExample { // 变量a,占用一个缓存行 (假设缓存行大小为64字节) public volatile long a; private long p1, p2, p3, p4, p5, p6, p7; // 填充字段,每个字段8字节,总计56字节 // 变量b,占用另一个缓存行 public volatile long b; private long p8, p9, p10, p11, p12, p13, p14; // 额外填充 } ``` - 为什么有效?这些填充字段确保 `a` 和 `b` 位于不同的缓存行中,避免相互干扰。 - 局限性:填充方法依赖硬件缓存行大小(通常是64字节),且JVM实现或硬件变化可能影响其效果。它不能保证在所有环境中稳定生效,是一种启发式解决方案[^2]。 - **`@Contended` 注解方法**: - **原理**:JDK8引入的 `sun.misc.Contended` 注解自动为变量添加填充,无需手动计算字节。 - **实现方式**: ```java import sun.misc.Contended; public class ContendedExample { @Contended // 注解确保value独占缓存行 public volatile long value = 0L; } ``` - 使用方法:在字段或类上添加 `@Contended`,JVM在运行时处理填充。 - 优点:简化代码,减少错误;自动处理不同缓存行大小。 - 注意事项:默认需要JVM启动参数 `-XX:-RestrictContended` 来启用,且在某些JVM版本中可能受限[^1][^3]。 - **其他注意事项**: - **volatile关键字的作用**:volatile保证变量的可见性和有序性(防止指令重排),但不能解决共享问题。相反,volatile变量更容易触发缓存行刷新,可能加剧共享[^1][^4]。 - **硬件依赖性**:共享问题在x86和ARM等主流CPU架构中均存在,但影响程度因缓存行大小和核心数量而异。测试性能时,建议在不同环境验证填充或注解效果[^2]。 - **最佳实践**:在高性能库中使用 `@Contended`(如Java并发包),对于用户代码优先选择填充方法以兼容性考虑。 #### 3. **总结** 共享是Java多线程中的隐性性能瓶颈,通过填充或 `@Contended` 注解可以有效缓解。关键点: - 共享源于缓存行共享(64字节单位),导致独立变量互相干扰。 - 避免方法包括手动填充(简单但不可靠)或 `@Contended` 注解(推荐但需JVM支持)。 - 实际开发中,结合性能测试选择合适方案,尤其在并发密集型应用中。 通过上述方法,您可以显著减少共享带来的性能损失,提升多线程程序的效率[^1][^3][^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值