一. 什么是单例模式
单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。
其实,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各种设计模式有了初步的了解。致敬!