Java中Synchronized关键字的内存语义是什么?

If two or more threads share an object, and more than one thread updates variables in that shared object, race conditions may occur.

To solve this problem you can use a Java synchronized block. A synchronized block guarantees that only one thread can enter a given critical section of the code at any given time. Synchronized blocks also guarantee that all variables accessed inside the synchronized block will be read in from main memory, and when the thread exits the synchronized block, all updated variables will be flushed back to main memory again, regardless of whether the variable is declared volatile or not.

The Java programming language provides multiple mechanisms for communicating between threads. The most basic of these methods is synchronization, which is implemented using monitors. Each object in Java is associated with a monitor, which a thread can lock or unlock. Only one thread at a time may hold a lock on a monitor. Any other threads attempting to lock that monitor are blocked until they can obtain a lock on that monitor. A thread t may lock a particular monitor multiple times; each unlock reverses the effect of one lock operation.

The synchronized statement computes a reference to an object; it then attempts to perform a lock action on that object's monitor and does not proceed further until the lock action has successfully completed. After the lock action has been performed, the body of the synchronized statement is executed. If execution of the body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor.

A synchronized method automatically performs a lock action when it is invoked; its body is not executed until the lock action has successfully completed. If the method is an instance method, it locks the monitor associated with the instance for which it was invoked (that is, the object that will be known as this during execution of the body of the method). If the method is static, it locks the monitor associated with the Class object that represents the class in which the method is defined. If execution of the method's body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor. 注意: 如果同一个类中有多个用synchronized修饰的方法, 那么对于同一个实例, 这些方法之间也是互斥的, 因为都是使用了这个实例的锁.

synchronized 的内存语义

  • 当线程释放锁时, JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程获取锁时, JMM会把该线程对应的本地内存置为无效. 从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
  • 锁的释放-获取volatile的写-读具有相同的内存语义, volatile可以看成是轻量级的锁.

线程执行互斥代码的过程

  1. 获取监视器锁
  2. 清空工作内存
  3. 从主内存中拷贝变量的最新副本到工作内存
  4. 执行代码
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放监视器锁

如果某个任务处于一个对标记为synchronized的方法的调用中, 那么在这个线程从该方法返回之前, 其它所有要调用类中任何标记为synchronized方法的线程都会被阻塞.

Java中Volatile关键字的内存语义是什么?

volatile keyword can make sure that a given variable is read directly from main memory, and always written back to main memory when updated

volatile是通过加入内存屏障禁止指令重排序来实现的

  • 对volatile变量执行写操作时, 会在写操作后加入一条store屏障指令, 这样就会把读写时的数据缓存加载到主内存中
  • 对volatile变量执行读操作时, 会在读操作前加入一条load屏障指令, 这样就会从主内存中加载变量
  • 当后一个操作是volatile写时, 不管前一个操作是什么, 都不能重排序. 这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后.
  • 当前一个操作是volatile读时, 不管后一个操作是什么, 都不能重排序. 这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前.
  • 当前一个操作是volatile写, 后一个操作是volatile读时, 不能重排序

所以说, volatile变量在每次被线程访问时, 都强迫从主内存中重读该变量的值, 而当该变量发生变化时, 就会强迫线程将最新的值刷新到主内存, 这样任何时刻, 不同的线程总能看到该变量的最新值.

  • 线程写volatile变量的过程

    1. 改变线程工作内存中volatile变量副本的值
    2. 将改变后的副本的值从工作内存刷新到主内存中
  • 线程读volatile变量的过程
    1. 从主内存中读取volatile变量的最新值到线程的工作内存中
    2. 从工作内存中读取volatile变量的副本

volatile变量也存在一些局限: 不能用于构建原子的复合操作, 因此当一个变量依赖旧值时就不能使用volatile变量, 例如在嵌入式设备中, volatile的变量在使用的过程中, 值可能会因为硬件产生变化.

JDK各版本对volatile的处理有什么不同

JDK5之前对volatile的处理和JDK5是不同的

  • 在JDK4及之前, 对volatile变量的读写与对其他变量的读写指令, 在编译优化阶段可能会被调换顺序
  • 在JDK5之后保证了发生在volatile变量之前的读写, 不会被调整到volatile变量的读写之后. 为了实现volatile内存语义, JMM会分别限制编译器重排序和处理器重排序

为了实现volatile的内存语义, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

JDK5以及之后的顺序保证(Happens-Before Guarantee): 读取之后不提前, 写入之前不推后

  • 如果代码中对某个变量的读取和写入发生在对volatile变量的写入之前, 那么编译后这个读写操作保证不会被调整到对volatile的写入之后. 注意这仅仅是保证发生在volatile写入之前的操作不会放到后面, 但是不能保证volatile写入之后的操作不会被放到前面.
  • 如果代码中对某个变量的读取和写入发生在对volatile变量的读取之后, 那么编译后这个读写操作保证不会被调整到对volatile的读取之前. 注意这也不能保证volatile读取之前的操作不会被放到后面.

JDK5的这个改变, 也是为了解决double-checked locking问题

double-checked locking 问题

double-checked locking是一种单例延迟初始化的实现, 代码如下

// double-checked-locking - don't do this!

private static Something instance = null;

public Something getInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null)
instance = new Something();
}
}
return instance;
}

This looks awfully clever -- the synchronization is avoided on the common code path. There's only one problem with it -- it doesn't work.

Why not? The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object. There are lots of other reasons why this is wrong, and why algorithmic corrections to it are wrong. There is no way to fix it using the old Java memory model.

Many people assumed that the use of the volatile keyword would eliminate the problems that arise when trying to use the double-checked-locking pattern. In JVMs prior to 1.5, volatile would not ensure that it worked (your mileage may vary). Under the new memory model, making the instance field volatile will "fix" the problems with double-checked locking, because then there will be a happens-before relationship between the initialization of the Something by the constructing thread and the return of its value by the thread that reads it.

什么是伪共享(False Sharing),为何会出现, 以及如何避免?

Memory is stored within the cache system in units know as cache lines. Cache lines are a power of 2 of contiguous bytes which are typically 32-256 in size. The most common cache line size is 64 bytes. False sharing is a term which applies when threads unwittingly impact the performance of each other while modifying independent variables sharing the same cache line. Write contention on cache lines is the single most limiting factor on achieving scalability for parallel threads of execution in an SMP system. I’ve heard false sharing described as the silent performance killer because it is far from obvious when looking at code.

To achieve linear scalability with number of threads, we must ensure no two threads write to the same variable or cache line. Two threads writing to the same variable can be tracked down at a code level. To be able to know if independent variables share the same cache line we need to know the memory layout, or we can get a tool to tell us. Intel VTune is such a profiling tool. In this article I’ll explain how memory is laid out for Java objects and how we can pad out our cache lines to avoid false sharing.

讨论这个问题, 需要先了解以下知识

  • 多核CPU的每个core都有自己的缓存
  • 每个core访问数据的时候, 首先会尝试从缓存中读取, 如果缓存中不存在, 再从内存中读取.
  • 每个core将数据从内存加载到缓存中是以块为单位的, 称为cache line, 一般大小是64字节

在实际的程序执行中, 如果定义两个相邻的long变量var0和var1, 现在出现这种情况

  1. core 0 和 core 1 分别在执行不同的线程, 其中 core 0 使用的 var0 和 core 1 使用的 var1 存储在了同一个 cache line上
  2. core 0 修改了 var0. 也就是说core 0对 var0 做了一次修改, 需要把这个cache line的所有数据同步到内存中. 同时需要把core 1 中的这个缓存置为失效, 这个过程是由CPU的缓存一致性协议(MESI)保证的.
  3. 当core 1 需要读取 var1 的时候就发现缓存失效了, 需要重新从内存中加载,

上面这个例子中, 缓存的存在不仅没有使访问变快, 反而使得这次访问变慢了. 所以问题在于对于var0的修改, 导致对于 var1 的访问缓存命中失效, 使得软件上没有关系的变量在硬件上耦合了.

所以伪共享问题可以表示为: 几个在逻辑上(使用上)并不包含在同一个内存单元内的数据, 由于被cpu加载在同一个缓存行cache line当中, 当在多线程环境下被不同的core执行, 导致缓存行失效而引起的缓存命中率降低.

在频繁访问的场景下会有很大的性能损耗. 解决的方式也就是避免二者在一个cache line里面. 由于一个cache line一般是64字节, 所以只需要在var0和var1后填充7个long型的变量即可.

Java多线程专题2: JMM(Java内存模型)的更多相关文章

  1. Java多线程专题1: 并发与并行的基础概念

    合集目录 Java多线程专题1: 并发与并行的基础概念 什么是多线程并发和并行? 并发: Concurrency 特指单核可以处理多任务, 这种机制主要实现于操作系统层面, 用于充分利用单CPU的性能 ...

  2. Java 运行时数据区和内存模型

    运行时数据区是指对 JVM 运行过程中涉及到的内存根据功能.目的进行的划分,而内存模型可以理解为对内存进行存取操作的过程定义.总是有人望文生义的将前者描述为 "Java 内存模型" ...

  3. Java多线程专题6: Queue和List

    合集目录 Java多线程专题6: Queue和List CopyOnWriteArrayList 如何通过写时拷贝实现并发安全的 List? CopyOnWrite(COW), 是计算机程序设计领域中 ...

  4. Java多线程专题3: Thread和ThreadLocal

    合集目录 Java多线程专题3: Thread和ThreadLocal 进程, 线程, 协程的区别 进程 Process 进程提供了执行一个程序所需要的所有资源, 一个进程的资源包括虚拟的地址空间, ...

  5. Java多线程专题4: 锁的实现基础 AQS

    合集目录 Java多线程专题4: 锁的实现基础 AQS 对 AQS(AbstractQueuedSynchronizer)的理解 Provides a framework for implementi ...

  6. Java多线程专题5: JUC, 锁

    合集目录 Java多线程专题5: JUC, 锁 什么是可重入锁.公平锁.非公平锁.独占锁.共享锁 可重入锁 ReentrantLock A ReentrantLock is owned by the ...

  7. Java多线程(四)java中的Sleep方法

    点我跳过黑哥的卑鄙广告行为,进入正文. Java多线程系列更新中~ 正式篇: Java多线程(一) 什么是线程 Java多线程(二)关于多线程的CPU密集型和IO密集型这件事 Java多线程(三)如何 ...

  8. Java多线程 -- 深入理解JMM(Java内存模型) --(五)锁

    锁的释放-获取建立的happens before 关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 下面是锁释放-获取的示例代 ...

  9. Java并发编程:JMM(Java内存模型)和volatile

    1. 并发编程的3个概念 并发编程时,要想并发程序正确地执行,必须要保证原子性.可见性和有序性.只要有一个没有被保证,就有可能会导致程序运行不正确. 1.1. 原子性 原子性:即一个或多个操作要么全部 ...

随机推荐

  1. Docker 与 K8S学习笔记(六)—— 容器的资源限制

    我们在启动Docker容器时,默认情况下容器所使用的资源是没有限制的,这样就会存在部分特别耗资源的容器会占用大量系统资源,从而导致其他容器甚至整个服务器性能降低,为此,Docker提供了一系列参数方便 ...

  2. mac学习Python第一天:安装、软件说明、运行python的三种方法

    一.Python安装 从Python官网下载Python 3.x的安装程序,下载后双击运行并安装即可: Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的. MAC 系统一般 ...

  3. MySQL与Oracle 差异比较之二函数

    函数 编号 类别 ORACLE MYSQL 注释 1 数字函数 round(1.23456,4) round(1.23456,4) 一样:ORACLE:select round(1.23456,4) ...

  4. Codeforces 567D:One-Dimensional Battle Ships(二分)

    time limit per test : 1 second memory limit per test : 256 megabytes input : standard input output : ...

  5. 人脸识别中的重要环节-对齐之3D变换-Java版(文末附开源地址)

    一.人脸对齐基本概念 人脸对齐通过人脸关键点检测得到人脸的关键点坐标,然后根据人脸的关键点坐标调整人脸的角度,使人脸对齐,由于输入图像的尺寸是大小不一的,人脸区域大小也不相同,角度不一样,所以要通过坐 ...

  6. vue是如何通过diff算法做渲染更新

    渲染页面 图中框起来的部分,vue会根据响应式变化的通知生成一颗新的 Virtual Dom Tree,然后将新的Virtual Dom Tree 和之前的 Virtual Dom Tree 做 di ...

  7. Java Swing 如何设置图片大小

    如下两行代码搞定: Image image = new ImageIcon("Img/ackground.jpg").getImage();// 这是背景图片 .png .jpg ...

  8. CSS基础 装饰 元素本身隐藏和显示效果及案例

    1.visibility:hidden; 2.display: none: 区别: 1.visibility:hidden 隐藏元素本身,且在网页中 占位置 2.display:none; 隐藏元素本 ...

  9. nginx - win系统启动一闪而过 ,服务没有启动成功

    这种现象是因为配置文件里配置的服务监听端口被占了

  10. 由浅入深学习Apache httpd原理与配置

    一.apache简介: Apache HTTPD又可以简称为httpd或者Apache,它是Internet使用最广泛的web服务器之一,使用Apache提供的web服务器是由守护进程httpd,通过 ...