单例工场相关内容

Posted 星星大BOSS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例工场相关内容相关的知识,希望对你有一定的参考价值。

 1 单例工场简介

单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”

Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

在有些系统中 , 由于为了保持数据的一致性等各种原因, 要求某些类只能创建一个实例。

1.1 特点:

  • 单例类只能有一个实例。

  • 单例类必须自己创建自己的唯一实例。

  • 单例类必须给所有其他对象提供这一实例。

意图:保证一个类仅有一个实例,并提供一个访问他的全局访问点。

主要解决:一个全局使用的类频繁的创建与销毁。

何时使用:当您想控制实例数目节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例:

  • 1、一个班级只有一个班主任。

  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。

  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

1.2 线程安全问题

一方面在获取单例的时候,要保证不能产生多个实例对象,后面会详细讲到五种实现方式; 另一方面,在使用单例对象的时候,要注意单例对象内的实例变量是会被多线程共享的,推荐使用无状态的对象,不会因为多个线程的交替调度而破坏自身状态导致线程安全问题,比如我们常用的VO,DTO等(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题)。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

1.3 优缺点

优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。 缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

1.4 单例模式的使用场景

• 需要频繁的进行创建和销毁的对象;

• 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;

• 工具类对象;

• 频繁访问数据库或文件的对象。

单例也有两种实现模式 : 分为懒汉式单例和饿汉式单例

4.2 饿汉式单例

//饿汉式单例 
public class Hungry 
    //可能会浪费空间 (如果一个类中方法变量十分的多,且在不能确定是否需要该实例的情况下,直接将对象加载到内存会十分耗费内存。)
    private byte[] data1=new byte[1024*1024]; 
    private byte[] data2=new byte[1024*1024]; 
    private byte[] data3=new byte[1024*1024]; 
    private byte[] data4=new byte[1024*1024]; 
    private Hungry()  
    private final static Hungry HUNGRY = new Hungry(); 
    public static Hungry getInstance() 
        return HUNGRY; 
     

饿汉式单例由于new一个对象时不是一个原子性操作,即 new一个对象时, java内部会进行更细致的线程操作,则导致在多线程的条件下会不安全

饿汉式的特点是类加载时直接创建这个实例来供外部访问, 代码示例如下 :

//饿汉式单例
public class Singleton 
 
    //生成唯一实例
    private static Singleton singleton = new Singleton();
 
    //私有化构造方法
    private Singleton() 
    
    //向外提供唯一实例的方法
    public static Singleton getInstance()
        return singleton;
    

//测试
public class Test 
    public static void main(String[] args) 
        //拿到的是唯一实例@1b6d3586
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1); //@1b6d3586
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton2); //@1b6d3586
    

如上所示, 饿汉式的特点就是在调用 getInstance() 方法之前单例已被创建

4.3 懒汉式单例

懒汉式单例则是在第一次调用getInstance() 方法时才创建此实例,如下:

//懒汉式单例
public class Singleton 
 
    //声明静态实例
    private static Singleton singleton;
 
    //私有化构造方法
    private Singleton()
 
    //向外提供单例的方法
    public synchronized static Singleton getInstance()
        if (singleton == null)
            singleton = new Singleton();
        
        return singleton;
    

 public static void main(String[] args) 
        // singleton1 与 singleton2为同一实例
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1);
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton2);
    

如上所示 , 懒汉式单例在第一次调用 getInstance() 方法时才会去创建这个实例, 但懒汉式会有线程安全问题 , 所以在方法上直接上了synchronized锁

  外界第一次访问懒汉式单例时, 才去创建这个实例, 那么如果此时有两个线程碰巧同时都去调用这个方法, 此时就有可能创建两个实例出来, 所以我们要在方法上加锁, 避免此情况发生
//懒汉式单例 
public class LazyMan
    
    private LazyMan()  
    
    private volatile static LazyMan lazyMan; //双重检测模式的懒汉式单例 ,即DCL懒汉式 
    public static LazyMan getInstance() 
        if(lazyMan==null)
            synchronized(LazyMan.class) 
                if(lazyMan==null) 
                    lazyMan=new LazyMan(); //不是一个原子性操作 
                    /** 
                    1.分配内存空间 
                    2.执行构造方法,初始化对象 
                    3.把这个对象指向这个空间 123 A 132 B 此时lazyMan还没有完成构造 
                    **/ 
                 
             
         
        return lazyMan; 
     
    //多线程下不安全 
    //多线程并发 
    public static void main(String[] args) 
        for(int i = 0; i < 10; i++) 
            new Thread(()->
                lazyMan.getInstance(); 
            ).start(); 
         
    

4.4 DSL懒汉式(双重锁懒汉模式)

DSL:Double Check Lock 思考:如果直接在获取对象的静态方法上加上synchronized会有什么影响?使得获取对象的操作变为了同步操作。无论是第一次创建对象还是已有对象的返回都是同步操作。在上面已知只有在第一次获取对象时在多线程的环境下才有可能发生线程安全问题。全部锁上,一律变成单线程操作会影响性能。

思考:为什么在变量User前使用volatile?

volatitle的三大特点:

  • 可见性

  • 不保证原子性

  • 禁止指令重排

在User变量前加上了Volatile关键字,这里就用到了它的禁止指令重排的特性。那么什么是指令重排呢?要知道,虽然在我们自己的代码中只有一行代码user =new User(),但在JVM内部他可能分为三步执行:1. 在堆内存开辟空间 2. 在开辟的空间保存对象信息 3. 由栈内对象变量指向堆内存的空间 。但是,JVM为了效率存在乱序执行的可能,原本是1->2->3,乱序后就可能发生 1->3->2,虽然最后的结果是一样,但放在代码中,当有多个线程去执行它的时候,就会有小概率发生异常。

而在User前使用Volatile关键字修饰,就可以防止user变量赋值指令发生重排,能够按照1、2、3的顺序执行不乱序。确保返回的对象变量在堆空间已经完成赋值。

思考:为什么在类中锁住的是user.class? synchronized 锁类与锁对象的区别?

  • synchronized加在非静态方法锁住的是对象,加载静态方法锁住的是整个class类。例如:我在一个F非静态方法前上synchronized关键字,那么它的作用范围只是对于同一个对象来说的。同一个对象,即使在多个线程中调用此方法,在同一时刻内只能有一个线程进入了此方法。但如果我创建了两个对象A,B,分别在三个线程中(线程T1,T2使用对象A,线程T3使用对象B)调用F这一相同方法,那么会发生什么呢? 同一时刻,在F方法中,线程(T1或T2)可能与T3同时调用这一个方法,而同一个时刻,线程T1,T2在只能有一个线程在执行F方法。 这就是对象锁,同一个时刻,在多线程中同一个对象只能有一个线程执行此方法

  • synchronized 加在静态方法锁住的类,这个类创建的所有对象在多线程中只能有一个对象在执行此方法 还是上面的例子,如果方法使用的是synchronized锁住类,那么同一个时刻,T1,T2,T3三个线程中只能有一个线程的一个对象在执行此方法。

4.5 内部类实现单例

//静态内部类实现
public class Holder 
    public static class InnerClass  
    public static Holder getInstance()
        return InnerClass.HOLDER;
     
    public static class InnerClass 
        private static final Holder HOLDER=new Holder(); 
    

以上是关于单例工场相关内容的主要内容,如果未能解决你的问题,请参考以下文章

(转载)Java设计模式探讨之单例模式

Java设计模式学习笔记,一:单例模式

php 设计模式 - 单例

聊聊“单例模式”的多种实现方式。

JAVA设计模式(6:单例模式详解)

开发那些事儿:如何利用Go单例模式保障流媒体高并发的安全性?