ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

多线程与高并发-part3

2021-09-21 12:05:56  阅读:181  来源: 互联网

标签:SingletonDemo 并发 number instance part3 线程 volatile 多线程 public


volatile

  1. volatile是Java虚拟机提供的轻量级同步机制
  2. 特点
    1. 保证可见性
      • JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
    2. 不保证原子性
    3. 禁止指令重排

JMM

  1. 就是Java内存模型
  2. 规定:
    • 线程解锁前,必须把共享变量值刷新回主内存
    • 线程加锁前,必须把读取主内存的最新值到自己的工作内存
    • 加锁和解锁是同一把锁。
  3. JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。
  4. 具体过程

数据传输速率:硬盘<内存<<cache<CPU

Volatile保证可见性验证

/**
 * 假设是主物理内存
 */
class MyData {

    int number = 0;

    public void addTo60() {
        this.number = 60;
    }
}

/**
 * 验证volatile的可见性
 * 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
 */
public class VolatileDemo {

    public static void main(String args []) {

        // 资源类
        MyData myData = new MyData();

        // AAA线程 实现了Runnable接口的,lambda表达式
        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t come in");

            // 线程睡眠3秒,假设在进行运算
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            myData.addTo60();

            // 输出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);

        }, "AAA").start();

        while(myData.number == 0) {
            // main线程就一直在这里等待循环,直到number的值不等于零
        }

        // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
        // 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
        System.out.println(Thread.currentThread().getName() + "\t mission is over");

        /**
         * 最后输出结果:
         * AAA	 come in
         * AAA	 update number value:60
         * 最后线程没有停止,并行没有输出  mission is over 这句话,说明没有用volatile修饰的变量,是没有可见性
         */

    }
}
  • 结果:线程未停止,并行没有输出该输出的话。
  • 修改:给number 加上Volatile关键字。最后主线程执行完毕,该输出的也输出了。

Volatile不保证原子性

  • 原子性:要么同时成功,要么同时失败

测试


/**
 * Volatile Java虚拟机提供的轻量级同步机制
 *
 * 可见性(及时通知)
 * 不保证原子性
 * 禁止指令重排
 */

import java.util.concurrent.TimeUnit;

/**
 * 假设是主物理内存
 */
class MyData {
    /**
     * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
     */
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    /**
     * 注意,此时number 前面是加了volatile修饰
     */
    public void addPlusPlus() {
        number ++;
    }
}

/**
 * 验证volatile的可见性
 * 1、 假设int number = 0, number变量之前没有添加volatile关键字修饰
 * 2、添加了volatile,可以解决可见性问题
 *
 * 验证volatile不保证原子性
 * 1、原子性指的是什么意思?
 */
public class VolatileDemo {

    public static void main(String args []) {

        MyData myData = new MyData();

        // 创建10个线程,线程里面进行1000次循环
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
        // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
        while(Thread.activeCount() > 2) {
            // yield表示不执行
            Thread.yield();
        }

        // 查看最终的值
        // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);

    }
}
  • 多次测试之后,值都小于20 * 1000

原因

  • 一个n++指令在字节码文件中的指令被拆分成三个
    1. 执行getfield 从主内存拿到原始n
    2. 执行iadd 进行加1操作
    3. 执行putfileld 把累加后的值写回主内存
      假设三个线程同时拿到主内存的值,然后三个线程分别在自己的工作内存中进行+1操作,但是并发进行的idd,又因为写操作只能有一个,假设此时是1号线程正在写,写完了,
      volatile的可见性原因,告知其他线程,主内存值已经被修改,但太快了,其他两个线程陆续执行了idd命令进行写操作,这就造成了其它线程没有接收到主内存的n改变,从而覆盖了原来的值,出现写丢失
  • 解决:
    1. 方法前面加synchronized关键字
    2. synchronized关键字是一个同量级同步机制,并发性降低,考虑使用JUC下面的原子包装类,使用AtomicInteger代替
/**
*  创建一个原子Integer包装类,默认为0
*/
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
    // 相当于 atomicInter ++
    atomicInteger.getAndIncrement();
}

volatile禁止指令重排

  1. 计算机在执行程序的时候,为了提高性能,编译器通常会对指令进行重排。
  2. 处理器在进行重排序的时候,必须要考虑指令之间的数据依赖性(要先有数据的声明才能进行值操作)
  3. 使用volatile进行读写的时候加入了屏障,防止出现指令重排

单例模式

  1. 这是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) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
  • 结果:并没有保证只有一个实例的创建,是被一些线程创建了。
  • 思考:多环境下如何保证单例呢?
  1. 加synchronized关键字,把获取实例的方法变为同步方法。
public synchronized static SingletonDemo getInstance() {
  if(instance == null) {
      instance = new SingletonDemo();
  }
  return instance;
}

缺点:synchronized属于重量级同步机制,降低了并发性
2. 使用双端检锁机制

public static SingletonDemo getInstance() {
  if(instance == null) {
      // 同步代码段的时候,进行检测
      synchronized (SingletonDemo.class) {
          if(instance == null) {
              instance = new SingletonDemo();
          }
      }
  }
  return instance;
}
  • 问题:因为指令重排的原因,在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。在初始化的时候,可以分为三步
    1. memory = allocate(); // 1、分配对象内存空间
    2. instance(memory); // 2、初始化对象
    3. instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
    步骤2和步骤3不存在数据依赖关系,所以可以进行重排的。
    1. memory = allocate(); // 1、分配对象内存空间
    2. instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
    3. instance(memory); // 2、初始化对象
    造成问题:执行到步骤2的时候,视图获取instance,会得到null,就因为此时对象的初始化还没有完成,而是在第三步才完成的,这样就造成了线程安全问题。

最终版

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            // a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
            synchronized (SingletonDemo.class) //b
            { 
           //c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
                if(instance == null) { 
                	// d 此时才开始初始化
                    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());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

标签:SingletonDemo,并发,number,instance,part3,线程,volatile,多线程,public
来源: https://www.cnblogs.com/yunge-thinking/p/15316263.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有