【JavaEE】volatile + final + wait-notify + join + park-unpark 相关原理

本文基于jdk8

本文所讲的一些原理都是在多线程中经常使用的内容。

参考:黑马程序员深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili


目录

volatile原理

Java内存模型(JMM)

可见性&有序性

双重检查锁应用

final原理

设置final变量

读取final变量

wait-notify原理

wait与sleep的区别

join原理

park-unpark原理

park与wait的区别


volatile原理

volatile是在多线程情况下一个常见的关键字,其作用的防止指令重排序和保证变量的可见性。要想了解其底层原理,要先知道Java内存模型相关的一些概念。


Java内存模型(JMM)

这里只是大概讲一下Java内存模型。

JMM决定一个线程共享变量 的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

可以大概这么理解


可见性&有序性

使用了volatile关键字后,某个线程对于共享变量的修改,其他线程可以立马知道最新修改后的值。

某个线程执行一些被volatile修饰的变量的代码是按照顺序来执行的。

这是用到了内存屏障 。内存屏障由读屏障写屏障构成。

  • 可见性
    • 写屏障保证在该屏障之前 对共享变量的改动都会同步到主存中
    • 读屏障保证在该屏障之后 对共享变量的读取都是加载主存中最新的数据
  • 有序性
    • 写屏障保证指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障保证指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 总结就是写指令之后加入写屏障,读指令之前加入读屏障

双重检查锁应用

java 复制代码
public final class Singleton {
    private Singleton() {
    }

    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) { // t1
                    // t1是最开始调用这个方法的,并且是第一个执行到这里的线程
                    // 下面这个赋值的语句可能会变成 先 INSTANCE=null,然后再执行构造方法 这样会造成 INSTANCE还是null的
                    // 加了volatile后就能保证 构造方法已经执行完毕了
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

可见性:这里的 INSTANCE 实例可能某个线程已经创建了,但是其他线程还不知道创建完成了。所以加了volatile后,后续其他线程访问导的都是最新数据。

指令重排序:就是上面的代码注释。


final原理

设置final变量

java 复制代码
public class TestFinal {
    int a = 20;
}

上面的变量a如果没有用final修饰,其步骤分成初始化a变量,此时a为0,然后在赋值20.

在多线程的情况下,有可能刚给a初始化完成,就会有其他线程来读取a的值,它就会把 0 读走。

有final修饰的话,它在写的时候会加入写屏障,从而让其他线程读不到0

读取final变量

没有final修饰的变量,只能从共享内存读,从堆中读数据效率低
有final修饰的变量

  • 较小的值直接复制(没有超过每种类型所表示的最大值)
  • 如果超过最大值,则放到该类的常量池,然后读取

可以知道,不用final修饰读共享变量时效率并不高。


wait-notify原理

当一个对象调用wait方法时,那么这个线程就进入了阻塞,具体原因还是和对象头Mark Word某部分将会指向Monitor,从而变成重量级锁。

当然上面的wait方法如果有等待时间的话,超过时间后也会进入到EntryList中。

wait与sleep的区别

wait和sleep最本质的区别就是 线程中的对象调用wait方法后,该线程会进入WaitSet中,此时其他线程可以对 该对象进行加锁;线程调用sleep方法后,它只是等待设定的时间,此时线程不会释放自己所持有的锁。


join原理

join是Thread类中的一个方法,如果线程A中调用了 线程B的join方法,那么线程A只能等到 线程B执行完成后再往后继续执行。其实join的底层原理就是调用了 wait()方法,源码如下

java 复制代码
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

这里体现出保护性暂停模式,后续会介绍。


park-unpark原理

park和unpark是LockSupport中的方法。LockSupport.part() 调用后就是暂停当前线程(不会释放锁资源),LockSupport.unpark(暂停线程的对象) 调用后让暂停的线程继续执行。

其底层由 "二元信号量来控制",可以理解成 unpaik 时这个信号量就会加一,但不会超过一,park时就会减一,但不会超过零。

park与wait的区别

  • wait后会释放锁资源,但是park不会
  • wait唤醒的时候是随机的,park是精确唤醒
  • 如果在没有wait对象直接notify,就会抛出异常;如果没有park,先unpark,那么后续在执行到park时,就不会休眠。
相关推荐
reiraoy几秒前
缓存解决方案
java
安之若素^15 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9921 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc1 小时前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁