详解Java并发编程基础之volatile

目录
  • 一、volatile的定义和实现原理
    • 1、Java并发模型采用的方式
    • 2、volatile的定义
    • 3、volatile的底层实现原理
  • 二、volatile的内存语义
    • 1、volatile的特性
    • 2、volatile写-读建立的happens-before关系
    • 3、volatile的写/读内存语义
  • 三、volatile内存语义的实现
    • 1、volatile重排序规则
    • 2、内存屏障  
    • 3、内存屏障示例
  • 四、volatile与死循环问题
  • 五、volatile对于复合操作非原子性问题

一、volatile的定义和实现原理

1、Java并发模型采用的方式

a)线程通信的机制主要有两种:共享内存和消息传递。

①共享内存:线程之间共享程序的公共状态,通过写-读共享内存中的公共状态来进行隐式通信;

②消息传递:线程之间没有公共状态,线程之间 必须通过发送消息来显式通信。

b)同步:用于控制不同线程之间操作发生相对顺序。在

共享内存模型中,同步是显式的进行的,需要显示的指定某个方法或者代码块在线程执行期间互斥进行。

消息传递模型中,由于消息的发送必定在消息的接受之前,所以同步是隐式的进行的。

c)Java并发采用的是共享内存模型,线程之间通信总是隐式的进行,而且这个通信是对程序员透明的。那么我们需要了解的是这个隐式通信的底层工作机制。

2、volatile的定义

Java编程语言中允许线程访问共享变量,为了确保共享变量能够被准确和一致性的更新,线程应该确保通过排它锁单独获得这个变量。

3、volatile的底层实现原理

a)在编写多线程程序中,使用volatile修饰的共享变量在进行写操作的时候,编译器生成的汇编代码中会多出一条lock指令,这条lock指令的作用:

  • ①将当前处理器缓存行中的数据写回到系统内存
  • ②这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

b)参考下面的这张图理解

二、volatile的内存语义

1、volatile的特性

a)首先我们来看对单个变量的读/写的实现(单个变量的情况可以看做是对同一个锁对这个变量的读/写进行了同步),看下面的例子

package cn.jvm.test;

public class TestVolatile1 {

    volatile long var1 = 0L;

    public void set(long l) {
        // TODO Auto-generated method stub
        var1 = l;
    }

    public void getAndIncrement() {
        // TODO Auto-generated method stub
        var1 ++; //注意++操作
    }

    public long get() {
        return var1;
    }
}

上面的set和get操作在语义上和使用synchronized修饰后一样,即下面的这种写法

package cn.jvm.test;

public class TestVolatile1 {

    volatile long var1 = 0L;

    public synchronized void set(long l) {
        // TODO Auto-generated method stub
        var1 = l;
    }

    public synchronized long get() {
        return var1;
    }
}

b)但是在上面的用例中,我们使用的var1++操作,整体上没有原子性,所以如果使用多线程方粉getAndIncrement方法的话,会导致读出的数据和主存中不一致的情况。

c)volatile变量的特性

①可见性:对一个volatile变量的读操作,总是能够看到对这个volatile变量最后的写入

②原子性:对任意单个volatile变量的读写具有原子性,但是对于volatile变量的复合型操作并不具备原子性

2、volatile写-读建立的happens-before关系

a)看下面的代码实例

package cn.jvm.test;

public class TestVolatile2 {

    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;
        flag = true;
    }

    public void reader() {
        if(flag) {
            int i =a;
            //...其他操作
        }
    }
}

b)在上面的程序中,假设线程A执行write方法,线程B执行reader方法,根据happens-before规则有下面的关系:

程序次序规则:①happens-before②; ③happens-before④

volatile规则:②happens-before③

传递性规则:①happens-before④

所以可以得到下面的这个状态图

3、volatile的写/读内存语义

a)下面是volatile的写/读内存语义

①当写一个volatile变量时候,JMM会将线程对应的本地内存中的共享变量值刷新到主内存中

②当读一个volatile变量的时候,JMM会将线程对应的本地内存置为无效,然后从主内存中读取共享变量

b)还是参照上面的程序示例,参考视图的模型来进行说明

①写内存语义的示意图:假设线程A执行writer方法,线程B执行reader方法,初始状况下线程A和B中的变量都是初始状态

②写内存语义的示意图:

三、volatile内存语义的实现

我们上面说到的基本上从宏观上而言都是说明了volatile保证内存可见性问题,volatile的另一个语义就是禁止指令重排序的优化。下面说一下volatile禁止指令重排序的实现细节

1、volatile重排序规则

①当第二个操作是volatile写的时候,不管第一个操作是什么,都不能进行指令重排序。这个规则确保volatile写之前的操作都不会被重排序到volatile写之后。也是为了保证volatile写对其他线程可见

②当第一个操作为volatile读的时候,不管第二个操作是什么,都不能进行重排序。确保volatile读之后的操作不会被重排序到volatile读之前

③当第一个操作是volatile写,第二个操作是volatile读的时候,不能进行重排序

如下所示,上面的是下表中的总结。

2、内存屏障  

编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止对特定类型的处理器重排序。下面是集中策略,后面会说明这几种情况

①在每个volatile写操作之前插入StoreStore屏障

②在每个volatile写操作之后插入StoreLoad屏障

③在每个volatile读操作之后插入LoadLoad屏障

④在每个volatile读操作之后插入LoadStore屏障

3、内存屏障示例

a)volatile写插入内存屏障之后的指令序列图

b)volatile读插入内存屏障后的指令序列图

四、volatile与死循环问题

1、先看下面的示例代码,观察运行结果,当共享变量isRunning没有被声明为volatile的时候,main线程会在2秒之后将共享变量isRunning置为false并且输出修改信息,这样新建的线程应该结束运行,但是实际上并没有,控制台中会一直保持运行的状态,并且不会打印线程结束执行;如下所示

package cn.jvm.test;

class ThreadDemo extends Thread {
    private  boolean isRunning = true;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始执行");
        while(isRunning) {

        }
        System.out.println(Thread.currentThread().getName() + " 结束执行");
    }
    public boolean isRunning() {
        return isRunning;
    }
    public void SetIsRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
}

public class TestVolatile4 {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        td.start();
        try {
            Thread.sleep(2000);
            td.SetIsRunning(false);
            System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

2、分析出现上面结果的原因

在启动线程ThreadDemo之后,变量isRunning被存在公共堆栈以及线程的私有堆栈中,后//续中线程一直在私有堆栈中取出isRunning的值,虽然main线程执行SetIsRunning方法修改了isRunning的值,但是这个值并没有被Thread-//0线程所知,就像上面说的Thread-0取得值一直都是私有堆栈中的,所以不会知道isRunning被修改,也就不会退出循环

3、按照上面的原因分析一下执行的时候的工作内存和主内存的情况,按照下面的分析我们很容易得出结论

上面的问题就是因为工作内存(私有堆栈)和主内存(公共堆栈)中的值不同步。而按照我们上面说到的volatile使得单个变量保证线程可见性,就可以对程序修改保证共享变量在main线程中的修改对Thread-0线程可见(结合volatile的实现原理)

4、修改之后的结果

package cn.jvm.test;

class ThreadDemo extends Thread {
    private volatile boolean isRunning = true;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始执行");
        while(isRunning) {

        }
        System.out.println(Thread.currentThread().getName() + " 结束执行");
    }
    public boolean isRunning() {
        return isRunning;
    }
    public void SetIsRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
}

public class TestVolatile4 {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        td.start();
        try {
            Thread.sleep(2000);
            td.SetIsRunning(false);
            System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

五、volatile对于复合操作非原子性问题

1、volatile能保证对单个变量在多线程之间的可见性问题,但是对于单个变量的复合操作不能保证原子性,如下代码示例,运行结果为

当然这个结果是随机的,但是不能保证运行结果是100000

在没有使用同步操作之前,虽然count变量是volatile的,但是由于count++操作是个复合操作

①从内存中取出count的值

②计算count的值

③将count的值写到内存中

这个复合操作由于volatile不能保证原子性,所以就会出现错误

package cn.jvm.test;

import java.util.ArrayList;
import java.util.List;

public class TestVolatile5 {
    volatile int count = 0;
    /*synchronized*/ void m(){
        for(int i = 0; i < 10000; i++){
            count++;
        }
    }

    public static void main(String[] args) {
        final TestVolatile5 t = new TestVolatile5();
        List<Thread> threads = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            threads.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    t.m();
                }
            }));
        }
        for(Thread thread : threads){
            thread.start();
        }
        for(Thread thread : threads){
            try {
                thread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println(t.count);
    }
}

2、下面按照JVM的内存工作来分析一下,即当前一个线程在计算count变量的时候,另一个线程已经修改了count变量的值,这样就必然会出现错误。所以对于这种复合操作就需要使用原子类或者使用synchronized来保证原子性(保证同步)

3、修改后的synchronized和使用原子类如下所示

package cn.jvm.test;

 import java.util.ArrayList;
 import java.util.List;

 public class TestVolatile5 {
     int count = 0;
     synchronized void m(){
         for(int i = 0; i < 10000; i++){
             count++;
         }
     }

     public static void main(String[] args) {
         final TestVolatile5 t = new TestVolatile5();
         List<Thread> threads = new ArrayList<>();
         for(int i = 0; i < 10; i++){
             threads.add(new Thread(new Runnable() {
                 @Override
                 public void run() {
                     t.m();
                 }
             }));
         }
         for(Thread thread : threads){
             thread.start();
         }
         for(Thread thread : threads){
             try {
                 thread.join();
             } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
         }
         System.out.println(t.count);
     }
 }
package cn.jvm.test;

 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;

 public class TestVolatile5 {
     AtomicInteger count = new AtomicInteger(0);
     void m(){
         for(int i = 0; i < 10000; i++){
             count.getAndIncrement();
         }
     }

     public static void main(String[] args) {
         final TestVolatile5 t = new TestVolatile5();
         List<Thread> threads = new ArrayList<>();
         for(int i = 0; i < 10; i++){
             threads.add(new Thread(new Runnable() {
                 @Override
                 public void run() {
                     t.m();
                 }
             }));
         }
         for(Thread thread : threads){
             thread.start();
         }
         for(Thread thread : threads){
             try {
                 thread.join();
             } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
         }
         System.out.println(t.count);
     }
 }

以上就是详解Java并发编程基础之volatile的详细内容,更多关于Java 并发编程 volatile的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java基础之详解基本数据类型的使用

    一.整型 主要扩展一下不同进制的整型 二进制.八进制.十进制.十六进制 * 二进制 : 0B(数字零+B) 0b(数字零+b) * 八进制 :0(数字零开头) * 十进制 :正常写就是十进制 * 十六进制 : 0X (数字零+X)0x (数字零+x) A-F 代表 10-15 1.1 测试代码 /** * 进制 : * 二进制 : 0B(数字零+B) 0b(数字零+b) * 八进制 :0(数字零开头) * 十进制 :正常写就是十进制 * 十六进制 : 0X (数字零+X)0x (数字零+x) *

  • Java基础之Unsafe内存操作不安全类详解

    简介 Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,直接操作内存就意味着 1.不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏. 2.Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉. 3.直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率. Unsafe 类 public final c

  • Java基础之详解HashSet的使用方法

    Java HashSet HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合. HashSet 允许有 null 值. HashSet 是无序的,即不会记录插入的顺序. HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的. 您必须在多线程访问时显式同步对 HashSet 的并发访问. HashSet 实现了 Set 接口. HashSet 中的元素实际上是对象,一些常见的基本类型可以使用它的包装类. 添加元素 HashSet

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • 详解Java并发编程之原子类

    目录 原子数组 AtomicIntegerArray 原子更新器 AtomicIntegerFieldUpdater 原子累加器 LongAdder 原子数组 原子数组有AtomicIntegerArray.AtomicLongArray.AtomicReferenceArray,主要是用来对数组中的某个元素进行原子操作.三个类的方法基本类似,这里只介绍一下AtomicIntegerArray的方法. AtomicIntegerArray 两个构造方法,第一个构造方法传入数组长度初始化一个所有值

  • 详解Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提升. synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性. 因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁.监视器锁. 如下实例,在没有使用synchronize

  • 详解java并发编程(2) --Synchronized与Volatile区别

    1 Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同步方法,锁是当前类的class对象. 对于同步方法块,锁是synchronized括号里配置的对象. 当一个代码,方法或者类被synchronized修饰以后.当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁.那么这样做有什么好处呢? 它主要确保多个线程在同一

  • 详解Java线程编程中的volatile关键字的作用

    1.volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true; 这段代码是很典型

  • 详解Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

  • 详解Java数据库连接JDBC基础知识(操作数据库:增删改查)

    一.JDBC简介 JDBC是连接java应用程序和数据库之间的桥梁. 什么是JDBC? Java语言访问数据库的一种规范,是一套API. JDBC (Java Database Connectivity) API,即Java数据库编程接口,是一组标准的Java语言中的接口和类,使用这些接口和类,Java客户端程序可以访问各种不同类型的数据库.比如建立数据库连接.执行SQL语句进行数据的存取操作. JDBC代表Java数据库连接. JDBC库中所包含的API任务通常与数据库使用: 连接到数据库 创

  • 详解Java网络编程

    一.网络编程 1.1.概述 1.计算机网络是通过传输介质.通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来,实现资源共享和数据传输的系统.网络编程就就是编写程序使联网的两个(或多个)设备(例如计算机)之间进行数据传输.Java语言对网络编程提供了良好的支持,通过其提供的接口我们可以很方便地进行网络编程. 2.Java是 Internet 上的语言,它从语言级上提供了对网络应用程 序的支持,程序员能够很容易开发常见的网络应用程序. 3.Java提供的网络类库,可以实现无痛的网络连接,联

  • 详解JAVA 函数式编程

    1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口. 1.2格式: 修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 } //或者 public interface MyFunctionalInterface { void myMethod(); } 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解

  • Java并发编程之关键字volatile知识总结

    一.作用 被 volatile 修饰的变量 1.保证了不同线程对该变量操作的内存可见性 2.禁止指令重排序 二.可见性 Java 内存模型(Java Memory Model) 是 Java 虚拟机定义的一种规范,即每个线程都有自己的工作空间,线程对变量的操作都在线程的工作内存中完成,再同步到主存中,这样可能会导致不同的线程对共享变量的操作,在各自线程工作空间内不一样的问题. 而用 volatile 修饰的变量,线程对该变量的修改,会立刻刷新到主存,其它线程读取该变量时,会重新去主存读取新值.

随机推荐