单例模式
1、定义
单例模式 确保一个类只有一个实例,并提供一个全局访问点。
单例模式的定义很好理解,单例模式能够确保一个类在任何时候只有一个实例,并且由类自己管理这个单独的实例,避免其它类产生实例,如果需要访问这个实例,可以通过类提供的全局访问点获取。下面我们就介绍一下单例模式的几种实现方式。
2、单例模式的实现方式
2.1、一种线程不安全的实现方式
我们先来看一种实现方式:
public class Singleton1 {
private static Singleton1 singleton;
private Singleton1() {}
public static Singleton1 getInstance() {
if(singleton == null) {
singleton = new Singleton1();
}
return singleton;
}
public void print() {
System.out.println("单例1被创建出来了");
}
}
这一种实现方式很好理解,我们分几步介绍一下:
- 首先我们声明一个static属性,利用static属性来保证实例的唯一性,但是值得注意的是,这里的static属性是私有的,这样一来这个属性就不会被外部类所引用,只在Singleton1类内部才可以使用。
- 接着,我们声明构造函数,在这里构造函数也是私有的,这样外部也就不能随意调用Singleton1的构造函数了,只有在类内部才能够调用,这样可以保证实例不会被随意地创建。
- 最后我们声明了一个静态方法用于获取实例,在这个方法中,我们先判断实例是否为null,如果是null,说明实例还未被创建,则创建这个实例并将其赋值给之前定义的static属性,如果不为null,则说明实例已经被创建出来了,直接返回static属性即可。
在这种实现方式中,我们在需要用到这个实例的时候才会去创建一个实例,如果我们永远用不到这个实例,那么它就永远不会产生,这就是延迟实例化。
我们可以思考一下这种实现方式是否存在问题?在多线程的情况下,这种单例的实现方式是线程不安全的,很可能会出现多个实例的情况,这与单例模式的设计初衷是不相符的,所以我们是否有一种线程安全的实现方式呢?
2.2、一种线程安全的实现方式
要想实现线程安全,一种很常用的方式就是使用synchronized
关键字,所以我们可以修改一下上面的代码:
public class Singleton2 {
private static Singleton2 singleton;
private Singleton2() {}
public static synchronized Singleton2 getInstance() {
if(singleton == null) {
singleton = new Singleton2();
}
return singleton;
}
public void print() {
System.out.println("单例2被创建出来了");
}
}
我们在getInstance()方法上加上synchronized关键字,这样就使每个线程在进入这个方法之前必须等待别的线程离开这个方法,这就保证了不可能存在两个线程同时进入此方法的情况。
这种方式确实解决了线程安全的问题,但是是否存在其他的问题呢?因为在getInstance()方法上使用了synchronized关键字,所以每个线程必须等待其他线程执行完,很明显这会降低性能,而且我们只有在第一次执行这个方法的时候需要用到同步,一旦我们创建好了static属性,就不需要使用同步了,但是实际上之后每次调用仍然需要同步,这反而拖垮了性能,有没有一种实现方式能够检查属性是否已经创建,如果没有创建才进行同步?我们接着往下看。
2.3、双重检查加锁实现方式
我们再次修改一下代码:
public class Singleton4 {
private volatile static Singleton4 singleton;
private Singleton4() {}
public static Singleton4 getInstance() {
if(singleton == null) {
synchronized (Singleton4.class) {
if(singleton == null) {
singleton = new Singleton4();
}
}
}
return singleton;
}
public void print() {
System.out.println("单例4被创建出来了");
}
}
在这段代码中,我们使用volatile
关键字修饰static属性,这样确保当singleton变量被初始化成Singleton4实例时,多线程能正确处理它。接着我们修改了getInstance()方法,这一次我们没有使用synchronized关键字修饰方法,而是使用了synchronized代码块,进入方法后首先判断属性是否为空,如果为空的话就进入同步代码块,进入同步代码块后会再检查一次属性是否为空,如果仍然为空才会创建实例。使用双重检查加锁,保证了程序中只存在一个实例,而且这种实现方式只在第一次的时候才彻底执行所有的代码,当实例创建出来后也不会进入同步代码块,大大减少了性能的耗费。但是值得注意的是,这种实现方式只适用于JDK1.5之后的版本。
2.4、不使用延迟实例化的实现方式
上面介绍的几种方式都是使用了延迟实例化,最后我们介绍一种不使用延迟实例化的实现方式,这种方式也能够保证线程安全:
public class Singleton3 {
private static Singleton3 singleton = new Singleton3();
private Singleton3() {}
public static Singleton3 getInstance() {
return singleton;
}
public void print() {
System.out.println("单例3被创建出来了");
}
}
这种方式我们依赖JVM在加载这个类的时候马上创建一个唯一的单例,JVM能够保证在任何线程访问静态属性之前先创建这个实例。这一种实现方式更加简洁,但是它是在加载类的时候就会创建这个实例,如果创建这个实例非常耗费资源,而程序执行过程中一直没有使用到它,那么将会造成很大的浪费。
3、总结
在本章中我们介绍了几种单例模式的实现,并分别介绍了它们的优缺点,在实际使用过程中,我们可以根据自己的实际情况选择合适的单例模式实现方式。