1 管理公共资源的对象只需要一个
1.1 有时只需要一个对象
每一个类都可以通过new来生成若干个对象,但是有些对象我们只需要一个,如:线程池,注册表,日志等对象,这些对象只能有一个,如果制造出多个对象,就会导致程序的行为异常,资源使用过量,导致不一样的结果等
像某个普通的类,如Person,可以随便的生成Person对象,因为Person类的初衷就是为了生成大量Person对象,而类似于注册表这种对象,是为了管理公共资源的,多个注册表会导致混乱,这种对象应该有且仅有一个
1.2 单例模式
单例模式:
确保一个类只有一个实例,并提供一个全局访问点
使用单例模式创建唯一的对象,可以这样做:
/**
* @author 雫
* @date 2021/3/4 - 11:57
* @function 注册表
* 该类只提供一个对象
*/
public class Registry {
private static Registry instance;
private Registry() {
}
public static Registry getInstance() {
if(instance == null) {
instance = new Registry();
}
return instance;
}
}
在上述的程序中,我们将构造器改为private类型,因此该类的对象不能在外部被创建,但提供一个静态方法getInstance()来让用户取得唯一的注册表对象,如果是第一次调用getInstance(),就会调用构造方法生成一个Registry对象,之后的每次调用都返回第一次创建好了的Registry对象
这里生成唯一对象的方式,采用了懒汉模式,即需要时再创建,这对资源敏感的对象特别重要
如果需要注册表对象,利用单例模式就可以保证只有一个,这样的全局资源只有一份,单例模式常常被用来管理共享的资源,如数据库连接池或线程池等
1.3 模拟巧克力锅炉
每个巧克力工厂都有一个锅炉,用来将巧克力和牛奶融合到一起,来看一个锅炉的模拟类:
/**
* @author 雫
* @date 2021/3/4 - 12:15
* @function 巧克力锅炉
*/
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
this.empty = true;
this.boiled = false;
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
//向锅炉中导入原料,倒入前需要保证锅炉是空的
//倒入后令锅炉不为空,煮沸标志为false
public void fill() {
if(isEmpty()) {
empty = false;
boiled = false;
}
}
//从锅炉中倒出原料,倒出前需要保证锅炉已满且已煮沸
//倒出后,令锅炉为空
public void drain() {
if(!isEmpty() && isBoiled()) {
empty = true;
}
}
//煮沸锅炉中的原料,开煮前需要保证锅炉不为空,且还未沸腾
public void boil() {
if(!isEmpty() && !isBoiled()) {
boiled = true;
}
}
}
这是一个简单的模拟类,但是现在有一个问题:
如果不小心生成了两个或多个ChocolateBoiler对象,会发生什么?
1,锅炉对象现实中只有一个,应该禁止生成多个
公共资源应该只能有且只有一个
2,生成多个对象会导致程序混乱
如cb1和cb2,调用cb1的fill()后,调用了cb2的boil()
又去调用了cb1的drain()
用单例模式改造ChocolateBoiler:
/**
* @author 雫
* @date 2021/3/4 - 12:29
* @function 单例巧克力锅炉
*/
public class SingletonChocolateBoiler {
private static SingletonChocolateBoiler chocolateBoiler;
private boolean empty;
private boolean boiled;
private SingletonChocolateBoiler() {
this.empty = true;
this.boiled = false;
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
public void setEmpty(boolean empty) {
this.empty = empty;
}
public void setBoiled(boolean boiled) {
this.boiled = boiled;
}
public static SingletonChocolateBoiler getInstance() {
if(chocolateBoiler == null) {
chocolateBoiler = new SingletonChocolateBoiler();
}
return chocolateBoiler;
}
}
其它方法...
测试:
通过单例模式保证了管理共享资源的对象只有一份
1.4 当多线程参与进来…
现在我们再使用多线程优化一下SingletonCholateBoiler,有两个线程,线程1和线程2,它们都会执行下面的代码:
SingletonChocolateBoiler c
= SingletonChocolateBoiler.getInstance();
c.fill();
c.boil();
c.drain();
如果是单线程环境,这个程序不会出现问题,但如果面对多线程:
原本只应该有一个的对象变成了两个,让接下来的工作变得混乱
为此我们需要将多线程环境考虑进去,有以下几种解决方法:
1,同步getInstance()方法
给getInstance()方法上锁,被static修饰的方法,被synchronized修饰后,其锁为类锁,两个线程竞争同一把锁,保证了方法的原子性
但是直接同步getInstance()方法一点也不好,只有第一次执行该方法时才需要同步,之后每次调用该方法,同步都是一种累赘,且同步一个方法可能造成程序执行效率下降100倍
如果类似getInstance()这样的方法被多次频繁地使用,就不建议使用synchronized直接同步方法,这会拖垮程序的性能
2,采用懒汉模式创建唯一对象
采用懒汉模式急切地创建对象,利用这个方法JVM保证任何线程在试图获取chocolateBoiler前,一定已经创建好了唯一对象
但是这样的方法,不适用于创建对象需要太大负担的类,如果系统中有许多懒汉模式创建对象的方法,而创建这些对象并不简单,那么系统的性能就会降低,且给用户不好的使用体验(打开系统,进行一系列对象创建,用户只能看着屏幕等待)
3,双重检查加锁
采用类锁保证多个线程竞争同一把锁,第一次生成对象时才需要同步,其它时候直接返回对象,这样 双重检查加锁 的方式可以摆脱同步方法和饿汉模式的缺点
1.5 单例模式小结
1,单例模式确保程序中一个类最多只有一个对象
2,实现单例模式需要私有的构造器,一个静态变量和一个静态方法
3,如果使用多个类加载器,可能导致单例失效而产生多个对象,即不同的类加载器可能会加载同一个类,以此让单例失效
4,使用锁来其它方式来确保多线程环境下,单例也能正常工作