多线程(五)—— 内存可见性

一、可见性

   多个线程对同一个变量(称为:共享变量)进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储在自己的存储器中,等到进行完操作后,再赋值回主存。

  这样做的好处是提高了运行的速度,同样优化带来的问题之一是变量可见性——如果线程t1与线程t2分别被安排在了不同的处理器上面,那么t1与t2对于变量A的修改时相互不可见,如果t1给A赋值,然后t2又赋新值,那么t2的操作就将t1的操作覆盖掉了,这样会产生不可预料的结果。因此,需要保证变量的可见性(一个线程对共享变量值的修改,能够及时地被其它线程看到)。

  注意:共享数据的访问权限必须定义为private

  多线程操作共享变量实现可见性过程JVM的内存模型如下:

 

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

 JVM模型两条规定:

   1、线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写

   2、不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

  那么共享变量应该怎样实现可见性呢?  

  比如:线程1对共享变量的修改想要被线程2及时看到,必须经过如下2步操作:
  把工作内存1中的更新过的共享变量刷新到主内存中,再将主内存中最新的共享变量的值更新到2的工作内存中。

  初始值X=0,线程1将X=1,到其他线程X值的更改,如下图的更改过程。

                  

   因此,要实现共享变量的可见性,必须保证两点:
   线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
   其他线程能够及时的把共享变量的最新值从主内存更新到自己的工作内存中。
   在Java语言层面支持的可见性实现原理方式有synchronize和volatile。

备注:

导致共享变量在线程间不可见的原因:
  线程的交叉执行;
  重排序结合线程的交叉执行;
  共享变量更新后的值没有在工作内存与主内存及时更新。

二、synchronize 

  能够实现代码的原子性(同步)和 内存的可见性。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性就像数据库里面的事务一样,他们是一个团队,同生共死。其实理解原子性非常简单,我们看下面一个简单的例子即可:

i = 0;---1

j = i ; ---2

i++; ---3

i = j + 1; ---4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。

1—在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 
2—包含了两个操作:读取i,将i值赋值给j 
3—包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 
4—同三一样
  在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。

   JVM对其的两条规定:
  线程解锁前,必须把共享变量的最新值刷新到主内存中;
  线程枷锁前,将清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中重新读取新的值。(枷锁与解锁需要是同一把锁)。
  线程解锁前对共享线程变量的修改在下次枷锁时对其他线程可见。

  线程 执行互斥代码的过程:    

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

三、volatile

  保证变量的可见性,不能保证变量的符合操作原子性。

实现内存可见:
       深入的说:通过加入内存屏障和禁止重排序优化实现。对其变量执行写操作时,会在写操作后加入一条store屏障指令;对其进行读操作时,会在读操作前加入一条load屏障指令。
       通俗的说:volatile变量在每次被线程访问时,都强迫从主线程中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中,这样任何时刻,不同的线程总能看到该变量最新的值

  线程写volatile变量的过程:
         1、改变线程工作内存中volatile变量副本的值;
    2、将改变后的副本的值从工作内存刷新到主内存。
    3、线程读volatile变量的过程:
    4、从主内存中读取volatile变量的最新值到线程的工作内存中;
    5、从工作内存中读取volatile变量的副本。

  验证 volatile 可以保证原子性。代码如下:

/**
 * 验证  volatile 是否保证原子性
 * @author Administrator
 *
 */
public class VolatileDemo {
    private  volatile 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();
        }
            this.number++;
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo();
        //实现500次自增
        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());
    }
}

  上述代码,理想情况或单线程时输出结果应该为:500,然而实际输出的结果是小于500的数字(多执行几遍)。这是因为 number++ 不是原子操作,会造成多个线程交叉执行。

方法一: synchronized

public void increase(){
        try {
            //更好的输出效果
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        synchronized(this){
            this.number++;
        }
    }

  当然,也可以 public synchronized int increase(){...} 但是这样造成程序性能更加低效。

方法二: java.util.concurrent.locks.ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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());
    }
}

方法三:java.util.concurrent.atomic.AtomicInteger;

import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
    private static AtomicInteger a = new AtomicInteger();
    public int getNumber(){
        return a.get();
    }
    public void increase(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        a.getAndIncrement();
    }
    /**
     * @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());
    }
}

  AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,因此十分适合高并发情况下的使用。

java.util.concurrent中实现的原子操作类包括:AtomicBoolean、AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicReference、 AtomicReferenceArray。

volatile适用场合

在多线程中安全的使用volatile变量必须同时满足两个条件:
  ①对变量的写入操作不依赖其当前值,如number++不可以,boolean变量可以
  ②该变量没有包含在具有其他变量的不变式中,如果有多个volatile变量,则每个volatile变量必须独立于其他的volatile变量

四、synchronize 与 volatile 比较

  1、volatile不需要枷锁,比synchronize更轻量级,不会堵塞程序;
  2、从内存可见角度讲,volatile读相当于枷锁,volatile写相当于解锁。
  3、synchronize既能保证可见性,又能保障原子性,而volatile只能保障可见性,不能保证原子性。
       4、synchronize 使用更加广泛。

  

来自慕课网课程:细说Java多线程之内存可见性

原文地址:https://www.cnblogs.com/SacredOdysseyHD/p/8438410.html

时间: 2024-10-12 16:28:07

多线程(五)—— 内存可见性的相关文章

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

1.JMM:Java Memory Model(Java内存模型) 关于synchronized的两条规定: 1.线程解锁前,必须把共享变量的最新值刷新到主内存中 2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁需要是同一把锁) 注:线程解锁前对共享变量的修改在下次加锁时对其他线程可见 2.线程执行互斥代码的过程: 1.获得互斥锁 2.清空工作内存 3.从主内存拷贝变量的最新副本到工作内存 4.执行代码 5.将更改后的共享变量的值刷

JavaSE学习53:细说多线程之内存可见性

一共享变量在线程间的可见性 (1)有关可见性的一些概念介绍 可见性:一个线程对共享变量值的修改,能够及实地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量.所 有的变量都存储在主内存中. 线程的工作内存:每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的 一份拷贝). (2)数据争用问题 多个线程对同一资源操作时,通常会产生进程,比如一个线程往消息队列插入数据,而另一个线程从消息队列取 出数据 当消息队

java多线程与内存可见性

一.java多线程 JAVA多线程实现的三种方式: http://blog.csdn.net/aboy123/article/details/38307539 二.内存可见性 1.什么是JAVA 内存模型 共享变量 :如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存

Java多线程之内存可见性

1.什么是JAVA 内存模型 Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互. 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的. Jav

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

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线 程的共享变量. Java内存模型: 线程对共享变量操作的规定: 共享变量可见性实现的原理: 可见性的必要条件: 可见性的实现方式:(final也可以) 线程执行互斥代码的过程: 重排序的概念: as-if-serial的概念: 导致共享变量在线程间不可见的原因: 可见性分析: 当一个线程访问object的一个sysnchronized(this)同步代

细说Java多线程支内存可见性

一.共享变量在线程间的可见性 可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是几个线程的共享变量. Java内存模型(Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节. Java内存模型(JVM):● 所有的变量都存储在主内存中.● 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主

多线程内存可见性

一.细说Java多线程之内存可见性(数据挣用)         1.共享变量在线程间的可见性                共享变量:如果一个变量在多个线程的工作内存中都存在副本,                         那么这个变量就是这几个线程的共享变量                可见性:一个线程对共享变量值的修改,能够及时的被其他线程看到                Java内存模型(JMM,Java Memory Model):                      描

java多线程 -- volatile 关键字 内存 可见性

内存可见性(Memory Visibility) 1 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象   状态后,其他线程能够看到发生的状态变化. 2 可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.   我们可以通过同步来保证对象被安全地发布.除此之外我们也可以使用一种更加轻量级的 volatile 变量. vola

Java多线程中的内存可见性

刚刚看了一下synchronized和volatile的区别,这里做一下笔记. 多线程中内存是如何分配的? 分为主内存和线程内存,当线程与其他线程共享一个变量时,便会把主内存的变量复制到线程内存中去.当发生对变量的修改时,会同步到主内存,主内存再同步到其他线程内存中去. Synchronized实现可见性 JMM对Synchronized规定: 线程加锁时,将清空线程内存中共享变量的值,从而使用共享变量时从主内存中重新读取新值. 线程解锁前,必须把共享变量的最新值刷新到主内存中. 线程执行互斥代