文章目录
单例模式
饿汉式单例模式:
1、普通方式
package SingleMode;
//饿汉式单例
public class HungryMode {
private HungryMode(){}
private final static HungryMode hunMode=new HungryMode();
public static HungryMode getHunMode(){
return hunMode;
}
}
2、静态代码块进行实例
package SingleMode;
//饿汉式单例
public class HungryMode {
private final static HungryMode hunMode;
static {
hunMode=new HungryMode();
}
private HungryMode(){}
public static HungryMode getHunMode(){
return hunMode;
}
}
问题:消耗内存。如果对象本身就很大,不使用时就创建消耗资源。
比如:
package SingleMode;
//饿汉式单例
public class HungryMode {
byte[] data=new byte[1024*1024];
byte[] data1=new byte[1024*1024];
byte[] data2=new byte[1024*1024];
byte[] data3=new byte[1024*1024];
private HungryMode(){}
private final static HungryMode hunMode=new HungryMode();
public static HungryMode getHunMode(){
return hunMode;
}
}
创建对象时的四个data对象将会消耗大量资源。
问题存在解决问题:懒汉式单例
懒汉式单例模式
原始模式(重重漏洞)
单线程OK
package SingleMode;
// 懒汉式单例
public class LazyMode {
private LazyMode(){}
private static LazyMode lazyMode;
public static LazyMode getLazyMode(){
if (lazyMode==null)lazyMode=new LazyMode();
return lazyMode;
}
}
多线程测试:
package SingleMode;
// 懒汉式单例
public class LazyMode {
private LazyMode(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMode lazyMode;
public static LazyMode getLazyMode(){
if (lazyMode==null)lazyMode=new LazyMode();
return lazyMode;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMode.getLazyMode();
}).start();
}
}
}
结果:
![image-20221026151855081](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/c3c5086f3fa3b38d099718cc81048ead.png)
显然不行。
问题1:多线程不安全,解决方式加锁
方式一:
方法加锁:
package SingleMode;
// 懒汉式单例
public class LazyMode {
private LazyMode(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMode lazyMode;
public static synchronized LazyMode getLazyMode(){
if (lazyMode==null)lazyMode=new LazyMode();
return lazyMode;
}
}
结果:线程安全
分析:每次调用方法时都要为同步付出代价,而我们实际要同步的是lazyman对象。
问题:性能差,抛弃。
方式二:
双重检查机制:
package SingleMode;
// 懒汉式单例
public class LazyMode {
private LazyMode(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMode lazyMode;
public static synchronized LazyMode getLazyMode(){
if (lazyMode==null){
synchronized (LazyMode.class){
if (lazyMode==null){
lazyMode=new LazyMode();
}
}
}
return lazyMode;
}
}
结果:线程安全,延迟加载,效率较高。
分析:具体化解决问题,在创建对象之前才进行同步。
问题:极端概率出错,lazyMode=new LazyMode();不是原子性操作。
解析:lazyMode=new LazyMode();过程
1、分配内存空间(地址)。
2、执行构造方法,创建对象。
3、将对象指向内存空间(地址)。
编译器为了优化执行过程,可能会进行指令重排序(将以上三步顺序打乱)。
问题发生原因:
A线程执行:132
B线程进入方法时,A执行到3,未完成构造。
此时B return对象使用,遇到空指针。
解决问题:lazyman对象加volatile关键字。
优化:
package SingleMode;
// 懒汉式单例
public class LazyMode {
private LazyMode(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static volatile LazyMode lazyMode;
public static synchronized LazyMode getLazyMode(){
if (lazyMode==null){
synchronized (LazyMode.class){
if (lazyMode==null){
lazyMode=new LazyMode();
}
}
}
return lazyMode;
}
}
到此线程安全问题解决,优化完毕。
回想一下静态内部类:
第一:JVM在类初始化时有一个过程,为了保证类在多线程下不被多次加载。在类加载的初始化期间,JVM会获取一个锁,这个锁会同步多个线程对同一个类的初始化。
(这不就天然解决了我们所要的单例!!!)
静态内部类实现:
package SingleMode;
public class LazyMode1 {
private LazyMode1(){
System.out.println(Thread.currentThread().getName()+" get instance ok ");
}
private static class singleInstance{
private static final LazyMode1 instance=new LazyMode1();
}
public static LazyMode1 getInstance(){
return singleInstance.instance;
}
// 测试代码:
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
LazyMode1.getInstance();
}).start();
}
}
}
测试结果:
![image-20221112163947411](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/ae373d15c6cd3cfa0939554ca3fb0137.png)
始终只有一个线程成功创建对象。单例成功!!!
总结:
1.静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。
(静态内部类是一个比较好的解决方式)
回到双重检查机制,但它是真的安全了吗??????
有一个问题出现了,java中的反射机制提供了另外的创建对象方式。
方式:通过单例对象去得到,修改构造器。
测试:
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
LazyMode lazyMode = LazyMode.getLazyMode();
//获取构造器
Constructor<? extends LazyMode> declaredConstructor = lazyMode.getClass().getDeclaredConstructor(null);
//构造器公开
declaredConstructor.setAccessible(true);
//构造器创建对象
LazyMode lazyMode1 = declaredConstructor.newInstance();
// 比较两个对象
System.out.println(lazyMode==lazyMode1);
}
结果:
![image-20221112170321451](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/c43bc8f0d10d9925e2ab3b23e246f3c7.png)
仅仅在单线程下就成功破坏了单例!
以上方式都破坏了单例,又该怎样解决呢???
思路:创建多个对象的肯定会走多次构造器,我们可不可以修改构造器,让构造方法只调用一次,下一次调用时抛出异常?
改进代码:
private LazyMode(){
if (lazyMode!=null){
throw new RuntimeException("不可以调用第二次哦!");
}
}
再次测试:
![image-20221112171038469](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/5b1ecbe8ec6b9318e1d90b5d2a0541a3.png)
明显我们成功了!!!
但是这里去检测的前提是调用方法实例化了类中给的单例对象,所有可以通过判断单例对象是否为空来控制构造方法的调用。
思路:
问题出现了,我们一开始就不用类中的getLazyMode方法去获取单例对象,此时lazymode将一直为null;
我们是不是可以一直调用构造器了???
实践:
class test{
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//获取构造器
Constructor<? extends LazyMode> declaredConstructor = LazyMode.class.getDeclaredConstructor(null);
//构造器公开
declaredConstructor.setAccessible(true);
//构造器创建对象
LazyMode lazyMode1 = declaredConstructor.newInstance();
LazyMode lazyMode2 = declaredConstructor.newInstance();
// 比较两个对象
System.out.println(lazyMode2==lazyMode1);
}
}
结果:
![image-20221112172743395](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/ddcbfed07fcfd3b58055affc27a02772.png)
可以看到单例又又又被破坏了!!!
新的思路
通过一个类内部变量控制构造函数只能使用一次
实践:
private static boolean lbcsjdbchidh212=false;
private LazyMode(){
if (!lbcsjdbchidh212){
lbcsjdbchidh212=true;
}else {
throw new RuntimeException("不可以调用第二次哦!");
}
}
测试结果:
![image-20221112174403845](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/1a40d1e34dfbec98ce801714135cbc5e.png)
成功阻止了多个对象创建!
新的思路
新的问题来了,反射可以修改字段值,前提获取变量名称。哪怕我们变量名称通过了加密,始终会有解密然后获取字段名称。
实践:
class test{
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//获取到变量
Field lbcsjdbchidh212 = LazyMode.class.getDeclaredField("lbcsjdbchidh212");
//修改变量权限为public
lbcsjdbchidh212.setAccessible(true);
//获取构造器
Constructor<? extends LazyMode> declaredConstructor = LazyMode.class.getDeclaredConstructor(null);
//构造器公开
declaredConstructor.setAccessible(true);
//构造器创建对象
LazyMode lazyMode1 = declaredConstructor.newInstance();
//修改变量值
lbcsjdbchidh212.set(lazyMode1,false);
// 创建第二个变量
LazyMode lazyMode2 = declaredConstructor.newInstance();
// 比较两个对象
System.out.println(lazyMode2==lazyMode1);
}
}
结果:
![image-20221112175227953](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/fdee611e6ad450403b7a3f07327a1e33.png)
很好,我们又破坏了单例
结论:总是相互对抗的,单例总是有办法破解的
观察源码时的问题
此时我们应该仔细去观察源码,对症下药
观察后发现,反射中创建对象的newInstance方法源码有一个问题:
![image-20221112175617616](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/be912375e1950939f5a678a504bd68a8.png)
可以看到枚举类是不能使用newInstance方法的
开始测试一下枚举是否能使用newIntance方法
package SingleMode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
single;
public SingleEnum getSingle(){
return single;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
SingleEnum single = SingleEnum.single;
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
SingleEnum single1 = declaredConstructor.newInstance();
System.out.println(single==single1);
}
}
结果:
明显反射破坏枚举失败,
但是抛出的异常居然是 java.lang.NoSuchMethodException,提示没有这个构造方法。
我们想看到的不是:
![image-20221112181216146](https://typora-cos-1313444299.cos.ap-nanjing.myqcloud.com/d78c86d33dad254cf70f1c8c805eb38c.png)
可是构造方法不重载不就是默认的无参构造吗?
why???
我们去反编译这个枚举类,查看源码,真的没有无参构造吗?
发现是无参构造的呀!
会不会我们没有看到真正的源码?
我们这里使用了反编译工具jad进行反编译查看源码
完成后我们应该算是看到了源码了
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SingleEnum.java
package SingleMode;
public final class SingleEnum extends Enum
{
public static SingleEnum[] values()
{
return (SingleEnum[])$VALUES.clone();
}
public static SingleEnum valueOf(String s)
{
return (SingleEnum)Enum.valueOf(SingleMode/SingleEnum, s);
}
private SingleEnum(String s, int i)
{
super(s, i);
}
public SingleEnum getSingle()
{
return single;
}
public static final SingleEnum single;
private static final SingleEnum $VALUES[];
static
{
single = new SingleEnum("single", 0);
$VALUES = (new SingleEnum[] {
single
});
}
}
明显发现问题所在,源码里真的没有无参构造!!!!!
构造方法为:
private SingleEnum(String s, int i)
{
super(s, i);
}
接下来我们获取这个构造器,继续尝试反射构造枚举对象
package SingleMode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
single;
public SingleEnum getSingle(){
return single;
}
}
class Test1{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
SingleEnum single = SingleEnum.single;
//获取参数为(String,int)的构造器
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
SingleEnum single1 = declaredConstructor.newInstance();
System.out.println(single==single1);
}
}
结果:
终于是我们想要的结果了
看来IDE欺骗了我们!