很多时候我们需要并发。逻辑上的“并发”就是同时做很多事情。并发常常能成倍提升吞吐量。
然而,在多个线程参与并发时,程序的执行顺序就会变得不确定。各种问题也随之而来。
经典的竞态条件问题,会让两个程序几乎同时运行时,结果和一前一后运行不同,且常常是不想看到的。这是因为它们缺乏原子性(Atomicity)。
可见性(Visibility)和有序性(Ordering)是另外两个核心特性。它们相对来说不那么直观,但很多看起来习以为常的程序逻辑,背后有它们的保证。
无论在什么编程语言,并发编程总是最重要的课题之一。
关于Java内存模型
Java内存模型和Java内存结构不一样。这是一个抽象的概念,主要和并发编程有关。
例如,堆和栈是Java内存结构的概念,但不是Java内存模型的。它更关心内存的所有者。在Java内存模型中,每个线程有自己的本地内存,同时有一个共享的主内存。
如果本地内存对于同一个共享变量有缓存,就可能出现可见性问题。一个经典的例子是,在其他线程已经修改公共变量后,缓存没能及时更新,程序把缓存中已过时的值当成了正确的。此时即使确保原子性,也会出现并发问题。
有序性问题比原子性和可见性更加晦涩。它不会导致经典的“竞态条件”,但在前驱、后继逻辑复杂时,它会导致各种错误的假设,产生隐蔽的问题。
The volatile
一个奇妙的关键字,用于修饰变量。它有两个主要作用:保证可见性和禁止指令重排序(这会导致有序性问题,然而编译器与CPU喜欢这么做,因为单线程下它们会带来帮助)。
它本身不会保证原子性,如果想用它保证并发安全,必须自己确保原子性(例如引入CAS)。
业务开发者一般不会接触到volatile,因为它们被封装在了各种原子类和并发友好数据结构中,例如AtomicInteger和ConcurrentHashMap。
CAS (Compare-And-Save)
CAS是一种操作,它会比较目标值是否等于某个设定值,如果相等则设定为另一个设定值,否则什么也不做(会传回这个消息)。整个操作是原子的,由硬件提供保证。
在Java,使用CAS的方法在sun.misc包的Unsafe类中。这个名字听起来就不是很友善,实际也确实如此–Java强烈不建议开发者直接使用它们,除非在开发底层构建,因为它们很难驾驭,容易写出问题。
常见的方法,了解即可:
compareAndSwapObject(Object o, long offset, Object expected, Object x)
compareAndSwapInt(Object o, long offset, int expected, int x)
compareAndSwapLong(Object o, long offset, long expected, long x)
这些方法直接映射底层的native方法,具体使用C++内联汇编和JNI调用实现。不当的操作(例如写错offset–这是内存地址偏移)可能会导致JVM崩溃。
业务开发者只需知道它们的原理,以及常用的原子类大量依赖它们。
ABA问题
这是CAS算法经典的问题。由于CAS只会比较目标值是否发生变化,“A被改成B再改回A”的情况就无从得知。
如果更改它们的函数完全没有副作用,这没有任何问题–但很多函数不是这样的,它们包含某些另外的约定。使用CAS的函数认为什么也没发生,但那个有副作用的函数已经执行了两次。
解决方法很简单:加递增版本号或时间戳。这可以保证只要发生过更改,CAS就永远不会判断“从未被更改过”。
这看起来很像数据库的乐观锁。实际上它们使用几乎一样的逻辑。
资源开销
和数据库乐观锁不同,CAS在发现冲突时会不断重试。这常常使用自旋锁实现–一个while不停地判断。这会让CPU用量暴涨。
通过延迟流水线执行指令(添加固定时长的等待),此问题可被有效缓解。不过,这取决于处理器的具体实现。某些处理器可能不会等待。
发表回复