单例模式的五种写法
概念
单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。
属于设计模式三大类中的创建型模式。
单例模式具有典型的三个特点:
只有一个实例。
自我实例化。
提供全局访问点。
其UML结构图非常简单,就只有一个类,如下图:
优点与缺点
优点:只生成了一个实例,节约系统资源,同时也能够严格控制客户对它的访问。
缺点:因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,扩展困难。
墙裂注意
:
注意单例模式所属类的构造方法是私有的
,所以单例类是不能被继承
的。
实现方式
常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁式、静态内部类式和枚举单例
。而在这五种方式中饿汉式和懒汉式
又最为常见。
饿汉式
饿汉式:线程安全,调用效率高。但是不能延时加载。
饿汉式是静态加载的时候实,不需要担心线程安全问题。
该模式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。
@AllArgsConstructor
@Data
public class SingletonHungry {
private Integer initA;
private Integer count;
//线程安全的
//类初始化时,立即加载这个对象
private static SingletonHungry instanceHungry = new SingletonHungry();
private SingletonHungry() {
initA=0;
count=0;
}
//方法没有加同步块,所以它效率高
//静态代码块尽量不要处理变量等逻辑,每次得到实例对象,都会走一遍,可有一些计数,共用了几次
public static SingletonHungry getInstanceHungry() {
instanceHungry.setCount(instanceHungry.getCount()+1);
return instanceHungry;
}
}
测试:
public class Test01 {
public static void main(String[] args) {
SingletonHungry singletonHungry = SingletonHungry.getInstanceHungry();
singletonHungry.setInitA(singletonHungry.getInitA()+1);
//1
System.out.println(singletonHungry.getInitA());
SingletonHungry singletonHungry2 = SingletonHungry.getInstanceHungry();
singletonHungry2.setInitA(singletonHungry.getInitA()+1);
//2
System.out.println(singletonHungry2.getInitA());
SingletonHungry singletonHungry3 = SingletonHungry.getInstanceHungry();
singletonHungry3.setInitA(singletonHungry.getInitA()+1);
//3
System.out.println(singletonHungry3.getInitA());
//始终只有一个实例对象
singletonHungry2.setInitA(100);
//100
System.out.println(singletonHungry3.getInitA());
//3
System.out.println(singletonHungry3.getCount());
}
}
变种
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return this.instance;
}
}
懒汉式
懒汉式:线程不安全。
在多线程环境下,但A.B两个线程都进入 if(instanceLazy == null),此时有可能会生成两个对象,所以是线程不安全的
案例:
由于该模式是在运行时加载对象的,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全。如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。
public class SingletonLazy {
//线程不安全的
private static SingletonLazy instanceLazy = null;
private SingletonLazy() {
}
//运行时加载对象
public static SingletonLazy getInstance() {
if (instanceLazy == null) {
instanceLazy = new SingletonLazy();
}
return instanceLazy;
}
}
解决方案:
1
加上synchronized关键字
,并发的时候也只能一个一个排队进行getInstance()方法访问。影响性能。
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
2
懒汉式(双重同步锁
):
一般的解决方案是使用双重同步锁机制,例子如下:
锁机制:保证同一时刻只有一个线程访问(即进行实例化)
在并发量高的情况下,不需要排队进getInstance()方法合理利用系统资源
public class SingletonLazySynchronized {
private static volatile SingletonLazySynchronized instanceLazySynchronized = null;
private SingletonLazySynchronized() {
}
//运行时加载对象
//静态的工厂方法
public static SingletonLazySynchronized getInstance() {
if (instanceLazySynchronized == null) {
synchronized(SingletonLazySynchronized.class){
if(instanceLazySynchronized == null){
instanceLazySynchronized = new SingletonLazySynchronized();
}
}
}
return instanceLazySynchronized;
}
}
**volatile
**关键字说明:禁止重排序的功能
没有此关键字时虽然加了锁 和双重判断,但是其实这个类是 线程不安全的
原因是:
这个要从cpu的指令开始说起
当我们执行
instanceLazySynchronized = new SingletonLazySynchronized();时 要执行什么操作呢
主要分三步
1.分配对象内存空间 memory = allocate()分配对象内存空间
2.ctorInstance()初始化对象
3.instance = memory 设置instance指向刚分配的内存
但是由于存在指令重排的情况
(单线程情况下无影响。多线程下会有影响)
由于2 和3 没有顺序的必然关系
也就可能会出现
1.分配对象内存空间 memory = allocate()分配对象内存空间
3.instance = memory 设置instance指向刚分配的内存
2.ctorInstance()初始化对象
此时我们假设有两个线程A和B进入
1.A 先执行了 3.instance = memory 设置instance指向刚分配的内存这个操作,但是还未来得及初始化对象。
2.B 判断 if(instance == null) 时 则会返回true 然后instance, 这时返回值就会出现问题 。
解决方案:
此时使用volatile关键字 则可以解决这个问题 volatile 关键字 有禁止重排序的功能
3
私有静态内部类实现单例模式,这种方式优于上面两种方式,他即实现了线程安全,又省去了null的判断,性能优于上面两种。
public class SingletonLazyStatic {
private static class LazyHolder {
private static final SingletonLazyStatic INSTANCE = new SingletonLazyStatic();
}
private SingletonLazyStatic (){}
public static final SingletonLazyStatic getInstance() {
return LazyHolder.INSTANCE;
}
}
常见应用场景
网站计数器。
项目中用于读取配置文件的类。
数据库连接池。因为数据库连接池是一种数据库资源。
Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。
Servlet中Application
Windows中任务管理器,回收站。
等等。
思考:
此时这个类则是线程安全的,当然如果要使用单例模式,推荐使用的还是枚举方法
枚举单例模式
为什么使用单例?
- 私有化构造器并不保险
《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。 - 序列化问题,任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”当然,这个问题也是可以解决的,想详细了解的同学可以翻看《effective java》第77条:对于实例控制,枚举类型优于readResolve
枚举示例
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
完整的枚举单例:
public class User {
private String lastName;
private Integer numbers;
//私有化构造函数 初始化
private User(){
lastName="zhangsan";
numbers=0;
}
//定义一个静态枚举类
static enum SingletonEnum{
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private User user;
//私有化枚举的构造函数,初始化一个对象实例
private SingletonEnum(){
user=new User();
}
//公开获取初始化的对象实例
public User getInstnce(){
return user;
}
}
//对外暴露一个获取User对象的静态方法
public static User getInstance(){
return SingletonEnum.INSTANCE.getInstnce();
}
}
测试:
System.out.println(User.getInstance());
System.out.println(User.getInstance());
System.out.println(User.getInstance()==User.getInstance());
结语
单元素的枚举类型已经成为实现Singleton的最佳方法