单例设计模式的目的就是让对象只创建一次,比如在 JDK 中的 Runtime 类默认的创建方式就是使用单例模式。同时数据库连接池、线程池等等一些重量级的对象只需要创建一次随处使用即可。

单例设计模式共有 7 种创建的方式。单例的主要特点就是构造方法是私有化的。

第一种:饿汉式(线程安全)

饿汉式的方式就是类在加载之后,就立即创建一个实例对象。它是线程安全的。

public class Singleton {

    public final static Singleton singleton = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return singleton;
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第二种:懒汉式(线程不安全)

懒汉式的方式就是类在加载之后,只有在调用 getInstance 方法时,它才会创建对象。它在单线程下可以保证单例,但是在多线程就无法保证单例的了。

public class Singleton {

    public static Singleton singleton = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第三种:懒汉式,同步方法(线程安全,低效率)

解决在多线程下单例的方法,简单粗暴的在 getInstance 方法加一个同步关键字,虽然可以保证线程的安全,但是效率太低了,因为每个线程调用该方法是都需要等待获得锁。

public class Singleton {

    public static Singleton singleton = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton();s
        }
        return singleton;
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第四种:懒汉式,同步代码块(线程不安全)

虽然加了同步关键字,但是一开始有多个线程访问该对象,当线程中获得的都是 null 时,则在同步代码块中等待获得锁,当第一个线程执行完 new 时,交给第二个,第二个再次 new 时,第一个线程获得的对象就和第二个线程获得的对象就不一样了。

public class Singleton {

    public static Singleton singleton = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第五种:懒汉式,同步代码块(线程安全,效率较高)

使用双重校验就可以解决,只需要在同步代码块中再次判断对象是否已经创建即可。同时为了保证可见性,在共享的变量加一个 volatile 即可。

可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

为什么会出现这种问题呢?

我们知道,Java 线程通信是通过共享内存的方式进行通信的,但是为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。

实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

public class Singleton {

    public static volatile Singleton singleton = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(singleton == null) {
            synchronized (Singleton.class) {
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第六种:静态内部类(线程安全)

使用静态内部类创建单例对象也是一种不错的方式,静态的内部类不会随着外部类的加载而加载,由于类在加载的时候就是线程安全的,所以这里用的是类加载的方式来创建的。

public class Singleton {

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

    private static class SingletonInstance {
        private final static Singleton INSTANCE = new Singleton();
    }

}

class Run {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        // 两个对象是否一样
        System.out.println(singleton == singleton1);
    }
}

第七种:枚举(线程安全)

使用枚举的方式也可以创建单例对象,当被反序列化的时候,第六种就无法保证对象是单例的了。Effective Java 作者推荐使用枚举的方式解决单例模式,但是感觉方式可能是平时最少用到的。

public enum Singleton {

    INSTANCE;
    public void ok() {
        System.out.println("ok");
    }

}

class Main {
    public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
        System.out.println(instance == instance1);
        instance.ok();
    }
}