首页 > 技术文章 > 单例模式和多线程

xiazhenbin 2020-11-04 22:02 原文

$Java$—单例设计模式(懒汉与饿汉)、

单例设计模式:简单地讲,就是保证一个类在内存中只能有一个对象

1、饿汉模式——立即加载

饿汉模式就是在使用类的时候已经将对象创建完毕,从中文的语境上看,有“着急”的含义,所以称为饿汉模式。

我们看一下饿汉模式是如何保证单例设计的,思路如下:

  1. 不能让其他程序用new创建该类的对象。否则就无法控制对象的个数,结果可能不止一个;
  2. 既然其他程序不能new该类对象,那么类要在自己内部创建一个对象,否则该类就永远无法创建对象了;
  3. 该类将创建的对象对外提供,让其他程序获取并使用。

饿汉式的代码:

class Single{
	//私有化构造方法,不能让其他程序new创建该类的对象
	private Single() {
		
	}
	//类在内部自己创建一个对象
	private static final Single s = new Single();
	//创建一个方法,让外部程序使用该类对象(创建对象的引用)
	/*此版本代码的缺点是不能有其他实例变量
	 *因为getInstance()方法没有同步
	 *所以有可能出现非线程安全的问题
	 */
	public static Single getInstance() {
		return s;
	}
} 

例子:

package JavaSingleton;
/*单例设计模式——饿汉模式*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start();
        t3.start();
    }
}


class Single{
    //私有化构造方法,不能让其他程序new创建该类的对象
    private Single() {
        
    }
    //类在内部自己创建一个对象
    private static final Single s = new Single();
    public static Single getInstance() {
        return s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:996721300
单例模式中对象的哈希码:996721300
单例模式中对象的哈希码:996721300

对象是同一个,也就是实现了饿汉式的单例设计模式。

 

2、懒汉模式——延迟加载

延迟加载就是在调用getInstance()方法时,实例对象才被创建,常见的实现方法是在getInstance()方法中进行new实例化。延迟加载,有“缓慢、不急迫”的含义,所以也称为“懒汉模式”。

package JavaSingleton;
/*单例设计模式——饿汉模式*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start();
        t3.start();
    }
}


class Single{
    private static Single s = null; 
    
    private Single() {}
    
    
    public static Single getInstance() {
        try {
            if(s == null) {
                Thread.sleep(5000);
                s = new Single();
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:470374073
单例模式中对象的哈希码:1893883033
单例模式中对象的哈希码:1354233829

打印了3中hashcode,说明创建出了3个对象,所以并不是单例设计模式。出现这个现象的原因是多个线程可以同时进入getInstance()方法,他们的执行是异步的。

 

关于饿汉式和懒汉式创建单例模式的区别:

饿汉式为自己被加载时就将自己实例化;懒汉式为只有在第一次被引用时,才会将自己实例化。

饿汉式,即静态初始化的方式,类一加载就实例化对象,所以要提前占用系统资源;而懒汉式又会面临多线程访问的

安全性问题,需要双重锁定才可以保证安全。

懒汉式和饿汉式具体使用哪个,取决于实际需求。

 

改进一:对$getInstance()$方法加锁

package JavaSingleton;
/*单例设计模式——饿汉模式*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start();
        t3.start();
    }
}


class Single{
    private static Single s = null; 
    
    private Single() {}
    
    
    synchronized public static Single getInstance() {
        try {
            if(s == null) {
                Thread.sleep(5000);
                s = new Single();
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:2116402421
单例模式中对象的哈希码:2116402421
单例模式中对象的哈希码:2116402421

通过给getInstance()方法加上synchronized同步锁,打印结果来看,可以使得多线程同步,最后得到相同的实例对象。但是synchronized给对象上锁,导致运行的效率很低。下一个线程必须等上一个线程释放锁后,才能继续持有锁。

 

改进二、使用同步代码块

第一种同步代码块

package JavaSingleton;
/*单例设计模式——饿汉模式*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start();
        t3.start();
    }
}


class Single{
    private static Single s = null; 
    
    private Single() {}
    
    
    public static Single getInstance() {
        try {
            synchronized(Single.class) {
                if(s == null) {
                    Thread.sleep(5000);
                    s = new Single();
                }                
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:1354233829
单例模式中对象的哈希码:1354233829
单例模式中对象的哈希码:1354233829

此同步代码块的方法和synchronized同步方法一样,效率也很低。 

 

第二种、同步代码块

package JavaSingleton;
/*单例设计模式——懒汉模式*/
/*Double-Checked Locking*/

public class t1 {
	public static void main(String[] args) {
		MyThread t1 = new MyThread();
		MyThread t2 = new MyThread();
		MyThread t3 = new MyThread();
		
		t1.start();
		t2.start(); 
		t3.start();
	}
}


class Single{
	private static Single s = null; 
	
	private Single() {}
	
	
	public static Single getInstance() {
		try {
			if(s == null) {
				Thread.sleep(5000);
				synchronized (Single.class) {
					s = new Single();
				}
			}				
		}catch (InterruptedException e) {
			e.printStackTrace();
		}
		return s;
	}
} 

class MyThread extends Thread{
	@Override
	public void run() {
		super.run();
		System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
	}
}
单例模式中对象的哈希码:1137552715
单例模式中对象的哈希码:900676177
单例模式中对象的哈希码:2048538601

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。出发点是好的,但是从打印结果来看,出现了线程不安全的现象。这是因为,一个线程进入了if(s == null)判断语句块,别的线程也通过了这个判断语句。虽然创建对象的语句被上了锁,但是这些线程会依次排队,new创建一个对象。 

 

第三种、使用DCL双检查锁机制

package JavaSingleton;
/*单例设计模式——懒汉模式*/
/*Double-Checked Locking*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start(); 
        t3.start();
    }
}


class Single{
    private static Single s = null; 
    
    private Single() {}
    
    
    public static Single getInstance() {
        try {
            if(s == null) {
                Thread.sleep(5000);
                synchronized (Single.class) {
                    if(s == null) {
                        s = new Single();
                    }                
                }
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:741397220
单例模式中对象的哈希码:741397220
单例模式中对象的哈希码:741397220

再对上面那个例子进行改进,我们可以使用DCL双检查锁机制来实现多线程环境中的懒汉模式的单例设计。“懒汉模式”遇到多线程,DCL双检查锁机制在懒汉多线程设计中应用广泛。

 

3、使用静态内置类实现多线程单例模式

package JavaSingleton;
/*单例设计模式——懒汉模式*/
/*Double-Checked Locking*/

public class t1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        
        t1.start();
        t2.start(); 
        t3.start();
    }
}

class Single{
    //使用静态内置类
    private static class SingleHander{
        private static Single s = new Single();
    }
    
    private Single() {}
        
    public static Single getInstance() {
        return SingleHander.s;
    }
} 

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("单例模式中对象的哈希码:" + Single.getInstance().hashCode());
    }
}
单例模式中对象的哈希码:2116402421
单例模式中对象的哈希码:2116402421
单例模式中对象的哈希码:2116402421

 那么为什么静态内部类实现的单例模式是线程安全的?先看一下什么是内部类

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化$s$。故内部类不占内存。只有当$getInstance()$方法第一次被调用时,使用s的时候,才会导致虚拟机加载内部类。这种方法能够保证线程安全,也能保证单例的唯一性。

类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>()方法,<clinit>() 方法只执行一次。

 

5、使用static代码块来实现单例模式

static静态代码块属于clinit方法,在类初始化的时候只能只用一次,故可以使用static代码块来实现单例模式。

package JavaSingleton;

public class t2 {
    public static void main(String[] args) {
        THREAD T1 = new THREAD();
        THREAD T2 = new THREAD();
        THREAD T3 = new THREAD();
    
        T1.start();
        T2.start();
        T3.start();
    }
}

class Myobject1 {
    private static Myobject1 instance = null;    //类对象
    
    private Myobject1() {}
    
    //使用静态代码块, 属于clinit方法,类初始化时只能执行一次
    static {
        instance = new Myobject1();
    }
    
    public static Myobject1 getInstance() {
        return instance;
    }
}

class THREAD extends Thread{
    @Override
    public void run() {
        super.run();
        for(int i=0; i<5; i++) {
            System.out.println("单例模式对象的哈希码: "Myobject1.getInstance().hashCode());
        }
    }
}
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715
单例模式对象的哈希码: 1137552715

  

5、序列化和反序列化的单例模式实现

在此之间,先简单介绍一下什么是序列化和反序列化。

java对象序列化:将对象的状态转化为字节流,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输,以后可以通过这些值再生成相同状态的对象。反序列化就是根据这些保存的信息重建对象的过程。

 

二、为什么要序列化和反序列化

当两个进程进行远程通信时,可以相互发送各种类型的数据(文本、图片、音频、视频等),这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。 

 

三、涉及到的API

java.io.ObjectOutputStream表示对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream表示对象输入流,它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。

只有实现了SerializableExternalizable接口的类的对象才能被序列化,否则抛出异常。 

 

四、序列化和反序列化的步骤

序列化:

 步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(“目标地址路径”))

  步骤二:通过对象输出流的writeObject()方法写对象:

out.writeObject("Hello");
out.writeObject(new Date());

  

反序列化:        

步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:      

ObjectInputStream in = new ObjectInputStream(new fileInputStream(“目标地址路径”));

 步骤二:通过对象输出流的readObject()方法读取对象:

String obj1 = (String)in.readObject();
Date obj2 =  (Date)in.readObject();

说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。

package JavaSingleton;
/*序列化:将java对象转化为字节序列的过程
 *反序列化: 将字节序列转化为java对象的过程*/

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamException;
import java.io.Serializable;

public class t1 {
    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();
        }
    }
}

//序列化和反序列化
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;
    }
    //一定要加上readResolve方法,不然创建结果不是单例的
    protected Object readResolve() throws ObjectStreamException{
        System.out.println("调用了readResolve方法!");
        return MyObjectHandler.myobject;
    }
    
}

 

单例对象的哈希码(序列化前):865113938
调用了readResolve方法!
单例对象的哈希码(序列化后):865113938

  

推荐阅读