java 多线程编程 三大特性
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。
当volatile修饰引用类型时,当你修改引用内部的参数时,那个参数不具有可见性,如果需要让内部参数具有可见性,应该在那个参数上加volatile修饰。
三级缓存
介绍
- 这是计算机的三级缓存
- 如图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)
- 上述的代码存在两个问题
- ready 需要被volite修饰
- ready=true 有几率先于number=42之前被执行。
对象的半初始化状态
- 当初始化对象时分别有5条指令
- 第一行为t对象申请内存
- 第三行调用t的init 方法也就是构造方法
- 第四行和第一步申请的内存进行关联
this 溢出问题
- 上述问题为this 中间状态溢出的问题
- 如上图num有可能输出为0或者8(当然大多数情况肯定是8)
- 因为指令重排序的问题,指令的顺序有可能会被颠倒,并且在创建对象的时候new 了一个线程,很有可能在线程里num 还是他的初始化状态也就是int 的默认值0.
- 所以不要在构造器里启动线程,创建线程是没有问题的。
解决重排序问题
- 在两条指令之间添加内存屏障防止两条指令有序。(cpu底层指令,jvm没有使用cpu底层指令)
- jvm的内存屏障,如下图
- volatile 解决禁止指令重排序,volatile其实也是通过jvm 内存屏障实现的,在对volatile 进行读写操作时 会对其添加内存屏障
原子性
概念
- 指的是一个命令不能并发执行(不能被其他线程打断)如下图
- 需要注意的是在java里就算是一条语句如n++,也不会内置保持其原子性,因为其翻译为字节码会是好几条指令。
锁是如何实现的,本质是什么
- 上锁的本质是把原本的并发操作改为了序列化操作
如何实现原子性
- 上锁(上锁以后效率肯定会变低)
锁的具体研究
移步 聊聊锁