java的23种设计模式之——单例模式

一. 什么是单例模式

单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。

其实,GoF对单例模式的定义是:保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

注:《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)"。

二. 为什么要使用单例模式呢?

在应用系统开发中,我们常常有以下需求:

1.在多个线程之间,比如servlet环境,共享同一个资源或者操作同一个对象。

2.在整个程序空间使用全局变量,共享资源。

3.大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。

因为Singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,Singleton模式就派上用场了。

三. 单例模式实现

1. 恶汉式

2. 懒汉式

3. 双重检查

下面看以下程序代码:

package com.susu.singeton;
public class Person {
	private String name;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
}

主类中进行调用:

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {

		Person person = new Person();
		person.setName("吉xx");
		Person person2 = new Person();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());

        /**
		 * 控制台输出:
		 * 吉xx
                 * su
		 */
	}
}

由程序执行结果,可以看出通过Person类,生成了两个不同的person对象。

那么,如何保证只生成一个对象,我们接下来对以上Person类进行改动。首先将Person类的构造函数私有化,那么就不能再new一个对象,此刻如何获得对象呢?接下来提供一个全局的静态方法,见以下代码。

package com.susu.singeton;
public class Person {
	private String name;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	//构造函数私有化
	private Person() {
	}
	
	//提供一个全局的静态方法
	public static Person getPerson() {
		return new Person();  //思考:直接返回一个对象,能否起到期望的单例效果。答案是不可以!!
	}
}

在主类中运行以下程序,同样产生person两个不同的对象。

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {

		//通过类调用静态方法。返回一个person对象。
		Person person = Person.getPerson();
		person.setName("吉xx");
		Person person2  = Person.getPerson();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());
		
		/**
		 * 控制台输出:
		 * 吉xx
                 * su
		 */
	}
}

那么为了保证只生成一个对象,我们可以使用到上面所提到的“饿汉式”方法。定义一个静态的常量person, 只有一份,不能改变(指new Person()这个引用不可以改变,引用中的数据还是可以改变的),在全局静态方法中return person常量。此时代码如下,实现了单例模式。

package com.susu.singeton;
public class Person {
	public static final Person person = new Person();
	private String name;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	

	//构造函数私有化
	private Person() {
	}
	
	//提供一个全局的静态方法
	public static Person getPerson() {
		return person;
	}
}

主类中进行调用:

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {

		Person person = Person.getPerson();
		person.setName("吉xx");
		Person person2  = Person.getPerson();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());
		
		/**
		 * 控制台输出:
		 * su
          su

		 */
		
	}
}

那么什么是懒汉式呢,我们重新添加一个Person2类。

可以声明一个静态的person,并且在全局的静态方法中增加一个判断,如果不增加判断,会有多个person对象。

package com.susu.singeton;
public class Person2 {
	private String name;
	private static Person2 person;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	//构造函数私有化
	private Person2() {
	}
	
	//提供一个全局的静态方法
	public static Person2 getPerson() {
		if(person == null) {  //person的初值为null
			person = new Person2();
		}
		return person;
	}
}

主类中进行调用

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {
       
		//Person2测试类
		Person2 person = Person2.getPerson();
		person.setName("吉xx");
		Person2 person2  = Person2.getPerson();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());
		
		/**
		 * 控制台输出:
		 * su
          su

		 */
		
	}
}

比较饿汉式和懒汉式:饿汉式在类加载的时候就会初始化,赋值,保证了只有一个对象,保证了单例。而懒汉式只有在单线程的时候会保证只有一个对象,在多线程中却不能保证只有一个对象。比如说,当第一个线程进入,判断初始对象为空,于是new一个对象(需要花费一定时间),在这个时间段,第二个线程也进入了,第一个初始化对象还没有完成,同样判断初始对象为null,也会new一个对象返回,这两个返回的对象不再是一个对象了。

为了解决这个问题,需要使用同步方法。新建一个Person3, 仅仅在Person2类中修改静态全局方法即可。当第一个进程进入,独占此方法,初始化完成之后,返回该对象。等到方法结束之后,第二个进程才可以进来。相比于Person2,解决了多线程问题。

package com.susu.singeton;
public class Person3 {
	private String name;
	private static Person3 person;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	//构造函数私有化
	private Person3() {
	}
	
	//提供一个全局的静态方法,使用同步方法
	public static synchronized Person3 getPerson() {
		if(person == null) {
			person = new Person3();
		}
		return person;
	}
}

主类进行调用:

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {
		//Person3测试类
		Person3 person = Person3.getPerson();
		person.setName("吉xx");
		Person3 person2  = Person3.getPerson();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());
		
		/**
		 * 控制台输出:
		 * su
          su
		 */
	}
}

下面我们讨论什么是双重检查。通过分析我们上一步的代码,实际上person = new Person()只执行一次,只在第一次的时候,由同步问题,只生成一个对象,第二次、三次都是直接返回person对象。当第一次第一个进程独占一个方法,并且返回person对象时,第二个、三个、四个进程也要排队等待,执行if(person==null)这一判断,影响程序的效率。针对接下来到达的进程,实际上if判断并不要求同步。总体来说,整个方法不需要同步,只在需要同步的地方进行同步。接下来我们可对这一问题进行修改。新建一个Person4类

//提供一个全局的静态方法
    public static Person4 getPerson() {
        if(person == null) {
            synchronized (Person4.class) {
                    person = new Person4();
                }
            }
        }
        return person;

以上代码实际上只做了一次检查,是有问题的。如果两个进程同时到达,都会进入if语句,1进程把持着同步语句,new一个对象,然后返回对象,离开。2进程也会new一个对象,返回对象,离开。此时如果有第三个进程到达,因为已经存在对象,所以直接返回。针对1、2进程可能会出现的问题,因此我们需要在同步中再增加一次检查。第二个if判断针对第一次使用,第一个if判断针对以后使用。

请看双重检查的最终代码。

package com.susu.singeton;
public class Person4 {
	private String name;
	private static Person4 person;
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	//构造函数私有化
	private Person4() {
	}
	
	//提供一个全局的静态方法
	public static Person4 getPerson() {
		if(person == null) {
			synchronized (Person4.class) {
				if(person == null) {
					person = new Person4();
				}
			}
			
		}
		return person;
	}
}

主类中进行调用:

package com.susu.singeton;
public class MainClass {
	public static void main(String[] args) {
		//Person4测试类
		Person4 person = Person4.getPerson();
		person.setName("吉xx");
		Person4 person2  = Person4.getPerson();
		person2.setName("su");
		System.out.println(person.getName());
		System.out.println(person2.getName());

		/**
		 * 控制台输出:
		 * su
             su
		 */
		
	}
}

相比于Person3,Person4中new对象只执行一次,实际上代码效率高了很多。

其中,Person类是饿汉式,Person2,Person3,Person4类属于懒汉式,双重检查属于懒汉式的一种,只是对懒汉式的一个改进。相对懒汉式,饿汉式代码编写比较简单,对多线程也是安全的。

                                               饿汉式PK懒汉式
饿汉式 懒汉式

1.在类加载时就创建实例,第一次加载速度快;

2.空间换时间;

3.线程安全;

 

1.第一次使用时才进行实例化,第一次加载速度慢;

2.时间换空间;

3.存在线程风险; 

  解决方案:同步锁;双重校验锁;静态内部类;枚举(后续慢慢了解)

   

 

                                  单例模式的优缺点与适用场景
优点 1.在内存中只有一个对象,节省内存空间
2.避免频繁的创建销毁对象,提高性能
3.避免对共享资源的多重占用。
缺点 1.扩展比较困难
2.如果实例化后的对象长期不利用,系统将默认为垃圾进行回收,造成对象状态丢失
适用场景 1.创建对象时占用资源过多,但同时又需要用到该类对象
2.对系统内资源要求统一读写,如读写配置信息
3.当多个实例存在可能引起程序逻辑错误,如号码生成器

最后,我们再回头看单例模式的定义:保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。满足这样要求的就是单例模式。

注:本文参照了北风学习在线的一名老师所讲解的有关单例模式的视频,老师讲的非常好,让我对java各种设计模式有了初步的了解。致敬!

你可能感兴趣的:(java)