目录
3.双重检查锁定(Double-Checked Locking):
5.注册式单例(Registration Style Singleton):
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
优点:
-
全局唯一性:单例模式确保在应用程序中只有一个实例,这对于共享资源、配置管理和维护全局状态非常有用。
-
懒加载:如果实例在第一次使用时才被创建,可以延迟实例化,从而节省资源。
-
全局访问点:单例提供了一个统一的入口点,使得可以轻松地访问单例实例,而不需要传递实例的引用。
-
节省内存:由于只有一个实例存在,因此节省了多个相同对象的内存。
-
避免竞态条件:在多线程环境中,单例模式可以通过加锁等机制来确保只有一个实例被创建,从而避免竞态条件。
缺点:
-
全局状态:单例模式引入了全局状态,这可能导致代码的复杂性和耦合性增加。全局状态可能会导致难以调试和理解的问题。
-
不适用于多线程:在多线程环境中,如果没有正确处理,单例模式可能导致性能问题或竞态条件。需要额外的措施来确保线程安全。
-
隐藏依赖:使用单例模式的类隐藏了其依赖关系,因为它们直接访问全局实例。这可能会使代码更难测试和维护。
-
不支持子类化:有时候,单例模式难以支持子类化,因为它将构造函数私有化,防止直接实例化子类。
-
单一职责原则:有时,将全局状态与其他功能耦合在一起可能会违反单一职责原则。
总之,单例模式是一个有用的设计模式,可以在需要确保只有一个实例存在的情况下使用。然而,开发人员应该谨慎使用,考虑到它可能引入的全局状态和复杂性。在多线程环境中,需要特别小心,以确保线程安全。
单例模式的5种实现方式:
1.饿汉式(Eager Initialization):
-
优点:
- 线程安全:实例在类加载时就创建,不会出现多线程竞争的问题。
- 简单:实现简单,不需要额外的同步措施。
-
缺点:
- 资源浪费:如果实例在应用程序启动时创建,可能会导致资源浪费。
- 不支持延迟初始化:无法实现延迟初始化,不适用于大型对象
public class HungrySingleton {
// 私有化构造方法:阻止通过构造方法实例化单例对象
private HungrySingleton(){}
// 私有属性:单例对象
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 提供全局访问点
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
静态代码块的方式也属于类加载时候执行
public class StaticHungrySingleton {
private StaticHungrySingleton(){}
private static StaticHungrySingleton hungrySingleton;
static {
hungrySingleton = new StaticHungrySingleton();
}
public static StaticHungrySingleton getInstance(){
return hungrySingleton;
}
}
2.懒汉式(Lazy Initialization):
-
优点:
- 延迟初始化:实例只在第一次使用时创建,节省了资源。
- 线程安全:可以通过加锁等机制实现线程安全。
-
缺点:
- 线程竞争:在多线程环境中,可能出现竞态条件,需要额外的同步措施。
- 性能开销:因为需要在访问时检查实例是否已创建,可能会导致性能开销。
a.线程不安全的懒汉式:
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySingleton = null;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySimpleSingleton();
}
return lazySingleton;
}
}
多线程环境下不安全的原因:如果多个线程能够同时进入if(lazySingleton == null),并且此时lazySingleton为空,就会有多个线程执行lazySingleton = new LazySimpleSingleton();将导致多次实例化lazySingleton.
b.线程安全的懒汉式:
通过对getInstance()方法加锁,可以实现一个时间点只有一个线程能够进入该方法,避免多次实例化的问题。
public class LazySynchronizedSingleton {
private static LazySynchronizedSingleton lazySingleton = null;
private LazySynchronizedSingleton(){}
public static synchronized LazySynchronizedSingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySynchronizedSingleton();
}
return lazySingleton;
}
}
3.双重检查锁定(Double-Checked Locking):
-
优点:
- 延迟初始化:实现了延迟初始化,避免了资源浪费。
- 线程安全:通过双重检查和同步块来确保线程安全。
-
缺点:
- 复杂性:实现较为复杂,需要小心处理细节,否则可能会出现错误
方法加上synchronized锁解决了线程安全问题,但是在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。所以为了兼顾性能和线程安全问题,我们可以通过双重检查锁的方式创建懒汉式单例。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;// volatile禁止指令重排序
private LazyDoubleCheckSingleton(){}
public static synchronized LazyDoubleCheckSingleton getInstance(){
if(lazySingleton == null){ // #1
// 如果lazySingleton为空才加锁进行初始化
synchronized(LazyDoubleCheckSingleton.class){ // #2
if (lazySingleton == null){ // #3
/* 如果没有第二次判断就会发生:
1.A执行完#1
2.在#1和#2之间发生了线程切换,切换到了B
3.线程B执行了#2,获取到了锁,并初始化了lazySingleton
4.切换到了线程A,线程A执行了#2,获取到了锁,并初始化了lazySingleton
这样就导致初始化了两次,所以必须进行二次校验
*/
lazySingleton = new LazyDoubleCheckSingleton(); // #4
}
}
}
return lazySingleton;
}
}
volatile关键字的作用是防止多线程环境下#3和#4发生重排序,可能导致NPE(空指针异常)。
4.静态内部类(Static Inner Class):
-
优点:
- 懒加载:实现了延迟初始化,且不需要额外的同步措施。
- 线程安全:利用类加载机制保证了线程安全性。
-
缺点:
- 不支持传递参数:无法传递参数给单例的构造函数。
我们知道用到synchronized关键字总归要上锁,对程序性能还是存在一定影响的。从类的初始化角度来考虑,可以采用静态内部类的方式。
/*
懒汉式单例-静态内部类只会在调用instance的时候初始化,延迟初始化的同时虚拟机提供了对线程安全的支持。
*/
public class LazyStaticInnerSingleton {
private LazyStaticInnerSingleton(){}
public static synchronized LazyStaticInnerSingleton getInstance(){
return LazySingletonHolder.lazySingleton;
}
private static class LazySingletonHolder {
private static LazyStaticInnerSingleton lazySingleton = new LazyStaticInnerSingleton();
}
}
5.注册式单例(Registration Style Singleton):
a.枚举(Enum):
-
优点:
- 简单:实现简单,线程安全,不需要处理细节问题。
- 序列化安全:自带序列化机制,防止通过反序列化创建新实例。
-
缺点:
- 不支持懒加载:枚举类型的实例在类加载时就创建,无法实现懒加载。
/*
Effective Java 推荐的方式:使用jvm封装的枚举类通过注册的形式获取单例,实际上也算一种饿汉式,但是性能损耗已经被jvm处理过,相比于个人进行优化要好得多。
*/
public enum LazyEnumSingleton {
INSTANCE;
// 使用时直接调用LazyEnumSingleton.INSTANCE.sayHello()实现单例。
public void sayHello(){
System.out.println("Hello world !");
}
}
b.容器实现(Container Managed Singleton):
-
优点:
- 灵活性:容器可以管理单例的生命周期,适用于某些特定的企业应用场景。
-
缺点:
- 依赖容器:需要依赖容器(如Java EE容器)来管理单例,不适用于所有应用。
- 复杂性:需要配置和维护容器。
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className){
synchronized (ioc){
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
c.本地线程单例(ThreadLocal):
-
优点:
- 单个线程安全:能保证在单个线程中是唯一的,天生的线程安全。
-
缺点:
- 不保证全局唯一:ThreadLocal 不能保证其创建的对象是全局唯一。
/*
ThreadLocal也算是一种注册式的单例实现:线程间隔离的ThreadLocal其实是存储在一个TreadLocalMap中,初始化的时候就put到了Map里,有就直接拿出来用。
ThreadLocal采用空间换时间的方式为每一个线程都提供了一份变量,同时访问而不影响。同步机制是采取时间换空间的方式排队访问。
*/
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
private static final ThreadLocal<ThreadLocalSingleton> tlSingleton = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue(){
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return tlSingleton.get();
}
}
三种破坏单例模式的方式:
1.反射
通过反射拿到私有的构造方法,设置setAccess()为true更改访问权限为public暴力初始化也能达到破坏单例的目的。
反射破坏单例的解决方案:在构造方法中添加限制条件,抛出异常阻止初始化
public class DestroySingletonReflex {
private DestroySingletonReflex(){
// 反射破坏单例的解决方案:在构造方法中添加判断,抛出异常
// if (DestroySingletonReflexHolder.destroySingletonReflex != null){
// throw new RuntimeException("不允许创建多个实例!");
// }
}
private static class DestroySingletonReflexHolder{
private static DestroySingletonReflex destroySingletonReflex = new DestroySingletonReflex();
}
public static DestroySingletonReflex getInstance(){
return DestroySingletonReflexHolder.destroySingletonReflex;
}
/*
通过反射破坏单例
*/
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 通过反射创建实例
Class<?> clazz = DestroySingletonReflex.class;
// 通过反射获取构造方法
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
// 暴力初始化
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1);
System.out.println(o2);
System.out.println("是否相等:" + o1==o2);
}
}
2.序列化和反序列化
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
反序列化破坏单例的本质是ObjectInputStream类的readObject()通过反射方式调用NewInstance()初始化实例。
反序列化破坏单例的解决方案:添加readResolve方法,改变checkResolve()结果阻止反射机制初始化实例。
public class DestroySingletonSerialize implements Serializable{
private DestroySingletonSerialize(){
}
private static class DestroySingletonReflexHolder{
private static DestroySingletonSerialize destroySingletonReflex = new DestroySingletonSerialize();
}
public static DestroySingletonSerialize getInstance(){
return DestroySingletonReflexHolder.destroySingletonReflex;
}
public Object readResolve(){
return getInstance();
}
/*
通过序列化破坏单例
*/
public static void main(String[] args) throws IOException, ClassNotFoundException {
DestroySingletonSerialize d1 = null;
DestroySingletonSerialize d2 = DestroySingletonSerialize.getInstance();
// 通过FileOutPutStream + ObjectOutPutStream将对象d2写到文件里进行序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
outputStream.writeObject(d2);
outputStream.flush();
outputStream.close();
// 通过FileInputStream和ObjectInputStream将文件中的对象读到d1中
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("tempFile"));
d1 =(DestroySingletonSerialize) objectInputStream.readObject();
System.out.println(d1);
System.out.println(d2);
System.out.println(d1==d2);
}
}
3.原型模式调用clone()方法
原型破坏单例:实现Cloneable的单例类调用clone()复制实例是通过copy内存实现的,没有使用构造方法。
原型破坏单例的解决方案:不实现Cloneable接口。
public class DestorySingletonPrototype implements Cloneable{
private DestorySingletonPrototype(){}
private static class DestorySingletonPrototypeHolder{
private static DestorySingletonPrototype singletonPrototype = new DestorySingletonPrototype();
}
public static DestorySingletonPrototype getInstance(){
return DestorySingletonPrototypeHolder.singletonPrototype;
}
public static void main(String[] args) throws CloneNotSupportedException {
DestorySingletonPrototype singletonPrototype = DestorySingletonPrototype.getInstance();
DestorySingletonPrototype singletonPrototype1 = (DestorySingletonPrototype)singletonPrototype.clone();
System.out.println(singletonPrototype1);
System.out.println(singletonPrototype);
System.out.println(singletonPrototype1 == singletonPrototype);
}
}
使用场景:
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons