zoukankan      html  css  js  c++  java
  • 设计模式——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....

  • 相关阅读:
    PAT Advanced 1067 Sort with Swap(0, i) (25分)
    PAT Advanced 1048 Find Coins (25分)
    PAT Advanced 1060 Are They Equal (25分)
    PAT Advanced 1088 Rational Arithmetic (20分)
    PAT Advanced 1032 Sharing (25分)
    Linux的at命令
    Sublime Text3使用指南
    IntelliJ IDEA创建第一个Groovy工程
    Sublime Text3 安装ftp插件
    Sublime Text3配置Groovy运行环境
  • 原文地址:https://www.cnblogs.com/gjmhome/p/14828572.html
Copyright © 2011-2022 走看看