volatile和synchronized 实现原理的差别

本文探讨了Java中volatile和synchronized关键字的作用及其在并发编程中的应用。详细解析了这两种关键字如何解决线程间通信的可见性问题,并对比了它们在性能上的差异。

提到volatile 和 synchronized 的时候不得不提到的一个东西就是JMM(java Memory Model)java内存模型。

因为在并发的过程中 经常要处理一些 可见性 、 原子性 、 有序性的问题。

并发编程中的两个关键问题是: 线程之间是 如何通信的。

这又分两种情况:
1、共享内存 ——— 隐式通信 volatile
2、 消息传递 ——— 显示通信 synchronized / lock

先看看没有 volatile 跟 synchronized来控制 线程的通信会出现什么问题
java内存模型
线程A把X = 0 改成 X = 1 时, 线程2 并不会马上知道。

什么时候要让线程2去解决这个问题,就成了解决线程直接通信的一个关键。

这也就是 可见性问题的所在。

java 提供了 volatile 和 synchronized 关键字 来处理这个可见性的问题,当然 使用lock 也可以,但这里先暂不做讨论。

先说synchronized是怎么做到的,synchronized 先锁住 共享的内存变量,然后线程A修改完之后将值返回到主内存,然后线程B获得锁以后才能获取值,所以通过代码层面的锁可以解决这个线程之间通信的 可见性问题。

但是代码层面,使用锁的话性能就低了,更好的解决办法是 使用volatile 关键字 来修饰这个共享变量。

下面我们来深入的分析 一下volatile 为什么就能做到比synchronized 性能好 还能保持可见性:

1、对于声明了 volatile 的变量,进行写操作的时候, JVM 会向 处理器发送一条 lock 的前缀指令。
2、将这个变量在自己工作内存的值,强制写回主内存。

此时就算线程A将 X 变量的最新值 写回了主存, 但是线程B不去拿,那线程B自己工作内存里的值也还是旧的,那主内存准备通知线程B 去刷新它自己工作内存中的值呢,所以接着看第三步。

3、在多处理器的情况下,保证各个处理器缓存一致性的特点,就会实现缓存一致性协议。
意思就是:每个处理器会 嗅探到 总线上的所传播的数据来检测自己缓存中的值是不是过期了,
当处理器的缓存对应的内存地址被修改以后,它就会将当前的处理器缓存的值设置为失效状态,然后去读那个最新的值。

这就是volatile 的实现原理,代码层面看起来没有加锁,实际上底层还是加了锁的。但是被JVM优化得很快了,它实现锁的开销很小。

再来看看synchronized的实现原理。
先看一段代码:

public class App {

    public static void main(String[] args) {
        test();
    }

    public static synchronized void test(){
    } 
}

将这段代码编译后,打开命令行,进入App.class所在目录 执行 javap -v App.class命令

可以看到App.class的字节码:

App.clas

我们把目光放到 第 4 行跟 第 6 行。 这就是synchronized的作用,调用了

monitorenter 跟 monitorexit 的指令, 这是基于JVM 实现的。

我们通过synchronized 声明了锁的范围, 当前的App对象会有一个自己的监视器,该监视器必须获得 当前对象的锁之后 monitorenter 才有资格去 调用 当前的这个线程方法,也就是字节码的第四行。然后锁运行完以后 就运行第六行指令 将锁释放。

ok 上个图估计会更好容易理解一点:

竞争锁的过程

多个请求进来,想要获取同步代码块的锁,必须先获取 monitorenter ,但monitorenter 只有一个,所以 其它线程 B、C、D、E 就都获取不到锁了,没有获取到锁的线程会被放到一个
synchronizedQueue的队列里。

然后获取到monitorenter 的那个线程 在执行完同步代码后,会 monitor.exit 。通知队列里的 B C D E 线程再去竞争锁。

这就是synchronized 的原理啦。
大概就是这样,如果不对之处,还麻烦各位友人 多多指点

### ### volatilesynchronized 的工作原理对比 在 Java 并发编程中,`volatile` `synchronized` 是实现线程同步的两种关键机制,它们在底层原理使用场景上存在显著差异。 #### 可见性与有序性的实现 `volatile` 关键字通过内存屏障缓存一致性协议来确保变量的可见性有序性。当一个变量被声明为 `volatile` 时,任何对该变量的写操作都会立即刷新到主内存中,并且读取操作会直接从主内存中获取最新值。这种机制保证了多线程环境下变量的可见性[^4]。此外,`volatile` 还能防止指令重排序,从而避免由于编译器优化导致的顺序问题。 相比之下,`synchronized` 通过对对象加锁的方式实现了更全面的同步机制。进入同步代码块之前,线程必须获得对象锁;释放锁之后,所有对共享变量的修改才会被写回主内存。这一过程不仅保证了可见性,还通过锁定机制排除了多个线程同时执行临界区的可能性,从而确保了原子性[^4]。 #### 原子性保障 尽管 `volatile` 提供了最低限度的原子操作(如读取写入),但它无法保证复合操作的原子性。例如,自增操作 `i++` 涉及读取、递增写入三个步骤,在没有额外同步措施的情况下,即使是 `volatile` 变量也无法避免竞态条件。因此,对于需要原子性的操作,推荐使用 `java.util.concurrent.atomic` 包中的原子类或通过 `synchronized` 来实现[^2]。 `synchronized` 则天然支持原子性。一旦某个线程获得了对象锁并进入同步代码块,其他尝试获取同一锁的线程将被阻塞,直到当前线程完成操作并释放锁。这种方式有效地隔离了并发访问带来的数据不一致风险,适用于复杂的同步需求。 #### 性能开销 作为轻量级同步工具,`volatile` 的性能通常优于 `synchronized`。它不会引起线程上下文切换,也不涉及锁的获取与释放,因此适合用于状态标记等简单场景。然而,由于其功能有限,过度依赖 `volatile` 容易引发程序错误,尤其是在处理复杂逻辑时[^1]。 `synchronized` 虽然曾经被认为是一种重量级锁,但随着 JVM 的不断优化(如偏向锁、轻量级锁等技术的应用),它的性能已经得到了显著提升。特别是在 JDK 6 及以后版本中,`synchronized` 在许多情况下表现出接近甚至超过 `volatile` 的效率,同时提供了更强的安全保障[^2]。 #### 使用建议 鉴于 `volatile` 对技术要求较高且容易出错,除非有充分把握掌握其正确用法,否则应优先考虑使用 `synchronized` 或者 `Lock` 接口提供的锁机制来确保并发安全。尤其在需要保证原子性更复杂同步控制的情形下,这些高级同步工具更为可靠。 ```java // 示例:使用 volatile 实现简单的状态通知机制 public class VolatileExample { private static volatile boolean flag = false; public static void main(String[] args) { new Thread(() -> { while (!flag) { // 等待 flag 变为 true } System.out.println("Flag is now true"); }).start(); try { Thread.sleep(1000); // 模拟延迟 } catch (InterruptedException e) { e.printStackTrace(); } flag = true; // 主线程改变 flag 值 } } ``` ```java // 示例:使用 synchronized 实现计数器 public class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值