编程Java开发者

什么是Java内存模型(JMM),它如何影响多线程编程?

用 Hintsage AI 助手通过面试

回答。

问题历史:

当Java刚出现时,并没有关于内存和线程如何相互作用的正式描述。结果是相同的程序在不同的JVM上可能表现不同,导致难以捕捉的错误。2004年,Java内存模型(JMM)被添加到Java规范中,旨在严格定义线程如何看到变量的更新。

问题:

在没有明确的内存模型的情况下,写操作和读操作可能会被编译器或处理器交错,这导致从不同线程访问共享变量时出现意外行为。这可能导致竞争条件、可见性问题以及难以调试的同步错误。

解决方案:

JMM定义了规则:一个线程所做的更改何时对其他线程可见。它建立了_happens-before_的概念、同步器(synchronized,volatile,final)、内部锁,并限制在多线程环境中指令的重组。

代码示例:

class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int get() { return count; } }

关键特性:

  • JMM定义了一个变量的更改何时对其他线程可见
  • 使用synchronized或volatile确保正确发布更改
  • 不当使用可能导致不正确的行为,即使没有明显的错误

有陷阱的问题。

为什么volatile不使操作原子化,而仅仅提供可见性?

Volatile仅保证线程之间的更改可见性,但不保证更改的原子性。例如,递增volatile变量不是原子操作,因为该操作由读取、修改和写入组成。

代码示例:

volatile int count = 0; count++; // 不是原子操作!

synchronized块能保证其外部的更改可见吗?

可以。在退出synchronized块后,块内所做的所有更改对其他线程可见,这些线程将进入同一对象监视器的块。

final字段的更改何时对其他线程可见?

只有当对象在传递其引用给其他线程之前完全构造时,final字段才能保证正确发布。例如,使用immutable对象。否则可能出现未初始化状态的可见性。

常见错误与反模式

  • 在线程间共享的变量上使用多线程而不进行同步
  • 仅依赖volatile进行复杂操作
  • 在线程间发布未构造的对象

生活中的例子

负面案例

开发者选择通过简单的volatile递增多线程访问网站的入口计数器

优点:

  • 更简单,不需要麻烦synchronized

缺点:

  • 在高负载下由于缺乏原子性,实际丢失递增
  • 竞争条件和不准确的统计数据

积极案例

使用AtomicInteger作为计数器或synchronized方法

优点:

  • 在任何负载下保证正确性
  • 不丢失更新

缺点:

  • 由于同步导致的轻微性能下降