单例模式
可以说是最简单的设计模式
当前进程确保一个类全局只有一个实例
优点:
- 单例模式在内存中只有一个实例,减少了内存开支
- 单例模式只生成一个实例,所以减少了系统的性能开销
- 单例模式可以避免对资源的多重占用
- 单例模式可以在系统设置全局的访问点
缺点:
- 单例模式一般没有接口,扩展很困难
- 单例模式不利于测试
- 单例模式与单一职责原则有冲突
什么情况下要用单例模式呢?
- 要求生成唯一序列号的环境
- 在整个项目中需要一个共享访问点或共享数据
- 创建一个对象需要消耗的资源过多
- 需要定义大量的静态常量和静态方法(如工具类)的环境
饿汉式(线程安全)
就是一上来就将对象加载进内存
package single;
public class Single {
private static Single single = new Single();
private Single(){
}
private static Single getInstance(){
return single;
}
}
如因此,不管我们的程序会不会用到,它都会在程序启动之初进行初始化。如果在程序中有大量的内存需要加载在1启动之初却用不到,这就会造成资源的浪费,所以就出现了懒汉式,用不到就不去加载。
懒汉式(线程不安全)
只有用到的时候才会加载,这就是懒加载。
package single;
public class LazyMan {
private static LazyMan lazyMan;
private static LazyMan getInstance(){
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
这种模式在单线程下是OK的,但是在并发多线程下就会出现问题。
例
package single;
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "ok");
}
private static LazyMan getInstance(){
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
结果理论上会生成个十个线程,打印出十个线程名,可是实际上只会出现一个或几个,会出现一些小问题。
DCL懒汉式
所以就有了双重效验锁模式,也叫DCL懒汉式
package single;
//懒汉式单例
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "ok");
}
//双重检测锁模式 DCL懒汉式
private static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
为什么要双重校验?
如果两个线程一起调用getInstance方法,并且都通过了第一次的判断lazyMan==null,那么第一个线程获取了锁,然后实例化了lazyMan,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了lazyMan。这就不符合我们的单例要求了。
但是,这里也存在一些问题,在if语句中 lazyMan = new LazyMan();并不是一个原子性操作,他会在底层(或者说内部)执行三个操作
- 给 lazyMan分配内存空间
- 执行 lazyMan 的构造方法初始化对象
- 把这个对象指向这个空间
这就有可能会造成指令重排的现象,也就是说,本来应该安装123的顺序去执行,但是他却是按照132或其他的顺序执行了,如果这是又有一个线程进来而lazyMan还没有分配完内存(或者是没有完成构造),这时就会引发问题,所以要在private volatile static LazyMan lazyMan;这里加上volatile用来防止指令重排这就成了完整的双重校验锁模式了
静态内部类
package single;
//静态内部类
public class Singles {
private Singles(){
}
private static Singles getInstance(){
return Inner.singles;
}
private static class Inner{
private final static Singles singles = new Singles();
}
}
静态内部类,不仅能实现懒加载而且线程安全还保持了指令优化的能力。
Singles类被装载时并不会立即实例化,而是在需要实例化的时候调用getInstance()方法,才会加载静态内部类Inner类,从而完成Singles的实例化。
使用反射破坏单例
但是
这也不是绝对的安全的,因为java中有个词叫:反射。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(instance);
}
输出结果:
mainok
mainok
single.LazyMan@677327b6
single.LazyMan@14ae5a5
可以清楚的看到这里两个对象是不一样的
那怎样防止反射呢?
我们可以在构造函数中在加一把锁
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不要使用反射来搞破坏!");
}
}
}
这就有了三把锁
可是如果都用反射来创建对象会怎么样呢?
那就会发现又正常建立对象了
single.LazyMan@677327b6
single.LazyMan@14ae5a5
那怎样解决呢?
这就可以使用一个变量来进行加密
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyMan {
private volatile static LazyMan lazyMan;
private static boolean flag = false;
private LazyMan(){
synchronized (LazyMan.class) {
if (flag == false){//当第二次创建对象的时候flag==true了就会执行else
flag = true;
}else {
throw new RuntimeException("不要使用反射来搞破坏!");
}
}
}
private static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan = declaredConstructor.newInstance();
LazyMan instance = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(instance);
}
}
现在假设我们知道了你加密的变量名字
我们来破坏他的权限
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan = declaredConstructor.newInstance();
//假设我们知道他的加密变量名
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);//解开私有权限
flag.set(lazyMan,false);//更改变量值
LazyMan instance = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(instance);
}
所以可以得出一个结论:道高一尺魔高一丈!
所以,现在我们看一下反射的原码,到底是什么。
newInstance的原码
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
到这里我们发现,if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);
是枚举类型就不会被反射,所以我们要看一下枚举到底长什么样可以不被反射,直接阅读原码,新建一个枚举类
package single;
public enum SingleEnum {
instance;
public SingleEnum getInstance(){
return instance;
}
}
然后在idea里找到这个枚举类的.class文件,可以看到他是有空参构造的,这是idea给的原码
package single;
public enum SingleEnum {
instance;
private SingleEnum() { /* compiled code */ }
public single.SingleEnum getInstance() { /* compiled code */ }
}
现在我们使用反射来试试
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class EnumTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingleEnum singleEnum = SingleEnum.instance;
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingleEnum enumTest = constructor.newInstance();
System.out.println(singleEnum);
System.out.println(enumTest);
}
}
然后你会发现我们被idea给骗了
这个错是说没有方法错误。
那idea骗了我们,我们就用命令行的方式进行查看
D:\idea-1\student\target\classes\single>javap -p Single.class
Compiled from “Single.java”
public class single.Single {
private static single.Single single;
private single.Single();
private static single.Single getInstance();
static {};
}
D:\idea-1\student\target\classes\single>
可以看出
这个和idea给出的是差不多的,cmd也骗了我们。
现在我们使用专业工具jad将.class文件反编译成.java文件,使用命令jad -sjava SingleEnum.class
SingleEnum.java文件如下
// 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 single;
public final class SingleEnum extends Enum
{
public static SingleEnum[] values()
{
return (SingleEnum[])$VALUES.clone();
}
public static SingleEnum valueOf(String name)
{
return (SingleEnum)Enum.valueOf(single/SingleEnum, name);
}
private SingleEnum(String s, int i)
{
super(s, i);
}
public SingleEnum getInstance()
{
return instance;
}
public static final SingleEnum instance;
private static final SingleEnum $VALUES[];
static
{
instance = new SingleEnum("instance", 0);
$VALUES = (new SingleEnum[] {
instance
});
}
}
现在修改代码
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class EnumTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingleEnum singleEnum = SingleEnum.instance;
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
SingleEnum enumTest = constructor.newInstance();
System.out.println(singleEnum);
System.out.println(enumTest);
}
}
结果如下,发现这才是我们想要的错误
本文只是学习时笔记,如有侵权请联系我删除