1.什么是volatile?
volatile则是轻量级的synchronized,它不会引起线程上下文的切换和调度。
java语言规范中的定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
通俗的讲就是一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的。如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
2.什么是happens-before原则?
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
解释:
程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。
锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
推导出的其它原则:
-
将一个元素放入一个线程安全的队列的操作Happens-Before从队列中取出这个元素的操作
-
将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作
-
在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作
-
释放Semaphore许可的操作Happens-Before获得许可操作
-
Future表示的任务的所有操作Happens-Before Future#get()操作
-
向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作
3.volatile有哪些特性?
1).原子性:volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如i++;
2).可见性:对一个volatile的读,总可以看到对这个变量最终的写;
3).JVM底层采用“内存屏障”来实现volatile语义。
4.volatile的操作系统语义。
计算机在程序运行中,每条指令都在cpu中运行,在执行过程中势必会涉及到数据的读写,但是数据都是存在主内存中的,读写主内存中的数据没有cpu中的指令执行的快,如果任何的读写都从主内存中必然会影响指令执行的效率,所以就有了cpu高速缓存。cpu高速缓存为cpu独有,只与该cpu执行的线程有关。
有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。
5.volatile的内存语义。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
小结:volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
6.volatile的重排序规则。
-
如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
-
当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
-
当第一个操作volatile写,第二操作为volatile读时,不能重排序。
7.解决数据一致性问题的方案。
-
通过在总线加LOCK#锁的方式;
-
通过缓存一致性协议。
但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
8.volatile底层是如何通过内存屏障实现的?
-
在每一个volatile写操作前面插入一个StoreStore屏障
-
在每一个volatile写操作后面插入一个StoreLoad屏障
-
在每一个volatile读操作后面插入一个LoadLoad屏障
-
在每一个volatile读操作后面插入一个LoadStore屏障
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
9.使用volatile的场景和注意事项。
必须满足以下两个条件:
-
对变量的写操作不依赖当前值;
-
该变量没有包含在具有其他变量的不变式中。
使用场景:状态标记、double check