1.序
今天为什么谈设计模式呢,因为设计模式对于我们找工作来说非常重要,记得我在面试华为的时候基本上把我知道的都讲了一遍,大概15-16种,因此给面试官留下了很好的印象,在面试其它的大公司的时候同样是这样,因为当时我花了很多时间把设计模式整理了一遍,所以因此受益,今天开始把每一种分享出来,今天先从单例模式开始吧,希望大家梦想成真!!!
2.单例模式初认识
单例模式是一种对象创建型模式,使用单例模式可以保证一个类只生成唯一的实例对象。也就是说在整个程序空间中,该类只存在一个实例对象。
其实就是保证一个类只有一个实例,同时提供能对该实例加以访问的全局访问方法。
在应用系统开发中,我们常常有以下需求:
1在多线程之间,比如servlet 环境。共享同一个资源或者操作同一个对象。
2在整个程序空间使用全局变量,共享资源。
3大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
因此singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,singleton模式就派上用场了。
3.单例模式的使用
3.1“懒汉式”与“饿汉式”的区别
所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。
“懒汉式”
“懒汉式”是在你真正用到的时候才去建这个单例对象:
-
私有化构造器
-
创建一个私有的实例static 先不实例化 为 null
-
通过公共方法调用 static 在方法里面进行判断,if = null实例化 !=null 直接return
比如:有个单例对象
public class Singleton{
private Singleton(){
}
private static Singleton singleton = null; //不建立对象
public static synchronized Singleton getInstance(){
if(singleton == null) {
//先判断是否为空
singleton = new Singleton (); //懒汉式做法
}
return singleton ;
}
}
“饿汉式”
“饿汉式”是在不管你用的用不上,一开始就建立这个单例对象:比如:有个单例对象
:一个类只能创建一个对象
-
私有化构造器
-
在类的内部创建一个类的实例,且为static
-
私有化对象,通过公共方法调用
-
此公共方法只能通过类来调用,因为设置的是static,同时类的实例也是static
public class Singleton{
public Singleton(){
}
private static Singleton singleton = new Singleton(); //建立对象
public static Singleton getInstance(){
return singleton ;//直接返回单例对象
}
}
3.2 GetInstance与new区别
new的使用:如Object _object = new Object(),new就是通过生产一个新的实例对象,或者在栈上声明一个对象,每部分的调用 *都是用的一个新的对象。
GetInstance的使用:在主函数开始时调用,返回一个实例化对象,此对象是static的,在内存中保留着它的引用,即内存中有一块区域专门用来存放静态方法和变量,可以直接使用,调用多次返回同一个对象。
3.3 实现Singleton 模式 7种方式
3.3.1饿汉,常用
首先是写明私有的构造方法防止被new,然后直接就实例化,最后调用,不存在线程安全问题。
/** * 单例模式,饿汉式,线程安全 */
public static class Singleton1{
private final static Singleton instance = new Singleton();
private Singleton1(){
}
public static Singleton getInstance(){
return instance;
}
3.3.2懒汉,不安全
可能会出现线程安全问题,因为new一个对象在JVM底层做了如下工作:
1 给 instance 分配内存
2 调用 Singleton 的构造函数来初始化成员变量
3 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
显然是不能保证原子性的。
单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断instance是否为null的 if语句,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时类型Singletonl就不再满足单例模式的要求了。为了保证在多线程环境下我们还只能得到类型的一个实例,需要加上一个同步锁。把 Singletonl稍作修改。
3.3.3加锁的懒汉,性能低
我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,第二个线程只能等待。当第一个线程发现实例还没有创建时,它创建出一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例己经被第一个线程创建出来了,第二个线程就不会重复创
建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。但是类型Singleton2 还 是 很 完 美 。我们每次通过属性Instance得到Singleton2 的实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
/** * 单例模式,懒汉式,线程安全,多线程环境下效率不高 */
public static class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {
}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
3.3.4 静态块,可以
当第一次引用getInstance()方法的时候,访问静态内部类中的静态成员变量,此时该内部类需要调用static代码块(因为首次访问该类)。而后再次访问getInstance()方***直接返回instace引用。这种做法相对于传统做法更加巧妙。
/** * 单例模式,懒汉式,变种,线程安全 */
public static class Singleton4 {
private static Singleton4 instance = null;
static {
instance = new Singleton4();
}
private Singleton4() {
}
public static Singleton4 getInstance() {
return instance;
}
}
3.3.5静态内部类 推荐
定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
优点:达到了lazy loading的效果,即按需创建实例。
/** * 单例模式,使用静态内部类,线程安全【推荐】 */
public static class Singleton5 {
private final static class SingletonHolder {
private static final Singleton5 INSTANCE = new Singleton5();
}
private Singleton5() {
}
public static Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
}
虚拟机会保证一个类的类构造器()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
3.3.6枚举,推荐
/** * 静态内部类,使用枚举方式,线程安全【推荐】 */
public enum Singleton06{
INSTANCE;
}
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
1 枚举类实现其实省略了private类型的构造函数
2枚举类的域(field)其实是相应的enum类型的一个实例对象
对于第一点实际上enum内部是如下代码:
public enum Singleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {
}
}
可以通过Singleton .INSTANCE来访问。
比较清楚的写法是:(底层解析下来)
public class SingletonExample5 {
private SingletonExample5(){
}
public static SingletonExample5 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample5 singleton;
//JVM保证这个方法绝对只调用一次
Singleton(){
singleton = new SingletonExample5();
}
public SingletonExample5 getInstance(){
return singleton;
}
}
}
为什么能保证线程安全
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。
还有一个重要的就是所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
3.3.7双重校验锁 推荐
思想:先判断一下是不是null,然后加锁,再判断一下是否为null。如果还是null,则可以放心地new。
还是不大明白为什么要判断两次null?
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
/** * 静态内部类,使用双重校验锁,线程安全【推荐】 */
public static class Singleton7 {
private volatile static Singleton7 instance = null;
private Singleton7() {
}
public static Singleton7 getInstance() {
if (instance == null) {
synchronized (Singleton7.class) {
if (instance == null) {
instance = new Singleton7();
}
}
}
return instance;
}
}
执行过程
双重校验锁方式的执行过程如下:
1.线程A进入 getInstance() 方法。
2.由于 singleton为 null,线程A在 //1 处进入 synchronized 块。
3.线程A被线程B预占。
4.线程B进入 getInstance() 方法。
5.由于 singleton仍旧为 null,线程B试图获取 //1 处的锁。然而,由于线程A已经持有该锁,线程B在 //1 处阻塞。
6.线程B被线程A预占。
7.线程A执行,由于在 //2 处实例仍旧为 null,线程A还创建一个 Singleton 对象并将其引用赋值给 instance。
8.线程A退出 synchronized 块并从 getInstance() 方法返回实例。
9.线程A被线程B预占。
10.线程B获取 //1 处的锁并检查 instance 是否为 null。
- 由于 singleton是非 null 的,并没有创建第二个 Singleton 对象,由线程A所创建的对象被返回。
3.3.8主函数
public static void main(String[] args) {
System.out.println(Singleton.getInstance() == Singleton.getInstance());
System.out.println(Singleton2.getInstance() == Singleton2.getInstance());
System.out.println(Singleton3.getInstance() == Singleton3.getInstance());
System.out.println(Singleton4.getInstance() == Singleton4.getInstance());
System.out.println(Singleton5.getInstance() == Singleton5.getInstance());
System.out.println(Singleton6.INSTANCE == Singleton6.INSTANCE);
System.out.println(Singleton7.getInstance() == Singleton7.getInstance());
}
4.总结
对于单例模式的描述就到这里,希望大家多实践,真正的了解每一种单例模式,并且能手写出其中的几种,因为这个是面试中必考的题目。并且在项目中也是经常使用的,所以大家加油啊!!!
5.个人推广
博客地址
https://blog.csdn.net/weixin_41563161
掘金https://juejin.cn/user/2814360172369271
知乎https://www.zhihu.com/people/hai-kuo-tian-kong-63-38-21