1.volatile是Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不能保证原子性
- 禁止指令重排
面试题:volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性。
2. JMM
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
- 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
多个线程中从主内存拷贝值到各自线程的工作内存,若有一个线程改了当前拷贝过来的这个值,并将修改好的值返回给了主内存,
那么此时其他线程不知道这个线程已经把值改了,此时必须要有一种机制,只要有一个线程修改完自己的工作内存的值,并写回给主内存以后要及时通知其他线程,这样即使通知的这种情况,就是JMM内存模型里面第一个重要特性:俗称:可见性。
JMM模型需要保证:可见性、原子性、有序性
3. 可见性的验证
- 假如int number = 0; number变量之前没有添加volatile关键字修饰,没有可见性。
import java.util.concurrent.TimeUnit;
class MyData{
int number = 0;
public void addto60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData(); //线程操作资源类
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in~");
//线程暂停3秒钟
try{ TimeUnit.SECONDS.sleep(3);}catch(InterruptedException e){e.printStackTrace();}
myData.addto60();
System.out.println(Thread.currentThread().getName()+"\t update number value"+myData.number);
},"AAA").start();
//第二个线程就是我们的main线程
while (myData.number==0){
//main主线程就一直在这里等待循环,直到number不再等于零
}
//若这句话打印出来了,说明main主线程感知到了number的值发生了变化,那么此时可见性就会被触发
System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
}
}
//输出,会卡在while循环处。
AAA come in~
AAA update number value60
- 在int number = 0之前添加volatile关键字修饰,volatile int number = 0;可以保证可见性。
import java.util.concurrent.TimeUnit;
class MyData{
volatile int number = 0;
public void addto60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in~");
try{
TimeUnit.SECONDS.sleep(3);}catch(InterruptedException e){e.printStackTrace();}
myData.addto60();
System.out.println(Thread.currentThread().getName()+"\t update number value"+myData.number);
},"AAA").start();
while (myData.number==0){
}
System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
}
}
//输出
AAA come in~
AAA update number value60
main 60
4. 不能保证原子性的例子
class MyData{
volatile int number = 0;
public void addto60(){
this.number = 60;
}
public void addplusplus(){
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addplusplus();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程都全部计算完成之后,再用main线程取得最终的结果值看是多少?
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
}
}
//输出,如果可以保证原子性,输出结果一定是20000
main 19101
JAVA内存模型(JMM)要求保证原子性,但是volatile是不保证原子性的
number++在多线程下是非线程安全的,如何不加synchronized解决?
可以使用java.util.concurrent.atomic包下的AtomicInteger(带原子性包装的整型类)来解决不保证原子性问题
class MyData{
volatile int number = 0;
public void addto60(){
this.number = 60;
}
public void addplusplus(){
number++;
}
AtomicInteger atomicInteger =new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addplusplus();
myData.addMyAtomic();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程都全部计算完成之后,再用main线程取得最终的结果值看是多少?
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
System.out.println(Thread.currentThread().getName()+"\t"+myData.atomicInteger);
}
}
//输出
main 19492
main 20000
AtomicInteger这个玩意它的底层原理知道吗?
- 答:知道,AtomicInteger底层是CAS
5. 指令重排
6. volatile的使用场景
单机版的单例模式:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance(){
if(instance==null){
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
}
}
单例模式在多线程环境下可能会存在安全问题
单例模式DCL代码,可以保证多线程下的安全问题。如下所示:
public class SingletonDemo {
private static volatile SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
/**
* 双重检测机制
* @return
*/
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <=10; i++) {
new Thread(() ->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
- DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
- 原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
- instance=new SingletonDem(); 可以分为以下步骤(伪代码)
memory=allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null - 步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象 - 但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
- 所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.