Java设计模式(九)单例模式

一、引言

 

       在很多场景中,我们需要控制某些类在系统中只存在一个实例,如线程池、资源管理器等,这就需要用到单例设计模式。单例模式能够将类的实例化过程封装起来加以控制,确保只产生一个实例对象,并向整个系统提供这个实例。


二、模式分析


        实现单例模式最好的方法就是让类自己负责产生并保存它的唯一实例,使用者只能获得这个实例的引用而不能创造新的实例。因此在单例模式中,我们要定义一个自身类型的私有静态引用成员变量,同时将构造方法私有化,以基本确保该类不能在外部被实例化(注意,利用反射机制也可以通过私有构造函数实例化对象,在这里我们不考虑这一点)。为了使他对象要想访问其他对象可以访问该类的唯一实例,我们还需要在该类中创建一个getInstance方法将该实例对象的引用返回,如下图所示。



三、分析与实现


      单例模式的实现方法有两种,分别称为懒汉式与饿汉式,我们首先来看懒汉式的实现方法。


3.1 线程不安全的懒汉式

class SingletonLazy {
    
    private static SingletonLazy instance = null;    // 用于维护唯一的实例
    public static int insNum;
    
    private SingletonLazy() {                        // 私有化构造函数确保对象无法在类外被实例化
        ++insNum;
    }// constructor
    
    public static SingletonLazy getInstance() {
        if (instance == null)                        // 确保对象只被被实例化一次
            instance = new SingletonLazy();          
        return instance;                             // 返回唯一实例的引用
    }// getInstance
    
}/*SingletonLazy*/

        可以看出,由于构造函数被私有化,所以无法在类外被调用,实例化过程被封装在getInstance方法中,且由于static字段的特性,构造函数仅会被调用一次,实现了单例模式的功能。另外可以发现,Singleton类在加载时并没有产生实例,而是在第一次调用getInstance方法的时候才产生实例。由于这种方法直到需要使用对象时时才产生实例,因此被称为懒汉式。


3.2 使用同步的线程安全懒汉式

        上面的代码虽然实现了单例模式的功能,但它确实线程不安全的。因为如果存在多条线程同时调用getInstance方法,如果在执行调用构造函数之前已有多条线程执行了if判断语句,那么还是会产生多个实例。线程测试代码如下:

public class SingletonDemo {
    
    public static void main(String[] args) {
        MultiThread threadWork = new MultiThread();
        Thread t1 = new Thread(threadWork);
        Thread t2 = new Thread(threadWork);
        Thread t3 = new Thread(threadWork);
        Thread t4 = new Thread(threadWork);
        t1.start(); t2.start(); t3.start(); t4.start();
    }// main
    
}/*Singleton*/

class MultiThread implements Runnable {
    
    @Override
    public void run() {
        SingletonLazy.getInstance();
        System.out.println("Instance number: " + SingletonLazy.insNum);
    }// run
    
}/*multiThread*/

执行结果(结果不唯一,可以多运行几次):


        为了保证线程安全,可以通过给getInstane方法加锁的方式确保实例的唯一性。

class SafeSingletenLazy {
    
    private volatile static SafeSingletenLazy instance = null; // 禁止乱序写入
    private static Object lock = new Object();          
    public static int instanceNumber;
    
    private SafeSingletenLazy() {
        ++instanceNumber;
    }// constructor
    
    public static SafeSingletenLazy getInstance() {
        if (instance == null)                           // 双重判断防止每次执行都同步
            synchronized(lock) {                        // 产生实例的代码块要加锁
                if (instance == null)
                    instance = new SafeSingletenLazy();
            }// synchronized
        return instance;
    }// getInstance
   
}/*SafeSingletenLazy*/

       通过将产生实例的代码块加锁的方式确保每次只有一个线程去进行是否已产生实例的判断,从而解决了上面的问题,读者可以将新的代码替换到本小节一开始给出的测试代码中运行一下。同步虽然能解决线程安全问题,但是又造成了效率的降低,因此我们又在同步代码块之外添加了一个判断实例是否为空的语句,从而线程间尽在实例产生之前同步,实例产生之后不再同步,提高了效率。注意由于Java存在乱序写入(也就是说先给实例分配空间,但是却不立即构造实例,此时指向实例的引用不为空,但实际上实例并没有构造完全,详细的内容请参照http://www.iteye.com/topic/652440))的问题,instance字段可能得到一个非空但不完整的实例而导致系统崩溃(可能是我的代码太简单,反正我反复事了n次都没有出现异常,你们要遇到了跟我说一声哈),要想避免出现这种情况,就需要使用volatile关键字禁止对instance字段的乱序写入优化。


3.3 使用内部类的线程安全懒汉式

       通过同步解决线程安全问题带来了效率的降低,还有另外一种懒汉式单例模式能够不需要同步就解决线程安全问题,它用到了静态内部类。

class SafeSingletenLazy {
    
    public static int instanceNumber = 0;
    private static SafeSingletenLazy instance = null; 
    
    private SafeSingletenLazy() {
        ++instanceNumber;
    }// constructor
    
    /**
     * 定义了一个静态内部类,在内部类中产生外部类实例,利用静态内部类仅产生一个实例的特性,保证
     * 了外部实例的单一性,同时由于内部类仅在被调用时才会被加载,因此符合懒汉式的特性。
     */
    private static class InstanceGenerator {
        public static final SafeSingletenLazy instance = new SafeSingletenLazy();
    }/*InstanceGenerator*/
    
    public static SafeSingletenLazy getInstance() {
        instance = InstanceGenerator.instance;       // 从内部类中提取外部类实例引用
        return instance;
    }// getInstance
   
}/*SafeSingletenLazy*/

        这种方法在单例类内定义了一个静态内部类,在内部类中调用外部类私有构造函数产生单例类实例,由于静态内部类仅产生一个实例,因此外部类的构造函数金杯调用了一次,只会产生一个实例,保证了外部实例的单一性。同时由于内部类仅在被调用时才会被加载,因此符合懒汉式的特性。


3.4 饿汉式

        除了懒汉式之外,还有一种单例模式的实现方法在类加载时就会产生一个实例,在需要使用实例时直接返回类的引用即可。

/**
 * 饿汉式的单例模式,利用静态成员变量的特性保证唯一实例,而且不同线程共用一个静态变量存储区,
 * 所以饿汉式单利具有天生的线程安全性。但是由于饿汉式在类加载时即创建实例,会在调用该实例之
 * 前占用一定的内存,不过也使第一次调用示例的速度很快。
 */
class HungerSingleton {
    
    public static final HungerSingleton singleInstance = new HungerSingleton();
    
    private HungerSingleton() {}
    
    public static HungerSingleton getInstance() {
        return singleInstance;
    }// getInstance
    
}/*HungerSingleton*/

        饿汉式的单例模式,利用静态成员变量的特性保证唯一实例,而且不同线程共用一个静态变量存储区,所以饿汉式单利具有天生的线程安全性。但是由于饿汉式在类加载时即创建实例,会在调用该实例 前占用一定的内存,不过也使第一次调用示例的速度很快。


四、总结

        饿汉式也就是静态加载的单例模式,它在类加载时就产生实例对象,使用速度快,天生线程安全,但是要提前占用系统资源。而懒汉式直到使用类时才加载,但是却产生了线程安全问题,需要使用双重锁定才能保证线程安全,使用时需要根据实际情况而定。下面是本章内容所涉及的所有测试代码的完整工程。

/**
 * 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在很多情况下,我
 * 们只希望系统中一个类只有一个实例对象,如线程池、资源管理器等,这就需要用到单例模式。
 * 
 * 实现单例模式最好的方法就是让自身负责产生并保存它的唯一实例,因此在单例模式中,我们要定义
 * 一个自身类型的私有静态引用成员变量,同时将构造方法私有化,以基本确保该类不能在外部被实例
 * 化(注意,利用反射机制也可以通过私有构造函数实例化对象,在这里我们不考虑这一点)。为了使
 * 他对象要想访问其他对象可以访问该类的唯一实例,我们还需要在该类中创建一个getInstance方
 * 法将该实例对象的引用返回。
 */
package dp9_singleton;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SingletonDemo {
    
    public static void main(String[] args) throws InterruptedException {
        /*UnsafeLazySingleton instance1 = UnsafeLazySingleton.getInstance();
        UnsafeLazySingleton instance2 = UnsafeLazySingleton.getInstance();
        System.out.println(instance1 == instance2);*/
        
        MultipleThreadTest1 test1 = new MultipleThreadTest1();
        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test1);
        t1.start(); t2.start();
        
        MultipleThreadTest2 test2 = new MultipleThreadTest2();
        Thread t3 = new Thread(test2);
        Thread t4 = new Thread(test2);
        t3.start(); t4.start(); 
        
        HungerSingleton instance1 = HungerSingleton.getInstance();
        HungerSingleton instance2 = HungerSingleton.getInstance();
        System.out.println(instance1 == instance2);
    }// main

}/*SingletonLazyDemo*/

/**
 * 懒汉式单例模式,实例在首次调用getInstance方法时才被产生,线程不安全,如果存在多个线程同
 * 时调用getInstance方法,则可能产生多个实例。
 */
class UnsafeLazySingleton {
    
    // 用于维护实例的引用
    private static UnsafeLazySingleton singleInstance = null;
    // 定义一个静态变量用于统计虚拟机中该实例的数量
    public static int instanceNumber = 0;
    
    private UnsafeLazySingleton() {
        ++instanceNumber;
    }// constructor
    
    public static UnsafeLazySingleton getInstance() {
        if (singleInstance == null) {
            singleInstance = new UnsafeLazySingleton();
        }// if
        return singleInstance;
    }// getInstance
    
}/*Singleton*/

/**
 * 定义一个多线程测试类,实现Runnable接口,重写run方法以在多个线程中调用getInstance方法
 */
class MultipleThreadTest1 implements Runnable {
    
    @Override
    public void run() {
        UnsafeLazySingleton.getInstance();
        System.out.println(UnsafeLazySingleton.instanceNumber);
    }// run
    
}/*MultipleThread*/

class SafeLazySingleton {
   
    // 用于维护实例的引用
    // private static SafeLazySingleton singleInstance = null;
    private static volatile SafeLazySingleton singleInstance = null;
    
    // 定义一个静态变量用于统计虚拟机中该实例的数量
    public static int instanceNumber = 0;
    
    // 定义一个同步锁
    private static Lock lock = new ReentrantLock();
    
    private SafeLazySingleton() {
        ++instanceNumber;
    }// constructor
    
    /** 
     * 对生成实例的代码段枷锁确保线程安全,但是由于每一次调用getInstance都要同步,效率低。
     */
    public static SafeLazySingleton getInstance1() {
        lock.lock();
        if (singleInstance == null) {
            singleInstance = new SafeLazySingleton();
        }// if
        lock.unlock();
        return singleInstance;
    }// getInstance  
    
    /**
     * 先判断是否是第一次实例化,如果是,则进入同步代码块,否则不进如同步代码块。但要注意,
     * 在Java中由于JVM存在乱序写入优化功能(http://www.iteye.com/topic/652440),使得
     * singleInstance成员变量可能得到一个非空但不完整的实例而导致程序出错,要想避免出现
     * 这种情况,就需要使用volatile关键字禁止对singleInstance成员变量的乱序写入优化。
     */
    public static SafeLazySingleton getInstance2() {
        if (singleInstance == null) {
            lock.lock();
            if (singleInstance == null) {
                singleInstance = new SafeLazySingleton();
            }// if
            lock.unlock();
        }// if
        return singleInstance;
    }// getInstance
    
    /**
     * 内部类方式实现线程安全的懒汉式单利模式,由于类仅在被调用后才会被加载,因此可以用内部
     * 类的方法实现延后实例化。
     */
    public static class InstanceGenerator {
        
        public static final SafeLazySingleton singleInstance = new SafeLazySingleton();
        
    }/*InstanceGenerator*/
    
    public static SafeLazySingleton getInstance3() {
        return InstanceGenerator.singleInstance;
    }// getInstance3
    
}/*SafeLazySingleton*/

/**
 * 定义一个多线程测试类,实现Runnable接口,重写run方法以在多个线程中调用getInstance方法
 */
class MultipleThreadTest2 implements Runnable {
    
    @Override
    public void run() {
        // SafeLazySingleton.getInstance1();
        // SafeLazySingleton.getInstance2();
        SafeLazySingleton.getInstance3();
        System.out.println(SafeLazySingleton.instanceNumber);
    }// run
    
}/*MultipleThreadTest2*/

/**
 * 饿汉式的单例模式,利用静态成员变量的特性保证唯一实例,而且不同线程共用一个静态变量存储区,
 * 所以饿汉式单利具有天生的线程安全性。但是由于饿汉式在类加载时即创建实例,会在调用该实例之
 * 前占用一定的内存,不过也使第一次调用示例的速度很快。
 */
class HungerSingleton {
    
    public static final HungerSingleton singleInstance = new HungerSingleton();
    
    private HungerSingleton() {}
    
    public static HungerSingleton getInstance() {
        return singleInstance;
    }// getInstance
    
}/*HungerSingleton*/










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值