zoukankan      html  css  js  c++  java
  • java单例模式实现

    1.最基本的单例模式

    /**
     * @author LearnAndGet
     * @time 2018年11月13日
     *  最基本的单例模式
     */
    public class SingletonV1 {
        
        private static SingletonV1 instance = new SingletonV1();;
        
        //构造函数私有化
        private SingletonV1() {}
    
        public static SingletonV1 getInstance() 
        {
            return instance;
        }
    }
    import org.junit.Test;
    public class SingletonTest {
        
        @Test
        public void test01() throws Exception
        {
            SingletonV1 s1 = SingletonV1.getInstance();
            SingletonV1 s2 = SingletonV1.getInstance();
            System.out.println(s1.hashCode());
            System.out.println(s2.hashCode());
        }
    }
    
    //运行结果如下:
    589873731
    589873731

    2.类加载时不初始化实例的模式

      上述单例模式在类加载的时候,就会生成实例,可能造成空间浪费,如果需要修改成,在需要使用时才生成实例,则可修改代码如下:

     1 public class SingletonV2 {
     2     
     3     private static SingletonV2 instance;
     4     
     5     //构造函数私有化
     6     private SingletonV2() {}
     7 
     8     public static SingletonV2 getInstance(){
    10         if(instance == null) 
    11         {
    12             instance = new SingletonV2();
    13         }
    14         return instance;
    15     }
    16 }

    然而,上述方案虽然在类加载时不会生成实例,但是存在线程安全问题,如果线程A在执行到第10行时,线程B也进入该代码块,恰好也执行好第10行,此时如果实例尚未生成,则线程A和线程B都会执行第12行的代码,各自生成一个实例,此时就违背了单例模式的设计原则。实际测试代码如下:

    public class SingletonTest {
    
        @Test
        public void test02() throws Exception
        {    
            for(int i=0;i<1000;i++) 
            {
                Thread th1 = new getInstanceThread();
                th1.start();
            }
        
        }
        
        class getInstanceThread extends Thread
        {
            public void run() 
            {
                try 
                {
                    SingletonV2 s = SingletonV2.getInstance();
                    System.out.println(Thread.currentThread().getName()+" get Instance "+s.hashCode()+" Time: "+System.currentTimeMillis());
                }catch(Exception e) 
                {
                    e.printStackTrace();
                }
            }
        }
        
    }

    经过多次测试,可能产生如下输出结果:

      

    3.线程安全的单例模式

      在上述单例模式下进行改进,在getInstance方法前加入 Sychronized关键字,来实现线程安全,修改后代码如下:

     1 public class SingletonV3 {
     2     
     3     private static SingletonV3 instance;
     4     
     5     //构造函数私有化
     6     private SingletonV3() {}
     7 
        //synchronized关键字在静态方法上,锁定的是当前类:sychronized关键字 8 public static synchronized SingletonV3 getInstance() 9 { 10 if(instance == null) 11 { 12 instance = new SingletonV3(); 13 } 14 return instance; 15 } 16 }

     增加sychronized关键字后,确实能够改善线程安全问题,但是也带来了额外的锁开销。性能受到一定影响。举例来说,此时如果有1000个线程都需要使用SingletonV3实例,因为加锁的位置在getInstance上,因此,每个线程都必须等待其他获取了锁的线程完全执行完锁中的方法后,才能够进入该方法并获取自己的实例。

    4.双重校检+线程安全单例模式

      于是可以在上述代码的基础上,只有当Singleton实例未被初始化时,对实例化方法加锁即可。在Singleton实例已经被初始化时,无需加锁,直接返回当前Singleton对象。代码如下:

     1     private static SingletonV4 instance;
     2     
     3     //构造函数私有化
     4     private SingletonV4() {}
     5 
     6     public static SingletonV4 getInstance()
     7     {
     8         if(instance == null) 
     9         {
    10             synchronized(SingletonV4.class) 
    11             {
    12                 //双重校检
    13                 if(instance == null) 
    14                 {
    15                     instance = new SingletonV4();
    16                 }
    17             }
    18         }
    19         return instance;
    20     }

    5.内部类单例模式

      尽管上述方案解决了同步问题,双重校检也使得性能开销大大减小,但是,只有有synchronized关键字的存在。性能多多少少还是会有一些影响,此时,我们想到了 "内部类"的用法。

      ①.内部类不会随着类的加载而加载

      ②.一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

      静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全。基于此,修改代码如下:

      

     /推荐指数:★★★★★
    
    
     1 public class SingletonV5 {
     2     //构造函数私有化
     3     private SingletonV5() {}
     4 
     5     static class SingetonGet
     6     {
     7         private static final SingletonV5 instance = new SingletonV5();
     8     }
     9     
    10     public static SingletonV5 getInstance() 
    11     {
    12         return SingetonGet.instance;
    13     }
    14 }

    6.反射都不能破坏的单例模式

      静态内部类实现的单例模式,是目前比较推荐的方式,但是在java功能强大反射的机制下,它就是个弟弟,此时利用反射仍然能够创建出多个实例,以下是创建实例的代码:

      

     1     @Test
     2     public void test4()
     3     {    
     4         //普通方式获取实例s1,s2
     5         SingletonV5 s1 = SingletonV5.getInstance();
     6         SingletonV5 s2 = SingletonV5.getInstance();
     7         //利用反射获取实例s3,s4
     8         SingletonV5 s3 = null;
     9         SingletonV5 s4 = null;
    10         try 
    11         {
    12             Class<SingletonV5> clazz = SingletonV5.class;
    13             Constructor<SingletonV5> constructor = clazz.getDeclaredConstructor();
    14             constructor.setAccessible(true);
    15             s3 = constructor.newInstance();
    16             s4 = constructor.newInstance();
    17         }catch(Exception e) 
    18         {
    19             e.printStackTrace();
    20         }
    21         
    22         System.out.println(s1.hashCode());
    23         System.out.println(s2.hashCode());
    24         System.out.println(s3.hashCode());
    25         System.out.println(s4.hashCode());    
    26     }

    输出结果如下:

      

    589873731
    589873731
    200006406
    2052001577

     可以看到,s1和s2拥有相同的哈希码,因此他们是同一个实例,但是s3、s4,是通过反射后用构造函数重新构造生成的实例,他们均与s1,s2不同。此时单例模式下产生了多个不同的对象,违反了设计原则。

    基于上述反射可能造成的单例模式失效,考虑在私有的构造函数中添加是否初始化的标记位,使私有构造方法只可能被执行一次。

    public class SingletonV6 {
        //是否已经初始化过的标记位
        private static boolean isInitialized = false;
        
        //构造函数中,当实例已经被初始化时,不能继续获取新实例
        private SingletonV6() 
        {
            synchronized(SingletonV6.class) 
            {
                if(isInitialized == false) 
                {
                    isInitialized = !isInitialized;
                }else 
                {
                    throw new RuntimeException("单例模式被破坏...");
                }
            }    
        }
    
        static class SingetonGet
        {
            private static final SingletonV6 instance = new SingletonV6();
        }
        
        public static SingletonV6 getInstance() 
        {
            return SingetonGet.instance;
        }
    }

    测试代码如下:

        @Test
        public void test5()
        {    
            SingletonV6 s1 = SingletonV6.getInstance();
            SingletonV6 s2 = null;
            try 
            {
                Class<SingletonV6> clazz = SingletonV6.class;
                Constructor<SingletonV6> constructor = clazz.getDeclaredConstructor();
                constructor.setAccessible(true);
                s2 = constructor.newInstance();
    
            }catch(Exception e) 
            {
                e.printStackTrace();
            }    
            System.out.println(s1.hashCode());
            System.out.println(s2.hashCode());
        }

      运行上述代码时,会抛出异常:

      

    java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.lang.reflect.Constructor.newInstance(Unknown Source)
        at SingletonTest.SingletonTest.test5(SingletonTest.java:98)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Unknown Source)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
        at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
    Caused by: java.lang.RuntimeException: 单例模式被破坏...
        at SingletonTest.SingletonV6.<init>(SingletonV6.java:26)
        ... 28 more
    2052001577

    7.序列化反序列化都不能破坏的单例模式

      经过上述改进,反射也不能够破坏单例模式了。但是,依然存在一种可能造成上述单例模式产生两个不同的实例,那就是序列化。当一个对象A经过序列化,然后再反序列化,获取到的对象B和A是否是同一个实例呢,验证代码如下:

      

    /**
     * @Author {LearnAndGet}
     * @Time 2018年11月13日
     * @Discription:测试序列化并反序列化是否还是同一对象
     */
    package SingletonTest;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInput;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutput;
    import java.io.ObjectOutputStream;
    
    public class Main {
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            SingletonV6 s1 = SingletonV6.getInstance();
            
            ObjectOutput objOut = null;
            
            try {
                //将s1序列化(记得将Singleton实现Serializable接口)
                objOut = new ObjectOutputStream(new FileOutputStream("c:\a.objFile"));
                objOut.writeObject(s1);
                objOut.close();
                
                //反序列化得到s2
                ObjectInput objIn = new ObjectInputStream(new FileInputStream("c:\a.objFile"));
                SingletonV6 s2 = (SingletonV6) objIn.readObject();
                objIn.close();
                
                System.out.println(s1.hashCode());
                System.out.println(s2.hashCode());
                
                } catch (Exception e) 
                {
                // TODO Auto-generated catch block
                e.printStackTrace();
                }
        }
    
    }

      输出结果如下:

      

    1118140819
    990368553

     可见,此时序列化前的对象s1和经过序列化->反序列化步骤后的到的对象s2,并不是同一个对象,因此,出现了两个实例,再次违背了单例模式的设计原则。

    为了消除问题,在单例模式类中,实现Serializable接口之后 添加对readResolve()方法的实现:当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。这就确保了在序列化和反序列化的过程中没人可以创建新的实例,修改后的代码如下:

      

    package SingletonTest;
    
    import java.io.Serializable;
    
    /**
     * @author LearnAndGet
     *
     * @time 2018年11月13日
     * 
     */
    public class SingletonV6  implements Serializable{
        //是否已经初始化过的标记位
        private static boolean isInitialized = false;
        
        //构造函数中,当实例已经被初始化时,不能继续获取新实例
        private SingletonV6() 
        {
            synchronized(SingletonV6.class) 
            {
                if(isInitialized == false) 
                {
                    isInitialized = !isInitialized;
                }else 
                {
                    throw new RuntimeException("单例模式被破坏...");
                }
            }    
        }
    
        static class SingetonGet
        {
            private static final SingletonV6 instance = new SingletonV6();
        }
        
        public static SingletonV6 getInstance() 
        {
            return SingetonGet.instance;
        }
        //实现readResolve方法
        private Object readResolve() 
        {
            return getInstance();
        }
    }

     重新运行上述序列化和反序列过程,可以发现,此时得到的对象是同一对象。

      

    1118140819
    1118140819

    8.总结

      在实际开发中,根据自己的需要,选择对应的单例模式即可,不一样非要实现第7节中那种无坚不摧的单例模式。毕竟不是所有场景下都需要实现序列化接口, 也并不是所有人都会用反射来破坏单例模式。因此比较常用的是第5节中的,内部类单例模式,代码简洁明了,且节省空间。

  • 相关阅读:
    为什么java使用对象序列化到文件,打开之后是乱码,而且更换编码格式查看也不能正常显示呢
    String类能够导入IDEA,但是里面的构造方法无法使用,一直显示报错
    IDEA不能提示导入java.io.File类,但是自己手写import没问题,最后找到了问题所在
    扑克牌发牌,排序的功能实现
    TFS 生成定义
    Git-tfs工具
    日期和时间
    调用惯例
    优化查找和排序
    使用更好的库
  • 原文地址:https://www.cnblogs.com/LearnAndGet/p/9953436.html
Copyright © 2011-2022 走看看