JUC多线程及高并发之volatile

一、volatile

1、volatile是什么?

volatile是java虚拟机提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

1.1、volatile可见性验证

不加volatile代码

package com.nuc;

import java.util.concurrent.TimeUnit;

/**
 * @Author yrx
 * @Date 2020-04-18 16:43
 * 验证volatile的可见性
 *  1.1、加入int number = 0,number变量之前根本没有添加volatile关键字修饰,因此没有可见性
 *      AAA	 come in
 *      AAA	 update number value : 60
 *
 *
 */
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 {
                // 暂停一会线程,那么其他线程也将读取到主内存中的值
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT060();
            System.out.println(Thread.currentThread().getName() + "\t update number value : "+myData.number);
        },"AAA").start();

        // 第二个线程是我们的main线程
        while (myData.number == 0){

            // main线程一直在这里等待训话,直到number值不再等于0
        }
        System.out.println(Thread.currentThread().getName() +"\t mission is over");
    }
}

class MyData{
    int number = 0;
    public void addT060(){
        this.number = 60;
    }
}

加volatile代码

package com.nuc;

import java.util.concurrent.TimeUnit;

/**
 * @Author yrx
 * @Date 2020-04-18 16:43
 * 验证volatile的可见性
 *  1.1、加入int number = 0,number变量之前根本没有添加volatile关键字修饰,因此没有可见性
 *      AAA	 come in
 *      AAA	 update number value : 60
 *  1.2、加volatile
 *      AAA	 come in
 *      AAA	 update number value : 60
 *      main	 mission is over
 *
 */
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 {
                // 暂停一会线程,那么其他线程也将读取到主内存中的值
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT060();
            System.out.println(Thread.currentThread().getName() + "\t update number value : "+myData.number);
        },"AAA").start();

        // 第二个线程是我们的main线程
        while (myData.number == 0){

            // main线程一直在这里等待训话,直到number值不再等于0
        }
        System.out.println(Thread.currentThread().getName() +"\t mission is over");
    }
}

class MyData{
    volatile int number = 0;
    public void addT060(){
        this.number = 60;
    }
}

1.2、volatile 不保证原子性验证

package com.nuc;

import java.util.concurrent.TimeUnit;

/**
 * @Author yrx
 * @Date 2020-04-18 16:43
 * 验证volatile的可见性
 *  1.1、加入int number = 0,number变量之前根本没有添加volatile关键字修饰,因此没有可见性
 *      AAA	 come in
 *      AAA	 update number value : 60
 *  1.2、加volatile
 *      AAA	 come in
 *      AAA	 update number value : 60
 *      main	 mission is over
 *  2、验证volatile不保证原子性
 *      2.1、原子性是指 不可分割、完整性、即某个线程正在做某个具体业务时,中间不可以被加塞,或者被分割,需要整体完整,
 *          要么同时成功,要么同时失败。 【保证数据完整一致性】
 *      2.2、volatile不保证原子性验证
 *             计算结果 main	 finally number value :19181
 *             如果能保证原子性的话,打印出来是 20000
 *
 *
 *
 */
public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }

        // 等待上面的线程全部计算完
        while (Thread.activeCount() > 2){
            // 不执行
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + "\t finally number value :"+myData.number);

    }

    /**
     * volatile 可见性验证
     */
    private static void seeOkByVolatile() {
        MyData myData = new MyData();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                // 暂停一会线程,那么其他线程也将读取到主内存中的值
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT060();
            System.out.println(Thread.currentThread().getName() + "\t update number value : "+myData.number);
        },"AAA").start();

        // 第二个线程是我们的main线程
        while (myData.number == 0){

            // main线程一直在这里等待训话,直到number值不再等于0
        }
        System.out.println(Thread.currentThread().getName() +"\t mission is over");
    }
}

class MyData{
    volatile int number = 0;
    public void addT060(){
        this.number = 60;
    }
    // 当然,如果加synchronized的话,可以保证线程安全的,我们这里主要用来验证 volatile
    public void addPlusPlus(){
        number++;
    }
}

原子性问题解决

package com.nuc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author yrx
 * @Date 2020-04-18 16:43
 * 验证volatile的可见性
 *  1.1、加入int number = 0,number变量之前根本没有添加volatile关键字修饰,因此没有可见性
 *      AAA	 come in
 *      AAA	 update number value : 60
 *  1.2、加volatile
 *      AAA	 come in
 *      AAA	 update number value : 60
 *      main	 mission is over
 *  2、验证volatile不保证原子性
 *      2.1、原子性是指 不可分割、完整性、即某个线程正在做某个具体业务时,中间不可以被加塞,或者被分割,需要整体完整,
 *          要么同时成功,要么同时失败。 【保证数据完整一致性】
 *      2.2、volatile不保证原子性验证
 *             计算结果 main	 finally number value :19181
 *             如果能保证原子性的话,打印出来是 20000
 *      2.3、如何解决原子性
 *              加 sync
 *              使用juc下的atomicInteger
 *                  main	 finally number value :19005
 *                  main	 finally number value :20000
 *
 *
 *
 */
public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 1; 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 finally number value :"+myData.number);
        System.out.println(Thread.currentThread().getName() + "\t finally number value :"+myData.atomicInteger);


    }

    /**
     * volatile 可见性验证
     */
    private static void seeOkByVolatile() {
        MyData myData = new MyData();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                // 暂停一会线程,那么其他线程也将读取到主内存中的值
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT060();
            System.out.println(Thread.currentThread().getName() + "\t update number value : "+myData.number);
        },"AAA").start();

        // 第二个线程是我们的main线程
        while (myData.number == 0){

            // main线程一直在这里等待训话,直到number值不再等于0
        }
        System.out.println(Thread.currentThread().getName() +"\t mission is over");
    }
}

class MyData{
    volatile int number = 0;
    public void addT060(){
        this.number = 60;
    }
    //
    public  void addPlusPlus(){
        number++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }

}

1.3、指令重排 【volatile禁止指令重排】

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一般分为以下三种:

  • 单线程环境里面确保程序最终执行结果和代码顺序执行的结构一致。
  • 处理器在进行重新排序时必须要考虑指令重排的数据依赖性
  • 多线程环境中线程交替执行,由于编译器优化重排的存在,连个线程使用的变量能否保持一致性是无法确定,结果无法预测。

image.png

案例一

public void  mySort(){
    int  x=11;//语句1
    int  y=12;//语句2
    x=x+5;//语句3
    y=x*x;//语句4
}

-- 在单线程情况下,我们的执行顺序是按照顺序执行的,但是
-- 在多线程情况下,我们的执行顺序就可能发生改变,比如:
1234
2134
1324

但是,语句4绝对不会第一条执行,因为要满足我们的指令重排的 数据依赖性

案例二

int a,b,x,y = 0
线程1                    线程2
x=a;                    y=b;
b=1;                    a=2;
结果:
x=0 y =0

如果编译器对这段代码进行执行重排优化后,可能出现下列情况:

线程1                   线程2
b=1;                    a=2;
x=a;                    y=b;
x=2 y=1

这也就说明:在多线程环境下,由于编译器优化重排的存在,两个线程使用的变量能都保持一致是无法确定的,因此:我们的volatile禁止指令重排,从而避免多线程环境下程序出现乱序执行的现象。
最后,附上学习笔记:
image.png

1.4、单例模式之双重检测模式

package com.nuc;

/**
 * @Author yrx
 * @Date 2020-04-23 20:14
 *
 * 单例模式之双端检测机制
 */
public class SingletonDemo {

    private static  SingletonDemo  instance = null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 构造方法");
    }

    /**
     * 双重检测机制
     *
     * @return
     */
    private static SingletonDemo getInstance(){
        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {

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

这种双端检索机制,因为指令重排的原因,导致是线程不安全的。原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance 的引用对象可能没有完成初始化

instance=new SingletonDem(); 可以分为以下步骤(伪代码) 
  
memory=allocate();//1.分配对象内存空间 
instance(memory);//2.初始化对象 
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null  


步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,
因此这种重排优化是允许的。

memory=allocate();//1.分配对象内存空间 
instance=memory;// 3 .设置instance的指向刚分配的内存地址,此时instance!=null  但对象还没有初始化完. 
instance(memory);// 2 .初始化对象 

但是指令重排只保证串行语义的执行一致性(单线程)并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就是造成了线程安全问题。

2、JMM

2.1、理论

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在 ,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存

3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存 ,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不 能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

image.png

2.2、详解

如下图所示,当我们在单线程操作的时候,我们new对象,并给age=25赋值,那么就相当于在我们的物理内存【主内存】的值是25,但在高并发的情况下,我们会有多个线程来给我们age去赋值,例如抢票系统,我们多个线程去改值,不是去改我们主内存的值,而是从主内存中拷贝到我们自己的工作区间,然后将25改成37,操作完成后,再讲我们的37写回我们的主内存中。但是其余线程,并不知道主内存已经从25改成37,所以我们需要有一种机制,有一个线程修改完自己工作区间的值,并写回给主内存后,要及时通知其他线程,这种及时通知的机制,其实就是JMM内存模型中的第一个特性,俗称可见性。所以:可见性其实就是一个线程发生了修改,其他线程马上获得了通知。
image.png

2.3、JMM特性

从三大特性中,我们可以发现,volatile其实就满足我们JMM三大特性中的两个

  • 可见

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.
这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题。

  • 原子

  • 有序

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排 ,一把分为以下3种

image.png

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.

处理器在进行重新排序是必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

发布了32 篇原创文章 · 获赞 39 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/yang735136055/article/details/105716761
今日推荐