单例模式与多线程
1.饿汉模式
该模式指调用方法前,实例已经被创建了。
/** * @author MM * @create 2019-03-04 16:39 **/ public class MyObject { //立即加载模式 private static MyObject myObject = new MyObject(); public MyObject() { } public static MyObject getInstance(){ // return myObject; } }
/** * @author MM * @create 2019-03-04 16:42 **/ public class SingletonThread1 extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } public static void main(String[] args) { SingletonThread1 t1 = new SingletonThread1(); SingletonThread1 t2 = new SingletonThread1(); SingletonThread1 t3 = new SingletonThread1(); t1.start(); t2.start(); t3.start(); } }
该模式线程安全。
2. 懒汉模式(延迟加载)
所谓延迟加载就是在调用获取实例方法时实例才被创建,常见的实例办法就是在获取实例时进行new 对象。
/** * @author MM * @create 2019-03-04 16:39 **/ public class MyObject { //延迟加载模式 private static MyObject myObject; public MyObject() { } public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; } }
修改上面myObject代码,继续执行后结果,粗看结果是正确的,但稍微再次修改一下。
public class MyObject { //延迟加载模式 private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { Thread.sleep(1000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
可见这种写法存在线程安全问题。
解决方案:
1):synchronized同步方法
public static synchronized MyObject getInstance() { try { if (myObject == null) { Thread.sleep(1000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; }
这种方法虽然能获取正确的结果,但这种方法效率上稍微有些低下,因为整个方法同步,下一个线程要获得对象,需等待上一个线程释放锁后才可以继续执行。
2):同步代码块
a:如果直接将整个代码块同步其实效率和同步方法时一样的
public static MyObject getInstance() { synchronized (MyObject.class){ try { if (myObject == null) { Thread.sleep(1000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } } return myObject; }
b:针对某些重要代码进行单独同步
public static MyObject getInstance() { try { if (myObject == null) { Thread.sleep(1000); synchronized (MyObject.class) { myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; }
只对创建实例的代码加锁,结果还是不正确的。
3):使用双重检测锁
public static MyObject getInstance() { try { if (myObject == null) { Thread.sleep(1000); synchronized (MyObject.class) { if(myObject == null){ myObject = new MyObject();//关键部分 } } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; }
表明上能达到线程安全。实际写法还是不对。
因为在 new MyObject() 的过程中,并不是一个原子操作,是可以进一步拆分为:
1.分配内存空间
2.初始化对象
3.设置 instance 指向刚分配的内存
但在jvm内部中如果经过指令重排后的结果可能为,1,3,2 那么在多线程环境中可能存在第一判断实例存在,但实际还未初识化的情况。
要解决这种问题,可以用 volatile 关键字防止指令重排。
private volatile MyObject myObject;
4):静态内部类方法
public class MyObject { public MyObject() { } //静态内部类模式 public static class MyObjectInstance{ private static MyObject myObject = new MyObject(); } public static MyObject getInstance() { return MyObjectInstance.myObject; } }
静态内部类能解决线程安全问题,但如果是遇到序列化对象时,使用这种默认方式运行得到的结果还是多实例的。
public class MyObject implements Serializable{ private static final long serialVersionUID = -245041196348963545L; public MyObject() { } //静态内部类模式 public static class MyObjectInstance{ private static MyObject myObject = new MyObject(); } public static MyObject getInstance() { return MyObjectInstance.myObject; } }
public class SingletonSerializalbe { public static void main(String[] args) { MyObject myObject = MyObject.getInstance(); //序列化 try { FileOutputStream fos = new FileOutputStream(new File("D:\\test.txt")); ObjectOutputStream outputStream = new ObjectOutputStream(fos); outputStream.writeObject(myObject); outputStream.close(); fos.close(); System.out.println(myObject.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } //反序列化 try { FileInputStream fins = new FileInputStream(new File("D:\\test.txt")); ObjectInputStream inputStream = new ObjectInputStream(fins); MyObject myObject1 = (MyObject) inputStream.readObject(); inputStream.close(); fins.close(); System.out.println(myObject1.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
可见反序列化后的对象和原来的对象是不一致,解决需要在对象中添加readResolve()方法
public class MyObject implements Serializable{ private static final long serialVersionUID = -245041196348963545L; public MyObject() { } //静态内部类模式 public static class MyObjectInstance{ private static MyObject myObject = new MyObject(); } public static MyObject getInstance() { return MyObjectInstance.myObject; } protected Object readResolve(){ System.out.println("readResolve..."); return MyObjectInstance.myObject; } }
对于Serializable and Externalizable classes,方法readResolve允许class在反序列化返回对象前替换、解析在流中读出来的对象。实现readResolve方法,一个class可以直接控制反序化返回的类型和对象引用。
方法readResolve会在ObjectInputStream已经读取一个对象并在准备返回前调用。ObjectInputStream 会检查对象的class是否定义了readResolve方法。如果定义了,将由readResolve方法指定返回的对象。
5):使用static代码块实现单例模式
public class MyObject implements Serializable{ private static final long serialVersionUID = -245041196348963545L; public MyObject() { } private static MyObject instance = null; static { instance = new MyObject(); } public static MyObject getInstance() { return instance; } }
6):使用枚举类实现单例
public class MyObject {
public enum MyObjectEnum {
MY_OBJECT_ENUM;
private MyObject instance = null;
MyObjectEnum() {
this.instance = new MyObject();
}
public MyObject getInstance(){
return instance;
}
}
public static MyObject getInstance(){
return MyObjectEnum.MY_OBJECT_ENUM.getInstance();
}
}
public class SingletonThread1 extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } public static void main(String[] args) { SingletonThread1 t1 = new SingletonThread1(); SingletonThread1 t2 = new SingletonThread1(); SingletonThread1 t3 = new SingletonThread1(); t1.start(); t2.start(); t3.start(); } }
枚举和static块类似,天然能保证线程安全。初始化时构造函数先被执行,该方法由jvm保证同步,效率很高。