什么是单例?
单例类在整个程序中只能由一个实例,这个类负责创建自己的对象,并保证只能有一个对象被创建
代码实现的要点:
1)私有构造器
2)持有该类的属性
3)对外提供获取实例的静态方法
下面为Java单例的几种实现形式
(1)饿汉式
类加载的时候就创建实例
private static Singleton1 instance=new Singleton1();//类加载时候就创建了
private Singleton1() {//私有化构造器
System.out.println("Singleton1 loaded");
}
public static Singleton1 getInstance() {//获取实例
return instance;
}
线程安全 反射不安全 反序列化不安全 在类加载的时候就被初始化,占浪费空间
但是这时候反序列化是不安全的,因为 静态变量在序列化是不会被保存的,所以在反序列化的时候会重新生成一个实例,这时候需要加一个readResolve方法
public Object readResolve() {
return instance;
}
(2)登记式(静态内部类)
/*
* 登记式
*/
public class Singleton2 {
private static class SingletonHolder{//定义一个私有静态内部类
private static Singleton2 instance=new Singleton2();//在静态私有内部类定义一个私有静态方法
}
private Singleton2() {
System.out.println("Singleton2 loaded");
if (SingletonHolder.instance!=null) {//防止通过反射来new实例
throw new IllegalStateException();
}
}
public static Singleton2 getInstance() {
return SingletonHolder.instance;
}
}
线程安全、防止反射攻击、反序列化不安全
(3)枚举式
public Singleton3{
INSTANCE{
@override
protected void doSomething(){
System.out.println("doSomething");
}
};
private abstract void doSomething();
}//该枚举类型反编译后的得到一个abstract的类,abstract的类无法通过反射实例化
线程安全、立即初始化 自动支持序列化 防止反序列化创建对象 直接防止反射攻击 更简单高效,但是不适合继承的类
(4)懒汉式
相对于饿汉式的区别,在使用的时候再去加载对象,节省资源
/*
* 懒汉式
*/
public class Singleton4 {
private static Singleton4 instance=null;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance==null) {
instance=new Singleton4();
}
return instance;
}
}
如果整个时候又多个线程访问这个实例,但是这个实例不存在,每个线程都会进入new多个实例,这个时候线程是不安全的,我们应该在该方法加上synchronized关键字来加同步锁实现线程同步,即
//同步方法
public static synchronized Singleton5 getInstance() {
if (instance==null) {
instance=new Singleton5();
}
return instance;
}
或者同步类
public static Singleton6 getInstance() {
synchronized (Singleton6.class) {//同步代码块
if (instance==null) {
instance=new Singleton6();
}
}
return instance;
}
然而,加synchronized(同步锁)则大大增加了系统开销
(5)双检索
减小了系统开销 ,相对于懒汉式,加if(instance==null){},即,只是一开始单例初始化的时候同步
public class Singleton7 {
private static Singleton7 instance=null;
private Singleton7() {
}
public static Singleton7 getInstance() {
if (instance==null) {//双检索,只是一开始单例初始化的时候同步
synchronized (Singleton7.class) {//同步代码块
if (instance==null) {
instance=new Singleton7();
}
}
}
return instance;
}
}
但是由于编译器自身的缘故,可能会出现指令重排,这时候需要在私有的属性前面加volatile关键字
private static volatile Singleton7 instance=null;
关于指令重排
,CPU和编译器为了提升程序的执行效率, 通常会按照一定的规则对指令进行优化, 如果两条指令互不依赖, 有可能它们执行的顺序并不是源代码编写的顺序。
比如instance=new Singleton7()会执行以下操作
1)分配对象内存空间
2)初始化对象
3)instance指向(1)中分配的空间
在某些编译器中,可能出现指令重排
1)分配对象内存空间
2)instance指向(1)中分配的空间(但是此时没有初始化对象)
3)初始化对象
比如又一个线程通过了第一个if语句通过了synchronized拿到锁进入了第二个if里面实例化了方法,分配了对象,但是这时候instance却没有初始化,这时候如果又有一个线程第一个if为false,直接返回了instance,而此时的instance却是没有初始化的实例,这时候就出问题了
volatile作用
- 保证可见性
- 不保证原子性
- 禁止指令重排
通过加volatile就禁止指令重排,保证cpu按照顺序指向指令,就不会担心以上问题了
(6)ThreadLocal
package singleton;
/*
* ThreadLocal保证一个线程之间是单例
*/
public class Singleton8 {
private static Singleton8 instance=null;
private Singleton8() {
}
private static final ThreadLocal<Singleton8> threadLocalSingleton=new ThreadLocal<Singleton8>() {
@Override
protected Singleton8 initialValue() {
return new Singleton8();
}
};
public static Singleton8 getInstance() {
return threadLocalSingleton.get();
}
}
不加锁,以空间换时间 ,为每个线程提供变量的独立副本,可以保证各自线程中是单例的,但是不同线程之间不是
(7)CAS(Compare And Swap)
public class Singleton9 {
private static final AtomicReference<Singleton9> instance=new AtomicReference<Singleton9>();
private Singleton9() {
System.out.println("Singleton9 loaded");
}
public static final Singleton9 getInstance() {
for(;;) {
Singleton9 current=instance.get();
if (current!=null) {
return current;
}
current=new Singleton9();
if (instance.compareAndSet(null, current)) {
return current;
}
}
}
}
比较交换技术 假设线程访问资源没有出现冲突,如果出现冲突就重试当前策略 无锁乐观策略,线程安全,实现比较复杂