单例
什么是单例?
- 类只有一个唯一实例
Java中的单例
单例的两种方式:
- 1.饱汉式单例
- 2.饿汉式单例
饿汉式单例
什么是饿汉式单例?
- 饿汉,就是实例对象一开始就已创建好(这里的开始指的是类初始化阶段);
经典写法如下:
public class HungrySingle {
//声明一个WellFedSingle对象并初始化实例对象,设计成私有
private static HungrySingle single = new HungrySingle();
//重写私有构造方法,防止外部进行实例化(但是依旧可以通过反射进行实例化)
private HungrySingle() {
System.out.println("类进行实例化...");
}
//公开给外部一个方法,获取single对象
public static HungrySingle getInstance() {
return single;
}
public void testOfSingle(){
System.out.println("这是HungrySingle类对象的一个测试方法...");
}
}
这样一个饿汉式单例就完成了,但是这个类存在两个隐患和一个优势:
我们先通过javap -verbose命令查看此类的字节码文件:
Constant pool:
... //过滤掉常量池
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #1 // class cn/march/algo/string/HungrySingle
3: dup
4: invokespecial #10 // Method "<init>":()V
7: putstatic #13 // Field single:Lcn/march/algo/string/HungrySingle;
10: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
}
通过代码我们可以看到static HungrySingle single = new HungrySingle();这条语句其实转换成static语句块中执行
static{}语句块实际相当于clinit初始化方法(对应类加载过程的初始化阶段);在此static块中调用init方法(即类的构造方法,实例化类对象)
在JVM中规定:
- 虚拟机会保证clinit方法在多线程环境下被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个类执行clinit方法,其他线程都需要堵塞等待;
所以饿汉式单例是线程安全的;
优势:
- 1.此类是线程安全;
缺点:
- 2.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
- 3.能够通过反射攻击(即可以通过反射API访问类的资源(比如获取构造器Constructor对象进行实例化))
详情请参考Java单例讨论
饱汉式单例(懒汉)
什么是饱汉式单例?
- 指类实例对象是一个懒加载的过程
经典写法如下:
public class WellFedSingle {
//声明静态类实例对象,初始化为null
private static WellFedSingle single = null;
//重写构造方法,设为私有
private WellFedSingle() {
System.out.println("类进行实例化...");
}
public static WellFedSingle getInstance() {
//进行判断,如果single为null(即还没有创建对象),进行实例化操作
if(single == null){
single = new WellFedSingle();
}
return single;
}
}
这样一个饱汉式单例产生了,但是这个类有3个隐患:
隐患:
- 1.线程不安全(PS:如下图);
图中HungrySingle类应该改为WellFedSingle类(因为图文件找不到了,见谅!!!)
线程A执行if(single == null){ … }的时候线程B此时也执行到if(single == null):
- 1.而A并没有实例化,所以B正好完成实例化操作,B完成实例化操作之后,A并没有及时获取single状态也对single进行实例化操作;
- 2.A完成实例化操作,但是B并没有及时响应获取single状态,所以B对single进行实例化操作;
线程A,B都产生了不同的single对象;
所以这就产生了线程安全问题;
解决方案:
//使用JDK1.5之后的volatile保证single对象的可见性,但是仅仅光声明volatile不行,还需同步处理
private static volatile WellFedSingle single = null;
//为什么要双重判断,因为如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。
public static WellFedSingle getInstance() {
if(single == null) { //1
synchronized(WellFedSingle.class) {
//进行判断,如果single为null(即还没有创建对象),进行实例化操作
if(single == null){
single = new WellFedSingle();
}
}
}
return single;
}
- 2.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
- 3.能够通过反射攻击(即可以通过反射API访问类的资源(比如获取构造器Constructor对象进行实例化))
分析内部类,枚举实现单例
内部类
经典写法如下:
public abstract class InnerSingle {
//重写构造方法,设置为私有
private InnerSingle(){
System.out.println("类正在实例化...");
}
//编写一个静态内部类
private static class InnerSingleHelper {
//声明外部类对象并进行实例化赋值
private static final InnerSingle innerSingle = new InnerSingle() {
};
}
public static InnerSingle getInstance() {
return InnerSingleHelper.innerSingle;
}
}
这样一个内部类单例完成,这样写有2个优势,1大隐患
优势:
- 1.线程安全,因为是类加载过程中就已经实例化(如饿汉)
- 2.不能通过反射来进行实例化(因为定义为抽象类不能进行实例化)
隐患:
- 3.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
枚举实现单例
写法如下:
enum EnumSingle {
INSTANCE(){
@Override
public void testOfSingle() {
System.out.println("测试实例方法...");
}
};
//定义一个实例抽象方法
public abstract void testOfSingle();
}
通过javap查看字节码文件:
abstract class cn.march.algo.string.EnumSingle extends java.lang.Enum<cn.march.algo.string.EnumSingle> {
public static final cn.march.algo.string.EnumSingle INSTANCE;
static {};
public abstract void testOfSingle();
public static cn.march.algo.string.EnumSingle[] values();
public static cn.march.algo.string.EnumSingle valueOf(java.lang.String);
}
我通过javap -verbose查看static{ … }中字节码如下:
static {};
Code:
stack=4, locals=0, args_size=0
0: new #1 // class cn/march/algo/string/EnumSingle
3: dup
4: ldc #12 // String INSTANCE
6: iconst_0
7: invokespecial #13 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #17 // Field INSTANCE:Lcn/march/algo/string/EnumSingle;
13: iconst_1
14: anewarray #1 // class cn/march/algo/string/EnumSingle
17: dup
18: iconst_0
19: getstatic #17 // Field INSTANCE:Lcn/march/algo/string/EnumSingle;
22: aastore
23: putstatic #19 // Field ENUM$VALUES:[Lcn/march/algo/string/EnumSingle;
26: return
由代码可以知道,用枚举实现单例是最安全且高效:
优势:
- 1.线程安全,在静态块进行初始化(类比饿汉);
- 2.不能通过反射实例化(因为EnumSingle是abstract类);
- 3.不能通过反序列化实例化对象
好了先写到这,今天的目标完成了;
参考:Java枚举单例