-
什么是JMM模型
JMM是java内存模型(Java Memory Model),不是java内存布局,不是所谓的栈、堆、方法区;
每个java线程都有自己的工作内存,操作数据时首先从主内存中读取数据,得到一份变量的拷贝,线程操作完成后再将操作结果写回主内存中;
JMM的三大特性:
- 可见性 线程对主内存变量的修改应该会立即通知其他的线程;
- 原子性 指一个操作时不可分割,不能执行到一半就被其他的线程打断
- 禁止指令重排 操作指令是有序的,不能被重排;
2、Votitle关键字的理解
volititle是java的关键字,是java提供的一种轻量级同步机制,也就是低配版本的Sychronized,能保证可见性与禁止指令重排,但是不能保证原子性;
可见性:
/**
* 可见性测试
* 可见性即在多线程操作时会保证修改后的变量对于其他的线程可见
* 1、变量在主内存中
* 2、各个线程操作时会将变量复制一份保存至单个线程内部变量中操作
* 3、线程修改后将修改后的数据反写回主内存,开启通知机制
*/
private static void volatitleVisibilityDemo() {
System.out.println("visiable test is start");
MyData myData = new MyData(); //资源类
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try {
TimeUnit.SECONDS.sleep(3);
myData.setTo60();
System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
while (myData.number == 0){
}
System.out.println(Thread.currentThread().getName()+"\t mission is over.main get number value:"+myData.number);
}
}
class MyData{
int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
public void addAtomic(){
atomicInteger.getAndIncrement();
}
MyData 是资源类,一开始number变量没有用volititle修饰,所以程序运行结果是;
visiable test is start
AAA come in
AAA update number value: 60
//虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。
如果对number添加了volatile修饰,运行结果是:
visiable test is start
AAA come in
AAA update number value: 60
main mission is over.main get number value:60
如果使用volatile修饰的变量修改后会立即体现在主内存上;
原子性
volatile不能保证原子性是因为,比如一条number++的操作,会形成3条指令;
getfield //读
iconst_1 //++常量1
iadd //加操作
putfield //写操作
假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。
如何解决原子性问题?
1、对addPlusPlus()方法添加锁。
2、使用java.util.concurrent.AtomicInteger类
/**
* 原子性测试
* 1、所谓原子性就是线程的操作不被中断
* 2、AtomicInteger能保证原子性,但是Volititle修饰的变量不能保证原子性
* 3、原因是因为AtomicInteger的底层是Unsafe类方法的
* int var5;
* do {
* var5 = this.getIntVolatile(var1, var2);
* } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
*
* return var5;
* 也就是CAS理论
*/
private static void atomicDemo() {
System.out.println("原子性测试!");
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int type finally number value:" + myData.number);
System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value:" + myData.atomicInteger);
}
结果:可见,由于volatile
不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger
可以保证原子性。
原子性测试!
main int type finally number value:16728
main AtomicInteger type finally number value:20000
有序性
有序性的理解:volatile能保证有序性,也就是防止指令重拍,所谓指令重排也是出于优化的考虑,CPU的指令顺序跟程序员自己编写的顺序不一致,就好比一份考卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以选择先做简单的后做难的;
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x * x; //语句4
以上例子可能出现的顺序有1234,2134,1342,三个都没有问题,结果都是x=16,y=256,但是如果是4开头的话,就有问题了,y=0,这个时候就不需要指令重排;
volatile是如何实现有序性的?
volatile底层是CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作顺序性,一个是保证变量对主内存的可见性,在指令之间插入一个Memory Barrier指令,告诉编译器和CPU,在Memory Barrier之间的指令禁止重排;
3、如何重用Volatile
单例模式的安全问题
先看一个问题:
package com.example.demo.thread;
public class SingletonDemo {
private static SingletonDemo singletonDemo =null;
public SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法!");
}
private static SingletonDemo getInstance() {
if(null ==singletonDemo){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i <20 ; i++) {
new Thread(()->{
singletonDemo.getInstance();
},String.valueOf(i+1)).start();
}
}
}
1 我是构造方法!
3 我是构造方法!
2 我是构造方法!
4 我是构造方法!
5 我是构造方法!
6 我是构造方法!
出现了6此的构造,说明有14次可以直接获取在主内存中获取类,但是有6个线程没有获取到类实例;单例模式不能复用;
解决方案:
常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。
//DCL(double check lock )双端加锁机制,虽然进行了同步机制但是在多线程情况下还是会有线程安全的问题
private static SingletonDemo getInstance() {
if(null ==singletonDemo){
synchronized (SingletonDemo.class){
if (null == singletonDemo){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();
可以大致分为三步
memory = allocate(); //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址
其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory
不为null。如果此时线程挂起,instance(memory)
还未执行,对象还未初始化。由于instance!=null
,所以两次判断都跳过,最后返回的instance
没有任何内容,还没初始化。(也就是还没有初始化对象就先分配引用地址)
解决的方法就是对singletondemo
对象添加上volatile
关键字,禁止指令重排。