简介
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。这篇博客很精彩哦,请一定要耐心看完哦
在Java设计模式中,单例模式相对来说算是比较简单的一种构建模式。适用的场景在于:对于定义的一个类,在整个应用程序执行期间只有唯一的一个实例对象。如Android中常见的Application对象。
基本思路
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。
单例的实现主要是通过以下两个步骤:
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
注意事项
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
这边主要介绍常用的几种:饿汉式、懒汉式、静态内部类
1,饿汉式
Singleton.java
package mode.singleton;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private final static Singleton INSTANCE = new Singleton();
/**
* 饿汉式
* @return Singleton
*/
public static Singleton getSingleton1() {
return INSTANCE;
}
}
饿汉式比较简单,所以就不做运行的显示了。
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
2,懒汉式(线程不安全)
Singleton.java
package mode.singleton;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
/**
* 懒汉式(又称延迟加载)
* @return
*/
public static Singleton getSingleton() {
if(null == instance) {
instance = new Singleton();
}
return instance;
}
}
上述代码是线程不安全的,为什么说线程不安全呢?就是当多个线程同时调用上述代码可能返回多个不同的实例,这违背了单利设计模式的思想。为了显示测试的效果,我模拟线程不安全的场景,代码如下:
Singleton.java
package mode.singleton;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
public static Singleton getSingleton() {
if(null == instance)
try {
Thread.currentThread().sleep(1000);//模拟不安全的场景
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
instance = new Singleton();
}
return instance;
}
}
SingleClient.java
package mode.singleton;
public class SingleClient {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Singleton s1 = Singleton.getSingleton();
System.out.println(s1);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Singleton s2 = Singleton.getSingleton();
System.out.println(s2);
}
});
t1.start();
t2.start();
}
}
运行结果:
很明显的可以看出上述返回的对象不是同一个对象,所以这是线程不安全的,不能使用
3,懒汉式(线程同步,同步整个方法,性能差不推荐使用)
Singleton.java
package mode.singleton;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
/**
* 懒汉式,同步方法,效率低
* @return
*/
public static synchronized Singleton getSingleton() {
if(null == instance) {
instance = new Singleton();
}
return instance;
}
}
虽然解决了线程安全问题,但是带来了性能问题,为什么说效率低呢?因为该方法是同步了整个方法,如果多个线程同时调用该方法时,需要一个个排队(相当于串行),虽然这里面的代码不多,但是线程多的话,就会累计起来,对性能造成影响。等下将该方法和下面的double check的性能做对比。
4,双重校验
Singleton.java
package mode.singleton;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static volatile Singleton instance;//这里使用volatile是防止jvm指令重排序
/**
* double check推荐使用
* @return
*/
public static Singleton getSingleton() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
首先这个时线程安全的,我们看下运行的结果,客户端代码同刚才不安全的懒汉式代码一样:
这里返回的两个对象时一样的,说明这个是线程安全的,并且性能也非常好。但是有一个需要注意的地方,需要使用violatile修饰对象,防止jvm优化时可能会对创建单例对象进行重排序,从而引起错误,使用violatile关键字修饰,就是告诉jvm虚拟机,这里不需要指令重排序优化。下面将该方法通上面的同步整个方法性能对比:
Singleton.java
package mode.singleton;
import java.sql.Time;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
/**
* 懒汉式,同步方法,效率低
* @return
*/
public static synchronized Singleton getSingleton2() {
long start = System.currentTimeMillis();
System.out.println("start = "+start);
try {
Thread.currentThread().sleep(500);//模拟耗时,方便直观显示
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(null == instance) {
instance = new Singleton();
}
long end = System.currentTimeMillis();
System.out.println("end = "+end);
return instance;
}
/**
* double check推荐使用
* @return
*/
public static Singleton getSingleton() {
long start = System.currentTimeMillis();
System.out.println("start = "+start);
try {
Thread.currentThread().sleep(500);//模拟耗时,方便直观显示
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
long end = System.currentTimeMillis();
System.out.println("end = "+end);
return instance;
}
}
SingletonClient.java
package mode.singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleClient {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
for(int i=0;i<5;i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Singleton singleton = Singleton.getSingleton2();//double check
//Singleton singleton2 = Singleton.getSingleton();//同步方法
}
});
pool.execute(thread);
}
pool.shutdown();
}
}
运行同步方法结果:
运行时间end(max)-start(min) = 2504从打印的结果来看,基本上是串行的,所以比较耗时
运行double check结果:
运行时间end(max)-start(min) = 502,从打印的结果来看,是并行的,既解决了线程安全、又保证了性能
5,静态内部类
Singleton.java
package mode.singleton;
import java.sql.Time;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
/**
* 静态内部类
* @return
*/
public static Singleton getSingleton() {
return SingletonInstance.INSTANCE;
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
我们来测试下上面描述的情况:内部类不会随着外部类的加载而加载,只有当用到时再加载(延时加载)
SingletonClient.java
package mode.singleton;
import java.sql.Time;
public class Singleton {
private Singleton() {};//私有话构造方法,这是单利设计模式的关键所在
private static Singleton instance;
static {
System.out.println("load Single");
}
public static void testLoad() {}//测试加载
/**
* 静态内部类
* @return
*/
public static Singleton getSingleton() {
return SingletonInstance.INSTANCE;
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
static {
System.out.println("load SingletonInstance");
}
}
}
SingletonClient.java
package mode.singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleClient {
public static void main(String[] args) {
Singleton.testLoad();
}
}
运行结果:
从运行结果看出内部类并没有随着外部类的加载而加载,接下来看看调用getSingleton()方法之后的运行结果
SingleClient.java
package mode.singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleClient {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
System.out.println(singleton);
}
}
运行结果:
从结果可以看出,只有当调用getSingleton()方法之后才会加载静态内部类。
好了,单利设计模式就讲到这里了,基本上经常使用的就这几个,当然还有其他的方法,比如枚举、同步代码块等,这里就不讲了。如有错误,欢迎指正。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/111192.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...