面试题中编写单例:
public class Lazy implements Serializable{
/**
* 设置默认版本号
*/
private static final long serialVersionUID = 1L;
//volative禁止指令重排,创建出多个对象-------->涉及到原子性操作问题
private static volatile Lazy lazy=null;
//设置标志位,如果对象已经被创建就修改标志位,在一定程度上防止反射创建对象
private static boolean flag=false;
//定义私有的构造方法
private Lazy(){
//单例防反射,此处还是存在问题的,对象在没有创建出来之前,还是可以通过反射创建对象
//--------->涉及到反射的知识
if(flag==false){
flag=true;
}else{
throw new RuntimeException("单例模式被破坏");
}
}
//供外获取对象
public static Lazy getInstance(){
//进行双重检查,提高效率
if(lazy==null){
//防止并发,使用同步代码块---------->涉及到并发问题,同步问题
synchronized(Lazy.class) {
if(lazy==null){
lazy=new Lazy();
}
}
}
return lazy;
}
//防序列化-------->涉及到序列化问题
public Object readResolve(){
return lazy;
}
}
此单例模式的优点:可以延迟加载,线程安全,可以防序列化,在一定程度上防反序列化。
如果面试中的单例模式可以引出很多的话题:
1、volative---->聊到java的内存模型---->聊到gc
2、反射
3、线程(同步问题,高并发引发的问题)
4、单例模式防反序列化
此处我们只讨论volatile和防反序列化
volative:
参考文章:https://www.cnblogs.com/chengxiao/p/6528109.html
我们先来看一个小案例:
public class Test01 {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
//思考:主线程在此处对flag修改会终止while循环吗?
t.flag=false;
}
}
class MyThread extends Thread{
boolean flag=true;
@Override
public void run() {
System.out.println("run-----start-----");
//如果循环条件为true则System.out.println("run-----end-----");会编译不通过的
//而flag是变量,编译期是检查不到的,所以不会报错
while(flag){
}
System.out.println("run-----end-----");
}
}
分析代码问题:
思考:主线程中将flag设置为false并不能终止while循环,为什么呢?
这里我们需要简单的了解一下Java Memory Model(JMM)
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,
以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
现在我们来解释一下以上代码:
主线程将主内存中的flag拷贝一份到自己的本地内存中,对它进行了修改。在它写回到主存之前线程b读到的flag是false,它对与主线程的修改是不可见的。
那么什么是可见性呢?
所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。
现在我们产生了新的问题:这样的问题怎么进行解决呢?
1、我们尝试着在while循环中加入一条语句:System.out.println("hello");
运行发现:while循环进行了终止。这个应该怎么解释呢?
解释:当while是空循环时,线程一直在执行,当在while循环中插入输出语句时,线程会在某个时间去到主存,查看flag的值,所以终止了循环。
2、第二种解决方案就是在flag变量加上volative。那么volative是什么意思呢?
解释:volative是java中提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。
同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,
倘若能恰当的合理的使用volatile,那就再好不过。
拓展:
volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。
将一个共享变量声明为volatile后,会有以下效应:
1、当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2、这个写操作会导致其他线程中的缓存无效。
以上讲述的是volatile的第一个特性:即设置变量可见。
我们回到我们的单例模式,我们的单例模式中的volatile的作用是什么呢?
分析:
单例模式中使用到的volatile特性是:禁止指令重排。
指令重排是什么意思呢?
解释:重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。
就是计算机为了为了提高执行效率会做一些优化,在不影响结果的情况下,可能会对一些语句进行调整。比如:
1、int a;
2、a=8;
3、int b=9;
4、int c=a+b;
正常来说,对于顺序结构,执行顺序是自上而下的,也即1234,但是可能由于指令重排的原因可能会变成3124或者1324。
————由于语句3、4不是原子操作,所以3、4也可能会被拆分成原子操作在进行重排————也就是说,对于非原子操作会在不影响最终的结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
拓展:
重排序遵循的规则:
1. 重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2. 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,
但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
我们说的重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,因为可能存在重排问题,
那么在高并发的情况下,单例模式不加volatile可能存在创建多个对象的问题。
对于volatile总结:
volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。
防反序列化
思考:为什么我们需要防反序列化?
解释: 一个类实现了 Serializable接口, 我们就可以把它往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象。注意,这里是重新new出来一个对象,只是这个对象与原来的对象一模一样而已,但是并不是原来的那个对象。当序列化遇到单例时,这里边就有了个问题: 从内存读出而组装的对象破坏了单例的规则。单例是要求一个JVM中只有一个类对象的, 而现在通过反序列化,一个新的对象克隆了出来。
那么为什么我们可以通过以下的方法就可以防止反序列化创建新的对象呢?
public Object readResolve(){
return lazy;
}
我们可以通过查看源码去查找答案,原码比较复杂,大致就是,ObjectInputStream类的readOrdinaryObject方法在调用readSerialData()方法后,就调用了 ObjectStreamClass类的Object invokeReadResolve(Object obj)方法,通过反射调用了我们自己写的readResolve方法。
此方法防止反序列化获取多个对象。
无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
注:本人处于学习java的初级阶段,对于知识的细节也是点到为止,没有深究,有兴趣的可以去挖掘的更深一点