第一、java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,

每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

JMM关于同步的规定:

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

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

3加锁解锁是同一把锁

由于JVM运行程序的实体就是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方为称为栈空间),工作内存是每个线程的私有数据区域,

而Java内存模型总规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,

首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再讲变量写回主内存,不能直接操作主内存中的变量,

各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,这个就是可见性

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图来说明这两个步骤:

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。

当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

总结:什么是Java内存模型:java内存模型简称jmm义了一个线程另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,多个线程同时访问一个数据的时候,

可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

第二、Vilatile的特性

2.1.vilatile定义

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

2.2.可见性

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后写回到主内存中的。这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,

另外一个线程BBB有对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说是并不可见的,这种工作内存与主内存同步延迟线程就造成了可见性问题,

代码案例:

class MyData{
volatile int number = 1; public void updateNumber(){
this.number = 60;
}
}
/**
* 验证volatitle的可见性
*
* 1.1假如int number =0;number 变量之前根本没有添加volatile关键字修饰
1.2添加volatile关键修饰
*/
public class VolatileDemo { public static void main(String[] args) throws Exception{
MyData myData = new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t common in");
//暂停一会儿线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.updateNumber();
System.out.println(Thread.currentThread().getName()+"\t update number\t"+myData.number);
},"AA").start();
while (myData.number==1){
//等于0一直等待
}
System.out.println("main number over\t"+myData.number);
} }

2.3.不保证原子性

原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。需要整体完整要么同时成功,要么同时失败,

也就是最终一致性

验证可见性和不保证原子性

不保证原子性的解决方法:

1.加sync

2.直接使用JUC下AtomicInteger

class MyData{
volatile int number = 0;
public void addNumber(){
this.number = 60;
}
public void addAtomicity(){
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
} /**
* 1验证volatile的可见性
* 1.1假如int number=0;number变量之前根本没有添加volatile关键字修饰,没有可见性
* 1.2添加了volatile,可以解决可见性问题
* 2验证volatitle不保证原子性
* 原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。
* 需要整体完整要么同时成功,要么同时失败
*
*/
public class VolatileDemo01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
myData.addAtomicity();
myData.addMyAtomic();
}
}
}).start();
}
//需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果看是多少?
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.number);
System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.atomicInteger);
} //volatile可见保证可见性,及时通知其他线程,主物理内存的值以及被修改了
public static void seeVolatile(){
MyData myData = new MyData();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("修改之前的值:"+myData.number);
//等待三秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用改变的方法
myData.addNumber();
System.out.println("修改之后的值:"+myData.number);
}
}).start(); while(myData.number==0){
//一直循环等待
}
System.out.println(Thread.currentThread().getName()+":"+myData.number);
}
}

2.4.禁止指令重排

volatile 禁止指令重排

JMM 有序性

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

源代码→编译器优化的重排→内存系统的重排→最终执行的指令

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

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

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

禁止指令重排总结

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

先了解一下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

一是保证特定操作的执行顺序,

二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和折腾Memory Barrier指令重排序,

也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

JMM线程安全得到保证

工作内存和主内存同步延时现象导致的可见性问题,可以使用synchronize或者volatile关键字进行解决,他们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化

那些地方用过volatile

1.单例模式DCL代码  2单例模式volatile分析

public class VolatileDemo02 {

    private static volatile VolatileDemo02 instance = null;
private VolatileDemo02(){
System.out.println(Thread.currentThread().getName()+"构造函数");
} //DCL(Double Check Lock双端检测机制)
public static VolatileDemo02 getInstance(){
if (instance == null) {
synchronized (VolatileDemo02.class){
if (instance == null) {
instance = new VolatileDemo02();
}
}
}
return instance;
} public static void main(String[] args) {
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileDemo02.getInstance();
}
}).start();
}
}
}

DCL(Double Check Lock双端检测机制)

DCL(双端检索)机制不一定线程安全,原因四有指令重排序的存在,加入volatile可以禁止指令重排

原因在与某一个执行到第一次检测,读取到的instance不为null时间,instance的引用对象可能没有完成初始化。

instance = new SingletonDemo();可以分为以下三步完成(伪代码)

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.5Volatile与Synchronized区别

(1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。

(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。

但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

synchronized太重了

volatile学习的更多相关文章

  1. volatile 学习笔记

    全面理解Java内存模型(JMM)及volatile关键字 正确使用 Volatile 变量 Java内存模型 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步.通信是指线程之间 ...

  2. java 语言多线程可见性(synchronized 和 volatile 学习)

    共享变量可见性实现的原理 java 语言层面支持的可见性实现方式: synchronized volatile 1. synchronized 的两条规定: 1 线程解锁前,必须把共享变量的最新值刷新 ...

  3. C++——volatile关键字的学习

    首先声明一点,本文是关于volatile关键字的学习,学习内容主要是来自一些大牛的网络博客. 一篇是何登成先生的C/C++ Volatile关键词深度剖析(http://hedengcheng.com ...

  4. JAVA多线程基础学习三:volatile关键字

    Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...

  5. Java多线程学习(三)volatile关键字

    转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79680693 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...

  6. 【java并发编程艺术学习】(三)第二章 java并发机制的底层实现原理 学习记录(一) volatile

    章节介绍 这一章节主要学习java并发机制的底层实现原理.主要学习volatile.synchronized和原子操作的实现原理.Java中的大部分容器和框架都依赖于此. Java代码 ==经过编译= ...

  7. 一个具体的例子学习Java volatile关键字

    相信大多数Java程序员都学习过volatile这个关键字的用法.百度百科上对volatile的定义: volatile是一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和 ...

  8. JVM学习(九)volatile应用

    一.初认volatile 首先学习volatile关键字时,我们先简单的了解一下它能干啥: 工作内存与主内存同步延迟现象导致的可见性问题: 可通过synchronized或volatile关键字解决, ...

  9. JAVA多线程学习- 三:volatile关键字

    Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...

随机推荐

  1. ubuntu18.04 开机自动启动脚本的坑

    哇 这个真的难受 找了半天的方案不能用 自暴自弃用的图形界面设置

  2. FFmpeg4.0笔记:封装ffmpeg的解码功能类CDecode

    Github https://github.com/gongluck/FFmpeg4.0-study/tree/master/Cff CDecode.h /********************** ...

  3. 使用php过滤emoji表情

    /** * 过滤字符串中表情 * @param $str string 昵称 * @return string */ public function filterEmoji($str) { $str ...

  4. git diff 命令介绍

    https://www.jianshu.com/p/6e1f7198e76a https://www.jianshu.com/p/5b6a014ac3db https://blog.csdn.net/ ...

  5. Java服务,内存OOM问题如何快速定位? (转)

    转自:公众号  架构师之路 问题:有一个Java服务出现了OOM(Out Of Memory)问题,定位了好久不得其法,请问有什么好的思路么? OOM的问题,印象中之前写过,这里再总结一些相对通用的方 ...

  6. spring boot技术干货

    Spring Boot2 系列教程(一)纯 Java 搭建 SSM 项目 Spring Boot2 系列教程(二)创建 Spring Boot 项目的三种方式 Spring Boot2 系列教程(三) ...

  7. Vue 路由(对路由页面编写做规范)

    前言 上一篇写了“Vue 路由拦截(对某些页面需要登陆才能访问)” 的博客,此篇是续上篇对路由页面模块化,可以看到之前的路由配置都写在main.js中,真正开发肯定不能都写在main.js,所以我们要 ...

  8. docker之常用命令

    1) docker run -p : --name mysql -v d:/docker/mysql/conf:/etc/mysql/conf.d -v d:/docker/mysql/logs:/l ...

  9. Django框架——基础之路由系统(urls.py)

    1.URL路由系统前言 URL是Web服务的入口,用户通过浏览器发送过来的任何请求,都是发送到一个指定的URL地址,然后被响应. 在Django项目中编写路由,就是向外暴露我们接收哪些URL的请求,除 ...

  10. 基于Chromium的浏览器已上线通用“显示密码”按钮

    基于Chromium的Edge在日前发布的Canary通道版本中,对用户界面进行了优化调整从而让InPrivate窗口变得更加简洁.在今天获得的版本更新中,微软继续带来了隐私相关的新内容--实现通用的 ...