什么是单例模式?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式(如下面例子中的LazyMan01.getInstance),可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
意图在于保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决了一个全局使用的类频繁地创建与销毁的问题。
面试要是说起单例模式,那肯定就会提到饿汉式和懒汉式.笔试手写的两种单例模式看代码:
package com.qiu.single;
//饿汉式单例
public class Hungry {
//因为下面new的对象都是static静态的,所以饿汉式可能会浪费空间
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(){//getInstance获取实例
return hungry;
}
}
因为new的对象是静态的会和类一起加载,所以用到数组的时候就会浪费空间.
所以就出来了一个懒汉模式
代码:
package com.qiu.single;
public class LazyMen01 {
private LazyMen01(){
//私有的构造器
}
private static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
return lazyMen;
}
}
但是这个代码是有问题的,单线程下是可以的,但是如果在并发测试下呢?在多线程下呢?
实例代码:
package com.qiu.single;
import com.qiu.LazyMen;
public class LazyMen01 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMen01.getInstance();
}).start();
}
}
private LazyMen01(){
//私有的构造器
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
return lazyMen;
}
}
居然有五个线程!
而且每次的执行结果都不一样.所以我们应该要再想办法.这个时候就想到了要加一把锁但是存在在加锁之前就被线程拿到了,所以我们要做两次判断,加锁判断一次.先判断lazyMan01是否为空,如果为空,那就加锁.
相关代码:
package com.qiu.single;
import com.qiu.LazyMen;
public class LazyMen01 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMen01.getInstance();
}).start();
}
}
private LazyMen01(){
//私有的构造器
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
这种就叫双重检测锁模式的懒汉式单例(DCL懒汉式)
但是:
这种代码还是有问题的,这里就要谈到一个关键词叫Volatile是不是很熟悉?
看下这几行代码:
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
**lazyMen=new LazyMen01();**这行代码你觉得有没有问题呢?
答案是有的,因为这种new对象不是一个原子性操作.(关于原子性操作大家可以看我前面的博客有专门的解释过.这里不做深入探讨)
我们先说下new对象的一个底层具体执行过程:
1.分配内存空间
2.执行构造方法
3.把这个对象指向这个空间
但是这样会发生一个指令重排的现象比如说:
我们期望的是执行123
但是他由于不具有原子性操作有可能执行顺序为132.假设多线程下A线程没有问题
但是B线程来了,A线程已经指向这个对象空间了,但是B还是认为lazyMen不为空了,
则B线程会直接走return lazyMen
此时这个lazyMan还没有完成构造,导致这个空间是虚无的,
所以必须要加上Volatile形成一个完整的双重检测锁:
private volatile static LazyMen01 lazyMen;
这样就能保证他不会被指令重排了.
这是面试时,必须要说到的点.
有能力的还能有更骚的操作哦.
比如说:静态内部类实现:
代码:
package com.qiu.single;
//静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
但是:
这样还是不安全的:JAVA中有一种类叫做反射只要有反射存在任何代码都会变得不安全.任何关键字都是纸老虎!!
就拿上面最完整的DCL单例来说
代码:
package com.qiu.single;
import com.qiu.LazyMen;
public class LazyMen01 {
public class LazyMen01 {
public static void main(String[] args) {
LazyMen01 instance = LazyMen01.getInstance();
LazyMen01 instance = LazyMen01.getInstance();
}
private LazyMen01(){
//私有的构造器
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
LazyMen01 instance = LazyMen01.getInstance();
LazyMen01 instance = LazyMen01.getInstance();
正常情况下上面两个实例是相等的.然后我们用反射来破坏这个单例
package com.qiu.single;
import com.qiu.LazyMen;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMen01 {
public static void main(String[] args) throws Exception {
LazyMen01 instance1 = LazyMen01.getInstance();
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
private LazyMen01(){
//私有的构造器
System.out.println(Thread.currentThread().getName()+":ok");
}
private volatile static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
按照单例的说法,他们两个应该是一样的才对,但是反射做到了破坏了单例.
然后我们就要想我们能不能去解决这种破坏呢?
思路.类只有一个,如果我们在类上加一把锁呢?
代码:
package com.qiu.single;
import com.qiu.LazyMen;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMen01 {
public static void main(String[] args) throws Exception {
LazyMen01 instance1 = LazyMen01.getInstance();
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
private LazyMen01(){//私有的构造器
synchronized (LazyMen.class){
if (lazyMen!=null){
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
}
System.out.println(Thread.currentThread().getName()+":ok");
}
private volatile static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
由于反射是从无参构造器走的,所以我们在私有构造器上面加了一把锁.如果lazyMan不等于null,那说明已经创建了实例了.这时候抛出异常.这样就形成了三重检测了,避免了某一种反射的破坏.
synchronized (LazyMen01.class){
if (lazyMen!=null){
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
}
貌似这种反射破坏解决了,但是人总是聪明的**,魔高一尺道高一丈**.
假设两个对象都是用反射来new的呢?
package com.qiu.single;
import com.qiu.LazyMen;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMen01 {
public static void main(String[] args) throws Exception {
//LazyMen01 instance1 = LazyMen01.getInstance();
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
LazyMen01 instance1 = declaredConstructor.newInstance();
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
private LazyMen01(){//私有的构造器
synchronized (LazyMen01.class){
if (lazyMen!=null){
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
}
System.out.println(Thread.currentThread().getName()+":ok");
}
private volatile static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
LazyMen01 instance1 = declaredConstructor.newInstance();
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
就是说两个对象都是用反射new出来的,那么结果怎么样了呢?
居然还是出来了两个实例.这样单例模式又被破坏了
再解决:
在任何情况下,可以通过非当前类对象.设置一个信号值,比如说现在定义一个变量,将变量名字设置的十分复杂.
private static boolean qiuzhikang =false;
意思就是在synchronized同步的时候加一个判断,如果最开始时变量qiuzhikang
为false.那么执行判断后,变量qiuzhikang
就变为了true.
否则就抛出异常.
package com.qiu.single;
import com.qiu.LazyMen;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMen01 {
private static boolean qiuzhikang = false;
public static void main(String[] args) throws Exception {
//LazyMen01 instance1 = LazyMen01.getInstance();
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
LazyMen01 instance1 = declaredConstructor.newInstance();
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
private LazyMen01() {//私有的构造器
synchronized (LazyMen01.class) {
if (qiuzhikang == false) {
qiuzhikang = true;
} else
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
System.out.println(Thread.currentThread().getName() + ":ok");
}
private volatile static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
改变的代码:
private LazyMen01() {//私有的构造器
synchronized (LazyMen01.class) {
if (qiuzhikang == false) {
qiuzhikang = true;
} else
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
来看看结果:
确实是解决了问题.
如果说别有用心的人不通过反编译的情况下,他是找不到我们设置的这个关键字的.如果关键字经过加密处理就更安全了.
但是:
再厉害的加密也会有人解密!
方法:改变你的变量值
假设我找到了你设置的隐藏的变量.找到之后,拿到变量就可以搞破坏了,具体实现过程看代码.
package com.qiu.single;
import com.qiu.LazyMen;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class LazyMen01 {
private static boolean qiuzhikang = false;
public static void main(String[] args) throws Exception {
//LazyMen01 instance1 = LazyMen01.getInstance();
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Field qiuzhikang = LazyMen01.class.getDeclaredField("qiuzhikang");
qiuzhikang.setAccessible(true);
Constructor<LazyMen01> declaredConstructor = LazyMen01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
LazyMen01 instance1 = declaredConstructor.newInstance();
qiuzhikang.set(instance1,false);
LazyMen01 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
private LazyMen01() {//私有的构造器
synchronized (LazyMen01.class) {
if (qiuzhikang == false) {
qiuzhikang = true;
} else
throw new RuntimeException("不要试图使用反射来破坏异常!");
}
System.out.println(Thread.currentThread().getName() + ":ok");
}
private volatile static LazyMen01 lazyMen;
private static LazyMen01 getInstance(){//创建一个静态方法,有返回值的
if (lazyMen==null){
synchronized (LazyMen01.class){
if (lazyMen==null){//加了一个判断如果lazyMen是空的情况下,再去new对象
lazyMen=new LazyMen01();
}
}
}
return lazyMen;
}
}
改变的代码:
Field qiuzhikang = LazyMen01.class.getDeclaredField("qiuzhikang");
qiuzhikang.setAccessible(true);
qiuzhikang.set(instance1,false);
就是将信号量重新改成false.这样单例又又又被破坏了.
道高一尺,魔高一丈
那到底怎么解决反射呢?
现在我们只能通过源码了,点newinstance进入;
枚举是JDK1.5出来的,自带单例模式.
代码:
package com.qiu.single;
//enum是一个什么?本身也就是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class test{
public static void main(String[] args) {
EnumSingle instance1= EnumSingle.INSTANCE;
EnumSingle instance2= EnumSingle.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
}
源码说反射是不能破坏枚举的.那我们来尝试一下.
首先分析源码里面是有参构造还是无参构造.
package com.qiu.single;
import java.lang.reflect.Constructor;
//enum是一个什么?本身也就是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class test{
public static void main(String[] args) throws Exception {
EnumSingle instance1= EnumSingle.INSTANCE;
System.out.println(instance1);
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
//NoSuchMethodException: com.qiu.single.EnumSingle.<init>()
System.out.println(instance2);
}
}
一番操作之后发现:
NoSuchMethodException: com.qiu.single.EnumSingle.<init>()
但是这与我们源码中的不同.
throw new IllegalArgumentException("Cannot reflectively create enum objects");
探究失败!!!
不服气!再来!!Idea是个大骗子.
通过CMD指令javap -p EnumSingle.class
还是有空参构造器
这是时候我们就要找到一个更专业的工具!jad
将EnumSingle.class文件反编译成java文件.
转换完成.
枚举类型的最终反编译.
这里用的不是无参构造,而是一个有参构造器!!!
好的,知道有参之后,改代码.
将
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
改成了:
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
看结果:
确实证明了反射不能破坏枚举的单例.