单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。主要用来避免一个全局使用的类频繁地创建与销毁,控制实例数目,节省系统资源的。。
关键代码:构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
实现
一、饿汉式
public class Singleton1 {
//构造方法私有化
private Singleton1(){}
//class文件加载时进行类的实例化
private static Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton1.getInstance().hashCode());
}).start();
}
}
}
优点:简单明了,线程安全
缺点:在类加载的时候就创建实例了,不管调不调用getInstance()方法,实例都已经被创建了。
二、还是饿汉式 与Singleton1的区别就是实例化放在了静态代码块中,优缺点与第一种一样
/**
* 饿汉式, 与Singleton1的区别就是实例化放在了静态代码块中
* */
public class Singleton2 {
//构造方法私有化
private Singleton2(){}
//class文件加载时进行类的实例化
private static Singleton2 INSTANCE ;
static {
INSTANCE = new Singleton2();
}
public static Singleton2 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton2.getInstance().hashCode());
}).start();
}
}
}
三、懒汉式
public class Singleton3 {
//构造方法私有化
private Singleton3(){}
private static Singleton3 INSTANCE;
public static Singleton3 getInstance() {
if (Objects.isNull(INSTANCE)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton3.getInstance().hashCode());
}).start();
}
}
}
说明:在调用getInstance()方法时创建实例,避免了饿汉式类加载时就创建实力的问题,但同时带来了新的问题,在高并发场景可能会创建多个实例,线程不安全。
上述代码main方法执行结果:
四、懒汉式 + 锁
public class Singleton4 {
//构造方法私有化
private Singleton4(){}
private static Singleton4 INSTANCE;
public static synchronized Singleton4 getInstance() {
if (Objects.isNull(INSTANCE)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton4();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton4.getInstance().hashCode());
}).start();
}
}
}
说明:通过对getInstance()加锁来保证线程安全,但是锁的粒度太大,开销不少。
上述代码main方法执行结果:
五、懒汉式+锁+减少同步代码块
public class Singleton5 {
//构造方法私有化
private Singleton5(){}
private static Singleton5 INSTANCE;
/*
* 存在线程不安全的问题,因为第一个if判断条件有可能导致在对象没有实例化之前,不同的线程进入到if代码块中,拿到锁之后进行实例化
* */
public static Singleton5 getInstance() {
if (Objects.isNull(INSTANCE)) {
//试图通过减小同步代码块的方式提高效率,但是并不可行
synchronized (Singleton5.class) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton5.getInstance().hashCode());
}).start();
}
}
}
说明:存在线程不安全的问题,因为第一个if判断条件有可能导致在对象没有实例化之前,不同的线程进入到if代码块中,拿到锁之后进行实例化,创建多个实例。
上述代码main方法执行结果:
六、懒汉式+双重检查锁
public class Singleton6 {
//构造方法私有化
private Singleton6(){}
/*
* 实例化一个对象要分为三个步骤:1分配内存空间、2初始化对象、3将内存空间的地址赋值给对应的引用
* 但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:1分配内存空间、2将内存空间的地址赋值给对应的引用、3初始化对象
* 如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象。
* 所以加上volatile关键字禁止指令重排
* */
private volatile static Singleton6 INSTANCE;
public static Singleton6 getInstance() {
if (Objects.isNull(INSTANCE)) {
//双重检查
synchronized (Singleton6.class) {
if (Objects.isNull(INSTANCE)) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton6.getInstance().hashCode());
}).start();
}
}
}
说明:使用了DCL方式保证了线程安全,只创建了一个实例,同时还保证了性能。其中关键字volatile作用看注释部分。
上述代码main方法执行结果:
七、静态内部类
public class Singleton7 {
//构造方法私有化
private Singleton7(){}
private static class Singleton7Holder {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return Singleton7Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton7.getInstance().hashCode());
}).start();
}
}
}
说明:加载外部类时不会加载内部类,这样可以实现懒加载,线程安全,类只加载一次
上述代码main方法执行结果:
八、枚举类
public enum Singleton8 {
INSTANCE;
public static Singleton8 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
System.out.println(Singleton8.getInstance().hashCode());
}).start();
}
}
}
说明:线程安全,目前还比较少见的方式
上述代码main方法执行结果:
总结:
推荐使用饿汉式,简单明了,涉及到需要延迟加载的情况下可以使用静态类的方式或者双重检查锁的方式,不建议直接使用懒汉式。