什么是单例模式?
保证类在内存中只能有一个对象,且构造私有。
主要解决: 一个全局使用的类频繁地创建与销毁。
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
饿汉模式
【意 】在类加载就创建了实例,
【优点】多线程安全,在类一加载就初始化了实例
【缺点】资源浪费
package com.singleton;
/**
* 饿汉模式
* 在类一加载的时候就创建了实列
* 缺点是,会出现资源浪费
*/
public class Hungry {
//资源浪费了
Byte[] byte1 = new Byte[1024];
Byte[] byte2 = new Byte[1024];
Byte[] byte3 = new Byte[1024];
Byte[] byte4 = new Byte[1024];
private Hungry(){
}
private static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
【注】这个单例是不安全的,我们可以用反射破坏这个单例。
【使用反射破坏】
class test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Hungry> declaredConstructor = Hungry.class.getDeclaredConstructor(null);
//为true 表示可以获得私有成员
declaredConstructor.setAccessible(true);
//没通过这个单例类u getInstance()方法 就创建了实例。
Hungry hungry = declaredConstructor.newInstance();
}
}
懒汉模式与线程安全懒汉模式
【意】当需要的时候才会创建类的实例
【优点】多线程不安全,加上synchronized使多线程下安全
【缺点】必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)
package com.singleton;
/**
* 当要使用的时候才创建实例
*/
public class Lazy {
private Lazy(){
System.out.println("线程"+Thread.currentThread().getName()+"创建了一个实例");
}
private static Lazy lazy ;
//没有synchronized会出现线程安全问题,所以在方法上加上synchronized就线程安全了
public static Lazy getInstance(){
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
【模拟多线程不安全】
class test2{
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
Lazy.getInstance();
},i+"").start();
}
}
}
/*输出:
线程1创建了一个实例
线程3创建了一个实例
线程4创建了一个实例
线程2创建了一个实例
线程0创建了一个实例
*/
为什么会出现这种情况?
指令重排,并且创建实例不是一个原子性操作。
创建实例的顺序:
1、分配内存空间
2、执行构造方法、初始化对象
3、把这对象指向这个内存空间
到了多线程,就会出现——A线程还没执行完,B线程来了发现为null继续创建对象。
【注】依然可以用反射来破坏这种单例
【使用反射破坏】
class test2{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
//为true 表示可以获得私有成员
declaredConstructor.setAccessible(true);
//没通过这个单例 getInstance()方法 就获得了实例。
Lazy lazy = declaredConstructor.newInstance();
}
}
DCL懒汉模式
什么是DCL?
DCL也叫 双检锁/双重校验锁(DCL,即 double-checked locking)。
也就是说我们给这个单例上双重检测,并且加上锁。防止多线程的不安全发生。
【优点】多线程安全
package com.singleton;
public class DCLLazy {
private DCLLazy() {}
//volatile防止指令重排,synchronized可以保证原子性
private static volatile DCLLazy dclLazy = null;
public static DCLLazy getInstance(){
//如果这个实例不为空了其他线程都不能进来,还可以防止线程重复拿到锁,浪费资源
if (dclLazy == null) {
//当第一个线程进入后会给这个类上锁
synchronized (DCLLazy.class) {
if (dclLazy == null) {
dclLazy = new DCLLazy();
}
}
}
return dclLazy;
}
}
【注】还是不能防止反射破坏这个单例
【使用反射破坏】
class test3{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<DCLLazy> declaredConstructor = DCLLazy.class.getDeclaredConstructor(null);
DCLLazy dclLazy = declaredConstructor.newInstance();
DCLLazy dclLazy2 = declaredConstructor.newInstance();
System.out.println(dclLazy.equals(dclLazy2)); //输出false ,说明不是同一个对象
}
}
那么我们给这个DCL加强下,在构造加上
private DCLLazy() {
synchronized (DCLLazy.class) { //加锁,保证多线程下原子性
if (judge == false) { //第一次创建实例为false,并修改judge为true
judge = true;
} else {
throw new RuntimeException("不要使用反射来破坏");
}
}
}
再次运行main方法,发现报错了,控制台出现了我们抛出了异常,那是不是代表解决了呢?并不是,反射能获取私有的属性并且改变值。
Caused by: java.lang.RuntimeException: 不要使用反射来破坏
at com.singleton.DCLLazy.<init>(DCLLazy.java:15)
... 6 more
修改反射代码:
class test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Field judge = DCLLazy.class.getDeclaredField("judge"); //获得属性
judge.setAccessible(true);
Constructor<DCLLazy> declaredConstructor =DCLLazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//创建了第一个实例
DCLLazy dclLazy = declaredConstructor.newInstance();
//出现把judge属性设为false,这样就能够继续创建实例了
judge.set(declaredConstructor,false);
DCLLazy dclLazy2 = declaredConstructor.newInstance();
//依然输出false ,说明依然不是同一个对象
System.out.println(dclLazy.equals(dclLazy2));
}
}
静态内部类
实现了懒加载,只有当内部类被调用了,才会被类加载器加载。
【优点】多线程安全,这种方式能达到双检锁方式一样的功效,但实现更简单
package com.singleton;
public class StaticSingleton {
private StaticSingleton(){}
public StaticSingleton getInstance(){
return instanceClass.STATIC_SINGLETON;
}
//静态内部类
public static class instanceClass{
private static final StaticSingleton STATIC_SINGLETON = new StaticSingleton();
}
}
【注】依然能用反射破坏这个单例
【反射破坏】
class test4{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<StaticSingleton> declaredConstructor = StaticSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //为true 表示可以获得私有成员
StaticSingleton ss = declaredConstructor.newInstance(); //没通过这个单例 getInstance()方法 就获得了实例。
}
}
枚举
【优点】这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,多线程安全。
描述: 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
【注】可以防止反射。
public enum EnumSingleton {
INSTANCE;
public void outPut(){
System.out.println("红火火恍恍惚惚");
}
}
我们来看看编译后的class文件:
发现被编译后的class文件有一个私有的构造方法,那我们试试用反射能不能调用到这个构造。
package li;
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {
}
public void outPut() {
System.out.println("红火火恍恍惚惚");
}
}
【使用反射尝试破坏】
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
发现报错了,告诉我们,没有这个构造方法。
Exception in thread "main" java.lang.NoSuchMethodException: li.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at li.test5.main(EnumSingleton.java:15)
我们使用jad进行反编译,先要打开jad,然后在cmd输入:jad -sjava EnumSingleton.class
然后出现了一个java后缀的源文件:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package li;
import java.io.PrintStream;
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(li/EnumSingleton, name);
}
//看这里,发现是有两个参数的有参构造方法
private EnumSingleton(String s, int i)
{
super(s, i);
}
public void outPut()
{
System.out.println("\u7EA2\u706B\u706B\u604D\u604D\u60DA\u60DA");
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
那我们用反射看能不能使用这个有参构造
class test5{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//使用有参构造
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
enumSingleton.outPut();
}
}
抛出了异常:告诉你不能使用反射来创建枚举对象
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at li.test5.main(EnumSingleton.java:17)
总结
一般情况下,不建议使用第 2 懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。
- Post link: https://lzhbk.github.io/2020/04/24/%E5%BD%BB%E5%BA%95%E7%8E%A9%E8%BD%AC%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/
- Copyright Notice: All articles in this blog are licensed under unless stating additionally.
若您想及时得到回复提醒,建议跳转 GitHub Issues 评论。
若没有本文 Issue,您可以使用 Comment 模版新建。