Java常用设计模式-Singleton单例模式

概念

一个类在整个系统当中,只能出现一个全局访问点。
特点:

  • 一个类只能提供一个实例对象
  • 必须自己创建这个唯一的实例对象
  • 必须自己给所有其他对象提供这个实例

为什么会有单例模式

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

编写一个单例模式的几种方式

  1. 饿汉式
public class Singleton{
//构造方法私有化
private Singleton(){}
//实例化唯一对象
private static Singleton singleton =new Singleton();
//提供获得对象的方法
public static Singleton getSingleton(){
return singleton;
}
}

优点:简单、线程安全、第一次使用的时候速度快,因为加载的时候就已经创建好了。
缺点:造成内存浪费,创建的实例化对象不一定会被使用到,如果实例对象又比较大的话

  1. 懒汉式
public class Singleton{
//构造方法私有化
private Singleton(){}
//实例化唯一对象
private static Singleton singleton;
//提供获得对象的方法
public static Singleton getSingleton(){
if(singleton==null){
singleton= new Singleton();
}
return singleton;
}}

优点:节省资源
缺点:线程不安全
线程不安全可以通过加锁解决

public class Singleton{
//构造方法私有化
private Singleton(){}
//实例化唯一对象
private static Singleton singleton;
//提供获得对象的方法
//添加同步关键字synchronized
public static synchronized Singleton getSingleton(){
if(singleton==null){
singleton= new Singleton();
}
return singleton;
}}

解决了线程安全问题,但是会造成堵塞,当一个线程进来的时候所有线程都会阻塞,即使是对象已经被实例化过了,也会导致阻塞,可以使用双重检查锁的方式

public class Singleton{
//构造方法私有化
private Singleton(){}
//实例化唯一对象
private static Singleton singleton;
//提供获得对象的方法
//添加同步关键字synchronized
public static  Singleton getSingleton(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton= new Singleton();
}}}
return singleton;
}}

这里又涉及到了Java的指令重排,指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。当Java创建一个对象的实例时,底层实际是有好几个步骤的,首先是在堆内存中创建一块内存地址,然后初始化,再将引用赋值给对象。正因为指令重排的存在,可能导致了初始化和赋值的顺序是不确定的,jvm进行指令重排的时候,也是在确保单线程下不影响最终结果才会重排,就像初始化和赋值两个操作顺序变化后是不会影响最终结果的。但是在多线程的情况下,假设线程A在创建单例对象时,jvvm进行了指令重排,此时A线程把内存地址赋值给了线程,但是对象还没有进行初始化,这个时候B线程进来,去获取了这个实例对象,那么这个时候取到的这个对象是一个状态错误的对象。对于这种情况,也提供了一个关键字volatile,用来保证内存可见和防止指令重排。

public class Singleton{
//构造方法私有化
private Singleton(){}
//实例化唯一对象
private static volatile Singleton singleton;
//提供获得对象的方法
//添加同步关键字synchronized
public static  Singleton getSingleton(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton= new Singleton();
}}}
return singleton;
}}

实例对象变量使用volatile修饰后,就可以避免指令重排带来的问题。
使用这种方法解决了线程安全问题,性能也得到一些提高,但是线程同步始终是会带来性能消耗,所以可以使用静态内部类的方式

public class Singleton{
//构造方法私有化
private Singleton(){}
//利用类加载方式,静态内部类只有在使用的时候才会进行初始化
//既解决了线程安全问题带来的性能损耗,又避免了内存的浪费
public static Singleton getSingleton(){
return SingletonHolder.singleton;
}
private static class SingletonHolder{
private static Singleton singleton=new Singleton();
}
}

实际在遇到Java反射机制的时候,上面所有的单例模式都会出现问题,例如:

Class<Singleton> singletonClass=Singleton.class;
Constructor c=singletonClass.getDeclaredConstructor();
c.setAccessible(true);
System.out.println(c.newInstance());
System.out.println(c.newInstance());
System.out.println(c.newInstance());

最后得到的结果显然是不符合单例模式的特点的
在这里插入图片描述
这种情况还可以直接在构造方法里添加代码判断

public class Singleton{
//构造方法私有化
private Singleton(){
   if (SingletonHolder.singleton!=null){
            throw new RuntimeException("不允许非法创建对象");
        }
}
//利用类加载方式,静态内部类只有在使用的时候才会进行初始化
//既解决了线程安全问题带来的性能损耗,又避免了内存的浪费
public static Singleton getSingleton(){
return SingletonHolder.singleton;
}
private static class SingletonHolder{
private static Singleton singleton=new Singleton();
}
}

优点:性能高、线程安全、避免内存浪费
缺点:构造方法本来就是用来创建对象的,在构造方法内抛出异常,不符合Java设计理念,违反了单一职责原则
可以使用枚举的方式,枚举本身就是单例的(底层有一个枚举的常量字典使用键值对的方式保存了所有枚举,键就是INSTANCE,值是EnumSingleton的实例),而且枚举是不允许使用反射创建对象的(jdk官方最大)

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

改到这里,发现枚举其实又变回了饿汉模式,既然是常量字典那在加载的时候就会赋值,如果实例比较大,还是会占用内存空间。
可以模仿Spring当中的做法,Spring的IOC就是一种容器式单例,也是把实例对象装在一个容器里面,每次创建对象的时候可以判断一下容器里面是否已经有了,有的话就直接取出来,没有就创建了之后再取出来。

public class IocSingleton {
    private IocSingleton(){}
    //ioc容器
    private static Map<String,Object> ioc=new ConcurrentHashMap<>();
    //获取实例对象
    public static Object getInstance(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)){
                Object object=null;
                try{
                    object=Class.forName(className).newInstance();
                    ioc.put(className,object);
                }catch(Exception e){
                    e.printStackTrace();
                }
                return  object;
            }else{
                return ioc.get(className);
            }
        }
    }
}

没有绝对完美的方法,按需选择即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值