单例模式顾名思义就是只含有一个实例,一个实例复用,达到减少创建对象的开销以及大大节省资源的效果,是java23种设计模式最简单应用最多的设计模式;单例模式的实现方法主要分为饿汉式和懒汉式两大类,
饿汉式
1
2
3
4
5
6
7
8public class SimpleInstance { public static SimpleInstance simpleInstance = new SimpleInstance(); private SimpleInstance() {} public static SimpleInstance getInstance() { return simpleInstance; } }
浅析:
类装载时就已经创建了实例,是天然的线程安全写法,但是因为不管是否使用都创建了实例,可能造成资源的浪费
在springboot架构下,所有的被注册为Bean的类默认都是单例模式,并且默认使用饿汉式,即在springboot项目运行时,所有的Bean就被实例化了
懒汉式
1
2
3
4
5
6
7
8
9
10
11public class SimpleInstance { public static SimpleInstance simpleInstance; private SimpleInstance() {} public static SimpleInstance getInstance() { if (simpleInstance == null) { simpleInstance = new SimpleInstance(); } return simpleInstance; } }
浅析:
延迟加载,弥补了饿汉式浪费资源的缺点,但是只适合在单线程下使用,多线程下在if (simpleInstance == null)判断中,还未来得及往下执行,另一个线程也通过了这个判断语句,导致创建了多个实例,因此多线程下这种方式不可用
懒汉式(synchronized同步方法)
1
2
3
4
5
6
7
8
9
10
11public class SimpleInstance { public static SimpleInstance simpleInstance; private SimpleInstance() {} public static synchronized SimpleInstance getInstance() { if (simpleInstance == null) { simpleInstance = new SimpleInstance(); } return simpleInstance; } }
浅析:
用synchronized 修饰获取实例的方法,解决上面那种方式的线程安全问题,但是synchronized 修饰方法太重量级了,在高并发下,在获取到锁的线程拿到实例前其他线程都会被阻塞,造成获取实例性能低下,因此不推荐使用
懒汉式(synchronized同步代码块)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class SimpleInstance { public static SimpleInstance simpleInstance; private SimpleInstance() {} public static SimpleInstance getInstance() { if (simpleInstance == null) { synchronized (SimpleInstance.class) { if (simpleInstance == null) { simpleInstance = new SimpleInstance(); } } } return simpleInstance; } }
浅析:
针对synchronized 同步方法方式的缺点,把synchronized 锁改为修饰代码块,并进行双重校验,第一个if 判断是在实例被创建后直接返回就不需要进入竞争同步锁的逻辑,这样解决了synchronized 同步方法方式高并发下性能低下的缺点;第二个if 判断是防止多个线程通过了第一个if 判断再等待获取同步锁再次创建实例,起到保证保证线程安全作用
这种方式看似好像已经完美解决上面几种方式的缺点,但是实际上还是有问题
问题在于new一个SimpleInstance对象的操作不是原子性,它分为以下几步:
- 给SimpleInstance对象分配内存空间
- 初始化SimpleInstance对象
- 将SimpleInstance对象指向其被分配的内存空间
由于JVM会优化指令的特性,这三条指令其实顺序不一定是按照上述顺序,JVM会对指令重排序,如果上述指令发生了重排序,顺序会变成以下这样
- 给SimpleInstance对象分配内存空间
- 将SimpleInstance对象指向其被分配的内存空间
- 初始化SimpleInstance对象
即第二步和第三步顺序调换,这样在多线程下,可能会发生这样的情况:线程A拿到synchronized同步锁去new一个SimpleInstance对象,执行到完第二步后,线程B抢占到cpu时间片,此时simpleInstance对象已经被分配了内存空间,但对象没有被初始化,而线程B执行第一个 if (simpleInstance == null)语句,由于simpleInstance对象已经被分配了内存空间,得到结果会是flase并返回一个null对象,这种结果显然是不允许的,因此这种方式不可用
懒汉式(synchronized同步代码块 + volatile关键字)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class SimpleInstance { public static volatile SimpleInstance simpleInstance; private SimpleInstance() {} public static SimpleInstance getInstance() { if (simpleInstance == null) { synchronized (SimpleInstance.class) { if (simpleInstance == null) { simpleInstance = new SimpleInstance(); } } } return simpleInstance; } }
浅析:
针对synchronized 同步代码块的缺点,可以利用volatile禁止指令重排序的特性,用volatile关键字修饰实例变量,这样在new一个SimpleInstance对象操作永远是这样的步骤:
- 给SimpleInstance对象分配内存空间
- 初始化SimpleInstance对象
- 将SimpleInstance对象指向其被分配的内存空间
这样就不会存在返回一个null对象的现象了,推荐使用这种方式
懒汉式(静态代码块)
1
2
3
4
5
6
7
8
9
10
11public class SimpleInstance { public static SimpleInstance simpleInstance; static { simpleInstance = new SimpleInstance(); } private SimpleInstance() {} public static SimpleInstance getInstance() { return simpleInstance; } }
浅析:
这种方式和饿汉式很类似,不同的是这种方式创建实例对象放在静态代码块里,静态代码块是JVM在类装载时不会立即执行,只有在类初始化的时候才会有且只执行一次,利用JVM这一特性保证了线程安全同时也实现了延迟加载,这种方式性能比synchronized同步代码块 + volatile关键字方式更好,因此推荐使用
懒汉式(枚举)
1
2
3
4
5
6public enum SimpleInstance { INSTANCE; private SimpleInstance() {} }
浅析:
枚举时JDK1.5特性,它是天然的线程安全以及延迟加载,上面的几种方法都存在一个安全问题,那就是可以通过反射创建多个实例,而枚举是无法通过反射创建实例,完美解决这一问题,并且枚举获取实例的性能是最优的,因此,枚举方式可以说是最完美的单例模式的实现方式,推荐使用
最后
以上就是帅气帅哥最近收集整理的关于浅析java设计模式之单例模式的全部内容,更多相关浅析java设计模式之单例模式内容请搜索靠谱客的其他文章。
发表评论 取消回复