这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
单例模式是设计模式之一,同时也是Java面试中的高频考点,这一篇来总结一下Java单例模式的各种写法。
单例模式介绍
单例模式的作用:
- 节省内存和计算,实例只创建一次,不必重复创建
- 保证结果正确,比如单例计数器,用作多线程的数据统计
- 方便管理,例如日期工具类,字符串工具类,不需要创建那么多的实例
单例模式适用场景:
- 无状态的工具类:比如日志工具类,不管是在哪里适用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
- 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
单例模式的8种写法
饿汉式——静态常量(可用)
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到lazy loading的效果,如果从始至终都未使用过这个实例,则会造成内存的浪费
饿汉式——静态代码块(可用)
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
这种方法和上面的方法类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
懒汉式——线程不安全(不可用)
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
这种写法起到了lazy loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if(singleton==null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会多次创建实例。所以在多线程环境下不可使用这种方式。
懒汉式——线程安全,同步方法(不推荐用)
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
解决上面第三种方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()
方法进行了线程同步。
缺点:效率太低了,每个线程在想获得类的实例的时候,执行getInstance()
方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
懒汉式——线程不安全,同步代码块(不推荐)
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,加入一个线程进入到了if(singleton==null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这是便会产生多个实例。
双重检查(推荐面试用)
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
优点:线程安全,延迟加载,效率较高
双重检查的好处:保证线程安全和性能
volatile作用:创建对象不是原子操作,要防止重排序
静态内部类(可用)
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
静态内部类在Singleton类被装载时不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程无法进入。
优点:避免了线程不安全,延迟加载,效率高。
枚举(推荐用)
public enum Singleton8 {
INSTANCE;
public void getInstance() {
}
}
不仅能避免多线程同步问题,还是懒加载,而且还能防止反序列化重新创建新的对象。
不同写法的对比
- 饿汉:简单,但是没有lazy loading
- 懒汉:有线程安全问题
- 静态内部类:可用
- 双重检查:面试用
- 枚举:最好
各种写法的适用场合
- 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
- 非线程同步的方法不能使用
- 如果程序一开始要加载的资源太多,那么就应该使用懒加载
- 饿汉式如果是对象的创建需要配置文件就不适用
- 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性,大部分情况还是推荐枚举,简单。