单例模式
1、定义
单例模式就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其实例的方法。
构造方法私有化
2、实现方式
2.1、饿汉式
顾名思义:饿汉,非常饿,该类被加载的时候就会马上实例化,一刻也不会等待
构造私有化
对象私有化
公共静态getInstance方法用来获取对象
public class Hungry {
// 构造器私有化是为了方式new 去创建对象
private Hungry(){
System.out.println(Thread.currentThread().getName()+"成功了");
}
private static final Hungry HUNGRY = new Hungry();
// 暴露公共方法是为了返回对象
public static Hungry getInstance(){
return HUNGRY;
}
// 多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Hungry.getInstance();
}).start();
}
}
}
优点:
- 写法简单,在类装载的时候就实现了实例化。避免了线程同步的问题
- 调用速度快,因为在类装载的时候对象已经创建完毕了
缺点:
- 因为在类装载的时候就已经完成了实例化,如果这个类一直用不到,造成了内存占用,以及实例的浪费
2.2、懒汉式(线程不安全)
顾名思义:懒汉比较懒,在类被加载的时候什么也不会干,只有当需要他的时候才会被实例化
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"成功了");
}
// 在类加载的时候先不创建对象
private static Lazy LAZY;
public static Lazy getInstance(){
// 当调用懒汉的时候才会去创建一个对象并返回
if(LAZY == null){
LAZY = new Lazy();
}
return LAZY;
}
// 多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LAZY.getInstance();
}).start();
}
}
}
运行了多次,发现多线程状态下不安全
优点:
- 解决了饿汉模式的缺点
缺点:
- 只能在单线程下使用,线程不安全
- 多线程状态下,一个线程执行到if(LAZY == null),还没来得及往下执行,另外一个线程也通过了这个判断语句,就会产生多个实例(上边的演示结果)
2.3、DCL懒汉式(双重检查)
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"成功了");
}
// volatile关键字的所用是防止指令重排
private volatile static Lazy LAZY;
// 同步代码块 更推荐使用 效率更高
public static Lazy getInstance(){
// 如果对象为空,再让线程去new对象
if(LAZY == null){
// 设置线程同步,每次只让一个线程去访问
synchronized (Lazy.class){
if(LAZY == null){
LAZY = new Lazy();
/*
因为new Lazy()的步骤有三步,且这三步不是原子性操
1、分配内存空间
2、执行构造方法,初始化对象
3、将对象指向分配的内存空间
在这三步过程中,期望执行123,但在极端情况下有一个线程A可能执行的步骤是132,
在执行步骤3的时候,A线程已经将LAZY对象的地址指向了内存中,
如果此时有一个有一个线程B调用了LAZY对象那么此时LAZY对象是空的可能会引发异常
所以需要给对象加volatile关键字防止指令重排
*/
}
}
}
return LAZY;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LAZY.getInstance();
}).start();
}
}
}
优点
- 解决了线程同步的问题
2.4、静态内部类
public class Holder {
private Holder(){
}
//通过内部类来获取对象
public static synchronized Holder getInstance(){
return Inner.HOLDER;
}
// 在内部类中创建Holder类的实例
public static class Inner{
private static final Holder HOLDER = new Holder();
}
}
优点:
- 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。
2.5、枚举类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
EnumSingle instance = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println("instance的hashCode" + instance.hashCode());
System.out.println("instance2的hashCode" + instance2.hashCode());
}
}
结果
instance的hashCode460141958
instance2的hashCode460141958
优点:
- 不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
3、注意点
懒汉式是我们一般最想要的东西,但是反射可以去破坏私有方法和属性,导致单例模式失效,下面我去演示一下
通过DCL懒汉获取一个对象后,通过反射再创建一个新的对象,查看效果
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName()+"成功了");
}
private static Lazy LAZY;
public static Lazy getInstance(){
if(LAZY == null){
synchronized (Lazy.class){
if(LAZY == null){
LAZY = new Lazy();
}
}
}
return LAZY;
}
public static void main(String[] args) throws Exception{
// 获取到Lazy类的实例
Lazy instance = Lazy.getInstance();
// 虽然在方法getInstance中已经设置了线程同步
// 通过反射来获取Lazy类中的空构造方法
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
// 无视私有属性的构造器
declaredConstructor.setAccessible(true);
// 通过构造器再去创建一个对象
Lazy instance1 = declaredConstructor.newInstance();
// 比较两个对象的hashCode
System.out.println("instance的hashCode:" + instance.hashCode());
System.out.println("instance1的hashCode:" + instance1.hashCode());
}
}
结果:
main成功了
main成功了
instance的hashCode:460141958
instance1的hashCode:1163157884
发现两个对象的hashCode并不一致,你可能会想到了,我在无参构造里边判断LAZY对象是否为空,如果不为空抛出一个运行期异常并且去加一个锁不就好了么,确实可以。来下边修改无参构造
private Lazy(){
// System.out.println(Thread.currentThread().getName()+"成功了");
synchronized (Lazy.class){
if(LAZY != null){
throw new RuntimeException("反射创建对象异常");
}
}
}
结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.single.Lazy.main(Lazy.java:42)
Caused by: java.lang.RuntimeException: 反射创建对象异常
at com.single.Lazy.<init>(Lazy.java:15)
... 5 more
确实好用,但是如果我要是两个对象都是用反射来进行创建呢?修改main方法
public static void main(String[] args) throws Exception{
// 虽然在方法getInstance中已经设置了线程同步
// 通过反射来获取Lazy类中的空构造方法
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
// 无视私有属性的构造器
declaredConstructor.setAccessible(true);
// 通过构造器再去创建两个个对象
Lazy instance = declaredConstructor.newInstance();
Lazy instance1 = declaredConstructor.newInstance();
// 比较两个对象的hashCode
System.out.println("instance的hashCode:" + instance.hashCode());
System.out.println("instance1的hashCode:" + instance1.hashCode());
}
结果:
instance的hashCode:460141958
instance1的hashCode:1163157884
是不是又被创建出来了,此时你可能又会说,我在类中定义一个非本类的静态加密变量,在构造中用来控制是否生成对象不就可以了嘛,这个想法也可以,测试一下,修改Lazy类
public class Lazy {
// 定义一个加密变量
private static boolean PORTERDONGS23W221GH21V = true;
private Lazy(){
synchronized (Lazy.class){
/*
if(LAZY != null){
throw new RuntimeException("反射创建对象异常");
}
*/
if(PORTERDONGS23W221GH21V){
PORTERDONGS23W221GH21V = false;
}else{
throw new RuntimeException("反射创建对象异常");
}
}
}
private static Lazy LAZY;
public static Lazy getInstance(){
if(LAZY == null){
synchronized (Lazy.class){
if(LAZY == null){
LAZY = new Lazy();
}
}
}
return LAZY;
}
public static void main(String[] args) throws Exception{
// 虽然在方法getInstance中已经设置了线程同步
// 通过反射来获取Lazy类中的空构造方法
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
// 无视私有属性的构造器
declaredConstructor.setAccessible(true);
// 通过构造器再去创建两个个对象
Lazy instance = declaredConstructor.newInstance();
Lazy instance1 = declaredConstructor.newInstance();
// 比较两个对象的hashCode
System.out.println("instance的hashCode:" + instance.hashCode());
System.out.println("instance1的hashCode:" + instance1.hashCode());
}
}
结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.single.Lazy.main(Lazy.java:50)
Caused by: java.lang.RuntimeException: 反射创建对象异常
at com.single.Lazy.<init>(Lazy.java:24)
... 5 more
确实好用,但是,俗话说的好,道高一尺,魔高一丈,这样我还是可以使用反射去破解,修改main方法
public static void main(String[] args) throws Exception{
// 虽然在方法getInstance中已经设置了线程同步
// 通过反射来获取Lazy类中的空构造方法
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
// 获取私有属性
Field field = Lazy.class.getDeclaredField("PORTERDONGS23W221GH21V");
// 无视私有属性的构造器
declaredConstructor.setAccessible(true);
// 通过构造器再去创建两个个对象
Lazy instance = declaredConstructor.newInstance();
// 将属性的值再次改为true
field.set(instance,true);
Lazy instance1 = declaredConstructor.newInstance();
// 比较两个对象的hashCode
System.out.println("instance的hashCode:" + instance.hashCode());
System.out.println("instance1的hashCode:" + instance1.hashCode());
}
结果:
instance的hashCode:1163157884
instance1的hashCode:1956725890
可以看到,通过反射也可以忽视成员的私有属性,来实现破解,既然懒汉可以破解,那饿汉和内部类也可以破解,难道就没有什么解决办法了吗?对还有枚举类型。
可以看一下newInstance的源码
看到如果使用反射去操作枚举类型会抛出异常,下边使用枚举去测试一下,咱们先通过idea查看编译完后的EnumSingle.class
可以看到,在编译完成的枚举类中,有一个无参构造,好,下边使用反射去攻破一下,修改EnumSingle的main方法
public static void main(String[] args) throws Exception {
// 先获取类的构造函数
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
// 忽略私有属性
declaredConstructor.setAccessible(true);
// 通过构造创建对象
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(enumSingle.INSTANCE);
}
结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.single.EnumSingle.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.single.EnumSingle.main(EnumSingle.java:19)
你可能会说,这没问题啊,你上边也说了,不能使用反射破解枚举,但是注意一下,运行结果中提示的是找不到无参构造NoSuchMethodException: com.single.EnumSingle.<init>()
,而newInstance的异常是IllegalArgumentException("Cannot reflectively create enum objects");
,并不是一个异常,难道我们被idea给骗了,没有无参构造?或者是验证失败了?
咱们找到编译的class文件的路径,通过cmd命令执行javap查看具体信息
难道也被javap命令给骗了?下面我是用jad反编译工具来反编译一下EnumSingle.class,jad下载地址,将jad工具复制到生成的class文件目录,通过cmd执行命令
执行完毕之后,打开反编译后的EnumSingle.java,发现构造是有参的,并不是无参的
修改咱们的main方法,将通过反射获取构造修改一下
public static void main(String[] args) throws Exception {
// 先获取类的构造函数
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
// 忽略私有属性
declaredConstructor.setAccessible(true);
// 通过构造创建对象
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(enumSingle.INSTANCE);
}
结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.single.EnumSingle.main(EnumSingle.java:23)
这次就成功了!!!!
所以相对于饿汉式、DCL懒汉式、静态内部类、枚举,这几种使用方式,枚举是比较安全的,但是更推荐使用DCL懒汉式。此处使用反射来操作懒汉只是作为一个扩展。
4、使用场景
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
使用细节
- 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
- 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
- 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)