Java中的原子类(如AtomicInteger)是如何利用CAS实现线程安全的?

上周,我的朋友给我讲了一个关于 Java 原子类的故事。
他表示,Java中的原子类,例如AtomicInteger,是通过CAS指令结合硬件支持来实现线程安全的。
听起来很复杂,不是吗?
2 02 3 年,刚开始学习Java多线程编程时,就听说了CAS。
这个东西是CPU提供的低级原子指令,它有三个操作数:内存位置V、期望的原始值A、新值B。
如果V等于A,则将V更新为B。
整个过程由硬件保证是原子的,不会被其他线程打断。

记得有一次,我在网上看到一个AtomicInteger的getAndIncrement()的例子。
线程读取当前值A并尝试使用CAS将V从A更新为A+1 如果V被其他线程修改并且CAS失败,则该线程进入自旋循环,重新读取最新值并重试,直到成功。

朋友说CAS相比传统锁有明显的效率优势。
传统的锁是悲观策略,例如synchronized/reentrantLock,它们假设冲突不可避免地会发生,并通过加锁来阻塞其他线程。
CAS是无锁设计。
可以尝试直接修改,失败后旋转再试。
在低争用场景下,CAS 一般会成功一次,避免所有开销锁并提高吞吐量和响应速度。

但是,他也就是说,CAS 也有问题,就像 ABA 问题一样。
当变量V从A→B→A变化时,CAS假设值没有变化,但中间状态变化可能会导致逻辑错误。
例如,如果银行账户余额从 1 00(A) → 1 5 0(B) → 5 0(A) 发生变化,CAS 会将其视为未更改,从而导致更新不正确。

朋友说,为了解决这个问题,Java提供了AtomicStampedReference和AtomicMarkableReference。
前者引入了版本标记,CompareAndSet 需要同时匹配值和版本标记;后者只是使用布尔标记来确定值是否已被修改。

他还告诉我,Java并发包中有很多原子类,比如AtomicLong、AtomicBoolean、AtomicReference、AtomicStampedReference、AtomicMarkableReference、AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray、LongAdder、DoubleAdder等,它们适用于各种场景,比如计数/统计、状态标签、对象引用更新、避免ABA问题、数组操作、高并发缓存等
朋友最后说,总结一下,Java原子类通过CAS指令实现无锁线程安全,在低争用场景下其性能优于传统锁。
针对ABA问题和高并发场景,提供版本戳控制、分段缓存等优化方案,覆盖从基本类型到复杂引用更新的多种并发需求。
听起来很神奇,不是吗?

Java Collections.synchronizedList如何保证线程安全

哈,我用了你前面提到的Collections.synchronizedList。
简单来说,它是Java中用来保证线程安全的一种方法,特别是在多线程环境下操作列表数据时。

首先,它通过同步代理机制工作。
这意味着它不会直接修改原始列表,例如ArrayList,而是返回一个同步代理对象。
该代理对象将包装原始列表,然后覆盖所有公共方法,例如添加、获取、删除,并在这些方法中添加同步检查。
这样,每当调用这些方法时,都会通过synchronized关键字锁定它们,以确保只有一个线程可以同时执行这些方法。

例如,如果您有一个同步列表,则可以在一个线程中安全地添加一项,然后在另一个线程中安全地检索该项,因为每个操作都是线程安全的。

但是需要注意的是,这种同步方法只保证了一种方法中的线程安全。
如果需要执行一系列操作,例如检查列表中是否包含某个项目然后添加该项目,这种复合操作需要手动同步。

此外,迭代器也需要特别注意。
虽然列表是同步的,但是如果其他线程在迭代器遍历过程中修改了列表,就会抛出ConcurrentModificationException。
因此,穿越时也必须手动上锁。

从性能上来说,synchronizedList在低并发场景下效果很好,但如果在高并发环境下,就会因为实例锁的存在而成为性能瓶颈。
此时您可能需要考虑其他并发容器,例如CopyOnWriteArrayList,或者使用显式锁定机制,例如ReentrantLock。

总的来说,Collections.synchronizedList是一个简单易用的线程安全工具,但在高并发环境下可能需要更复杂的解决方案。
开发人员使用它时,必须小心手动同步复合操作和迭代器,以避免并发问题。
无论哪种方式,您都可以根据自己的具体需求选择合适的解决方案。
我时常思考这个问题。
毕竟,并发编程有时是相当复杂的。

在Java中如何保证Map操作线程安全

谈论Java线程安全确实很头疼,但有时你必须经历一番茫然之后才能给别人可靠的建议。
比如我几年前参与的一个项目,还在使用Java 7 ,当时大家都说ConcurrentHashMap比较好用,所以我就跳了起来。

当时项目有一个很大的地图,需要存储各种用户操作记录。
读多于写,但并发量却丝毫不减。
我一开始就选择了ConcurrentHashMap,认为它读写分离,应该有不错的性能。
结果一上线就出现了问题。
这不是性能问题,而是迭代问题。

记得有一次,我修改了迭代器中的一个元素,直接抛出了ConcurrentModificationException。
当时我发现ConcurrentHashMap的迭代器是弱一致的。
这仅保证了迭代器创建时的数据一致性,以后可能不正确。
这个威胁当时差点让我被解雇。

后来查资料,了解到Collections.synchronizedMap。
简单易用,适合低并发或者临时线程安全的场景。
我用这个方法来包装Hashmap,问题就解决了。
但是,使用此方法时要小心。
穿越时您必须手动锁定,否则问题将会持续存在。

还有另一个项目。
由于有很多遗留代码,我需要一个哈希表一定会用到。
说实话,这个库现在很少用了,性能一般,但是兼容性不错。
我只是用它来进行新旧系统之间的接口转换,反正也不是频繁的操作。

最头疼的是手动同步。
有一次,我手动给一个方法加了synchronized锁,但是锁的范围控制不好,导致其他线程等待。
这种危险让我意识到,手动同步虽然灵活,但是容易出现问题,维护成本也很高。

简而言之,我的建议是:如果你不能使用锁,那就不要使用它们。
当你确实需要使用它们时,优先选择ConcurrentHashMap,对于简单场景使用Collections.synchronizedMap,对于遗留代码使用Hashtable,最后使用手动同步。
请记住,锁粒度越小,性能越好,但也应该注意稳定性问题和锁争用。
哈哈,这是我的经历。