JAVA设计模式之单例模式

来源:互联网 发布:新东方 网络家教 编辑:程序博客网 时间:2024/05/21 10:45

1. 什么是单例模式?

Head First设计模式》书中定义,单例模式是确保一个类最多只有一个实例,并提供全局访问点。在java中实现单例模式需要一个私有构造函数、一个静态变量和一个静态方法(枚举法实现单例模式例外)。使用单例模式必须要时刻注意并解决线程安全问题。

自己理解,单例模式中,类负责创建自己的对象,同时确保只有一个对象被创建。这个类提供了访问自己这个唯一对象的方式,可以直接访问,不需要实例化。

2. 优点

1)对于系统中反复使用的对象,可以一次创建多次使用,减少了反复创建对象的系统开销

2)由于系统中只存在一个对象,可以避免对共享资源的多重占用,可以节约系统内存资源。

3. 缺点

1)单例模式没有抽象层,不能继承进行扩展(单例模式的构造器是私有的,除了自己其他类不能访问,因此不能够继承)。

2)单例类的职责过重,除了要维护自己对象的创建,还需要干一下其他的事情,违背了“单一职责原则”。

4. 使用场景

1)需要频繁实例化然后销毁的对象。

2)创建对象耗时又耗资源,而且系统使用此对象又比较频繁,比如IO操作也数据库连接等。

3)有状态的工具类。

4)应用实例:系统外部资源对象(打印机)、系统内部配置文件对象、web系统计数器、日志应用、多线程线程池设计、数据库连接池设计、HttpApplication是单例的典型应用。

5. 单例模式的5中实现方式

5.1 懒汉模式

/**

 * 单例模式-懒汉模式

 */

public class LazySingleton {

 

private static LazySingletonlazySingleton=null;

private LazySingleton() {

System.out.println("LazySingleton has instanced!");

}

 

/**

 * 同步方法,保证只会有一个线程进入此方法执行程序,确保了线程安全问题

 * 只要是有线程调用getInstance方法,方法都会同步,

 * 但实际上只需要第一次调用的时候同步(解决办法为双重校验锁式

 * 因此对系统效率造成很大的问题

 */

public static synchronized LazySingleton getInstance(){

if(lazySingleton==null){

lazySingleton=new LazySingleton();

}

return lazySingleton;

}

}

5.2 双重检验锁

/**

 * 单例模式-双重检验锁

 */

public class DoubleCheckLockSingleton {

 

private volatile static DoubleCheckLockSingleton dclSingleton =null;

 

private DoubleCheckLockSingleton() {

System.out.println("DoubleCheckLockSingleton has instanced!");

}

 

/**

 * 在JDK1.5之后,双重检查锁定才能够正常达到单例效果。为什么叫双重检验锁,

 * 因为有两次检查 dclSingleton == null,一次是在同步块外,一次是在同步块内。

 * 为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,

 * 如果在同步块内不进行二次检验的话就会生成多个实例了。

 */

public static DoubleCheckLockSingleton getInstance() {

if (dclSingleton ==null) {

synchronized (DoubleCheckLockSingleton.class) {

if (dclSingleton ==null) {

dclSingleton = new DoubleCheckLockSingleton();

}

}

}

return dclSingleton;

}

}

 

注意使用关键字volatiledclSingleton =new DoubleCheckLockSingleton();

这个操作并非是原子性的,在JVM中主要干了下面三个事情:

a. dclSingleton分配内存;

b. 调用DoubleCheckLockSingleton的构造函数来初始化对象;

c. dclSingleton对象指向分配的内存空间(执行完这步dclSingleton就为非null了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时dclSingleton已经是非null了(但却没有初始化),所以线程二会直接返回dclSingleton,然后使用,然后顺理成章地报错。

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完1-2-3之后或者1-3-2之后,不存在执行到1-3然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个volatile变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是Java 5以前的JMMJava内存模型)是存在缺陷的,即时将变量声明成volatile也不能完全避免重排序,主要是volatile变量前后的代码仍然存在重排序问题。这个volatile屏蔽重排序的问题在Java 5中才得以修复,所以在这之后才可以放心使用volatile

5.3 饥汉模式

/**

 * 单例模式-饥汉式

 */

public class HungrySingleton {

/*在类初始化的时候就进行了实例化,因此不存在线程安全问题*/

private static HungrySingletonhungrySingleton=new HungrySingleton();

private HungrySingleton() {

System.out.println("HungrySingleton has instanced!");

}

 

public static HungrySingleton getInstance(){

return hungrySingleton;

}

}

 

饥汉模式不是一种延迟加载模式,是当系统加载类的时候就进行了实例化,不管有没有调用getInstance方法,若永远没有调用getInstance方法,实例化的对象就没有被使用,造成系统资源浪费。

5.4 静态内部类

/**

 * 单例模式-静态内部类式

 */

public class StaticInnerClassSingleton {

 

private StaticInnerClassSingleton() {

System.out.println("StaticInnerClassSingleton has instanced!");

}

 

private static class InnerClass {

private static StaticInnerClassSingletonsicSingleton =new StaticInnerClassSingleton();

}

 

public static StaticInnerClassSingleton getInstance() {

return InnerClass.sicSingleton;

}

}

静态内部类在外部类加载的时候是不会加载的,只有当使用的时候才会加载,因为是私有的,所有只能通过getInstance方法才能调用,而static关键字又保证了sicSingleton实例对象只会被实例化一次,因此这种方法是懒汉式的,不会出现线程安全问题也不依赖JDK版本,是比较好的一种方式。

5.5 枚举(Enum

/**

 * 单例模式-枚举式

 */

public enum EnumSingleton {

INSTANCE;

private Resource resource;

private EnumSingleton(){

resource=new Resource();

}

public Resource getInstance(){

return resource;

}

public static void main(String[]args) {

Resource r=EnumSingleton.INSTANCE.getInstance();

Resource r2=EnumSingleton.INSTANCE.getInstance();

System.out.println(r.equals(r2));

}

}

 

/**

 * 需要使用单例模式的资源

 */

class Resource{

Resource(){

System.out.println("Resource has instanced!");

}

}

 

结果为:
Resource has instanced!

True

 

枚举默认是线程安全的,是从java5才出现的,而且还能防止反序列化导致的对象复制。但是使用的比较少,是《effective java》推荐实现单例模式的一种方法。


参考资料http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/

【四川乐山程序员联盟,欢迎大家加群相互交流学习5 7 1 8 1 4 7 4 3】

 

原创粉丝点击