单例模式
- 单例模式就是在整个运行时域中,一个类只会创建一个实例对象
- 为什么需要单例模式?因为在Java中,有的类的创建和销毁对资源来说开销并不大,例如String可能会被频繁的创建和销毁;但是有些类比较复杂庞大并且能够复用,如果频繁创建销毁,会造成性能的浪费,这也是为什么需要单例模式的原因
单例模式需要考虑的问题
1.懒汉式
- 懒汉式:顾名思义就是可以懒加载的单例模式,只有当第一次调用getInstance()方法的时候,才会创建这个单例对象,减少了不必要的开销,但是会有线程安全问题
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
2.饿汉式
- 饿汉式:我们发现线程安全的问题出现在了构建对象的阶段,如果我们在编译期构建对象,在运行时访问对象,就不会有线程安全问题了,但是饿汉式无法懒加载,也就是说在这个类被加载的时候,这个单例对象就随之被创建了,如果后期没有用到这个对象的话,会造成性能的浪费
public class Singleton{
private Singleton(){}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
3.懒汉式改造形态1
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
4.懒汉式改造形态2
- 问题:多个线程执行到语句2后,虽然只有一个线程可以抢到锁,但是这个线程释放锁之后,其他线程拿到锁后又重复创建对象
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){ //1
if(instance == null){ //2
synchronized(Singleton.class){ //3
instance = new Singleton(); //4
}
}
return instance;
}
}
5.懒汉式改造形态3
- 可以解决上面的问题:其他线程拿到锁之后,再次判断是否已经创建对象,并且可以解决后续线程来访问getInstance()方法时不会每次都进行同步,效率有所提高
- 出现新的问题:instance = new Singleton()不是原子操作
- 指令上分为3步:1.分配内存,2.初始化对象,3.对象指向内存地址
- JVM可能会为了效率进行指令重排,如果是132的执行顺序,会得到一个半初始化的对象,即instance != null 但是还没有被初始化的对象,这时如果执行完第3步之后发生了线程上下文切换,另一个线程就会因为instance != null而拿到未初始化成功的对象
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
6.double-check locking
- 这是懒汉式改造的最终形态,也被称为双重检测锁机制
- 在instance对象上加了volatile关键字,禁止创建对象时的指令重排,从而实现了线程安全+懒加载的单例模式
public class Singleton{
private Singleton(){}
private volatile static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
7.静态内部类实现的单例模式
public class Singleton{
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.instance;
}
}
public void test() throws Exception{
Constructor constructor = Class.forName("com.sample.Singleton").getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = (Singleton) constructor.newInstance();
Singleton s2 = (Singleton) constructor.newInstance();
System.out.println(s1 == s2);//false
}
8.枚举单例模式
- 上面所有创建单例模式的方式,都可以被人为的反射暴力破解,拿到类的私有构造器并且创建多例对象
- enum关键字相当于让这个类继承了Enum这个抽象类,无法被反射破解
public enum Singleton{
INSTANCE;
}
- Enum有构造器,但是无法被反射获取到,也就是无法通过反射来创建对象,从而真正实现单例,但是缺点就是无法懒加载
//Enum类的构造器
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
为什么要double-check?只有一个sync感觉也是线程安全的。还有volatile禁止创建对象时的指令重排是什么意思?
刚看了一下,两个synchronized是我手快忘记删了 只有方法内部的同步锁就可以 然后volatile关键字会在读写操作的时候实现内存屏障来禁止指令重排 至于指令重排是啥呢 你可以去看看Java并发编程的资料吧 感觉三两句话解释不清楚= = 后续如果我不懒的话应该也会写多线程的相关笔记hh