单例模式:在一个应用中一个类对应的对象只有一个。
常见的单例应用:spring中默认bean为单例,JavaWeb中Application对象。在程序中有些类只需要一个对象,比如全局配置信息,公共服务对象。 单例模式能减少资源的浪费,减少程序配置的复杂度。学习了下各个实现单例的思想。
理想的单例默认应有的特性:线程安全,多线程调用效率高,能延迟加载。
一、单例模式实现方式
1.恶汉式
package singleton;
/**
* 恶汉式单例模式
*/
public class Singleton01 {
//01 私有化构造器
private Singleton01(){}
//02 静态初始化需要单例的类
private static Singleton01 instance = new Singleton01();
/**
* 返回当前单例
* 因为单例对象在对象初始化时就生成,不存在多线程竞争
* 优点:不需要synchronize同步,并发调用效率高
* 缺点:不能延迟加载,当此单例未使用时 造成了资源浪费
*/
public static Singleton01 getInstance(){
return instance;
}
}
2.懒汉式
package singleton;
/**
* 懒汉模式
*/
public class Singleton02 {
//01 私有化构造器
private Singleton02(){}
//02 静态需要单例的类
private static Singleton02 instance ;
/**
* 返回当前单例 增加 synchronize同步 解决多线程调用线程安全问题
* 优点:需要synchronize同步,并发调用效率低
* 缺点:延迟加载,当此单例未使用时 节省资源
*/
public static synchronized Singleton02 getInstance(){
if(instance == null){
instance = new Singleton02();
}
return instance;
}
}
3.双重检验锁(在实际使用时有一定概率出现线程安全问题返回的对象未初始化完成)
package singleton;
/**
* 双重检测锁
*/
public class Singleton03 {
//01 私有化构造器
private Singleton03(){}
//02 静态需要单例的类
private static Singleton03 instance ;
/**
* 解决 资源浪费和同步调用效率低问题
*
* 编译器优化问题和jvm底层模型 有时会出问题:
* 在于JVM 是先分配空间引用 给instance 再实例化对象 还是先实例化完之后 再给引用给instance
* 仅供参考一般不使用
*/
public static Singleton03 getInstance(){
if(instance == null){
//锁对象
synchronized (Singleton03.class){
if(instance == null){
synchronized (Singleton03.class){
instance = new Singleton03();
}
}
}
}
return instance;
}
}
4.内部类(推荐)--符合 线程安全,多线程调用效率高,延迟加载
package singleton;
/**
* 静态内部类(懒加载方式)
*/
public class Singleton04 {
//01 私有化构造器
private Singleton04(){}
/**
*单例模式要求: 1.线程安全 2.调用效率高 3.懒加载
*当外部调用getInstance()时才会加载内部类,同时实例化单例。类加载时 线程安全。
* 此方式兼顾了 调用效率和延迟加载
*
*/
public static Singleton04 getInstance(){
//加载内部类时jvm会自动加锁 直到类初始化完
return SingletonInner.instance;
}
//静态内部类
private static class SingletonInner{
private static final Singleton04 instance= new Singleton04();
}
}
5.枚举
package singleton;
/**
* 枚举 枚举本身就是单例模式 。 jvm提供保证
* 无延迟加载
*/
public enum Singleton05 {
/**
* 定义枚举元素,就代表singleton
* 本身就是单例 ,调用效率高,但是没延迟加载
*/
instance("tim");
Singleton05(String name){
this.name = name;
}
String name = "tom";
public void getName(){
System.out.println(name);
}
}
二、单例模式注意事项
除枚举单例模式是JVM来维护,其他方式实现的单例模式不做安全限制,都可以通过反射和序列化方式破解。下面以内部类单例模式为例,测试破解。
1.反射方式破解单例模式
import singleton.Singleton06Reflect;
import java.lang.reflect.Constructor;
public class client1 {
public static void main(String[] args) throws NoSuchMethodException {
//通过反射打破单例
Singleton06Reflect s6reflect = Singleton06Reflect.getInstance();
Singleton06Reflect ss6reflect = Singleton06Reflect.getInstance();
System.out.println(s6reflect);
System.out.println(ss6reflect);
//获取class对象
Class clazz6 = Singleton06Reflect.class;
//获取无参构造函数
Constructor constructor = clazz6.getDeclaredConstructor(null);
//打破权限
constructor.setAccessible(true);
try{
//反射代用构造函数,实例化对象
Singleton06Reflect s = (Singleton06Reflect) constructor.newInstance(null);
Singleton06Reflect ss = (Singleton06Reflect) constructor.newInstance(null);
System.out.println(s);
System.out.println(ss);
}catch (Exception e){
e.printStackTrace();
}
}
}
输出
可见反射生成的为新对象,打破了我们使用单例模式的初衷
防范措施及思路:反射调用构造函数 生成新的对象,那么可以在构造函数里面做单例的判断,阻止生成新的对象。
private Singleton06Reflect() throws Exception {
if(SingletonInner.instance != null){
throw new Exception("此类为单例,禁止生成新对象,你可以通过getInstance()获取单例对象");
}
}
测试
2.序列化方式打破单例模式
要实现对象的序列化那么对象要实现Serializable接口 否则序列化时会报java.io.NotSerializableException。
import singleton.Singleton06Reflect;
import java.io.*;
public class client2 {
public static void main(String[] args) throws IOException {
//通过反序列化 打破单例
Singleton06Reflect s6reflect = Singleton06Reflect.getInstance();
//字节数字输出流
ByteArrayOutputStream baos =null ;
//对象输出留言
ObjectOutputStream oos =null ;
//字节数组输入流
ByteArrayInputStream bais =null;
//字节数组输出流
ObjectInputStream ois =null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
//序列化对象s6reflect对象到字节数组流中
oos.writeObject(s6reflect);
//从输出字节数组流中读取字节
bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);
//反序列化为新对象
Singleton06Reflect sarry = (Singleton06Reflect) ois.readObject();
System.out.println(s6reflect);
System.out.println(sarry);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
ois.close();
bais.close();
oos.close();
baos.close();
}
}
}
结果反序列化后为一个新对象
防范措施及思路。 通过序列化和反序列化的api查阅,当被序列化的对象有public Object readResolve()方法时,直接返回此方法返回的对象。
到此比较完整的单例模式类如下使用内部类的方式:
package singleton;
import java.io.Serializable;
/**
* 静态内部类(懒加载方式)
*/
public class Singleton06Reflect implements Serializable {
//01 私有化构造器
private Singleton06Reflect() throws Exception {
if(SingletonInner.instance != null){
throw new Exception("此类为单例,禁止生成新对象,你可以通过getInstance()获取单例对象");
}
}
/**
*单例模式要求: 1.线程安全 2.调用效率高 3.懒加载
*当外部调用getInstance()时才会加载内部类,同时实例化单例。类加载时 线程安全。
* 此方式兼顾了 调用效率和延迟加载
*
*/
public static Singleton06Reflect getInstance(){
//加载内部类时jvm会自动加锁 直到类初始化完
return SingletonInner.instance;
}
//静态内部类
private static class SingletonInner{
private static Singleton06Reflect instance = null;
static {
try {
instance = new Singleton06Reflect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//反序列化时直接返回此对象,解决反序列化破解单例模式
public Object readResolve(){
return SingletonInner.instance;
}
}
三、几种单例模式的性能测试
经过测试性能:恶汉式>枚举>内部类>双重检验锁>>>懒汉式
懒汉式因为存在锁的竞争,所以性能最低。其他几种模式相差不大,结合消耗资源的大小,推荐 枚举和内部类 模式
性能测试代码:
import singleton.*;
import java.util.concurrent.CountDownLatch;
public class PerformanceTest {
public static void main(String[] args) throws InterruptedException {
//性能测试
Long currentTime = System.currentTimeMillis();
//线程计数器
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for(int i =0;i<1000;i++){
new Thread(new Runnable() {
public void run() {
for(int i=0 ;i<1000000;i++){
Object o = Singleton02.getInstance();
}
//此线程执行结束计数器减一
countDownLatch.countDown();
}
}).start();
}
//等待所有线程执行完
countDownLatch.await();
Long end = System.currentTimeMillis();
System.out.println(end-currentTime);
}
}