单例模式
饿汉模式:全局的单实例在类构建时构建
public class Hungary{
private static final Hungary HUNGARY = new Hungary();
private Hungary(){}
public static Hungary getInstance(){
return HUNGARY;
}
}
优点:
- 饿汉式没有加任何的锁,因此执行效率比较高
缺点:
- 饿汉式在一开始类加载的时候就实例化,无论使用与否,都会实例化,所以会占据空间,浪费内存,尤其是存在很多需要加载的资源情况下。
枚举模式:自带单例模式,枚举本质也是一个类,在jdk1.5之后就存在
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
优点:代码实现简洁清晰。并且她还自动支持序列化机制,绝对防止多次实例化(防反射)。
懒汉模式:在加载类时不创建对象,在需要是在创建对象
public class LazyMan{
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
}
优点:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。
缺点:线程不安全,在单一线程下没有问题,但是多线程就有问题了
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
LazyMan instance = LazyMan.getInstance();
System.out.println(instance);
}
}).start();
}
}
//=======结果=======
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@4bd8a307
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@721cca89
//==============
可以发现多次运行结果不一样,所以为了保证安全又诞生了双检索懒汉模式
双检索懒汉模式(DCL):通过加锁保证线程安全。
public class DoubleCheck{
private DoubleCheck(){}
//volatile 关键字作用可以是保证可见性或者禁止指令重排
private volatile static DoubleCheck instance;
public static DoubleCheck getInstance(){
if(instance==null){
synchronized(DoubleCheck.class){
if(instance==null){
instance=new DoubleCheck();
/**
*new对象分三步
1.在内存开辟空间
2.调用构造器,初始化对象
3.将对象指向内存空间
但是,由于new对象不是原子性操作,所以可能存在指令重排执行顺序发生变化
a线程 123
b线程 132
//可能会导致空指针异常,所以需要给变量加volatile关键字
*/
}
}
}
return instance;
}
}
优点:综合了懒汉式和饿汉式两者的优缺点整合而成,既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
思考?加锁就一定能保证线程安全嘛?
探究反射破坏单例模式
可以测试通过反射创建对象:
public static void main(String[] args) throws Exception {
DoubleCheck instance = DoubleCheck.getInstance();
//反射创建对象
Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleCheck instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
//=========结果============
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4
//========================
我们可以通过测试看出,通过反射创建了一个新的对象,单例模式被破坏了。于是接着演变:
public class DoubleCheck {
private DoubleCheck(){
//在构造器中在加一层判断
synchronized (DoubleCheck.class){
if (instance!=null){
throw new RuntimeException("不能通过反射创建对象~");
}
}
}
private volatile static DoubleCheck instance;
public static DoubleCheck getInstance() {
if (instance == null) {
synchronized (DoubleCheck.class) {
if (instance == null) {
instance = new DoubleCheck();
}
}
}
return instance;
}
}
测试:
public static void main(String[] args) throws Exception {
DoubleCheck instance = DoubleCheck.getInstance();
Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleCheck instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~
[图片上传失败...(image-e0d83b-1653634504965)]
很明显我们加了三重验证防止反射创建对象,但是,这种情况是我们一开始就调用了getInstance()方法,执行了构造器中的同步代码,如果一开始就使用反射创建对象,那么依旧可以创建,测试一下:
public static void main(String[] args) throws Exception {
Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleCheck instance = constructor.newInstance();
DoubleCheck instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4
为了防止这种情况,继续进行优化:
public class DoubleCheck {
//定义一个标志位 可以是任意字符或者进行加密操作
private static boolean bk = false;
private DoubleCheck(){
//同步之后将标志位设为ture 第二次调用构造器就报错,确保只创建一次
synchronized (DoubleCheck.class){
if (!bk){
bk=true;
}else {
throw new RuntimeException("不能通过反射创建对象~");
}
}
}
private volatile static DoubleCheck instance;
public static DoubleCheck getInstance() {
if (instance == null) {
synchronized (DoubleCheck.class) {
if (instance == null) {
instance = new DoubleCheck();
}
}
}
return instance;
}
}
继续测试:
public static void main(String[] args) throws Exception {
Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleCheck instance = constructor.newInstance();
DoubleCheck instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~
说明可以通过加标志位的方式确保构造器只调用一次,只能创建一个对象。但是如果通过反编译等各种手段得到了标志位的话,依旧可以破坏单例模式,继续测试:
public static void main(String[] args) throws Exception {
//通过反射获得标志位
Field bk = DoubleCheck.class.getDeclaredField("bk");
bk.setAccessible(true);
Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleCheck instance = constructor.newInstance();
//在创建第一个对象后 恢复标志位
bk.set(instance,false);
DoubleCheck instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@330bedb4
com.zunhui.single.DoubleCheck@2503dbd3
结果我们又破坏了单例模式。
思考?那么,为什么通过反射就能破坏单例模式,就没有反射不能破坏的单例嘛?
我们通过查看constructor.newInstance();的源码:
//其中有这样一个异常,说反射不能创建enum对象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
那就说明反射不能创建枚举对象,在jdk中自己设置了不能通过反射创建枚举对象的机制。那我们测试一下:
编写一个枚举类:
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
反编译枚举类java文件,查看.class文件:
可以看到我们的枚举类本质也是一个类,继承了Enum父类,并且存在无参构造,那我们测试一下通过反射创建对象:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance = EnumSingle.INSTANCE;
Constructor constructor = EnumSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
//NoSuchMethodException: com.zunhui.single.EnumSingle.
EnumSingle instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
结果:
结果发现,报了一个不存在无参构造的异常,按理来说应该是报枚举不能被反射创建对象,那这是什么原因呢,我们接着探究,使用jad反编译工具将EnumSingle.class转为EnumSingle.java
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/zunhui/single/EnumSingle, name);
}
//======================
private EnumSingle(String s, int i)
{
super(s, i);
}
//======================
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
通过查看源码,我们发现枚举类确实不存在无参构造,而是存在一个有参构造,两个参数分别是String和int,那我们接着修改测试代码:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance = EnumSingle.INSTANCE;
//修改为获取有参构造
Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
//IllegalArgumentException: Cannot reflectively create enum objects
EnumSingle instance1 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
结果:
符合我们的预期,那就说明枚举类是一个特殊的类,通过有参构造实例化对象,且不能通过反射创建对象。