设计模式之单例模式(java)

Java实现单例

单例模式:顾名思义就是这个类只能产生一个对象,无论你实例化多少次,他总是指向内存中的同一块区域。
如何能保证一个类只能实例化一个对象呢?不如我们先看看如何实例化对象,请看下面的代码片段
public class Men {
}
没错,上面的Men类空空如也,表面看似如此,但是根据Java语言的规范,一个类要是没有显式的定义构造器的时候,会有一个空参构造器(有人称之为默认构造器),而此构造器的实现继承自基类java.lang.Object,请看下面代码片段
public class Men {
    public Men() {
        super();
    }
}
有了构造器,下面我们来对此进行实例化,请看:
Men i = new Men();
当代码执行到这一行的时候,类加载类首先加载Men.class文件(如果此类没有被加载),堆内存中开辟空间,然后将创建好的对象的引用赋值给变量 i;实例化结束。从实例化的过程可以看的出来,每一个new关键字都会开辟内存,实例化一个新的对象,
问题来了,如何保证使用者只new一次,没有办法,不如将对象实例化的权利只交给自己一个人,请看代码:
public class Men {
    // 私有化构造器
    private Men() {
        super();
    }
}
这样构造器私有化以后,使用者是无法擅自实例化对象的,但是他们又要如何创建对象呢?我要提供一个创建对象的静态方法给他,为什么会是静态的呢?因为他在调用此方法之前还没有实例,只能调用类级别的方法。而此方法的名字一般为newInstance,当然你也可以是任意符合语法的名字,请看:
public class Men {
    private Men() {
        super();
    }
    public static Men newInstance(){
        return new Men();
    }
}
如此以来,使用者就只能调用newInstance方法实例化类,或者说获取实例对象,但是目前还不能达到单例的目的,请继续看下面的片段:
public class Men {
    private static Men i = new Men();
    private Men() {
        super();
    }
    public static Men newInstance(){
        return i;
    }
}
上面代码从理论上来说,静态属性在类被初次加载时执行一次初始化,无论你调用多少次newInstance方法,每次返回的都是同一个实例,对吗?不如我们做个测试,请看:
import static org.junit.Assert.*;
import org.junit.Test;

public class MenTest {
    @Test
    public void testNewInstance() {

        Men i1 = Men.newInstance();
        Men i2 = Men.newInstance();

        assertTrue(i1.equals(i2));
    }
}
执行上面的单元测试,你会看见一道绿条出现在你的眼前,恭喜,你的方法中的断言是正确的。说明了i1和i2是同一个对象。这样我们就实现了一个单例,
  • Keep the bar green to keep the code clean.要时刻记住这句话,曾经我有个同事看见我写下@Test的时候,他说好的程序员不需要写单元测试,从前人的经验来看,单位测试还是比较重要的,不要怕麻烦而忽略的单步测试,当你的应用日益庞大起来,单元测试尤为重要。有空的话开篇博客专门讨论。
言归正传,话说我们已经有了单例,但是我们每次加载这个类的时候都要用到它的实例么?未必。请看:
public class Men {
    private static Men i = new Men();
    private byte[] data;
    private Men() {
        super();
        try {
            Thread.sleep(100000);
            data = new byte[1024 * 1000000];
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static Men newInstance() {
        return i;
    }
    public static String sayHello(){
        return "Hello";
    }
}
我们只想调用sayHello这个静态方法,不需要实例对象,然而根据类的加载机制,当你Men.sayHello()的时候必须实例化对象,从构造器可以看出,实例化这个对象的过程相当的痛苦,既费时又费内存,如何是好。
上面单例模式的实现,有人称之为“饿汉式”,饿急了,一上来就初始化,当然问题也一目了然。为了解决此问题,我们讨论单例的另一种实现——”懒汉式“,请看如下代码:
public class Men {
    private static Men i = null;
    private Men() {
        super();
    }
    public static Men newInstance() {
        if (i == null) {
            i = new Men();
        }
        return i;
    }
}
同样我们还是执行上段单元测试,因为我们对外暴露的方法始终没有变化,你同样会看到一个绿bar.说明我们不管调用多少次实例化方法,实例始终只有一个,没有问题,是的,单线程运行当然没有问题,那么多线程呢?下来我们对程序稍稍改动,让他运行慢下来,我们细细看看。请看:
public class Men {
    private static Men i = null;
    private Men() {
        super();
    }
    public static Men newInstance() {
        if (i == null) {
            try {
                Thread.sleep(10000);
                i = new Men();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return i;
    }
}
  • 线程睡眠只是模拟方法执行需要很长时间,没有实际意义。所有的异常都让虚拟机来为我们处理,我们只关注单例本身。
下面代码让他运行再多线程环境下,请看单元测试:
public class MenTest {
    @Test
    public void testNewInstance2() throws InterruptedException {

        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start();
        thread2.start();

        Thread.sleep(20000); //保证主线程运行的时间长于其他子线程

    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Men i = Men.newInstance();
        System.out.println(i);
    }
}
从控制台可以看到我们两个线程打印的i有时候是相同的,而有时候是不同的,当然,你的方法越耗时,i不是同一实例的几率越高。
分析一下,当第一个线程执行run方法的时候他调用Men.newInstance();判断 i == null为真,进入if语句内部,程序再慢慢的执行,还没有执行到 i = new Men();情况发生了变化,第二个线程夺取了CPU的执行权,当他执行到if条件的时候,当然也为真,这种情况下,两个线程都进入 If块,创建两个实例是迟早的事情,如何解决这个问题,请看代码:
public class Men {
    private static Men i = null;
    private Men() {
        super();
    }
    public synchronized static Men newInstance() {
        if (i == null) {
            try {
                Thread.sleep(10000);
                i = new Men();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return i;
    }
}
仔细看看这个改造有什么变化,方法签名只多了个关键字synchronized,这样一个线程只能等待另一个线程执行完后才能执行,再执行测试用例,即使你的方法执行的时间再长,两个线程打印的i都是一样的。好,完美了吗?继续往下看。
synchronized修饰方法的时候,整个方法都要线程等待,而我们这个方法中假如有别不存在线程安全的代码的话,这个方法加锁势必会影响性能,我们知否只要保证判空和实例化原子的进行就OK呢,继续改造,请看代码:
public class Men {
    private static Men i = null;
    private Men() {
        super();
    }
    public static Men newInstance() {
        synchronized (Men.class) {
            if (i == null) {
                try {
                    Thread.sleep(10000);
                    i = new Men();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return i;
    }
}
继续跑用例,但是同一个对象,貌似我们的实现已经很完美了,用大腿再想想。
  • 为了代码的清洁,下面的代码中我将去掉线程睡眠部分
考虑这样一个问题,我们执行到Men.newInstance()的时候,如果说i变量已经不为null,也就是说程序的其他部分已经调用过newIntance()方法了,我们还有必要让程序进入同步块吗?答案是显然的,请看代码:
public class Men {
    private static Men i = null;

    private Men() {
        super();
    }

    public static Men newInstance() {
        if (i == null) {
            synchronized (Men.class) {
                if (i == null) {
                    i = new Men();
                }
            }
        }
        return i;
    }
}
一切都完美了,这就是double-checked locking设计实现单例模式。
然而事情并非你想的那么简单。我们的程序也越来越复杂了,此事要从JVM说起,JVM只定义了标准,并不是具体的实现,在实例化对象的时候,我们假设两个内存模型:
  1. 开辟内存空间,然后把内存地址返回给变量i,下来对刚才开辟的空间进行初始化操作。
  2. 开辟内存空间,先初始化刚才开辟的空间,初始化结束后将内存地址返回给变量i。
第二种内存模型当然没有问题,我们来分析第一种,当线程1实例化对象的时候,开辟完了空间,将内存地址返回给了变量i,但是还没有初始化,这时候线程2来了, 他检测到i已经不是null,直接返回了,显然这时候的i还是个残疾,程序就出错了。
还是不完美,难道Java中没有实现单例的方法吗?肯定有,在JDK5之后,volatile关键字终于有了它的价值。下面是对这个关键字的描述,他很复杂,这里不再深入讨论。
volatile: Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.
继续看我们的代码:
public class Men {
    private volatile static Men i = null;
    private Men() {
        super();
    }
    public static Men newInstance() {
        if (i == null) {
            synchronized (Men.class) {
                if (i == null) {
                    i = new Men();
                }
            }
        }
        return i;
    }
}
可以看出,我们用volatile关键字去修饰属性,问题得到解决了,这下该完美了吧,要是我们遇到了历史项目呢,而他的编译级别恰恰是JDK5之前的版本。
单例模式之最终版本,先看代码:
public class Men {

    private Men() {
        super();
    }

    private static class ProductInstance {
        private static final Men i = new Men();
    }

    public static Men newInstance() {
        return ProductInstance.i;
    }
}
首先此类没有静态属性,所以类加载的时候不会初始化,只有当调用newInstance()的时候,才会加载静态内部类PorductInstance,而它却有一个静态属性,也就是我们要生产的实例对象。最后一种方法简单实用,并且Effiective Java也推荐的这种方式。到此,我们完美的实现了单例模式。
但是,博文还没有完,再往下看:
package com.zhangkai.test1;

/**
 * 
 * @author ZhangKai
 *
 */
public enum MenEnum {

    I;

    public String sayHello() {
        return "Hello";
    }
}
JDK5之后加入的新成员enum,我们都知道,每一个枚举值是他的一个实例,那我们定义一个枚举值,他自然只有一个实例,当然,有了枚举,然我们实现“多少例”模式,都没有问题,具体枚举的其他功能,我们有空再讨论。请看单元测试:
package com.zhangkai.test1;

import static org.junit.Assert.*;

import org.junit.Test;

public class MenEnumTest {

    @Test
    public void test() {
        String re = MenEnum.I.sayHello();
        assertEquals("Hello", re);
    }

}
keep the bar green.
写到最后,long long ago,看到一篇文档,已经找不到出处,幸运的是,当时以文本的形式保存至今,除枚举部分外,以上内容均以那篇文章为基础,以个人理解,整理成文,
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值