1.volatile是Java虚拟机提供的轻量级同步机制
- 1.1保证可见性(一个线程对主内存中变量的修改,其他线程会马上收到通知)
- 1.2不保证原子性
- 1.3禁止指令重排
2.JMM(Java Memory Model)(Java内存模型)
JMM本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范顶一了程序各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 1.线程解锁前,必须把共享变量的值刷新回主内存。
- 2.线程加锁前,必须读取主内存的最新值到自己的工作内存。
- 3.加锁解锁是同一把锁。
JMM特性:
- 1.可见性:通过JMM介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的,这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。
- 2.原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
- 3.有序性:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下3种:
源代码=》编译器优化的重排=》指令并行的重排=》内存系统的重排=》最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。
处理器在进行重排序必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都是存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下:
3.volatile关键字保证可见性
示例代码:
class MyData {
//volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
volatile int number = 0;
public void changeNumber() {
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.changeNumber();
System.out.println(Thread.currentThread().getName() + " number has change:" + myData.number);
}, "AAA").start();
while (myData.number == 0) {
//TODO
}
System.out.println(Thread.currentThread().getName() + " finished");
}
}
4.volatile关键字不保证原子性
示例代码:
class MyData {
volatile int number = 0;
public void addadd() {
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
int threadNumber = 20;
MyData myData = new MyData();
CountDownLatch downLatch = new CountDownLatch(threadNumber);
for (int i = 1; i <= threadNumber; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addadd();
}
downLatch.countDown();
}).start();
}
/*while (Thread.activeCount() > 2) {
}*/
try {
downLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " number:" + myData.number);
}
}
运行结果(运行结果number很小概率会出现20000):
main number:19143
Process finished with exit code 0
出现此结果的原因分析:number++操作被拆分成了3个指令:
- 1.执行getfield拿到原始number;
- 2.执行iadd进行加1操作;
- 3.执行putfield写把累加后的值写回;
- 4.在写回主内存的过程中会出现覆盖的情况(部分写数据会丢失)
5.volatile不保证原子性问题解决
- 1.加synchronized锁,对于number++操作,synchronized锁太重量级不推荐。
- 2.推荐使用Java并发包下的atomic原子类(基于CAS)。
6.哪些地方用到了volatile关键字
- 1.单例模式在多线程环境下存在线程安全问题。
示例代码:
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造函数");
}
public static SingletonDemo getInstance() {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
运行结果:
1 构造函数
4 构造函数
3 构造函数
2 构造函数
0 构造函数
解决方法:synchronized方法属于重量级锁,并发性不高,不推荐使用。
public static synchronized SingletonDemo getInstance() {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
- 2.单例模式volatile分析
示例代码:
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造函数");
}
/**
* DCL(Double Check Lock 双端检查机制)
*
* @return
*/
public static SingletonDemo getInstance() {
if (singletonDemo == null) {
synchronized (SingletonDemo.class) {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
运行结果:
0 构造函数
从运行结果上看没有问题,但是实际上存在问题:
DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
原因在于某一个线程执行到第一次检测,读取到的singletonDemo的引用对象没有完成初始化。
singletonDemo=new SingletonDemo();可以分为以下3步完成(伪代码)
memory=allocate();///1.分配对象内存空间
instance(memory);///2.初始化对象
instance=memory;///3.设置singletonDemo指向刚分配的内存地址,此时singletonDemo!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中没有改变,因此这种重排优化是允许的。
memory=allocate();///1.分配对象内存空间
instance=memory;///3.设置singletonDemo指向刚分配的内存地址,此时singletonDemo!=null,但是对象还没有初始化完成。
instance(memory);///2.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程的语义一致性。
所以当一条线程访问singletonDemo不为null时,由于singletonDemo实例未必初始化完成,也就造成了线程安全问题。
此时添加volatile关键字,可以禁止指令重新排序。
public class SingletonDemo {
private static volatile SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造函数");
}
/**
* DCL(Double Check Lock 双端检查机制)
*
* @return
*/
public static SingletonDemo getInstance() {
if (singletonDemo == null) {
synchronized (SingletonDemo.class) {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}