单例模式作为23种设计模式中最简单、最常用的一种,是需要一个java攻城狮掌握的,目前实现单例模式有三种方式:懒汉模式、饿汉模式、枚举方式
实现单列模式大至分为三个步骤,1.构造器私有化 2.实例化变量私有化 3.返回对象的方法
一:懒汉模式
懒,顾名思义就是对象我都懒得创建。Talk is cheap, show me the code!
1.0懒汉模式:
public class Lazy {
//构造器私有化
private Lazy(){}
private static Lazy instance;
//返回对象的方法
public static Lazy getInstance(){
if (null == instance){
instance = new Lazy();
return instance;
}
return instance;
}
}
创建测试类测试:
class Test{
public static void main(String []args){
System.out.println(Lazy.getInstance());
System.out.println(Lazy.getInstance());
}
}
输出结果:
single.Lazy@30dae81
single.Lazy@30dae81
但是这种模式在并发情况下会出现问题,当两个线程同时调用了getInstance()方法,此时instance为null,这时两个线程都会创建不同的Lazy对象,所以做出以下测试:
class Test{
public static void main(String []args){
for (int i = 0; i < 100; i++) {
new Thread(new MyThread()).start();
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Lazy.getInstance());
}
}
测试结果:
single.Lazy@2770f418
single.Lazy@dc7a4fc
single.Lazy@6d5af378
single.Lazy@6d5af378
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
single.Lazy@dc7a4fc
...............
避免出现上述情况,我们可以使用synchronized关键字。可以在方面前面加static synchronized Lazy getInstance()
,这样是能实现要求的,但是这样会造成每一次创建对象的时候都要等待上一个结束,在用户高峰期会造成效率极低。所以在这里我们使用同步代码块
2.0懒汉模式:
public class Lazy {
//构造器私有化
private Lazy(){}
private static Lazy instance;
//返回对象的方法
public static Lazy getInstance() {
if (null == instance) {
//位置x
synchronized (Lazy.class){
if (null == instance){
instance = new Lazy();
return instance;
}
}
}
return instance;
}
}
这就是著名的double-cheking。在同步代码块里面再次检测是否已经创建了对象是避免一个线程在位置x处等待上一个线程完成,避免重复创建对象。
二:饿汉模式
跟懒汉模式相反,对象我都给你创建好。Talk is cheap, show me the code!
饿汉模式1.0
public class Lazy {
//构造器私有化
private Lazy(){}
//创建对象并私有化
private static Lazy instance = new Lazy();
//返回对象的方法
public static Lazy getInstance() {
return instance;
}
}
这个是线程安全的,只要加载了Lazy类,就返回该对象的引用,但是在大环境中这是不友好的,会创建过多的对象
在这里每一次加载Lazy类的时候就创建了该对象,容易造成资源的浪费,可以使用静态内部类实现懒加载进行优化
饿汉模式2.0
public class Lazy {
//构造器私有化
private Lazy(){}
private static class Get{
private static Lazy instance = new Lazy();
}
//返回对象的方法
public static Lazy getInstance() {
return Get.instance;
}
}
JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心多线程并发问题,而且static对象只会被加载一次,所以每次返回的对象还是第一次返回的对象,实现了饿汉模式懒加载
但是上面的懒汉模式和饿汉模式还是可以通过反射、序列化、克隆进行破解
1.通过反射破解:
class Test{
public static void main(String []args) throws Exception{
System.out.println(Lazy.getInstance());
//使用反射得到构造器
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
//关闭检查,访问私有资源
constructor.setAccessible(true);
//创建对象
Lazy lazy = constructor.newInstance();
System.out.println(lazy);
}
}
输出结果
single.Lazy@30dae81
single.Lazy@1b2c6ec2
2.通过序列化破解
class Test{
public static void main(String []args) throws Exception{
Lazy lazy = Lazy.getInstance();
System.out.println(lazy);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("lazy.pkl"));
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("lazy.pkl"));
objectOutputStream.writeObject(lazy);
Lazy lazy1 = (Lazy) objectInputStream.readObject();
System.out.println(lazy1);
}
}
输出结果
single.Lazy@30dae81
single.Lazy@4ccabbaa
在这里为什么会不相等呢,任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例
3.使用克隆进行破解
public class Lazy implements Serializable,Cloneable {
private static boolean flag = true;
//构造器私有化
private Lazy(){
if (flag){
flag = false;
} else {
System.out.println("单例模式正在被攻击");
}
}
//创建对象并私有化
private static class Get{
private static Lazy instance = new Lazy();
}
//返回对象的方法
public static Lazy getInstance() {
return Get.instance;
}
private Object readResolve(){
return Get.instance;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试:
class Test{
public static void main(String []args) throws Exception{
Lazy lazy = Lazy.getInstance();
Lazy lazy1 =(Lazy) lazy.clone();
System.out.println(lazy);
System.out.println(lazy1);
}
}
运行结果:
single.Lazy@30dae81
single.Lazy@1b2c6ec2
那么既然有破解方式那就有反破解方式:
1.反反射破解方式:
public class Lazy {
private static boolean flag = true;
//构造器私有化
private Lazy(){
if (flag){
flag = false;
} else {
System.out.println("单例模式正在被攻击");
}
}
//创建对象并私有化
private static class Get{
private static Lazy instance = new Lazy();
}
//返回对象的方法
public static Lazy getInstance() {
return Get.instance;
}
}
测试结果:
single.Lazy@30dae81
单列模式正在被攻击!
single.Lazy@1b2c6ec2
哈哈……
2.反序列化破解:
public class Lazy implements Serializable {
//构造器私有化
private Lazy(){}
//创建对象并私有化
private static class Get{
private static Lazy instance = new Lazy();
}
//返回对象的方法
public static Lazy getInstance() {
return Get.instance;
}
private Object readResolve(){
return Get.instance;
}
}
测试结果:
single.Lazy@30dae81
single.Lazy@30dae81
在这里,readResolve()方法跟对象的序列化相关(这样倒是解释了为什么 readResolve方法是private修饰的)。
3.反克隆破解
之所以会被克隆破解,是因为实现了Cloneable接口,并重写了方法,当你实现了这个接口时就要考虑到他的弊端,所以防止破解,就是不实现该接口呗!
三:枚举方式实现单例模式:
1.枚举单例的定义:
public enum Instance {
INSTANCE;
public Instance getInstance(){
return INSTANCE;
}
}
其实编译之后相当于:
public final class Instance extends Enum<Instance>{
public static final Instance INSTANCE;
public static Instance[] values();
public static Instance valueof(String s);
static {};
}
1.测试反射破解:
public enum Instance {
INSTANCE;
public Instance getInstance(){
return INSTANCE;
}
public static void main(String []args) throws Exception{
Instance instance1 = Instance.INSTANCE;
Instance instance2 = Instance.INSTANCE;
System.out.println("正常情况下是否相同:" + (instance1 == instance2));
Constructor<Instance> constructor = Instance.class.getDeclaredConstructor();
constructor.setAccessible(true);
Instance instance3 = constructor.newInstance();
System.out.println("反射情况下:" + (instance3 == instance1));
}
}
运行结果:
正常情况下是否相同:true
Exception in thread "main" java.lang.NoSuchMethodException: single.Instance.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at single.Instance.main(Instance.java:14)
因为一个类标注为枚举时,实际上就是继承了Enum类,这是一个抽象类,没有无参构造方法,只有String.class,int.class)的构造器,在Enum源码中这两个参数是name和ordial两个属性,所以抛出异常。如果调用父类的构造方法时也会抛出异常,因为在Constructor类的newInstance方法源码中,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
2.测试序列化破解:
public enum Instance {
INSTANCE;
public Instance getInstance(){
return INSTANCE;
}
public static void main(String []args) throws Exception{
Instance instance1 = Instance.INSTANCE;
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test"));
objectOutputStream.writeObject(instance1);
Instance instance3 = (Instance) objectInputStream.readObject();
System.out.println("反射情况下:" + (instance3 == instance1));
}
}
结果:
反射情况下:true
测试成功