Concurrency Programming 七
Java JMM(Java Memory Model, 内存模型)
- JMM定义了主存, 工作内存(抽象概念). 底层对应着 CPU寄存器, 缓存, 硬件内存, CPU指令优化等
JMM体现在以下几个方面
原子性: 保证指令不会受到线程上下文切换的影响
可见性: 保证指令不会受 CPU缓存的影响
有序性: 保证指令不会受 CPU指令并行优化的影响
可见性
- 当一个子线程频繁的从主线程读取共享变量 a时, JIT编译器会将 a的值, 缓存到自己工作内存中, 来提高效率. 此时即使在主线程, 再修改 a的值, 子线程中依然是旧值, 也就是不会同步的(不可见)
- 例子:
public class App {
static boolean a = true; // 共享变量加上 volatile修饰, 则会对所有线程一直是可见的
static int i;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (a) {
i++;
}
System.out.println("end loop!");
});
t1.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("stop!" + i);
a = false;
}
}
* 在以上例子中, 在变量 a声明处, 加上关键字 volatile, 就可以让子线程一直同步 a的变动(可见性), 也就是不进行 a的缓存
volatile(易变关键字):
- 被 volatile修饰的共享变量总是对所有的线程是可见的, 也就是当一个线程修改了一个被 volatile修饰的共享变量的值, 新值总会被其它线程立即得知
- 禁止指令重排序优化
* volatile不能保证原子性, 仅用在一个写线程, 多个读线程的情况
有序性
- JIT编译器在不影响正确性的前提下, 会在运行时的代码做一些优化(也就是调整语句的执行顺序), 这称之为
指令重排
, 此优化在多线程下, 会影响结果的正确性 - 通过
并发压测项目
复现指令重排
引起的异常结果:
# 1. 在项目根目录处, 执行下载, 并加到 Maven Module里
$ mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0
# 2. 以下代码加到压测项目内, 复现异常结果0, 可以指定多个 如: id={"1", "4"}
@JCStressTest
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "unexpected value!")
@State
public class ConcurrencyTest {
int num = 0;
boolean ready = false;
@Actor
public void actor1(I_Result r) { // 线程1执行的代码
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) { // 线程2执行的代码
num = 2;
ready = true; // 此处, 更改顺序上在 ready变量上加 volatile, 则可以将之前的 num变量一起防止指令重排序
}
}
# 3. 将压测项目打包
$ mvn clean install
# 4. 开始压测
$ java -jar ordering\target\jcstress.jar
# 5. 从压测日志中查出结果为0的复现次数
以上压测代码的标准结果为3种:
- 情况1:线程1先执行, 此时 ready=false, 因此 r1结果为 1
- 情况2:线程2先执行 num=2, 但没来得及执行 ready=true, 线程1执行了, 此时结果还是1
- 情况3:线程2执行到 ready=true, 线程1执行了, 此时通过 if条件, 因此结果为4(因为已执行过 num=2)
* 指令重排导致的, 最后一种情况是:
- 情况4: 线程2先执行, 此时指令重排, ready=true和 num=2的顺序被切换, 先执行了 ready=true, 然后切换到线程1, if条件通过, 相加值为0的 num, 最终结果为0
* 解决方法: 将共享变量修饰为 volatile, 这样可以禁用指令重排
volatile原理
volatile的底层实现原理是内存屏障(Memory Barrier/Memory Fence)
- 在加了 volatile的变量, 写指令后会加入写屏障(sfence), 读指令前会加入读屏障(lfence)
- volatile保证了共享变量的可见性和有序性, 但无法保证原子性. 而 synchronized是可以保证原子性
- 如何保证可见性
- 写屏障: 在该屏障之前的共享变量的改动, 都同步到主存中
- 读屏障: 在该屏障之后的共享变量的读取, 都是主存中的最新数据
- 如何保证有序性
- 写屏障会确保指令重排序时, 不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时, 不会将读屏障之后的代码排在读屏障之前
*写屏障仅仅是保证了之后的读能够读到最新的结果, 但不能保证读跑到它前面去. 而有序性的保证也只是保证了当前线程内相关代码的不被重排序. 也就是说无法避免指令交错
原子性
- 原子性指一个操作是不可被中断的. 即使在多个线程一起执行的时候, 也就是一个操作一旦开始, 就不会被其它线程干扰. 主要是通过加锁方式 如 synchronized, ReentrantLock或 CAS来做
*注 synchronized语句, 既可以保证代码块的原子性, 也同时保证可见性. 缺点是 synchronized属于重量级操作, 性能低下
终止模式之两阶段终止模式
- 如何优雅的终止指定线程, 指的优雅意思是, 给被终止后处理后事的机会
- 两阶段终止模式:
- interrupt()& isInterrupted()中断线程方式:
class CCTV {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
System.out.println(current.getName() + ", 被终止后处理后事!");
break;
}
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(current.getName() + ", 将结果保存!");
} catch (InterruptedException e) {
System.out.println(current.getName() + ", 监控被中断!");
current.interrupt();
}
// 执行监控操作
}
},"监控器");
thread.start();
}
public void stop() {
System.out.println(Thread.currentThread().getName() + ", 终止监控!");
thread.interrupt();
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
CCTV t1 = new CCTV();
t1.start();
TimeUnit.MILLISECONDS.sleep(4500);
t1.stop();
}
}
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
main, 终止监控!
监控器, 监控被中断!
监控器, 被终止后处理后事!
- 通过 volatile修饰的共享变量, 作为停止的标记:
class CCTV {
private Thread thread;
private volatile boolean stop = false; // 共享变量 volatile修饰, 保证该变量在多个线程之间的可见性
public void start() {
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
System.out.println(current.getName() + ", 被终止后处理后事!");
break;
}
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(current.getName() + ", 将结果保存!");
} catch (InterruptedException e) {
System.out.println(current.getName() + ", 监控被中断!");
}
// 执行监控操作
}
},"监控器");
thread.start();
}
public void stop() {
System.out.println(Thread.currentThread().getName() + ", 终止监控!");
stop = true;
thread.interrupt();
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
CCTV t1 = new CCTV();
t1.start();
TimeUnit.MILLISECONDS.sleep(4500);
t1.stop();
}
}
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
监控器, 将结果保存!
main, 终止监控!
监控器, 监控被中断!
监控器, 被终止后处理后事!
同步模式之 Balking
- Balking(犹豫)模式: 当点击启动时, 如果某线程已在运行中, 则不能重复启动
# 例子1
class MonitorService {
// 判断是否已有线程在监控着
private volatile boolean running;
public void start() {
synchronized (this) {
if (running) {
return;
}
running = true;
}
// 启动监控线程
}
}
# 例子2
public final class Singleton {
private Singleton() {}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) { // double-checked locking
synchronized (Singleton.class) { // 为缩减锁的范围
if (INSTANCE == null) { // double-checked locking
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!