常见的单例模式有5五种,各有优缺点,至于有哪5种呢?如下:
1:恶汉式(线程安全,调用效率高,但是不能延迟加载)
2:懒汉式(线程安全,但是调用效率不高,但是可以延迟加载)
3:双重检测锁式(线程安全,但是效率很低)
4:静态内部类式(线程安全 调用效率高,可以延迟加载)
5:枚举单例(线程安全 调用效率高,不能延迟加载)
现在写代码分别对上面几种方式做一个简单的说明:
恶汉式:
package cn.pattern.signinstance;
/**
* 恶汉式
* @author admin
*/
public class SingleDemo1 {
private static SingleDemo1 INSTANCE = new SingleDemo1();
private SingleDemo1(){}
public static SingleDemo1 getInstance(){
return INSTANCE;
}
}
这就是恶汉式的代码,简单,但是有个问题就是SingleDemo1 类在被类加载器加载到内存中,但是还没开始用getInstance方法式,对象先被创建了,这样就带来一个问题就是getInstance方法可能在整个项目中就没一次被调用,这样就带来了浪费资源(其实就是内存),这是恶汉式的一个缺点,优点就是它很早就被类装载器加载到了内存中,所以不管你在外部如果调用它都不会造成线程安全问题,因为它预先已经创建了改对象,所以不管在外部怎么调用都是之前创建的对象
懒汉式:
所谓懒汉式就是什么时候用到对象才去创建对象,这样就避免了资源的浪费,这是懒汉式相对恶汉式的一个优点,代码如下:
package cn.pattern.signinstance;
/**
* 懒汉式
*/
public class SingleDemo2 {
private static SingleDemo2 INSTANCE =null;
private SingleDemo2(){}
public static SingleDemo2 getInstance(){
if(INSTANCE==null){
INSTANCE = new SingleDemo2();
}
return INSTANCE;
}
}
但是这个会有个问题就是多个线程去调用getInstance()方法会造成对象不唯一,也就是创建多个对象的情况,这样就破坏了单例模式的原则了,单例模式本来就是要保证对象在内存中的唯一性,那么是在哪行代码引起的呢?分析如图:
解决这个问题就是要实现同步了,同步意思就是在某一个时刻只能有一个线程访问该方法,这样就保证了只能创建一个对象,线程是安全了,但是由于调用要进行等待,所以造成了调用效率不是很高,因为锁的存在,使用synchronized对方法进行同步:
public static synchronized SingleDemo2 getInstance(){
if(INSTANCE==null){
INSTANCE = new SingleDemo2();
}
return INSTANCE;
}
懒汉式的有点是可以延迟加载,避免了资源浪费 线程安全, 缺点是由于要实现同步所以调用效率不高 因为每次调用都是要等待锁被释放
双重检测锁式:
这个出现是基于上面的懒汉式每次都要去判断锁是否释放了,这个只判断一次,相对于优化了访问效率,代码如下:
package cn.pattern.signinstance;
/**
* 双重检测
* @author admin
*/
public class SingleDemo3 {
private volatile static SingleDemo3 instance = null;
private SingleDemo3() {}
public static SingleDemo3 getInstance() {
if (instance == null) {
synchronized (SingleDemo3.class) {// 1
if (instance == null) {// 2
instance = new SingleDemo3();// 3
}
}
}
return instance;
}
}
但是这个由于编译器优化原因和JVM虚拟机内部模式原因偶尔会错,所以这种最好不要用,这个效率比懒汉式效率高,但是比恶汉式效率低
静态内部类的实现
代码如下:
package cn.pattern.signinstance;
/**
* 静态内部类方式实现单例
* @author admin
*/
public class SingleDemo4 {
private SingleDemo4(){}
private static class SingleInnerDemo4{
private static final SingleDemo4 INSTANCE = new SingleDemo4();
}
public static SingleDemo4 getInstance(){
return SingleInnerDemo4.INSTANCE;
}
}
这个不像恶汉式那样类被装在到内存中就实例化对象,静态内部类是没有的,只有外部调用了getInstance()方法时,才会加载和初始化静态内部类,同时类加载的过程是安全的,所以是线程安全的,而且实例化对象时使用了final修饰,所以一旦赋值就不能改变其值,所以静态内部类的方式实现单例优点就是线程安全 调用效率高,而且还达到了延迟加载的效果,很多开源项目中单例采用这种方式
枚举实现单例,
代码:
package cn.pattern.signinstance;
public enum SingleDemo5{
INSTANCE;
}
package cn.pattern.signinstance;
public class SingleDemo6 {
public static void main(String[] args) {
SingleDemo5 s1= SingleDemo5.INSTANCE;
SingleDemo5 s2= SingleDemo5.INSTANCE;
System.out.println(s1==s2);
}
}
枚举实现单例
优点:
枚举在底层实现就保证了它是单例,这样就避免了通过反射和反序列化的漏洞
缺点:
不能延迟加载
破解单例模式(不包含枚举)
下面就以恶汉式为例 利用反射对单例进行操作,你会发现单例的漏洞
package cn.pattern.signinstance;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 测试几种单例模式的效率
*/
public class Client {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
SingleDemo1 singleDemo1 = SingleDemo1.getInstance();
SingleDemo1 singleDemo2 = SingleDemo1.getInstance();
System.out.println("singleDemo1="+singleDemo1);
System.out.println("singleDemo2="+singleDemo2);
try {
Class clazz = (Class) Class.forName("cn.pattern.signinstance.SingleDemo1");
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);//跳过检查权限
SingleDemo1 s1 = constructor.newInstance();
SingleDemo1 s2 = constructor.newInstance();
System.out.println("s1="+s1);
System.out.println("s2="+s2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
打印结果:
singleDemo1=cn.pattern.signinstance.SingleDemo1@2a139a55
singleDemo2=cn.pattern.signinstance.SingleDemo1@2a139a55
s1=cn.pattern.signinstance.SingleDemo1@15db9742
s2=cn.pattern.signinstance.SingleDemo1@6d06d69c
你会发现前二个是同一个对象,后二个是不同的对象,这就是利用了反射对单例进行了破坏,解决方案其实可以在你的构造函数中判断是否已经实力化了,如果已经实例化了就抛出异常
private SingleDemo1(){
if(INSTANCE!=null){ 多次调用直接抛出异常
throw new RuntimeException();
}
}
现在再执行的话就会报错了:
singleDemo1=cn.pattern.signinstance.SingleDemo1@2a139a55
singleDemo2=cn.pattern.signinstance.SingleDemo1@2a139a55
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at cn.pattern.signinstance.Client.main(Client.java:20)
Caused by: java.lang.RuntimeException
at cn.pattern.signinstance.SingleDemo1.
... 5 more
下面是利用反序列化的方式破解单例模式,代码如下
package cn.pattern.signinstance;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
*/
public class Client {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
SingleDemo1 singleDemo1 = SingleDemo1.getInstance();
SingleDemo1 singleDemo2 = SingleDemo1.getInstance();
System.out.println("singleDemo1="+singleDemo1);
System.out.println("singleDemo2="+singleDemo2);
try {
FileOutputStream fos = new FileOutputStream("e:/data.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleDemo1);
oos.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
//读取数据
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/data.txt"));
SingleDemo1 s1 = (SingleDemo1) ois.readObject();
System.out.println("s1="+s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
当然SingleDemo1类要实现Serializable接口才行,打印结果
singleDemo1=cn.pattern.signinstance.SingleDemo1@2a139a55
singleDemo2=cn.pattern.signinstance.SingleDemo1@2a139a55
s1=cn.pattern.signinstance.SingleDemo1@55f96302
你会发现singleDemo1和s1的值不相同,那么这种如何防止破坏单例的呢?就是在反序列化的时候,如果写了readResolve()方法则直接返回之前创建的对象,而不创建新的对象,
解决方案:
/**
* 恶汉式
* @author admin
*/
public class SingleDemo1 implements Serializable{
private static final long serialVersionUID = 1L;
private static SingleDemo1 INSTANCE = new SingleDemo1();
private SingleDemo1(){
if(INSTANCE!=null){
throw new RuntimeException();
}
}
public static SingleDemo1 getInstance(){
return INSTANCE;
}
public Object readResolve() throws ObjectStreamException{
return INSTANCE;
}
}
现在执行再打印结果:
singleDemo1=cn.pattern.signinstance.SingleDemo1@2a139a55
singleDemo2=cn.pattern.signinstance.SingleDemo1@2a139a55
s1=cn.pattern.signinstance.SingleDemo1@2a139a55
ok,这样就解决了反序列化破坏单例的实现
现在是比较5种方式实现单例的效率
package cn.pattern.signinstance;
import java.util.concurrent.CountDownLatch;
/**
* 比较5种创建单例实现的效率
*/
public class TimeClinet {
public static void main(String[] args) throws Exception {
int threadNum = 10;
long start = System.currentTimeMillis();
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i
结果是:
恶汉式总消耗的时间:14
package cn.pattern.signinstance;
import java.util.concurrent.CountDownLatch;
/**
* 比较5种创建单例实现的效率
*/
public class TimeClinet {
public static void main(String[] args) throws Exception {
int threadNum = 10;
long start = System.currentTimeMillis();
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i
结果是:
双重检测锁式总消耗的时间:24
package cn.pattern.signinstance;
import java.util.concurrent.CountDownLatch;
/**
* 比较5种创建单例实现的效率
*/
public class TimeClinet {
public static void main(String[] args) throws Exception {
int threadNum = 10;
long start = System.currentTimeMillis();
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i
结果是:
静态内部类方式实现单例总消耗的时间:23
package cn.pattern.signinstance;
import java.util.concurrent.CountDownLatch;
/**
* 比较5种创建单例实现的效率
*/
public class TimeClinet {
public static void main(String[] args) throws Exception {
int threadNum = 10;
long start = System.currentTimeMillis();
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i
结果是:
枚举实现单例方式总消耗的时间:19
总结:
效率从高到底:如图:特别说明这是在我自己的机制下跑的结果
ok,写完!