高并发的可见性搞不明白,就不用再研发了

本篇重点介绍

可见性是java中一种并不直观的特性,是指线程之间的可见性,即一个线程修改的状态对另一个线程是否是可见的,也就是一个线程修改了内存中的结果另一个线程能否马上就能看到。通常情况下,因为线程执行速度的快慢导致了线程间数据读取的先后问题,我们无法确保执行读操作的线程能适地看到其线程写入的值,有时甚至是根本不可能的事情。在高并发中可见性问题是保障线程之间交互的一个重要知识点。

禁止缓存的可见性变量

volatile是轻量级的同步机制

关键点:保证可见性、不保证原子性、禁止指令重排

保证可见性

解析:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

当不添加volatile关键字时示例:

package com.jian8.juc;import java.util.concurrent.TimeUnit;/** * 1验证volatile的可见性 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 */public class VolatileDemo {    public static void main(String[] args) {        visibilityByVolatile();//验证volatile的可见性    }    /**     * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改     */    public static void visibilityByVolatile() {        MyData myData = new MyData();        //第一个线程        new Thread(() -> {            System.out.println(Thread.currentThread().getName() + "\t come in");            try {                //线程暂停3s                TimeUnit.SECONDS.sleep(3);                myData.addToSixty();                System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num);            } catch (Exception e) {                // TODO Auto-generated catch block                e.printStackTrace();            }        }, "thread1").start();        //第二个线程是main线程        while (myData.num == 0) {            //如果myData的num一直为零,main线程一直在这里循环        }        System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num);    }}class MyData {    //    int num = 0;    volatile int num = 0;    public void addToSixty() {        this.num = 60;    }}

输出结果:

thread1	 come inthread1	 update value:60//线程进入死循环

当我们加上volatile关键字后,volatile int num = 0;输出结果为:

thread1	 come inthread1	 update value:60main	 mission is over, num value is 60//程序没有死循环,结束执行

不保证原子性

描述:原子性是指数据整体在当前的业务中不可分割、具有完整性,数据在业务流转过程中必须保证完整,要么同时成功要么同时失败。

验证示例(变量添加volatile关键字,方法不添加synchronized):

package com.jian8.juc;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/** * 1验证volatile的可见性 *  1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 * * 2.验证volatile不保证原子性 *  2.1 原子性指的是什么 *      不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败 */public class VolatileDemo {    public static void main(String[] args) {//        visibilityByVolatile();//验证volatile的可见性        atomicByVolatile();//验证volatile不保证原子性    }        /**     * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改     */	//public static void visibilityByVolatile(){}        /**     * volatile不保证原子性     * 以及使用Atomic保证原子性     */    public static void atomicByVolatile(){        MyData myData = new MyData();        for(int i = 1; i <= 20; i++){            new Thread(() ->{                for(int j = 1; j <= 1000; j++){                    myData.addSelf();                    myData.atomicAddSelf();                }            },"Thread "+i).start();        }        //等待上面的线程都计算完成后,再用main线程取得最终结果值        try {            TimeUnit.SECONDS.sleep(4);        } catch (InterruptedException e) {            e.printStackTrace();        }        while (Thread.activeCount()>2){            Thread.yield();        }        System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num);        System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger);    }}class MyData {    //    int num = 0;    volatile int num = 0;    public void addToSixty() {        this.num = 60;    }    public void addSelf(){        num++;    }        AtomicInteger atomicInteger = new AtomicInteger();    public void atomicAddSelf(){        atomicInteger.getAndIncrement();    }}

执行三次结果为:


//1.main	 finally num value is 19580	main	 finally atomicnum value is 20000//2.main	 finally num value is 19999main	 finally atomicnum value is 20000//3.main	 finally num value is 18375main	 finally atomicnum value is 20000//num并没有达到20000

禁止指令重排

描述:在计算机CPU中代码逻辑均是以一组组的指令形式交付给CPU。有序性是指在计算机执行程序时,为了提高性能,编译器在代码达到某些条件时会对代码逻辑的指令排序。

单线程环境里面每次执行代码只有一个线程,也就确保了程序最终执行结果和代码顺序执行的结果一致。(某些存在的竞态环境也会触发) 在多线程环境中由于线程交替执行,编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,导致了结果无法预测。


高并发的可见性搞不明白,就不用再研发了


代码实例:

描述:在公共类中声明成员变量:int a,b,x,y=0


高并发的可见性搞不明白,就不用再研发了

说明:常规情况下,线程按照正常的逻辑先后执行。在高并发下,如果编译器对这段程序代码执行重排优化后,可能出现如下情况:

高并发的可见性搞不明白,就不用再研发了

说明:在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

volatile可以实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在执行前Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。(本质上由于高速缓存禁止了)


高并发的可见性搞不明白,就不用再研发了

JMM(java内存模型)

JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM关于同步的规定

① 线程解锁前,必须把共享变量的值刷新回主内存

② 线程加锁前,必须读取主内存的最新值到自己的工作内存

③ 加锁解锁时同一把锁

解析:由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在JVM主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在私有内存区域中进行。每个线程在这个过程中都要将变量从主内存拷贝到自己的独占内存空间,在私内存中对变量副本进行操作,操作完成后再将变量副本写回主内存。由于不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)也必须通过主内存来完成。Voliate可以很好地保证共享变量在线程之间的可见性。

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) {        //构造方法只会被执行一次//        System.out.println(getInstance() == getInstance());//        System.out.println(getInstance() == getInstance());//        System.out.println(getInstance() == getInstance());        //并发多线程后,构造方法会在一些情况下执行多次        for (int i = 0; i < 10; i++) {            new Thread(() -> {                SingletonDemo.getInstance();            }, "Thread " + i).start();        }    }}

说明:其构造方法在多线程情况下可能会被执行多次

利用【volatile】的解决方式:

单例模式DCL代码

DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断

public static SingletonDemo getInstance() {        if (instance == null) {            synchronized (SingletonDemo.class) {                if (instance == null) {                    instance = new SingletonDemo();                }            }        }        return instance;    }

大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次。DCL(双端检锁)机制不一定能线程安全,由于线程执行速度快慢有别的原因或指令重排序,可能导致重入的风险。

描述在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance=new SingleDemo();可以被分为一下三步(伪代码):

memory = allocate();//1.分配对象内存空间instance(memory);	//2.初始化对象instance = memory;	//3.设置instance执行刚分配的内存地址,此时instance!=null

指令重排是不会对有明显依赖关系的代码进行重排序的,我们的代码可能存在着逻辑上的先后关系,但JVM并无法智能识别。比如图中步骤2和步骤3在依赖上不存在关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变串行语义执行的一致性,在JVM看来这种重排优化是被允许的。如图如果3步骤提前于步骤2,而instance还没有初始化完成,这个时候便出现A线程访问instance不为null时,由于instance实例尚未初始化完成,线程A本该获取到存在的instance实例,但由于错误判断获取也就造成了线程安全问题。

单例模式volatile代码

说明:在上述代码中的SingletongDemo实例上加上【volatile】:

private static volatile SingletonDemo instance = null;

有任何问题欢迎留言交流~


整理总结不易,如果觉得这篇文章有意思的话,欢迎转发、收藏,给我一些鼓励~

有想看的内容或者建议,敬请留言!

最近利用空余时间整理了一些精选Java架构学习视频和大厂项目底层知识点,需要的同学欢迎私信我发给你~一起学习进步!有任何问题也欢迎交流~

Java日记本,每日存档超实用的技术干货学习笔记,每天陪你前进一点点~

想要做读写分离,送你一些小经验

场景二:假设你有一个 8 核 64G 的主库,8 核 64G 的从库,4 核 32G 的从库,从配置上来看,4 核 32G 的从库处理能力肯定是要低于其他两个的,这个时候如果我们没有定制流量分发的比例,就会出现低配数据库压力过高而导致的问题。

以网易有道为例,揭秘小程序转介绍获客的底层逻辑

编辑导读:教育作为国之根本,一直在社会发展中占据重要位置。近几年,教育行业发展迅速,不少企业将重心放在了线上,通过各种渠道获客。本文将以网易有道为例,从产品、渠道、创意三个角度拆解小程序转介绍是如何实现的,希望对你有帮助。

大数据分析需要什么技术架构?

对于企业而言,坐拥庞大的数据资源,想要实现大数据分析,首要的就是要搭建起自身的大数据系统平台,而每个公司都有自己特定的业务场景,因此在大数据平台上的需求是不一样的。

高并发的可见性搞不明白,就不用再研发了

可见性是java中一种并不直观的特性,是指线程之间的可见性,即一个线程修改的状态对另一个线程是否是可见的,也就是一个线程修改了内存中的结果另一个线程能否马上就能看到。

SQL注入续篇(Web漏洞及防御)

没有任何报错信息输出,无法判断SQL注入测试语句是否正确,通过构造sleep注入的SQL测试语句,根据页面的返回时间判断数据库中存储了哪些信息!