文章目录
- 一、 简介
- 二、详细介绍
- 1. 立即加载/饿汉模式
- 2. 延迟加载/懒汉模式
- 3. 使用静态内置类实现单例模式
- 4. 序列化和反序列化的单例模式
- 5. 使用static代码块实现单例模式
- 6. 使用enum枚举数据类型实现单例模式
一、 简介
在标准的23个设计模式中,单例模式在应用中是比较常用的。单多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成了在使用多线程的单例子模式时会出现一些意外。这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。
二、详细介绍
1. 立即加载/饿汉模式
什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化。立即加载从中文的语境来看,是“着急”“急迫”的含义。所以也被称为“饿汉模式”。如下面的代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class Myobject{
//立即加载(静态变量会在类初始化时进行初始化)
private static Myobject myobject=new Myobject();
private Myobject(){
}
public static Myobject getInstance(){
return myobject;
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Myobject.getInstance().hashCode());
}
}
由打印结果可以知道,hashcode是同一个值,说明对象是同一个对象,也就实现了立即加载型单例模式,现在我们分析一下它存在的问题,它的缺点就是不能有其它实例变量,如果我们在getInstance中加入其它实例变量,并赋值,由于代码没有被同步,所以容易出现非线程安全问题,导致变量值被覆盖,解决方法我们后续再谈。
2. 延迟加载/懒汉模式
延迟加载的定义与原理 延迟加载是开发过程中灵活获取对象的一种求值策略,该策略在定义目标对象时并不会立即计算实际对象值,而是在该对象后续被实际调用时才去求值,所以也被称为懒汉模式。更改上面代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class Myobject{
//立即加载(静态变量会在类初始化时进行初始化)
private static Myobject myobject;
private Myobject(){
}
public static Myobject getInstance(){
if(myobject !=null)
{}else
{
myobject=new Myobject();
}
return myobject;
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Myobject.getInstance().hashCode());
}
}
由结果可以知道,这种对象加载模式会在多线程中创建多个对象的实例的情况,与单例模式的初衷是背离的。那么如果解决该方案这种错误的单例模式的问题?下面给出了几种解决方案:
- 声明synchronized关键字
即然多个线程可以同时进入getInstance()方法,我们只需要对getInstance()方法声明synchrozied关键字即可
synchronized public static Myobject getInstance(){
try{
if(myobject !=null){
}else{
Thread.sleep(3000);
myobject=new Myobject();
}
}catch(InterruptedException e)
{
e.printStackTrace();
}
return myobject;
}
此方法在加入同步Synchronized
关键字后得到相同实例的对象,但整个方法被上锁后,运行效率会非常低,下一个线程想要取得对象,必须等上一个线程释放完锁后,才可以执行。那换成同步代码块可以快速解决吗(对运行的代码块加上锁)?答案是一样的,不能快速解决,原因是同步代码块依然是获得的是对象锁,所以效率依然很低下。下面尝试使用针对某些重要代码进行单独的同步来测试一下:
public static Myobject getInstance(){
try{
if(myobject!=null){
}else{
//模拟在创建对象之前做的一些准备工作
Thread.sleep(3000);
//使用synchronized(MyObject.class)
//但还是有非线程安全问题
//多次创建MyObject类的对象,结果并不是单例
synchronized (Myobject.class){
myobject=new Myobject();
}
}
}catch(InterruptedException e)
{
e.printStackTrace();
}
return myobject;
}
}
此方法使同步synchronized语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率的确得到了提升,但遇到多线程情况还是无法得到一个实例对象。到底如何解决懒汉模式下的多线程情况呢,下面介绍使用DCL双检查锁机制:
class Myobject{
//立即加载(静态变量会在类初始化时进行初始化)
private volatile static Myobject myobject;
private Myobject(){
}
public static Myobject getInstance(){
try{
if(myobject!=null){
}else{
//模拟在创建对象之前做的一些准备工作
Thread.sleep(3000);
synchronized (Myobject.class){
myobject=new Myobject();
}
}
}catch(InterruptedException e)
{
e.printStackTrace();
}
return myobject;
}
}
使用volatile修改变量myObject,使该变量在多个线程间可见,另外禁止myObject=new MyObject()代码重排序。myObject=new MyObject()代码包含3个步骤。
- memory=allocate() :分配对象的内存空间
- ctorInstance():初始化对象
- myObject():设置instance指向刚分配的内存地址
JIT编译器有可能将这三个步骤重排序成。
- memory=allocate():分配对象的内存空间
- myObject=memory:设置instance指向刚分配的内存地址
- ctorInstance(memory):初始化对象
这时,构造方法虽然还没有执行,但myObject对象已经具有内存地址,即值不是null。当访问myObject对象中的值是,是当前声明数据类型的默认值。运行结果可以发现,使用DCL双检查锁成功解决了懒汉模式下的多线程问题,DCL也是大多数线程结合单例模式使用的解决方案。
3. 使用静态内置类实现单例模式
DCL可以解决多线程单例模式的非现场安全问题。我们还可以使用其他方法达到同样的效果。
public class Myobject{
//内部类方法
private static class MyObjectHandler{
private static Myobject myobject=new Myobject();
}
private Myobject(){
}
public static Myobject getInstance(){
return MyObjectHandler.myobject;
}
}
原理是,静态内部类定义阶段就已经初始化了,在静态内部类定义我的Myobject时,由于它被static修饰,所以在初始化MyObjectHandler类时,它也被初始化了,且只有一个副本,所以访问时也会只存在一个实例,这样就实现了单例模式。
4. 序列化和反序列化的单例模式
如果将单例对象进行序列化,使用默认的反序列化行为取出的对象是多例的。
//定义一个UserInfo对象
public class UserInfo {
}
//定义Myobject类
public class Myobject implements Serializable{
private static final long SerializableUID=888L;
public static UserInfo userInfo=new UserInfo();
private static Myobject myobject=new Myobject();
private Myobject(){
}
publicstatic Myobject getInstance(){
return myobject;
}
}
//main函数
public class Main {
public static void main(String[] args) throws InterruptedException {
try{
Myobject myobject=Myobject.getInstance();
System.out.println("序列化-myobject="+myobject.hashCode()+"userinfo:"+myobject.userInfo.hashCode());
FileOutputStream fosref=new FileOutputStream(new File("myObject-File.txt"));
ObjectOutputStream oosRef=new ObjectOutputStream(fosref);
oosRef.writeObject(myobject);
oosRef.close();
fosref.close();
}catch(FileNotFoundException e)
{
e.printStackTrace();
}catch(IOException e)
{
e.printStackTrace();
}
try{
FileInputStream fisRef=new FileInputStream(new File("myObject-File.txt"));
ObjectInputStream iosRef=new ObjectInputStream(fisRef);
Myobject myobject=(Myobject)iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println("序列化-myObject="+myobject.hashCode()+"userinfo:"+myobject.userInfo.hashCode());
}catch(FileNotFoundException e)
{
e.printStackTrace();
}catch(IOException e)
{
e.printStackTrace();
}catch(ClassNotFoundException e)
{
e.printStackTrace();
}
}
}
从打印结果可以看出,在反序列化时创建了新的MyObject对象,但UserInfo对象得到复用,因为hashcode是同一个。为了实现Myobject对象在内存中一直呈现单例效果,我们可以在反序列化中使用下面方法,对原有的Myobject对象进行复用:
public class Myobject implements Serializable{
private static final long SerializableUID=888L;
public static UserInfo userInfo=new UserInfo();
private static Myobject myobject=new Myobject();
private Myobject(){
}
public static Myobject getInstance(){
return myobject;
}
protected Object readResolve()throws ObjectStreamException{
return Myobject.myobject;
}
}
上面加入的方法的作用是在反序列化时不创建新的Myobject对象,而是复用JVM内存中原有的Myobject单例对象,即UserInfo对象被复用,这就实现了对Myobject序列化和反序列化保持单例性。那么为什么序列化和反序列化会破坏单例模式呢?序列化和反序列化可以破坏单例模式,因为在反序列化过程中,JVM会创建一个新的对象,而不是使用单例类中的静态实例。当一个单例类的实例被序列化并再次反序列化时,会导致创建新的对象,从而破坏了单例的特性。这种情况发生的原因是,当对象被序列化时,它的状态信息被保存到字节流中,而在反序列化时,JVM会根据字节流重新创建对象。由于单例类的构造函数是私有的,无法直接调用,因此JVM无法使用单例类的构造函数来创建实例。相反,它会调用默认的反序列化机制来创建一个新的对象,并忽略掉单例类中的任何特殊实现。为了解决这个问题,需要在单例类中添加一个特殊的方法readResolve()。该方法在反序列化时被调用,并返回原始的单例实例。通过在readResolve()方法中返回单例实例,可以确保在反序列化时仍然使用同一个对象,从而保持了单例的特性。总之,为了在序列化和反序列化过程中保持单例的特性,必须在单例类中实现readResolve()方法,并确保返回单例实例。这样可以防止通过反序列化操作破坏单例模式。
5. 使用static代码块实现单例模式
静态代码块中的代码在类加载的时候就会执行,所以我们可以应用静态代码块这个特性实现单例模式。
public class Myobject{
private static Myobject instance=null;
private Myobject(){
}
static{
instance =new Myobject();
}
public static Myobject getInstance(){
return instance;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Myobject.getInstance().hashCode());
}
}
6. 使用enum枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似。枚举(Enum)数据类型在Java中可以用于实现单例模式,其原理是利用枚举类型的特性保证了单例的实现。在使用枚举实现单例模式时,只需要定义一个包含单个枚举常量的枚举类。这个枚举常量就是单例的实例,而枚举类本身就是单例类。枚举类中的枚举常量在加载枚举类时就会被实例化,并且枚举常量的实例是线程安全的。这意味着无论是多线程环境下还是通过序列化和反序列化,枚举单例都能保持单例的特性。
public class Myobject{
public Myobject(){
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}
enum Singleton {
INSTANCE;
private Myobject myobject;
private Singleton(){
myobject=new Myobject();
}
public Myobject getInstance(){
return myobject;
}
}