JMM和Votitle关键字

  1. 什么是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关键字,禁止指令重排。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值