【JUC】共享模型之内存

1. 概述

上一小节主要介绍了《共享模型之管程》相关内容,介绍了在 Java 层面如何使用synchronized关键字保证多个线程并发运行的原子性,而本章聚焦于JMM(Java Memory Model)层面,深入了解主存、工作内存相关原理与运行流程,本章重点需要解决以下三方面问题:

  • 原子性:保证临界区代码执行的原子性(可通过悲观锁实现)
  • 可见性:保证指令执行不会受到缓存的影响(一个线程的修改操作对其他线程可见)
  • 有序性:保证指令执行顺序不会受到 CPU 并行优化干扰

2. 可见性

2.1 问题引入

我们先来看看下面这段代码的效果:

/**
 * 测试可见性
 * @author ricejson
 */
@Slf4j
public class TestVisible {
    private static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!isStop) {

            }
            log.debug("任务运行结束...");
        }, "work").start();
        // 睡眠1s后设置工作线程停止运行
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        log.debug("主线程退出...");
    }
}

这段代码本意是主线程在 1s 后睡眠结束并设置isStop变量为 true,此处工作线程访问变量不满足循环条件后退出运行(以此达到中断工作线程运行的效果),但是实际效果如下图所示。

现象说明当主线程退出后(一定将 isStop 设置为true),工作线程访问到的isStop变量仍然为 false,造成无法退出循环的现象

2.2 可见性分析

想要知道2.1小节为什么会造成这种现象的原因?我们必须要掌握 JMM 当中主内存、工作内存的运行机制,如下图所示。

其实不难分析,按照原先的程序运行过程,work线程需要每次循环都获取到主内存当中的变量值,因此JVM当中的 JIT 会进行优化:将主内存的变量缓存到工作内存当中,因此后续即使主线程对主内存的变量进行修改,work线程访问的始终是工作内存的缓存结果(主内存变量对工作线程不可见)

2.3 解决方案

解决办法很简单,只需要在对应的变量定义上加上volatile关键字,该关键字可以用于控制变量访问始终通过主内存获取,修改后的代码如下:

/**
 * 测试可见性
 * @author ricejson
 */
@Slf4j
public class TestVisible {
    private volatile static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!isStop) {

            }
            log.debug("任务运行结束...");
        }, "work").start();
        // 睡眠1s后设置工作线程停止运行
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        log.debug("主线程退出...");
    }
}

运行效果如下:

2.4 扩展—两阶段终止模式

2.4.1 背景

当前的两阶段终止模式代码如下:

/**
 * 测试两阶段终止模式
 * @author ricejson
 */
@Slf4j
public class TestTwoPhaseTerminating {
    public static void main(String[] args) throws InterruptedException {
        Thread monitor = new Thread(() -> {
            Thread thread = Thread.currentThread();
            while (true) {
                if (thread.isInterrupted()) {
                    // 被终止
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.debug("执行系统监控...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 设置中断标记
                    thread.interrupt();
                }
            }
        }, "monitor");
        monitor.start();
        TimeUnit.SECONDS.sleep(3);
        // 中断线程运行
        monitor.interrupt();
    }
}

❗ 注意:该代码存在的问题

使用interrupt方法进行终止的方案,很容易在catch块遗漏interrupt方法(sleep线程被打断会清除中断标记)

2.4.2 可见性优化

我们可以尝试自定义标记变量用来控制监控进程是否继续运行,改造代码如下:

/**
 * 两阶段终止模式优化-可见性
 * @author ricejson
 */
@Slf4j
public class TestTwoPhaseTerminatingV2 {
    private static volatile boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread monitor = new Thread(() -> {
            while (true) {
                if (isStop) {
                    // 被终止
                    isStop = false; // 重置为false
                    break;
                }
            }
        }, "monitor");
        monitor.start();
        TimeUnit.SECONDS.sleep(3);
        // 中断线程运行
        isStop = true;
        monitor.interrupt(); // 如果是sleep则立即唤醒
    }
}

💡 提示:这里一定需要给 isStop 变量使用 volatile 关键字修饰,因为存在可见性问题!

2.4.3 犹豫模式优化

以上代码仍有优化空间——如果在多线程环境下,启动多个线程运行 start 方法就会启动多个监控线程(冗余)因此可以通过一个变量来控制是否启动新的监控线程,改造后的代码如下:

/**
 * 两阶段终止模式优化-犹豫模式
 * @author ricejson
 */
@Slf4j
public class TestTwoPhaseTerminatingV3 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTerminating tpt = new TwoPhaseTerminating();
        tpt.start();
        tpt.start();
        TimeUnit.SECONDS.sleep(3);
        tpt.stop();
    }
}

class TwoPhaseTerminating {
    public static volatile boolean isStop = false;
    public static volatile boolean starting = false;
    public static Thread monitor = null;
    public void start() {
        synchronized (this) {
            if (starting) {
                // 不启动新的
                return;
            }
            starting = true;
        }
        monitor = new Thread(() -> {
            while (true) {
                if (isStop) {
                    // 被终止
                    isStop = false; // 重置为false
                    starting = false;
                    break;
                }
            }
        }, "monitor");
        monitor.start();
    }

    public void stop() {
        // 中断线程运行
        isStop = true;
        monitor.interrupt(); // 如果是sleep则立即唤醒
    }
}

💡 提示:

  1. 这里需要加上 synchronized 保证多个线程代码之间的原子性
  2. 这里一定需要给 starting 变量使用 volatile 关键字修饰,因为存在可见性问题!(synchronized只能保证内部可见性)

3. 有序性

3.1 指令重排序

JVM在不影响代码的运行结果的前提下,有可能会调换代码指令的执行顺序,从而导致了程序运行结果的异常

int i = 10;
int j = 20;

例如上述代码可能会被 JMM 优化为以下代码顺序:

int j = 20;
int i = 10;

3.2 问题演示

使用 maven 工具并添加以下依赖:

<!--
Copyright (c) 2017, Red Hat Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

 * Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

 * Neither the name of Oracle nor the names of its contributors may be used
   to endorse or promote products derived from this software without
   specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.concurrent</groupId>
    <artifactId>jcstress_ordering</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>


    <!--
       This is the demo/sample template build script for building concurrency tests with JCStress.
       Edit as needed.
    -->

    <prerequisites>
        <maven>3.0</maven>
    </prerequisites>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jcstress</groupId>
            <artifactId>jcstress-core</artifactId>
            <version>${jcstress.version}</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!--
            jcstress version to use with this project.
          -->
        <jcstress.version>0.5</jcstress.version>

        <!--
            Java source/target to use for compilation.
          -->
        <javac.target>1.8</javac.target>

        <!--
            Name of the test Uber-JAR to generate.
          -->
        <uberjar.name>jcstress</uberjar.name>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <compilerVersion>${javac.target}</compilerVersion>
                    <source>${javac.target}</source>
                    <target>${javac.target}</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <id>main</id>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>${uberjar.name}</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jcstress.Main</mainClass>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/TestList</resource>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

添加如下类代码(src/main/java/org/example/TestOrdering.java):

package org.example;

import org.openjdk.jcstress.annotations.Actor;
import org.openjdk.jcstress.annotations.Expect;
import org.openjdk.jcstress.annotations.JCStressTest;
import org.openjdk.jcstress.annotations.Outcome;
import org.openjdk.jcstress.annotations.State;
import org.openjdk.jcstress.infra.results.I_Result;


@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class TestOrdering {
    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

使用 maven 相关工具进行打包成 jar 包后运行,效果如下:

可以发现,出现最终结果为 0 的情况为 266 次(极低概率但仍存在),这说明确实存在指令重排序的过程

3.3 解决方案

解决方法也非常简单,同样是依赖于volatile关键字,此时被该关键字修饰的变量之前的代码都不会被优化执行顺序,修改相关代码重新运行结果,效果如下:

4. volatile 原理

volatile 关键字的底层实现是内存屏障技术(Memory Barrier)

  • 对于volatile修饰的变量的写指令后会加入写屏障
  • 对于volatile修饰的变量的读指令之前会加入读屏障

4.1 可见性原理

示例代码如下:

public class TestVisible {
    private volatile static boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!isStop) {

            }
            log.debug("任务运行结束...");
        }, "work").start();
        // 睡眠1s后设置工作线程停止运行
        TimeUnit.SECONDS.sleep(1);
        isStop = true;
        log.debug("主线程退出...");
    }
}

由于对于isStop变量使用了volatile关键字进行修饰,会产生以下效果:

  1. main线程中对于isStop变量产生了写指令,因此会加上写屏障,之前的代码都会强制写入主存中
  2. work线程中对于isStop变量产生了读指令,因此会加上读屏障,之后的代码都会强制从主存中获取

4.2 有序性原理

示例代码如下:

@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class TestOrdering {
    int num = 0;
    volatile boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

于对于ready变量使用了volatile关键字进行修饰,会产生以下效果:

  1. actor2线程中对于ready变量产生了写指令,因此会加上写屏障,之前的代码无法被重排序到该写指令之后
  2. actor1线程中对于ready变量产生了读指令,因此会加上读屏障,之后的代码无法被重排序到该读指令之前

4.3 double-checked-locking 问题

以著名的单例模式的 DCL 问题为例:

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

上述代码具有指令重排序的问题,JVM 层面的INSTANCE = new Singleton();操作会产生如下指令:

  1. new:开辟内存、创建对象
  2. dup:复制引用地址
  3. invokespecial:调用构造方法完成初始化
  4. putstatic:完成引用赋值

因此在多线程环境下,如果发生了指令重排序过程,导致 4 指令在 3 指令之前执行,另一个线程判断 INSTANCE 不为 null 于是直接返回使用了未初始化的对象,会造成非法的结果,解决方案就是给 INSTANCE 变量使用 volatile关键词修饰,此时保证 3 操作一定在 4 操作之前完成

❗ 注意:synchronized能保证原子性、可见性、有序性,但需要注意以下要点

  1. 共享变量被 synchronized 完全包裹时才能保证有序性
  2. 有序性 ≠ 禁止指令重排序

5. happens-before 规则

happens-before 总结了一套规则用于保证共享变量的可见性与有序性,符合该规则的程序能够保证对于共享变量的写操作对于其他线程对该共享变量读操作可见,下面列举部分规则:

  1. 对于使用 volatile 修饰的共享变量的写操作对于其他线程的读操作可见
  2. 线程释放锁之前对于共享变量的写操作对于加锁的其他线程的读操作可见
  3. 线程start之前的写操作对该线程运行后的读操作可见
  4. 线程结束运行之前的写操作,对于其他已知它结束的读可见(比如 join 等操作)
  5. 传递性规则,如果 A 对于 B 可见且 B 对于 C 可见,那么 A 对于 C 可见
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值