懒汉模式
/** * @Description 懒汉模式 实现了延迟加载,synchronized会额外消耗资源,因此执行效率较低 */ public class LazySingleton { private static LazySingleton lazySingleton; private LazySingleton(){ } public synchronized static LazySingleton getLazySingleton(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } public static void main(String[] args) { LazySingleton h1 = LazySingleton.getLazySingleton(); LazySingleton h2 = LazySingleton.getLazySingleton(); System.out.println(h1==h2); } }
饿汉模式
1 /** * @Description 饿汉模式 * 类加载时完成对象实例化,避免多线程同步问题,但是无论是否使用对象,都在占用内存空间 */ public class HungrySingleton { private static HungrySingleton singleton = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getSingleton(){ return singleton; } public static void main(String[] args) { HungrySingleton h1 = HungrySingleton.getSingleton(); HungrySingleton h2 = HungrySingleton.getSingleton(); System.out.println(h1==h2); } }
双重校验锁模式
@description: 1.懒加载,2 volatile 保证了指令不会重排
*/public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton singleton; private DoubleCheckSingleton() { } public static DoubleCheckSingleton getInstance(){ if(singleton == null){ synchronized (DoubleCheckSingleton.class){ if(singleton == null){ singleton = new DoubleCheckSingleton(); } } } return singleton; } public static void main(String[] args) { DoubleCheckSingleton d1 = DoubleCheckSingleton.getInstance(); DoubleCheckSingleton d2 = DoubleCheckSingleton.getInstance(); System.out.println(d1==d2); } }
singleton = new DoubleCheckSingleton();实际过程分为3步
1.为singleton分配内存空间
2.初始化singleton
3.singleton指向分配的内存地址
由于jvm具有指令重排的特性,因此指令可能变成1->3->2。指令重排在单线程下不会出现问题,但多线程环境中会造成一个线程获得没有初始化的实例
使用volatile可以禁止指令重排,在多线程环境下也能正常运行
静态内部类实现单例模式
1.静态内部类实现单例模式,既保证了延迟加载,又能保证线程安全
public class Singleton {
private Singleton() {
System.out.println("Singleton初始化方法");
}
static {
System.out.println("Singleton的静态块");
}
public static Singleton getInstance(){
return SingletonInner.INSTANCE;
}
private static class SingletonInner{
static {
System.out.println("静态内部类.....");
}
private static final Singleton INSTANCE = new Singleton();
}
// 重新readResolve,解决反序列化创建新对象问题
private Object readResolve() {
return SingletonInner.INSTANCE;
}
public static void main(String[] args) {
//创建外部类对象,不会自动加载静态内部类
Singleton singleton = new Singleton();
//访问内部类对象时或者访问内部类对象的静态属性时,才会去初始化类,这样保证了延迟加载
Singleton.SingletonInner inner = new SingletonInner();
}
}
静态内部类实现单例模式也不是完全安全的;反射,反序列化会破坏它的单例。当把对象序列化后,再次反序列化时属于深拷贝(完全复制一个对象,但是会在内存中申请新的空间)
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
//将单例对象序列化
FileOutputStream fos = new FileOutputStream("D://test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
//反序列化生成一个新对象,两次hashcode值不一样
FileInputStream fis = new FileInputStream("D://test.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton result = (Singleton) ois.readObject();
System.out.println(result.hashCode());
}
1) 从类的加载分析为什么使用静态内部类出现延迟加载
类加载的生命周期分为7个阶段: 加载,验证,准备,解析,初始化,使用,卸载
- 加载: 在这个阶段虚拟机需要完成三件事
(1) 通过一个类的全限定名来获取定义此类的二进制字节流
(2) 将这个字节流中的静态储存结构转化为方法区的运行时数据结构
(3) 在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口 - 验证: 这个阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备: 为静态变量分配内存空间并设置初始值,这些变量使用的内存都在方法区中分配。int类型的变量默认值为0,布尔类型的默认值为false,非基本类型的初始值为null
- 解析: 虚拟机将常量池的符号引用替换为直接引用
- 初始化: 执行类的clinit()方法(clinit = class + initialize 意为类的初始化)包括为类的静态变量赋初始值和执行静态代码块中的内容,执行的先后顺序取决于在源文中出现的位置
要执行一个类的方法,需要先完成该类的初始化。 从类的生命周期中看,静态代码块的执行在类初始化阶段,main方法的执行在使用阶段。因此上面会先执行静态块中的代码
对于初始化阶段,虚拟机严格规定: 有且只有5种情况必须对类立即进行初始化
(1) 遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果没有对类初始化,则先触发类的初始化。这4个指令对应的java场景是: 使用new创建一个新对象、访问或设置一个类的静态属性、访问类的静态方法
(2) 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,则先触发其初始化
(3) 当初始化一个类时,如果发现其父类还没有初始化,则先触发其父类的初始化
(4) 当虚拟机启动时,用户需要指定一个执行的主类(包含main方法的类),虚拟机会先初始化启动类
(5) 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
静态内部类实现单例,触发第一种情况的getstatic方法,即访问该类的静态变量,这个时候触发Inner类的初始化.
2) 线程安全
虚拟机会保证一个类的clinit()方法在多线程环境中被正确加锁,同步。如果多线程同时去初始化一个类,那么只有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果一个类的clinit()方法中有很耗时的操作,可能会造成多个线程长期阻塞
参考:https://www.jianshu.com/p/ef263cf7f7f9
2.枚举实现单例模式
单实例枚举实现单例模式保证了线程安全,不会被反射,反序列化破坏单例模式
public enum EnumDemo {
INSTANCE;
//构造方法默认就是private
EnumDemo() {
}
public static String getMsg() {
return "调用枚举实现单例的方法";
}
}
public class Test {
public static void main(String[] args) {
//获取的是同一个实例,枚举底层定义的是一个常量
EnumDemo demo1 = EnumDemo.INSTANCE;
EnumDemo demo2 = EnumDemo.INSTANCE;
System.out.println(demo2 == demo1);
//调用方法
EnumDemo.INSTANCE.getMsg();
}
}
验证不能通过反射创建枚举实例
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//通过反射获取实例
Constructor<EnumDemo> constructor = EnumDemo.class.getDeclaredConstructor();
constructor.setAccessible(true);
//或抛出异常,不存在构造函数
EnumDemo enumDemo = constructor.newInstance();
System.out.println(enumDemo);
}
}
//反编译后的代码
public final class EnumDemo extends Enum
{
public static EnumDemo[] values()
{
return (EnumDemo[])$VALUES.clone();
}
public static EnumDemo valueOf(String name)
{
return (EnumDemo)Enum.valueOf(com/kuangstudy/Singleton/EnumDemo, name);
}
// 构造函数为有参构造
private EnumDemo(String s, int i)
{
super(s, i);
}
public EnumDemo getInstance()
{
return INSTANCE;
}
public static final EnumDemo INSTANCE;
private static final EnumDemo $VALUES[];
static
{
INSTANCE = new EnumDemo("INSTANCE", 0);
$VALUES = (new EnumDemo[] {
INSTANCE
});
}
}
验证反射中利用枚举的有参构造获取实例,是否可行
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//通过反射获取实例,会抛出异常信息
// “Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects”
Constructor<EnumDemo> constructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumDemo enumDemo = constructor.newInstance();
System.out.println(enumDemo);
}
//newInstance()方法中已经表名枚举不能通过反射创建实例
java的序列化机制针对枚举是做特殊处理的。在序列化枚举时,只会存储枚举常量的名称。
反序列化过程中,会根据名字查找对应的枚举实例。
public static void main(String[] args) throws IOException, ClassNotFoundException {
EnumDemo e1 = EnumDemo.INSTANCE;
System.out.println(e1.hashCode());
//将单例枚举对象序列化
FileOutputStream fos = new FileOutputStream("D://test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(e1);
//枚举的序列化是特殊处理的,并不是深拷贝,因此两次的hash值相同
FileInputStream fis = new FileInputStream("D://test.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
EnumDemo result = (EnumDemo) ois.readObject();
System.out.println(result.hashCode());
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。