4.共享模型之内存
-
Java内存模型JMM(Java Memory Model),它定义了主存、工作内存等抽象概念。底层对应着cpu寄存器、缓存、硬件内存、cpu指令优化等。
JMM体现在:原子性:保证指令不会受到线程上下文切换的影响。
可见性:保证指令不会受到cpu缓存的影响
有序性:保证指令不会受cpu执行并行优化的影响 -
可见性问题原因:1.初始状态,线程刚开始从主内存读取了变量的值到工作内存 2.因为线程要频繁从主内存中读取变量的值,JIT编译期会将变量的值缓存到自己工作内存的高速缓存中,减少对主存中变量的访问,提交效率
3.当别的线程修改了变量的值并同步到主存后,该线程仍然是从高速缓存中读取值,所以永远是旧值。
解决方法:volatile(易变关键字),它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,而必须到主存中,线程操作volatile变量都是直接操作主存。
PS:给对象加锁也可以,但是要创建Monitor锁,比较重量级,synchronized既可以保证可见性,也可以原子性
volatile只能保证看到最新值(可见性),不能解决指令交错(原子性) -
多线程下指令重排会影响正确性,即调整语句的执行顺序。
现代cpu支持多级指令流水线,例如同时支持取指令-指令译码-执行指令-内存访问-数据写回的处理器,就可以称之为五级指令流水线,这时cpu可以在一个时钟周期内,同时运行五条指令的不同阶段。本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令的吞吐率。 -
volatile的底层实现原理是内存屏障。保证可见性:写指令后会加入写屏障,读指令前会加入读屏障。
写屏障保证在该屏障之前的操作,对共享变量的改动,都同步到主存当中。
读屏障保证在该屏障之后的操作,对共享变量的读取,加载的是主存中的最新数据
保证有序性:写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
但不能保证原子性:不能解决指令交错,有序性只是保证了本线程内相关代码不被重排序,但多线程之间的原子 性无法保证
更底层是读写变量时使用lock指令来保证多核cpu之间的可见性和有序性 -
happens-before:规定对共享变量的写操作对其他线程的读操作可见,它是可见性和有序性的一套规则总结。
m代表锁,x代表变量
1.线程解锁m之前对x的写,对接下来对m加锁的其他线程对x的读可见、
2.线程对volatile变量的写,对接下来其他线程对x的读可见
3.线程start之前对x的写,对该线程开始后对x的值的读可见
4.线程结束前对x的写,对其他线程得知它结束后的读可见(其他线程调用isAlive()或join()等方法等它结束)
5.线程t1打断t2前对x的写,对于其他线程得知t2被打断后对x的读可见
6.对变量默认值(0,false,null)的写,对其他线程对x的读可见
7.具有传递性,如果x hb->y并且y hb-> ,那么x->z -
线程安全单例习题:
1.为什么加final:防止子类不适当地覆盖了父类中的一些方法,破坏了单例
2.如果实现了序列化接口,还要做什么来防止反序列化破坏单例?
反序列化也可以生产对象,跟单例模式生成的对象不是同一个,这时可以在类中加入以下方法返回单列对象
public Object readResolve(){
return INSTANCE;
}
3.为什么设置为private,能否防止反射创建新的实例? 防止其他的类创建创建对象。
不能,因为反射可以得到构造器对象,可以设置构造器对象的setAccessible,暴力调用构造方法创建实例
4.private static final Singleton INSTANCE = new Singleton();这样初始化能否保住线程安全?
可以,静态成员变量是在类加载的时候由jvm创建
5.为什么提供静态方法,而不是直接将INSTANCE设置为public?
可以提高对象的封装性,内部实现一些懒惰的初始化,创建单例对象时有更多的控制,提供泛型的支持 -
枚举单例:
1.枚举单例是如何限制实例个数的?
枚举类里定义的枚举对象定义时有几个将来就有几个,相当于是静态成员变量,是单实例的
2.枚举单例在创建时是否有并发问题 没有,也是在类加载的时候完成的
3.枚举单例能否被反射破坏单例? 不能
4.枚举单例能否被反序列化破坏单例? 枚举类默认实现了序列化接口,但系统已经做了处理
6.枚举单例属于懒汉式还是饿汉式? 饿汉式
7.枚举单例如果希望加入一些单例创建时的逻辑化接口该怎么做? 写一个构造方法