【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修饰的变量的代码是按照顺序来执行的。

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

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

双重检查锁应用

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变量

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()方法,源码如下

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时,就不会休眠。
  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值