什么是单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意事项
- 单例模式只能有一个实例(只能有一个对象)
- 单例类必须自己创建自己的唯一实例(在方法内部创建自己的对象)
- 单例类必须给其他所有对象提供这一实例
要点
- 私有构造方法
将该类的构造函数私有化,目的是禁止其他程序创建该类的对象,同时也是为了提醒查看代码的人我这里是在使用单例模式,防止他人将这里任意修改。 - 指向自己实例的私有静态引用
- 以自己实例为返回值的静态共有方法
为什么方法和引用都是静态
程序调用类中方法只有两种方法- 创建一个类的对象,用该对象去调用类中的方法
- 通过类名直接调用类中的方法
因为单例模式构造函数私有化,所以不能通过第一种方法调用方法,只能通过第二种方式
而使用类名调用的方法只能是静态方法,静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的
有什么用?
应用举例:
- 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
单例模式实现
- 创建单例类
class SingleObject{
private static SingleObject instance = new SingleObject(); //指向自己实例的私有静态引用
private SingleObject(){} //私有构造方法
public static SingleObject getInstance() { //以自己实例为返回值的静态共有方法
return instance;
}
}
- 获取单例类的唯一方法
public class Main {
public static void main(String[] args) {
SingleObject instance = SingleObject.getInstance(); //通过静态方法获取唯一的实例
}
}
上面的单例是基于饿汉式,什么是饿汉式,继续往下看
单例模式的几种实现方式
1.饿汉式
**是否线程安全:**是
实现方式
class SingleObject{
private static SingleObject instance = new SingleObject(); //定义时直接实例化
private SingleObject(){} //私有构造方法
public static SingleObject getInstance() { //以自己实例为返回值的静态共有方法
return instance;
}
}
基于classloader机制避免了多线程的同步问题
**优点:**没有加锁也可以实现线程安全,在安全的情况下效率变高
**缺点:**类加载的时候就初始化,浪费内存
2.懒汉式 线程不安全
**是否线程安全:**否
实现方式
class SingleObject{
private static SingleObject instance; //定义时不实例化
private SingleObject(){}
public static SingleObject getInstance() {
if (instance==null) { //如果已经被实例,直接返回
instance = new SingleObject(); //没有被实例,创建实例后返回
}
return instance;
}
}
严格意义上不属于单例模式
线程不安全,多线程不能工作
3.懒汉式 线程安全
**是否线程安全:**是
实现方式
class SingleObject{
private static SingleObject instance;
private SingleObject(){}
public static synchronized SingleObject getInstance() { //直接在instance上加synchronized锁
if (instance==null) {
instance = new SingleObject();
}
return instance;
}
}
**优点:**第一次调用才初始化,对内存友好
**缺点:**synchronize锁效率很低
4.双检锁(DCL懒汉式)
JDK版本 1.5以后
是否线程安全: 是
class SingleObject{
private static SingleObject instance;
private SingleObject(){}
public static SingleObject getInstance() {
if (instance==null) { //先判断是否为空
synchronized (SingleObject.class) { //如果为空就加锁
if (instance == null) { //可能在判断为空和加锁之间的时间又线程创建了这个实例,所以要再进行一步判断
instance = new SingleObject();
}
}
}
return instance;
}
}
在线程安全懒汉式上减小了粒度,提高了性能
但是在极端情况下还是会有问题,因为instance = new SingleObject();
不是原子操作,具体会分为三步操作
- 分配内存空间
- 执行构造方法
- 把对象指向这个空间
可能会发生指令重排
,导致指令按照132这个顺序执行,在极端情况下,线程A执行了13两个方法,这时有一个线程B进入了最外层的判断,这时因为instance已经有了内存空间,但是instance并没有被构造,所以B会返回一个空的内存空间
所以要在instance前加volatile避免指令重排
class SingleObject{
private static volatile SingleObject instance; //避免指令重排
private SingleObject(){}
public static SingleObject getInstance() {
if (instance==null) {
synchronized (SingleObject.class) {
if (instance == null) {
instance = new SingleObject();
}
}
}
return instance;
}
}
但是这种情况还是有问题,如果我们用获得的唯一一个对象实例,使用反射来获取这个类的构造题,通过setAccessible(true)
来修改构造器的权限,然后再通过构造器新建一个对象,就会导致创建出两个对象
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
SingleObject instance = SingleObject.getInstance();
Constructor<SingleObject> declaredConstructor = SingleObject.class.getDeclaredConstructor(); //获取构造器反射
declaredConstructor.setAccessible(true); //破坏权限
SingleObject singleObject = declaredConstructor.newInstance(); //通过反射创建对象
}
}
如果通过加锁和flag判断,也会出现反编译得到flag的变量名进行破译的操作
我们进入newInstance
源码
可以看到枚举类型是不能用反射新建的
所以这里我们推荐用enum来创建单例模式
enum
JDK版本: JDK1.5起
线程安全: 是
enum SingleObject{
INSTANCE;
public static SingleObject getInstance() {
return INSTANCE;
}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
不能通过反射来进行实例化
参考文章
单例模式
【狂神说Java】单例模式-23种设计模式系列
单例模式
什么是单例模式?单例模式有什么作用?为什么要用单例模式
单例模式
单例模式中为什么构造函数为私有?为什么指向本身实例的类属性为静态?为什么以自己实例为返回值的类方法为静态?
为什么单例模式中的成员函数都是静态的?