【多线程与并发】:多线程与内存可见性

目录

    • 目录
    • 前言
    • 1.基本概念
      • 1.1线程与进程
      • 1.2JAVA内存模型(JMM)
      • 1.3常见的可见性错误
    • 2.解决方案
      • 2.1synchronized关键字
      • 2.2volatile关键字

前言

可见性是一种复杂的属性,因为其错误总会违背我们的直觉。在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。然而,当读和写操作在不同线程中执行时,情况却并非如此。

通常没我们是无法确保执行读操作的线程能适时的看到其他线程写入的值,有时候甚至是不可能的事情。

那么,为什么无法确保执行读操作的线程能适时的看到?为了彻底了解该问题,首先我们需要了解一些线程的基本概念。

1.基本概念

1.1线程与进程

进程:程序(任务)的执行过程,是动态的;持有资源(内存,文件)和线程,是资源和线程的载体。

线程:线程是系统中最小的执行单元,同一进程中有很多线程,线程共享进程的资源。

换句话说,n个线程(n大于等于1)和所有的资源共同组成了进程,参见下图:

【多线程与并发】:多线程与内存可见性_第1张图片

1.2JAVA内存模型(JMM)

什么是JAVA内存模型?

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

抽象示意图如下:
【多线程与并发】:多线程与内存可见性_第2张图片

从上图可以看出,如果要在线程1和线程2之间通信的话,必须要经过下面两个步骤:
1、线程1把本地内存1中更新过的共享变量刷新到主内存中去。
2、线程2到主内存中去读取线程2之前已更新过的共享变量。

到这里,我们就可以知道,由于线程之间的交互都发生在主内存中,但对于变量的修改又发生在自己的工作内存中,经常会造成读写共享变量的错误,我们也叫可见性错误,而这也就是内存可见性的重要之处。

1.3常见的可见性错误

下面我们列举一些常见的可见性错误。

  1. 重排序:
    在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

  2. 失效数据:
    在缺乏同步的程序中没可能出现的一种结果就是失效数据。当读线程查看变量时,可能会得到一个已经失效的值,其原因可能是共享变量更新后的值没有在工作内存和主内存中及时更新。更糟糕的是,失效值可能不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。

    3.非原子的64位操作
    当线程在没有同步的情况下读取变量,可能会获取一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)。
    最低安全性适用于绝大多数变量,但是存在一个意外:非volatile类型的64位数值变量(double和long)。对于非volatile类型的double和long变量,JVM允许将64位的读操作和写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读到某个值的高32位和另一个值的低32位。

2.解决方案

2.1synchronized关键字

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,在这种情况下,可以保证,在锁被释放之前,A看到的变量值在B获得锁后同样可以由B看到。

这是因为,JMM关于synchronized的两条规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中

  2. 线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

这样,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

虽然这种方式非常好用,但是也存在着一些问题:
会带来性能问题,效率特别低,造成线程阻塞。

2.2volatile关键字

java 提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当多个线程进行操作共享数据时,可以保证内存中的数据可见。 相较于synchronized是一种较为轻量级的同步策略。

把一个变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可变的地方,因此在读取volatile变量时总会返回最新写入的值。

线程写volatile变量的过程:

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

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

线程读volatile变量的过程:

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

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

虽然volatile变量很方便,但是也存在一些局限性。volatile变量通常用作某个操作完成、发生中断或者状态的标志。因为volatile变量只能保证可见性不能保证原子性。如果对volatile变量进行递增操作(count++),那么该操作并不是原子性的,其“读-改-写”是对多线程而言是分步无序的,我们无法确保其按照顺序执行,会导致结果的错误。

所以:
volatile方案:

  1. 能够保证volatile变量的可见性

  2. 不能保证变量状态的”原子性操作(Atomic operations)”

你可能感兴趣的:(多线程与并发)