饿汉式
/**
* 饿汉模式(静态变量)
*/
public class HungryMan {
//构造器私有化 (防止 new )
private HungryMan(){}
//类的内部创建对象
private final static HungryMan hungryMan = new HungryMan();
//对外暴露一个静态的公共方法
public static HungryMan getInstance(){
return hungryMan;
}
}
/**
* 饿汉模式(静态代码块)
*/
public class HungryMan {
//构造器私有化 (防止 new )
private Hungry(){}
//类的内部创建对象
private final static HungryMan hungryMan;
static{
hungryMan = new HungryMan();
}
//对外暴露一个静态的公共方法
public static HungryMan getInstance(){
return hungryMan;
}
}
以上的两种方式其实差不多,有一样的优缺点:
-
在类加载时完成实例化,避免了线程同步问题。
-
如果从头到尾都没有用过该实例,就会造成内存的浪费。
总结:可以用,但可能会造成浪费,不推荐使用
懒汉式
/**
* 懒汉式(线程不安全)
*/
public class LazyMan {
private LazyMan(){}
private static LazyMan instance;
//提供一个静态的公有方法,当使用到该方法时,再去创建instance
public static LazyMan getInstance(){
if(instance == null){
instance = new LazyMan();
}
return instance;
}
}
虽然起到了懒加载的效果,避免内存浪费。但是会造成线程不安全。
如果在多线程下,一个线程进入了if判空,还未来得及往下new对象,另一线程也进入了If判空,就会产生多个实例。因此,在实际开发中不能这样使用单例模式。
所以我们就会想到要解决线程不安全的问题,在getInstnce()方法中加上 synchronized锁
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
但是这样会造成效率低下的问题,每个线程在想获得类的实例时,还得等其他线程执行完这个同步方法。实际上,这个方法只需要执行一次实例化代码就够了,后面的直接return。
有一种很优秀的DCL(双重检查锁)模式,不仅达到了懒加载效果,还保证了线程安全以及提高效率。
//双重检查代码(Double-Check)
public class LazyMan {
//单例模式必须构造器私有
private LazyMan(){}
private static LazyMan lazyMan;
//双重检测锁模式 懒汉式单例 DCL(双重检查锁)懒汉式
public static LazyMan getInstance(){
//第一层判空 提高性能 避免资源浪费
//如果不判空的话 就跟第三种的效率是一样低的
if ( lazyMan == null ) { //step1
synchronized (LazyMan.class) {
//由于可能多个线程都进入了step1,由于锁定机制,一个线程进入该代码块时,其他线程
//仍在排队进入该代码块,如果不做判断,当前线程即使创造了实例,下一个线程也不知道,就会继续创建一个实例。
if (lazyMan == null) {
lazyMan = new LazyMan(); //不是一个原子性操作
}
}
}
return lazyMan;
}
//我们可以创建一个man方法进行测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan lazyMan = LazyMan.getInstance();
System.out.println(lazyMan.hashCode());
}).start();
}
}
}
结果显示:
看起来似乎没有什么问题了,但实际上存在一个并发陷阱---- 当lazyMan != null时,仍可能指向一个空对象
因为 lazyMan = new LazyMan(); //不是一个原子性操作,它可以被抽象为下面几条JVM指令
memory = allocate(); //1:分配对象的内存空间
initLazyMan(memory); //2:初始化对象
lazyMan = memory; //3:设置lazyMan指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序
操作 3很有可能 排在了操作 2 之前,即引用lazyMan指向内存memory时,这段崭新的内存还没有初始化——即,引用lazyMan指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于lazyMan已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
这个时候,需要volatile来解决了! vlatile能保持内存可见性
//再静态变量前加上 volatile关键字
private static volatile LazyMan lazyMan;
静态内部类
//静态内部类 既达到了懒加载效果,又保证了线程安全
public class OuterClass {
private OuterClass() {
}
public static class InnerClass {
private static final OuterClass outerClass = new OuterClass();
}
public static OuterClass getInstance(){
return InnerClass.outerClass;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
OuterClass outerClass = OuterClass.getInstance();
System.out.println(outerClass.hashCode());
}).start();
}
}
}
结果就不贴出来了,每个线程里的实例化对象哈希码都是一样,即同个实例。
当Holder类被加载时,静态内部类并不会马上被加载,只有调用Holder的getInstance()方法时,静态内部类才会被加载,这就实现了第一点:懒加载效果;
而类的静态属性只会在第一次加载类时初始化。 也就是说当JVM装载类时,底层提供了装载机制保证了初始化是单线程的,即线程安全的。
总结:推荐使用
枚举
//枚举本身也是一个class
public enum SingleTon {
INSTANCE;
}
//创建一个测试类
public class TestSingleton {
public static void main(String[] args) {
SingleTon instance = SingleTon.INSTANCE;
SingleTon instance2 = SingleTon.INSTANCE;
System.out.println(instance == instance2);
}
}
结果输出:
反射会破坏单例模式
public static void main(String[] args) throws Exception {
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//跟LazyMan.getInstance(); 不是同一个实例
LazyMan instance = declaredConstructor.newInstance();
flag.set(instance,false);
//当设置有标志位,两个通过反射生成的实例对象依旧有办法通过 并且不是同一个实例
//利用属性重新赋值 flag.set(instance,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
反射不能破坏枚举
import java.lang.reflect.Constructor;
import java.util.EmptyStackException;
//enum 是一个什么? 本身也是一个class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
//EnumSingle instance2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstruct = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstruct.setAccessible(true);
EnumSingle instance2 = declaredConstruct.newInstance();
System.out.println(instance1);
System.out.println(instance2);
// java.lang.NoSuchMethodException: single.EnumSingle.<init>() 没有空参构造器 不是我们想要的错误
// 把无参换成有参 就会报我们理想的错误:Cannot reflectively create enum objects
}
}
总结
- 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需
要频繁创建销毁的对象,使用单例模式可以提高系统性能。 - 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使
用new。