个人博客网:https://wushaopei.github.io/    (你想要这里多有)

说明:多线程的内存可见性涉及到多线程间的数据争用,也涉及到了多线程间的数据可见性

一、共享变量在线程间的可见性

1、可见性介绍:

可见性: 一个线程对共享变量值的修改,能够及时地被其他线程看到。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

2、Java内存模型(JMM)

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

在内存模型中:

所有的变量都存储在主内存中;

每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

内存模型图:

JMM线程操作内存的两条基本的规定:

第一条关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写

第二条关于线程间工作内存:不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要经过主内存来完成。

3、共享变量可见性实现的原理:

线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤

  • 把工作内存1中更新过的共享变量刷新到主内存中
  • 把住内存中最新的共享变量的值更新到工作内存2中

流程图:

二、synchronized实现可见性

由前面可以知道,要实现共享变量的可见性,必须保证两点

  • 线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
  • 其他线程能够及时把共享变量的最新值从住内存更新到自己的工作内存中

1、可见性的实现方式

【1】Java语言层面支持的可见性实现方式

  •  synchronized
  •  volatile

【2】synchronized能够实现的 两个功能

  • 原子性(同步)
  • 可见性

【3】JMM关于synchronized 的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中;
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要的是同一把锁)

线程解锁前对共享变量的修改在下次加锁时对其他线程可见

【4】线程执行互斥代码的过程:

  1. 获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

【5】指令重排序的概念以及类型

重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

  1. 编译器优化的重排序(编译器优化)
  2. 指令集并行重排序(处理器优化)
  3. 内存系统的重排序(处理器优化)

示例:指令重排序 可能 造成的结果

【6】   as-if-serial

 as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循   as-if-serial 语义)

    int num1 = 1;           //第1行代码
int num2 = 2; //第2行代码
int sum = num1 + num2 ; //第3行代码

单线程: 第1、2行的顺序可以重排,但第3行不能

重排序不会给单线程带来内存可见性问题

多线程中程序交错执行时,重排序可能会造成内存可见性问题

2、synchronized实现可见性代码

package mkw.demo.syn;

public class SynchronizedDemo {
//共享变量
private boolean ready = false;
private int result = 0;
private int number = 1;
//写操作
public void write(){
ready = true; //1.1
number = 2; //1.2
}
//读操作
public void read(){
if(ready){ //2.1
result = number*3; //2.2
}
System.out.println("result的值为:" + result);
} //内部线程类
private class ReadWriteThread extends Thread {
//根据构造方法中传入的flag参数,确定线程执行读操作还是写操作
private boolean flag;
public ReadWriteThread(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
//构造方法中传入true,执行写操作
write();
}else{
//构造方法中传入false,执行读操作
read();
}
}
} public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
//启动线程执行写操作
synDemo .new ReadWriteThread(true).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//启动线程执行读操作
synDemo.new ReadWriteThread(false).start();
}
}

3、synchronized实现可见性分析:

【1】没有进行重排序

执行结果:result的值为3

【2】进行重排序

执行结果:result的值为0

【3】分析:导致共享变量在线程间不可见的原因

  1. 线程的交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在工作内存与主内存间及时更新

【4】 安全代码:

【5】synchronized 实现内存可见性的解决方案:

  1. 添加了synchronized 的地方相当于加了一把锁,被添加锁的地方,在一定时间内只能当前线程释放锁,其他线程才有机会进入其中执行代码;
  2. synchronized  使得 同步的情况下,共享变量在被第二次调用前便被同步到了主内存,实现了共享变量的即时更新

重点:为啥synchronized原子性可以避免线程交叉执行:因为synchronized加锁在对象上,执行read方法的线程1获得了对象锁,那线程2不能获得对象锁也就不能执行write方法,因为要执行write方法需要获得锁。但是线程1可以继续执行write方法,因为write方法和read方法可以使用同一把锁,synchronized锁可以重入

4、synchronized实现可见性结果分析:

此处执行结果为6的情况进行分析:
1 synchronized完美保证共享变量的可见性
2 但是不加此关键字,并不意味着就不能实现可见性

【1】为何不加synchronized也会执行可见性,主内存及时更新被获取最新值”?

原因有很多个

①即使没有加synchronized,也可能是可见的,在大多数情况都是可见的,因为编译器优化了,会揣摩程序的意图,程序运行很多次,只会有很少的情况不可见。

②因为当时定义说加synchronized一定会可见性,而不加也没说一定不会,只是有可能不会,因为现在Java做了一些优化:尽量实现可见性;但是不能保证每次都成功,只是成功概率比较大99%,但还是有1%的情况会失败。所以处于安全考虑,尽量加synchronized关键字100%成功。

【2】有时候依然不存在线程交叉情况,但还是会先执行第二个线程,因为第一个线程把CPU让位出来,所以为了避免这种情况,可以在第一个线程后附上代码:sleep(1000);1秒之后才有机会执行线程2。

【3】synchronized+sleep();黄金搭档。

三、volatile实现可见性

1、volatile能够保证可见性

volatile关键字:

  • 能够保证volatile变量的可见性
  • 不能保证volatile变量复合操作的原子性

volatile 如何实现内存可见性:

深入来说:通过加入内存屏障和禁止重排序优化来实现的。

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • 对volatile变量执行读操作时,会在读操作前后加入一条load屏障指令

执行引擎对 volatile 的操作:

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

线程 读、 写 volatile 变量的过程

【1】线程写volatile变量的过程:

  1. 改变线程工作的内存中volatile变量副本的值
  2. 将改变后的副本的值从工作内存刷新到主内存

【2】线程读volatile变量的过程:

  1. 从主内存中读取volatile变量的最新值到线程的工作内存中
  2. 从工作内存中读取volatile变量的副本

volatile不能保证volatile变量复合操作的原子性:

public class VolatileDemo {

	private Lock lock = new ReentrantLock();
private int number = 0; public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() { @Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}

注意:

//如果还有子线程在运行,主线程就让出CPU资源
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount()>1){
        Thread.yield();
}

理论来讲,最后的值应该是500,但是因为num++;不是原子操作,且volatile关键字又没有原子性,所以偶尔会出现<500的情况。

2、程序分析

num++不是原子操作,原子操作意为(所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch  --百科),volatile能保证可见性,但是在多线程调度时 num++ 被拆分为

1)从主存中读取num值;

2) i = num + 1;

3) 写回i 到主存的num

基于 number = 5 的分析:

1、线程A读取到的number 为 5;

2、线程B读取到的number 为 5;

3、线程B 执行加1 操作, number ++

4、线程B写入最新的 number 为 3 中的  5+ 1 = 6;

5、线程A 执行加 1 操作没有向主内存读取共享变量,故 依旧是 由 原始变量  number = 5 开始 加 1 操作,即 此时 number  = 5+ 1 ;

6、线程 A 写入最新的 number  值 ,此时内存中的 只是 将 线程B 的 6 改成了 线程 A 的6 ,实际上只是同值的 覆盖,而非 递增

安全性解决方案:

保证number自增操作的原子性:

  • 使用synchronized关键字

  • 使用ReentrantLock(java.until.concurrent.locks包下)

  • 使用AtomicInteger (vava.util.concurrent.atomic包下)

3、保证 number 变量在线程中的原子性

【1】用synchronized 保证 number 变量在线程中的原子性

        public synchronized void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.number++; }

【2】用ReentrantLock 实现number 变量在线程中的原子性

     private Lock lock = new ReentrantLock();
private int number = 0; public int getNumber(){
return this.number;
} public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}

4、volatile适用场合

要在多线程中安全的使用volatile变量,必须同时满足:

a)对变量的写入操作不依赖其当前值

不满足:number++、count=count*5等

满足:boolean变量、记录温度变化的变量等

b)该变量没有包含在具有其他变量的不变式中

不满足:不变式low<up

5、synchronized和volatile比较

a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程

b)从内存可见性的角度来讲,volatile的读相当于加锁,volatile的写相当于解锁

c)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

补充:

【1】对于64位(long、double)变量的读写可能不是原子操作:
.Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来进行

导致问题:有可能会出现读取到"半个变量"的情况
解决方法:加volatile关键字

【2】问:即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存见得到及时的更新?

答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快滴刷新缓存,所以一般情况下很难看到这种问题.

细说Java多线程之内存可见性笔记的更多相关文章

  1. 细说Java多线程之内存可见性

    编程这些实践的知识技能,每一次学习使用可能都会有新的认识 一.细说Java多线程之内存可见性(数据挣用)         1.共享变量在线程间的可见性                共享变量:如果一个 ...

  2. Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

    Java多线程之内存可见性和原子性:Synchronized和Volatile的比较     [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article ...

  3. java多线程之内存可见性-synchronized、volatile

    1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享 ...

  4. Java多线程之内存可见性

    阅读本文约“3分钟” 共享变量在线程间的可见性 synchronized实现可见性 volatile实现可见性 —指令重排序 —as-if-serial语义 —volatile使用注意事项 synch ...

  5. java多线程03-----------------volatile内存语义

    java多线程02-----------------volatile内存语义 volatile关键字是java虚拟机提供的最轻量级额的同步机制.由于volatile关键字与java内存模型相关,因此, ...

  6. Java 多线程高并发编程 笔记(一)

    本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...

  7. 1 Java线程的内存可见性

    Java内存的可见性 可见性: 一个线程对共享变量的修改,能够及时被其它线程看到 共享变量: 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量 Java内存模型(JM ...

  8. God 1.1.1 多线程之内存可见性

    共享变量在线程间的可见性 synchronize实现可见性 volatile实现可见性 指令重排序 as-if-serial语义 volatile使用注意事项 synchronized和volatil ...

  9. 深度解析Java多线程的内存模型

    内部java内存模型 硬件层面的内存模型 Java内存模型和硬件内存模型的联系 共享对象的可见性 资源竞速 Java内存模型很好的说明了JVM是如何在内存里工作的,JVM可以理解为java执行的一个操 ...

随机推荐

  1. 【Hadoop离线基础总结】Sqoop数据迁移

    目录 Sqoop介绍 概述 版本 Sqoop安装及使用 Sqoop安装 Sqoop数据导入 导入关系表到Hive已有表中 导入关系表到Hive(自动创建Hive表) 将关系表子集导入到HDFS中 sq ...

  2. zabbix部署与配置

    zabbix部署与配置 1.zabbix的web界面是基于php开发,所以创建lnmp环境来支持web界面的访问 yum install nginx php php-devel php-mysql p ...

  3. CentOS6.5x64采用静默模式安装64位oracle11g

    1.下载oracle11g64位版本的源文件,并上传到Linux服务器,下载地址自行百度,若实在找不到请留言. 2.Package安装检查安装: 通过yum工具直接安装: yum -y install ...

  4. Javascript中的apply与call

    一丶定义 每个函数都包含两个非继承而来的方法:apply()和call().这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值. 1.apply()方法 apply( ...

  5. Docker学习笔记(三):Dockerfile及多步骤构建镜像

    Dockerfile指令 官方文档地址:https://docs.docker.com/engine/reference/builder/ Dockerfile是一个文本格式的配置文件,其内容包含众多 ...

  6. myeclipse 2017 CI 破解

    1.首先下载破解文件(破解前先关闭myeclipse),链接:https://pan.baidu.com/s/1CPFH4Nga3xITSyj-BCVeaw 提取码:mkvz 2.将下载的破解文件解压 ...

  7. js面试题(转)

    https://segmentfault.com/a/1190000015288700 1 介绍JavaScript的基本数据类型 Number.String .Boolean .Null.Undef ...

  8. django 两种创建模型实例的方法

    1. 添加一个classmethod from django.db import models class Book(models.Model): title = models.CharField(m ...

  9. 14.3 Go iris

    14.3 Go iris 下载 go get -u -v github.com/kataras/iris 代码示例 package main import "github.com/katar ...

  10. Jenkins-插件开发(简单demo)

    推荐:官网创建插件案例:https://jenkins.io/doc/developer/tutorial/run/ 官方的这篇文章讲的很详细了,我就补充补充其中遇到的一些问题. 前置条件:maven ...