鸿 网 互 联 www.68idc.cn

当前位置 : 服务器租用 > 编程语言开发 > erlang > >

Java设计模式之单例设计模式

来源:互联网 作者:佚名 时间:2016-05-23 10:23
在聊单例设计模式之前,我们首先来了一个设计模式的分类以及相关的概念。设计模式指的是GOF23,GOF指的是Group of four,直译过来就是“四人帮”。 接下来我们聊一下什么是单例设计模式: 所谓的单例设计模式指的是:保证一个类只有一个实例,并且提供一个访问

在聊单例设计模式之前,我们首先来了一个设计模式的分类以及相关的概念。设计模式指的是GOF23,GOF指的是Group of four,直译过来就是“四人帮”。



接下来我们聊一下什么是单例设计模式:
所谓的单例设计模式指的是:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。这是要注意的。更加详细的内容请看下面的一幅图:

比如说我们的任务管理器。任务管理器的就是一个很典型的单例设计模式。比如说,当我们已经启动了任务管理器以后,那么当我们在试图去再创建一个任务管理器的时候,其实整个的任务管理器的页面是没有动的。因为我们的任务管理器只有一个,我们不能够再创建更多的任务管理器。垃圾回收站也是一个典型的单利设计模式。数据库连接池的设计一般也是采用单例设计模式,引文数据库连接时一个数据库资源。如果不断地new 对象,那么这样做的话,是很消耗内存和资源的。
单利设计模式的有点以及分类;


现在我们来看一下饿汉式单例设计模式是怎么实现的。
代码如下:
/*饿汉式单例设计模式特点:
* 1)线程安全的。在这个访问该实例的方法中,我们没有使用synchronized。原因我们在创建s这个对象的时候,
* 是在类加载的时候,立即创建这个对象。类加载的过程是天然线程安全的。在加载类的时候,是天然的线程安全的模式
* 因为这个s对象是在类加载的时候进行创建的,所以是线程安全的。
* 2)调用的效率高。因为在方法getInstance()中,没有使用synchronized,所以在访问这个s对象的时候,
* 效率自然是很高的
* 3)由于是立即加载这个对象s的,所以没有延时加载的优势。也就是说,即使在后面的编程过程当中,即使
* 没有使用到这个对象s,但是这个对象s还是被创建出来了。这样的话,可能就会造成浪费资源和内存。*/
/*这是饿汉式的单例模式*/
public class SingletonDemo01 {
//    在类加载的时候,一上来就创建一个对象。或者说是立即加载这个对象。
//    不管这个对象在后面的编程中有没有用到。
//    在类初始化的时候,立即加载和创建这个对象。
    private static SingletonDemo01 s=new SingletonDemo01();
//    必须将构造器私有化
    private SingletonDemo01(){}
//    创建一个访问该实例的全局访问点。
    public static SingletonDemo01 getInstance(){
        return s;
    }
}
关于饿汉式的图解:

下面我们在来看一下懒汉式的实现。注意懒汉式,其实也叫做懒加载。有延时加载的优势。也就是说,它不是在类加载的时候立即加载,而是在需要的时候才进行加载。由于不是在类加载的时候,创建这个单例对象,所以它不是线程安全的。在多线程进行访问的时候,如果并发率很高的话,很可能会出现问题。因此需要使用同步标志synchronized.尽管这样是线程安全了,但是由于被synchronized同步了,所以在访问这个对象的时候,就会造成这个效率的降低。这是要注意的。
/*这是懒汉式单例设计模式
* */
public class SingletonDemo02 {
/*不是在类加载的时候就创建这个对象,只有在使用的时候,才创建这个对象,因此可以充分利用资源了。但是
* 也因此带来了问题。什么问题呢?就是线程不安全。因此使用了synchronized同步块。所以,效率比较低*/
    private static SingletonDemo02 instace;
    private SingletonDemo02(){}
    /*为什么是线程不安全的呢,在有多个线程并且线程的并发率比较高的时候,那么很可能就会造成线程不安全
    * 比如说有俩个线程AB.当线程A正好访问到(instace == null)的时候,就被挂起来了。那么线程B进来的时候,
    * 创建了一个对象。当B线程完成了任务的时候,那么A线程再继续进行,那么又创建了另外的一个对象
    * 因此很可能就创建了多个的对象。因此需要使用同步synchronized*/
    public static synchronized  SingletonDemo02 getInstace() {
        if (instace == null) {
            instace = new SingletonDemo02();
            
        }
        return instace;
    }
}
不过懒汉式的单例设计模式,其实是可以进一步进行优化的。主要是将同步块synchronized放到if块。这样就可以提高懒汉式的效率了。其实处理并发的时候,主要是考虑第一次在创建这个对象的时候,防止创建多个不同的对象就
可以了。

class Jvm{

//将构造器私有化,避免外部直接创建对象

private Jvm(){

}

//创建一个静态的变量

private static Jvm instance=null;

    public static  Jvm getInstance3(long time){

       if(null==instance){//如果对象已经创建了的话,就直接RETURN 了,这样的话,就能够提高效率

synchronized(Jvm.class){//这里锁定的是静态的信息。因为在静态方法中不可以使用this.

if(null==instance){

try {

Thread.sleep(time);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

instance=new Jvm();

}

}

       }

       return instance;

}

关于使用懒汉式的优缺点的一些说明:

其实单例模式除了饿汉式和懒汉式以外,还有其它的三种方式。下面我们再来一个一个地介绍。
首先是通过静态内部类来实现(这其实也是属于懒加载)
我们在考虑单例模式的效率的时候,一般会考虑三个方面。
(1)线程安全
(2)效率高
(3)有延时加载的优势,避免资源的浪费
那么通过静态内部类的加载方式,就可以完全地实现这三个的优势。是一种比较好的单例模式。具体的代码如下:
/*通过静态内部类的加载方式,可以完美地实现单例的所有的三个优势
* (1)有延时加载的优势。我们知道,类只有在使用的时候,才会被加载。而我们的对象instance 恰好就是通过
* 静态类来进行加载的。因此我们只有在通过使用方法getInstace()的时候,我们的类才会被加载
* ,我们的对象instace才会被加载,因此这样的话,我们就会有延时加载的优势
* 2)线程安全的。由于我们的对象instance是通过类加载的方式来进行加载的。但是类加载的过程是天然的线程安全的
* 因此这个是线程安全的
* 3)调用的效率高:由于这种方式是线程安全的,因此我们没有使用到同步块,因此效率自然就是比较高的*/
public class SingleDemo03 {
    private SingleDemo03(){}
//    静态内部类。final可以有也可以没有
    private static class SingletonClassInstance {
        private static final SingleDemo03 instace=new SingleDemo03();
    }

    public static SingleDemo03 getInstance(){
        return SingletonClassInstance.instace;
    }

}
关于通过内部类来进行加载的优点的说明如下:

还有一种单例的实现的方式,而且也是比较简单的方式,就是通过枚举的方式来进行实现。枚举是天然的单例模式。而且,很简单。但是,有一点,很遗憾的是,通过这种方式来实现的话,没有延时加载的优势。这是要注意的。具体的代码如下:
public enum  SingleDemo04 {
//    定义一个枚举的元素,它就代表了SingleDemo04的一个实例。
    INSTANCE;
//    可以有自己的操作。
    public void OPERATION(){

    }

    public static void main(String[] args) {
        SingleDemo04 s1=SingleDemo04.INSTANCE;
        SingleDemo04 s2=SingleDemo04.INSTANCE;
        System.out.println(s1 == s2);
    }
}
最后的结果自然是
true
其实,通过枚举来实现单例模式的更好的优点是:避免了通过反射和反序列化来进行加载。这是要注意来的。因为对于一般的类,即使你的类构造器被私有化了,但是还是可以通过反射哈反序列化的方式来进行加载。但是,如果是通过枚举来实现的话,就不能通过这种方式来进行实现了。
有关通过枚举来实现单例模式的优缺点,请看下面的图:

还有一个可以实现单例模式的就是通过双重检测锁的方式。但是这个方式由于JVM底层的构造和内部的优化问题,偶尔的情况下,会出现问题,因此不建议使用。具体的介绍如下:

五种的方式介绍完了,下面我们来总结一下这五种方式之间的区别:

下面我们继续来介绍一下如何通过反射来破解我们的单例模式(不包括枚举型):
/*饿汉式单例设计模式特点:
* 1)线程安全的。在这个访问该实例的方法中,我们没有使用synchronized。原因我们在创建s这个对象的时候,
* 是在类加载的时候,立即创建这个对象。类加载的过程是天然线程安全的。在加载类的时候,是天然的线程安全的模式
* 因为这个s对象是在类加载的时候进行创建的,所以是线程安全的。
* 2)调用的效率高。因为在方法getInstance()中,没有使用synchronized,所以在访问这个s对象的时候,
* 效率自然是很高的
* 3)由于是立即加载这个对象s的,所以没有延时加载的优势。也就是说,即使在后面的编程过程当中,即使
* 没有使用到这个对象s,但是这个对象s还是被创建出来了。这样的话,可能就会造成浪费资源和内存。*/
/*这是饿汉式的单例模式*/
public class SingletonDemo01 {
//    在类加载的时候,一上来就创建一个对象。或者说是立即加载这个对象。
//    不管这个对象在后面的编程中有没有用到。
//    在类初始化的时候,立即加载和创建这个对象。
    private static SingletonDemo01 s=new SingletonDemo01();
//    必须将构造器私有化
    private SingletonDemo01(){}
//    创建一个访问该实例的全局访问点。
    public static SingletonDemo01 getInstance(){
        return s;
    }
}

接着是一个调用者:
/*当然这其中是不包括通过枚举来达到单例设计模式。因为枚举是天然的单例模式*/
/*如何通过反射来破解单例模式。以及如何防止单例模式被破解*/
    /*很明显的就是,通过反射,我们可以很容易地破解单例模式*/
    /*但是我们的单例模式也可以通过在构造器中进行修改,那么就可以很容易地避免通过反射来创建多个对象*/
public class Client {
    public static void main(String[] args) {
        SingletonDemo01 s1=SingletonDemo01.getInstance();
        SingletonDemo01 s2=SingletonDemo01.getInstance();
        System.out.println(s1 == s2);
        try {
            Class<SingletonDemo01> clazz= (Class <SingletonDemo01>) Class.forName("com.lg.singleton.SingletonDemo01");
                Constructor<SingletonDemo01> c=clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            SingletonDemo01 s3 = c.newInstance();
            SingletonDemo01 s4 = c.newInstance();
            System.out.println(s3);
            System.out.println(s4);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
}
最后输出的结果是:
true
com.lg.singleton.SingletonDemo01@2a139a55
com.lg.singleton.SingletonDemo01@15db9742
很明显后面的俩个对象是不一样的。
但是如果我们把c.setAccessible(true);给注释掉的话,那么输出的结果就会变成下面的样子:
true
java.lang.IllegalAccessException: Class com.lg.singleton.Client can not access a member of class com.lg.singleton.SingletonDemo01 with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(Unknown Source)
	at java.lang.reflect.AccessibleObject.checkAccess(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.lg.singleton.Client.main(Client.java:16)
为什么会得到这样的结果呢?因为在反射中我们规定,如果要访问一个类的私有属性的话,那么必须使用setAccessible(true),这样的话,就可以跳过私有构造器的检查。否则的话,是不行的。
那么我们如何来防止这种情况的发生呢?
我们可以在构造器中做一些改变,手动抛出异常:
public class SingletonDemo01 {
//    在类加载的时候,一上来就创建一个对象。或者说是立即加载这个对象。
//    不管这个对象在后面的编程中有没有用到。
//    在类初始化的时候,立即加载和创建这个对象。
    private static SingletonDemo01 s=new SingletonDemo01();
//    必须将构造器私有化
    private SingletonDemo01() {
        if (s != null) {
            throw new RuntimeException();//这里做出了改变
        }
    }
//    创建一个访问该实例的全局访问点。
    public static SingletonDemo01 getInstance(){
        return s;
    }
}
那么最后输出的结果为:
true
java.lang.IllegalAccessException: Class com.lg.singleton.Client can not access a member of class com.lg.singleton.SingletonDemo01 with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(Unknown Source)
	at java.lang.reflect.AccessibleObject.checkAccess(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.lg.singleton.Client.main(Client.java:16)
这样的话,通过抛出异常,就可以防止通过反射来创建更多的对象了。
当然,在一般的项目开发中,我们是不需要考虑这么多的。
除了通过反射来破解单例模式以外,我们还可以通过序列化和反序列化的方式来破解单例模式:
需要实现一个接口servializable
public class SingletonDemo01 implements Serializable {
//    在类加载的时候,一上来就创建一个对象。或者说是立即加载这个对象。
//    不管这个对象在后面的编程中有没有用到。
//    在类初始化的时候,立即加载和创建这个对象。
    private static SingletonDemo01 s=new SingletonDemo01();
//    必须将构造器私有化
    private SingletonDemo01() {
        if (s != null) {
            throw new RuntimeException();
        }
    }
//    创建一个访问该实例的全局访问点。
    public static SingletonDemo01 getInstance(){
        return s;
    }
}


通过反序列化来进行破解:
public class Client {
    public static void main(String[] args) {
        SingletonDemo01 s1=SingletonDemo01.getInstance();
        SingletonDemo01 s2=SingletonDemo01.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
//        通过反射
        /*try {
            Class<SingletonDemo01> clazz= (Class <SingletonDemo01>) Class.forName("com.lg.singleton.SingletonDemo01");
                Constructor<SingletonDemo01> c=clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            SingletonDemo01 s3 = c.newInstance();
            SingletonDemo01 s4 = c.newInstance();
            System.out.println(s3);
            System.out.println(s4);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
*/
//        通过序列化和反序列化。

        try {
            File file = new File("c:/aa.txt");
            FileOutputStream fis = new FileOutputStream(file);
            ObjectOutput os = new ObjectOutputStream(fis);
            os.writeObject(s1);
            fis.close();
            os.close();
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
          SingletonDemo01 s3= (SingletonDemo01) ois.readObject();
            System.out.println(s3);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

最后输出的结果为:
com.lg.singleton.SingletonDemo01@2a139a55
com.lg.singleton.SingletonDemo01@2a139a55
true
com.lg.singleton.SingletonDemo01@33909752
很明显的是,这俩个是不同的对象。
那么如何防止这种情况的发生呢?
public class SingletonDemo01 implements Serializable {
//    在类加载的时候,一上来就创建一个对象。或者说是立即加载这个对象。
//    不管这个对象在后面的编程中有没有用到。
//    在类初始化的时候,立即加载和创建这个对象。
    private static SingletonDemo01 s=new SingletonDemo01();
//    必须将构造器私有化
    private SingletonDemo01() {
        if (s != null) {
            throw new RuntimeException();
        }
    }
//    创建一个访问该实例的全局访问点。
    public static SingletonDemo01 getInstance(){
        return s;
    }
/*这个方法是基于回调的。在反序列化的时候,如果是定义了readResolve()方法,那么就会返回方法中指定的
* 对象,而不需要再创建新的对象*/
    private Object readResolve() {//新增加的基于回调的方法。
        return  s;
    }
}
这样的话,最后输出的结果为:
com.lg.singleton.SingletonDemo01@2a139a55
com.lg.singleton.SingletonDemo01@2a139a55
true
com.lg.singleton.SingletonDemo01@2a139a55
很明显,这几个对象都是一样的。
下面我们通过实际的测试来比较这几种单例模式的效果:


long start= System.currentTimeMillis();
//启动很多的线程获得单例
long end=System.currentTimeMillis();
long time=end-start;
在Main线程中使用这样的方式来计算时间是不对的。因为time实际上计算的是Main线程从开始到结束所需要的时间。但是这样明显是不对的。为了计算时间,我们可以使用countDownLatch.
以下是用来测试时间效率的代码:
public class Client2 {
    public static void main(String[] args) {
        int threadNum=10;
         final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        long start=System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 100000; j++) {
                        Object o=SingletonDemo01.getInstance();
                    }
//                    每执行完一个线程,那么线程的个数就减少一。
                    countDownLatch.countDown();
                }
            }).start();
        }
//       使主线程一直等待。其实就是阻塞主线程。主要是阻塞的话,其实里面都是有一个while循环,
//        知道所有的线程都执行完毕。
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        long time=end-start;
        System.out.println(time);
    }
}
运行的结果为:
21
其它的单例创建方式的测试也一样。

以上所有就是关于单例设计模式的全部的介绍。



网友评论
<