单例模式介绍
单例模式, 是为了确保在整个软件体统中, 某个类对象只有一个实例, 并且该类通常会提供一个对外获取该实例的 public 方法(静态方法).
比如日志, 数据库连接池等对象, 通常需要且只需要一个实例对象, 这就会使用单例模式.
单例模式的八种模式
饿汉式(静态常量)
饿汉式(静态代码块)
懒汉式(线程不安全)
懒汉式(同步方法)
懒汉式(同步代码块)
懒汉式(双重检查)
静态内部类
枚举
下面依次来说明一下:
饿汉式(静态常量)
通常, 我们创建一个对象的方式就是 new, 但是, 当我们考虑只创建一个实例的时候, 就应该禁止外部来通过 new 的方式进行创建. 同时, 由于无法使用 new, 你应该考虑提供一个获取单例对象的方式给别人.
思路
1. 将构造器私有化(防止外部 new, 但是对反射还是有局限)
2. 类的内部创建对象
3. 对外提供一个获取实例静态的 public 方法
代码实现:
- public class Singleton1 {
- public static void main(String[] args) {
- HungrySingleton hungrySingleton = HungrySingleton.getInstance();
- HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
- System.out.println(hungrySingleton == hungrySingleton1);
- }
- }
- class HungrySingleton {
- //1. 私有化构造器
- private HungrySingleton() {
- }
- // 2. 类内部创建对象, 因为步骤 3 是 static 的,
- // 所以实例对象是 static 的
- private final static HungrySingleton instance = new HungrySingleton();
- //3. 对外提供一个获取对象的方法,
- // 因为调用方式的目的就是为了获取对象,
- // 所以该方法应该是 static 的.
- public static HungrySingleton getInstance() {
- return instance;
- }
- }
运行程序显示, 我们的确只创建了一个对象实例.
小结
优点: 代码实现比较简单, 在类加载的时候就完成了实例化, 同时, 该方式能够避免线程安全问题.
缺点: 在类装载的时候就完成实例化, 没有达到 Lazy Loading 的效果. 如果从始至终从未使用过这个实例, 则会造成内存的浪费.
这种方式基于 classloder 机制避免了多线程的同步问题, 不过, instance 在类装载时就实例化, 在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种, 因此不能确定有其他的方式 (或者其他的静态方法) 导致类装载, 这时候初始化 instance 就没有达到 lazy loading 的效果.
总结: 这种单例模式可以使用, 但是可能造成内存的浪费.
饿汉式(静态代码块)
该方式和第一种区别不大, 只是将创建实例放在了静态代码块中.
由于无法使用 new, 你应该考虑提供一个获取单例对象的方式给别人.
思路
1. 将构造器私有化(防止外部 new, 但是对反射还是有局限)
2. 类的内部创建对象(通过静态代码块)
3. 对外提供一个获取实例静态的 public 方法
代码实现:
- public class Singleton2 {
- public static void main(String[] args) {
- HungrySingleton hungrySingleton = HungrySingleton.getInstance();
- HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
- System.out.println(hungrySingleton == hungrySingleton1);
- }
- }
- class HungrySingleton {
- //1. 私有化构造器
- private HungrySingleton() {
- }
- // 2. 类内部创建对象, 因为步骤 3 是 static 的,
- // 所以实例对象是 static 的
- private final static HungrySingleton instance;
- static {
- instance = new HungrySingleton();
- }
- //3. 对外提供一个获取对象的方法,
- // 因为调用方式的目的就是为了获取对象,
- // 所以该方法应该是 static 的.
- public static HungrySingleton getInstance() {
- return instance;
- }
- }
小结
该方式只是将对象的创建放在静态代码块中, 其优点和缺点与第一种方式完全一样.
总结: 这种单例模式可以使用, 但是可能造成内存的浪费.(同第一种)
懒汉式(线程不安全)
该方式的主要思想就是为了改善饿汉式的缺点, 通过懒加载(在使用的时候再去加载), 达到节约内存的目的.
由于无法使用 new, 你应该考虑提供一个获取单例对象的方式给别人.
思路
1. 将构造器私有化(防止外部 new, 但是对反射还是有局限)
2. 类的内部创建对象, 懒加载, 在使用的时候才去加载
3. 对外提供一个获取实例静态的 public 方法
代码实现:
- public class Singleton3 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- class LazySingleton {
- //1. 私有化构造器
- private LazySingleton() {}
- //2. 类的内部声明对象
- private volatile static LazySingleton instance;
- //3. 对外提供获取对象的方法
- public static LazySingleton getInstance() {
- // 判断类是否被初始化
- if (instance == null) {
- // 第一次使用的时候, 创建对象
- instance = new LazySingleton();
- }
- return instance;
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- LazySingleton instance = LazySingleton.getInstance();
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
执行程序后, 发现了问题:
// 运行结果:
线程 Thread-0 开始执行
线程 Thread-1 开始执行
线程 Thread-1 初始化对象 1391273746
线程 Thread-0 初始化对象 547686109
小结
优点: 起到了懒加载的作用, 但是只能在单线程情况下使用.
缺点: 多线程下不安全, 如果一个线程进入到 if 语句中阻滞(还未开始创建对象), 另一线程进入并通过了 if 判断, 则会创建多个实例, 这一点就违背了单例的目的.
结论: 实际情况下, 不要使用这种方式.
懒汉式(线程安全, 同步方法)
思路
同上一中方式一样, 但是为了解决多线程安全问题, 使用同步方法.
代码演示:
- public class Singleton4 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- class LazySingleton {
- //1. 私有化构造器
- private LazySingleton() {}
- //2. 类的内部声明对象
- private volatile static LazySingleton instance;
- //3. 对外提供获取对象的方法
- public synchronized static LazySingleton getInstance() {
- // 判断类是否被初始化
- if (instance == null) {
- // 第一次使用的时候, 创建对象
- instance = new LazySingleton();
- }
- return instance;
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- LazySingleton instance = LazySingleton.getInstance();
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
运行结果如下所示:
线程 Thread-1 开始执行
线程 Thread-0 开始执行
线程 Thread-0 初始化对象 681022576
线程 Thread-1 初始化对象 681022576
小结
优点: 起到了懒加载的效果, 同时, 解决了线程安全问题.
缺点: 效率低下, 每次想要获取对象的时候, 去执行 getInstance()都是通过同步方法. 而且, 初始化对象后, 再次使用的时候, 应该直接 return 这个对象.
总结: 可以在多线程条件下使用, 但是效率低下, 不推荐.
懒汉式(线程安全, 同步代码块)
思路
同样是为了解决多线程安全问题, 不过采用的是同步代码块.
代码实现:
- public class Singleton5 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- class LazySingleton {
- //1. 私有化构造器
- private LazySingleton() {}
- //2. 类的内部声明对象
- private volatile static LazySingleton instance;
- //3. 对外提供获取对象的方法
- public static LazySingleton getInstance() {
- // 判断类是否被初始化
- if (instance == null) {
- // 第一次使用的时候, 创建对象
- synchronized (LazySingleton.class) {
- instance = new LazySingleton();
- }
- }
- return instance;
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- LazySingleton instance = LazySingleton.getInstance();
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
代码看上去没有问题, 那么运行效果如何呢:
// 运行结果:
线程 Thread-1 开始执行
线程 Thread-0 开始执行
线程 Thread-1 初始化对象 1368942806
线程 Thread-0 初始化对象 1187311731
那么, 我们发现, 打脸了, 多线程情况下, 创建了两个对象, 并未达到单例的目的.
小结
不推荐使用这种方式.
懒汉式(线程安全, 双重检查机制)
思路
针对懒汉式的多线程问题, 我们可谓是操碎了心: 同步方法可以解决问题, 但是效率太低了; 同步代码块则根本不能保证多线程安全. 如何能做到 "鱼和熊掌兼得" 呢? 既然同步代码块的效率较好, 那么我们就针对这个方式进行改良: 双重检查机制, 即在 getInstance()内进行两次检查, 第一次通过 if 判断后, 初始化对象之前, 进行同步并再次进行判断. 这样做的目的: 既能解决线程安全问题, 同时避免第二次使用对象的时候还要执行同步的代码.
代码实现:
- public class Singleton6 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- class LazyDoubleCheckSingleton {
- //1. 私有化构造器
- private LazyDoubleCheckSingleton() {}
- //2. 类的内部声明对象
- private volatile static LazyDoubleCheckSingleton instance;
- //3. 对外提供获取对象的方法
- public static LazyDoubleCheckSingleton getInstance() {
- // 判断类是否被初始化
- if (instance == null) {
- // 第一次使用, 通过 if 判断
- // 加锁
- synchronized (LazyDoubleCheckSingleton.class) {
- // 拿到锁后, 初始化对象之前, 再次进行判断
- if (instance == null) {
- instance = new LazyDoubleCheckSingleton();
- }
- }
- }
- return instance;
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
运行结果如下所示:
// 运行结果:
线程 Thread-0 开始执行
线程 Thread-1 开始执行
线程 Thread-1 初始化对象 996963733
线程 Thread-0 初始化对象 996963733
小结
优点:
解决了上一种方式中的线程安全问题, 同时实现了延迟加载的效果, 节约内存;
第二次使用的时候, if 判断为 false, 直接返回创建好的对象, 避免进入同步代码, 提高了效率;
结论: 推荐使用这种方式, 实际工作中也比较常见这种方式.
静态内部类
思路
为了实现多线程情况下安全, 除了手工加锁, 还有别的方式. 现在, 我们采用静态内部类的方式. 这种方式利用了 JVM 加载类的机制来保证只初始化一个对象.
思路同样是私有化构造器, 对外提供静态的公开方法; 不同之处是, 类的创建交给静态内部类来时实现.
代码实现
- public class Singleton7 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- class StaticInnerSingleton {
- // 1. 构造器私有化
- private StaticInnerSingleton() {}
- // 2. 通过静态内部类来初始化对象
- private static class InnerClass {
- private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
- }
- // 3. 对外提供获取对象的方法
- public static StaticInnerSingleton getInstance() {
- return InnerClass.INSTANCE;
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- StaticInnerSingleton instance = StaticInnerSingleton.getInstance();
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
运行结果:
线程 Thread-0 开始执行
线程 Thread-1 开始执行
线程 Thread-0 初始化对象 1326533480
线程 Thread-1 初始化对象 1326533480
OK, 我们发现, 这种方式达到了预期的效果.
小结
优点:
这种静态内部类的方式, 通过类加载机制来保证了初始化实例时只有一个实例.
类的静态属性只有在第一次加载类的时候初始化, 而 JVM 能保证线程安全, 在类的初始化过程中, 只有一个线程能进入并完成初始化.
静态内部类方式实现了懒加载的效果, 这种方式不会在类 StaticInnerSingleton 加载的时候进行初始化, 而是在第一次使用时调用 getInstance()方法初始化, 能够起到节约内次的目的.
该方式的 getInstance()方法, 通过调用静态内部类的静态属性返回实例对象, 避免了每次调用时进行同步, 效率高.
结论: 线程安全, 效率高, 代码实现简单, 推荐使用.
枚举
思路
在静态内部类的方式中, 我们借用了 JVM 的类加载机制来实现了功能, 同样, 还可以借用 Java 的枚举来实现单例模式.
- public class Singleton8 {
- public static void main(String[] args) {
- TestThread testThread = new TestThread();
- Thread thread = new Thread(testThread);
- Thread thread1 = new Thread(testThread);
- thread.start();
- thread1.start();
- }
- }
- enum EnumSingleton {
- INSTANCE;
- public void sayHi() {
- System.out.println("Hi," + INSTANCE);
- }
- }
- class TestThread implements Runnable {
- @Override
- public void run() {
- System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
- try {
- // 为了演示多线程情况
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- EnumSingleton instance = EnumSingleton.INSTANCE;
- System.out.println("线程" + Thread.currentThread().getName() + "初始化对象" + instance.hashCode());
- }
- }
运行结果如下:
线程 Thread-0 开始执行
线程 Thread-1 开始执行
线程 Thread-1 初始化对象 1134798663
线程 Thread-0 初始化对象 1134798663
小结
优点: 这中方式需要在 JDK1.5 以上的版本中使用, 利用枚举来实现单例模式. 不仅能避免多线程同步问题, 而且还能防止反序列化重新创建新的对象. 在《Effective Java》中提到了这种方式, 其作者推荐.
结论: 推荐使用.
来源: https://www.cnblogs.com/JackHou/p/11317284.html