单例设计模式

x33g5p2x  于2021-11-15 转载在 其他  
字(2.2k)|赞(0)|评价(0)|浏览(342)

使用场景

单例模式的作用是 保证一个类在JVM中只能存在一个对象,防止多余的对象浪费JVM的内存

创建方式

1、饿汉式

  1. public class Singleton{
  2. private static Singleton singleton = new Singleton();
  3. //必须设置为private,防止通过构造函数来创建对象
  4. private Singleton(){
  5. }
  6. public static Singleton getInstance(){
  7. return singleton;
  8. }
  9. }

饿汉式是直接在静态代码块中直接生成,当类一被加载后,就会生成对应的对象。

2、懒汉式

  1. public class Singleton{
  2. private static Singleton singleton = null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if(singleton == null){
  7. singleton = new Singleton();
  8. }
  9. return singleton;
  10. }
  11. }

上述的代码在多线程的情况下,会出现错误,当两个线程同时进入singleton = null 的时候,会同时创建两个Singleton对象,返回两个不同的实例。

最简单粗暴的方式就是加锁

  1. public class Singleton{
  2. private static Singleton singleton = null;
  3. private Singleton(){
  4. }
  5. public static synchronized Singleton getInstance(){
  6. if(singleton == null){
  7. singleton = new Singleton();
  8. }
  9. return singleton;
  10. }
  11. }

上述的代码可以达到线程安全,但是性能太低,因为当singleton不为null的时候,获取也需要加锁

  1. //错误的例子
  2. public class Singleton{
  3. private static Singleton singleton = null;
  4. private Singleton(){
  5. }
  6. public static Singleton getInstance(){
  7. if(singleton == null){
  8. synchronized (Singleton.class) {
  9. singleton = new Singleton();
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

上述的代码是错误的,因为当两个线程同时抢锁来创建实例的时候,第一个线程创建完后,第二个在进入临界区的时候,又会重新创建一次实例,导致实例被创建了两次,返回了两个不同的实例。

所以,要在临界区代码中加入一个双重判空。

  1. public class Singleton{
  2. private static Singleton singleton = null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if(singleton == null){
  7. synchronized (Singleton.class) {
  8. if(singleton == null){
  9. singleton = new Singleton();
  10. }
  11. }
  12. }
  13. return singleton;
  14. }
  15. }

但是上述的代码还是会存在问题,因为JVM会对singleton = new Singleton()进行指令优化。

singleton = new Singleton();这个指令并不是原子操作,JVM会对其进行指令重排

singleton = new Singleton(); 正常的执行流程为

1、 allocate memory //为对象分配内存
2、 initiate(memory) //初始化对象
3、singleton = memory //将对象赋予给引用

指令重排后的执行流程
1、 allocate memory //为对象分配内存
2、 singleton = memory //将未初始化的对象赋予给引用
3、initiate(memory) //初始化对象

并且当执行到第2步的时候,也就是将未初始化的对象赋值给引用后,如果这时其他线程调用了getInstance(),此时singleton != null,所以会得到一个未初始化的对象,当线程调用未初始化的对象的时候,就会出错。

  1. public class Singleton{
  2. private volatile static Singleton singleton = null;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if(singleton == null){
  7. synchronized (Singleton.class) {
  8. if(singleton == null){
  9. singleton = new Singleton();
  10. }
  11. }
  12. }
  13. return singleton;
  14. }
  15. }

用volatile修饰 singleton,JVM不会对volatile修饰的对象进行指令重排。

3、静态内部类

  1. public class Singleton{
  2. private Singleton(){}
  3. private static class SingletonFactory{
  4. private static Singleton singleton = new Singleton();
  5. }
  6. private static Singleton getInstance(){
  7. return SingletonFactory.singleton;
  8. }
  9. }

相关文章