单例模式
1.1 何为单例模式
单例模式(Singleton)是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
单例的实现主要是通过以下两个步骤:
1)将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,
只有通过该类提供的静态方法来得到该类的唯一实例;
2)在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,
如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
1.2 单例模式的应用场景
1)需要生成唯一序列的环境
2)需要频繁实例化然后销毁的对象。
3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
4)方便资源相互通信的环境
例1:在windows操作系统中,双击点开回收站后,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口,只会弹出之前打开的回收站。在实际使用中并不存在需要同时打开两个回收站窗口的必要性。如果每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。
例2:网站的计数器,一般也是采用单例模式实现,如果存在多个计数器,每一个用户的访问都刷新计数器的值,这样的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制
1.3 单例模式的优缺点
优点:
1)在内存中只有一个对象,节省内存空间;
2)避免频繁的创建销毁对象,可以提高性能;
3)避免对共享资源的多重占用,简化访问;
4)为整个系统提供一个全局访问点。
缺点:
1)不适用于变化频繁的对象;
2)滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
3)如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
1.4 常见的五种单例模式
– 主要:
• 饿汉式:线程安全,调用效率高,不能延时加载
• 懒汉式:线程安全,调用效率不高,可以延时加载。
– 其他:
• 双重检测锁式:由于JVM底层内部模型原因,偶尔会出问题。不建议使用
• 静态内部类式:线程安全,调用效率高,可以延时加载)
• 枚举单例:线程安全,调用效率高,不能延时加载
1.5 单例模式的实现
1.5.1 懒汉模式
懒汉式单例模式在被外部类调用时创建实例,因类加载速度快,但运行时获取对象的速度慢
实现的三种方式:
1)线程不安全
public class LazySingleton {
// 指向自己实例的私有静态引用
private static LazySingleton instance;
// 私有的构造方法
private LazySingleton() {
}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static LazySingleton getInstance() {
// 被动创建,在真正需要使用时才去创建
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这种方式起到懒加载的效果,但只能在单线程下使用,如果在多线程下,一个线程进入if(instance == null)判断语句块,还没有执行产生实例的语句,另一个线程又进来,这时会产生多个实例,所以不安全。
2)使用synchronized同步,线程安全
public class LazySingleton {
private static LazySingleton intance = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (intance == null) {
intance = new LazySingleton();
}
return intance;
}
}
在方法上使用synchronized锁进行同步,解决了线程不安全的问题,但是这个同步方法影响的区域太大,如果多个对象想获取这个对象的时候会柱塞在这排队,效率太低。
3)使用同步代码块,线程安全
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
同步代码块保证线程安全但是不满足单例,在多线程下依旧会有多个实例
1.5.2 饿汉模式
类加载的方式是按需加载,且加载一次。因此,饿汉式单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;并且由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
public class HungrySingleton {
// 指向自己实例的私有静态引用,主动创建
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 私有的构造方法
private HungrySingleton() {
}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static HungrySingleton getSingleton1() {
return hungrySingleton;
}
}
优点:写法简单,在类装载的时候就完成实例化,避免线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终未使用过这个实例,则会造成内存的浪费。
1.5.3 双重加锁机制
使用双重检测同步延迟加载去创建单例的做法,不但保证了单例,而且提高了程序运行效率
public class Singleton {
private volatile static Singleton doubleLockSingleton;
private Singleton() {
}
public Singleton getInstance() {
//先判断是否存在,不存在再加锁处理
if (null == doubleLockSingleton) {
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
synchronized (Singleton.class) {
if (null == doubleLockSingleton) {
doubleLockSingleton = new Singleton();
}
}
}
return doubleLockSingleton;
}
}
1.5.4 静态初始化
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
1.5.5 枚举(推荐)
枚举的方式是比较少见的一种实现方式,但是代码实现却更简洁清晰。并且自动支持序列化机制,防止多次实例化。
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE;
}
}
具体枚举单例
public class User {
//私有化构造函数
private User() {
}
//定义一个静态枚举类
static enum SingletonEnum {
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private User user;
//私有化枚举的构造函数
private SingletonEnum() {
user = new User();
}
public User getInstnce() {
return user;
}
}
//对外暴露一个获取User对象的静态方法
public static User getInstance() {
return SingletonEnum.INSTANCE.getInstnce();
}
}
public class Test {
public static void main(String[] args) {
System.out.println(User.getInstance());
System.out.println(User.getInstance());
System.out.println(User.getInstance() == User.getInstance());
}
}
结果:
xx.exercise.Sort.User@5b6f7412
xx.exercise.Sort.User@5b6f7412
true
总结:
懒汉式(包含线程安全和线程不安全梁总方式)都比较少用;
饿汉式和双检锁都可以使用,可根据具体情况自主选择;
在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;
若涉及到反序列化创建对象时,可以使用枚举方式。
注:
此文来源于《Java程序员面试笔试宝典》一书
仅作为本人学习过程的记录
填写原创是因为找不到相关链接
如有不妥请联系本人,会立即删除
此书对于我这种小白来说非常有用,讲解详细,知识点全面,目前正在根据此书学习,陆续会记录更多知识点。