目录
恶汉式单例模式
只要是单例模式,先进行构造器的私有化,将无参构造方法加private进行私有
恶汉式是不管需不需要直接生成对象和开辟属性空间,容易造成资源的浪费
package com.ujiuye.single;
//恶汉式单例
public class Hungry {
//恶汉式会容易浪费空间,不管什么直接全部加载会造成资源的浪费
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
//最重要的思想是,构造器私有
private Hungry(){
}
//恶汉式是一上来就先把对象创建
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例模式(重点)
懒汉式正常情况下会经过判断进行生成对象,但当遇到多线程的时候会出现问题,通过synchronized加上类锁(成为DCL懒汉式)之后虽然能正常使用,但是在new lazyMan对象的这个过程其实会出现原子性的问题,就是会发生指令的重排序问题,所以可以在创建对象的时候加上volatile关键字进行防止重排序
volatile在Java并发编程中常用于保持内存可见性和防止指令重排序。内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态;防止指令重排:在基于偏序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率,但同时也引入了一些问题。
package com.ujiuye.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
//懒汉式单例
public class LazyMan {
private static boolean wangjiacheng = false;
//先构造器私有,不让别人new对象
private LazyMan(){
//为了解决反射的破坏需要加一把锁
//但是当,创建的两个对象都是是用反射来做,就又会被破坏
synchronized (LazyMan.class){
//通过加一个关键字 可以解决反射问题
if (wangjiacheng==false){
wangjiacheng=true;
}else {
throw new RuntimeException("不要试图用使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+"线程启动");
}
//为了解决原子性的问题要加上volatile这个关键字
private volatile static LazyMan lazyMan;
//双重检测锁模式的懒汉式单例,简称 DCL懒汉式
public static LazyMan getInstance(){
//通过加类锁来解决多线程问题
if (lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
//如果对象为空,则new对象
//但是这一步还是有问题,不是原子性操作
/*
* 会经过三步
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
* 正常情况是123
* 但是可能会132
* 当A线程完成了132,实际上空间里并没有值,当下一个B线程到来后会认为空间里已经有值了,会直接
* return这个lazyMan,返回一个空值
* */
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
// //单线程下可以实现,但是多线程并发下会出现问题,会多次执行,需要加锁
// public static void main(String []args){
// for (int i = 0; i < 10; i++) {
// new Thread(()->{
// LazyMan.getInstance();
// }).start();
// }
// }
//反射
public static void main(String[]args) throws Exception {
//LazyMan instance = LazyMan.getInstance();
//继续破坏关键字,来测试
Field wangjiacheng = LazyMan.class.getDeclaredField("wangjiacheng");
wangjiacheng.setAccessible(true);
//空参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(String.class,int.class);
//设置权限,无视private私有的构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance1 = declaredConstructor.newInstance();
wangjiacheng.set(instance1,false);
LazyMan instance2 = declaredConstructor.newInstance();
//System.out.println(instance);
System.out.println(instance1);
System.out.println(instance2);
}
}
静态内部类单例模式
package com.ujiuye.single;
import java.lang.reflect.Constructor;
//静态内部类单例模式
public class Holder {
//只要是单例模式都需要构造器私有
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
//创建静态内部类
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
单例模式虽然可以通过加锁和私有构造器的方法来实现多重保护,但是由于反射的存在,还是会不安全,所以又了枚举的产生
枚举类
package com.ujiuye.single;
import java.lang.reflect.Constructor;
//枚举类 jdk1.5之后出现的,本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception{
EnumSingle instance = EnumSingle.INSTANCE;
EnumSingle instance1 = EnumSingle.INSTANCE;
System.out.println(instance.equals(instance1));
//可以看出枚举是一个自带单例模式的结构,如果通过反射来破坏它呢?
//先拿到他的class,测试一下他的无参构造方法
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
//破坏他的私有权限
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
//但是居然报错,说明枚举类里面没有这个空参构造方法
//Exception in thread "main" java.lang.NoSuchMethodException
System.out.println(instance1.equals(instance2));
}
}
通过我的测试发现,枚举类可以避免反射带来的问题,比较安全,但是枚举的源码显示提供了无参构造方法,但经过我的测试我发现其实并没有,反编译后发现提供了有参构造方法,两个参数分别为String和int
idea自带的反编译工具
windows系统可以直接win+R打开dos窗口
输入javap -p -类名.class进行反编译
MAC电脑需要通过终端先cd进入目标类的文件夹下,然后通过javac 类名.java进行编译
然后通过javap -p -类名.class进行反编译
setAccessible
这个关键字可以破坏private构造器的私有权限,反射来破坏我们的单例模式
反射(Reflection)
通过反射可以获取任何类的内部信息,并能直接操作任何对象的内部属性和方法
类加载完成之后,在堆内存的方法区里会产生一个Class类型的对象,并且一个类只有一个Class类对象,这个对象中包含了这个类里的完整的结构信息,我们可以通过这个对象看到类的结构,这就是反射的含义
反射相关的API
java.lang.Class 代表一个类
java.lang.reflect.Method 代表类的方法
java.lang.reflect.Field 代表类的成员变量
java.lang.reflect.Constructor 代表类的构造方法
等等,先掌握这四个就够用了
反射之前要先获取Class的对象
获取Class实例的三种方式
1.通过类名.class的方式,最安全常用
2.通过对象.getClass()方法
3.通过Class类的静态方法forName()获取,可能会抛出一个异常
class.forName(类的全路径);
4.基本类型的包装类都有一个Type属性
例如:Integer.TYPE;也是返回一个Class对象,是int
5.获取类的父类类型
Class实例.getSuperClass()这个方法可以获取Class实例的父类类型
哪些类型有Class对象
1.class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类;
2.interface:接口
3.[]:数组
4.enum:枚举
5.annotation:注解@interface
6.primitive type:基本数据类型
7.void
首先执行方法区里的东西,执行的时候会创建一个Class对象放入堆中,然后进行栈中的main方法,会对变量进行一个初始化赋值,然后会new 对象,放入堆中,这个对象会去找他的Class对象,然后Class对象会重新给这个变量赋值,返回到栈中
什么时候会发生类的初始化
类的主动引用
类的主动引用一定会发生类的初始化,有以下几种情况
1.当虚拟机启动,先初始化main方法所在的类
2.new一个类的对象
3.调用类的静态成员(除了final定义的),静态方法
4.使用java.lang.reflect包的方法对类进行反射
5.当初始化一个类,如果他的父类没有被初始化,则会先初始化他的父类
类的被动引用
类的被动引用不会发生类的初始化,有以下几种情况
1.当访问一个静态域的时候,只有真正声明这个域的类才会被初始化,如当通过子类调用父类的静态变量, 不会导致子类初始化
2.通过数组定义类引用,不会出发类的初始化
3.引用常量不会触发类的初始化
类加载器的作用
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
常见的几种类加载器
package com.ujiuye.single;
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception{
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取系统类加载器的父类加载器-->扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
//获取扩展类加载器-->根加载器(C/C++能获取)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
//测试当前类是哪个加载器
ClassLoader classLoader = Class.forName("com.ujiuye.single.ClassLoaderDemo").getClassLoader();
System.out.println(classLoader);
//测试jdk内部的类是谁加载的,根加载器加载的,底层用C写的,所以找不到为null
ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader1);
//如何获取系统类加载器可以加载的路径
System.out.println(System.getProperty("java.class.path"));
}
}
依靠反射动态的获取对象
通过newInstance方法获取对象
先获得类的Class对象,通过对象.newInstance()方法可以创建一个对象
1.本质上是调用了类的无参构造器
2.类的构造器的访问权限要足够
通过构造器创建对象
首先获取类的Class对象,通过对象.getDeclaredConstructor(参数类型.class)方法可以获取类的构造器
然后通过构造器.newInstance(参数)方法
invoke方法的作用是激活有两个参数
先通过class对象拿到方法
Method method= class对象.getMethod("方法名字","参数的.class")
然后method.invoke("用这个方法的对象","方法参数赋的值");
setAccesssible的作用