Java 单例模式实现
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。以下是几种常见的Java单例实现方式:
1. 饿汉式(Eager Initialization)
//单例模式(饿汉模式)
//要求Singleton 类只包含一个实例
class Singleton{
//static 当前成员成为了类属性
//此处instance 就可以保证在当前Java进程中只有一份(只能确保在当前进程)
private static Singleton instance=new Singleton();
public static Singleton getInstance(){
return instance;
}
//将构造方法设为private 防止外部使用,创建新的实例
private Singleton(){
}
}
public class Demo12 {
//此时new 实例会报错,因为构造方法设为私有,不能被外部访问
//Singleton s= new Singleton();
Singleton si=Singleton.getInstance();//获取实例方法
}
特点:
-
类加载时就创建实例
-
线程安全
-
可能造成资源浪费(如果实例未被使用)
2. 懒汉式(Lazy Initialization)
//单例模式(懒汉模式)
class SingletonLazy{
private static SingletonLazy instance=null;
static Object locker=new Object();
//相对于饿汉模式,创建对象的时机推迟到第一次使用的时候再创建
public static SingletonLazy getInstance() {
synchronized(locker){
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo13 {
SingletonLazy instance=SingletonLazy.getInstance();
}
特点:
-
延迟初始化
-
线程不安全(只出现在实例化之前)=>通过synchronized可解决
-
性能较差(每次获取实例都要同步)
关于线程安全问题,按上面的方法,只要一进入方法就会加锁,但是懒汉模式的线程安全问题只存在于第一次创建实例对象时存在,因此存在效率变低的问题,通过下面的类型可以解决
3. 双重检查锁(Double-Checked Locking)
class SingletonLazy{
private volatile static SingletonLazy instance=null;
static Object locker=new Object();
//相对于饿汉模式,创建对象的时机推迟到第一次使用的时候再创建
public static SingletonLazy getInstance() {
//双重加锁
//这个条件用于判断是否需要加锁
if(instance==null){
synchronized(locker){
//是否需要创建新实例
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo13 {
SingletonLazy instance=SingletonLazy.getInstance();
}
特点:
-
延迟初始化
-
线程安全
-
高性能(只有第一次创建时需要同步)
-
需要volatile关键字修饰实例对象防止指令重排序
为什么只有第一次需要加锁
第一次创建之后, 实例已存在,直接返回,避免不必要的锁开销
volatile
修饰的instance
变量确保:
禁止指令重排序(防止返回未完全初始化的对象)
写操作(创建实例)对其他线程立即可见
如果没有volatile,instance = new SingletonLazy()
可能会被重排序为:
-
分配内存空间
-
将引用赋值给instance(此时instance!=null)
-
初始化对象
这样其他线程可能拿到未完全初始化的实例。
import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;
class DBUtil{
private volatile static DataSource dataSource=null;
public static DataSource getDataSource(){
if(dataSource==null){
synchronized(DBUtil.class){
if(dataSource==null){
//这种写法有问题
//因为锁不禁止重排序,而 volatile不保证多步操作的原子性。
dataSource=new MysqlDataSource();
((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1: 3306/hhh? characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root2");
((MysqlDataSource)dataSource).setPassword("27838");
}
}
}
return dataSource;
}
}
场景:多步非原子操作(如 new
+ setUrl
+ setUser
+ setPassword
)
volatile DataSource dataSource; // 线程A
dataSource = new MysqlDataSource(); // (1)
((MysqlDataSource) dataSource).setUrl(...); // (2)
((MysqlDataSource) dataSource).setUser(...); // (3)
((MysqlDataSource) dataSource).setPassword(...); // (4)
❌ 问题:
-
volatile
只保证dataSource
引用的写入是原子的(即(1)
的赋值)。 -
但
(2)
,(3)
,(4)
是独立的操作,volatile
不保证它们作为一个整体原子执行。
可能的重排序问题
线程A |
线程B |
---|---|
执行 | |
线程切换 → 线程B 运行 | |
| |
| |
继续执行 |
即使 dataSource
是 volatile
,线程B 仍然可能拿到一个未完成初始化的对象。
解决方法:先进行初始化再赋值
synchronized (DBUtil.class) {
if (dataSource == null) {
MysqlDataSource temp = new MysqlDataSource(); // 临时变量
temp.setUrl(...);
temp.setUser(...);
temp.setPassword(...);
dataSource = temp; // 最后一步:赋值给 volatile 变量
}
}
或者使用静态内部类,如下所示6
4. 静态内部类(Initialization-on-demand Holder)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
特点:
-
延迟初始化
-
线程安全
-
无需同步
-
实现简洁
5. 枚举实现(Enum Singleton)
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
特点:
-
线程安全
-
防止反射攻击
-
防止序列化破坏单例
-
简洁高效
最佳实践建议
-
如果不需要延迟加载,推荐使用饿汉式
-
如果需要延迟加载,推荐使用静态内部类方式
-
如果需要防止反射和序列化破坏单例,推荐使用枚举方式
-
在Java 5及以上版本,可以使用双重检查锁方式
单例模式适用于需要全局唯一实例的场景,如配置管理、线程池、数据库连接池等。