本文基于jdk8
本文所讲的一些原理都是在多线程中经常使用的内容。
参考:黑马程序员深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili
目录
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时,就不会休眠。