卧槽,小小的单例模式竟然有这么多种写法?,java后端面试宝典
一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、
-
内部持有一个私有的静态单例模式的实例。
-
提供一个公共的静态方法用于获取单例对象。
单例模式的几种实现方式
1、饿汉式单例
实现
class Singleton1
{
private static Singleton1 instance=new Singleton1();
private Singleton1() {};
public static Singleton1 getInstance()
{
return instance;
}
}
优缺点
这种实现方式的优点实现简单,可以看到仅需几行代码就可以实现,该方式可以在多线程下很好的工作,保证线程安全。
缺点就是单例类一加载对象就被创建,没有延迟初始化,这在创建实例比较耗费资源时可能造成浪费资源启动变慢。
思考
饿汉式单例设计模式是如何保证线程安全的呢?
这个问题就需要归功于java类加载机制了,在文章的末尾给出了这个问题解释。
2、饿汉式变种
实现
class Singleton2
{
private static Singleton2 instance=null;
static
{
instance=new Singleton2();
}
private Singleton2(){};
public static Singleton2 getInstance()
{
return instance;
}
}
这种方式与上面的饿汉式单例设计模式几乎没有什么区别,同样可以保证线程安全。
3、懒汉式单例设计
class Singleton3
{
private static Singleton3 instance=null;
private Singleton3(){};
public Singleton3 getInstance()
{
//1
if(instance==null)
{
//2
instance=new Singleton3();
}
return instance;
}
}
优缺点
这种方式实现起来也比较简单,代码量比较好,而且这种方式使用延迟初始化,可以避免资源的浪费。
但是这种方式只能在单线程下工作的很好,在多个线程并发执行时并不能保证单例。
思考
这种方式为什么在多个线程并发执行的时候不能保证单例呢?
来看下面这张图:
当线程A执行getInstance方法是判断instance为null,进入到if中,刚准备创建对象,这是cpu被线程B抢去了,这是线程B执行getInstance方法判断instance也为null。然后线程A重新抢到了cpu,由于前面已经判断过,所以将会创建单例对象,而同样等到线程B拿到cpu也会创建线程对象。这样就创建出了两个对象。
4、懒汉式单例(线程安全1)
实现
class Singleton4
{
private static Singleton4 instance=null;
private Singleton4(){};
public synchronized Singleton4 getInstance()
{
if(instance==null)
{
instance=new Singleton4();
}
return instance;
}
}
优缺点
这种方式在解决了上面所说的线程安全的问题,并没有引入编码上太大的复杂度,使用synchronized保证方法的同步。
缺点就是这种方式在并发度比较高的情况下性能并不好,即便后续单例已经被创建了,每次获取单例对象都要经历加锁解锁。
补充
随着java对synchronized的不断优化,锁升级等是的synchronized的性能损耗没有那么严重了,但是这种方式仍然是简洁但不优美。
5、懒汉式单例(双重校验锁)
实现
class Singleton5
{
private static volatile Singleton5 instance=null;
private Singleton5(){};
public Singleton5 getInstance()
{
if(instance==null)
{
synchronized (Singleton5.class)
{
if(instance==null)
{
instance=new Singleton5();
}
}
}
return instance;
}
}
优缺点
这种方式针对第四种写法进行了改进,因为当单例对象被创建后就不需要再对其进行加锁同步,只需要保证单例对象的创建过程的线程安全性即可,这种优化方式属于细化锁的粒度,只在该加锁的地方加锁。
缺点就是代码量上去了,需要synchronized和volatile保证,理解上有一定难度。
思考
1、为什么需要两次if判断,判断为null之后进入同步代码块直接创建对象不就可以,一次可不可以?下面再来看张图:
因为当多个线程一次执行到synchronized代码块时已经执行完成了if检查,那么当线程获取到锁的时候不会再检查,即使其他线程已经创建了对象。所以当线程拿到锁之后需要再次检查一下在阻塞期间其他线程是否已经完成单例对象的初始化。
2、为什么需要volatile修饰单例对象,不用可不可以。
这里就要对这段代码分析一下
if(instance==null)
{
instance=new Singleton5();
}
jvm创建对象大体可以分为以下三步:
-
首先根据类的全限定名查找类的符合因为,判断该类是否已经被加载、验证、准备、解析过如果没有,执行上述步骤,然后为其创建一个对应的Class对象。
-
在确保了类被加载之后,在堆内存中开辟一块内存,执行类的初始化。
-
在栈中创建一个引用,指向堆内存中开辟的这一块内存。
正常情况下是这样的,然后我们说只要有正常情况就会有不正常情况,不正常情况就是,jvm为了实际执行时会根据性能、指令流等将指令进行重排序执行。经过重排序后那么3就有可能在2的前面,也就是有可能出现1->3->2的情况,这样在多线程的情况下,线程一块栈中已经有该类实例的引用,即认为对象已经创建完成,那么在执行上述if判断的时候就会返回false从而直接返回对象实例,但是该实例其实还并为完全初始化,这样会为外界提前暴露一个尚未完全初始化的单例对象实例,这种情况是比较危险的。
那么用volatile就管用了吗?是的volatile的一个作用就是可以防止指令的重排序,volatile通过插入内存屏障来防止后续的指令重排序到前面,从而可以确保jvm实际执行的时候就是按照1->2->3的顺序执行的,这样就没问题了。
6、枚举
实现
enum Singleton6
{
INSTANCE;
public void doSomething()
{
System.out.println(“doSomething…”);
}
//该方法可不需要,直接通过Singleton6.INSTANCE也可。
public static Singleton6 getInstance()
{
return INSTANCE;
}
}
优缺点
枚举实现单例的方式是在《Effective java》中推荐的一种实现方式,是因为该方式实现起来十分的简单,而且可以保证多线程下的线程安全性。同时,对于一些可能破坏序列化的手段,这个我们后面会讨论,该方式也可以防止。
缺点:要说非要找缺点就是尽管这种实现方式看起来比较完美,但是实际应用的并不是很多,就是讨论的比较热烈,但是应用的不太广泛。
(搜索公众号Java知音,回复“2021”,送你一份Java面试题宝典)
思考
1、枚举是如何实现线程安全的?
我们对上面的代码反编译后看看枚举背后到底有什么秘密。
从这里我们看出,我们的INSTANCE被加上了static final修饰,这样当类被加载的时候,JVM就保证了其线程安全性。
2、枚举是如何防止被破坏的呢?
首选剧透一下,破坏单例模式有两种方法,分别是序列化和反射。那么先看枚举是如何防止序列化破坏单例模式的。
我们看一看普通的序列化需要有哪些操作?
public static void main(String[] args) throws IOException, ClassNotFoundException
{
FileOutputStream fout = new FileOutputStream(“Singleton.obj”);
ObjectOutputStream out=new ObjectOutputStream(fout);
Singleton1 instance = Singleton1.getInstance();
out.writeObject(instance);
FileInputStream fin = new FileInputStream(“Singleton.obj”);
ObjectInputStream in = new ObjectInputStream(fin);
Singleton1 singleton1 = (Singleton1) in.readObject();
System.out.println(instance==singleton1);
}
输出结果为:
既然是这样,我们就顺藤摸瓜,看看ObjectInputStream的readObject()方法和ObjectOutputStream的writeObject()方法有什么?
public final void writeObject(Object obj) throws IOException
{
if (enableOverride)
{
writeObjectOverride(obj);
return;
}
try
{
writeObject0(obj, false);//这里是关键
}
catch (IOException ex)
{
if (depth == 0)
{
writeFatalException(ex);
}
throw ex;
}
}
writeObject里面调用了 writeObject0(obj, false);那我们点进去看看这个方法
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try
{
//… …
//省略了其他的代码。。
// remaining cases
if (obj instanceof String)
{
writeString((String) obj, unshared);
}
else if (cl.isArray())
{
writeArray(obj, desc, unshared);
}
else if (obj instanceof Enum)
{
writeEnum((Enum<?>) obj, desc, unshared);//这里是关键
}
else if (obj instanceof Serializable)
{
writeOrdinaryObject(obj, desc, unshared);
}
else
{
if (extendedDebugInfo)
{
throw new NotSerializableException(
cl.getName() + “\n” + debugInfoStack.toString());
}
else
{
throw new NotSerializableException(cl.getName());
}
}
}
finally
{
depth–;
bout.setBlockDataMode(oldMode);
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、ZooKeeper 、数据结构、算法、Elasticsearch 、Kafka 、微服务、Linux。
这不,马上就要到招聘季了,很多朋友又开始准备“金三银四”的春招啦,那我想这份“java高分面试指南”应该起到不小的作用,所以今天想给大家分享一下。
请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析
3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-ZEu6a7I9-1711828522370)]
最后
一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、ZooKeeper 、数据结构、算法、Elasticsearch 、Kafka 、微服务、Linux。
这不,马上就要到招聘季了,很多朋友又开始准备“金三银四”的春招啦,那我想这份“java高分面试指南”应该起到不小的作用,所以今天想给大家分享一下。
[外链图片转存中…(img-vYCXwsI0-1711828522371)]
请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析
更多推荐
所有评论(0)