首页 > 代码库 > JAVA设计模式之单例模式

JAVA设计模式之单例模式

概念:
 
 Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。

单例模式有以下特点:

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

 

单件模式用途:
  单件模式属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。

  单件模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。

  在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例,这些应用都或多或少具有资源管理器的功能。

  它的用途十分广泛,打个比方,我们开发了一个简单的留言板,用户的每一次留言都要将留言信息写入到数据库中,最直观的方法是没次写入都建立一个数据库的链接。这是个简单的方法,在不考虑并发的时候这也是个不错的选择。但实际上,一个网站是并发的,并且有可能是存在大量并发操作的。如果我们对每次写入都创建一个数据库连接,那么很容易的系统会出现瓶颈,系统的精力将会很多的放在维护链接上而非直接查询操作上。这显然是不可取的。

  如果我们能够保证系统中自始至终只有唯一一个数据库连接对象,显然我们会节省很多内存开销和cpu利用率。这就是单件模式的用途。

  当然单件模式不仅仅只用于这样的情况。在《设计模式:可复用面向对象软件的基础》一书中对单件模式的适用性有如下描述:

    • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时
    • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时

下面对单件模式的懒汉式与饿汉式进行简单介绍:   

  • 饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建
  • 懒汉式:当程序第一次访问单件模式实例时才进行创建

如何选择:

  • 如果单件模式实例在系统中经常会被用到,饿汉式是一个不错的选择
  • 反之如果单件模式在系统中会很少用到或者几乎不会用到,那么懒汉式是一个不错的选择

一、懒汉式单例

package singletonPattern.example;

/**
 * 懒汉式单例类
 * 
 * 特点:当程序第一次访问单件模式实例时才进行创建。
 * 
 * 懒汉模式的特点是加载类时比较快,但运行时比较慢-线程不安全
 * 
 * @author otowa
 * @Date 2016-12-21 14:01
 */
public class IdleSingleton {
    
    // 声明类的唯一实例,使用private static 修饰
    private static IdleSingleton idleSingleton = null;

    // 将构造函数私有化,不允许外部直接创建对象
    private IdleSingleton() {}
    
    /**
     * 1.静态工厂方法(线程不安全)
     * @return
     */
    @SuppressWarnings("unused")
    private static IdleSingleton getInstanceIsNotSafe() {
        if(idleSingleton == null) {
            idleSingleton = new IdleSingleton();
        }
        return idleSingleton;
    }
    
}
  idleSingleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

 (事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

  但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例。

要实现线程安全,有以下三种方式:

  都是对getInstance这个方法改造,保证了懒汉式单例的线程安全。

package singletonPattern.example;

/**
 * 懒汉式单例类
 * 
 * 考虑线程安全
 * 
 * @author otowa
 * @Date 2016-12-21 14:01
 */
public class IdleSingleton {
    
    // 声明类的唯一实例,使用private static 修饰
    private static IdleSingleton idleSingleton = null;

    // 将构造函数私有化,不允许外部直接创建对象
    private IdleSingleton() {}
    
    /**
     * 1.静态工厂方法(线程安全)
     * 在方法上加同步锁
     * @return
     */
    @SuppressWarnings("unused")
    private static synchronized IdleSingleton getInstanceSafeOne() {
        if(idleSingleton == null) {
            idleSingleton = new IdleSingleton();
        }
        return idleSingleton;
    }
    
    /**
     * 2.静态工厂方法(线程安全)
     * 双重检查锁定
     * @return
     */
    @SuppressWarnings("unused")
    private static IdleSingleton getInstanceSafeTow() {
        if(idleSingleton == null) {
            //加锁同步代码块
            synchronized(IdleSingleton.class) {
                if(idleSingleton == null) {
                    idleSingleton = new IdleSingleton();
                }
            }
        }
        return idleSingleton;
    }
    
    /**
     * 3.静态工厂方法(线程安全)
     * 静态内部类
     * 比之方法上加同步锁、双重检查锁定好一些,既实现了线程安全,又避免了同步带来的性能影响
     * @return
     */
    private static class LazyHolder {
        
        private static final IdleSingleton INSTANCE = new IdleSingleton();
    }
    @SuppressWarnings("unused")
    private static IdleSingleton getInstanceSafeThree() {
        
        return LazyHolder.INSTANCE;
    }
    
}

二、饿汉式单例

package singletonPattern.example;

/**
 * 饿汉式单例类(线程安全)
 * 
 * 特点:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
 * 
 * 饿汉模式的特点是加载类时比较慢,但运行是比较快-线程安全
 * 
 * @author otowa
 * @Date 2016-12-21 16:01
 */
public class HungrySingleton {
    
    //    声明类的唯一实例,饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
    private static final HungrySingleton HUNGRYSINGLETON = new HungrySingleton();

    //    私有构造方法
    private HungrySingleton() {}
    
    public static HungrySingleton getInstance() {
        
        return HUNGRYSINGLETON;
    }
}

三、登记式单例(可忽略)

  登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 

  这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

package singletonPattern.example;

import java.util.HashMap;
import java.util.Map;

/**
 * 登记式单例(可忽略)
 * 
 * 类似Spring里面的方法,将类名注册,下次从里面直接获取
 * 
 * @author otowa
 * @Date 2016-12-21 16:15
 */
public class RegSingleton {
    
    //静态Map集合
    private static Map<String, RegSingleton> staticMap = new HashMap<String, RegSingleton>();
    
    static {
        RegSingleton regSingleton = new RegSingleton();
        staticMap.put(regSingleton.getClass().getName(), regSingleton);
    }

    //保护的默认构造器
    protected RegSingleton() {
        
        System.out.println("走几次");
    }
    
    /**
     * 静态工厂方法,返还此类惟一的实例
     * @return
     */
    public static RegSingleton getInstance(String className) {
        
        if(className == null){
            className = RegSingleton.class.getName();
            System.out.println("name == null--->name="+className);  
        }
        if(!staticMap.containsKey(className)){
            
            try {
                staticMap.put(className, (RegSingleton) Class.forName(className).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return staticMap.get(className);
    }
    
    /**
     * 一个示意性的商业方法
     * @return
     */
    public String about() {      
        return "Hello, I am RegSingleton.";      
    }
    
    /**
     * main方法测试输出
     * @param args
     */
    public static void main(String[] args) {
        RegSingleton regSingleton1 = RegSingleton.getInstance(null);
        
        RegSingleton regSingleton2 = RegSingleton.getInstance(null);
        
        System.out.println(regSingleton1 == regSingleton2);
    }
}

饿汉式和懒汉式区别

  从名字上来说,饿汉和懒汉,

  饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

  而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

  1、线程安全:

    饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

    懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。


  2、资源加载和性能:

  饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

  而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

  至于1、2、3这三种实现又有些区别:

    第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

    第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

    第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

什么是线程安全?

  如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

应用

以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:

package singletonPattern.entity;
/**
 * 懒汉式单例实体类
 * 
 * 双重检查锁定的方式(线程安全)
 * 
 * @author otowa
 * @Date 2016-12-21 17:07
 */
public class Person {  
    String name = null;  
  
    private Person() {}  
  
    private static volatile Person person = null;  
  
    public static Person getInstance() {  
           if (person == null) {    
             synchronized (Person.class) {    
                if (person == null) {    
                    person = new Person();   
                }    
             }    
           }   
           return person;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public void printInfo() {  
        System.out.println("the name is " + name);  
    }  
  
}  

  可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢,原因已经在下面评论中提到。

  还有疑问可参考:单例模式与双重检测  和  The "Double-Checked Locking is Broken" Declaration

package singletonPattern.test;

import singletonPattern.entity.Person;
/**
 * 
 * 测试懒汉式单例实体类
 * 
 * 双重检查锁定的方式(线程安全)
 * @author otowa
 * @Date 2016-12-21 17:10
 */
public class TestIdleSingleton {

    public static void main(String[] args) {
        
        //创建两个实例
        Person person1 = Person.getInstance();
        person1.setName("jason");
        
        Person person2 = Person.getInstance();
        person2.setName("otowa");
        
        //内存地址比较
        System.out.println("ONE-->"+person1+"\nTOW-->"+person2);
        if(person1 == person2){
            System.out.println("创建的是同一个实例");  
        }else{  
            System.out.println("创建的不是同一个实例");  
        }  
    }
}

运行结果:

技术分享

结论:

  由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

  对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。

文章参考:一个本科小生的奋斗史

JAVA设计模式之单例模式