您的位置:首页 > 编程语言 > Java开发

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

2016-05-20 20:24 423 查看
在聊单例设计模式之前,我们首先来了一个设计模式的分类以及相关的概念。设计模式指的是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(){}
/*为什么是线程不安全的呢,在有多个线程并且线程的并发率比较高的时候,那么很可能就会造成线程不安全
* 比如说有俩个线程A和B.当线程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 blocke.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();}}}
最后输出的结果是:
truecom.lg.singleton.SingletonDemo01@2a139a55com.lg.singleton.SingletonDemo01@15db9742
很明显后面的俩个对象是不一样的。
但是如果我们把c.setAccessible(true);给注释掉的话,那么输出的结果就会变成下面的样子:
truejava.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;}}
那么最后输出的结果为:
truejava.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@2a139a55com.lg.singleton.SingletonDemo01@2a139a55truecom.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@2a139a55com.lg.singleton.SingletonDemo01@2a139a55truecom.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() {@Overridepublic 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
其它的单例创建方式的测试也一样。
以上所有就是关于单例设计模式的全部的介绍。

                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息