synchronized和volatile

synchronized

  1. synchronized关键字的作用

    • 在Java中,synchronized关键字用于确保多个线程在访问共享资源时的安全性。它通过获取对象的内置锁(也称为monitor)来实现同步,保证了同一时刻只有一个线程可以进入同步代码块或同步方法(临界区),从而避免了多线程环境下的竞态条件。
  2. 临界区内的串行访问

    • 当一个线程进入synchronized保护的临界区时,其他线程需要等待该线程执行完毕并释放锁之后才能进入。这样确保了临界区内的操作是串行执行的,不会出现多个线程同时访问临界资源的情况,从而保证了数据的一致性和正确性。
  3. 临界区外的操作

    • synchronized仅保证了临界区内的操作是串行化的,对于临界区外的操作,Java内存模型并不保证这些操作不会被重排序。
  4. 指令重排序的影响

    • 指令重排序是编译器和处理器为了提高性能而进行的优化操作,它可以改变代码中指令的执行顺序,但不会改变程序的最终结果。在多线程环境中,如果不加控制地进行指令重排序,可能会导致数据竞争和线程安全性问题。
  5. 具体影响

    • synchronized保护的临界区之外,即使某些操作没有直接进入临界区,编译器和处理器仍然可能对这些操作进行重排序。这意味着,即使在临界区外,也需要通过其他手段(如volatilefinalAtomic类等)来确保变量的可见性和操作的有序性,以防止不正确的并发行为发生。

综上所述,虽然synchronized关键字通过内置锁确保了临界区内的串行访问,但并不负责防止编译器和处理器对临界区外的操作进行指令重排序。因此,在编写多线程代码时,除了正确使用synchronized外,还需要结合其他技术手段来确保整个程序的线程安全性和正确性。


volatile

volatile 是Java中的一个关键字,用于声明变量。它的作用是告诉编译器和虚拟机,该变量可能会被多个线程同时访问,因此不能进行某些优化,确保每次读取该变量时都是从主内存中读取,每次修改该变量时都会立即写入主内存,而不会使用缓存。

为什么需要volatile?

在多线程并发编程中,线程之间的可见性是一个重要问题。当一个线程修改了共享变量的值时,另一个线程能够立即看到修改后的值。这种保证是通过主内存和工作内存(线程的本地缓存)的交互来实现的。使用volatile关键字可以确保:

  • 可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到这个修改,而不是使用本地缓存中的值。
  • 禁止指令重排序:volatile变量的读写会插入特定的内存屏障指令,防止编译器和处理器对指令进行优化重排序,确保操作的有序性。

使用场景

  1. 状态标记:当一个变量用于标记应用程序的状态(例如是否运行、是否停止),并且多个线程需要协同工作来改变或检查这个状态时,可以使用volatile保证状态的可见性,避免使用锁带来的性能开销。

  2. 双重检查锁定(Double-Checked Locking):在单例模式中,当需要在第一次获取实例时才加锁并初始化实例,可以使用volatile关键字来确保多线程环境下的安全性和性能。

  3. 计数器或标记变量:当一个变量用于多线程环境下的计数或标记,如线程中断标志、事件触发等,可以使用volatile来保证可见性。

  4. 轻量级同步:在某些情况下,volatile可以作为一种比synchronized更轻量级的同步机制,用于简单的线程同步需求。

注意事项

  • volatile不能保证原子性。如果一个操作涉及到递增、递减等复合操作,并且要保证原子性,需要使用Atomic类或者synchronized关键字来保证。
  • 谨慎使用volatile来代替锁。虽然volatile可以提供一定程度的同步,但在复杂的多线程并发控制下,仍然需要使用更强大的同步机制。

对于volatile,什么是主内存,什么是本地缓存?

在Java内存模型中,涉及到volatile变量的可见性时,主内存和本地缓存是两个重要的概念。

在多核处理器架构中:

  1. 缓存层次

    • L1缓存:每个核心拥有自己的L1缓存,用于存储数据和指令。L1缓存是最快的,但容量较小。
    • L2缓存:每个核心的L1缓存之上是L2缓存,它也是高速缓存,但比L1缓存稍慢,但容量较大。
    • L3缓存:在多核处理器中,所有核心共享一个L3缓存。L3缓存的容量通常比L2大,但速度比L1和L2缓存慢一些。
  2. 内存访问流程

    • 当一个线程需要访问内存时,首先会检查自己的L1缓存。如果数据不在L1缓存中(缺失),则会去L2缓存查找。
    • 如果数据也不在L2缓存中,线程会进一步查找L3缓存。因为L3缓存是共享的,所以其他核心可能已经将数据放置在L3缓存中。
    • 如果数据在L3缓存中找到(命中),线程就可以直接从L3缓存读取数据,这比从主内存中读取速度要快得多。
    • 如果数据在L3缓存中也不命中,线程将需要从主内存(RAM)中读取数据,这个过程的延迟相对较高,因为需要跨越更长的存储层次和更大的物理距离。

  1. 主内存(Main Memory)

    • 主内存是所有线程共享的内存区域。
    • 所有的volatile变量都存储在主内存中。
    • 线程的工作内存(Thread’s Working Memory,或称本地内存)中的变量值必须从主内存中读取。
    • 当一个线程修改了一个volatile变量的值,它会立即将修改后的值刷新到主内存中,而不是仅仅更新自己的本地缓存。
  2. 本地缓存(Thread’s Working Memory)

    • 每个线程都有自己的本地缓存,也称为工作内存。
    • 线程在执行过程中,会把主内存中的变量拷贝到自己的工作内存中进行操作。
    • 线程对变量的所有操作(读取、赋值)都是在工作内存中进行的。
    • 当一个线程访问volatile变量时,它会将工作内存中该变量的值置为无效,然后重新从主内存中读取最新的值。

如何保证可见性?

  • 写操作:当一个线程写入一个volatile变量时,会将修改后的值立即刷新到主内存中,保证了其他线程能够立即看到最新的值。

  • 读操作:当一个线程读取一个volatile变量时,会从主内存中读取最新的值,而不是使用本地缓存中的值,从而保证了可见性。

因此,主内存是所有线程共享的存储区域,而本地缓存则是每个线程私有的存储区域。volatile关键字通过在这两者之间的交互,确保了变量的可见性,即一个线程对volatile变量的修改对其他线程是可见的。


什么是指令重排?多线程下会导致什么结果?

禁止指令重排是指编译器和处理器在进行代码优化时,不能改变程序中指令的执行顺序。在多线程环境下,如果没有禁止指令重排的保证,可能会导致程序的执行顺序与预期不符,从而引发线程安全性问题。

示例说明禁止指令重排的重要性:

考虑以下的单例模式双重检查锁定(Double-Checked Locking)的经典实现方式:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() { }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这个例子中,关键点是volatile关键字修饰的instance变量。volatile确保了以下两点:

  1. 可见性:任何一个线程修改了instance的值,其他线程能够立即看到最新的值。

  2. 禁止指令重排:在初始化instance变量时,防止指令重排导致的问题。

具体来说,如果没有使用volatile关键字,可能会出现以下问题:

  • 指令重排问题:在初始化instance时,通常需要进行三步操作:1)分配内存空间;2)初始化对象;3)将instance指向分配的内存空间。如果没有禁止指令重排的保证,可能会发生如下的重排操作:

    • 线程A执行了1和3,但是还没有执行2,此时线程B检测到instance不为null(即使实际上还没有初始化完成),直接返回instance,这时候得到的instance实际上还没有完成初始化,会导致程序错误。

通过使用volatile关键字修饰instance,可以确保所有的写操作都将立即反映到主内存中,所有的读操作也会直接从主内存中读取,从而避免了上述的指令重排问题,保证了单例模式的线程安全性。

因此,禁止指令重排在多线程编程中尤为重要,特别是在需要复杂操作顺序保证的情况下,volatile的使用可以有效地避免由指令重排引发的潜在问题。

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值