你的位置:首页 > Java教程

[Java教程]java多线程安全的单例模式


概念:
  java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
  单例模式有一下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

这里主要详细介绍两种:懒汉式和饿汉式

一、立即加载/饿汉式

在调用方法前,实例就已经被创建,代码:

 1 package com.weishiyao.learn.day8.singleton.ep1; 2  3 public class MyObject { 4   // 立即加载方式==恶汉模式 5   private static MyObject myObject = new MyObject(); 6  7   private MyObject() { 8   } 9   10   public static MyObject getInstance() {11     // 此代码版本为立即加载12     // 此版本代码的缺点是不能有其他实例变量13     // 因为getInstance()方法没有同步14     // 所以有可能出现非线程安全的问题15     return myObject;16   }17 }

 

创建线程类

1 package com.weishiyao.learn.day8.singleton.ep1;2 3 public class MyThread extends Thread {4   @Override5   public void run() {6     System.out.println(MyObject.getInstance().hashCode());7   }8 }

创建运行类

 1 package com.weishiyao.learn.day8.singleton.ep1; 2  3 public class Run { 4   public static void main(String[] args) { 5     MyThread t1 = new MyThread(); 6     MyThread t2 = new MyThread(); 7     MyThread t3 = new MyThread(); 8     t1.start(); 9     t2.start();10     t3.start();11   }12 }

运行结果

1 1677728952 1677728953 167772895

hashCode是同一个值,说明对象也是同一个,说明实现了立即加载型的单利模式

二、延迟加载/懒汉式

在调用方法以后实例才会被创建,实现方案可以是将实例化放到无参构造函数当中,这样只有当调用的时候才会创建对象的实例,代码:

 1 package com.weishiyao.learn.day8.singleton.ep2; 2  3 public class MyObject { 4   private static MyObject myObject; 5    6   private MyObject() { 7      8   } 9   10   public static MyObject getInstance() {11     // 延迟加载12     if (myObject != null) {13       14     } else {15       myObject = new MyObject();16     }17     return myObject;18   }19 }

创建线程类

1 package com.weishiyao.learn.day8.singleton.ep2;2 3 public class MyThread extends Thread {4   @Override5   public void run() {6     System.out.println(MyObject.getInstance().hashCode());7   }8 }

创建运行类

package com.weishiyao.learn.day8.singleton.ep2;public class Run {  public static void main(String[] args) {    MyThread t1 = new MyThread();    t1.start();  }}

运行结果

1 167772895

这样虽然取出了一个对象的实例,但是如果在多线程的环境中,就会出现多个实例的情况,这样就不是单例模式了

运行测试类

 1 package com.weishiyao.learn.day8.singleton.ep2; 2  3 public class Run { 4   public static void main(String[] args) { 5     MyThread t1 = new MyThread(); 6     MyThread t2 = new MyThread(); 7     MyThread t3 = new MyThread(); 8     MyThread t4 = new MyThread(); 9     MyThread t5 = new MyThread();10     t1.start();11     t2.start();12     t3.start();13     t4.start();14     t5.start();15   }16 }

运行结果

1 9802581632 12247170573 18518894044 1888205045 1672864109

既然出现问题,就要解决问题,在懒汉模式中的多线程的解决方案,代码:

第一种方案,最常见的,加synchronized,而synchronized可以加到不同的位置

第一种,方法锁

 1 package com.weishiyao.learn.day8.singleton.ep3; 2  3 public class MyObject { 4   private static MyObject myObject; 5    6   private MyObject() { 7      8   } 9   10   synchronized public static MyObject getInstance() {11     // 延迟加载12     try {13       if (myObject != null) {14         15       } else {16         // 模拟在创建对象之前做一些准备性的工作17         Thread.sleep(2000);
19 myObject = new MyObject();
21 }22 23 } catch (InterruptedException e) {24 e.printStackTrace();25 }26 return myObject;27 }28 }

这种synchronized的同步方案导致效率过于低下,整个方法都被锁住

第二种synchronized使用方案

 1 package com.weishiyao.learn.day8.singleton.ep3; 2  3 public class MyObject { 4   private static MyObject myObject; 5    6   private MyObject() { 7      8   } 9   10   public static MyObject getInstance() {11     // 延迟加载12     try {13       synchronized (MyObject.class) {14         if (myObject != null) {15           16         } else {17           // 模拟在创建对象之前做一些准备性的工作18           Thread.sleep(2000);
20 myObject = new MyObject();
22 }23 }24 25 26 } catch (InterruptedException e) {27 e.printStackTrace();28 }29 return myObject;30 }31 }

这种方法效率一样很低,方法内的所有代码都被锁住,只需要锁住关键代码就好,第三种synchronized使用方案

 1 package com.weishiyao.learn.day8.singleton.ep3; 2  3 public class MyObject { 4   private static MyObject myObject; 5    6   private MyObject() { 7      8   } 9   10   public static MyObject getInstance() {11     // 延迟加载12     try {13         if (myObject != null) {14           15         } else {16           // 模拟在创建对象之前做一些准备性的工作17           Thread.sleep(2000);18           synchronized (MyObject.class) {19             myObject = new MyObject();20           }21       }22       23       24     } catch (InterruptedException e) {25       e.printStackTrace();26     }27     return myObject;28   }29 }

这么写看似是最优方案了,但是,运行一下结果,发现,其实它是非线程安全的

结果:

1 12247170572 9711734393 18518894044 12247170575 1672864109

Why?

虽然锁住了对象创建的语句,每次只能有一个线程完成创建,但是,当第一个线程进来创建完成Object对象以后,第二个线程进来还是可以继续创建的,因为我们紧紧只锁住了创建语句,这个问题解决方案

 1 package com.weishiyao.learn.day8.singleton.ep3; 2  3 public class MyObject { 4   private static MyObject myObject; 5    6   private MyObject() { 7      8   } 9   10   public static MyObject getInstance() {11     // 延迟加载12     try {13         if (myObject != null) {14           15         } else {16           // 模拟在创建对象之前做一些准备性的工作17           Thread.sleep(2000);18           synchronized (MyObject.class) {19             if (myObject == null) {20               myObject = new MyObject();21             }22           }23       }24       25       26     } catch (InterruptedException e) {27       e.printStackTrace();28     }29     return myObject;30   }31 }

只需要在锁里面再添加一个判断,就可以保证单例了,这个是DCL双检查机制

结果如下:

1 12247170572 12247170573 12247170574 12247170575 1224717057

 三、使用内置静态类实现单例

主要代码

 1 package com.weishiyao.learn.day8.singleton.ep4; 2  3 public class MyObject { 4   // 内部类方式 5   private static class MyObjectHandler { 6     private static MyObject myObject = new MyObject(); 7   } 8  9   public MyObject() {10   }11   12   public static MyObject getInstance() {13     return MyObjectHandler.myObject;14   }15 }

线程类代码

1 package com.weishiyao.learn.day8.singleton.ep4;2 3 public class MyThread extends Thread {4   @Override5   public void run() {6     System.out.println(MyObject.getInstance().hashCode());7   }8 }

运行类

 1 package com.weishiyao.learn.day8.singleton.ep4; 2  3 public class Run { 4   public static void main(String[] args) { 5     MyThread t1 = new MyThread(); 6     MyThread t2 = new MyThread(); 7     MyThread t3 = new MyThread(); 8     MyThread t4 = new MyThread(); 9     MyThread t5 = new MyThread();10     t1.start();11     t2.start();12     t3.start();13     t4.start();14     t5.start();15   }16 }

结果

18518894041851889404185188940418518894041851889404

通过内部静态类,得到了线程安全的单例模式

四、序列化和反序列化单例模式

内置静态类可以达到线程安全的问题,但如果遇到序列化对象时,使用默认方式得到的结果还是多例的

MyObject代码

package com.weishiyao.learn.day8.singleton.ep5;import java.io.Serializable;public class MyObject implements Serializable {    /**   *   */  private static final long serialVersionUID = 888L;  // 内部类方式  private static class MyObjectHandler {    private static MyObject myObject = new MyObject();  }  public MyObject() {  }    public static MyObject getInstance() {    return MyObjectHandler.myObject;  }  //  protected MyObject readResolve() {//    System.out.println("调用了readResolve方法!");//    return MyObjectHandler.myObject;//  }}

业务类

 1 package com.weishiyao.learn.day8.singleton.ep5; 2  3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.FileNotFoundException; 6 import java.io.FileOutputStream; 7 import java.io.IOException; 8 import java.io.ObjectInputStream; 9 import java.io.ObjectOutputStream;10 11 public class SaveAndRead {12   public static void main(String[] args) {13     try {14       MyObject myObject = MyObject.getInstance();15       FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));16       ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);17       oosRef.writeObject(myObject);18       oosRef.close();19       fosRef.close();20       System.out.println(myObject.hashCode());21     } catch (FileNotFoundException e) {22       e.printStackTrace();23     } catch (IOException e) {24       e.printStackTrace();25     }26     FileInputStream fisRef;27     try {28       fisRef = new FileInputStream(new File("myObjectFile.txt"));29       ObjectInputStream iosRef = new ObjectInputStream(fisRef);30       MyObject myObject = (MyObject) iosRef.readObject();31       iosRef.close();32       fisRef.close();33       System.out.println(myObject.hashCode());34     } catch (FileNotFoundException e) {35       e.printStackTrace();36     } catch (IOException e) {37       e.printStackTrace();38     } catch (ClassNotFoundException e) {39       e.printStackTrace();40     }41     42     43   }44 }

结果

1 9709287252 1099149023

两个不同的hashCode,证明并不是同一个对象,解决方案,添加下面这段代码

1   protected MyObject readResolve() {2     System.out.println("调用了readResolve方法!");3     return MyObjectHandler.myObject;4   }

在反序列化的时候调用,可以得到同一个对象

1 System.out.println(myObject.readResolve().hashCode());

结果

1 12553013792 调用了readResolve方法!3 1255301379

相同的hashCode,证明得到了同一个对象

五、使用static代码块实现单例

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码快这个特性来实现单利模式

MyObject类

 1 package com.weishiyao.learn.day8.singleton.ep6; 2  3 public class MyObject { 4   private static MyObject instance = null; 5  6   private MyObject() { 7     super(); 8   } 9   10   static {11     instance = new MyObject();12   }13   14   public static MyObject getInstance() {15     return instance;16   }17 }

线程类

 1 package com.weishiyao.learn.day8.singleton.ep6; 2  3 public class MyThread extends Thread { 4   @Override 5   public void run() { 6     for (int i = 0; i < 5; i++) { 7       System.out.println(MyObject.getInstance().hashCode()); 8     } 9   }10 }

运行类

 1 package com.weishiyao.learn.day8.singleton.ep6; 2  3 public class Run { 4   public static void main(String[] args) { 5     MyThread t1 = new MyThread(); 6     MyThread t2 = new MyThread(); 7     MyThread t3 = new MyThread(); 8     MyThread t4 = new MyThread(); 9     MyThread t5 = new MyThread();10     t1.start();11     t2.start();12     t3.start();13     t4.start();14     t5.start();15   }16 }

运行结果:

 1 1678885403 2 1678885403 3 1678885403 4 1678885403 5 1678885403 6 1678885403 7 1678885403 8 1678885403 9 167888540310 167888540311 167888540312 167888540313 167888540314 167888540315 167888540316 167888540317 167888540318 167888540319 167888540320 167888540321 167888540322 167888540323 167888540324 167888540325 1678885403

通过静态代码块只执行一次的特性也成功的得到了线程安全的单例模式

六、使用enum枚举数据类型实现单例模式

枚举enum和静态代码块的特性类似,在使用枚举时,构造方法会被自动调用,也可以用来实现单例模式

MyObject类

 1 package com.weishiyao.learn.day8.singleton.ep7; 2  3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.SQLException; 6  7  8 public enum MyObject { 9   connectionFactory;10   11   private Connection connection;12   13   private MyObject() {14     try {15       System.out.println("调用了MyObject的构造");16       String url = "jdbc:mysql://172.16.221.19:3306/wechat_1?useUnicode=true&characterEncoding=UTF-8";17       String name = "root";18       String password = "111111";19       String driverName = "com.mysql.jdbc.Driver";20       Class.forName(driverName);21       connection = DriverManager.getConnection(url, name, password);22     } catch (ClassNotFoundException e) {23       e.printStackTrace();24     } catch (SQLException e) {25       e.printStackTrace();26     }27   }28   29   public Connection getConnection() {30     return connection;31   }32 }

线程类

 1 package com.weishiyao.learn.day8.singleton.ep7; 2  3 public class MyThread extends Thread { 4   @Override 5   public void run() { 6     for (int i = 0; i < 5; i++) { 7       System.out.println(MyObject.connectionFactory.getConnection().hashCode()); 8     } 9   }10 }

运行类

 1 package com.weishiyao.learn.day8.singleton.ep7; 2  3 public class Run { 4   public static void main(String[] args) { 5     MyThread t1 = new MyThread(); 6     MyThread t2 = new MyThread(); 7     MyThread t3 = new MyThread(); 8     MyThread t4 = new MyThread(); 9     MyThread t5 = new MyThread();10     t1.start();11     t2.start();12     t3.start();13     t4.start();14     t5.start();15   }16 }

运行结果

 1 调用了MyObject的构造 2 56823666 3 56823666 4 56823666 5 56823666 6 56823666 7 56823666 8 56823666 9 5682366610 5682366611 5682366612 5682366613 5682366614 5682366615 5682366616 5682366617 5682366618 5682366619 5682366620 5682366621 5682366622 5682366623 5682366624 5682366625 5682366626 56823666

上面这种写法将枚举类暴露了,违反了“职责单一原则”,可以使用一个类将枚举包裹起来

 1 package com.weishiyao.learn.day8.singleton.ep8; 2  3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.SQLException; 6  7  8 public class MyObject { 9   10   public enum MyEnumSingleton {11     connectionFactory;12     13     private Connection connection;14     15     private MyEnumSingleton() {16       try {17         System.out.println("调用了MyObject的构造");18         String url = "jdbc:mysql://172.16.221.19:3306/wechat_1?useUnicode=true&characterEncoding=UTF-8";19         String name = "root";20         String password = "111111";21         String driverName = "com.mysql.jdbc.Driver";22         Class.forName(driverName);23         connection = DriverManager.getConnection(url, name, password);24       } catch (ClassNotFoundException e) {25         e.printStackTrace();26       } catch (SQLException e) {27         e.printStackTrace();28       }29     }30     31     public Connection getConnection() {32       return connection;33     }34   }35   36   public static Connection getConnection() {37     return MyEnumSingleton.connectionFactory.getConnection();38   }39 }

更改线程代码

 1 package com.weishiyao.learn.day8.singleton.ep8; 2  3 public class MyThread extends Thread { 4   @Override 5   public void run() { 6     for (int i = 0; i < 5; i++) { 7       System.out.println(MyObject.getConnection().hashCode()); 8     } 9   }10 }

结果

 1 调用了MyObject的构造 2 1948356121 3 1948356121 4 1948356121 5 1948356121 6 1948356121 7 1948356121 8 1948356121 9 194835612110 194835612111 194835612112 194835612113 194835612114 194835612115 194835612116 194835612117 194835612118 194835612119 194835612120 194835612121 194835612122 194835612123 194835612124 194835612125 194835612126 1948356121

以上总结了单利模式与多线程结合时遇到的各种情况和解决方案,以供以后使用时查阅。