常见Java面试题:我们说StringBuilder是线程不安全的,是什么原因呢?

哎呀,StringBuilder 这个东西真是一个老话题了。
老实说,多年来我在问答论坛上多次被问到这个问题。
现在我们来谈谈为什么它不是线程安全的。

首先,StringBuilder类本身在设计时并没有考虑到同步机制。
如您所知,该方法中的所有方法都没有被设计为线程安全的。
就像你开着一辆车,路上有很多人,但车只有一个刹车,你想什么时候停就什么时候启动,想什么时候启动就什么时候启动。
没有红绿灯。
这不安全。

此外,在使用多个线程时也会出现此问题。
例如,如果线程A和线程B想要同时修改StringBuilder,如果没有同步机制,它们可能会互相覆盖并弄乱数据。
我见过很多这样的情况,尤其是在高并发场景下。
例如,2 008 年北京奥运会期间,当时的论坛系统就频繁出现此类问题。

第二,虽然StringBuilder在单线程环境下表现非常好,但这种性能优势在多线程环境下就变成了隐患。
为了少量的性能提升而牺牲了线程安全。
就像骑电动车闯红灯一样。
会发生什么事情吗?
因此,在多线程环境中使用 StringBuilder 时必须特别小心。
您应该使用线程安全的 StringBuffer 或自己锁定它以确保线程安全。
当时我不太明白,但是后来看了各种资料,我才渐渐明白了。

这意味着这个StringBuilder线程不安全。
主要原因是没有同步机制,容易出现问题。
使用时必须小心。

java经典面试题——并发编程-java内存模型JMM

我记得有一次我正在写一个多线程程序。
线程A更改共享变量后,线程B很长时间无法读取最后的值。
我花了很长时间才弄清楚问题是由JMM重新排序引起的。

例如: 爪哇 类 ReorderExample { 区间 a = 0; 布尔标志 = false;
void write() { a = 1 ; // 操作1 标志=真; // 操作 2 (可以与操作 1 重新排序)
无效读取(){ if (flag) { // 操作 3 整数 i = a; // 操作4 (由于重新排序,可能会读取旧值) } }
如果线程A先执行flag = true,线程B可以直接进入if块,但是a的值仍然是0。
这是一个典型的重排序问题。

后来想了一下,本质上JMM应该在多线程编程上划清界限,让不同的JVM能够达到一致的结果。
它关心三件事:重新排序、原子性和内存可见性。

关于重新排序最烦人的事情是编译器和处理器随机改变指令的顺序以便更快地工作。
比如原本执行为1 ,2 ,3 ,可能会变成1 ,3 ,2 ,如果这个在多线程中使用,那就乱了。
爪哇 无效条目(){ a = 1 ; // 1 标志=真; // 2 (向前移动)
如果线程B先读取flag = true,但a还没有更新,就会发生错误。

Atomic比较简单,像i++,其实分为三个阶段:读、修改、写。
如果线程 A 只是读取 i,线程 B 修改 i,而 A 继续写入,事情就会变得混乱。
这应该使用同步来锁定。

内存浏览是个大问题。
如果一个线程更改了该值,另一线程如何知道这一情况?如果没有同步,CPU可能会将值存储在缓存中并继续使用旧值。

JMM还制定了“发生在之前”的规则,例如运营计划。
谁先谁后应该很清楚。
例如,易失性写入发生在后续读取之前,并且锁释放发生在后续锁定之前。

波动指标相当有趣的。
这就像一个小广播。
一旦写操作完成,其他线程将立即知道它。
而且,编译器和CPU不能任意重新排列包含易失值的指令。
爪哇 易失性布尔初始化 = false;
void init() { 初始化=真; // 写入可变的
无效检查(){ while (!初始化) Sleep(); // 读取易失性 使用配置();
线程B依靠易失性可见性来不断获取线程A初始化的值。

(同步)锁很重,就像警卫的锁一样。
谁进入、谁离开都必须排队,这也保证了原子性。
但性能还不如不稳定。

最后一个字段也很有趣。
在构造函数中初始化最终值后,当其他线程读取对象引用时,它们可以保证看到最后的值而不会阻塞。
因为JMM不允许在构造函数中重新排序写入操作和对象引用赋值。

总之,JMM有这些规则可以让多线程开发不再那么令人头疼。
但选择哪种机制要根据场景而定。
使用波动性来多读少写,并使用锁来提供复杂的同步。

等等,我最近看到一篇文章,说有些JIT编译器也优化了易失性?这很有趣,我下次一定要尝试一下。