【设计模式】单例模式

转载请注明出处:http://blog.csdn.net/u012250875/article/details/78860519

一 、概述

对象实例化的常见问题

我们编程过程中常常会遇到以下这些问题:
1)想要创建一个全局唯一的实例,以此来保证全局获取的数据信息状态一致性,如配置,登录状态等
2)创建的对象耗费资源比较严重,但没有必要频繁创建大量这样的实例,如一些连接器,解析器等

什么是单例?

定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

常见实例化手段

要确保一个类只有一个实例,我们先来看看实例化的手段有哪些,然后才能针对这些实例化手段一一进行针对,常见的实例化手段如下(有遗漏可以留言补充):

1.使用new关键字

使用new关键字是java中实例化类的最主要手段

Person p = new Person("王二小");
Dog d = new Dog();
2.使用反射机制

使用反射实例化,通用流程是三步走:
1)获取元类(什么是元类?
2)通过元类获取构造器
3)通过构造器实例化对象

下面我们以反射获取Person实例为例进行演示

Class Person{
    public String name;
    public Person(){
    }
    public Person(String name){
        this.name = name;
    }
}

public static void main(String[] args) throws Exception {
    //1.获取元类
    Class<Person> cls = Person.class;
    //2.获取构造方法
    Constructor<Person> con = cls.getConstructor(String.class);//如果存在无参构造器,第二步第三步可以用cls.newInstance()直接代替,但内部实现是一样的
    //3.用构造方法实例化
    Person xm = con.newInstance("xiaoming");
}
3.使用反序列化

使用反序列化实例化对象,一般都是先将对象进行序列化,使用时在进行反序列化成对象

public static void main(String[] args) throws Exception{
    Person obj = Person("xiaoming");
    serialize(obj, "E:\\xm.txt");//序列化到本地
    Person xm = (Person) deSerialize("E:\\xm.txt");//反序列化成对象
}

//序列化
public static void serialize(Object obj, String path) throws IOException {
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream(path));
        oos.writeObject(obj);
    } finally {
        if (null != oos){
            oos.close();
        }
    }
}
//反序列化
public static Object deSerialize(String path) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream(path));
        return ois.readObject();
    } finally {
        if (null != ois)
            ois.close();
        }
    }
}
4.使用clone

使用clone方法进行创建实例,需要实现Cloneable接口,如果自定义类实现该接口,需要重写clone()方法将clone方法的修饰符改为public,外部才能进行调用。

public static void main(String[] args) throws  Exception{
    Person p = new Person();
    Person p2 = (Person) p.clone();//可以利用已存在的person实例,clone出一个新对象
}

public class Person implements Cloneable{
    //成员属性,成员方法省略
    @Override
    public Object clone() throws CloneNotSupportedException{
        //方法体省略......
    }
}

二 、单例模式的演进

为了保证类的实例个数有且仅有一个,我们都过针对上述各种实例化手段来解决问题,而实例化手段中最主要的方式是new,因此先解决主要矛盾,然后依次优化性能,保证线程安全,解决反序列化问题,反射问题。我们下面一点一点来演进整个过程。

1.饿汉式单例1

首先我们想要控制new的使用,最容易想到的就是私有化构造方法,这样除本类以外都不能使用new关键字来实例化了,这样实例化过程必然要放在单例类中,然后提供一个类方法供外部访问这个唯一实例。下面是最简单的单例,饿汉式单例,申明静态变量并直接实例化。

public class Singleton01 {
    private static final Singleton01 INSTANCE = new Singleton01();

    private Singleton01(){
        Utils.sleep(1000);
    }

    public static Singleton01 getInstance(){
        return INSTANCE;
    }

    public void dosomething(){
        Utils.sleep(1000);
    }

}

2.饿汉式单例2

先申请静态变量,然后在静态块中进行初始化,和饿汉式单例1大同小异,仅仅初始化位置稍有区别。

public class Singleton02 {
    private static Singleton02 INSTANCE = null;
    /**
     * 类加载时会执行静态块
     */
    static {
        INSTANCE = new Singleton02();
    }

    private Singleton02() {
        Utils.sleep(1000);
    }

    public static Singleton02 getInstance() {
        return INSTANCE;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

饿汉式单例一和饿汉式单例2是否线程安全呢?答案是肯定的,因为static变量和static初始化块的初始化过程是串行,是由jls(java语言特性)保证。

3.懒汉式单例1(单线程可用,多线程不安全单例)

针对饿汉式单例,存在一点问题,不管是否使用,饿汉式单例一开始就将类实例化了,占据着内存资源,我们可以通过懒加载进行优化,直到第一次调用类方法要使用实例时才去实例化,因此就有了下面的代码

public class Singleton03 {
    private static Singleton03 instance = null;

    private Singleton03() {
        Utils.sleep(1000);
    }

    public static Singleton03 getInstance() {
        if (instance == null) {
            instance = new Singleton03();
        }
        return instance;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

这样写确实是懒加载,但是我们发现在多线程的时候却会出现问题,你会发现多线程调用getInstance获取到的可能并不是同一个单,尤其是单例的构造方法执行时间较长时,进行多线程并发,这种现象就很明显(demo中故意在构造方法中睡眠了1秒钟)。模拟一下两个线程执行过程,假如线程1执行到if (instance == null)的时候对线程1来说if条件成立,此时线程1交出cpu资源,如果线程2也执行到if (instance == null),由于线程1还没有执行new,因此对于线程2来说if条件也成立,接下来线程2交出cpu资源,线程1执行,则会new一个实例出来,接下来线程1再交出cpu资源,轮到线程2执行,由于已经执行过if语句了,并且成立,所以线程2也执行new的操作,这样就产生了两个实例,两个线程执行尚且如此,更多线程时,执行情况更不可预料。因此上面的写法单线程下尚可使用,多线程下是不安全的。

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(){
                public void run() {
                    System.out.println(Singleton03.getInstance());
                };
            }.start();
        }
    }
//打印结果
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@20d60561
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@1216b8f7
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@59cda0d
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@67d26a8e
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@6826907c
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@b7127ff
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@7d23e299
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@6b8d721c
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@4a96a9c7
pyf.ood.dp._01创建类._01单例模式._01单例.Singleton03@6441c612

4.懒汉式单例2(多线程安全单例,性能较差)

懒汉式单例最简单保证线程的方法是在方法上加上synchronized 同步关键字,多个线程执行该方法时,会一个一个执行完后才会轮到下一个执行,因此就不存在线程不安全的问题。

public class Singleton04 {
    private static Singleton04 instance = null;

    private Singleton04() {
        Utils.sleep(1000);
    }

    public synchronized static Singleton04 getInstance() {
        if (instance == null) {
            instance = new Singleton04();
        }
        return instance;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

5.懒汉式单例3(采用同步块方式同步部分代码来优化性能,但仍然有问题)

懒汉式单例2是线程安全的,但是效率存在问题,因为对整个方法加同步锁了,因此并发时整个方法体的执行过程都需要进行串行,其实我们发现只有实例为空的时候new 实例的过程才需要进行同步,因此将同步锁加到判空语句以内,new实例的外面。如下

public class Singleton05 {
    private static Singleton05 instance = null;

    private Singleton05() {
        Utils.sleep(1000);
    }

    public static Singleton05 getInstance() {
        if (instance == null) {
            synchronized (Singleton05.class) {
                instance = new Singleton05();
            }
        }
        return instance;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

6.懒汉式单例4(同步块+双检锁优化性能,但遇到指令重排序会有问题)

懒汉式单例3中,使用同步块在if (instance == null) 内部对实例化单例的代码上进行加锁依旧会有问题,在什么时候会有问题?在调用getInstance()方法的第一次且第一次访问就是多线程并发访问时,就会产生出现多实例问题,因为多个线程第一次并发调用该方法时,由于是第一次调用,所以instance未实例化,如果线程轮流执行if (instance == null)语句,则会出现所有线程都可以依次进入到if分支内部,然后排队轮流执行同步块中的new操作。如果第一次调用getInstance()方法是单线程,则以后的多线程并发访问也是安全的,那怎么解决第一次就产生并发访问的问题呢,在同步块内部在进行一次判空,由于第一次执行,即使都绕过了外层的判空语句,由于同步块中的代码是同步的,需要串行执行,因此,同步块内部这个判空语句是绕不开的,只要实例化一次后,内层判空语句必定为false,因此就保证第一次多线程并发不会出现问题,这就是双检锁(DCL)。DCL方式,通过内层判空语句保证首次调用的线程安全,通过外部判空语句保证非首次访问时的效率

public class Singleton06 {
    private static Singleton06 instance = null;

    private Singleton06() {
        Utils.sleep(1000);
    }

    public static Singleton06 getInstance() {
        if (instance == null) {
            synchronized (Singleton06.class) {
                if (instance == null) {
                    instance = new Singleton06();
                }
            }
        }
        return instance;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

7.懒汉式单例5(同步块+双检锁优化性能,并禁止指令重排序,但jdk1.5以下不可用)

懒汉式单例4采用了DCL方式,看似很完美了,但是事情真是这样的么?其实不然,由于jvm会进行指令重排序。什么叫指令重排序,就是jvm为了优化性能,对原子指令进行乱序执行,这里的乱序是在保证非原子指令正常执行的前提下进行乱序的,如instance = new Singleton06()这句代码,是非原子指令,他还可以拆解成三个原子步奏:
1)申请内存空间
2)实例化对象,为对象成员变量赋初值
3)将instance引用指向分配的内存
这三步中,1的顺序是确定的,一定是最先执行,但是2和3到底谁先执行并不能保证,所以可能出现1-2-3或者1-3-2,如果出现1-3-2这种情况,执行完1再执行3,由于执行了3时,instance相当于已经指向内存完成,即instance引用指向不为null,假如此时有一个新线程调用getInstance()时,先进入外层判空条件,会发现instance不为空,直接返回给调用处,由于实际上instance指向的实例并没初始化,使用该引用将出现问题。
如何解决该问题,该问题的缘由就是由于jvm的指令重排序造成的,那么只要让jvm不进行指令重排序即可,java中提供了一个关键字volatile(volatile有两层语义,这里使用的是禁止重排序的语义),将该关键字去修饰instance变量,则在instance实例化过程中会避免重排序,那么问题得到解决,代码如下

public class Singleton07 {
    // volatile关键字有两层语义:1.可见性(工作内存中变量修改后立即刷入主内存) 2.禁止指令重排序
    private static volatile Singleton07 instance = null;

    private Singleton07() {
        Utils.sleep(1000);
    }

    public static Singleton07 getInstance() {
        if (instance == null) {
            synchronized (Singleton07.class) {
                if (instance == null) {
                    instance = new Singleton07();
                }
            }
        }
        return instance;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

8.懒汉式单例6(采用静态内部类的方式)

懒汉式单例5使用 懒加载+双检锁+volatile实现单例,很完美?依旧不完美,由于java的volatile是个保留字,在jdk中很早就有了,但是如其名,只是保留字,知道jdk1.5才进行实现,因此volatile在jdk1.5前是无效的,好在现在大多数环境都是1.5及以上,倒也无妨。有没有更好的方法实现单例懒加载呢?有,如下所示采用静态内部类的方式

public class Singleton08 {
    private Singleton08() {
        Utils.sleep(1000);
    }

    public static Singleton08 getInstance() {
        return InnerClass.INSTANCE;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }

    private static class InnerClass {
        public final static Singleton08 INSTANCE = new Singleton08();
    }
}

为什么静态内部类能保证线程安全,且能实现懒加载呢?
保证线程安全:是由于静态变量实例化过程由jls保证多线程安全;
延迟加载:是由静态内部类只在被使用时(本例中为执行return InnerClass.INSTANCE)才会被装载,类被装载后,类中静态变量才会进行实例化

9.枚举式单例

除了上面的各种实现复杂的单例,还有一种简单的实现方式,就是枚举,写法简单,调用也很简单,写法如下:

public enum Singleton09 {

    INSTANCE;

    public void dosomething() {
        Utils.sleep(1000);
    }
}

//调用
public static void main(String[] args) throws Exception {
    Singleton09.INSTANCE.dosomething();//调用单例执行doSomething方法
}

枚举为什么能实现单例?由枚举特性保证!
枚举特性:
1.构造方法私有化
2.每个实例分别是一个全局常量,并且不能使用new实例化
3.不能序列化

枚举唯一的缺点是枚举在jdk1.5提出,因此只能在jdk1.4以后使用。(对现在jdk1.5,1.6普及的今天其实不算什么缺点,jdk1.9都有了,只是还未普及…)

10.防止反序列化破坏单例

上面各种花式写单例,都是针对怎么防止new出多个实例来,下面看看怎么防止被反序列化。第一不要实现序列化接口,但是要是实现了序列化接口,该怎么防止反序列化?下面看看如何处理,该方法来自Effective Java中序列化章节中提及的问题,readResolve()方法会在反序列化时被调用并返回结果,我们这里只要重新实现该方法即可,并将我们的INSTANCE进行返回就能保证反序列化后依旧是我们刚才的实例,写入如下。(疑问:readResolve方法到底是谁的方法?有弄清楚该问题的可以告诉一声)

public class Singleton10 implements Serializable {
    private static final Singleton10 INSTANCE = new Singleton10();

    private Singleton10() {
        Utils.sleep(1000);
    }

    public static Singleton10 getInstance() {
        return INSTANCE;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

11.防止反射破坏单例(饿汉式单例防止反射需要特别注意静态变量顺序问题)

除了上面几种破坏单例的方式外,还有反射可以破坏单例,我们针对反射,只需要添加一个标记即可,标记该类是否已经实例化过了,如果已经进行实例化了,则抛出异常,否则正常实例化,实现代码和测试代码如下

//懒汉式防止反射破坏
public class Singleton11 implements Serializable {
    private static final long serialVersionUID = 1L;
    private static volatile Singleton11 INSTANCE;
    private static volatile boolean ISINITIAL = false;

    private Singleton11() {
        synchronized (Singleton11.class) {//记得构造方法需要同步处理,否则依旧不能保证单例
            if (ISINITIAL) {
                throw new RuntimeException("该类已经实例化");
            } else {
                ISINITIAL = true;
            }
        }

        // Utils.sleep(1000);
    }

    public static Singleton11 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton11.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton11();
                }
            }
        }
        return INSTANCE;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }

    /**
     * 针对反序列化时多例问题的解决
     * 
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    //防止反射破坏单例的验证如下
    public static void main(String[] args) throws Exception {
        // 反射测试
        System.out.println(Singleton11.getInstance());
        Constructor<?>[] c = Singleton11.class.getDeclaredConstructors();
        System.out.println(c.length);
        c[0].setAccessible(true);
        c[0].newInstance();//直接抛出运行时异常
    }

}

//饿汉式防止反射破坏
public class Singleton12 implements Serializable {
    private static final long serialVersionUID = 1L;
    private static volatile boolean ISINITIAL = false;//如果采用饿汉式单例,该boolean量放在INSTANCE之前,否则失效
    private static Singleton12 INSTANCE = new Singleton12();
    // private static volatile boolean ISINITIAL = false;// 如果采用饿汉式单例,该boolean量放在INSTANCE之前,否则失效

    private Singleton12() {
        synchronized (Singleton12.class) {
            if (ISINITIAL) {
                throw new RuntimeException("该类已经实例化");
            } else {
                ISINITIAL = true;
            }
        }

    }

    public static Singleton12 getInstance() {
        return INSTANCE;
    }

    public void dosomething() {
        Utils.sleep(1000);
    }

    /**
     * 针对反序列化时多例问题的解决
     * 
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        // 反射测试
        System.out.println(Singleton12.getInstance());
        Constructor<?>[] c = Singleton12.class.getDeclaredConstructors();
        System.out.println(Singleton12.class.getDeclaredField("ISINITIAL").getBoolean(null));
        ;
        c[0].setAccessible(true);
        c[0].newInstance();//直接抛出运行时异常
    }

}

12.分布式中的单例

在分布式环境中,单例模式将失效(所有的多元jvm环境中,上述的单例模式将不成立),此时可以依靠jboss,weblogic等提供的单例服务来实现单例需求。

13.小结:

可用的单例写法:
1.采用静态变量实现饿汉式单例
2.采用静态块实现饿汉式单例
3.采用双检锁+volatile+同步块的懒汉式单例(jdk1.4+可用)
4.采用枚举实现单例(jdk1.4+可用)
5.采用静态内部类实现懒汉式单例
6.防止反序列化破坏单例,要防止实现序列化接口,如果实现序列化接口则重写readResolve方法来组织破坏
7.防止clone破坏单例,防止实现clone接口
8.防止反射破坏单例,采用标记位来判断是否只实例化过一次

三 、扩展

单例模式的本质到底是什么?写了这么多,其实单例模式想做的事情就是控制实例个数,而单例是实例个数控制的特例,当需要控制的数量为一变为单例。单例的本质是对实例个数的控制!下面来看看单例模式的常见变体

1.有上限的多例模式

持有有限个实例个数,这样可以在减少频繁的new实例情况下,提升处理能力,如同一个餐厅,我们没必要每炒一盘菜就雇佣一个厨师。一天卖20个才需要请二十个厨师?这就是频繁new的毛病,但是当一个厨师忙不过来,我们可以再请一个或两个厨师同时开工。看到这里,多例模式似乎很像一个概念:池,他与池有相似处,但也有差异。

public class MultiInstance {
    private final static int NUMBER_OF_INSTANCE = 2;
    private final static List<MultiInstance> list = new ArrayList<>();
    static {
        for (int i = 0; i < NUMBER_OF_INSTANCE; i++) {
            list.add(new MultiInstance());
        }
    }

    public static MultiInstance getInstance() {
        return list.get(new Random().nextInt(NUMBER_OF_INSTANCE));
    }

    private MultiInstance() {
        Utils.sleep(1000);
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

2.登记式单例

登记式单例,每产生一个对象就进行登记,如果下次需要某个单例,首先查找一下,是否已经有了,有则进行复用,没有再创建,一般分两种,一种是登记单例类本身,另一种是登记其他类,应用场景各有差异。
1)登记自己
下面的写法只做一个示例,演示通过map的key来登记一个自身示例。这种形式的单例一般可应用在还原点的保存,如记录某个时间点的状态的数据,一般采用reg,save方法。

public class RegSingleton {
    private static final Map<String, RegSingleton> map = new HashMap<>();

    public static RegSingleton getInstance(String key) {
        if (!map.containsKey(key)) {
            synchronized (RegSingleton.class) {
                if (!map.containsKey(key)) {
                    RegSingleton singleton = new RegSingleton();
                    map.put(key, singleton);
                }
            }
        }
        return map.get(key);
    }

    private RegSingleton() {
        Utils.sleep(1000);
    }

    public void dosomething() {
        Utils.sleep(1000);
    }
}

2)登记其他类
这种登记式单例,做android的小伙伴一定很眼熟,看到第一眼应该想到了Context.getSystemService(String name)方法了。是的,我们平时获取各种管理器就采用这种形式,系统初始环境时,已经将各个管理器进行登记到了上下文中,通过该形式作为统一出口进行提供各个系统的管理器。同样在spring中的查询服务中也是类似的形式,采用lookup(name)来查找注册的单例。

这种形式的登记式单例的使用场景一般是:当系统中存在大量单例时,可以采用这种方式来提供统一的单例获取渠道,先将所有单例进行登记,然后由调用者通过name去获取然后使用。

public class RegSingleton2 {
    public static final String WINDOW_SERVICE = "WindowManger";
    public static final String ACITIVITY_SERVICE = "ActivityManger";
    public static final String ALARM_SERVICE = "AlarmManager";

    private static final Map<String, Object> map = new HashMap<>();

    static {
        map.put(ALARM_SERVICE, AlarmManager.getInstance());
        map.put(WINDOW_SERVICE, WindowManager.getInstance());
        map.put(ACITIVITY_SERVICE, ActivityManager.getInstance());
    }

    public static synchronized void registerService(String key, Object service) {
        if (!map.containsKey(key)) {
            map.put(key, service);
        }
    }

    public static <T> T getInstance(String key) {
        return (T) map.get(key);
    }

    private RegSingleton2() {
        Utils.sleep(1000);
    }

    public void dosomething() {
        Utils.sleep(1000);
    }

    public static void main(String[] args) {
        ActivityManager acm = RegSingleton2.getInstance(ACITIVITY_SERVICE);
        acm.dosomething();
        Object diyService = new Object();
        RegSingleton2.registerService("diy", diyService);
        Object diy1 = RegSingleton2.getInstance("diy");
        Object diy2 = RegSingleton2.getInstance("diy");
        System.out.println(diy1 == diy2);
    }

    static class AlarmManager {
        private static final AlarmManager INSTANCE = new AlarmManager();

        private AlarmManager() {

        }

        public static AlarmManager getInstance() {
            return INSTANCE;
        }

        public void dosomething() {
            System.out.println("我管理闹钟系统");
        }
    }

    static class ActivityManager {
        private static final ActivityManager INSTANCE = new ActivityManager();

        private ActivityManager() {

        }

        public static ActivityManager getInstance() {
            return INSTANCE;
        }

        public void dosomething() {
            System.out.println("我管理Acitivity");
        }
    }

    static class WindowManager {
        private static final WindowManager INSTANCE = new WindowManager();

        private WindowManager() {

        }

        public static WindowManager getInstance() {
            return INSTANCE;
        }

        public void dosomething() {
            System.out.println("我管理Window");
        }
    }
}

四、 应用

上面对单例进行详细说明,下面来看看怎么应用到实战中去

1.全局配置

假如用户登录我们的app后,在各处业务中都会用到登录的user相关的信息(如id,username等),登录状态,偏好配置。常做的方法就是状态存入数据库或者xml中,要使用时进行查询。但是从性能角度,应该优先放入内存中,供随时读取使用,避免频繁查询。由于全局公用的一些状态需要全局一致性,因此应该采用单例完成,并且初始化单例时,将成员属性进行初始化。如下

public class AppConfig {
    private static AppConfig instance;
    private User user;
    //偏好配置等其他属性,这里省略,用user说明问题即可


    private AppConfig(Context ctx) throws Exception {
        //采用多级缓存机制
        this.user = Mock.getFromDB(ctx);
        if (this.user == null) {
            this.user = Mock.getFromNetwork(ctx);
            if (this.user != null) {
                Mock.saveToDB(this.user);
            } else {
                throw new Exception("未查询到该用户");
            }
        }
    }

    public static synchronized AppConfig getInstance(Context ctx) throws Exception {
        if (instance == null) {
            instance = new AppConfig(ctx);
        }
        return instance;
    }

    public User getUser() {
        return this.user;
    }

}

2.负载均衡器

负载均衡是提供集群设备可用性的一种方式,如何将请求分发到不同的服务器上,如何根据服务器压力大小决定是否增加添加服务器还是删减服务器,这样逻辑管理就需要一个单例类来控制,如果采用多个实例来控制请求分发,服务器增减将大大的增加实现的复杂度,因为你需要将每次的执行策略告诉其他的负载均衡管理器,以保证信息公知,下面做一个示例

public class LoaderBalancer {
    private static final LoaderBalancer INSTANCE = new LoaderBalancer();
    private List<Server> servers = new ArrayList<>();

    private LoaderBalancer() {
        for (int i = 0; i < 10; i++) {
            Server s = new Server();
            // 初始化服务器
            s.setConnNumber(0);
            // ...略
            servers.add(s);
        }
    }

    public static LoaderBalancer getInstance() {
        return INSTANCE;
    }

    public synchronized void route(String request) {
        // TODO 根据可用性权重将请求分配到不同服务器,根据压力来增减服务器
        // 常用方法:轮询法,加权轮询法,随机法,加权随机法,哈希法,连接数最小法

        System.out.println("负载均衡器做了一些事情");
    }

    private void addServer(Server s) {
        servers.add(s);
        System.out.println("增加了一台服务器");
    }

    private void removeServer(Server s) {
        servers.remove(s);
        System.out.println("移除了一台服务器");
    }

    static class Server {
        private String ip;
        private int connNumber;
        private Memery memery;
        private HardPan hardPan;
        private CPU cpu;
        private int availability;

        public int getAvailability() {
            // TODO 根据内存,cpu,硬盘,连接数等硬件性能推出一个可用性值,这里随机下
            availability = new Random().nextInt(100);
            return availability;
        }

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public int getConnNumber() {
            return connNumber;
        }

        public void setConnNumber(int connNumber) {
            this.connNumber = connNumber;
        }

        public Memery getMemery() {
            return memery;
        }

        public void setMemery(Memery memery) {
            this.memery = memery;
        }

        public HardPan getHardPan() {
            return hardPan;
        }

        public void setHardPan(HardPan hardPan) {
            this.hardPan = hardPan;
        }

        public CPU getCpu() {
            return cpu;
        }

        public void setCpu(CPU cpu) {
            this.cpu = cpu;
        }
    }

    class HardPan {
        long size;
        long usedSize;
        String brand;

        public long getSize() {
            return size;
        }

        public void setSize(long size) {
            this.size = size;
        }

        public long getUsedSize() {
            return usedSize;
        }

        public void setUsedSize(long usedSize) {
            this.usedSize = usedSize;
        }

        public String getBrand() {
            return brand;
        }

        public void setBrand(String brand) {
            this.brand = brand;
        }
    }

    class Memery {
        long size;
        float usedSize;

        public long getSize() {
            return size;
        }

        public void setSize(long size) {
            this.size = size;
        }

        public float getUsedSize() {
            return usedSize;
        }

        public void setUsedSize(float usedSize) {
            this.usedSize = usedSize;
        }
    }

    class CPU {
        long frequency;
        float usedPercent;

        public long getFrequency() {
            return frequency;
        }

        public void setFrequency(long frequency) {
            this.frequency = frequency;
        }

        public float getUsedPercent() {
            return usedPercent;
        }

        public void setUsedPercent(float usedPercent) {
            this.usedPercent = usedPercent;
        }
    }
}

3.计数器

要实现一个全局可用的计数器,单例往往也是较好的手段,代码如下

public class Counter {
    private static final Counter INSTANCE = new Counter();
    private int counter;

    private Counter() {

    }

    public static Counter getInstance() {
        return INSTANCE;
    }

    public synchronized int increase() {
        return counter++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    System.out.println(Counter.getInstance().increase());
                };
            }.start();
        }
    }
}

为什么不采用静态变量呢?因为使用静态变量不能保证多线程安全!

4.其他应用

1)系统API Runtime Runtime.getRuntime()
2)spring的bean默认单例
3)spring中lookup(“”)
4)android中的LayoutInflater
5)android中的ImageLoader图片加载框架(一般在Application中初始化的类一般都是单例)
6)android中许多第三方日志工具(android中的日志框架一般都会在Application中配置)
7)gson解析json时,往往会封装一个单例类,如果解析频繁,则可以采用多例模式
8)android中的EventBus,EventBus.getDefault()

五 、一些的问题

1.单例与工具类区别

1)工具类往往和单例类一样,采用私有化的构造方法,
2)工具类中所有方法采用static修饰,而单例类一般只有入口方法采用static修饰,其他方法采用实例方法形式。
3)单例类往往有状态,而工具方法往往是无状态的。

2.单例与静态变量的选择

1) 全局变量只是提供了对象的全局的静态引用,但并不能确保只有一个实例;
2) 全局变量是急切实例化的,在程序一开始就创建好对象,对非常耗费资源的对象,或是程序执行过程中一直没有用到的对象,都会形成浪费;
3) 静态初始化时可能信息不完全,无法实例化一个对象。即可能需要使用到程序中稍后才计算出来的值才能创建单例;
4) 使用全局变量容易造成命名空间(namespace)污染
5)静态变量不能保证线程安全

3.Android开发中使用单例的注意事项

android中经常用到单例,如前面举例的全局配置。但是android需要注意的一个问题是,如果app发生crash,单例中的静态变量会变为null,因此要采用多级缓存机制来进行恢复静态变量中的数据。保证各处调用时业务的正确性。这也是为什么android不要用静态变量来保持状态的原因,如果仅仅用静态变量来保存状态,一旦发生crash,则会出现使用静态变量的地方也出现crash,这样导致crash雪崩。
从缓存中恢复的实例和crash之前的实例不是一个实例啊,这能算单例么?其实我们在很多时候需要的是全局同一时间有唯一实例,让全局共享该实例的状态,这样就足够了。

参考
设计模式与禅
那些年,我们一起写过的“单例模式”:http://www.cnblogs.com/bugly/p/6541983.html
Effective Java

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值