在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。
一、立即加载/“饿汉模式”
立即加载就是使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。立即加载/“饿汉模式”是在调用方法前,实例已经被创建了。
示例:从输出结果可以看出,虽然3个线程同时执行了同一个对象的同一个方法,但是输出的hashCode值是一样的,说明对象是同一个,因为private static MyObject myObject = new MyObject();所以也就实现了立即加载型单例设计模式。
package test; public class MyObject { //立即加载 == 懒汉模式 private static MyObject myObject = new MyObject(); private MyObject() { } public static MyObject getInstance() { //缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。 return myObject; } }
package extthread; import test.MyObject; public class MyThread extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }
package test.run; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }
180612070 180612070 180612070
二、延迟加载/“懒汉模式”
延迟加载就是在调用get()方法时实例才被创建,常见的办法就是在get()方法中进行new实例化。
(1)延迟加载/“懒汉模式”实现
示例:由于只有一个线程,所以从结果来看确实取得了一个对象的实例,但是在多线程环境中,就会取出多个实例的情况,这与单例模式的初衷是相违背的。
package test; public class MyObject { private static MyObject myObject; private MyObject() { } public static MyObject getInstance() { //延迟加载 if (myObject != null) { } else { myObject = new MyObject(); } return myObject; } }
package extthread; import test.MyObject; public class MyThread extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }
package test.run; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread t1 = new MyThread(); t1.start(); } }
51011016
(2)延迟加载/“懒汉模式”缺点
示例:验证多线程中延迟加载的错误性。从输出可以看出来,3个线程创建了3个不同的实例,这是因为3个线程没有实现同步,即一开始都判断没有实例,所以都创建了实例。
package test; public class MyObject { private static MyObject myObject; private MyObject() { } public static MyObject getInstance() { try { if (myObject != null) { } else { //模拟在创建对象前做一些准备工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
package extthread; import test.MyObject; public class MyThread extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }
package test.run; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }
379353320 1026810021 1839450825
(3)延迟加载/“懒汉模式”解决方案
①声明synchronized关键字
示例:修改getInstance()方法,加上synchronized关键字,既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可,这样就可以得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,就必须等待上一个线程释放锁之后,才可以继续执行。执行完需要花费9秒的时间。
package test; public class MyObject { private static MyObject myObject; private MyObject() { } synchronized public static MyObject getInstance() { try { if (myObject != null) { } else { //模拟在创建对象前做一些准备工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
379353320 379353320 379353320
②尝试同步代码块
示例:之前的加synchronized关键字是对整个方法上锁,这里虽然使用了同步代码块,但是几乎是整个方法里面的代码,虽然可以达到效果,但这对运行效率来说也是不利的。执行总时间也是9秒。
package test; public class MyObject { private static MyObject myObject; private MyObject() { } public static MyObject getInstance() { try { // 这种方法等同于 // synchronized public static MyObject getInstance() // 效率一样很低,全部代码被上锁。 synchronized (MyObject.class) { if (myObject != null) { } else { Thread.sleep(3000); myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
621856954 621856954 621856954
③针对某些重要的代码进行单独的同步
示例:同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升。虽然不对sleep(3000)上锁这个思路很对,但是还是无法解决问题啊,3个线程还是分别都创建了一个实例。
package test; public class MyObject { private static MyObject myObject; private MyObject() { } public static MyObject getInstance() { try { if (myObject != null) { } else { // 模拟在创建对象之前做一些准备性的工作 Thread.sleep(3000); //使用synchronized (MyObject.class) //虽然部分代码被上锁 //但是还是有非线程安全问题 synchronized (MyObject.class) { myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
180612070 621856954 686587801
④使用DCL双检查锁机制
示例:采用双重检查锁机制,成功解决了延迟加载遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。整个代码执行时间远少于9秒。
package test; public class MyObject { private volatile static MyObject myObject; private MyObject() { } //使用双监测机制来解决问题,既保证了不需要同步代码的异步执行性,有保证了单例的效果 public static MyObject getInstance() { try { if (myObject != null) { } else { //模拟在创建对象之前做的一些准备性的工作 Thread.sleep(3000); synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } //称为Double-Check Locking }
686587801 686587801 686587801
三、使用静态内置类实现单例模式
除了DCL外,其他的方法也能达到同样的效果。
示例:通过使用静态内之类的方式也可以实现3个线程只创建一个对象的需求。
package test; public class MyObject { //内部静态类方式 private static class MyObjectHandler { private static MyObject myObject = new MyObject(); } private MyObject() { } public static MyObject getInstance() { return MyObjectHandler.myObject; } }
180612070 180612070 180612070
四、序列化与反序列化的单例模式实现
静态内置类可以达到线程安全问题,但如果需要序列化对象时,使用默认的方式运行得到的结果还是多例的。
示例1:不使用readResolve()方法时,得到的不是同一个对象。
package test; import java.io.ObjectStreamException; import java.io.Serializable; public class MyObject implements Serializable { private static final long serialVersionUID = 888L; //静态内置类方式 private static class MyObjectHandler { private static final MyObject myObject = new MyObject(); } private MyObject() { } public static MyObject getInstance() { return MyObjectHandler.myObject; } // protected Object readResolve() throws ObjectStreamException { // System.out.println("调用了readResolve方法"); // return MyObjectHandler.myObject; // } }
package test.run; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import test.MyObject; public class SaveAndRead { public static void main(String[] args) { try { MyObject myObject = MyObject.getInstance(); FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt")); ObjectOutputStream oosRef = new ObjectOutputStream(fosRef); oosRef.writeObject(myObject); oosRef.close(); fosRef.close(); System.out.println(myObject.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } try { FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt")); ObjectInputStream iosRef = new ObjectInputStream(fisRef); MyObject myObject = (MyObject) iosRef.readObject(); iosRef.close(); fisRef.close(); System.out.println(myObject.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
1975012498 990368553
示例2:在反序列化中使用readResolve()方法时,就会得到同一个对象。
package test; import java.io.ObjectStreamException; import java.io.Serializable; public class MyObject implements Serializable { private static final long serialVersionUID = 888L; //静态内置类方式 private static class MyObjectHandler { private static final MyObject myObject = new MyObject(); } private MyObject() { } public static MyObject getInstance() { return MyObjectHandler.myObject; } protected Object readResolve() throws ObjectStreamException { System.out.println("调用了readResolve方法"); return MyObjectHandler.myObject; } }
1975012498 调用了readResolve方法 1975012498
五、使用static代码块实现单例模式
静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式。
示例:从输出结果也可以看出,使用静态代码块也可以解决问题。
package test; public class MyObject { private static MyObject instance = null; private MyObject() { } static { instance = new MyObject(); } public static MyObject getInstance() { return instance; } }
686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801
六、使用enum枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,也可以利用这个特性实现单例设计模式。
示例:利用public enum MyObject{}枚举类,可以实现单例设计模式。
package test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public enum MyObject { connectionFactory; private Connection connection; private MyObject() { try { System.out.println("调用了MyObject的构造器"); String url = "jdbc:sqlserver://localhost:1079;databaseName=ghydb"; String username = "sa"; String password = ""; String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; Class.forName(driverName); connection = DriverManager.getConnection(url, username, password); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public Connection getConnection() { return connection; } }
package extthread; import test.MyObject; public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(MyObject.connectionFactory.getConnection() .hashCode()); } } }
调用了MyObject的构造器
686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801 686587801
七、完善使用enum枚举实现单例模式
示例:六中的示例将枚举类进行曝露,违反了“职责单一原则”,修改MyObject类进行完善。
package test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class MyObject { public enum MyEnumSingleton { connectionFactory; private Connection connection; private MyEnumSingleton() { try { System.out.println("创建MyObject对象"); String url = "jdbc:sqlserver://localhost:1079;databaseName=y2"; String username = "sa"; String password = ""; String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; Class.forName(driverName); connection = DriverManager.getConnection(url, username, password); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public Connection getConnection() { return connection; } } public static Connection getConnection() { return MyEnumSingleton.connectionFactory.getConnection(); } }