并发编程的三个特性
原子性
一个操作或者多次操作,所有的操作全部都执行且不会受到任何因素的干扰而中断。要么都执行,要么都不执行。synchronized可以保证代码片段的原子性。
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到最新修改的值。volatile关键字可以保证共享变量的可见性。
有序性
代码在执行的过程中先后顺序,java在编译器以及运行期间的优化,代码的执行顺序未必就是我们编写的代码的顺序。volatile关键字可以禁止指令进行重排序优化。
Java如何实现原子操作
- 在Java中可以通过锁和循环CAS实现原子操作
使用循环CAS实现原子操作
- JVM中的CAS是利用了处理器的CMPXCHG指令实现的,自旋CAS的基本思路就是循环进行CAS直到成功为止。
- cmpxchg(void *ptr, unsigned long old, unsigned long new);
函数的功能是:将old与ptr指向的内容比较;如果一样,就将new的写入到ptr中,返回old;如果不相等,则返回ptr指向的内容。 - 在Java中可以使用Unsafe.class,里面提供了compareAndSwap()的native的方法。
- 在JDK的并发包中提供了一些原子类来支持原子操作,例如:AtomicReference、AtomicInteger等
使用CAS引起的三个问题:
ABA问题。CAS在操作值的时候,需要去检验值有没有发生变化,如果没有发生变化就更新。如果一个值最开始是A,中间变成B,后面又变成了A。那么CAS操作的时候,会觉得它没有变化,实际上是变化了。可以使用版本号来解决ABA问题。在JDK中提供了AtomicStampedReference类来解决ABA问题,对每一个相应的reference都有一个对应的stamp,如果引用和这个印记都相等,则更新。
循环时长开销大。自旋如果长时间不成功,会给CPU带来非常大的开销。可以设置超过一定执行时间或者一定次数的时候,就退出循环。
只能保证一个共享变量的原子操作。当一个共享变量执行操作时,我们可以采用原子类来保证原子操作。貌似多个变量的执行原子操作时候,我们可以采用AtomicReference来保证对象的原子性。
锁机制实现原子操作
锁机制保证了只有获得锁线程才能操作锁定的内存区域。除了偏向锁,JVM进入同步块的时候,采用循环CAS方式获取锁;退出同步块的时候,采用CAS方式来释放锁。
Java如何实现可见性
- JMM(java memory model)通过主内存与每个线程之间的交互,来为代码提供内存可见性的保证。
- 如上图所示,线程A把本地内存更新过的变量更新到主内存中,线程B到主内存中去读取线程A更新过的共享变量。
- 为了保证可见性,Java编译器在生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序。
Load Barrier 读屏障。在指令之前插入读屏障,可以让高速缓存的变量失效,强制从主内存中加载数据。
Store Barrier 写屏障。在指令之后插入写屏障,可以让写入缓冲中最新的变量的值更新到主内存中,让其他线程可见。
Java如何实现有序性
- 重排序是编译器和处理器为了优化程序性能而对指令序列进行重排序的一种手段。
- as-if-serial 不管处理器编译器怎么重排序,程序的执行结果不能被改变。
- happens-before
传递性:A happens-before B,B happens-before C,那么A happens-before C。
volatile变量规则:对一个volatile变量写入,happens-before任意一个对该变量的读取。
监视锁的规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
程序的顺序规则:一个线程的每个操作,happens-before该线程中任意后续操作。