volatile
JMM:
内存模型,不是内存布局。每个java线程都有自己的工作内存,操作数据的时候都是从主内存读取,拷贝到自己的工作内存,操作完成写回到主内存。
可能带来可见性、原子性、有序性的问题。
volatile是一种轻量级的同步机制。保证了可见性,有序性,但不能保证原子性。
1. volatile 的可见性测试
class MyData{
volatile int number=0;
public void addTO60(){
this.number=60;
}
}
/**
* 1.验证volatile的可见性
* 1.1加入int number = 0;
*/
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 {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTO60();
System.out.println(Thread.currentThread().getName()+"\t update number valuer"+ myData.number);
},"AAA").start();
//第二个线程就是我们的main线程
while (myData.number==0){
}
System.out.println(Thread.currentThread().getName()+"\t mission is over"+ myData.number );
}
}
mydate资源类,在number没有用volatile修饰是,程序的运行结果是
AAA come in
AAA update number valuer60
用volatile修饰后
AAA come in
AAA update number valuer60
main mission is over60
2. volatile 没有原子性及解决
全部代码:
class MyData2{
volatile int number=0;
public void addTO60(){
this.number=60;
}
public void addPlusPlus(){
number++;
}
AtomicInteger atomicInteger=new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 1.验证volatile的可见性
* 1.1加入int number = 0;number变量之前根本没有添加volatile关键字修饰,没有可见性
* 1.2 添加了volatile,可以解决可见性问题
*
* 2.验证volatile不保证原子性
* 2.1 原子性是什么?
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被分割,同时成功,同时失败
* 2.2 volatile不保证原子性
* 为什么? ++操作本身在字节码层面分成了三个步骤,加入三个线程分别执行++ 操作,都先从主内存中拿到最开始的0,
* 线程1进行++操作后,通知线程2,线程2将自己拿到的数据修改为1,但是线程3执行的也非常快,打断了线程1的通知,造成了写覆盖。
* 所以最后的结果本来应该是3,但是其实是2或者1.
* 2.3 怎么解决?
* 使用AtomicInteger
*/
public class VolatileDemo2{
public static void main(String[] args) {
MyData2 myData2 = new MyData2();
for(int i=1;i<=20;i++){
new Thread(()->{
for(int j=1;j<=1000;j++){
myData2.addPlusPlus();
//开始加入atomicInteger
myData2.addMyAtomic();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程都计算完成后,再用main线程去的最终的结果看是多少?
while(Thread.activeCount()>2){//默认后台有两个线程,一个main,一个gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData2.number);
System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value:"+myData2.atomicInteger);
}
}
在只有volatile修饰的时候,执行结果有可能是20000,但是大部分时候是不到20000,没有原子性。
解决方法: 1. 加synchronized锁—重量级;
2. 用AtomicInteger类,底层使用的是unsafe的getAndAddInt方法。
源码讲解https://baijiahao.baidu.com/s?id=1647621616629561468&wfr=spider&for=pc
main finally number value:19979
main AtomicInteger type finally number value:20000
3. volatile有序性
int n = 1;//1
int m = 5;//2
n = n+6;//3
m = n*n;//4
在多线程的情况下,以上的例子在执行的时候可能出现的顺序有1234、2134、1342,但是结果不变。不会出现4首先执行,应为没有先加载n;
这时不需要指令重排。
volatile底层使用cpu的内存屏障 指令,有两个租用,一个是保证特定操作的顺序,二是保证可见性。
4. 那些地方用过volatile
单例模式:
public class SingletomDemo {
private static SingletomDemo instance=null;
private SingletomDemo() {
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
}
//DCL模式 双端检索机制
public static SingletomDemo getInstance(){
if(instance == null){
synchronized (SingletomDemo.class){
if(instance==null){
instance= new SingletomDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for(int i = 0;i <=10;i++){
new Thread(()->{
SingletomDemo.getInstance();
},String.valueOf(i+1)).start();
}
}
}
常见的加锁的单例模式在多线程下还是有可能有线程安全问题;
memory = allocate();//1.分配内存
instance(memory);//2.初始化对象
instance=memory;//3.设置引用地址
2、3 没有数据依赖关系,可能发生指令重排。如果发生,此时内存已经分配,那么instance=memory不是null。如果此时线程挂起,instance(memory)没有执行,对象没有初始化。犹豫instance!=null ,所以两次判断都跳过,最后返回的instance没有任何内容。
解决方法就是在singletondemo的对象上加上volatile。禁止指令重排。