保证多个线程使用同一个实例
1.懒汉模式
只有在使用时才会创建对象
需要注意的是:
1.线程并发访问问题
2.编译器,CPU 有可能对指令进行重排序,导致使用到尚未初始化 的实例,可以通过添加volatile 关键字进行修饰,volatile 修饰的字段,可以防止指令重排。
/**
* 懒加载模式
*/
class LazySingleton {
private volatile static LazySingleton instance;
//私有化构造方法,防止外部创建对象
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
/* 字节码层
JIT , CPU 有可能对如下指令进行重排序
1 .分配空间
2 .初始化
3 .引用赋值
如重排序后的结果为如下
1 .分配空间
3 .引用赋值 如果在当前指令执行完,有其他线程来获取实例,将拿到尚未初始化好的实例对象
(在(instance==null)进行判断时,判定当前instance对象不为空,直接进入到下一行代码 return instance,
此时这个线程拿到的就相当于一个空对象,进行操作时会出现空指针异常,所以我们使用 volatile 关键字防止指令重排)
2 .初始化(如果使用 volatile 关键字将不会出现这种指令重排的情况)
*/
}
}
}
return instance;
}
}
2.饿汉模式
类加载的 初始化阶段就完成了 实例的初始化 。(我的理解:当类被调用时,无论是静态变量还是方法或创建对象、创建对象的子类等就会加载当前类)
借助jvm 类加载机制,保证实例的唯一性(初始化过程只会执行一次)
/**
* 饿汉模式
*/
class HungrySingleton {
// 私有化成员变量,防止外部直接调用访问
private static HungrySingleton instance = new HungrySingleton();
// 私有化构造方法,防止外部直接访问
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
3.静态内部类
也是利用类的加载机制来保证线程安全
只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一 种形式。
/**
* 静态内部类创建单例对象
*/
class InnerClassSingleton {
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
// 调用当前静态方法获取单例对象,同时也是加载当前对象
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
4.通过反射攻击实例:
/**
* 静态内部类创建单例对象
*/
class InnerClassSingleton {
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
// 调用当前静态方法获取单例对象,同时也是加载当前对象
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
public static void main(String[] args) throws Exception {
// 通过反射获取到 InnerClassSingleton 中的构造方法
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// 使用获取到的构造方法创建对象
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
//判断通过反射获取到的对象和通过内部类返回的对象是不是同一个实例
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton == instance);//false
}
}
静态内部类防止反射破坏
在私有的构造方法中添加非空判断
/**
* 在构造方法中添加非空判断来保证对象不被使用反射方式创建
*/
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException(" 单例不允许多个实例 ");
}
}
5.枚举类型
从newInstance方法的源码中可以看出外界不能通过反射创建枚举类型对象的实例
并且枚举类型默认就是单例的,请看下面代码
public enum TestEnum {
INSTANCE;
}
测试:
@Test
public void test05(){
TestEnum i1 = TestEnum.INSTANCE;
TestEnum i2 = TestEnum.INSTANCE;
System.out.println(i2 == i1); // ture
}
6.通过反序列化流攻击实例
示例:
使用 jackson 提供的 objectMapper 将对象序列化和反序列化
@Test
public void test06() throws JsonProcessingException {
User user1 = new User("小马666", "123456");
// 将java对象序列化为json格式的字符串
String json = objectMapper.writeValueAsString(user1);
// 将字符串反序列化为java对象
User user2 = objectMapper.readValue(json, User.class);
System.out.println(user1 == user2); //false
}
解决方案
// 实现 Serializable 接口表示当前类可以被序列化到本地文件中
class InnerClassSingleton implements Serializable {
// 指定序列化时的版本,保证因当前类中文本内容发生改变后可以正常被反序列化(可以搜索查看相关文章)
static final long serialVersionUID = 10008011116L;
//... 此处省略
// 定义 readResolve() 方法保证对象被反序列化后是同一个对象
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}