Design Pattern学习笔记之单例(Singleton Pattern)

Design Pattern学习笔记之单例(Singleton Pattern)

1.    引子--Whois?

a.      从类图看,在所有模式中最简单,只有一个类。

b.      限定某个类只能有一个实例。

c.      为其他类提供公共的访问方法。

d.      看起来简单,但实现该模式时,要多加注意,特别是在多线程的场景下。

2.    不辨不明—为什么?

a.      只有一个?这有什么用?

实际中存在“one and onlyone”的需求,线程池、连接池、缓存、配置管理、…

b.      犯得上为“only one”设计模式么?

单例模式是经过时间检验的确保一个类只被实例化一次的最好的方法,其他方法都有不同缺点。

c.      使用全局变量有什么不好?

说一个缺点:全局变量在应用启动时进行初始化,而初始化的这个全局变量可能是个很耗费资源的事情;更糟糕的事情是:你可能根本用不到该资源。

d.      尽管如此,可我感觉事情还是不应该被上升到模式的高度,搞得复杂了

如果你能很好地控制对静态变量的访问和修改,那就用不到这个模式了(后面我们会看到这不是容易做到的事情);换个角度看问题:怎样阻止实例化多个类?

3.    思考练习—howto implement?

a.  怎样创建对象?

New MyObject(),其他对象在任何需要的时候都可以创建该对象,可以创建多个对象

b.  怎样避免创建多个?

Public MyClass(){

Private MyClass(){}

}

只有类自己能创建该对象,有什么问题?

c.  怎样获得初始的那个对象?

Public MyClass(){

          PrivateMyClass(){}

Public static MyClass getInstance(){

                   return new MyClass();

}

}

怎样保证只有一个MyClass对象?

Public MyClass(){

         Private static MyClass myClass;

         Private MyClass(){}

         Public static MyClass getInstance(){

         If(myClass == null){

         myClass = new MyClass();

}

         return myClass;

}

}

4.    问题引入—巧克力工厂

查理的巧克力工厂需要用电脑来控制对巧克力和牛奶的煮沸,工厂有个500加仑的煮沸罐子,巧克力和牛奶的混合物先在这个罐子中煮沸后,在倒出冷却后加工成巧克力。

我们来做一个类,用于控制这个煮沸和倒出的生产过程,以下的约束应该注意:罐子空的时候才能在填充,罐子不空且已经煮沸的时候才能倒出,罐子不空且没有煮沸时才能煮沸。其中任何一个条件判断的有问题时,就会导致严重的后果。

我们来看看代码:

public class UglyChocolateBoiler {

    private boolean empty;

    private boolean boiled;

 

    public UglyChocolateBoiler() {

       empty = true;

       boiled = false;

    } 

   

    public void fill() {

       if (isEmpty()) {

           empty = false;

           boiled = false;

           // fill theboiler with a milk/chocolate mixture

       }

    }

 

    public void drain() {

       if (!isEmpty() && isBoiled()) {

           // drain theboiled milk and chocolate

           empty = true;

       }

    }

 

    public void boil() {

       if (!isEmpty() && !isBoiled()) {

           // bring thecontents to a boil

           boiled = true;

       }

    }

 

    public boolean isEmpty() {

       return empty;

    }

 

    public boolean isBoiled() {

       return boiled;

    }

}

如果这个煮沸的控制器有多个,事情就会变得无法控制。我们用单例模式来改进这个煮沸的控制器,代码如下:

public class ChocolateBoiler {

    private boolean empty;

    private boolean boiled;

    private static ChocolateBoiler uniqueInstance;

 

    private ChocolateBoiler() {

       empty = true;

       boiled = false;

    }

 

    public static ChocolateBoiler getInstance() {

       if (uniqueInstance == null) {

           System.out.println("Creatingunique instance of Chocolate Boiler");

           uniqueInstance = new ChocolateBoiler();

       }

       System.out.println("Returninginstance of Chocolate Boiler");

       return uniqueInstance;

    }

 

    public void fill() {

       if (isEmpty()) {

           empty = false;

           boiled = false;

           // fill the boilerwith a milk/chocolate mixture

       }

    }

 

    public void drain() {

       if (!isEmpty() && isBoiled()) {

           // drain theboiled milk and chocolate

           empty = true;

       }

    }

 

    public void boil() {

       if (!isEmpty() && !isBoiled()) {

           // bring thecontents to a boil

           boiled = true;

       }

    }

 

    public boolean isEmpty() {

       return empty;

    }

 

    public boolean isBoiled() {

       return boiled;

    }

}

5.    理论篇—单例模式的定义

The SingletonPattern ensures a class has only one instance, and provides a global point ofaccess to it.

单例模式确保一个类只有一个实例,并且提供一个全局的访问点。

代码:

public class Singleton {

    private static Singleton uniqueInstance;

 

    // other useful instance variables here

 

    private Singleton() {}

 

    public static Singleton getInstance() {

       if (uniqueInstance == null) {

           uniqueInstance = new Singleton();

       }

       return uniqueInstance;

    }

 

    // other useful methods here

}

看看类图:

6.    问题来了—多线程带来什么?

在前面单线程的场景下,我们的煮沸控制器运行良好,但是如果在多线程情况下,情况就变得不同。

多个线程都调用getInstance方法,会有什么问题?

可能会生成多个煮沸器的实例,从而破坏了对煮沸器有且只能有一个实例的约束。

怎么改进?

1.      使用synchronized的关键字修饰是个简单易用的方法:

public class Singleton {

    private static Singleton uniqueInstance;

 

    // other useful instance variables here

 

    private Singleton() {}

 

    public static synchronized Singleton getInstance() {

       if (uniqueInstance == null) {

           uniqueInstance = new Singleton();

       }

       return uniqueInstance;

    }

 

    // other useful methods here

}

         但是我们知道synchronized关键字会带来极大的性能损耗,大约损失100倍的性能,如果getInstance在应用中使用很频繁,这不是一个好方法。

2.      由于需要加同步控制的只在第一次初始化实例的时候,我们在jvm加载类的时候就做好实例化的工作。

public class Singleton {

private static Singleton uniqueInstance = new Singleton();

 

private Singleton() {}

 

public static SingletongetInstance() {

    return uniqueInstance;

}

}

         当实例化该类需要耗费大量资源时,这也不是个好方法,因为无论是否使用该对象,都需要先实例化;我们更希望在使用的时候才进行实例化。

3.      使用双重校验来缩小同步代码块的范围从而提高性能

 public class Singleton {

    private volatile static Singleton uniqueInstance;

 

    private Singleton() {}

 

    public static Singleton getInstance() {

       if (uniqueInstance == null) {

           synchronized (Singleton.class) {

              if (uniqueInstance == null) {

                  uniqueInstance = new Singleton();

              }

           }

       }

       return uniqueInstance;

    }

}

Volatile关键字提供线程间的同步,不能协调对互斥资源的访问,因此在初始化实例时,使用synchronized同步,并进行双重判断。

7.    题外话—volatile关键字

Java 语言中的 volatile变量可以被看作是一种“程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

锁提供了两种主要特性:互斥(mutualexclusion)和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的—— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

         Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:1. 对变量的写操作不依赖于当前值。2. 该变量没有包含在具有其他变量的不变式中。

         非线程安全的一种使用:

         publicclass NumberRange {

   private int lower, upper;

 

   public int getLower() { return lower; }

   public int getUpper() { return upper; }

 

   public void setLower(int value) {

       if (value > upper)

           throw new IllegalArgumentException(...);

       lower = value;

    }

 

   public void setUpper(int value) {

       if (value < lower)

           throw new IllegalArgumentException(...);

       upper = value;

    }

}

这种方式限制了范围的状态变量,因此将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) ——一个无效值。至于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原子化—— 而将字段定义为volatile 类型是无法实现这一目的的。

Volatile不会阻塞,对于读操作而言,该关键字几乎不增加额外的操作;对于写操作而言则增加很多操作,但总体而言比synchronized的性能要好。

8.    Review

The SingletonPattern ensures a class has only one instance, and provides a global point ofaccess to it.

单例模式确保一个类只有一个实例,并且提供一个全局的访问点。

1.      在jdk1.2版本之前,如果一个单例的对象没有引用,就会被垃圾回收,导致再次调用getInstance时会获取一个新的对象,带来一堆问题。

2.      在应用中单例模式的对象应该很少,如果你的应用有大量的对象都是单例模式,需要重新考虑你的设计。

3.      在多个类加载器的情况下,单例模式并不能保证只实例化一次。

4.      单例模式保证应用中有且只有一个实例。

5.      单例模式提供一个全局的访问点,访问单例的对象。

6.      Java中使用私有的构造器、静态变量、静态方法实现单例。

7.      在多线程的场景下,要根据实际要求,选择单例的实现方式。

8.      注意在多线程的场景下,在JDK1.5之后,双重校验才能提供线程安全。

9.      注意如果使用多个类装载器,单例可能被破坏。

10.  在jdk1.2之前,使用单例时需要使用全局变量持有对该对象的引用,不然会被回收。

OO准则:

a. 封装变化,encapsulate what varies

b. 组合优于继承, favorcomposition over inheritance

c. 面向接口编程,program to interfaces, not implementation

d. 致力于实现交互对象之间的松散耦合, strive for loosely coupled designs between objects that interact

e. 类应该对于扩展开发,对于修改封闭, classes should be open for extension but closed for modification

f. 依赖于抽象类或者接口而不是具体类。Depend on abstraction. Do not depend on concrete classes.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值