一、单例 Singleton
在设计模式之前引出设计模式的的分类。图片出处https://blog.csdn.net/weixin_40550118/article/details/107209866
1 饿汉式,类加载到内存后,就实例化一个单例,用JVM保证线程安全,不论是否用到都会初始化。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();//static 保证class被load到内存之后只加载一次,final保证此类不允许改变
private Singleton() {};//私有构造保证类不能被外部new出来
public static Singleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Singleton m1 = Singleton.getInstance();
Singleton m2 = Singleton.getInstance();
System.out.println(m1 == m2);
}
}
2.饿汉式的变种方式,同样是static加载方式
public class Singleton {
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
private Singleton() {};
public static Singleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Singleton m1 = Singleton.getInstance();
Singleton m2 = Singleton.getInstance();
System.out.println(m1 == m2);
}
}
3.懒汉式 用到时候才初始化,但多线程情况下会导致创建出来的对象不是同一个对象的问题。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) { //此处不能保证线程安全
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
4. 懒汉式 加锁解决线程安全问题,但是每次都要加锁,性能有损耗。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static synchronized Singleton getInstance() {//简单加锁不可行
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
5.缩小加锁范围,但是还是没能解决多线程问题
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {//多线程情况下可能会有多个线程进行初始化。
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
6.双重校验锁进行判断防止频繁加锁带来性能影响
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
7.静态内部类方式,比第一种方式更完美,只有在调用getInstance方法才会创建Singleton的实例。也是由JVM保证只创建一次。
public class Singleton {
private Singleton() {
}
//Singleton 的构造方法是私有的,但是在内部类可以初始化
private static class SingletonHolder {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;//返回静态内部类初始化的对象
}
}
综上所述,1-6-7方式可用。
以上方式都会被反序列化进行破坏,因为普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
8.Enum方式
public enum Singleton {
INSTANCE;
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Singleton.INSTANCE.hashCode());
}).start();
}
}
}
(以下内容引自https://blog.csdn.net/qq_41486137/article/details/102959453)
Joshua Bloch大神的《Effective Java》书中提到使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。并且没有以上方式的线程安全问题。
因为定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。
通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。
而且,枚举中的各个枚举项同时通过static来定义的。如:
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后代码为:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUMKaTeX parse error: Expected '}', got 'EOF' at end of input: …); ENUMVALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)中介绍过,
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,
而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。
另外,使用枚举也可以避免反射和反序列化破坏。因为在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。
单例模式的优点
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
- 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决