转载请注明出处: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