java 多线程编程 三大特性

可见性

volatile

使用

private static /*volatile*/ Boolean b = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(
                () -> {
                    System.out.println("start");
                    while (b) {
                    }
                    System.out.println("end");

                }
        ).start();
        TimeUnit.SECONDS.sleep(1);
        b = false;
    }
  • 如上b 设置为false。子线程里是无法感知的。
  • 原因是每个线程启动时都会把变量放到当前线程的缓存中,每次读取的时候拿到的都是之前存起来的true,所以这个线程不会结束。
  • 解决方案: 如果想让变量在其他线程可见需要放开代码中的注释volatile就可以保持线程的可见性。
  • 原因是volatile 所修饰的变量,所有线程都会去主线程中读,子线程自然可见。

除了volatile以外的解决方案

private static /*volatile*/ Boolean b = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(
                () -> {
                    System.out.println("start");
                    while (b) {
                        System.out.println("running");
                    }
                    System.out.println("end");
                }
        ).start();
        TimeUnit.SECONDS.sleep(1);
        b = false;
    }
  • 如果这么写线程依然会被停止。
  • 原因是部分操作会触发,子线程自动同步主线程中的内存如System.out.println(“”);中存在synchronized,该语法就会触发。
  • 但是不推荐这么使用,因为sync 等于加了把🔒,性能肯定会变差所以还是推荐使用volatile。
    image-1650811379202

当volatile修饰引用类型时,当你修改引用内部的参数时,那个参数不具有可见性,如果需要让内部参数具有可见性,应该在那个参数上加volatile修饰。

三级缓存

介绍

image-1650811863191

  • 这是计算机的三级缓存
    image-1650811941376
  • 如图L1 在核的内部 L2也在核内部,而L3 在cpu的内部
  • 当读取数据时是按照如下顺序读取的,1层1层读取。

缓存行

  • 读取一个数据的时候会,访问其相邻的数据,比如一个数组,你读取了第一个元素往往会读取他23456的数据,所以我们把这一整块数据,同时读取到内存中,我们把这一整块称作缓存行(cache line),一行最多64个字节。
  • 读取数据不会一个一个读,而是一行一行读。
  • 当两个线程修改同一个缓存行的数据的时候,效率反而更低,原因是两个线程之间的数据,需要互相通知另一个线程数据发生了变化,反而会互相干扰。
  • 解决方法是,如果你想规避这个问题,在两个线程读取的两个不同的内存左右追加一些填充字节,如数据为Long 类型,则在其左右两边填充 56个字节,让其占满64个字节。

上面的方案有些难以使用

  • 在jdk1.8 提供了注解 @Contended,被这个注解标注的数据,不会与其他数据处于同一个缓存行。
  • 使用@Contended 需要在jvm环境变量里添加 -XX:-RestrictContended标注,因为jvm 默认是关闭@Contended的。
  • 不建议使用@Contended,没必要如此骨灰级操作。

有序性

  • 在多线程环境下,代码并非一定有序进行如
a = 10;
b = 5;
  • b = 5 可能会在 a = 10之前执行。
  • 只是因为java 底层优化,当两个互相没有依赖关系的指令被写出时,有可能出现乱序(概率非常低)
  • 指令并不一定是顺序执行的,对于汇编来说规则是:他不影响单线程的最终一致性(在多线程环境可能会发生bug)
    image-1650891524165
  • 上述的代码存在两个问题
  1. ready 需要被volite修饰
  2. ready=true 有几率先于number=42之前被执行。

对象的半初始化状态

image-1650891937959

  • 当初始化对象时分别有5条指令
  1. 第一行为t对象申请内存
  2. 第三行调用t的init 方法也就是构造方法
  3. 第四行和第一步申请的内存进行关联

this 溢出问题

image-1650892698397

  • 上述问题为this 中间状态溢出的问题
  • 如上图num有可能输出为0或者8(当然大多数情况肯定是8)
  • 因为指令重排序的问题,指令的顺序有可能会被颠倒,并且在创建对象的时候new 了一个线程,很有可能在线程里num 还是他的初始化状态也就是int 的默认值0.
  • 所以不要在构造器里启动线程,创建线程是没有问题的。

解决重排序问题

  1. 在两条指令之间添加内存屏障防止两条指令有序。(cpu底层指令,jvm没有使用cpu底层指令)
  2. jvm的内存屏障,如下图
    image-1650893982626
  3. volatile 解决禁止指令重排序,volatile其实也是通过jvm 内存屏障实现的,在对volatile 进行读写操作时 会对其添加内存屏障

原子性

概念

  • 指的是一个命令不能并发执行(不能被其他线程打断)如下图
    image-1650954084464
  • 需要注意的是在java里就算是一条语句如n++,也不会内置保持其原子性,因为其翻译为字节码会是好几条指令。

锁是如何实现的,本质是什么

  • 上锁的本质是把原本的并发操作改为了序列化操作

如何实现原子性

  • 上锁(上锁以后效率肯定会变低)

锁的具体研究

移步 聊聊锁