单例模式
单例模式 (Singleton Pattern)是软件工程中的一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
实现单例模式的方法
前置条件
创建一个User类,模拟单例模式中创建对象使用。
c
public class User {
private Integer id;
private String name;
private String password;
public User() {
}
public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
懒汉式(Lazy Initialization)
单例模式中常见的模式之一,懒汉式可以做到使用单例对象时才创建对象,可以实现延迟加载,但是存在线程安全问题,需要通过synchronized关键字保证了线程安全,但会影响性能。
c
/*懒汉式 线程不安全 需要使用 synchronized*/
public class Lazy {
private static User user;
//没有synchronized关键字,线程不安全,多线程调用此方法时会创建不同地址值的User对象
//对外提供接口
public static synchronized User getUser() {
if (user == null) {
user = new User(1,"zhao","123456");
}
return user;
}
public static void main(String[] args) {
//测试多线程下的懒汉式
for (int i = 0; i < 10; i++) {
new Thread(() -> {
User user = getUser();
System.out.println(user);
}
).start();
}
}
}
若没有添加synchronized关键字执行结果为:
会出现创建出多个对象的情况,背离了单例模式的初初衷。
添加synchronized关键字后可以保障对象的唯一性
饿汉式(Eager Initialization)
单例模式中常见的模式之一,饿汉式是在类加载时就创建实例比较简单,可以保证线程安全,但不支持延迟加载。
c
/*
* 饿汉式 线程安全
* */
public class Hungry {
//类加载时就创建实例
private static final User user = new User(1,"zhao","123456");
//对外提供接口
public static User getUser(){
return user;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
User user = getUser();
user.setId(2);
System.out.println(user);
}
).start();
}
}
}
运行结果:
实现了单例模式
双重锁式(Double-Checked Locking)
双锁结构,优化懒汉式,为了提高性能同时保持线程安全性,可以采用双重检查锁定的方式。
volatile关键字: 是一种轻量级的同步机制,适用于那些不需要复杂同步逻辑的简单场景。如果应用需要更复杂的并发控制,那么应该考虑使用更高级的同步工具
优点:
1、可见性:
当一个线程修改了 volatile 变量的值,这个修改会立即对其他线程可见。
每次读取 volatile 变量时,都会直接从主内存中读取最新的值,而不是使用缓存中的旧值。
每次写入 volatile 变量时,都会立即将更新后的值写回主内存,确保其他线程可以看到最新的状态。
2、禁止指令重排序:
Java 编译器和处理器为了优化性能,可能会对指令进行重排序。然而,对于 volatile 变量的操作,编译器和运行时环境都必须遵守一定的规则,不能将 volatile 写操作放到读操作之后,也不能将 volatile 读操作放到写操作之前。这有助于维持程序的逻辑正确性。
缺点:
1、不保证原子性:
尽管 volatile 提供了可见性和有序性,但它并不提供原子性。这意味着如果对 volatile 变量执行复合操作(如 i++),这些操作仍然可能受到竞态条件的影响,因为它们不是原子性的。要确保原子性,可以考虑使用同步机制、Atomic 类或锁等方法。
synchronized 关键字: 是 Java 中用于实现线程同步的关键字,它能够确保在多线程环境中对共享资源的安全访问。
c
/*
双锁结构,优化懒汉式,为了提高性能同时保持线程安全性,可以采用双重检查锁定的方式。
*/
public class DoubleChecked {
// 使用volatile避免指令重排序,保证user的可见性
private static volatile User user;
//对外提供接口
public static User getUser() {
if (user == null) {
synchronized (User.class) {
if (user == null) {
user = new User(1, "zhao", "123456");
}
}
}
return user;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
User user = getUser();
System.out.println(user);
}
).start();
}
}
}
运行结果:
静态内部类式(Static Inner Class)
利用了Java语言的特性,只有当内部类被加载时才会创建单例实例,因此它是线程安全且延迟加载的。
静态内部类: 只有在它被显式使用时才会被加载,比如创建静态内部类的实例或者访问其静态成员。
c
/*
* 内部类 利用了Java语言的特性,只有当内部类被加载时才会创建单例实例,因此它是线程安全且延迟加载的。
*/
public class StaticInnerClass {
//创建内部类:只有当内部类被加载时才会创建单例实例
private static class GetUserInnerClass{
private static final User user = new User(1,"zhao","123456");
}
//对外提供接口
public static User getUser(){
return GetUserInnerClass.user;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
User user = getUser();
System.out.println(user);
}
).start();
}
}
}
运行结果:
枚举式(Enum)
枚举类型的单例模式之所以线程安全,主要是因为Java语言规范对枚举类型的初始化提供了保证,并且在多个方面限制了创建新实例的可能性。以下是具体的原因:
1. 类加载机制
Java的类加载器在加载类时是线程安全的。当一个类第一次被加载到JVM中时,类加载器会确保这个过程是原子性的,即同一时间只有一个线程可以执行类的加载和初始化。对于枚举类型来说,它的静态字段(包括枚举常量)是在类加载期间初始化的,这意味着所有枚举实例的创建都是在这个安全的过程中完成的。
2. 构造函数私有化
枚举类型的构造函数默认是私有的,这防止了外部代码通过new关键字来创建新的枚举实例。即使使用反射技术尝试调用私有构造函数,在Java 5及以上版本中,JVM也特别处理了这种情况,以确保无法通过反射创建额外的枚举实例。
3. 静态初始化块
枚举实例是通过静态初始化块创建的,而静态初始化块只会在类加载时执行一次,并且由JVM保证其线程安全性。由于枚举实例是静态成员变量的一部分,它们的初始化也是线程安全的。
4. 序列化机制
Java的序列化机制为枚举类型提供了特殊的支持。当试图反序列化一个枚举实例时,JVM不会创建一个新的对象,而是返回已经存在的枚举常量。如果有人尝试通过反序列化来创建新的枚举实例,JVM会抛出InvalidObjectException异常,从而防止了这种攻击。
5. 克隆保护
枚举类型还重写了Cloneable接口中的clone()方法,使其抛出CloneNotSupportedException异常。这阻止了通过克隆方式创建新的枚举实例。
6. 反射保护
如前所述,尽管反射可以用来访问私有构造函数或字段,但Java的反射API对枚举类型进行了特殊的处理,使得无法利用反射来创建新的枚举实例。
c
//枚举实现单例模式
//枚举类型本质上就是线程安全的,并且默认情况下是不可变和序列化的,非常适合用来实现单例模式。
public enum Enum {
GETUSER;
//枚举实现单例模式
private User user;
//构造方法私有化,创建user对象
Enum() {
user = new User(1,"zhao","123456");
}
//获取user对象
public User getUser() {
return user;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Enum e = Enum.GETUSER;
User user = e.getUser();
System.out.println(user);
}
).start();
}
}
}
运行结果
总结
1. 饿汉式(Eager Initialization)
特点:类加载时即创建实例。
优点:线程安全,实现简单。
缺点:不支持延迟加载,可能浪费资源。
2. 懒汉式(Lazy Initialization)
特点:第一次调用 getInstance() 方法时才创建实例。
优点:支持延迟加载。
缺点:基本实现不是线程安全的;需要额外措施确保线程安全(如同步整个方法或使用双重检查锁定)。
3. 双重检查锁定(Double-Checked Locking)
特点:懒加载且线程安全,只在第一次创建实例时加锁。
优点:线程安全,支持延迟加载,性能较好。
缺点:实现稍微复杂一些。
4. 静态内部类(Static Inner Class)
特点:利用了Java语言特性,只有当静态内部类被加载时才会创建单例实例。
优点:线程安全,支持延迟加载,代码简洁。
缺点:相对不太直观。
5. 枚举(Enum)
特点:最简洁、最安全的方式,天然防止反射和序列化攻击。
优点:线程安全,支持延迟加载,代码非常简洁,防止反序列化攻击。
缺点:扩展性有限,不能继承其他类(只能实现接口)。
选择哪种方式?
1、如果你确定你的应用不会有多线程问题或者你不关心性能,那么饿汉式可能是最简单的解决方案。
2、如果你需要延迟加载并且希望保持线程安全,那么建议使用双重检查锁定、静态内部类或枚举的方式来实现懒汉式单例模式。
3、枚举类型的单例模式是实现单例模式的一个非常好的选择,特别是在你需要一种简单、线程安全并且能抵抗反射和序列化攻击的情况下。