23种设计模式——单例模式
下面进行的"冗杂"的文字描述(是从runoob中摘抄的)
一、介绍
概述
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
**缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
二、实现
我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。
SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo,我们的演示类使用 SingleObject 类来获取 SingleObject 对象。
三、代码实现
在代码中有8种的实现方法,与其说是多种实现方法,其实可以说是为了实现他的在多线程情况下的安全性和加载中就进行实例化,还是要用的时候进行实例化的解决过程出现的实现方法。
简单点说:就是为了解决两种问题的讨论过程而产生的8种单例代码,而这两种问题就是: 1.线程是否安全, 2.是否在加载时就已经实例化了
第一种: 饿汉式(即加载时就进行实例化)
package com.sleep_zjx.singleton;
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用
* 唯一缺点:不管用到与否,类装载时就完成实例化
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {return INSTANCE;}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
第二种,也是饿汉式(通过静态代码块来进行实现)
package com.sleep_zjx.singleton;
/**
* 这里其实是和Mgr01是一样的,但这里采用了static静态块来进行调用,本质是一
* 模一样的
*/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
private Mgr02() {};
public static Mgr02 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
虽然饿汉式能保证线程安全,而且编写也简单易懂,但它还是有个被一些人不太认可的地方,那就是这个类只要是被加载到内存就立马进行实例化了。因为有的人就觉得不需要在加载时立马就进行实例化,希望在需要的时候再进行实例化,而不是加载就立马实例化。故有了下面的一系列的代码实现过程:
第三种: 懒汉式(在需要到这个类的时候在进行实例化)
package com.sleep_zjx.singleton;
/**
* lazy loading
* 也称为懒汉式
* 虽然达到了按需初始化的目的,但却带来了线程不安全的问题
* 但懒汉式会带来线程安全问题
*
*/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {}
public static Mgr03 getInstance() {
//在下面这里,如果有多个线程同时过来的话,可能会生成不止一个对象
if (INSTANCE == null) {
/* try {
//sleep1秒打乱是因为使得其中线程进入的时间差产生更多
Thread.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}*/
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
虽然Mgr03能实现需要时在实例,但也引进了线程不安全的情况。为解决这种线程不安全,就出现了Mgr04
package com.sleep_zjx.singleton;
/**
* lazy loading
* 也称为懒汉式
* 虽然达到了按需初始化的目的,但却带来了线程不安全的问题
* 可以通过synchronized解决,但也带来了效率下降的问题
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {}
public static synchronized Mgr04 getInstance() {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
Mgr04中为解决线程安全的情况就使用了synchronized来加锁,但效率就降低了,故此为解决效率降低,就出现了Mgr05
package com.sleep_zjx.singleton;
/**
*lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,却带来了线程不安全的问题
* 可以通过synchronized解决,但也会带来效率下降
* 这次虽然解决了,但之前的问题再次出现了
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {}
public static Mgr05 getInstance() {
//当有两个线程已经进入了判断了
if(INSTANCE == null) {
//当一个线程结束锁资源的时候,另一把锁(同一时间已经通过上面的判断
// 的线程)进去又重新new了一下
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
Mgr05解决了效率下降的情况,但同时就不能保证到线程安全的问题了,故有人又提出了Mgr06
package com.sleep_zjx.singleton;
/**
*lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,却带来了线程不安全的问题
* 可以通过synchronized解决,但也会带来效率下降
* 在Mgr05基础上进行了双重判断
*/
public class Mgr06 {
private static Mgr06 INSTANCE;
private Mgr06() {}
public static Mgr06 getInstance() {
//当有两个线程已经进入了判断了
if(INSTANCE == null) {
synchronized (Mgr06.class) {
//双重判断
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
Mgr06在其中进行了双重判断,解决了这些问题,可以说是“完美的一种写法”,但后面又有人提出了更完美的一种
双重判断中的第一层有必要判断吗?
第一次是要进行判断的,因为如果不判断的话,每一次进去的时候都需要进行上锁。效率下降了一点,而且在多线程的时候,如果一起进行来的话,就直接停止在锁位置了。
package com.sleep_zjx.singleton;
/**
* 最完美之一(比Mgr01还好,Mgr01在加载的时候就被实例化了,而Mgr07则在加
* 载的时候不初始化(因为实例化放在内部类中,只有当类中调用getInance的时候
* 才会实例化)
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {
private Mgr07() {}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
这种方法比Mgr06更好的地方就在于使用了内部类(内部类就算是外部类已经加载了,内部类也不是不会加载的,除非等调用到内部类的时候,它才会去进行加载),然而在《Effective java》一书中出现了更为巧妙的一种实现单例的方法。
package com.sleep_zjx.singleton;
/**
* 在一本书中就是通过枚举这种方法来实现的《Effective java》
* 不仅可以解决线程同步,还可以防止反序列化。
* 枚举类为什么没有反序列化?
* 因为枚举类是没有构造方法,故不能去构造该对象,故此不能反序列化
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
为什么Joshua Bloch 大神说:单元素的枚举类型已经成为实现Singleton的最佳方法?
上述实现单例的代码中,我们可以知道实现单例模式主要有三个特点:
- 构造方法私有化
- 实例的变量引用私有化
- 获取实例的方法共享
而对于为什么要使用枚举呢,也是因为上面实现单例模式的特点来说明的:
《effective java》中提到:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”—— 简而言之的话,就是构造方法私有化也能通过反射进行破解,但可以通过加异常来进行解决。
序列化前后两个对象并不相等。简单来说“任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”当然,这个问题也是可以解决的,想详细了解的同学可以翻看《effective java》第77条:对于实例控制,枚举类型优于readResolve。
枚举单例的好处:
避免反射攻击
反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败
避免序列化问题
通过enum枚举类来进行的话,不仅可以解决了线程同步等等问题,还可以防止反序列化。
枚举类为什么能避免反序列化?
- 因为java在定义枚举的时候,将其定义为无构造方法,故此因为枚举类是没有构造方法,故不能去构造该对象,故此不能反序列化。
简单的说下,反序列是什么?
序列化与反序列化是开发过程中不可或缺的一步,简单来说,序列化是将对象转换成字节流的过程,而反序列化的是将字节流恢复成对象的过程。两者的关系如下:
也就是反序列化就是从持久化的设备中读取信息(不准确的抽象理解)。