【JVM】多线程如何保证并发正确性?

本文深入探讨了线程安全的概念、分类以及实现方法,包括互斥同步、非阻塞同步和无同步方案。详细解析了Java虚拟机的锁优化技术,如自旋锁、锁消除、锁粗化、轻量级锁和偏向锁,帮助理解高效并发编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  站在计算机的角度去抽象、解决问题,是面向过程的编程思想;站在现实世界的角度去抽象、解决问题,是面向对象的编程思想。然而计算机世界与现实世界存在一些差异,必须让程序在计算机中正确无误的进行,然后实现高效,即保证并发的正确性和实现线程的安全性。

线程安全

一、定义

  1.线程安全
  当多个线程访问一个对象时,不考虑这些线程在运行时环境下的调度和交替执行,不需进行额外同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,则这个对象是线程安全的。
  2.特征
  代码本身封装了所有必要的正确性保障手段(如互斥同步等),使调用者无需关心多线程的问题,也无需采取任何措施保证多线程的正确调用。

二、Java语言中的线程安全

  1.分类
  按照线程安全的“安全程度”由强至弱排序,将各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
  2.具体分析
  • 不可变:不可变(Immutable)的对象一定是线程安全的,final关键字可以体现。
  • 绝对线程安全:定义严格,不管运行时环境如何,调用者都不需要任何额外的同步措施。
  • 相对线程安全:保证对这个对象单独的操作是线程安全的,调用时不需做额外的保障,如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
  • 线程兼容:对象本身不是线程安全的,通过调用端使用同步手段保证并发环境下,对象线程安全,如ArrayList、HashMap等。
  • 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境下并发使用代码。如Thread类的suspend()、 resume()两个方法。

线程安全实现方法

  线程安全似乎是一件由代码如何编写来决定的事情,但虚拟机提供的同步和锁机制也有非常重要的作用,同时更偏重于锁机制。

一、互斥同步

  这是一种常见的并发正确性保障手段。
  1.概念
  同步指在多个线程并发访问共享数据时,同一个时刻只有一个线程使用共享数据;互斥是实现同步的一种手段,临界区、互斥量和信号量是实现互斥的主要方式。互斥是因,同步是果;互斥是方法,同步是目的。
  2.synchronized关键字
  synchronized关键字是实现互斥同步的基本手段,经过编译后,会在同步块前后形成monitorenter和monitorexit两个字节码指令,reference类型的参数致命要锁定和解锁的对象。
  synchronized同步块对同一条线程是重入的,同步块在已进入的线程执行完之前,会阻塞后面其他线程进入。
  Java线程映射到操作系统的原生线程上,要阻塞或唤醒一个线程,都需要操作系统帮忙完成,需要从用户态转换到核心态中,这会耗费很多的处理器时间,状态转换消耗的时间可能比用户代码执行时间还要长,因此synchronized是Java语言中一个重量级(Heavyweight)的操作。
  3.ReentrantLock重入锁
  ReentrantLock也具备线程重入特性,在代码写法上,表现为API层面的互斥锁,lock() 、unlock()配合try/finally语句块完成互斥同步,保证线程安全。
  增加了高级功能,主要有3个:等待可中断、可实现公平锁,以及可以绑定多个条件。
  4.synchronized与ReentrantLock的对比
  JDK1.5的单核和多核处理器情况下,两者的吞吐量对比图,多线程环境下synchronized的吞吐量下降得非常严重,而ReentrantLock则能基本保持在同一个比较稳定的水平上。JDK1.6及以上两者的性能基本持平,提倡在synchronized能实现需求时,优先考虑使用synchronized进行同步。

在这里插入图片描述
在这里插入图片描述
  互斥同步主要问题是进行线程阻塞和唤醒带来的性能问题,这种同步也叫阻塞同步(Blocking Synchronized),属于一种悲观的并发策略。

二、非阻塞同步

  非阻塞同步,是在硬件指令集发展后,基于冲突检测的乐观并发策略。
  1.常用的硬件指令
  测试并设置(Test-and-Set)
  获取并增加(Fetch-and-Increment)
  交换(Swap)
  比较并交换(Compare-and-Swap)
  加载链接/条件存储(Load-Linked/Store-Conditional)
  其中前3条是大多数指令集中的处理器指令,后面的两条是现代处理器新增的。CAS不能涵盖互斥同步的所有使用场景,存在“ABA”问题,大部分情况下ABA问题不会影响程序并发的正确性,如果要解决ABA问题,改用传统的互斥同步会比原子类更高效。
  2.使用CAS操作的方法
  在JDK1.5后,才有使用CAS操作的方法。sun.misc.Unsafe类中的compareAndSwapInt()和compareAndSwapLong等几个方法包装提供。
  3.反射手段或Java API间接使用(AtomicInteger类)

三、无同步方案

  如果一个方法不涉及共享数据,就不需要同步措施保证正确性,一些代码本身是线程安全的,主要有下面两类:
  1.可重入代码
  如果一个方法的返回结果可以预测,输入相同的数据,返回相同的结果,它就满足可重入性要求,即线程安全。
  2.线程本地存储
  共享数据的可见范围限制在同一个线程内,无须同步保证线程间的数据争用后的安全。java.lang.ThreadLocal实现线程本地存储功能。

锁优化

  高效并发是从JDK1.5到JDK1.6的一个重要改进,实现了各种锁优化技术,使得线程之间更高效共享数据,解决共享数据竞争问题,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。

一、自旋锁与自适应自旋

  1.自旋锁
  有一个以上的处理器,两个或以上的线程同时执行,后面请求锁的线程不放弃处理器的执行时间,执行一个忙循环(自旋),以便持有锁的线程很快释放锁而获取到锁。
  2.自适应的自旋锁
  自旋时间由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  3.总结
  自旋锁在JDK1.6中默认开启,参数-XX:+UseSpinning开启。在锁被占用的时间短的情况下,适合使用自旋等待,自旋次数默认值是10次,参数-XX:PreBlockSpin控制。
  JDK1.6引入自适应自旋,程序运行和性能监控信息的完善,可以帮助虚拟机对程序锁的状况预测更准确。

二、锁消除

  锁消除在虚拟机即时编译器运行时,对一些代码要求同步,但被检测到不存在共享数据竞争的锁进行消除。
  1.判断依据
  逃逸分析下,判断一段代码中,堆上所有数据都不会逃逸出去,不能被其他线程访问到,则认为他们是线程私有,当做栈上数据对待,同步加锁就无必要。
  2.例子
  String是不可变类,对字符串连接操作总是生成新的String对象进行。JDK1.5之前,StringBuffer对象的append()操作来处理;JDK1.5及以后,会使用StringBuilder对象的append()操作处理。我们发现,StringBuffer.append()中都有一个同步块,锁里面的代码不会逃逸出去,其他线程无法访问到它。因此,在即时编译后,代码中会忽略所有同步而直接执行了。

三、锁粗化

  虚拟机在探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

四、轻量级锁

  在没有多线程竞争的前提下,使用轻量级锁来减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  1.虚拟机对象的内存布局
  分为两部分:一部分是存储对象自身的运行时数据,如hashCode、GC分代年龄等,官方称之为“Mark Word”;另一部分是存储指向方法区对象类型数据的指针;若是数组对象,还会有一个额外部分存储数组长度。
  2.轻量级锁执行过程
  (1)未锁定:代码进入同步块时,若同步对象未被锁定(锁标志位01),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,存储锁对象目前Mark Word的拷贝(Displaced Mark Word),如下图13-3;
  (2)轻量级锁:虚拟机使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,更新动作成功,线程则拥有了该对象锁,对象Mark Word的锁标志位为00,对象处于轻量级锁定状态,如图13-4;
  (3)重量级锁:若CAS更新操作失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,是则当前线程拥有此对象锁,继续执行同步块代码;否则锁对象被其他线程抢占,两条以上线程争用同一个锁,轻量级锁将膨胀为重量级锁,锁标志状态值为10,Mark Word中存储的是指向重量级锁(互斥量)的指针,后面等待所的线程进入阻塞状态。

在这里插入图片描述

在这里插入图片描述

五、偏向锁

  JDK1.6引入的一项锁优化,在无竞争的情况下把整个同步都消除掉,连CAS操作都不用做。
  1.特点
  偏向于第一个获得偏向锁的线程,执行过程中,如果其他线程没有获取该锁,则当前持有偏向锁的线程不会进行同步。
  2.过程
  默认启用偏向锁,-XX:+UseBiasedLocking,当锁对象第一次被线程获取时,虚拟机将把对象头中标志位设为01,偏向模式。使用CAS操作把获取到偏向锁的线程ID记录在对象的Mark Word中;
  CAS操作成功,持有偏向锁的线程每次进入此锁相关的同步块时,不进行任何同步操作(如Locking、Unlocking及对Mark Word的Update等);
  其他线程尝试获取偏向锁时,偏向模式结束。
  3.偏向锁、轻量级锁的状态转换
  根据锁对象目前是否处于被锁定状态,撤销偏向(Remove Bias)后恢复到未锁定(标志位01)或轻量级锁定(标志位00)的状态,后续的同步操作如轻量级锁执行过程。

在这里插入图片描述

小结

  线程安全的概念和分类、同步实现的方式及虚拟机底层运作原理,一系列锁优化措施实现高效并发,这些知识是高级程序员必备知识之一。
感谢您的访问!
<think>好的,我现在需要回答用户关于“如果不用volatile,JVM怎么保证数据的可见性?”的问题。首先,我得回忆一下JMM(Java内存模型)相关的知识,特别是关于可见性的部分。用户可能已经知道volatile的作用,现在想了解其他方法。 用户之前的对话里讨论了equals和hashCode的重写,现在转向并发问题,可能是在学习多线程编程。我需要先明确可见性的概念:即一个线程修改共享变量后,其他线程能立即看到修改后的值。而volatile通过内存屏障来保证可见性和禁止指令重排序,但用户想知道不用volatile时如何实现。 首先,想到的是synchronized关键字。synchronized同步块或方法在进入时会获取,导致线程的工作内存失效,从而从主内存重新读取变量。退出时会将变量刷新回主内存,这样其他线程就能看到更新。所以,使用synchronized可以保证可见性。需要解释这个过程,并给出示例代码。 其次,Lock接口的实现类,比如ReentrantLock,在lock和unlock时也会类似synchronized,有内存屏障的效果。这同样能保证可见性。可以对比synchronized,说明其机制类似,但需要显式调用lock和unlock。 另外,原子类如AtomicInteger,它们内部使用volatile变量和CAS操作,可能用户不直接使用volatile,但原子类间接依赖了volatile。不过,用户的问题可能允许这种情况,因为用户自己没显式用volatile,而是通过原子类的方法来保证。这点需要澄清,可能应该提到原子类,但指出其内部机制。 还有,final字段的可见性。当一个对象被正确构造后,final字段的初始化值对其他线程是可见的,这属于JMM的保证。不过这可能属于特殊情况,用户的问题可能更多指普通变量,所以可能作为补充点提及。 另外,线程的启动和终止,比如Thread.start()和Thread.join()也会带来内存可见性的保证。例如,主线程启动子线程前修改的变量,子线程能看到;子线程结束后的状态变化,主线程调用join后也能看到。这也是JMM的规则,但用户可能不知道,需要解释。 然后需要考虑这些方法各自的适用场景,以及和volatile的对比。比如synchronized和Lock除了可见性还提供互斥,而volatile只解决可见性和有序性,不保证原子性。所以在需要同步的时候,用更合适,而仅需要可见性时可能用volatile更轻量。 可能的误区是用户认为这些方法可以完全替代volatile,但实际上它们各自有不同的使用场景。例如,如果仅需要可见性而不需要,那么使用volatile更高效。但用户的问题是不使用volatile的情况下如何保证,所以需要列举其他方法。 总结下来,回答的结构应该是先说明可见性的重要性,然后分点讲解synchronized、Lock、原子类、final、线程操作等方法,并分别举例说明。同时比较它们的异同,强调正确同步的重要性,以及为什么在某些情况下必须用volatile或其他机制。 需要确保用词准确,比如“happens-before”原则,JMM的规定等,但用中文解释清楚。例如,synchronized块的进入和退出会建立happens-before关系,从而保证变量的可见性。同样,Lock的加和解也有类似的效果。 另外,用户可能的疑问是为什么这些方法可以替代volatile,所以需要解释它们的内存语义,比如的获取和释放会触发内存屏障,强制刷新工作内存到主存,从而让其他线程可见。而volatile则是每次访问都直接从主存读写,但的机制在进入和退出时做同样的事情,只是粒度更大。 最后,提醒用户如果没有正确同步(即不使用任何机制),可能会导致可见性问题,如脏读等,所以正确选择同步机制很重要。可能还需要提到其他机制如ThreadLocal,但ThreadLocal是避免共享,所以不适用此问题。 检查是否有遗漏的方法,比如volatile变量的写操作对其他线程的读可见,而的释放和获取同样有这种效果。所以,虽然问题是不用volatile,但其他同步手段实际上也达到了同样的可见性保证,只是方式不同。 现在需要组织语言,用清晰的结构分点回答,每个方法给出简要说明和示例,帮助用户理解不同场景下的解决方案。</think>在 Java 中,如果不使用 `volatile`,可以通过以下方法保证数据的可见性。这些方法均基于 **Java 内存模型(JMM)** 的规则和 **happens-before 原则**,确保多线程间的数据同步。 --- ### 1. **使用 `synchronized` 同步块** **机制**: `JVM` 规定,线程在进入 `synchronized` 块时会清空本地内存(工作内存),从主内存重新加载共享变量;退出 `synchronized` 块时会将修改后的变量刷新回主内存。 **示例**: ```java public class Counter { private int count = 0; public synchronized void increment() { count++; // 线程安全的操作 } public synchronized int getCount() { return count; // 保证读取最新值 } } ``` **特点**: - 通过的获取释放实现可见性和原子性。 - 适用于需要同时保证可见性和原子性的场景。 --- ### 2. **使用显式(`Lock` 接口)** **机制**: 显式(如 `ReentrantLock`)的 `lock()` 和 `unlock()` 操作 `synchronized` 类似,会触发内存屏障,强制将工作内存的修改刷新到主内存,并清空其他线程的本地内存。 **示例**: ```java public class Counter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } } ``` **特点**: - 更灵活的机制,但需手动管理的释放。 - 同样保证可见性和原子性。 --- ### 3. **使用原子类(`Atomic` 系列)** **机制**: 原子类(如 `AtomicInteger`)通过底层 `CAS(Compare-And-Swap)` 操作和 `volatile` 变量实现可见性原子性。虽然用户未显式使用 `volatile`,但原子类内部依赖了 `volatile` 的语义。 **示例**: ```java public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 线程安全且可见 } } ``` **特点**: - 仅需关注原子操作,无需手动同步。 - 适用于简单变量(如计数器)。 --- ### 4. **通过线程启动终止的规则** **机制**: JMM 规定以下操作会保证可见性: - **线程启动**:`Thread.start()` 前的修改对子线程可见。 - **线程终止**:子线程结束后的修改对调用 `Thread.join()` 的线程可见。 **示例**: ```java public class VisibilityExample { private int value = 0; public static void main(String[] args) throws InterruptedException { VisibilityExample example = new VisibilityExample(); Thread thread = new Thread(() -> { System.out.println(example.value); // 一定能看到主线程修改后的 value }); example.value = 42; thread.start(); thread.join(); } } ``` --- ### 5. **`final` 关键字** **机制**: `final` 字段的初始化值在对象正确构造后(即构造函数结束前),对其他线程是可见的。这是 JMM 对 `final` 的强制保证。 **示例**: ```java public class FinalExample { private final int finalValue; // 构造完成后,finalValue 对所有线程可见 public FinalExample(int value) { this.finalValue = value; } } ``` --- ### 总结 | 方法 | 适用场景 | 可见性保证机制 | |---------------------|----------------------------|----------------------------------| | `synchronized` | 需要同步的代码块/方法 | 的获取释放触发内存屏障 | | 显式(`Lock`) | 需要灵活控制的同步 | `lock()`/`unlock()` 触发内存屏障 | | 原子类(`Atomic`) | 简单变量的原子操作 | 内部 `volatile` + CAS | | 线程启动/终止 | 主线程线程间的数据传递 | JMM 的 happens-before 规则 | | `final` 关键字 | 不可变对象的初始化 | JMM 对 `final` 的特殊保证 | **注意事项**: - 若未使用上述任何机制,多线程间可能出现 **脏读**(读取到过期的数据)或 **可见性问题**。 - `volatile` 的轻量级特性使其适用于仅需可见性(无需原子性)的场景(如标志位),而其他方法通常需要配合或原子操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值