文章目录
单例模式
单例模式(Singleton Pattern)是 Java 中23种设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
应用实例:
1、一个班级只有一个班主任。
2、一个国家只能有一个领导人
3、2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
实现方式
最为熟知的莫过于饿汉式和懒汉式了。
1)饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
特点:典型的以空间换时间,它在类加载的过程就实例化对象,不管你使不使用都先创建出来。
优点:线程安全且没有加锁,执行效率高,实现简单
缺点:类加载时就初始化,可能之后并不会使用而浪费内存
public class Singleton {
private static Singleton singleton=new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
2)懒汉式(线程不安全)
是否 Lazy 初始化:是
是否多线程安全:否
特点:最基本的实现方式,实现简单。但是在多线程环境下会产生并发问题,不适用于多线程。
public class Singleton {
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if (singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
3)DCL懒汉式(线程安全)
是否 Lazy 初始化:是
是否多线程安全:是
特点:DCL全称为double-checked locking(双重检测锁),它采用双锁机制,在保证多线程安全的同时也能提升执行效率。
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
介绍:这种经典的多线程下实现单例模式的方式。
(1)两层判断的作用?
在getInstance()方法内它进行了两次判断,内层的判断用于保证对象只能在第一次调用时初始化(即确保单例),外层的判断使得程序效率大幅提升,因为在synchronized 锁的同步下,每一次线程访问getInstance()都会被阻塞(即使对象已经被创建),而加上外层判断则保证了对象在创建完成后不必访问同步代码块了,从而提升效率。
(2)为什么singleton的定义要加上volatile关键字?
这个就要从volatile的作用去回答了。说到volatile大家一定能会想起它的三个特点:保证可见性,不保证原子性、指令重排序。这个关键字涉及到JMM与缓存一致型协议,具体详情可以参考:Java并发编程:volatile关键字解析,这里使用volatile就是为了防止指令重排序。
对于指令重排序,可以举个简单的例子:
public class test {
public static void main(String[] args) throws InterruptedException {
int a=1; // 步骤1
int b=2; // 步骤2
int c=a+b; // 步骤3
}
}
在上面的例子中,由于步骤1和步骤2之间并没有相互依赖的关系,因此它们是有可能相互交换位置的,而步骤3则依赖于步骤1和步骤2,所以步骤3不会参与交换。最终指令的顺序可能是1,2,3也有可能是1,3,2。
而在创建对象(singleton = new Singleton())的时候会经过三个步骤:
1、分配内存空间
2、执行构造方法,初始化对象
3、把singleton指向这个空间
这里可以看到步骤3和步骤2是没有相互依赖关系的,因此指令重排后可能会出现1,3,2的顺序,这在单线程下是绝对没问题的,但在多线程的时候就会出错。
假设有线程1和线程2,线程1先执行,当它执行到步骤三时(注意现在是1,3,2的顺序),singleton已经指向了一个对象,但是这个对象想在还没有初始化(还没执行步骤2)。此时轮到线程2执行,它在进行到 if (singleton == null) 这步时,由于singleton已经指向一个对象了,因此它会直接返回这个对象,但此时对象还没有初始化。
所以为了保证安全,我们必须要在定义时加上volatile关键字。
(3)DCL懒汉式一定能保证单例吗?
对于多线程环境,DCL懒汉式确实解决了并发问题,但是它仍然无法确保单例。破坏单例的实现方式有反射、clone方法与序列化对象方式,这些都可能导致单例失效。后两种我们可以通过不实现相应接口来解决,但私有属性和方法对于反射就相当于一层纸,可以轻易破坏。
反射破坏单例:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton instance1 = Singleton.getInstance();
Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
constructor.setAccessible(true); // 无视私有构造器
Singleton instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
结果如下:
Singleton@1540e19d
Singleton@677327b6
可以看到这两个对象是不一样的,因此单例模式又失效了,为了解决这个问题,我们可以在设一个静态变量,并在私有构造器中进行判断。
设置一个静态flag:
public class Singleton {
private volatile static Singleton singleton;
private static boolean flag=false; // 定义一个静态类型的flag变量
private Singleton() {
synchronized (Singleton.class){
if (!flag){ // 判断对象是否创建过
flag=true;
}else {
throw new RuntimeException("请别用反射破坏单例模式");
}
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton instance1 = Singleton.getInstance();
Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
constructor.setAccessible(true); // 无视私有构造器
Singleton instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
结果如下:
Exception in thread "main" java.lang.reflect.InvocationTargetException <4 internal class>
at Singleton.main(Singleton.java:38)
Caused by: java.lang.RuntimeException: 请别用反射破坏单例模式
at Singleton.<init>(Singleton.java:18)
... 5 more
这样表面上解决了通过反射破坏单例的问题,但是若知道了这个静态变量的名字,仍然可以采用反射的形式破坏单例:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Singleton instance1 = Singleton.getInstance();
Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
constructor.setAccessible(true); // 无视私有构造器
Field flag = instance1.getClass().getDeclaredField("flag"); // 通过名字获取静态变量
flag.setAccessible(true); // 无视私有
flag.set(instance1,false);
Singleton instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
结果如下:
Singleton@677327b6
Singleton@14ae5a5
因此,即使我们深思熟虑,但仍然可能导致单例失效。
4)静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。相比于饿汉式,由于由于内部类在不被访问的情况下是不会被加载的,因此它能够实现延迟加载,节省内存资源。
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return innerClass.singleton;
}
private static class innerClass {
private static final Singleton singleton=new Singleton();
}
}
这种方式仍然可以采用反射、克隆与序列化的方式破坏单例。
克隆与序列化破坏单例
public class Singleton implements Serializable, Cloneable {
private Singleton() {
}
public static Singleton getInstance() {
return innerClass.singleton;
}
private static class innerClass {
private static final Singleton singleton = new Singleton();
}
public static void main(String[] args) throws IOException, ClassNotFoundException, CloneNotSupportedException {
Singleton instance1 = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Singleton instance2 = (Singleton) ois.readObject(); // 序列化
Singleton instance3= (Singleton) instance1.clone(); // 克隆
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
}
}
结果如下:
Singleton@6d6f6e28
Singleton@6d03e736
Singleton@568db2f2
5)枚举
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
结果如下:
Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at EnumSingle.main(EnumSingle.java:17)
可以看到,使用枚举能够很好地解决反射破坏单例的隐患。
另外,枚举这种方式实现自动序列化机制,防止序列化创建对象。
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws IOException, ClassNotFoundException, CloneNotSupportedException {
EnumSingle instance1 = EnumSingle.INSTANCE;
// 序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance1);
System.out.println(instance2);
}
}
结果如下:
Exception in thread "main" java.lang.ClassCastException: EnumSingle cannot be cast to Singleton
at EnumSingle.main(EnumSingle.java:23)
单例模式的拓展(多例)
要求:创建一个门票类,开启3个线程进行售卖,总共只有500张票。
这里采用DCL懒汉式实现。
门票类:
import java.util.Date;
public class Tickets {
private static int count = 0; // 以售卖票数
private final static int num = 500; // 门票数目
private Date time;
private Double price;
private Tickets() {
count++;
}
private Tickets(Date time, Double price) {
this.time = time;
this.price = price;
count++;
}
// 无参
public static Tickets createTickets(){
if (count<num){
synchronized (Tickets.class){
if (count<num){
System.out.println("正在卖出第"+(count+1)+"张票");
return new Tickets();
}else {
return null;
}
}
}
return null;
}
// 有参
public static Tickets createTickets(Date time, Double price){
if (count<num){
synchronized (Tickets.class){
if (count<num){
System.out.println("正在卖出第"+(count+1)+"张票");
return new Tickets(time,price);
}else {
return null;
}
}
}
return null;
}
// set/get方法
}
测试类:
public class test {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(()->{
while (true){
Tickets tickets = Tickets.createTickets();
if (tickets==null){
System.out.println("对不起,门票已经卖完");
break;
}
}
}).start();
}
}
}
测试结果:
正在卖出第1张票
正在卖出第2张票
正在卖出第3张票
...
正在卖出第498张票
正在卖出第499张票
正在卖出第500张票
对不起,门票已经卖完
对不起,门票已经卖完
对不起,门票已经卖完
Process finished with exit code 0
总结
- 在单线程的环境中上面各种方法都是没有问题的。但在多线程环境时懒汉式则会出现并发问题,需要采用DCL懒汉式,一般来说不建议采用普通的懒汉式;
- 若不在意内存的使用或者确保对象一定使用,则可以直接采用饿汉式,方便简单;
- 如果特别指明需要延迟加载,可以采用DCL懒汉式或者静态内部类的形式;
- 如果涉及到反序列化创建对象或者考虑反射,可以枚举方式(最佳)。
参考
Java设计模式之饿汉式和懒汉式
菜鸟教程:单例模式
java序列化,看这篇就够了
狂神说JUC笔记
Java并发编程:volatile关键字解析