设计模式——01单例模式:Singleton

/**
* 单例:把构造方法设置为私有的,别人(其他类里)new不了,而且全局只有这一个实例,在本类里可以,因为private的性质
* 因为实例定义是final 且 static的,每次调用getSingleton()返回的就是这一个;
* 所以是每次返回的都是同一个实例。这里需要注意的是实例化操作(new Singleton();)在声明其变量时就要操作;
* 否则你放getSingleton()里,那么每次都new一个新的,那就没意义了。
*/

1、饱汉式:

示例1:

package class01;

public class Singleton {
    
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    
    public static Singleton getSingleton() {
        return INSTANCE;
    }
    public void print() {
        System.out.println("this is Singleton");
    }

    public static void main(String[] args) {
        Singleton singleton1 = new Singleton();
        Singleton singleton2 = new Singleton();
        System.out.println(singleton1 == singleton2);

    }

}

输出为:

 false 

解释:

这里把new的操作放在了main中,自然是两个对象,在这里可以new,是因为在同一个类中,参见private和public的属性。

示例2:

package class01;

public class Main {

    public static void main(String[] args) {
        
        //Singleton singleton = new Singleton();//不能直接new
        Singleton singleton = Singleton.getSingleton();
        Singleton singleton2 = Singleton.getSingleton();
        System.out.println(singleton == singleton2);
    }
}

输出:

true

解释:

返回的是同一个对象。

 类加载到内存后,就实例化一个单例,JVM保证线程安全;

 唯一缺点:不管是否用到,类装载时就完成实例化(话说自己不用干嘛要实例化,是吧?)


* 为了解决上面的问题:提出了懒汉式实例化,lazy loading,即用到的时候在new;
* 虽然达到了按需初始化的目的,但是却带来了线程不安全的问题。
* 变量为非final的,有final必须初始化.

package class01;

public class Singleton2 {
    
    private static Singleton2 INSTANCE;
    private Singleton2() {}
    
    public static Singleton2 getSingleton() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }
    public void print() {
        System.out.println("this is Singleton");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Singleton2.getSingleton().hashCode());
            }).start();
        }
    }
}

但是这样就会造成线程不安全。编写如上main方法,来揭示在多线程下得到的实例是不是同一个对象,为了区分明显,我们可以在getSingleton(){}方法内的  if (){} 里写入睡眠语句,如下:

    public static Singleton2 getSingleton() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

运行后,输出:

512585151
945515189
945515189
76872907
76872907
945515189
945515189
945515189
945515189
945515189

即发现hashcode并不完全一致(虽然完全一致并不代表是同一个对象,但是不一致肯定不是同一个对象)。

so...........

为了改善线程不安全,我们可以使用synchronized来给方法加锁。
但是这样就效率低下了。

    public static synchronized Singleton2 getSingleton() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

为了改进这个问题,我们可以使用代码块加锁的方式,只对方法里不安全的代码进行加锁。

 1     public static Singleton2 getSingleton() {
 2         if (INSTANCE == null) {
 3             synchronized (Singleton2.class) {
 4                 try {
 5                     Thread.sleep(1);
 6                 } catch (InterruptedException e) {
 7                     // TODO Auto-generated catch block
 8                     e.printStackTrace();
 9                 }
10                 INSTANCE = new Singleton2();
11             }
12         }
13         return INSTANCE;
14     }

执行结果为:

945515189
945515189
945515189
945515189
945515189
945515189
512585151
512585151
512585151
76872907

我们发现这样做,依然不是线程安全的。

为什么呢?

原因在于:当线程A 进入到第2 行时,此时A 暂停,线程B 也进入第2 行,之后B 线程取得锁,B 线程继续执行new 操作完毕,释放锁。

之后才轮到A 去执行,此时拿到锁,这里就出现问题了,下一步A 仍旧会去再执行new 操作。这样就new 了两次。

为了避免面这个问题。特此引入双重判空结构。

而且,变量INSTANCE要设置成: private static volatile Singleton2 INSTANCE; ,需要加上关键词 volatile,关于该关键词的用法及详解大家可以参考我这篇文档,Java之先行发生原则与volatile关键字详解

    public static Singleton2 getSingleton() {
        if (INSTANCE == null) {
            synchronized (Singleton2.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton2();
                }
            }
        }
        return INSTANCE;
    }

输出:

512585151
512585151
512585151
512585151
512585151
512585151
512585151
512585151
512585151
512585151

hashcode都一致。

 

静态内部类方式

JVM保证单例

加载外部类时不会加载静态内部类,这样可以实现懒加载。

package class01;

public class Singleton3 {
    
    /**
     * 静态内部类方式
     * JVM保证单例
     * 加载外部类时不会加载内部类,这样可以实现懒加载。
     */
    private Singleton3() {}
    
    private static class SingletonHolder {
        private static final Singleton3 INSTANCE = new Singleton3();
    }
    
    public static Singleton3 getSingleton() {
        return SingletonHolder.INSTANCE;
    }
    
    public void print() {
        System.out.println("this is Singleton");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Singleton3.getSingleton().hashCode());
            }).start();
        }
    }
}

输出:

1172810421
1172810421
1172810421
1172810421
1172810421
1172810421
1172810421
1172810421
1172810421
1172810421

hashcode都一致。

这里的线程安全是JVM保证的。

最后一种是最完美的写法。

但是,JAVA的创始人不乐意了,干出了一个更完美的写法:在《Effective Java》书中给出了这样的例子:

package class01;

public enum Singleton4 {
    /**
     * 不仅可以解决线程同步问题,还可以防止反序列化
     */
    INSTANCE;
    public void m() {}
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton4.INSTANCE.hashCode());
            }) .start();
        }
    }
}

大家注意:这里是enum 枚举,而不是class。另外枚举不需要构造函数

输出:

76872907
76872907
76872907
76872907
76872907
76872907
76872907
76872907
76872907
...

 

 

总结

一般情况下是用第一种和第四种的,因为比较简单。

 

 

 

Over....

 

posted @ 2021-05-30 18:09  额是无名小卒儿  阅读(61)  评论(0编辑  收藏  举报