多线程之美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++;
        }
    }

    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、原因分析
对单个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多线程编程核心技术- 高洪岩

原文地址:https://www.cnblogs.com/flydashpig/p/11875652.html

时间: 2024-10-22 04:20:03

多线程之美1一volatile的相关文章

多线程中对static和volatile的理解

问题来源于编码规范的一个例子 一. 关于server模式下的主存和工作内存 规则40     多线程访问同一个可变变量,需增加同步机制 说明:根据Java Language Specification中对Java内存模型的定义, JVM中存在一个主内存(Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的.每个线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无

细说.NET中的多线程 (六 使用MemoryBarrier,Volatile进行同步)

上一节介绍了使用信号量进行同步,本节主要介绍一些非阻塞同步的方法.本节主要介绍MemoryBarrier,volatile,Interlocked. MemoryBarriers 本文简单的介绍一下这两个概念,假设下面的代码: using System; class Foo { int _answer; bool _complete; void A() { _answer = 123; _complete = true; } void B() { if (_complete) Console.W

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

Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个关键字的作用就是告诉编译器,只要是被此关键字修饰的变量都是易变的.不稳定的.那为什么是易变的呢?因为volatile所修饰的变量是直接存在于主内存中的,线程对变量的操作也是直接反映在主内存中,所以说其是易变的. 一.Java内存模型 Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理

多线程之美5一 AbstractQueuedSynchronizer源码分析&lt;一&gt;

AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ? 5.1.acquire(int arg ) ? 5.2.release(int arg) 释放锁 6.总结 前言 在多线程环境下,我们一般会对临界区资源(共享资源)进行加锁,释放锁,保证同一时刻最多只有一个线程(独占模式),就如去公共厕所里,在使用一个小房间时会加锁避免自己在使用的时候,别人突然闯进来一样,引起不必要的麻烦,在使用完后,再打开锁,其他人才可使用

多线程之美6一CAS与自旋锁

1.什么是CAS CAS 即 compare and swap 比较并交换, 涉及到三个参数,内存值V, 预期值A, 要更新为的值B, 拿着预期值A与内存值V比较,相等则符合预期,将内存值V更新为B, 不相等,则不能更新V. 为什么预期值A与内存值V不一样了呢? 在多线程环境下,对于临界区的共享资源,所有线程都可以访问修改,这时为了保证数据不会发生错误,通常会对访问临界区资源加锁,同一时刻最多只能让一个线程访问(独占模式下),这样会让线程到临界区时串行执行,加锁操作可能会导致并发性能降低,而循环

多线程之美7一ReentrantReadWriteLock源码分析

目录 前言 在多线程环境下,为了保证线程安全, 我们通常会对共享资源加锁操作,我们常用Synchronized关键字或者ReentrantLock 来实现,这两者加锁方式都是排他锁,即同一时刻最多允许一个线程操作,然而大多数场景中对共享资源读多于写,那么存在线程安全问题的是写操作(修改,添加,删除),我们是否应该考虑将读和写两个分开,只要运用合理,并发性能是不是可以提高,吞吐量增大呢? ReentrantReadWriteLock已经为我们实现了这种机制,我们一起来看它是怎样实现的吧! 1.读写

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

共享变量可见性实现的原理 java 语言层面支持的可见性实现方式: synchronized volatile 1. synchronized 的两条规定: 1 线程解锁前,必须把共享变量的最新值刷新到主内存中. 2 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要是同一锁) 线程解锁前对共享变量的修改在下次加锁时对其他线程可见. 2. volatile 实现可见性 深入来说,通过加入内存屏障和禁止重排序优化来实现 的. 对volatil

JAVA多线程并发变量控制方法之volatile修饰工作原理

在JAVA中,每个线程都有一块属于自己的工作内存区,该内存区会保存一份从主内存拷贝过来的公共变量值.不加volatile修身的变量在每个线程中的值修改一般都是独立的.及如下图所示. 1.开始执行时A.B线程从主内存区load变量i=0.此时值是一样的. 2.当B线程将变量i=1时,此时并不会立刻写入i=1到主内区,所以A线程工作区i还是等于0. 3.如果i变量用Volatile修饰后,B线程改变变量i的值,会立刻返回写回主内存区中.如下图所示. package list; public clas

(C#) 多线程修改布尔值, volatile

参考: https://msdn.microsoft.com/en-us/library/x13ttww7(VS.80).aspx http://stackoverflow.com/questions/2474945/is-it-safe-to-use-a-boolean-flag-to-stop-a-thread-from-running-in-c-sharp https://msdn.microsoft.com/en-us/library/7a2f3ay4(v=vs.80).aspx