单例模式
1 饿汉模式
private static Singleton single=new Singleton();
特点: 饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全 ,但是缺点是内存开销( 用与不用都占着空间 )。
2 懒汉模式
public class Singleton1 {
//1、第一步先将构造方法私有化
private Singleton1() {}
//2、然后声明一个静态变量保存单例的引用
private static Singleton1 single = null;
//3、通过提供一个静态方法来获得单例的引用
public static Singleton1 getInstance() {
if (single == null) {
single = new Singleton1();
}
return single;
}
}
特点:被外部类调用的时候内部类才会加载 ,线程不安全
为什么线程不安全?
显而易见,多线程可能同时进入到single = new Singleton1();
解决线程不安全
getInstance()方法加上synchronized关键字保证某时刻只能有一个线程进入到getInstance()方法。
当第一个线程执行到 synchronized 会上锁 ,第二个钱程试图执行getInstance()方法就会变成 MONITOR 状态,出现阻塞。
synchronized锁住整个方法,存在性能问题
双重校验锁模式
一种更细腻,优化后的的synchronized。
public class Singleton {
//volatile变量 保证可见性、禁止指令重排序 不保证原子性
private volatile Singleton singleton;
public Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//三步执行:
// 1.为singleton分配内存空间
// 2.初始化singleton
// 3.将singleton指向分配的内存地址
//但是由于JVM具有指令重排的特性,执行的顺序可能会1>3>2,
// 指令重排在单线程下不会有线程安全问题,
// 但在多线程环境下,会导致一个线程获得还没有初始化的实例
//例如线程T1执行1、3,此时线程T2调用getSingleton()方法后发现singleton不为空,
//因此返回singleton,但此时singleton 还未被实例化
//使用volatile关键字可以禁止指令重排序,保证在多线程环境下能正常运行
singleton = new Singleton();
}
}
}
return singleton;
}
}
3 内部类
public class LazyInnerClassSingleton{
private LazyInnerClassSingleton(){}
public static Singleton1 getInstance() {
return LazyHolder.INSTANCE;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton INSTANCE=new LazyInnerClassSingleton();
}
}
内部类的静态变量默认不加载。
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronzed 的性能问题。
4 通过反射破坏单例
以上的构造方法除了加上 private 关键字,没有做任何处理 如果我们使用反射来调用其构造方法,再调用 getlnstance()方法,应该有两个不同的实例
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor<?> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
//相当于new了两次
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.priintln(o1==o2); //false
5 通过序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。 如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例
public class Singleton implements Serializable{...}
测试:
Singleton1 s1=null;
Singleton1 s2=Singleton1.getInstance();
FileOutputStream fos=null;
fos=new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.close();
FileInputStream fis=new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton1) ois.readObject();
ois.close();
System.out.println(s1==s2); //false
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致 ,实例化了两次,违 背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实 很简单,只需要增加 readResolve()方法即可
public class SeriableSingleton implements Serializable{
public final static SeriableSingleton INSTANCE =new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
为什么加上readResolve()可以保证反序列化的对象与源对象相等?
从 ois.readObject()说起,通过jdk源码发现是通过反射获取INSTANCE对象,开销大
readResolveMethod.invoke(obj, (Object[]) null);
注册式单例模式
注册式单例模式分两种:一 种为枚举式单例模式 ,另一种为容器式单例模式
枚举式单例
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;
}
public static void main(String[] args) throws Exception {
EnumSingleton instance1=null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos=null;
fos=new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.close();
FileInputStream fis=new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData()==instance2.getData()); //true
}
}
通过jad工具反编译EnumSingleton.class 发现枚举模式类似饿汉模式,在静态代码块给实例进行了赋值
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
1 验证序列化能否破坏枚举单例。看源码ois.readObject->readObject0->readEnum发现
Enum<?> en = Enum.valueOf((Class)cl, name);
原来枚举类型是通过Enum.valueOf方法返回
2 验证能否通过反射创建枚举类型
测试代码
Class clazz= EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.newInstance();
运行抛出异常java.lang.NoSuchMethodException
.
意思是没有找到无参的构造方法,看Enum类发现只有一个protected构造方法
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
更换测试代码
Class clazz= EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
c.newInstance();
再次报错 java.lang.IllegalArgumentException: Cannot reflectively create enum objects
即不能用反射来创建枚举类型
容器式单例
public class ContainerSingleton{
private ContainerSingleton(){}
private static Map<String,Object> ioc =new ConcurrentHashMap<String,Object>();
public static Object getBean(String className){
synchronized(ioc){
if(!ioc.containsKey(className)){
Object obj=null;
try{
obj =Class.forName(className).newInstance();
ioc.put(class,obj);
}catch(Exception e){
e.printStackTrace();
}
return obj;
} else{
return ioc.get(className);
}
}
}
}
容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。 存在序列化和反射攻击问题
线程单例实现ThreadLocal
ThreadLocal不能保证其创建的对象是全局的,但是能保证在单个线程中是唯一的,天生是线程安全的
public class ThreadLocalSingleton{
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance=
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
测试代码
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExecutorThread());
Thread t2 = new Thread(new ExecutorThread());
t1.start();
t2.start();
System.out.println("End");
结果
com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
End
Thread-0com.gupaoedu.singleton.Singleton1@4f9d1ec9
Thread-1com.gupaoedu.singleton.Singleton1@4f9d1ec9
我们发现,在主线程中无论调用多少次,获取到的实例都是同一个。在两个子线程中分别获取到了不同的实例。那么ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达 到线程安全的目的,会给方法上锁,以时间换空间,ThreadLocal将所有的对象全部放在ThreadLocalMap 中,为 个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。