单例模式
定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局都可以该实例的访问方法。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
- 避免对资源的多重占用
缺点:
- 无抽象层,扩展难
- 单例类职责过重,不符合单一职责的设计原则。即充当着工厂角色,提供了工厂方法。又充当了产品角色,包含了一些业务方法。将产品的创建和产品自身的功能融为一体。
适用:业务要求控制实例数目,节省系统资源的时候。此时判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
单例模式简单实例
说明:两次调用getInstance()获取的对象为统一对象,构造器为private外部无法实例化对象。
实现注意:
- 单例类的构造器为私有 Singleton1(){}
- 提供一个自身的静态私有成员变量 instance
- 提供一个公有的静态工厂方法 getInstance()
代码:
Singleton1
public class Singleton1 {
//多线程访问安全
private static Singleton1 instance=null;
//私有构造器保证了实例的唯一,外部无法进行实例化
private Singleton1(){}
//从而保证了每次返回的对象都是唯一的一个
public static Singleton1 getInstance()
{
if(instance==null)
{
instance=new Singleton1();
}
return instance;
}
}
public class Test1 {
public static void main(String[] args) {
Singleton1 s1=Singleton1.getInstance();
Singleton1 s2=Singleton1.getInstance();
//结果为true
System.out.println(s1==s2);
}
}
饿汉式单例类
类加载时立即被实例化,在线程还没有的时候就创建了单例对象,保证了线程的安全,不存在多线程访问的安全问题。
优点:没有加任何锁,执行效率高。
缺点:类加载的时候已经实例化,占空间,费内存。不管该实例用于不用都存在
举例说明:
饿汉Singleton1
public class Singleton1 {
//类加载过程中就有了实例对象 因此多线程访问安全
//2种实例均可 new Singleton1() 和 static
private static final Singleton1 singleton1;//=new Singleton1();
static
{
singleton1=new Singleton1();
}
private Singleton1(){}
public static Singleton1 getInstance()
{
return singleton1;
}
}
测试Client
public class Client extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+Singleton1.getInstance());
}
public static void main(String[] args) {
Thread t1=new Client();
t1.start();
Thread t2=new Client();
t2.start();
//结果
//Thread-0 com.baiwenxuan.Singleton.hungry.Singleton1@c10f5b9
//Thread-1 com.baiwenxuan.Singleton.hungry.Singleton1@c10f5b9
}
}
以上两个线程同时获取实例,所得到的运行结果成功说明了饿汉式单例类是线程安全的。
懒汉式单例类
类加载时不会实例化,只有被外部调用的时候,内部类才会加载。多线程情况下,必须对getInstance方法进行加锁操作。使得方法变成线程同步,一个处于Running,其他处于Monitor监控状态。
优点:用时创建,释放了不必要占据的空间
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
举例说明:
a.不加锁多线程实例
LazySingle
public class LazySingle {
private static LazySingle lazysingle=null;
private LazySingle(){}
public static LazySingle getInstance()
{
if(lazysingle==null)
{
lazysingle=new LazySingle();
}
return lazysingle;
}
}
测试
public class ExectorThread implements Runnable {
@Override
public void run() {
LazySingle lazySingle=LazySingle.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingle);
}
}
public class Test {
public static void main(String[] args) {
//不加锁的话 结果不一样
new Thread(new ExectorThread()).start();
new Thread(new ExectorThread()).start();
new Thread(new ExectorThread()).start();
new Thread(new ExectorThread()).start();
/*结果如下
*Thread-1 com.baiwenxuan.Singleton.lazy.LazySingle@2e36b8b3
Thread-0 com.baiwenxuan.Singleton.lazy.LazySingle@7aa3aa20
Thread-2 com.baiwenxuan.Singleton.lazy.LazySingle@7aa3aa20
Thread-3 com.baiwenxuan.Singleton.lazy.LazySingle@2e36b8b3
*/
}
}
该不加锁懒汉单例类,在多线程不能正常工作。
b.加锁多线程实例
LazySingle
public class LazySingle {
private static LazySingle lazysingle=null;
private LazySingle(){}
//加锁 synchronized
public synchronized static LazySingle getInstance()
{
if(lazysingle==null)
{
lazysingle=new LazySingle();
}
return lazysingle;
}
}
测试
public class Test {
public static void main(String[] args) {
//不加锁的话 结果不一样
for(int i=0;i<100;i++)
new Thread(new ExectorThread()).start();
//结果
//所有线程获得的对象实例一致
}
}
加锁可以有效使得多线程访问更加安全。
双重检查锁单例模式
这种方式采用volatile 、synchronized 双锁机制,安全且在多线程情况下能保持高性能。
synchronized 在创建实例时,进行加锁。因为如果锁住方法,虽然解决了对象重复创建的问题,但是在高并发下,频繁调用getInstance()方法都需要加锁,导致了性能的下降。
volatile 对变量使用volatile,可以保证各个线程对某变量的即时可见性。jvm中多线程的内存模型是每个线程有自己的工作缓存(即主存的一个副本),工作前从内存读取,工作完成后需要把工作缓存中更新过的值刷新到内存中。
//双重检查锁懒汉单例
public class LazySingle1 {
private volatile static LazySingle1 lazysingle1=null;
private LazySingle1(){}
public static LazySingle1 getInstance()
{
if(lazysingle1==null)
{
synchronized(LazySingle1.class)
{
if(lazysingle1==null)
{
lazysingle1=new LazySingle1();
}
}
}
return lazysingle1;
}
}
说明:
可能同一时间多个线程一起通过了第1次代码 if(lazysingle1==null) 的判断,但同一时间只有一个线程获得锁后进入临界区,进入临界区后还要再判断一次单例类是否已被其它线程实例化,以避免多次实例化。由于双重加锁实现仅在实例化单例类时需要加锁,多线程情况下能保持高性能。
静态内部类单例模式
这种方式同样利用了 classloader 机制来保证初始化 LIZY时只有一个线程,能达到双检锁一样的功效,但实现更简单。对静态域使用延迟初始化。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式是 HungrySingle2 类被装载了,LIZY不一定被初始化。因为 HungryHolder类没有被主动使用,只有通过显式调用 getInstance() 方法时,才会显式装载 HungryHolder类,从而实例化 LIZY。
public class HungrySingle2 {
/*静态内部类单例模式:这种方式能达到双检锁方式一样的功效,但实现更简单。解决了饿汉的内存浪费问题,
相当于synchronized,提升了性能。内部类是在线程访问前初始化,避免了线程不安全的问题。*/
private HungrySingle2(){}
public static HungrySingle2 getInstance()
{
return HungryHolder.LIZY;
}
private static class HungryHolder
{
private static final HungrySingle2 LIZY=new HungrySingle2();
}
}
枚举类单例模式
序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
- 硬盘存储和读取,把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 网络通信发送和接收,在网络上传送对象的字节序列。
注意:序列化和反序列化会导致单例模式失败:
举例说明:
EnumSingleTest
public class EnumSingleTest {
public static void main(String[] args) {
try{
//过程序列化
HungrySingle3 instance1=null;
HungrySingle3 instance2=HungrySingle3.getInstance();
//instance2.setData(new Object());
//把对象写入流
FileOutputStream fos=new FileOutputStream("HungrySingle3.obj");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
//读出对象
FileInputStream fis=new FileInputStream("HungrySingle3.obj");
ObjectInputStream ois=new ObjectInputStream(fis);
HungrySingle3 instance3=(HungrySingle3) ois.readObject();
ois.close();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
System.out.println(instance3==instance2);
/*得到结果如下
null
com.baiwenxuan.Singleton.reg.HungrySingle3@5cad8086
com.baiwenxuan.Singleton.reg.HungrySingle3@6acbcfc0
false
*/
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
* 饿汉单例模式类
* 显示序列化和反序列化对单例模式的影响
*/
public class HungrySingle3 implements Serializable {
private static HungrySingle3 instance=new HungrySingle3();
private HungrySingle3(){}
public static HungrySingle3 getInstance()
{
return instance;
}
}
解决方案:使用枚举类实现单例模式类
优点:不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
代码:
public class Test {
public static void main(String[] args) {
try{
EnumSingleton instance1=null;
EnumSingleton instance2=EnumSingleton.getInstance();
instance2.setData(new Object());
//把对象写入流
FileOutputStream fos=new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
//读出对象
FileInputStream fis=new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois=new ObjectInputStream(fis);
instance1=(EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1.getData()==instance2.getData());
/*运行结果如下
java.lang.Object@5f184fc6
java.lang.Object@5f184fc6
INSTANCE
INSTANCE
true
*/
}catch(Exception e){
e.printStackTrace();
}
}
}
/*
枚举式单例模式:在静态代码中给INSTANCE进行了赋值,是饿汉单例模式的实现。
枚举类是通过类名和类对象找到一个唯一的枚举对象。
而不是通过类加载多次加载枚举对象的,因此解决了序列化破坏单例模式问题。
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance()
{
return INSTANCE;
}
}