【JAVA】多线程之内存可见性

news/2024/5/17 18:18:08 标签: 多线程, 内存可见性, synchronized, volatile

                                    多线程内存可见性

一、什么是可见性?

一个线程对共享变量值的修改,能够及时地被其他线程所看到。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

工作内存:每个线程拥有自己的工作内存,只能对自己工作内存中的变量副本进行修改,而不能直接修改主内存中的变量。

变量副本:主内存中变量的一份拷贝

 

二、主内存与工作内存之间的关系

注意:

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

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

 

三、共享变量可见性实现的原理

线程1对共享变量的修改要想被线程2及时看到,必须要经过如下的2个步骤

(1)把工作内存1中更新过的共享变量刷新到主内存中

(2)将主内存中最新的共享变量的值更新到工作内存2中

变量传递顺序

 

四、可见性实现方式

(1)synchronized,能够实现原子性(同步)与可见性

(2)volatile,能够实现变量可见性,但不能保证变量的原子性


synchronized%E5%AE%9E%E7%8E%B0%E5%8F%AF%E8%A7%81%E6%80%A7">【1】synchronized实现可见性

JMM(java内存模型)中关于synchronized的两条规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中读取最新的值

那么,线程执行互斥代码的流程就是:

(1)获得互斥锁

(2)清空工作内存

(3)从主内存中拷贝变量的最新副本到工作内存中

(4)执行互斥代码

(5)将更改后的共享变量的值刷新到主内存中

(6)释放互斥锁


【2】volatile实现可见性

通过加入内存屏障和禁止指令重排序优化来实现的。每次读取用volatile修饰的变量的值,都会从主内存中读取该变量。

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。

那么,线程写volatile变量的过程:

(1)改变线程工作内存中volatile变量副本的值

(2)将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的值的过程:

(1)从主内存中读取volatile变量的最新值到线程的工作内存中

(2)从工作内存中读取volatile变量的副本


 

【3】volatile不能保证变量复合操作的原子性

例如:以下操作不是原子操作

private int number=0;
number++;

可以分解成如下操作:

(1)读取number的值

(2)将number的值加1

(3)写入最新的number的值


假如我们使得volatile int  i=0;并且大量线程调用i的自增操作,那么volatile可以保证变量的安全吗?

不可以保证,volatile不能保证变量操作的原子性,自增操作包括三个步骤,分别是读取,加一,写入,由于这三个子操作的原子性不能被保证,那么n个线程总共调用n次i++的操作后,最后的i的值并不是大家想的n,而是一个比n小的数。

解释:比如A线程执行自增操作,刚读取到i的初始值0,然后就被阻塞了。B线程现在开始执行,还是读取到i的初始值0,执行自增操作,此时i的值为1。然后A线程阻塞结束,对刚才拿到的0执行加一与写入操作,执行成功后,i的值被写成1了,我们预期输出2,可是输出的是1,输出比预期小。

代码验证:

package day0829;
 
import java.util.ArrayList;
import java.util.List;
 
public class VolatileTest {
    public volatile int i = 0;
 
    public void increase() {
        i++;
    }
 
    public static void main(String args[]) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        VolatileTest test = new VolatileTest();
        for (int j = 0; j < 10000; j++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    test.increase();
                }
            });
            thread.start();
            threadList.add(thread);
        }
 
        //等待所有线程执行完毕
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.print(test.i);
    }
}

此时输出为:


那么如何保证i自增操作的原子性呢?

(1)使用synchronized关键字

(2)使用ReentranLock

(3)使用AtomicInteger


相关问题:

(1)即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存中得到及时的更新?

一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为cpu在执行时会很快地刷新缓存,所以一般情况下很难看到这种不可见的问题。

 

五、synchronizedvolatile的区别

两者的区别请移步我的另外一篇博客volatilesynchronized的区别

 

六、多线程中其他知识点

指令重排序

代码书写的顺序与程序实际执行的顺序不同,指令重排序是编译器或处理器为了提高性能而做的优化。


as-if-serial语义

无论如何进行重排序,程序执行的结果与代码原本顺序执行的结果一致(java会保证在单线程下遵循此语义)

重排序不会给单线程带来内存可见性问题,但在多线程中,程序交错执行,重排序可能会造成内存可见性问题

 

 


http://www.niftyadmin.cn/n/1705720.html

相关文章

使用CSS计数器美化有序列表

在web设计中&#xff0c;使用一种井井有条的方法来展示数据是十分重要的&#xff0c;这样用户就可以很清晰的理解网站所展示的数据结构和内容&#xff0c;使用有序列表就是实现数据有组织的展示的一种简单方法。 如果你需要更加深入地控制有序列表数字的样式&#xff0c;你可能…

【JAVA】创建线程的两种方式Thread与Runnable

创建线程的两种方式Thread与Runnable 一、简要说明 创建线程的两种方式&#xff0c;一是继承Thread类&#xff0c;二是实现Runnable接口&#xff0c;最后都是依据Thread类的构造方法实例化出一个线程对象&#xff0c;调用线程对象的start()方法&#xff0c;就可以通知线程启动…

充满期待的2007

上篇对2006年的图书创作工作和心情进行了一下回顾和总结。尽管有一些不如意&#xff0c;但总的来说&#xff0c;还是一个收获的年份&#xff0c;至少有多份大奖是我一直期盼的&#xff0c;可算是圆了我的一个梦。尽管&#xff0c;对于我个人来说&#xff0c;就主要写作工作来说…

【操作系统】进程与线程的区别

进程与线程的区别 进程与线程是两个比较容易混淆的概念&#xff0c;但实际上他们是两个不一样的东西。 一、各自包含什么&#xff1f; 进程是线程的容器&#xff0c;因此简单地来讲&#xff0c;一个进程内部包含一个或多个线程。 线程是进程的一个实体&#xff0c;包含程序计…

【JAVA】程序初始化的顺序

程序初始化的顺序 我们先从一段程序开始 package day0901;class A {public static int a getA();static {System.out.println("父类的静态方法");}{System.out.println("父类的非静态代码块");}public A() {System.out.println("父类的构造函数&qu…

两种纯CSS方式实现hover图片pop-out弹出效果

实现原理 主要图形的组成元素由背景和前景图两个元素&#xff0c;以下示例代码中&#xff0c;背景元素使用伪元素 figure::before 表示&#xff0c; 前景元素使用 figure img 表示&#xff0c;当鼠标 hover 悬浮至 figure 元素时&#xff0c;背景元素产生变大效果&#xff0c;…

网络安全管理-用户账号

现在的linux在安全方面都已经很完善了&#xff0c;但是还存在一些安全隐患&#xff0c;这就需要我们在安全机制上的加强。1、账号和口令主要是/etc/passwd和/etc/shadow两个文件&#xff0c;/etc/passwd主要是存放账户&#xff0c;/etc/shadow主要是存放用户口令。这两个文件非…

【JAVA】重载与重写的含义与区别

重载与重写的含义与区别 重载与重写是java多态性的不同表现方式&#xff0c;在编程中特别常见。理解两者的区别&#xff0c;对我们编写高质量的代码尤为重要。 一、重载&#xff08;overload&#xff09; 重载是指在一个类中定义了多个同名的方法&#xff0c;他们有着不同的参…