1 解决的问题
- 频繁创建增加内存+时间。
2 单例模式(Singleton Pattern)
单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式。它的优点也显而易见:
- 它能够避免对象重复创建,节约空间并提升效率
- 避免由于操作不同实例导致的逻辑错误
单例模式有两种实现方式:饿汉式和懒汉式。
2.1 实现方式
2.1.1 实现 1:饿汉式
概念:变量在声明时便初始化。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}特点:
- 构造方法为 private,保证其他类无法实例化此类。
- 弊端:类加载后立即创建出来,占用内存和初始化时间。
- 等待被调用,被称为饿汉式。
- 优点:线程安全。
2.1.2 实现 2:懒汉式
概念:需要用时再初始化。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}特点:
- 优点:按需加载,避免内存浪费,降低类初始化时间。
- 弊端:多线程不安全。
2.1.3 实现 3:懒汉式-加锁
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}特点:执行效率低。
2.1.4 实现 4:懒汉式-双检锁(推荐)
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}instance = new Singleton(); 正常顺序如下:(1)分配内存、(2)初始化对象、(3)更新地址。
但是可能 JVM 会指令重排,变成(1)分配内存、(3)更新地址、(2)初始化对象。当执行完(3)后,若线程 B 对对象进行判断,发现对象不为 null,那么就会返回一个未初始化的对象,从而造成错误。
2.1.5 实现 5:静态内部类(推荐)
public class Singleton {
private static class SingletonHolder {
public static Singleton instance = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}更深层次问题:
- 静态内部类方式是怎么实现懒加载的
- 静态内部类方式是怎么保证线程安全的
Java 类的加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。
另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用 Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量 instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。
第二个问题的答案是 Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。
2.1.6 实现 6:枚举(推荐)
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
public class User {
private User(){ }
static enum SingletonEnum{
INSTANCE; //创建一个枚举对象,该对象天生为单例
private User user;
private SingletonEnum(){ //私有化枚举的构造函数
user=new User();
}
public User getInstnce(){
return user;
}
}
public static User getInstance(){ //对外暴露一个获取User对象的静态方法
return SingletonEnum.INSTANCE.getInstnce();
}
}
public class Test {
public static void main(String [] args){
System.out.println(User.getInstance());
System.out.println(User.getInstance());
System.out.println(User.getInstance()==User.getInstance());
}
}2.2 其他安全问题
除了 线程安全问题 限制创建对象,还有其他创建对象的方式:克隆 、 反射 、 序列化。
2.2.1 创建对象 1:克隆
要克隆一个类,需要(1)被克隆类实现 Cloneable 接口,(2)重写 clone() 函数。一般单例不会实现该接口,不存在破坏问题。
2.2.2 创建对象 2:反射
设置 Flag 标志位解决,但是通过反射修改 Flag 标志位同样还是会破坏。
public class Singleton {
// 私有构造方法
private Singleton() {
/*
反射破坏单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
2.2.3 创建对象 3:序列化
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = null;
// 序列化
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(singleton1);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
try {
singleton2 = (Singleton) ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(singleton1 == singleton2); // 输出:false
}
}
通过从 readObject 跟到 readOrdinaryObject,定位到下述代码:
- **
isInstantiable()**:一个 serializable/externalizable 的类是否可以在运行时被实例化; - **
desc.newInstance()**:通过反射的方式调用无参构造函数创建一个新对象;
- **
desc.hasReadResolveMethod()**:判断类是否实现了 readResolve()函数; - **
desc.invokeReadResolve(obj)**:有的反射调用此函数,如果在此函数中返回实例就可以了;
修改后的单例类代码:
import java.io.Serializable;
public class Singleton implements Serializable {
private Singleton() { }
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Object readResolve() {
return getInstance();
}
}2.3 枚举单例
安全简单,没有懒加载,最佳实践
public enum SingletonEnum {
INSTANCE;
private final AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
// 调用
SingletonEnum.INSTANCE.getId()
2.4 应用场景
-
处理资源访问冲突
日志工具类,多个实例,多线程并发写入可能存在互相覆盖的情况。 -
全局唯一类
数据在系统中只保存一份,如:用来封装全局配置信息的类
QQ 和微信,消息内容属于即时加载,打开软件时就加载各个聊天内容;朋友圈属于懒汉式加载。
《代码整洁之道》一书中也说到:不提倡使用懒加载方式,因为程序应该将构建与使用分离,达到解耦。饿汉式在声明时直接初始化变量的方式也更直观易懂。所以在使用饿汉式还是懒汉式时,需要权衡利弊。
一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。