Java设计模式-3.单例模式概述与使用

  Java设计模式-单例模式概述与使用

参考:https://blog.csdn.net/fly910905/article/details/79286680

1、单例模式的概述与使用

  • 单例模式,就是要确保类在内存中只有一个对象,该对象必须自动创建,并且对外提供。
  • 优点:系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁对象的方法,单例模式无疑可以提高系统的性能。
  • 缺点:没有抽象层,因此扩展很难。职责过重,在一定程序上违背了单一职责。
  • 单例模式的分类:
             单例模式之饿汉式:类一加载的时候就创建对象。
             单例模式之懒汉式:类一加载的时候并不急着去创建对象,而是在需要用的时候才去创建对象。

2、单例模式之饿汉式:开发时使用

(0)单例模式的实现步骤:3部曲

  • 1)把类的构造方法私有化
  • 2)在类中的成员位置自己创建一个供别人使用的唯一的类对象
    • 由于静态方法只能访问静态成员变量,因此这个类对象需要加静态static修饰
    • 又因为不能够让外界直接访问修改这个类对象的值,因此需要加私有修饰符private修饰
  • 3)在类中提供一个对外公共的访问方式,为了让外界能够直接使用该公共访问方式,该方式需要加静态static修饰

(1)实现单例模式的学生类

package cn.itcast_03;
public class Student {
	private Student() { }
	private static Student s = new Student();
	public static Student getStudent() {
		return s;
	}
}

(2)测试该单例模式

package cn.itcast_03;

/*
 * 单例模式:保证类在内存中只有一个对象。
 * 如何保证类在内存中只有一个对象呢?
 * 		A:把构造方法私有
 * 		B:在成员位置自己创建一个对象
 * 		C:通过一个公共的方法提供访问
 */
public class StudentDemo {
	public static void main(String[] args) {
		// Student s1 = new Student();
		// Student s2 = new Student();
		// System.out.println(s1 == s2); // false
		Student s1 = Student.getStudent();
		Student s2 = Student.getStudent();
		System.out.println(s1 == s2);
		System.out.println(s1); // null,cn.itcast_03.Student@175078b
		System.out.println(s2);// null,cn.itcast_03.Student@175078b
	}
}

3、单例模式之懒汉式:面试时使用

(1)实现单例模式的老师类

package cn.itcast_03;
public class Teacher {
	private Teacher() {  }
	private static Teacher t = null;
	public synchronized static Teacher getTeacher() {
		// 假设有三个线程 t1,t2,t3
		if (t == null) {
		    //t1,t2,t3
		    t = new Teacher();
		}
		return t;
	}
}

(2)测试该单例模式

package cn.itcast_03;
public class TeacherDemo {
	public static void main(String[] args) {
		Teacher t1 = Teacher.getTeacher();
		Teacher t2 = Teacher.getTeacher();
		System.out.println(t1 == t2);
		System.out.println(t1); // cn.itcast_03.Teacher@175078b
		System.out.println(t2);// cn.itcast_03.Teacher@175078b
	}
}

4、单例模式的面试题

 * 单例模式:
 *         饿汉式:类一加载就创建对象
 *         懒汉式:用的时候,才去创建对象
 * 面试题:单例模式的思想是什么?请写一个代码体现?
 *         开发:饿汉式(是不会出问题的单例模式)
 *         面试:懒汉式(可能会出问题的单例模式)
 *                   A:懒加载(延迟加载)  :不会出问题 
 *                   B:线程安全问题:可能会出问题—— 怎么解决呢?加 同步操作符:synchronize
 *                       a:是否多线程环境    是
 *                       b:是否有共享数据    是
 *                       c:是否有多条语句操作共享数据  是

 

==============================================================

==============================================================

1、单例模式的概述:

Java中单例模式(Singleton)是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。

2、单例模式的好处:

  1. 它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;
  2. 它能够避免由于操作多个实例导致的逻辑错误;
  3. 如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式值得考虑。

3、单例1——饿汉模式(推荐)

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){
    }
    public static Singleton getInstance(){
        return instance;
    }

(1)从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类;然后提供了一个静态实例,并返回给调用者。饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对创建实例,实例在整个程序周期都存在。

(2)优点:只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。

(3)缺点:即使这个类的实例没有用到也会被创建,而且在类加载之后就被创建,内存被浪费了。

(4)使用场景:这种实现方式适合类的实例占用内存比较小、在初始化时就会被用到的情况。但是,如果单例占用的内存比较大、或者单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

4、单例2——懒汉模式(未加锁)

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

(1)从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类;然后提供了一个静态实例,并返回给调用者。懒汉模式也是比较简单的一种实现方式。

(2)优点:懒汉模式中,单例是在需要的时候才去创建,如果单例已经创建,再次调用这个获取接口getInstance()将不会重新创建新的对象,而是直接返回之前已经创建好的对象。

(3)适用场景:如果某个单例使用的次数较少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。

(4)缺点:这里的懒汉模式并没有考虑线程安全问题,在多线程环境中,多个线程可能会并发调用它的getInstance()方法,从而导致同时创建多个实例,因此需要加锁来解决线程同步问题,见5。

5、单例3——懒汉模式(加锁、双重校验锁、volatile)(推荐)

未加锁的懒汉模式单例的缺点前面已经叙述了,即存在线程安全问题。为了避免在多线程环境中多个线程同时创建多个实例,可以使用加锁来解决,代码如下所示:

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){
    }
    public static synchronized Singleton getInstance(){
        //Single Checked
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

(1)加锁的懒汉模式,看起来既解决了线程安全问题,又实现了延迟加载,然而,它存在着性能问题,依然不够完美。

(2)synchronized关键字是一个重量级锁,synchronized修饰的同步方法比一般方法要慢很多。如果多次调用该类的getInstance()方法,累积的性能损耗就比较大了。因此,就有了双重校验锁,先看下它的代码实现:

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance() {
        //Single Checked
        if (instance == null) {
            synchronized (Singleton.class) {
                //Double checked
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(1)从双重校验锁代码可以看到,在同步代码块的外面多了一层单例对象是否为空的判断if (instance == null)。由于单例对象只需要创建一次,如果后面再次调用getInstance()方法只需要直接返回单例对象即可。因此,在大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。

(2)不过还需要考虑一种情况:假如两个线程A和B,线程A执行了if (instance == null)语句,它会认为单例对象没有创建;此时,调度器切到了线程B,线程B也执行到了同样的语句if (instance == null),线程B也认为单例对象没有创建;然后,两个线程依次执行同步代码块,并且分别创建了一个单例对象,此时会出现问题。为了解决这个问题,还需要在同步代码块中再增加一个单例对象是否为空的判断if (instance == null),也就是上面看到的代码中的double checked。

(3)可以看到,双重校验锁既实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?双重校验锁的隐患:指令重排优化导致的双重校验锁失效。

  • 这里要提到Java中的指令重排优化。所谓的指令重排优化,就是指在不改变原语义的情况下,通过调整指令的执行顺序让java程序运行的更快。但是,JVM中并没有规定编译器优化相关的具体内容,也就是说,JVM可以根据实际情况,自由的进行指令重排序优化。
  • 双重校验锁的隐患,其关键就在于:由于指令重排优化的存在,导致初始化Singleton对象和将对象地址赋给instance字段的顺序是不确定的。比如:某个线程创建单例对象时,在构造方法被调用之前,就为该单例对象分配了内存空间并将该单例对象的属性设置为默认值,此时,就可以将分配的内存地址赋值给instance字段了,然而,该对象可能还没有被初始化。若紧接着另外一个线程来调用getInstance(),取到的就是状态不正确的单例对象,程序就会出错。

(4)以上就是双重校验锁会失效的原因,不过还好,在JDK1.5及之后的版本增加了volatile关键字。volatile的一个语义就是:禁止指令重排序优化,也就保证了instance变量被赋值的时候类的单例对象已经初始化好了,从而避免了上面说到的问题。

PS:Java中的volatile关键字是什么?

  1. 理解volatile关键字作用的前提,是要理解Java内存模型,volatile关键字的作用主要有两个:(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。(2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能,JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。
  2. 从实践角度而言,volatile的一个重要作用就是和CAS(compare and swap)结合,保证了变量的原子性。详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。(1)CAS,语义是比较和替换,它是设计并发算法时用到的一种技术。(2)简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。

(1)volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程环境中,对volatile成员变量的操作对其它线程是透明的。(2)volatile变量可以保证下一个读取操作会在上一个写操作之后发生(参考: http://blog.csdn.net/fly910905/article/details/79283557)。

基于“双重校验锁实现的懒汉式单例”有可能会出现指令重排导致锁失效的背景,下面给出了解决方案:类的静态属性“单例对象”加上volatile关键字(禁止指令重排优化)。

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance() {
        //Single Checked
        if (instance == null) {
            synchronized (Singleton.class) {
                //Double checked
                if (instance == null) {
                    instance = new Singleton();
                }
 
            }
        }
        return instance;
    }
}

6、单例4——静态内部类模式(推荐)

除了上面三种方式,还有另外一种实现单例的方式,通过静态内部类来实现。首先看一下它的实现代码:

public class Singleton{
    private static class SingletonHolder{
        public static Singleton instance = new Singleton();
    }
    private Singleton(){
    }
    public static Singleton newInstance(){
        return SingletonHolder.instance;
    }
}

这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建单例对象。这样的话,只要应用程序中不使用这个单例对象,JVM就不会去加载这个单例对象,也就不会创建单例对象,从而实现懒汉式的延迟加载,也就是说这种方式可以同时保证延迟加载和线程安全。

7、单例5——枚举模式(推荐)


public class EnumSingleton{
    private EnumSingleton(){
    }
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private static enum Singleton{
        INSTANCE;
        private EnumSingleton singleton;
        //JVM会保证此方法绝对只调用一次
        private Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
    public static void main(String[] args) {
        EnumSingleton obj1 = EnumSingleton.getInstance();
        EnumSingleton obj2 = EnumSingleton.getInstance();
        //输出结果:obj1==obj2?true
        System.out.println("obj1==obj2?" + (obj1==obj2));
    }
}
  • 上面的类EnumSingleton是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。 
  • 获取资源的方式很简单,只要 Singleton.INSTANCE.getInstance() 即可获得所要实例。
  • 下面我们来看看单例是如何被保证的: 
    • 首先,在枚举中,我们明确了构造方法限制为私有,在我们访问枚举实例的时候会执行这个构造方法。
    • 同时,每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 也就是说,因为JVM会保证Enum中的实例只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。 

最开始提到的四种实现单例的方式都有共同的缺点:

(1)需要额外的工作来实现序列化,否则,每次反序列化一个序列化的对象时都会创建一个新的实例。

(2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举模式的单例模式,除了线程安全和防止反射调用私有构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象,强烈推荐!最后借用 《Effective Java》一书中的话:单元素的枚举类型已经成为实现Singleton的最佳方法!

 

8、单例模式的线程安全性

首先要说的是,单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建出来一次。

单例模式有很多种的写法,我总结一下:

(1)饿汉式:线程安全

(2)懒汉式:非线程安全

(3)双检锁:线程安全

(4)静态内部类:线程安全

(5)枚举:线程安全

 

参考来源:

(1)http://www.importnew.com/12773.html

(2)http://blog.csdn.net/goodlixueyong/article/details/51935526

(3)https://blog.csdn.net/fly910905/article/details/79286680

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值