单例模式使用案例
- 数据库的连接池;
- Spring中的Bean默认也是单例的;
单例模式的特性
- 将构造函数私有化
- 在类的内部创建实例
- 提供获取唯一实例的方法
编写单例模式的代码
饿汉式
public class Singleton {
// 1.将构造函数私有化,不可以通过new的方式来创建对象
private Singleton() {
}
// 2.在类的内部创建自行实例
private static Singleton instance = new Singleton();
// 3.提供获取唯一实例的方法
public static Singleton getInstance() {
return instance;
}
}
这是实现一个安全的单例模式的最简单粗暴的写法,这种实现方式我们称之为饿汉式。之所以称之为饿汉式,是因为肚子很饿了,想马上吃到东西,不想等待生产时间。这种写法,在类被加载的时候就把Singleton实例给创建出来了,所以也不存在多线程竞争问题。
饿汉式的缺点就是,一上来就创建对象了,如果该实例从始至终都没被使用过,则会造成内存浪费,没起到lazy loading的效果。
简单懒汉式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上边方式在单线程环境下是行的,在多线程环境下就不行了,在并发获取实例的时候,可能会存在构建了多个实例的情况。所以,需要对此代码进行下改进,要解决也很简单,我们只要加锁就行。
public class SingletonSafe {
private static volatile SingletonSafe singleton;
private SingletonSafe() {
}
public static SingletonSafe getSingleton() {
if (singleton == null) { //这个判空主要是为了提升性能
synchronized (SingletonSafe.class) {
if (singleton == null) { //不考虑性能只需要这一个判空就行
singleton = new SingletonSafe();
}
}
}
return singleton;
}
}
这里采用了双重校验的方式,对懒汉式单例模式做了线程安全处理。通过加锁,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。
静态内部类方式
public class Singleton {
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:
//反射方式获取
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton); //结果为false,这两个实例不是同一个,违背了单例模式的原则
}
//序列化测试,需要类实现serialize接口
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance); //结果为false,这两个实例不是同一个,违背了单例模式的原则
}
枚举类型实现
在effective java 中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
//第一种方式
public enum DataSourceEnum {
INSTANCE;
private DBConnection connection = null;
private DataSourceEnum() {
connection = new DBConnection();
}
public DBConnection getConnection() {
return connection;
}
}
//调用
public static void main(String[] args) {
DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection();
DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection();
System.out.println(con1 == con2);
}
//第二种方式在类里调用
public class User {
//私有化构造函数
private User(){ }
//定义一个静态枚举类
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();
}
}
创建单例的方式:
(1)饿汉式
(2)简单懒汉式(在方法加锁)
(3)DCL双重检测加锁(进阶懒汉式)
(4)静态内部类实现懒汉式
(5)枚举方式
为什么饿汉式是线程安全的呢?
饿汉模式下,调用static方法去获取单例,这个时候,会触发初始化类,作为static field会首先被初始化,然后再被方法调用,不存在多线程竞争,更不存在线程安全的问题。
为什么静态内部类是线程安全的呢?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的方法。
clinit方法这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的clinit方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的clinit,其他的线程都要阻塞等待,直到这个线程执行完clinit方法。然后执行完clinit方法后,其他线程唤醒,但是不会再进入clinit方法。也就是说同一个加载器下,一个类型只会初始化一次。
所以即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。之所以这里变量定义的时候不需要volatil,因为只有一个线程会执行具体的类的初始化代码clinit。
最后
感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!