多线程之美1一volatile
目录
一、java内存模型
1.1、抽象结构图
1.2、概念介绍
二、volatile详解
2.1、概念
2.2、保证内存可见性
2.3、不保证原子性
2.4、有序性
一、java内存模型
1.1、抽象结构图
1.2、概念介绍
java 内存模型
即Java memory model(简称JMM), java线程之间的通信由JMM控制,决定一个线程对共享变量的写入何时对另一个线程可见。
多线程通信通常分为2类:共享内存和消息传递
JMM采用的就是共享内存来实现线程间的通信,且通信是隐式的,对程序开发人员是透明的,所以在了解其原理了,才会对线程之间通信,同步,内存可见性问题有进一步认识,避免开发中出错。
线程之间如何通信?
在java中多个线程之间要想通信,如上图所示,每个线程在需要操作某个共享变量时,会将该主内存中这个共享变量拷贝一份副本存在在自己的本地内存(也叫工作内存,这里只是JMM的一个抽象概念,即将其笼统看做一片内存区域,用于每个线程存放变量,实际涉及到缓存,寄存器和其他硬件),线程操作这个副本,比如 int i = 1;一个线程想要进行 i++操作,会先将变量 i =1 的值先拷贝到自己本地内存操作,完成 i++,结果 i=2,此时主内存中的值还是1,在线程将结果刷新到主内存后,主内存值就更新为2,数据达到一致了。 如果线程A,线程B同时将 主内存中 i =1拷贝副本到自己本地内存,线程A想要 将i+1,而线程B想要将 int j=i,将赋值给j,那么如何保证线程之间的协作,此时就会涉及到线程之间的同步以及内存可见性问题了。(后文分析synchronized/lock)
那线程之间实现通信需要经过2个步骤,借助主内存为中间媒介:
线程A (发送消息)-->(接收消息) 线程B
1、线程A将本地内存共享变量值刷新到主内存中,更新值;
2、线程B从主内存中读取已更新过的共享变量;
共享内存中涉及到哪些变量称为共享变量?
这里的共享内存指的是jvm中堆内存中,所有堆内存在线程之间共享,因为栈中存储的是方法及其内部的局部变量,不在此涉及。
共享变量:对于多线程之间能够共同操作的变量,包含实例域,静态域,数组元素。即有成员变量,静态变量等等,
不涉及到局部变量(所以局部变量不涉及到内存可见性问题)
多线程在java内存模型中涉及到三个问题
- 可见性
- 原子性
- 有序性(涉及指令重排序)
二、volatile详解
2.1、概念
-1、volatile 是 java中的关键字,可修饰字段,可以保证共享变量的在内存的可见性,有序性,不保证原子性。
-2、作用:在了解java内存模型后,才能更加了解volatile在JMM中的作用,volatile在JMM中为了保证内存的可见性,即是线程之间操作共享变量的可见性。
- volatile写和读的内存语义
volatile 写的内存语义:
当写一个volatile修饰的共享变量时,JMM会把该线程的本地内存的共享变量副本值刷新到主内存中;
volatile 读的内存语义:
当读一个volatile修饰的共享变量时,JMM会将该线程的本地内存的共享变量副本置为无效,要求线程重新去主内存中获取最新的值。
- java内存模型控制与volatile冲突吗?什么区别?
不冲突!java内存模型控制线程工作内存与主内存之间共享变量会同步,即线程从主内存中读一份副本到工作内存,又刷新到主内存,那怎么还需要 volatile来保证可见性,不是JMM自己能控制吗,一般情况下JMM可以控制 2份内存数据一致性,但是在多线程并发环境下,虽然最终线程工作内存中的共享变量会同步到主内存,但这需要时间和触发条件,线程之间同时操作共享变量协作时,就需要保证每次都能获取到主内存的最新数据,保证看到的工作变量是最后一次修改后的值,这个JMM没法控制保证,这就需要volatile或者后文要讲的 synchronized和锁的同步机制来实现了。
2.2、保证内存可见性
1、多个线程出现内存不可见问题示例
/**
* @author zdd
* Description: 测试线程之间,内存不可见问题
*/
public class TestVisibilityMain {
private static boolean isRunning = true; // 可尝试添加volatile执行,其余不变,查看线程A是否被停止
//private static volatile boolean isRunning = true; public static void main(String[] args) throws InterruptedException {
//1,开启线程A,读取共享变量值 isRunning,默认为true
new Thread(()->{
// --> 此处用的lamda表达式,{}内相当于Thread的run方法内部需执行任务
System.out.println(Thread.currentThread().getName() + "进入run方法");
while (isRunning == true) {
}
System.out.println(Thread.currentThread().getName()+"被停止!");
},"A").start();
//2,主线程休眠1s, 确保线程A先被调度执行
TimeUnit.SECONDS.sleep(1);
//3,主线程修改共享变量值 为flase,验证线程A是否能够获取到最新值,跳出while循环 --> 验证可见性
isRunning =false;
System.out.println(Thread.currentThread().getName() +"修改isRunning为: " + isRunning);
}
}
执行结果如下图:
- 2、一个容易忽视的问题
上面代码 while里面是一个空循环,没有操作,如果我在里面加一句打印语句,线程A会被停止,这是怎么回事呢?
原:while (isRunning == true) {}
改1:
while (isRunning == true) {
System.out.println("进入循环");
}
原来 println方法里面加了 synchronized关键字,在加了锁既保证原子性,也保证了可见性,会实现线程的工作内存与主内存共享变量的同步。
源代码如下:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
改2:
while (isRunning == true) {
//改为这样,也可以停止线程A
synchronized (TestVisibilityMain.class){}
}
2.3、不保证原子性
- 1、示例代码
/**
* @author zdd
* Description: 测试volatile的不具有原子性
*/
public class TestVolatileAtomic {
private static volatile int number;
//开启线程数
private static final int THREAD_COUNT =10;
//执行 +1 操作
public static void increment() {
//让每个线程进行加1次数大一些,能够更容易出现volatile对复合操作(i++)没有原子性的错误
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() +"的number值: "+number);
}
public static int getNumber() {
return number;
}
public static void main(String[] args) throws InterruptedException {
TestVolatileAtomic volatileAtomic = new TestVolatileAtomic();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i]=
new Thread(()->{
// 做循环自增操作
volatileAtomic.increment();
System.out.println(Thread.currentThread().getName() +"的number值: "+volatileAtomic.getNumber());
},"thread-"+i);
}
for (int i = 0; i <10; i++) {
//开启线程
threads[i].start();
}
//主线程休眠4s,确保上面线程都执行完毕
TimeUnit.SECONDS.sleep(4);
System.out.println("执行完毕,number最终值为:"+volatileAtomic.getNumber());
}
}
执行结果:number的最后值不一定是 10*10000= 100000的结果
- 2、解决上诉问题
//1,increment()方法上加上 synchronized关键字同步
public static synchronized void increment() {
//让每个线程进行加1次数大一些,能够更容易出现volatile对复合操作(i++)没有原子性的错误
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() +"的number值: "+number);
}
//2,使用Lock,使用其实现类可重入锁 ReentrantLock
static Lock lock = new ReentrantLock();
//执行 +1 操作
public static void increment() {
lock.lock();
try {
for (int i = 0; i < 10000; i++) {
number++;
}
System.out.println(Thread.currentThread().getName() + "的number值: " + number);
} finally {
lock.unlock();
}
}
运行结果如图:
- 3、原因分析
对单个volatile变量的读/写具有原子性,而对像 i++这种复合操作不具有原子性。
上面代码 i++操作可以分为3个步骤
-1 先读取变量i的值 i
-2 进行i+1操作 temp= i+1
-3 修改i的值 i= temp
比如:比如在线程A,B同时去操作共享变量i, i的初始值为10,A,B同时去获取i的值,A对i进行 temp =i+1,此时i的值还没变, 线程B也对i进行 temp=i+1了,线程A执行i=temp的操作,i的值变为11,此时由于 volatile可见性,会刷新A的 i值到主内存,主内存中i此时也更新为11了,线程B接收到通知自己i无效了,重新读取i=11,虽然i=11,但是已经进行过 temp= i+1了,此时temp =11,线程B继续第三步,i=temp =11, 预期结果是i被A,B自增各一次,结果i=12,现在为11,出现数据错误。
2.4、有序性
- 重排序
-1,重排序概念:重排序是编译器和处理器为了优化程序性能而对指令序列重新排序的一种手段
即:程序员编写的程序代码的顺序,在实际执行的时候是不一样的,这其中编译器和处理器在不影响最终执行结果的基础上会做一些优化调整,有重新排序的操作,为了提高程序执行的并发性能。
-2,重排序分类: 编译重排序,处理器重排序
-4,单线程下,重排序没有问题,但是在多线程环境下,可能会破坏程序的语义.
- volatile 防止重排序保证有序性
为了实现volatile的内存语义,JMM会限制编译器和处理器重排序
-1 制定了重排序规则表防止编译器重排序
volatile重排序规则表(图摘自书-并发编程的艺术)
-2 插入内存屏障防止处理器重排序
参考资料:
1、Java并发编程的艺术- 方腾飞
2、java多线程编程核心技术- 高洪岩
多线程之美1一volatile的更多相关文章
- java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析
java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...
- Java多线程学习(三)volatile关键字
转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79680693 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...
- JAVA 多线程随笔 (一) 可见性和volatile关键字
// 先上代码 1 public class NoVisibility { private static boolean ready; private static int number; priva ...
- 细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)
上一节介绍了使用信号量进行同步,本节主要介绍一些非阻塞同步的方法.本节主要介绍MemoryBarrier,volatile,Interlocked. MemoryBarriers 本文简单的介绍一下这 ...
- JAVA多线程基础学习三:volatile关键字
Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...
- 多线程(三)~多线程中数据的可见性-volatile关键字
我们先来看一段代码: ①.线程类,用全局布尔值控制线程是否结束,每隔1s打印一次当前线程的信息 package com.multiThread.thread; publicclassPrintStri ...
- 多线程与高并发(四)volatile关键字
上一篇学习了synchronized的关键字,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volatile是一个轻量级的同步机制. 前面学习了Java的内存模型,知 ...
- 多线程之美5一 AbstractQueuedSynchronizer源码分析<一>
AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 5.1.acquire(int arg ) 5.2.rel ...
- 多线程之美6一CAS与自旋锁
1.什么是CAS CAS 即 compare and swap 比较并交换, 涉及到三个参数,内存值V, 预期值A, 要更新为的值B, 拿着预期值A与内存值V比较,相等则符合预期,将内存值V更新为B, ...
随机推荐
- Docker的安装及加速器配置
简介 Docker是一个开源项目 ,其主要目标是实现轻量级的操作系统虚拟化解决方案.Docker的基础是Linux容器(LXC)等技术.在LXC的基础上Docker进行了进一步的封装,让用户不需关心容 ...
- StringBuffer的一些小整理
大家好,欢迎大家在百忙当中来到我的博客文,也许是因为各种需要到此一游,哈哈.不过来到这里不会让您失望的,此段博文是这段时间不忙的时候整理出来的,对于刚学java基础的同学非常适合.下面言归正传: 首先 ...
- 剑指Offer(十九)——顺时针打印矩阵
题目描述 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字. 例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 ...
- vue使用 封装websocket心跳包
---恢复内容开始--- 这套代码可以拿过去直接用 一些注意我会在下面代码中加上注释: 谢谢支持 核心代码 //这里需要引入vuex import store from './store'; let ...
- php中转菜刀脚本过狗免杀
<?php $target="url";//shell 地址 菜刀连这个脚本做中转 $post=''; $i=0; foreach($_POST AS $K=>$V) ...
- 实战SpringCloud响应式微服务系列教程(第八章)构建响应式RESTful服务
本文为实战SpringCloud响应式微服务系列教程第八章,讲解构建响应式RESTful服务.建议没有之前基础的童鞋,先看之前的章节,章节目录放在文末. 1.使用springboot2.1.4构建RE ...
- $.ajax.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link rel= ...
- python语法入门之流程控制
python语法入门之流程控制 流程控制是指控制流程,具体指控制程序执行的流程. 流程控制分为三种: 1.顺序结构 程序从上而下的正常执行(正常执行的代码就是顺序结构) 2.分支结构 赋予程序人的思维 ...
- 【java基础之异常】死了都要try,不淋漓尽致地catch我不痛快!
目录 1.异常 1.1 异常概念 1.2 异常体系 1.3 异常分类 1.4 异常的产生过程解析 2. 异常的处理 2.1 抛出异常throw 2.2 Objects非空判断 2.3 声明异常thro ...
- Unity5-ABSystem(三):AssetBundle加载
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/lodypig/article/detai ...