JAVA中各种单例模式的实现与分析

2021-08-30  本文已影响0人  冬天里的懒喵

单例模式是学习设计模式过程中最基本的一个设计模式,基本上一开始学习就会学到单例模式,实际上在java中实现单例模式有很多种写法,不同写法也会导致不同的问题。
那么究竟哪些写法能用,而哪些写法不能用,或者不同实现方法在什么场景下能使用。本文对现有的9种单例模式的实现方式进行分析。

1.饿汉式单例模式--采用静态常量的方式

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式  采用静态常量的方式实现。
 *   简单实用,线程安全。
 *   唯一缺点是不管用到与否,在类加载的时候都会进行实例化。
*@date  2021/8/30 13:44
*/
public class SingletonDemo1 {
    
    private final static SingletonDemo1 INSTANCE = new SingletonDemo1();
    
    private SingletonDemo1() {}
    
    public static SingletonDemo1 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        SingletonDemo1 singleton1 = SingletonDemo1.getInstance();
        SingletonDemo1 singleton2 = SingletonDemo1.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

此种实现方式的优点在于,写法简单,在类加载的过程种就完成了所需对象的实例化操作,从而避免的线程安全问题。

缺点在于,饿汉式单例模式,无论所需的对象是否被用到,一上来就会先创建这个对象,如果这个对象在整个业务过程中不被用到,那么势必会造成内存的浪费。

2.饿汉式单例模式--采用静态代码块的方式

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式  采用静态代码块的方式实现。
 *   实际上等价于静态常量的方式实现,都是在类加载过程中就实现了目标对象的实例化。两者是等价的 ,优缺点也一致。
 *   简单实用,线程安全。
 *   唯一缺点是不管用到与否,在类加载的时候都会进行实例化。
*@date  2021/8/30 13:55
*/
public class SingletonDemo2 {

    private final static SingletonDemo2 INSTANCE;
    
    static {
        INSTANCE = new SingletonDemo2();
    }

    private SingletonDemo2() {}

    public static SingletonDemo2 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        SingletonDemo2 singleton1 = SingletonDemo2.getInstance();
        SingletonDemo2 singleton2 = SingletonDemo2.getInstance();
        System.out.println(singleton1 == singleton2);
    }
}

采用静态代码块的方式实现的单例模式与静态常量的方式实现的单例模式实际上是等价的,都是在类加载的过程中就实现了目标对象的实例化。从而避免了线程安全问题。

其缺点也与静态常量的饿汉模式一致,可能会造成内存的浪费。

3.懒汉式单例模式--基本实现

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--基本实现
 * 懒汉式单例模式虽然能起到懒加载的效果,达到节约内存空间的目的。
 * 但是在多线程的条件下,如果一个线程进入了if判断,还没有执行,而另外一个线程也进入if判断。
 * 此时并会导致返回多个实例。因此这种方式在生产环境是不可取的。
 * 在getInstance方法中,添加了sleep时间,通过main方法中多线程执行效果就会非常明显,可以发现这样会导致每次输出的hashcode都不相同。
 * 
*@date  2021/8/30 14:04
*/
public class SingletonDemo3 {
    
    private static SingletonDemo3 INSTANCE;

    public SingletonDemo3() {
    }

    public static SingletonDemo3 getInstance() {
        if(INSTANCE == null) {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new SingletonDemo3();
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0,100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo3.getInstance().hashCode());
            }).start();
        });
    }
}

最基本的懒汉式单例模式如上所示。我们只需要在getInstance方法中进行判断,if(INSTANCE == null) ,如果为True,则说明对象未被实例化,现在直接进行实例化即可。
但是这种方式会引入线程安全的问题,在多线程的环境下,如果一个线程进入了if判断,还没有执行完成,而另外一个线程也进入if判断。此时并会导致返回多个实例。
因此这种方式在生产环境是不可取的。在getInstance方法中,专门添加了sleep时间,通过main方法中多线程执行效果就会非常明显,可以发现这样会导致每次输出的hashcode都不相同。

最终结论:此种实现方式虽然会降低不必要的内存开销,但是会导致线程安全问题,在并发情况下可能每次调用都创建一个新的实例,因此这种方法是不推荐的。

4.懒汉式单例模式--在方法上加锁

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--方法上加锁
 * 考虑到基本的懒汉式单利模式的线程安全的问题,最简单粗暴的方式就是在getInstance方法上加锁。
 * 这种方式能解决线程安全的问题,但是,在方法上粗暴的使用synchronized,将并行的方式直接变成了串行化。极大的降低了效率。
 * 用在生产系统中该方法将成为系统的瓶颈所在,因此这种方式虽然可用,但是并不推荐使用。
 * 
*@date  2021/8/30 14:33
*/
public class SingletonDemo4 {

    private static SingletonDemo4 INSTANCE;

    public SingletonDemo4() {
    }

    public static synchronized SingletonDemo4 getInstance() {
        if(INSTANCE == null) {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new SingletonDemo4();
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0,100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo4.getInstance().hashCode());
            }).start();
        });
    }
}

通过在getInstance方法上加synchronized来实现的懒汉式单利模式。经过测试,这种写法能避免线程安全的问题,在mian函数中进行测试,全部的hashcode都相同。
但是这种写法的问题在于,直接将synchronized加锁在getInstance方法上,这样会导致,如果并行的请求getInstance方法,将不得不变成串行化操作。
这样在并发场景中使用将极大的影响系统的性能。因此虽然这种方式能实现单例模式,但是并不推荐在生产环境中来使用。

5.懒汉式单例模式--在方法内部加同步代码块

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--在方法中加同步代码块
 * 既然将synchronized加锁到getInstance方法中,这样会导致效率的下降,那么我们可以尝试将锁细化,将synchronized加锁在if判断之后。
 * 但是经过实验,我们发现,这种方式会带来线程安全的问题。
 * 当一个线程进入了if判断,还没执行同步块中的代码,此时另外一个线程也进入了if判断区域,那么只要if判断通过,虽然后面有synchronized保护,
 * 这也只能将这两个线程在new对象的过程中变成了顺序操作,从根本上来说,还是创建了两个实例。我们通过main函数执行可以很好的验证这一点。
 *
*@date  2021/8/30 14:47
*/
public class SingletonDemo5 {
    
    private static SingletonDemo5 INSTANCE;

    public SingletonDemo5() {
    }

    public static SingletonDemo5 getInstance() {
        if (INSTANCE == null) {
            synchronized(SingletonDemo5.class) {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new SingletonDemo5();
            }
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0, 100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo5.getInstance().hashCode());
            }).start();
        });
    }
}

考虑到将synchronized加锁在getInstance方法可能带来效率问题,因此,我们可以进一步尝试锁的细化。将synchronized的同步代码块加在if判断内部。
实验结果证明,这种方式不仅不会对效率有帮助,还导致线程的同步问题,每次输出的hashcode也都不一样了。导致创建了多个目标对象。
这是因为,如果有一个线程已经执行完了if判断,之后虽然进入了同步块,但是还没执行完成,Instance还是空的。那么此时再有另外一个线程执行getInstance.
那么if判断会判断其通过,从而执行其内部的同步代码块。这样虽然加锁导致了串行化,但是实例的对象还是会被创建多次。
因此,此种方法不是一个可用的单例模式的实现方式。我们在生产环境中不推荐使用。

6.懒汉式单例模式--Double Check

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--同步块double check
 * 考虑到同步代码块会存在线程安全问题,这个问题都是if判断引起的,那么一种解决方法就是在同步代码块中增加double check ,既实现双重判定检查。
 * 经过验证,这种方式能在大多数情况下都能很好的实现单例模式,执行main函数,基本上hashcode都相同。
 * 但是还是会在少数情况下,出现多个实例的问题。我们可以思考一下这个问题,这个问题正是jvm的可见性造成的。
 * 前面我们的判断都是线程还在执行中,没有对INSTANCE进行赋值,后续的线程就要进入if判断了,因此会造成目标对象被初始化多次,
 * 那么我们假设,如果第一个线程已经执行完了对INSTANCE的赋值,加锁结束,此时恰好有一个线程已经进入了第一个if判断,正在等待锁。
 * 拿到锁之后,进入第二个if判断,但是由于可见性问题,此时第二个线程还不能看到线程一的值已经被写入完毕。误以为还是空,因此再次实现一次实例化。
 * 
*@date  2021/8/30 15:02
*/
public class SingletonDemo6 {

    private static SingletonDemo6 INSTANCE;

    public SingletonDemo6() {
    }

    public static SingletonDemo6 getInstance() {
        if (INSTANCE == null) {
            synchronized(SingletonDemo6.class) {
                if(INSTANCE == null) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new SingletonDemo6();
                }
            }
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0, 100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo6.getInstance().hashCode());
            }).start();
        });
    }
}

考虑到同步代码块会存在线程安全问题,这个问题都是if判断引起的,那么一种解决方法就是在同步代码块中增加double check ,既实现双重判定检查。
经过验证,这种方式能在大多数情况下都能很好的实现单例模式,执行main函数,基本上hashcode都相同。
但是还是会在少数情况下,出现多个实例的问题。我们可以思考一下这个问题,这个问题正是jvm的可见性造成的。
前面我们的判断都是线程还在执行中,没有对INSTANCE进行赋值,后续的线程就要进入if判断了,因此会造成目标对象被初始化多次,
那么我们假设,如果第一个线程已经执行完了对INSTANCE的赋值,加锁结束,此时恰好有一个线程已经进入了第一个if判断,正在等待锁。
拿到锁之后,进入第二个if判断,但是由于可见性问题,此时第二个线程还不能看到线程一的值已经被写入完毕。误以为还是空,因此再次实现一次实例化。

7.懒汉式单例模式--Double Check + volatile

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--同步块double check + volatile
 * 考虑到Double check实现的单例模式存在可见性问题,我们可以通过在INSTANCE上加上volatile来实现。
 * 这样就能避免在前面DoubleCheck实现的单例模式里的问题,由于INSTANCE具备了可见性,此时再通过DoubleCheck的方式来实现,就不会出现目标对象实例化多次的问题。
 *  但是这种方式还是存在一个问题就是,如果我们无法避免反序列化的问题。通过反序列化,仍然可以将这个类实例化多次。
*@date  2021/8/30 15:02
*/
public class SingletonDemo7 {

    private static volatile SingletonDemo7 INSTANCE;

    public SingletonDemo7() {
    }

    public static SingletonDemo7 getInstance() {
        if (INSTANCE == null) {
            synchronized(SingletonDemo7.class) {
                if(INSTANCE == null) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new SingletonDemo7();
                }
            }
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0, 100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo7.getInstance().hashCode());
            }).start();
        });
    }
}

为了解决DoubleCheck创建的单例模式中的可见性问题,我们在INSTANCE上增加了volatile,通过happens-before 原则,避免指令重排序,保障了INSTANCE的可见性。
这样在生产环境中,如果我们不考虑反序列化方式可以将这个类创造多个实例之外,这种方式是目前我们在上述所有当例模式的最优写法。

不过需要注意的是,如果通过反序列化,或者反射,那么可能就可能绕开DoubleCheck,造成目标对象被实例化多次。

8.懒汉式单利模式--静态内部类

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author dhaibo1986@live.cn
*@description 懒汉式单例模式--静态内部类
 * 结合饿汉模式的优点,既然饿汉模式可以完美而又简单的实现单例模式,而且还能保证线程安全。那么可以参考饿汉模式,结合懒汉模式懒加载的优点。
 * 在其内部建立一个静态的内部类,这个类只有调用getInstance的时候才会被加载,而利用classLoader,从而保证只有一个实例会被实例化。
 * 这种实现方式同样是不能防止反序列化的。如果要解决这个问题,可以通过Serializable、transient、readResolve()实现序列化来解决。
*@date  2021/8/30 15:02
*/
public class SingletonDemo8 {

    public SingletonDemo8() {
    }

    private static class SingletonDemo8Holder{
        private static final SingletonDemo8 INSTANCE = new SingletonDemo8();
    }
    
    public static SingletonDemo8 getInstance() {
        return SingletonDemo8Holder.INSTANCE;
    }


    public static void main(String[] args) {
        IntStream.range(0, 100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo8.getInstance().hashCode());
            }).start();
        });
    }
}

结合饿汉模式的优点,既然饿汉模式可以完美而又简单的实现单例模式,而且还能保证线程安全。那么可以参考饿汉模式,结合懒汉模式懒加载的优点。
在其内部建立一个静态的内部类,这个类只有调用getInstance的时候才会被加载,而利用classLoader,从而保证只有一个实例会被实例化。
这种实现方式同样是不能防止反序列化的。如果要解决这个问题,可以通过Serializable、transient、readResolve()实现序列化来解决。

9.懒汉式单利模式--利用枚举

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.stream.IntStream;

/**
 * @author dhaibo1986@live.cn
 * @description 懒汉式单例模式--枚举
 * 在《effective java》中还有一种更简单的写法,那就是枚举。也是《effective java》作者最为推崇的方法。
 * 这种方法不仅可以解决线程同步问题,还可以防止反序列化。
 * 枚举类由于没有构造方法(枚举是java中约定的特殊格式,因此不需要构造函数。),因此不能够根据class反序列化之后实例化。因此这种写法是最完美的单例模式。
 * 
 * @date 2021/8/30 15:02
 */
public class SingletonDemo9 {

    public SingletonDemo9() {
    }

    public static SingletonDemo9 getInstance() {
        return Sigleton.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        IntStream.range(0, 100).forEach(i -> {
            new Thread(() -> {
                System.out.println(SingletonDemo9.getInstance().hashCode());
            }).start();
        });
    }


    private enum Sigleton {
        INSTANCE;

        private final SingletonDemo9 instance;

        Sigleton() {
            instance = new SingletonDemo9();
        }

        public SingletonDemo9 getInstance() {
            return instance;
        }
    }
}

在《effective java》中还有一种更简单的写法,那就是枚举。也是《effective java》作者最为推崇的方法。
这种方法不仅可以解决线程同步问题,还可以防止反序列化。
枚举类由于没有构造方法(枚举是java中约定的特殊格式,因此不需要构造函数。),因此不能够根据class反序列化之后实例化。因此这种写法是最完美的单例模式。

上一篇下一篇

猜你喜欢

热点阅读