Java中多线程与并发_volatile关键字的深入理解

一、volatile关键字

volatile是JVM提供的一种轻量级的同步机制,特性:

1.保证内存可见性

2.不保证原子性

3.防止指令重排序

二、JMM(Java Memory Model)

Java内存模型中规定了所有的变量都存储在主内存中(如虚拟机物理内存中的一部分),每条线程还有自己的工作内存(如CPU中的高速缓存),线程的工作内存中保存了该线程使用到的变量到主内存的副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存和工作内存的交互关系如下图所示:

三、验证

1.验证volatile的可见性

1.1 假如 int num = 0; num变量之前根本没有添加volatile关键字修饰,没有可见性

1.2 添加了volatile,可以解决可见性问题

MyData类

class MyData {
 volatile int num = 0;

 public void addT060() {
 this.num = 60;
 }
}

内存可见性验证,其中两个线程分别为AAA线程和main线程

 //volatile可以保证可见性,及时通知其它线程,主内存的值已经被修改
 @Test
 public 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 num value: " + myData.num);
 },"AAA").start();

 //第2个线程是我们的main线程
 while (myData.num == 0) {
  //main线程就一直在这里等待循环,直到num值不再等于0.
 }
 System.out.println(Thread.currentThread().getName() + "\t mission is over,main get num value: " + myData.num );
 }

对num变量加volatile修饰后结果

AAA come in
AAA update num value: 60
main 我能见到AAA线程对num修改的结果啦,main get num value: 60

Process finished with exit code 0

2.验证volatile不保证原子性

2.1 原子性指的是什么意思?

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

2.2 volatile不保证原子性的案例演示

2.3 为什么不保证原子性?

2.4 如何保证原子性

加sync

使用我们juc下的AtomicInteger (底层实现CAS)

给MyData类加addPlusPlus()方法

class MyData {//MyData.java ===> MyData.class ===> JVM字节码
 int num = 0;

 public void addT060() {
 this.num = 60;
 }

 //请注意,此时num前面是加了关键字修饰的,volatile不保证原子性
 public void addPlusPlus() {
 num++;
 }
}

2.2 volatile不保证原子性的案例演示

num++在多线程操作的情况下不保证原子性的

创建20个线程并行执行num++操作2000次,多次测试,结果不为40000

public static void main(String[] args) {
 MyData myData = new MyData();

 for (int i = 1; i <= 20; i++ ) {

  new Thread(() -> {
  for (int j = 1; j <= 2000; j++) {
   myData.addPlusPlus();
  }

  },String.valueOf(i)).start();
 }

 //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
 while(Thread.activeCount() > 2) {
  Thread.yield();
 }

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

结果:数值小于40000,出现写值丢失的情况

main  finally num value:38480

Process finished with exit code 0

2.3 为什么不保证原子性?

因为当线程A对num++操作从自己的工作内存刷新到主内存时,还未通知到其他线程主内存变量有更新的瞬间,其他线程对num变量的操作结果也对主内存进行了刷新,从而导致了写值丢失的情况

num++通过汇编指令分析,通过javap反编译得到如下汇编指令

class com.slx.juc.MyData {
 volatile int num;

 com.slx.juc.MyData();
 Code:
 0: aload_0
 1: invokespecial #1   // Method java/lang/Object."<init>":()V
 4: aload_0
 5: iconst_0
 6: putfield #2   // Field num:I
 9: return

 public void addT060();
 Code:
 0: aload_0
 1: bipush 60
 3: putfield #2   // Field num:I
 6: return

 public void addPlusPlus();
 Code:
 0: aload_0
 1: dup
 2: getfield #2   // Field num:I
 5: iconst_1
 6: iadd
 7: putfield #2   // Field num:I
 10: return
}

可见num++被拆分成了3个步骤,简称:读-改-写

  • 执行getfield拿到原始num;
  • 执行iadd进行加1操作;
  • 执行putfield写把累加后的值写回

2.4 如何保证原子性

加sync

使用我们juc下的AtomicInteger (底层实现CAS)

MyData类中添加原子类操作方法

 AtomicInteger atomicInteger = new AtomicInteger();
 public void addMyAtomic() {
 atomicInteger.getAndIncrement();
 }

调用该方法打印结果

 public static void main(String[] args) {
 MyData myData = new MyData();

 for (int i = 1; i <= 20; i++ ) {

  new Thread(() -> {
  for (int j = 1; j <= 2000; j++) {
   myData.addMyAtomic();
  }

  },String.valueOf(i)).start();
 }

 //需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
 while(Thread.activeCount() > 2) {
  Thread.yield();
 }

 System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type ,finally num value:" + myData.atomicInteger);
 }

测试结果为40000,不会出现之前int类型的丢失值的情况

main  AtomicInteger type ,finally num value:40000

Process finished with exit code 0

总结

到此这篇关于Java中多线程与并发_volatile关键字的文章就介绍到这了,更多相关Java多线程与并发_volatile关键字内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java 内存溢出的原因和解决方法

    你是否遇到过Java应用程序卡顿或突然崩溃的情况?您可能遇到过Java内存泄漏.在本文中,我们将深入研究Java内存泄漏的确切原因,并推荐一些最好的工具来防止内存泄漏发生. 什么是JAVA内存泄漏? 简单地说,Java内存泄漏是指对象不再被应用程序使用,而是在工作内存中处于活动状态. 在Java和大多数其他编程语言中,垃圾收集器的任务是删除不再被应用程序引用的对象.如果不选中,这些对象将继续消耗系统内存,并最终导致崩溃.有时java内存泄漏崩溃不会输出错误,但通常错误会以java.lang.Ou

  • javascript:;与javascript:void(0)使用介绍

    最近看了好几个关于<a>标签和javascript:void(0)的帖子,谨记于此,以资查阅. 注:以下代码未经全面测试,但每一种方法可能会出现的情况都基本做了说明. 在做页面时,如果想做一个链接点击后不做任何事情,或者响应点击而完成其他事情,可以设置其属性 href = "#",但是,这样会有一个问题,就是当页面有滚动条时,点击后会返回到页面顶端,用户体验不好. 目前有如下几种解决办法: 1)点击链接后不做任何事情 <a href="javascript:

  • JAVA8 十大新特性详解

    "Java is still not dead-and people are starting to figure that out." 本教程将用带注释的简单代码来描述新特性,你将看不到大片吓人的文字. 一.接口的默认方法 Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下: 复制代码 代码如下: interface Formula {    double calculate(int a); default do

  • java redis 实现简单的用户签到功能

    业务需求是用户每天只能签到一次,而且签到后用户增加积分,所以把用户每次签到时放到redis 缓存里面,然后每天凌晨时再清除缓存,大概简单思想是这样的 直接看代码吧如下 @Transactional @Override public void signIn(Integer memberId) throws BizException { if(memberId==null){ throw new BizException(ErrorCode.BIZ_EXCEPTION.getErrcode(), "

  • java.net.SocketException: Connection reset 解决方法

    自从SEOTcs系统11月份24日更新了一下SEO得分算法以来,一直困扰我的一个问题出现了,java的数据job任务,在执行过程中会经常报以下的错误: "2011-12-03 18:00:32 DefaultHttpClient [INFO] I/O exception (java.net.SocketException) caught when processing request: Connection reset by peer: socket write error2011-12-03

  • JAVA Iterator 转成 List 的操作

    List转到Iterator容易,JDK本身就支持,反过来的实现方式如下: 1.使用Apache Common Collections 2.自己实现的方法转换 3.Guaa实现转换 方式1: #Apache Commons Collections: import org.apache.commons.collections.IteratorUtils; Iterator<Element> myIterator = //some iterator List<Element> myLi

  • java实现app签到功能

    本文实例为大家分享了java实现app签到功能的具体代码,供大家参考,具体内容如下 1.首先设计二张表,第一张表sign_calc记录用户连续签到次数,字段id,user_id,continue_days,第二张表sign_detail签到详情表id,user_id,sign_date. 2.app端用户点击签到接口controller如下: /*** * app签到接口 * @author xuhaibo * @param accid * @return */ @ResponseBody @R

  • JAVA Web.xml加载顺序过程详解

    web.xml加载过程(步骤): 1.启动WEB项目的时候,容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点: <listener></listener> 和 <context-param></context-param> 2.紧接着,容器创建一个ServletContext(上下文),这个WEB项目所有部分都将共享这个上下文. 3.容器将<context-param></context-param>转化为键值对,

  • Java中多线程与并发_volatile关键字的深入理解

    一.volatile关键字 volatile是JVM提供的一种轻量级的同步机制,特性: 1.保证内存可见性 2.不保证原子性 3.防止指令重排序 二.JMM(Java Memory Model) Java内存模型中规定了所有的变量都存储在主内存中(如虚拟机物理内存中的一部分),每条线程还有自己的工作内存(如CPU中的高速缓存),线程的工作内存中保存了该线程使用到的变量到主内存的副本拷贝,线程对变量的所有操作(读取.赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量.不同线程之间无法直接访

  • java 中多线程生产者消费者问题详细介绍

    java 中多线程生产者消费者问题 前言: 一般面试喜欢问些线程的问题,较基础的问题无非就是死锁,生产者消费者问题,线程同步等等,在前面的文章有写过死锁,这里就说下多生产多消费的问题了 import java.util.concurrent.locks.*; class BoundedBuffer { final Lock lock = new ReentrantLock();//对象锁 final Condition notFull = lock.newCondition(); //生产者监视

  • Java中常见的并发控制手段浅析

    目录 前言 1.1 同步代码块 1.2 CAS自旋方式 1.3 锁 1.4 阻塞队列 1.5 信号量Semaphore 1.6 计数器CountDownLatch 1.7 栅栏 CyclicBarrier 1.8 guava令牌桶 1.9 滑动窗口TimeWindow 1.10 小结 前言 单实例的并发控制,主要是针对JVM内,我们常规的手段即可满足需求,常见的手段大概有下面这些 同步代码块 CAS自旋 锁 阻塞队列,令牌桶等 1.1 同步代码块 通过同步代码块,来确保同一时刻只会有一个线程执行

  • Java中多线程Reactor模式的实现

    目录 1. 主服务器 2.IO请求handler+线程池 3.客户端 多线程Reactor模式旨在分配多个reactor每一个reactor独立拥有一个selector,在网络通信中大体设计为负责连接的主Reactor,其中在主Reactor的run函数中若selector检测到了连接事件的发生则dispatch该事件. 让负责管理连接的Handler处理连接,其中在这个负责连接的Handler处理器中创建子Handler用以处理IO请求.这样一来连接请求与IO请求分开执行提高通道的并发量.同时

  • 详解Java中多线程异常捕获Runnable的实现

    详解Java中多线程异常捕获Runnable的实现 1.背景: Java 多线程异常不向主线程抛,自己处理,外部捕获不了异常.所以要实现主线程对子线程异常的捕获. 2.工具: 实现Runnable接口的LayerInitTask类,ThreadException类,线程安全的Vector 3.思路: 向LayerInitTask中传入Vector,记录异常情况,外部遍历,判断,抛出异常. 4.代码: package step5.exception; import java.util.Vector

  • java 中newInstance()方法和new关键字的区别

    java 中newInstance()方法和new关键字的区别 * 它们的区别在于创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类. * 那么为什么会有两种创建对象方式?这主要考虑到软件的可伸缩.可扩展和可重用等软件设计思想. * 我们使用关键字new创建一个类的时候,这个类可以没有被加载.但是使用newInstance()方法的时候, * 就必须保证:1.这个类已经加载:2.这个类已经连接了. * newInstance()实际上是把new这个方式分解为两步,即首先调用Class

  • Java中同步与并发用法分析

    本文较为详细的分析了Java中同步与并发的用法.分享给大家供大家参考.具体分析如下: 1.同步容器类包括两部分:vector和hashtable 另一类是同步包装类,由Collections.synchronizedXXX创建.同步容器对容器的所有状态进行串行访问,从而实现线程安全. 它们存在如下问题: a) 对于符合操作,需要额外的锁保护.比如迭代,缺少则添加等条件运算. b) toString,hashCode,equals都会间接的调用迭代,都需要注意并发.   2.java5.0中的并发

  • Java中的break和continue关键字的使用方法总结

    一.作用和区别   break的作用是跳出当前循环块(for.while.do while)或程序块(switch).在循环块中的作用是跳出当前正在循环的循环体.在程序块中的作用是中断和下一个case条件的比较.   continue用于结束循环体中其后语句的执行,并跳回循环程序块的开头执行下一次循环,而不是立刻循环体.   二.其他用途   break和continue可以配合语句标签使用. 这个都很简单,下面给个综合实例,看看就明白 了: /** * Created by IntelliJ

  • Java中多线程下载图片并压缩能提高效率吗

    目录 前言 实现思路 实测 前言 需求 导出Excel:本身以为是一个简单得导出,但是每行得记录文件中有一列为图片url,需要下载所有记录行对应得图片,然后压缩整个文件夹. 这里只做4.5.得代码讲解描述,其它也没什么好说得,话不多说上代码. 实现思路 多线程实现使用了线程池,Jdk1.8并发包下的CompletableFuture 第一步:得到基础数值 // 线程数 Integer threadNum = 10; // 每条线程需要处理的图片数 int dataNum = imageInfoV

  • java中多线程与线程池的基本使用方法

    目录 前言 继承Thread 实现Runnale接口 Callable 线程池 常见的4种线程池. 总结 前言 在java中,如果每个请求到达就创建一个新线程,开销是相当大的.在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多.除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源.如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或"切换过度"而导致系统资源不足.为了防止资源不足,服务器应用程

随机推荐