1. 单例模式常见问题为什么要有单例模式单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只需要一个对象来协调整个系统的操作时,这种模式就非常有用.它描述了如何解决重复出现的设计问题,比如我们项目中的配置工具类,日志工具类等等。
如何设计单例模式 1.单例类如何控制其实例化2.如何确保只有一个实例通过一下措施解决这些问题:private构造函数,类的实例话不对外开放,由自己内部来完成这个操作,确保永远不会从类外部实例化类,避免外部随意new出来新的实例。
该实例通常存储为私有静态变量,提供一个静态方法,返回对实例的引用。如果是在多线程环境下则用锁或者内部类来解决线程安全性问题。2. 单例类有哪些特点 私有构造函数它将阻止从类外部实例化新对象它应该只有一个实例这是通过在类中提供实例来方法完成的,阻止外部类或子类来创建实例。
这是通过在java中使构造函数私有来完成的,这样任何类都不能访问构造函数,因此无法实例化它。
单实例应该是全局可访问的单例类的实例应该是全局可访问的,以便每个类都可以使用它。在Java中,它是通过使实例的访问说明符为public来完成的。节省内存,减少GC因为是全局至多只有一个实例,避免了到处new对象,造成浪费内存,以及GC,有了单例模式可以避免这些问题3. 单例模式8种写法下面由我给大家介绍8种单例模式的写法,各有千秋,存在即合理,通过自己的使用场景选一款使用即可。
我们选择单例模式时的挑选标准或者说评估一种单例模式写法的优劣时通常会根据一下两种因素来衡量:1.在多线程环境下行为是否线程安全2.饿汉以及懒汉3.编码是否优雅1. 饿汉式线程安全的public class SingleTon{private static final SingleTon INSTANCE = new SingleTon;private SingleTon{ }public static SingleTon getInstance{return INSTANCE;}public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println;}}
这种写法是非常简单实用的,值得推荐,唯一缺点就是懒汉式的,也就是说不管是否需要用到这个方法,当类加载的时候都会生成一个对象。除此之外,这种写法是线程安全的。
类加载到内存后,就实例化一个单例,JVM保证线程安全,2. 饿汉式线程安全。
public class SingleTon{private static final SingleTon INSTANCE ;static {INSTANCE = new SingleTon; }private SingleTon{}public static SingleTon getInstance{return INSTANCE;} public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println;}}
3. 懒汉式线程不安全。public class SingleTon{private static SingleTon instance ;private SingleTon{}public static SingleTon getInstance{ if{ instance = new SingleTon; } return instance;}public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println; // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
这种写法虽然达到了按需初始化的目的,但却带来线程不安全的问题,至于为什么在并发情况下上述的例子是不安全的呢 // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; }
为了使效果更直观一点我们对getInstance 方法稍做修改,每个线程进入之后休眠一毫秒,这样做的目的是为了每个线程都尽可能获得cpu时间片去执行。代码如下public static SingleTon getInstance{if{try { Thread.sleep;} catch { e.printStackTrace;}instance = new SingleTon;} return instance; }
执行结果如下为了解决这个问题,我们可以采取加锁措施,所以有了下面这种写法4. 懒汉式线程安全。
public class SingleTon{private static SingleTon instance ;private SingleTon{}public static SingleTon synchronized getInstance{if{ instance = new SingleTon;}return instance;}public static void main {SingleTon instance1 = SingleTon.getInstance;SingleTon instance2 = SingleTon.getInstance;System.out.println; // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
由于第三种方式出现了线程不安全的问题,所以对getInstance方法加了synchronized来保证多线程环境下的线程安全性问题,这种做法虽解决了多线程问题但是效率比较低。因为锁住了整个方法,其他进入的现成都只能阻塞等待了,这样会造成很多无谓的等待。于是可能有人会想到可不可以让锁的粒度更细一点,只锁住相关代码块可否?所以有了第五种写法。5. 懒汉式线程不安全public class SingleTon{private static SingleTon instance ;private SingleTon{}public static SingleTon getInstance{if{ synchronied{ instance = new SingleTon; }}return instance;}public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println; // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完instance是null的时候,线程A就进入了if块并且持有了synchronized锁,但是同时另外一个线程t2在线程t1还未创造出实例之前,就又进行了instance是否为null的判断,这时instance依然为null,所以线程t2也会进入if块去创造实例,他会在synchronized代码外面阻塞等待,直到t1释放锁,这时问题就出来了,有两个线程都实例化了新的对象。
造成这个问题的原因就是线程进入了if块并且在等待synchronized锁的过程中有可能上一个线程已经创建了实例,所以进入synchronized代码块之后还需要在判断一次,于是有了下面这种双重检验锁的写法。6. 懒汉式线程安全public class SingleTon{private static volatile SingleTon instance ;private SingleTon{}public static SingleTon getInstance{if{ synchronied{ if{ instance = new SingleTon; } }}return instance;}public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println; // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
这种写法基本趋于完美了,但是可能需要对一下几点需要进行解释:
- 第一个判空的作用 ?
- 第二个判空的作用 ?
- 为什么变量修饰为volatile ?
在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。首先要明白在JVM创建新的对象时,主要要经过三步。1.分配内存2.初始化构造器3.将对象指向分配的内存的地址因为仅仅一个new 新实例的操作就涉及三个子操作,所以生成对象的操作不是原子操作而实际情况是,JVM会对以上三个指令进行调优,其中有一项就是调整指令的执行顺序。所以,在指令被排序的情况下可能会出现问题,假如 2和3的步骤是相反的,先将分配好的内存地址指给instance,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为instance对象已经实例化了,直接返回一个引用。
如果这时还没进行构造器初始化并且这个线程使用了instance的话,则会出现线程会指向一个未初始化构造器的对象现象,从而发生错误。7. 静态内部类的方式public class SingleTon{public static SingleTon getInstance{return StaticSingleTon.instance;}private static class StaticSingleTon{ private static final SingleTon instance = new SingleTon;}public static void main { SingleTon instance1 = SingleTon.getInstance; SingleTon instance2 = SingleTon.getInstance; System.out.println; // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
- 因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。
- 另外由于静态变量只初始化一次,所以singleton仍然是单例的。
public Enum SingleTon{INSTANCE;public static void main { // 通过开启100个线程 比较是否是相同对象 for{ new Thread-> System.out.println.hashCode) ).start; } }}
这种写法从语法上看来是完美的,他解决了上面7种写法都有的问题,就是我们可以通过反射可以生成新的实例。但是枚举的这种写法是无法通过反射来生成新的实例,因为枚举没有public构造方法。感谢大家看到最后,我是不务正业的程序汪,文章每天持续更新!欢迎大家指出我的文章的不足之处,也欢迎大家关注、点赞+转发。