在学习此关键字之前,我们先了解一下JMM规范和并发编程中的三个概念。
1、JMM规范
JMM(Java module memory,Java内存模型)是一个抽象概念,并不真实存在于内存。它是用于定义程序中各个变量(成员变量、类变量、数组元素等)的一组规范和规则,指定变量的访问方式。
规定: \color{red}{规定:}规定:
- 线程解锁之前必须将共享变量刷新回主内存;
- 线程加锁之前必须读取主内存中变量的最新值到工作空间;
- 解锁和加锁必须是同一把锁。
大家可能不解其意,这就需要涉及另一个概念:线程空间 \color{green}{线程空间}线程空间.。
什么是线程空间? \color{grey}{什么是线程空间?}什么是线程空间?
程序执行JMM规范的实体是线程,当线程创建时,JMM会为其创建一个私有内存(也称为工作内存、本地内存或栈空间 工作内存、本地内存或栈空间工作内存、本地内存或栈空间)。JMM规定所有变量都保存在主内存,线程访问变量时需为变量创建一个副本至工作内存进行操作,完成后将变量值返回主内存,且线程通信在主内存进行。
2、并发编程的三个概念
- 可见性: \color{green}{可见性:}可见性:指线程对变量的修改,其他线程可见;
- 原子性: \color{blue}{原子性:}原子性:指线程对变量的操作的整个过程不会被阻塞或分割;
- 有序性: \color{brown}{有序性:}有序性:也称为“指令重排” \color{red}{“指令重排”}“指令重排”,指程序运行时,编译器基于提高性能需要,以指令间的数据依赖性作为依据对指令进行重新排列。执行顺序:编译器重排 → 指令并行重排 → 内存系统重排。
3、volatile
3.1 概述
volatile
是一种轻量级的同步机制,而synchronized
属于重量级(“级”是指对变量访问的限制程度)。
volatile
遵循JMM规范实现了可见性和有序性,但不保证原子性。因此,限制线程在访问由volatile
修饰的变量时,从主内存获取数据,而不是从工作内存,在数据操作完成后再刷新回主内存,故在保证原子性的情况下,可实现线程安全。
如何保证原子性?如程序中不存在多线程对变量进行非原子性操作,举个例:a++
是原子操作,而a+=1
不是。
3.2 volatile 的一个经典应用
关于单例模式,可查阅博文《关于对Java单例模式的理解与简述》。
从文中可知,“双重检测机制” \color{green}{“双重检测机制”}“双重检测机制”可解决“懒汉式”的线程安全问题。其实,“双重同步锁”也有漏洞。
以那篇博文的示例为例:
instance = new Singleton();
实例化分为三步:
- 创建实例,分配内存;
- 实例初始化;
- 令
instance
指向此实例。
其中,2和3都依赖于1,而2与3之间没有依赖关系,故指令重排会将2与3对调(原因可能是实例初始化耗时较长)。
因此,当instance
指向实例时,实例可能还未初始化,下一个线程就会出现并发问题(暂不清楚原因),用volatile
禁止指令重排即可解决。
4、volatile 的运用
从上文可知,volatile
关键字可以解决这两种情形下的线程安全问题。
- 多线程并发访问变量,线程体中不存在非原子操作的情况;
- 弥补双重同步锁 \color{green}{双重同步锁}双重同步锁的漏洞。
我们一一进行测试。(学以致用)
4.1 情形一
创建10个线程对同一个成员变量并发修改一万次。
volatile int a; public static void main(String[] args) throws Exception { C c1 = new C();// C 是当前类名 int i = 10; while (i-- > 0) { new Thread(() -> { int n = 10000; while (n-- > 0) { c1.a++; } }).start(); } Thread.sleep(10000);// 主线程停留10s足以保证10个子线程运行完成 System.out.println(c1.a); }
最后c1.a
的输出结果并不是100000
(10s足够10个子线程执行完成)。可见,并未解决线程安全问题。
4.2 情形二
多线程并发调用newInstance()
获取单例模式类实例。
实体类。
class SingleTon { private static SingleTon instance; private SingleTon() {} public static SingleTon newInstance() { if (instance == null) { synchronized (SingleTon.class) { if (instance == null) { instance = new SingleTon(); } } } return instance; } }
测试:创建一万个线程并发调用newInstance()
,判断获取的实例是否都为单例。
List<SingleTon> list = new Vector<>(); int i = 10000; while (i-- > 0) { new Thread(() -> { SingleTon s1 = SingleTon.getInstance(); if (list.size() > 0 && list.indexOf(s1) == -1) System.out.println("违反单例");// 未执行 list.add(s1); }).start(); } Thread.sleep(1000); System.out.println(list.size());// 10000
Vector 类线程同步,故list.add(s1)
也是线程同步的。
未打印“违反单例
”,表示list
中存储的所有s1
都指向同一个实例,保证了“单例”,说明线程安全。
不过,还证明不了这是volatile
的功劳,因为双重检测机制 \color{blue}{双重检测机制}双重检测机制本身对线程安全就有很大的保证性。
于是,我把10000
改成了100000
,好吧。。。还是未打印“违反单例
”。
最后
大家肯定也看出来了,在上面的示例中,我的本意是想创建十万个线程调用newInstance()
,通过是否打印“违反单例
”来触发双重同步锁 \color{brown}{双重同步锁}双重同步锁的漏洞,然后用volatile
声明instance
来解决线程安全问题,可失败了。。。
因此,我对volatile
关键字的理解还不够透彻。
本文的目的是为了让大家对volatile
关键字有一个初步的了解,我继续努力!
本文完结。