java设计模式1.单例模式

设计模式分类:

  • 创建型模式:工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式:适配器模式、组合模式、装饰器模式、代理模式、门面模式、桥接模式、享元模式。
  • 行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、访问者模式、调停者模式等。

设计模式原则:
开闭原则:对扩展开放,对修改封闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级,想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。

  • 1.单一职责原则

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。

  • 2.里氏替换原则

任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

里氏代换原则是对“开-闭”原则的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

  • 3.依赖倒转原则

面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。

  • 4.接口隔离原则

每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

  • 5.迪米特法则(最少知道原则)

一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

  • 6.合成复用原则

尽量首先使用合成/聚合的方式,而不是使用继承。

  • 单例模式

 要点:

  • 1.只能有一个类实例;
  • 2.类必须自行创建这个实例;
  • 3.类必须自行向整个系统提供这个实例。

常见实现方式:饿汉式、延迟加载、.延长初始化占位类模式、枚举方式。

  • 1. 饿汉式
public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

java初始器中采用了特殊的方式来处理静态域,并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显示的同步。然而,这仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以避免数据破坏。

这种在第一次引用该类的时候就创建对象实例,不管实际是否需要,其缺点在于该类加载的时候就会直接new 一个静态对象出来,当系统中这样的类较多时,会使得启动速度变慢 ,所以比较适合小系统。

 

  • 2. 延迟加载(非线程安全)
public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) 
           singleton = new Singleton();
        return singleton;
    }
}

写法很简单,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象。但是这样有一个致命弱点:如果有两条线程同时调用getSingleton()方法,就有很大可能导致重复创建对象。

 

  • 3. 延迟加载(线程安全)
public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public synchronized static Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }    
}

考虑线程安全,对singleton的null判断以及new的部分使用synchronized进行加锁。由于getSingleton的代码路径很短,因此如果getSingleton没有被多个线程频繁调用。那么不会存在激烈的竞争。但是早期JVM在性能上存在一些有待优化的地方(1.5及之前),同步哪怕没有竞争的同步都存在着巨大的性能开销。

 

  • 4. 双重检查锁
public class Singleton {
 
    private static volatile Singleton singleton;
 
    private Singleton(){
 
    }
 
    public  static Singleton getSingleton(){
        if(singleton != null){
            return singleton;
        }
 
        synchronized(Singleton.class){
            if(singleton == null){
               singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

于是对上一种方式进行改进,进行双重检查,因为在大多数情况是获取一个已经构造好的Resource引用,所以代码路径并不会涉及到同步块,所以就避免了在同步上的消耗。但是又由于对于实例状态的第一次判断访问没有使用同步,将会存在可见性问题,同样是不安全的,因此需要使用volatile来保证初始化实例的可见性。

 

  • 5. 延长初始化占位类模式
public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){
    
    }
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

使用一个专门的内部静态类来初始化Singleton,JVM将推迟Holder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Singleton,因此不需要额外的同步。当任何一个线程第一次调用getSingleton时,都会使Holder被加载和被初始化,此时静态初始化器将执行Singleton的初始化操作。其实就是将单例的初始化动作放在了一个被延迟了的类加载动作中,这个类加载动作保证了单例初始化的线程安全

但是,上面提到的所有实现方式都有两个共同的缺点:需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。另外阻止不了使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

 

  • 6. 枚举方式
public enum Singleton  {
    SPRING,SUMMER,AUTUMN,WINTER;
}

使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java 也推荐使用枚举来实现单例。关于java枚举的介绍可以单独写一个篇章了,这里简单介绍下枚举类是如何做到线程安全以及防止反序列创建新对象的。

enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,可以使用反编译进行查看:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }
 
    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }
 
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

public final class T extends Enum,说明该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类。它的属性和方法都是static类型的,因此是线程安全的。

另外,之前的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

public static extends Enum> T valueOf(Class enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
}  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

#笔记内容参考

1.《java并发编程实战》

2.《java与模式》

3. http://www.hollischuang.com/archives/197

转载于:https://www.cnblogs.com/shanhm1991/p/9897462.html

你可能感兴趣的:(java设计模式1.单例模式)