在Java虚拟机中通过Volatile关键字提供了轻量级的同步机制:保证可见性、不保证原子性、可禁止指令重排。
- 当一个变量被
Volatitle
修饰了之后,JMM会把该线程的本地内存的共享变量刷新到主内存;同时会将本地内存置为无效,线程接下来会从主内存中读取共享变量。 ==> 也就说使用主内存保证了可见性。 - 对于指令重排我们知道是在不影响单线程程序执行结果的前提下JMM优化的一种结果。那么可以使用
Volatitle
禁止指令重排,从而实现有序性。 - 而有序性的详细实现是基于
Volatile
的happens-befor
关系:其实就是一个传递性的连接关系,A > B, B > C => A > C
,这种连接关系就是happens-befor
关系。(补充数学原理:偏序关系 ->偏序关系是集合中元素之间的一种关系,它满足反身性(对于任意元素a,a和自己存在关系)、反对称性(如果a和b存在关系,且b和a存在关系,则a和b必须相等)和传递性(如果a和b存在关系,b和c存在关系,则a和c也必须存在关系)。
)
1),应用场景:状态变量标记
对于某个状态变量被多个线程共享的时候,其他的线程需要能及时感知到这个变量的变化情况,则可以通过volatile
关键字修饰变量,以此可以保证修改对其他线程立刻可见(实现的原理:将变量直接放在主内存中,并且不会被线程缓存)。另外这种方式对于读写操作都是无锁的,它无法完全替代 synchronized、Lock
,因为它并没有提供原子性,但是基于多线程共享层面来讲,由于不存在加锁和释放锁的动作,所以其成本是低于synchronized、Lock
的。
2),应用场景:单例模式
典型双重检查锁定(DCL),如下代码(创建对象
)分析:instance= new Singleton();
这段代码其实是分为三步执行:
- 为
instance
分配内存空间 - 初始化
instance
- 将
instance
指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance
() 后发现 instance
不为空,因此返回 instance
,但此时 instance
还未被初始化。使用 volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
class Singleton{
// 通过volatile禁止指令重排
private volatile static Singleton instance = null;
private Singleton() {
}
// 通过静态方法将创建对象的方法提供给外部调用
public static Singleton getInstance() {
//第一次加锁是为了性能考虑,因为线程安全发生在对象初始化,如果第一次不判断相当于全局控制,造成浪费。
if(instance == null) {
synchronized (Singleton.class) {
// 双检锁:即通过两次判断来避免两个线程同时创建对象,这样当第一个创建对象完成,第二个会直接结束,不再创建
if(instance==null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
3),Volatile为啥不能保证原子性?
因为Volatile关键字是通过将变量保存到主内存中实现的可见性,即便多个线程对于该变量的值是可见的,但是因为多线程竞争阻塞(比如计算i++
),可能会出现线程A、B同时读到了变量的值0,但是A阻塞了,B获取将值加1,改变了变量的值,但是对于线程A来说,也早就读取到了变量的值为0此时它也会加1,那么并不会出现结果2,而是由1到1,即最终仍然是1的情况。
public class DemoTest {
// 如果是使用volatile修改的i++,是无法保证原子性的,每次输出的结果都是不一致的
// static volatile int i = 0;
// 使用并发安全的集合,此时计算结果是正确的100000
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(
() -> {
for (int k = 0; k < 50000; k++) {
// i++;
atomicInteger.incrementAndGet();
}
System.out.println("线程执行完毕");
}
).start();
}
// 当前等待是必然要有的,不然计算结果始终为0
Thread.sleep(1000);
// System.out.println("最终计算结果:" + i);
System.out.println("最终计算结果:" + atomicInteger.get());
}
}
网友评论