文章目录
单例模式的定义与特点
单例(Singleton
)模式的定义:
是指确保一个类在任何情况下都绝对只有一个实例,隐藏其所有的构造方法,并提供一个全局访问点。属于创建型模式。
单例模式有 3 个特点:
1.单例类只有一个实例对象;
2.该单例对象必须由单例类自行创建;
3.单例类对外提供一个访问该单例的全局访问点;
单例模式的结构
单例模式是设计模式中最简单的模式之一,但也是面试时常被问到的。
通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
单例模式的主要角色如下:
单例类:包含一个实例且能自行创建这个实例的类。
访问类:使用单例的类。
单例模式的实现
说起单例模式的实现,我们首先会想到的就是“饿汉式"和"懒汉式"下面就详细的讲讲。
饿汉式单例
特点:在单例类首次加载的时候就创建实例。
优点:没有任何锁,执行效率高,性能高。
缺点:在某些情况下,可能会造成内存的浪费,因为不管你用不用,它都会在类加载时创建一个对象。
常见的饿汉式单例写法:
/**
* 常见的饿汉式单例写法
*/
public class HungrySingleton {
//类加载时就创建HungrySingleton这个实例对象
private static final HungrySingleton hungrySingleton = new HungrySingleton();
//私有化构造函数
private HungrySingleton(){}
//提供全局访问点
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
静态代码块写法:
/**
* 静态代码块写法
* 看来更有逼格,其实和上边的差不多
*/
public class HungryStaticSingleton {
//类加载时就创建HungrySingleton这个实例对象
private static final HungryStaticSingleton hungryStaticSingleton;
static {
hungryStaticSingleton = new HungryStaticSingleton();
}
//私有化构造函数
private HungryStaticSingleton(){}
//提供全局访问点
public static HungryStaticSingleton getInstance(){
return hungryStaticSingleton;
}
}
因为饿汉式的缺点,为了避免内存造成不必要的浪费,所以出现懒汉式单例。
懒汉式单例
特点:在被外部类调用时才创建实例,解决了饿汉式的内存浪费。
优点:节省内存,减少不必要的内存浪费。
1.最简单的懒汉式写法:
缺点:线程不安全
/**
* 最简单的懒汉式
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton;
//私有化构造函数
private LazySimpleSingleton(){}
//提供全局访问点,用的时候才创建实例
public static LazySimpleSingleton getInstance(){
if (lazySimpleSingleton == null){
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
线程破坏懒汉式单例的事故现场:
测试代码:
public class LazySimpleSingletonTest {
@Test
public void test1(){
new Thread(()->{
System.out.println(LazySimpleSingleton.getInstance());}
).start();
new Thread(()->{
System.out.println(LazySimpleSingleton.getInstance());}
).start();
}
}
多次运行发现两条线程会有可能会创建出不同的实例对象(如下图),这就违背了单例模式的定义了。
于是产生了懒汉式的第二种写法(加锁)
2.懒汉式第二种写法(加锁):
解决了线程不安全问题。
缺点:性能低,加锁后导致并行变串行
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton;
//私有化构造函数
private LazySimpleSingleton(){}
//提供全局访问点
public static LazySimpleSingleton getInstance(){
//加锁,解决线程不安全问题
synchronized (LazySimpleSingleton.class) {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
return lazySimpleSingleton;
}
}
3.懒汉式第三种写法(双重检查锁):
解决了线程不安全问题的同时,也解决了性能低的问题。
注意:变量lazySimpleSingleton
加volatile
修饰。因为线程中是存在指令重排序的问题,变量定义的时候会创建一块内存,而创建实例的时候也会创建一块内存,变量要指向创建实例的内存。线程运行中会导致这些顺序发生变化,也就是指令重排序的问题。所以加volatile
关键字修饰保证有序性。
缺点:两个if
判断导致代码看起来不是那么的优雅,可读性不高。
public class LazySimpleSingleton {
private static volatile LazySimpleSingleton lazySimpleSingleton;
//私有化构造函数
private LazySimpleSingleton(){}
//提供全局访问点
public static LazySimpleSingleton getInstance(){
//检查是否需要加锁阻塞
if (lazySimpleSingleton == null){
synchronized (LazySimpleSingleton.class) {
//检查是否需要创建新的实例
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
4.懒汉式第四种写法(静态内部类):
特点:利用Java语法的特点,写法优雅,性能高,懒加载避免了内存浪费。看似已经非常完美啦。
解释:因为它创建实例是在静态内部类中,而静态内部类在被使用到的时候才会被加载,所以说避免了内存浪费。
/**
* 懒汉式静态内部类写法
*/
public class LazyStaticInnerClassSimpleSingleton {
//私有化构造函数
private LazyStaticInnerClassSimpleSingleton(){}
//全局访问点
private static LazyStaticInnerClassSimpleSingleton getInstance(){
return StaticInner.Instance;
}
//静态内部类,创建实例
private static class StaticInner{
private static final LazyStaticInnerClassSimpleSingleton Instance = new LazyStaticInnerClassSimpleSingleton();
}
}
上边说到的
懒汉式静态内部类写法
,看似已经很完美了。其实不然,上边所说到的所有单例写法其实有两个共同的缺点:
1.能够被反序列化破坏
2.能够被反射破坏
反序列化破坏单例的事故现场
序列化:
就是把内存中对象的状态转化为字节码的形式,把字节码通过IO输出流写到磁盘上,持久化保存下来。
反序列化:
就是将持久化的字节码内容,通过IO输入流读取到内存中,转化成一个实例对象。
这里咱们就拿常见的饿汉式单例写法
来演示:
测试代码:
@Test
public void test(){
HungrySingleton instance1 = HungrySingleton.getInstance();
try {
//将instance1对象 从内存写入到硬盘
FileOutputStream fos = new FileOutputStream("HungrySingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance1);
oos.flush();
oos.close();
//从硬盘读取到内存
FileInputStream fis = new FileInputStream("HungrySingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Object instance2 = ois.readObject();
ois.close();
//比较instance1和instance2是否是同一个实例
System.out.println(instance1 );
System.out.println(instance2);
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果
: 可以发现,反序列化出来的对象实例和全局访问点获取出来的对象实例不相同,即违背了单例原则。
解决办法
:其实很简单,只需要在单例类中加一个方法:
再次看运行结果
:
反射破坏单例的事故现场
这里咱们就拿懒汉式静态内部类写法
来演示:
测试代码:
public class LazyStaticInnerClassSimpleSingletonTest {
@Test
public void test() throws Exception {
Class <?> clazz = LazyStaticInnerClassSimpleSingleton.class;
//获取所有无参构造器(包括私有)
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
//强制访问
constructor.setAccessible(true);
//创建实例
Object o1 = constructor.newInstance();
Object o2 = constructor.newInstance();
//比较是否是同一个实例对象
System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);
}
}
运行结果: 可以发现,反射直接就绕过了单例类提供的唯一访问点。这就是反射破坏单例的事故现场。
解决办法
:可以在无参构造方法里做个判断,抛异常。
再来看运行结果
:
是不是反射就创建不了对象了。但是,但是,本来很优雅的代码,结果你在构造方法中抛个异常,几个意思?感觉就很奇怪,这就不能忍了吧。且看下回分析。。。下边分析啊,新的写法又要来了,乃金刚不坏之身:序列化破坏不了它,反射也破坏不了它。
枚举式单例
堪称完美的单例啊,就下边一个缺点。。。
优点:优雅的代码,线程安全且避免了序列化和反射的破坏(因为反射是创建不了枚举对象的,直接会报错,待会看源码)。
缺点:类初始化时,就创建了这个枚举,在某些情况下可能造成内存浪费。
/**
* 枚举式单例
*/
public enum EnumSingleton {
INSTANCE; //这个就是实例
//定义一个属性,并提供get,set方法
private String name;
public String getName() {return name;}
public void setName(String name) {this.name = name;}
//全局访问点
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
测试代码:
public class EnumSingletonTest {
@Test
public void test(){
//创建两个实例对象
EnumSingleton instance1 = EnumSingleton.getInstance();
EnumSingleton instance2 = EnumSingleton.getInstance();
/**
* 比较两个实例对象是否是同一个
* 如果是同一个对象实例,那么返回true,并且用instance2可以获取到instance1设置的名字
*/
System.out.println(instance1==instance2);
instance1.setName("枚举单例");//用instance1 设置名字
System.out.println(instance2.getName());//用instance2来获取名字
}
}
运行结果:
聊一聊为什么枚举单例不会被反射破坏:
看一段源码:
找到Constructor
这个类中的第416
行,判断如果这个类是被ENUM
(枚举)修饰的,那么就直接抛出Cannot reflectively create enum objects
。
靠,为啥人家抛异常就行,咱们在无参构造里抛个异常就不优雅了啊? 别问为啥,人家是官方,人家牛掰!
ThreadLocal单例
特点:保证线程内部的全局唯一,且天生线程安全。
注意:是线程内部全局唯一,也就是说,一个线程一个单例实例,线程之间是相互隔离的。
/**
* ThreadLocal单例写法
*/
public class ThreadLocalSingleton {
//私有化无参构造
private ThreadLocalSingleton(){}
//交给ThreadLocal创建实例
private static final ThreadLocal<ThreadLocalSingleton> instance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
//全局访问点
private static ThreadLocalSingleton getInstance(){
return instance.get();
}
}
测试代码:
public class ThreadLocalSingletonTest {
@Test
public void test(){
ThreadLocalSingleton mainInstance1 = ThreadLocalSingleton.getInstance();//主线程实例1
ThreadLocalSingleton mainInstance2 = ThreadLocalSingleton.getInstance();//主线程实例2
System.out.println("主线程两个实例:");
System.out.println(mainInstance1);
System.out.println(mainInstance2);
new Thread(()->{
ThreadLocalSingleton thread1Instance = ThreadLocalSingleton.getInstance();//thread1线程实例
System.out.println("thread1线程实例:"+thread1Instance);
}, "thread1").start();
new Thread(()->{
ThreadLocalSingleton thread2Instance = ThreadLocalSingleton.getInstance();//thread2线程实例
System.out.println("thread2线程实例是否相同:"+thread2Instance);
}, "thread2").start();
}
}
运行结果:
可以看出来,主线程创建的两个实例是相同的,所以说:线程内部创建出来的实例是相同的
但是,每个线程创建出来的实例是不同的,所以说:一个线程一个单例实例,线程之间是相互隔离的。
单例模式总结
优点:
1.在内存中只有一个实例,减少内存开销
2.可以避免对资源的多重占用
3.设置全局访问点,严格控制访问
缺点:
1.没有接口,扩展困难,如果要扩展,只有修改代码,没有其他途径
重点:
1.私有化构造器
2.保证线程安全
3.延迟加载
4.防止被序列化和反序列化破坏
5.防止被反射破坏