1. 什么是单例模式
确保一个类只有一个实例,并且提供一个全局访问点。
2. 为什么使用单例模式?
有一些对象其实我们只需要一个。比如线程池、缓存、还有操作系统中的任务管理器等。
以任务管理器为例,若是能打开过个任务管理器,每个任务管理器显示的内容一致,会造成资源的浪费;若多个任务管理器显示的内容不一致,这与实际不符,也会造成用户的困惑。
所以单例模式就是为了解决这些问题。
3. 单例模式的实现?
- 饱汉式
package com.fine.singleton;
//饱汉式
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return instance;
}
}
单例模式首先为了让外界无法创建自己的对象,先将自己的构造器初始化。在类加载时就自己创建了一个对象,外界可以通过一个get方法来获取这个对象。
这个模式的优点是线程安全的,在加载类的过程中就产生了这个对象,无论多少个线程来获取对象,获取的都是这个创建好的对象。
这种模式有一些缺点,如果这个对象在创建过程中需要耗费大量的资源,但是后续过程中又一直没有用到这个对象,那么会造成资源浪费,可以使用饿汉式来解决这个问题
- 饿汉式
package com.fine.singleton;
//饿汉式
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
饿汉式在加载类的时候并不创建对象,而是在需要的时候才进行创建。
优点是避免了对不需要的对象在加载类时创建造成的资源浪费。
缺点是县城不安全的,如果有连个线程同时进入到 if语句中,则会造成实例的重复创建。为了解决这个问题可以做以下改进。
- 线程安全的饿汉式
package com.fine.singleton;
//饿汉式 线程安全
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {}
public synchronized static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
在getInstance中加入了synchronized 可以保证只有一个线程可以进入该方法,所以是线程安全的。
缺点是在第一次调用getInstance方法时就已经创建了对象,在此之后就不需要进行同步了,可以进行直接返回,但是在这个类中以后还会频繁的调用这个同步方法,会造成效率的降低,为了解决这个问题,可以采用双重检查加锁。
- 双重检查加锁
package com.fine.singleton;
//饿汉式
public class Singleton4 {
private static volatile Singleton4 instance;
private Singleton4() {}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized(Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
采用双重加锁,只有当前实例还未创建才会进入同步块,如果此时instance依然是null,才会创建对象。第二次判断是为了保证线程安全,如果两个线程同时进入到第一个判断里面,在其中一个线程创建好对象后会释放锁,另一个线程获得锁,如果此时没有判断,那么这个线程会继续创建对象,第二次判断可以避免这种情况的发生。
- 枚举式
package com.fine.singleton;
public enum Singleton5 {
INSTANCE;
public Singleton5 getInstance(){
return INSTANCE;
}
}
/*编译后
* public final class Singleton5 extends Enum< Singleton5 > {
public static final Singleton5 SINGLETON5;
public static EnumSingleton[] values();
public static EnumSingleton valueOf(String s);
static {};
}
*
* */
为什么使用枚举类来实现单例模式呢?
1: 以上的类如果可以序列化,任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。在对这个对象进行序列化和反序列化,会生成一个和原来不同的对象;
2: 以上的类都可以通过反射来获取私有的构造器来创建新对象。
反射是通过获取构造器,调用Constructor类的newInstance方法创建对象,在这个方法中,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。保证了前后的一致性。
4. 单例模式优缺点
-
优点
(1)由于单例模式在内存中只有一个实例,减少了内存开支。
(2)由于单例模式只生成一个实例,当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。
(3)单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。 -
缺点
(1)由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了“单一职责原则”。