java并发编程(4) JMM + 可见性


前言

这一系列资料基于黑马的视频:java并发编程,目前还没有看完,整体下来这是我看过的最好的并发编程的视频。下面是根据视频做的笔记。在下面的笔记中,还穿插 Java 并发编程这本书的讲解,从4开始,里面涉及的知识点有很多都是《Java并发编程的艺术》这本书涉及到的,所以写的时候每一篇文章的知识点尽量做到少而详细


1. Java内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。关于 JMM 的内存模型,在前面的并发编程中谈到了,也是参考 《Java并发编程的艺术》这本书

JMM 体现在以下几个方面:

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

public class Test32 {
    // 易变
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(true){
                if(!run) {
                    break;
                }
            }
        });
        t.start();

        sleep.mySleep(1);
        run = false; // 线程t不会如预想的停下来
    }
}

分析一下为什么:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
    在这里插入图片描述
  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
    在这里插入图片描述
  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述
  4. 总结一下,其实最重要的原因就是因为主存和高速缓存中的数据不同步造成的

解决方法:使用 volatile(易变关键字)



2. 可见性

1. volatile 初体验

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

关键字 volatile 告知程序任何对变量的访问都需要从主存中获取到,而不是从缓存中获取。而对于变量的改变则必须同步刷新回共享内存,它能保证所有线程对该变量的可见性。

举个例子:上面的那段代码中把 boolean 变量定义成 volatile 之后,其他的线程会感知到这个变量的变化,因为这个变量被修改之后会刷新回主存,那么此时其他线程再读取的时候就会读到最新的值的。当然,对于指令重排序这些的影响后面再谈。

但是,过多使用 volatile 关键字也是不好的,会减低程序执行的效率。



2.可见性 vs 原子性

1. 什么是原子性

一个或者多个操作,执行的时候要么完全执行,要么就不执行。也就是说执行的过程中不可被打断。比如因为CPU时间片带来的线程上下文切换这些因素。



2. 可见性 vs 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况。

上面的代码只能保证同步写回,但是不能保证在执行的过程中还是会出现一些被打断的意外。还是CPU时间片这种,是避免不了的。

对于上面的代码,从字节码角度理解:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 

getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 

iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

但是无法保证原子性,就是说对于上面这类的操作,线程安全还是无法避免,还是需要锁或者原子类来实现。




注意:

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。
  • 但缺点是synchronized 是属于重量级操作,性能相对更低

最后提出一个问题: 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

public class Test32 {
    // 易变
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(true){
                System.out.println();
                if(!run) {
                    break;
                }
            }
        });
        t.start();

        sleep.mySleep(1);
        run = false; // 线程t不会如预想的停下来
    }
}

答:我们查看 System.out.println() 的源码,会发现其实内部是用了 Synchronized 来保证可见性的
在这里插入图片描述


3. 两阶段终止-volitatile

使用 volitatile 关键字优雅的中断一个线程的操作,我们使用 volitatile 设置一个标记位 stop ,当 stop 设置为 true 的时候,其他线程可以同步收到 stop 的结果,然后停止。使用 monitorThread.interrupt(); 是为了防止停止后再输出一句 log.debug("执行监控记录"),因为此时线程在 sleep,而主线程已经调用 stop 了,如果不打断,至少还会向下运行完这个语句再 break

@Slf4j
public class Test13 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();

        Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();
    }
}

@Slf4j
class TwoPhaseTermination {
    // 监控线程
    private Thread monitorThread;
    // 停止标记
    private volatile boolean stop = false;
    // 判断是否执行过 start 方法
    private boolean starting = false;

    // 启动监控线程
    public void start() {
        //用锁防止多线程问题
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
        }
        monitorThread = new Thread(() -> {
            while (true) {
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        //interrupt是为了不多输出一句执行监控记录
        monitorThread.interrupt();
    }
}
DEBUG [monitor] (23:55:48,964) (Test13.java:54) - 执行监控记录
DEBUG [monitor] (23:55:49,977) (Test13.java:54) - 执行监控记录
DEBUG [monitor] (23:55:50,982) (Test13.java:54) - 执行监控记录
DEBUG [main] (23:55:51,471) (Test13.java:22) - 停止监控
DEBUG [monitor] (23:55:51,471) (Test13.java:49) - 料理后事



4. 同步模式之balking

当一个线程发现有其他线程在做同一件事的时候,此时线程无需再执行相同的操作。其实代码和上面的相同了。输出结果也是一样的。

关键代码:使用 synchronized 来保证单个线程执行,一个线程执行后,另外的线程只要进入就直接 return;

 synchronized (this) {
	 if (starting) { // false
          return;
      }
      starting = true;
  }
@Slf4j
public class Test13 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        tpt.start();
        tpt.start();


        Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();
    }
}

@Slf4j
class TwoPhaseTermination {
    // 监控线程
    private Thread monitorThread;
    // 停止标记
    private volatile boolean stop = false;
    // 判断是否执行过 start 方法
    private boolean starting = false;

    // 启动监控线程
    public void start() {
        //用锁防止多线程问题
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
        }
        monitorThread = new Thread(() -> {
            while (true) {
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        //interrupt是为了不多输出一句执行监控记录
        monitorThread.interrupt();
    }
}





如有错误,欢迎指出!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值