概念 & 原理
1. 定义
- 保证一个类仅仅有一个实例,并提供一个全局访问点
- 类型:创建型
2. 适用场景
- 想确保任何情况下都绝对只有一个实例
- 实际场景:数据库连接池
3. 优缺点
- 优点:在内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;设置了全局访问点,严格控制访问(对外不让new)
- 缺点:没有借口,扩展困难
4.单例模式的重点内容
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化
- 反射
懒汉式案例
public class Singleton {
//懒汉式
private static Singleton uniqueInstance;
private Singleton(){}
//保证线程安全
public static synchronized Singleton getInstance(){
if(uniqueInstance==null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
}
public class Application {
public static void main(String[] args) {
// TODO Auto-generated method stub
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
多线程debug验证线程安全
package com.rjxy.demo1;
public class Application {
/**
* @param args
*/
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("end...");
}
}
class T implements Runnable{
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+singleton.hashCode());
}
}
开启debug模式:
- 可以发现,三个线程在三个断点处都已经停了下来,suspend表示挂起
1. 情况一调试
- 让线程 0 先进入 if 语句,但不要让线程 0 为 uniqueInstance 赋值:
- 让线程 1 也进去,让其为 uniqueInstance 赋值:
- 回到线程 0 ,让其也为 uniqueInstance 赋值:
- 线程 1 还没有返回,在这种情况之下,uniqueInstance 的返回值是最后的那个创建实例的线程:
在这种情况下,线程生成了不止一个实例,但之所以输出相同,是概率原因。如果有很多线程的话,且生成对象需要消耗资源,那么很可能导致系统故障
2. 情况二调试
- 在情况一的最后,不再回到线程 0 为 uniqueInstance 赋值,而是先线程一直接返回输出。
- 线程 1 输出后,线程 0 创建对象输出
- 可以看到,单例模式竟然创建了两个对象!存在线程安全问题
3. 解决方法
- synchronized关键字。需要注意的是如果加在静态方法上锁的是这个类的class文件。如果不是静态方法,相当于锁的堆内存中生成的对象。
//保证线程安全
public static synchronized Singleton getInstance(){
if(uniqueInstance==null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
缺点:synchronized 存在加锁和解锁的开销。而且锁的是class文件,范围比较大,对系统性能有一定影响。
- DoubleCheck双重检查实战原理
下面这种方式将加锁推迟到if语句之后,带来的好处就是在实例被创建出来之后,获取该实例的时候,就不必大费周章地去加锁解锁,节约资源。但是如果是多线程的话,就有大问题了。 uniqueInstance = new Singleton();这句代码要经过:①分配内存空间、②初始化对象、③将索引指向该内存空间。其中②和③在多线程的情况下会发生重排序,造成异常的出现,当然这是有概率的,并不绝对。解决办法就是 volatile 关键字,这个关键字会避免重排序的发送。关于重排序、volatile原理我会在后面的博客中讲到。
public class Singleton {
//懒汉式
private volatile static Singleton uniqueInstance;
private Singleton(){}
//保证线程安全
public static synchronized Singleton getInstance(){
if(uniqueInstance==null) {
synchronized(Singleton.class) {
uniqueInstance = new Singleton();
}
}
return uniqueInstance;
}
}