单例对象的类必须保证只有一个实例存在。许多时候,整个系统只需要拥有一个的全局对象
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能
例如,每台计算机可以有若干个打印机,但只能有一个 Printer Spooler(单例) ,以避免两个打印作业同时输出到打印机中。【类似临界区?】
比如,每台计算机可以有若干通信端口,系统应当集中 (单例)管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。
单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法
立即加载 : 在类加载初始化的时候就主动创建实例;
延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。
单例在单例类被加载时候,就实例化一个对象并交给自己的引用
// 饿汉式单例
public class Singleton1 {
// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();
// 私有的构造方法:因为一个类只能构造一个对象,因此不能随便new,得设成私有保证安全
private Singleton1(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}
由于类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。
// 懒汉式单例
public class Singleton2 {
// 指向自己实例的私有静态引用
private static Singleton2 singleton2;
// 私有的构造方法
private Singleton2(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton2 getSingleton2(){
// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
总结:
1. 饿汉懒汉的区别就是,饿汉一开始直接new了后面return不需要判空,懒汉一开始没有主动new所以return之前才new
2. 从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些
饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;
而懒汉式,很可能多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例。
// 线程安全的懒汉式单例
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2(){}
// 使用synchronized修饰,临界资源的同步互斥访问,防止new操作多次执行
public static synchronized Singleton2 getSingleton2(){
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
与上面传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton2()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。
缺点:运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗
// 线程安全的懒汉式单例
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2(){}
public static Singleton2 getSingleton2(){
synchronized(Singleton2.class){ // 使用 synchronized 块,临界资源的同步互斥访问
if (singleton2 == null) {
singleton2 = new Singleton2();
}
}
return singleton2;
}
}
缺点:运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高
// 线程安全的懒汉式单例
public class Singleton5 {
// 从外部无法访问的私有内部类,按需加载,用时加载,也就是延迟加载
private static class Holder {
private static Singleton5 singleton5 = new Singleton5();
}
private Singleton5() {
}
public static Singleton5 getSingleton5() {
return Holder.singleton5;
}
}
是调用getSingleton5()的时候才创建单例,而不是类被加载的时候,
优点:效率较高
// 线程安全的懒汉式单例
public class Singleton3 {
//使用volatile关键字防止重排序(JVM编译器-指令重排),因为 new Instance()是一个非原子操作,可能导致指向一个不完整的实例
private static volatile Singleton3 singleton3;
private Singleton3() {}
public static Singleton3 getSingleton3() {
// Double-Check idiom
if (singleton3 == null) {
synchronized (Singleton3.class) { // 1
// 只需在第一次创建实例时才同步
if (singleton3 == null) { // 2
singleton3 = new Singleton3(); // 3
}
}
}
return singleton3;
}
}
解读:首先判断现在是否有实例,若没有,则将该class锁起来准备创建,只允许一个线程进入。对于线程,进入了要判断现在实例是否存在,如果不存在(说明这时候是我第一个拿到了锁,有资格创建)就创建,如果存在了说明我不是第一个拿到锁的线程,之前的线程创建了,我就不创建了。
优点:使用双重检测同步延迟加载,不但保证了单例,而且切实提高了程序运行效率
注意:必须使用volatile修饰单例的引用
我理解的原因:在某个线程的内存里还没改,但是主存已经改了
PS:volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
真正原因:
因为new是非原子操作,包括“1 分配内存空间-2 初始化对象-3 singleton3指向刚刚分配的内存地址”,而这三步可能是无序的(指令重排序),有可能变成“1 分配内存空间-3 singleton3指向刚刚分配的内存地址-2 初始化对象”,所以有可能对象还没有初始化好就被指向了(即当成指向一个非null),此时别的线程认为他是非null的,就会得到一个未初始化的对象,导致错误。
而volatile关键字能阻止指令重排,保证指令的执行顺序
补充:volatile不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。
// 线程安全的懒汉式单例
public class Singleton4 {
// ThreadLocal 线程局部变量
private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<Singleton4>();
private static Singleton4 singleton4 = null;
private Singleton4(){}
public static Singleton4 getSingleton4(){
if (threadLocal.get() == null) {// 第一次检查:该线程是否第一次访问
createSingleton4();
}
return singleton4;
}
public static void createSingleton4(){
synchronized (Singleton4.class) {
if (singleton4 == null) {// 第二次检查:该单例是否被创建
singleton4 = new Singleton4(); // 只执行一次
}
}
threadLocal.set(singleton4);// 将单例放入当前线程的局部变量中
}
}
是上一个方法的变体。将双重检测的第一层检测条件 if (instance == null) 转换为线程局部范围内的操作
ThreadLocal 也只是用作标识而已,用来标识每个线程是否已访问过:如果访问过,则不再需要走同步块,这样就提高了一定的效率。
所有单例模式的缺点:无法防止利用反射重复构建对象
利用反射构造单例的方法:
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));//false,是两个对象
第一步,获得单例类的构造器。
第二步,把构造器设置为可访问。
第三步,使用newInstance方法构造对象。
因此必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。
那如何阻止反射?——用枚举实现单例,方法如下:
public enum SingletonEnum {
INSTANCE;
}
因为用了enum语法糖,JVM会阻止反射获取枚举类的私有构造方法
但是枚举方法缺点在于,他不是懒加载,在枚举类被加载的时候就初始化了
private static Singleton1 singleton1 = new Singleton1() 还是 = null
https://blog.csdn.net/czqqqqq/article/details/80451880
https://zhuanlan.zhihu.com/p/33102022?from_voters_page=true
https://www.cnblogs.com/xuwendong/p/9633985.html