woieha320r的博客

线程&锁

指令重排序

· CPU为了保证流水线的效率,会在不影响最终结果的情况下将指令重排序,JIT也有类似机制。比如第二条指令依赖第一条指令的值,但第二条指令开始执行时 第一条指令还未出结果,CPU会从后续指令挑选若干执行顺序不影响最终结果的指令插到一二条指令之间以保证流水线无需等待

· 但如果此时还有另外一个线程依赖第二条指令执行的时机,那么这个重排序机制会破坏原本的代码逻辑,因为第二条指令的执行时机在重排序下是不可控的

jvm的线程内存模型

· 和堆栈内存模型是两种层面的东西,工作内存是当前线程的栈帧(线程独享),主内存是存“线程共享数据”的地方(实例字段、类字段、数组元素等位于 堆/方法区的数据)。线程之间共享数据只能通过主内存进行。

线程模型

volatile

· volatile保证可见性(写操作后立即将结果更新到主内存,其他线程强制放弃工作内存而从主内存取新值)、有序性(禁止CPU发生指令重排序)

· volatile并不对自身运算保证原子性,比如四个线程执行被volatile修饰的x++,Java里单纯的读写都是原子的,但在自增结果值写到x之前,其他线程已经 取走了x的旧值去进行自己的自增,虽然确实执行了四次,但最后的结果会小于4

· volatile的另一个语义是阻止指令重排序,JIT会在生成的机器指令中促使CPU不要发生重排序

· 这两个特性使得volatile的应用场景类似于如下所示(shutdown和dowork是俩线程)

volatile

synchronized

· synchronized对于持有同锁的其他线程保证原子性(要么不执行要么不中断)、可见性(锁释放前将结果更新到主内存)

· jvm遇到方法调用时会查看class中的方法表,检查其是否被声明为synchronized,如果是,那么当前线程尝试获得class/obj的所有权(加锁), class/obj取决于方法是否static,获得后才执行方法指令

· 对于局部级别的synchronized,编译器会生成monitorenter指令尝试获得栈顶元素的所有权(加锁),栈顶元素也就是synchronized的参数。 每个monitorenter都必须搭配一个monitorenter来释放锁,这俩指令中间的指令就是被synchronize包裹的内容。

· 它在持有锁的同时可再次持有,也就是可重入,否则会产生死锁。

双检锁式懒汉单例

双检索单例

· new包含“创建空间”、“初始化”、“地址赋值”,如果“地址赋值”因为发生指令重排序而被提前到“初始化”之前,其他线程就可以直接通过引用操作数据了,但此时 数据还未初始化

· 第一次if与锁无关,而synchronized保证的是在释放锁前的任意时机将结果同步到主内存,是为了保证同锁线程的可见性,也只能保证同锁线程的可见性。对于 不持有同锁的线程,可以在锁释放前就看到实例对象已非null,但它实际上却还未被初始化,所以需要将字段声明为volatile来阻止指令重排序的发生

· 至于为啥要双检,见设计模式-单例篇(https://woieha320r.github.io/设计模式/单例)

通过为x赋值三次模拟三条指令,用线程断点调试观察到,当thread-1将x赋值1后,thread-2的首次if为false

public class TestSynchronizedSeen {
    private static Integer x;

    public static void getInstance() {
        if (null == x) {
            synchronized (TestSynchronizedSeen.class) {
                if (null == x) {
                    x = 1;
                    x = 2;
                    x = 3;
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> TestSynchronizedSeen.getInstance(), "thread-1").start();
        new Thread(() -> TestSynchronizedSeen.getInstance(), "thread-2").start();
    }
}

java.util.concurrent.locks.lock

· 另一种互斥锁实现。以ReentrantLock重入锁为例,相较synchronized多了仨功能:等待可中断(正在等待的线程可选择放弃等待)、公平锁(按申请顺序 获得锁,性能下降)、锁绑定多个条件

· 使用它需要编程者决定何时加锁放锁,一般在finally中释放锁,jvm不会像自动处理synchronized一样处理Lock接口的实现

原子类

· java.util.concurrent.atomic这个包下提供了一些原子类,它们的方法保证原子性

判断线程安全

· 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》12.3.6先行发生原则

线程安全的类

· 对于Java中声称线程安全的类,它表示对对象的单次操作是安全的,如果不同线程分别连续调用不同方法,仍需仔细考虑同步问题。比如如下例子虽然remove和 get都线程安全但如果不对两个for添加以vector为锁的synchronized会导致越界异常

示例

异常处理

· java.lang.Runable.run()不能抛异常,只能在内部捕获。对于非受查异常可以在Thread对象start前调用其setUncaughtExceptionHandler方法 来设置自定义异常处理。

jvm的线程实现

· jvm对线程的实现方式,不影响编程,对编程者是透明的

· jvm规范并未规定线程实现方式,一般有三种:内核线程、用户线程、用户线程+轻量级进程混合。主流的HotSpot采用1:1的内核线程,线程调度由操作系统 全权负责,所以在Java中设置线程优先级仅仅是对操作系统的建议,不一定有效

jvm对锁的优化

这些东西不影响编程,只是jvm对锁实现的优化手段,对编程者是透明的

  • 自旋锁:线程获取不到锁的时候不挂起而是循环等待

  • 锁消除:jvm会判断加锁的地方是否真的会发生线程争夺,没有的话jit不会生成加锁的指令

  • 锁膨胀:把循环中的锁扩大到循环外

  • 轻量级锁、偏向锁:没明白🤷‍♂️