我技术群里的一位小伙伴,昨天去面试,就因为一个单例模式,然后叫他回去等通知了。
下面是这位同学被问到的问题:
1、说说单例模式的特点?
2、你知道单例模式的具体使用场景吗?
3、单例模式常见写法有几种?
4、怎么样保证线程安全?
5、怎么不会被反射攻击?
6、怎样保证不会被序列化和反序列化的攻击?
7、枚举为什么会不会被序列化?
.....
你也可以尝试行的回答这几个题,看看自己能回答上几个。
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
特点:
**目的:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
案例:一家企业只能有一个CEO,有多个了其实乱套了。
需要确保任何情况下都绝对只有一个实例。
比如:ServletContext
、ServletConfig
、ApplicationContext
、DBTool
等,都使用到了单列模式。
从名字上就能看出,饿汉:饿了就得先吃饱,所以,一开始就搞定了。
饿汉式主要是使用了static,饿汉式也有两种写法,但本质可以理解为是一样的。
public class HungrySingleton{
private static final HungrySingleton INSTANCE;
static {
INSTANCE=new HungrySingleton();
}
// private static final HungrySingleton INSTANCE=new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
饿汉式有个致命的缺点:浪费空间,不需要也实例化。如果是成千上万个,也这么玩,想想有多恐怖。
于是,就会想到,能不能在使用的时候在实例化,从而引出了懒汉式。
顾名思义,就是需要的时候再创建,因为懒,你不调用我方法,我是不会干活的。
下面是懒汉式的Java代码实现:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {//01
lazySingleton = new LazySingleton();//02
}
return lazySingleton;
}
}
进入getInstance方法,先判断lazySingleton是否为空,为空,则创建一个对象,然后返回此对象。
但是,问题来了:
两个线程同时进入getInstance方法,然后都去执行01这行代码,都是true,然后各自进去创建一个对象,然后返回自己创建的对象。
这岂不是不满足只有唯一 一个对象的了吗?所以这类存在线程安全的问题,那怎么解决呢?
第一印象肯定都是想到加锁。于是,就有了下面的线程安全的懒加载版本:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
//简单粗暴的线程安全问题解决方案
//依然存在性能问题
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
给getInstance方法加锁同步锁标志synchronized,但是又涉及到锁的问题了,同步锁是对系统性能优影响的,尽管JDK1.6后,对其做了优化,但它毕竟还是涉及到锁的开销。
每个线程调用getInstance方法时候,都会涉及到锁,所以又对此进行了优化成为了大家耳熟能详的双重检查锁。
代码实现如下:
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {//01
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {//02
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
这段代码中,在01行,如果不为空,就直接返回,这是第一次检查。如果为空,则进入同步代码块,02行又进行一次检查。
双重检查就是现实if判断、获取类对象锁、if判断。
上面这段代码,看似没问题,其实还是有问题的,比如:指令重排序(需要有JVM知识垫底哈)
指令重排是什么意思呢?
比如java中简单的一句
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
为了防止指令重排序,所以,我们可以使用volatile来做文章(注意:volatile能防止指令重排序和线程可见性)。
于是,更好的版本就出来了。
public class LazyDoubleCheckSingleton {
//使用volatile修饰
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
尽管相比前面的版本,确实改进了很多,但依然有同步锁,还是会影响性能问题。于是,又进行优化为静态内部类方式:
下面是静态内部类的代码实现:
public class LazyStaticSingleton {
private LazyStaticSingleton() {
}
public static LazyStaticSingleton getInstance() {
return LazyHolder.LAZY_STATIC_SINGLETON;
}
//需要等到外部方法调用是猜执行
//巧用内部类的特性
//JVM底层执行,完美的规避了线程安全的问题
private static class LazyHolder {
private static final LazyStaticSingleton LAZY_STATIC_SINGLETON = new LazyStaticSingleton();
}
}
利用了内部类的特性,在JVM底层,能完美的规避了线程安全的问题,这种方式也是目前很多项目里喜欢使用的方式。
但是,还是会存在潜在的风险,什么风险呢?
可以使用 反射 暴力的串改,同样也会出现创建多个实例:
反射代码实现如下:
import java.lang.reflect.Constructor;
public class LazyStaticSingletonTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyStaticSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance();
Object object1 = LazyStaticSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
这段代码运行结果为false。
所以,上面说的双重检查锁的方式,通过反射,还是会存在潜在的风险。怎么办呢?
在《Effect java 》这本书中,作者推荐使用枚举来实现单例模式,因为枚举不能被反射。
下面是枚举式的单例模式的代码实现:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
我们把上面反射的那个代码,来测试这个枚举式单例模式。
public class EnumTest {
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance();
Object object1 = EnumSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
运行这段代码:
java.lang.NoSuchMethodException: com.tian.my_code.test.designpattern.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:41
还真的不能用反射来搞。如果此时面试官,为什么枚举不能被反射呢
?
我们在反射的代码中
Constructor constructor = clazz.getDeclaredConstructor(null);
这行代码是获取他的无参构造方法。并且,从错误日志中,我们也可以看到,错误出现就是在getConstructor0方法中,并且,提示的是没有找到无参构造方法。
很奇怪,枚举也是类,不是说如果我们不给类显示定义构造方法时候,会默认给我们创建一个无参构造方法吗?
于是,我想到了一个办法,我们可以使用jad这个工具去反编译的我们的枚举式单例的.class文件。
找到我们的class文件所在目录,然后我们可以执行下面这个命令:
C:\Users\Administrator>jad D:\workspace\my_code\other-local-demo\target\classes
com\tian\my_code\test\designpattern\singleton\EnumSingleton.class
Parsing D:\workspace\my_code\other-local-demo\target\classes\com\tian\my_code\t
st\designpattern\singleton\EnumSingleton.class... Generating EnumSingleton.jad
注意:class文件目录以及生成的jad文件所在的目录。
然后打开EnumSingleton.jad 文件:
于是,我就想到了,那我们使用有参构造方法来创建:
public class EnumTest {
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
//强行访问
constructor.setAccessible(true);
Object object = constructor.newInstance("田维常",996);
Object object1 = EnumSingleton.getInstance();
System.out.println(object == object1);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
再次运行这段代码,结果:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:45)
提示很明显了,就是不让我们使用反射的方式创建枚举对象。
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//Modifier.ENUM就是用来判断是否为枚举的
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
所以,到此,我们才算真正的理清楚了,为什么枚举不让反射的原因。
我们以非线程安全的饿汉式来演示一下,看看序列化是如何破坏到了模式的。
public class ReflectTest {
public static void main(String[] args) {
// 准备两个对象,singleton1接收从输入流中反序列化的实例
HungrySingleton singleton1 = null;
HungrySingleton singleton2 = HungrySingleton.getInstance();
try {
// 序列化
FileOutputStream fos = new FileOutputStream("HungrySingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("HungrySingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (HungrySingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
com.tian.my_code.test.designpattern.singleton.HungrySingleton@7e6cbb7a
com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
false
看到了吗?
使用序列化是可以破坏到了模式的,这种方式,可能很多人不是很清楚。
我们对非线程安全的饿汉式代码进行稍微修改:
public class HungrySingleton implements Serializable{
private static final HungrySingleton INSTANCE;
static {
INSTANCE=new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
//添加了readResolve方法,并返回INSTANCE
private Object readResolve方法,并返回(){
return INSTANCE;
}
}
再次运行上那段序列化测试的代码,其结果如下:
com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
true
嘿嘿,这样我们是不是就避免了只创建了一个实例?
答案:否
在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))
方法。
在readOrdinaryObject方法中有这么一段代码:
Object obj;
try {
//是否有构造方法,有构造放就创建实例
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
}
//判断单例类是否有readResolve方法
if (desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
}
//invokeReadResolve方法中
if (readResolveMethod != null) {
//调用了我们单例类中的readResolve,并返回该方法返回的对象
//注意:是无参方法
return readResolveMethod.invoke(obj, (Object[]) null);
}
绕了半天,原来他是这么玩的,上来就先创建一个实例,然后再去检查我们的单例类是否有readResolve无参方法,我们单例类中的readResolve方法
private Object readResolve(){
return INSTANCE;
}
我们重写了readResolve()无参方法,表面上看是只创建了一个实例,其实只创建了两个实例。
紧接着,面试官继续问:枚举式单例能不能被序列化破坏呢?
答案:不能被破坏,请看我慢慢给你道来。
don't talk ,show me the code。
我们先来验证一下是否真的不能被破坏,请看代码:
public class EnumTest {
public static void main(String[] args) {
// 准备两个对象,singleton1接收从输入流中反序列化的实例
EnumSingleton singleton1 = null;
EnumSingleton singleton2 = EnumSingleton.getInstance();
try {
// 序列化
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
INSTANCE
INSTANCE
true
确实,枚举式单例是不会被序列化所破坏,那为什么呢?总得有个证件理由吧。
在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))
方法。
case TC_ENUM:
return checkResolve(readEnum(unshared));
在readEnum方法中
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
//重点
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
//...其他代码省略
}
}
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
//enumType.enumConstantDirectory()返回的是一个HashMap
//通过HashMap的get方法获取
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
//返回一个HashMap
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
//使用的是HashMap
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
所以,枚举式单例模式是使用了Map<String, T>,Map的key就是我们枚举类中的INSTANCE。由于Map的key的唯一性,然后就缔造出唯一实例。江湖上也把这个枚举式单例模式叫做注册式单例模式
。
在Spring中也是有大量使用这种注册式单例模式,IOC
容器就是典型的代表。
本文讲述了单例模式的定义、单例模式常规写法。单例模式线程安全问题的解决,反射破坏、反序列化破坏等。
注意:不要为了套用设计模式,而使用设计模式。而是要,在业务上遇到问题时,很自然地联想单设计模式作为一种捷径方法。
在内存中只有一个实例,减少内存开销。可以避免对资源的多重占用。设置全局访问点,严格控制访问。
没有借口,扩展性很差。如果要扩展单例对象,只有修改代码,没有其他途径。
单例模式是 不符合开闭原则的。
单例模式的重点知识总结:
如果有收获,请点个赞、在看,也建议分享给大家一起看单例模式。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Fwi6TLm4ucLgvlXc5lQCZw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。