happens-before
一、简介
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见,需要我们自己来控制。
二、案例展示
1、线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见,即 使用 synchronized 保证了可见性。
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
2、线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
3、线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
4、线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join() 等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
5、线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
}, "t2");
t2.start();
new Thread(() -> {
sleep(1);
x = 10;
t2.interrupt();
}, "t1").start();
while (!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
6、对变量默认值(0,false,null)的写,对其它线程对该变量的读可见,并且具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,如下代码:
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start()
三、习题演练
3.1 balking 模式
下面的代码只希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
public class TestVolatile {
volatile boolean initialized = false;
void init() {
if (initialized) {
return;
}
doInit();
initialized = true;
}
private void doInit() {
}
}
存在问题,假设 t1 和 t2 线程同时执行 init() 方法,当执行 if 判断的时候,都是 false,每个线程都会执行一次 doInit() 方法,虽然 initialized 使用了 volatile 修饰,但还是无法解决原子性问题,需要使用 synchronized 来解决。
3.2 线程安全单例习题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全。
3.2.1 饿汉式
饿汉式的特点是类加载就会导致该单实例对象被创建,如下代码:
// 问题1:为什么加 final?
// 回答:防止被子类继承,破坏单例
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例?
// 回答:需要在单例类中加一个返回值类型为 Object 的 readResovle() 方法,方法的返回值为单例对象。
// 在反序列的过程中,一旦发现 readResolve() 方法返回了对象,那么它就会采用你返回的这个对象。
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
// 回答:防止被其他类创建对象,不能防止反射来创建新的实例,因为反射可以获取构造器对象,还可以设置相关的属性,创建新的实例。
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
// 回答:可以保证,是 JVM 帮我们保证线程安全的
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
// 回答:方法可以提供更好的封装性,内部可以实现懒惰的初始化。还可以支持泛型
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
3.2.2 枚举
可以使用枚举类实现单例。如下代码
// 问题1:枚举单例是如何限制实例个数的
// 回答:枚举类里面定义的枚举对象,定义了几个将来就有几个对象,它相当于是枚举类里面的静态成员变量。
// 问题2:枚举单例在创建时是否有并发问题
// 回答:没有,因为静态成员变量是在类加载阶段完成的,不存在并发问题。
// 问题3:枚举单例能否被反射破坏单例
// 回答:不能
// 问题4:枚举单例能否被反序列化破坏单例
// 回答:不能
// 问题5:枚举单例属于懒汉式还是饿汉式
// 回答:由于也是类加载阶段创建的,所以也属于饿汉式的
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
// 回答:加一些构造方法即可
enum Singleton {
INSTANCE;
}
3.2.3 懒汉式
懒汉式的特点是类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建,如下代码:
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 问:分析这里的线程安全, 并说明有什么缺点?
// 回答:是线程安全的,但是锁的范围有点大,每次调用都需要加锁,导致性能很低
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
3.2.4 双重锁检查
如下代码:
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
// 回答:因为 synchronized 里面构造方法的指令和赋值的指令会重排序,加上 volatile 可以防止指令重排序
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
// 回答:这种方式只有第一次调用时会调用同步代码块,后面的调用直接返回了,提高了性能
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
// 回答:是为了防止第一次并发访问时单例对象不要被重复创建
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
3.2.5 静态内部类
如下代码:
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
// 回答:是懒汉式的,只有当用到的时候,才会对 LazyHolder 类进行加载,对里面的静态变量进行初始化
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
// 回答:不会存在线程安全问题。
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
原文地址:https://blog.csdn.net/xhf852963/article/details/140515929
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!