代理模式PROXY
静态代理
定义一个代理规范,规定代理和目标对象实现同样的方法
举个例子,银行柜员和银行都要有取钱的方法,我们才能通过银行柜员去取银行的钱
public interface Proxy {void withdraw();
}
public class Bank implements Proxy{@Overridepublic void withdraw(){System.out.println("银行正在为你取钱");}
}
public class Stuff implements Proxy{Bank bank;@Overridepublic void withdraw() {if(bank == null){bank = new Bank();}System.out.println("银行柜员正在为你取钱");bank.withdraw();System.out.println("银行柜员取钱完成");}
}
public class User {public static void main(String[] args) {Stuff stuff = new Stuff();stuff.withdraw();}
}
动态代理
首先我们编写一个目标类,也就是真正干活的类
public class TrainStation implements ProxyMethod {public TrainStation() {}@Overridepublic void sellTicket() {System.out.println("火车站正在出票");}@Overridepublic boolean refund() {System.out.println("火车站正在退票");return true;}
}
然后定义一个接口,规定我们想要代理的方法
public interface ProxyMethod {void sellTicket();boolean refund();}
编写代理工具类,负责生成代理对象
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;public class ProxyUtil {public static ProxyMethod creatProxy(TrainStation trainStation) {ProxyMethod proxyMethod = (ProxyMethod) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),new Class[]{ProxyMethod.class},new InvocationHandler() {@Override// 当我们使用代理对象public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getName().equals("sellTicket")) {System.out.println("代理正在买票");}if (method.getName().equals("refund")) {System.out.println("代理正在退票");}Object res = method.invoke(trainStation, args);if (method.getName().equals("sellTicket")) {System.out.println("代理正在出票");}if (method.getName().equals("refund")) {System.out.println("代理正在退票");}return res;}});return proxyMethod;}
}
最后client测试
public class Client {public static void main(String[] args) {TrainStation trainStation = new TrainStation();ProxyMethod p = ProxyUtil.creatProxy(trainStation);p.sellTicket();boolean b = p.refund();System.out.println(b);}
}
动态代理分为两种一种是jdk代理
JDK动态代理是Java标准库中提供的一种代理方式,它可以在运行时动态生成一个代理对象,代理对象实现和原始类一样的接口,并将方法调用转发给被代理对象,同时还可以在方法调用前后执行额外的增强处理。
JDK动态代理通过反射机制实现代理功能,其原理分为以下几个步骤:
创建实现InvocationHandler接口的代理类工厂:在调用Proxy类
的``静态方法newProxyInstance时,会动态生成一个代理类。该代理类实现了目标接口,并且持有一个
InvocationHandler类型的引用。 InvocationHandler接口:InvocationHandler是一个接口,它只有一个方法
invoke。在代理对象的方法被调用时,JVM会自动调用
代理类的invoke`方法,并将被调用的方法名、参数等信息传递给该方法。
调用代理对象的方法:当代理对象的方法被调用时,JVM会自动调用代理类的invoke方法。在invoke方法中,可以根据需要执行各种逻辑,比如添加日志、性能统计、事务管理等。
invoke方法调用:在invoke方法中,通过反射机制调用目标对象的方法,并返回方法的返回值。在调用目标对象的方法前后,可以执行额外的逻辑。
单例模式
懒汉式,线程不安全
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
懒汉式,线程安全
public class Singleton {public static Singleton singleton;private Singleton(){}// 主要就是加了锁机制,避免两个线程都检查到null,然后分别创建了singletonpublic static synchronized Singleton getInstance(){if (singleton == null){singleton = new Singleton();}return singleton;}
}
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
饿汉式,线程安全
public class Singleton {// static 一方面保证了instance在类加载的时候就会生成// 另一方面保证了这个instance是属于这个类的private static Singleton singleton = new Singleton();private Singleton(){}public static Singleton getInstance(){return singleton;}
}
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化
,浪费内存。
它基于 classloader 机制
避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
双检锁/双重校验锁(DCL,即 double-checked locking),线程安全,效率高
首先来看一下单线程懒汉模式的缺点
public class Singleton {// 声明静态变量public static Singleton singleton;// 私有化构造方法,使得外界不能调用newprivate Singleton(){}// 构造一个静态方法,属于这个类的静态方法// 在这个方法中检查类的静态变量是否已经生成// 如果没有生成,在方法中生成public static Singleton getInstance(){if (singleton == null){singleton = new Singleton();}return singleton;}
}
一个情况是,如果有一个线程A检查singleton == null条件成立,此时另一个线程B也检查singleton == null条件成立。
B线程new了一个singleton,同样的A线程也new一个singleton,这样就不符合单例模式了。
另外一个情况是,如果线程A判断singleton == null条件成立,
于是开始new singleton(),但是new的过程并不是一气呵成的,
他需要三个步骤,分别是,分配空间,初始化Singleton,把内存空间地址赋给singleton,这三个步骤有可能被指令重排序,即先发生分配空间,把内存空间地址赋给singleton,然后再初始化,
如果分配空间,把内存空间地址赋给singleton,这两步完成之后,B线程进来判断singleton == null不成立,虽然对象没有完全建立起来,但是这个引用已经有值了,直接把未完全new的对象返回了,这是不对的。
首先我们来解决第一个情况,为了避免两个线程进入都进入判断,我们可以给整个函数加锁
public class Singleton {// 声明静态变量public static Singleton singleton;// 私有化构造方法,使得外界不能调用newprivate Singleton(){}// 构造一个静态方法,属于这个类的静态方法// 在这个方法中检查类的静态变量是否已经生成// 如果没有生成,在方法中生成public static synchronized Singleton getInstance(){if (singleton == null){singleton = new Singleton();}return singleton;}
}
但是实际上这是没有必要的,因为如果对整个函数加锁,在singleton已经存在之后,众多的读操作
会被阻塞在函数之外,排队等待进去函数,这是不必要的,不需要阻塞单例生成之后的读操作。
可以采用另一种写法
public class Singleton {// 声明静态变量public static Singleton singleton;// 私有化构造方法,使得外界不能调用newprivate Singleton(){}// 构造一个静态方法,属于这个类的静态方法// 在这个方法中检查类的静态变量是否已经生成// 如果没有生成,在方法中生成public static Singleton getInstance(){synchronized(Singleton.class){if (singleton == null){singleton = new Singleton();}}return singleton;}
}
这样写还有一个问题,就是线程确实不需要在函数外进行等待,但是进入函数之后,线程还是在继续等待,等待判断singleton == null,
这样的写法保证了判断和产生新对象是一气呵成的,但是效率不够
public class Singleton {private volatile static Singleton singleton;private Singleton (){}public static Singleton getSingleton() {if (singleton == null) {synchronized (Singleton.class) {// 第一个进来的线程会看到null,后续线程不会看到nullif (singleton == null) {singleton = new Singleton();}}}return singleton;}
}
这样写一方面保证了效率,因为singleton == null不成立的时候,也就是单例已经产生之后,直接可以进行读操作,不需要排队等待
另一方面也解决了重复new的问题,因为在单例未生成的时候,需要排队等待,不会产生两个线程都判断singleton == null,都产生单例的情况。
情况一到此就解决了,那么情况二呢?
这时候如果线程A正在new新对象,并且发生了执行重新排序,已经完成了分配空间,把地址赋值给引用两个操作,这时线程B走到了第一个singleton == null,发现不成立,直接取走了单例,但是这个时候的单例并不是一个合法的单例。如何解决这个问题呢?
可以使用关键字volatile
volatile有两个作用,
其一,加volatile的对象,如果想要读取这个对象,需要从主内存读取一份,保证读到的是最新的对象,如果对这个对象进行了写操作,要立刻写回主内存,保证主内存中一直都是最新的对象。
其二,禁止指令重排序,这样就保证了new对象的工程中最后才对引用singleton赋值,避免了判断singleton == null不成立,但是读取的时候读到的是一个不完全的singleton。
静态内部类,线程安全,延迟加载
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton {private static class SingletonHelp{private static final Singleton singleton = new Singleton();}private Singleton(){}public static Singleton getSingleton(){return SingletonHelp.singleton;}
}
这种方法一方面克服了传统的饿汉式外部类加载就产生单例的浪费,因为singleton类加载的时候内部类并没有被加载,所以内部类的静态变量并未生成。
另一方面,这种方法也保证了线程安全,因为单例的生成是类加载的时候一次性生成的,不存在线程并发的问题。
PS:静态内部类
静态内部类(static nested class)的类加载过程与普通类有一些相似之处,但也有其独特之处。以下是静态内部类的类加载过程的详细描述:
1. 类加载时机
- 独立加载:静态内部类是属于外部类的一个静态成员,但它和外部类是独立的。因此,静态内部类不会在外部类加载时自动加载,只有在静态内部类本身被使用时才会加载。
- 使用场景:静态内部类会在以下情况发生时加载:
- 静态内部类的静态成员(字段、方法)被调用时。
- 静态内部类的实例被创建时。
- 静态内部类的静态初始化块被执行时。
2. 类加载步骤
与普通类的加载过程相同,静态内部类的加载过程包括以下几个步骤:
-
加载(Loading):
- 虚拟机通过类加载器读取静态内部类的.class文件,将其字节码加载到内存中,并创建一个
Class
对象来表示该类。
- 虚拟机通过类加载器读取静态内部类的.class文件,将其字节码加载到内存中,并创建一个
-
连接(Linking):
- 验证(Verification):确保静态内部类的字节码文件格式正确,满足Java语言规范的要求。
- 准备(Preparation):为静态内部类的静态变量分配内存,并将其初始化为默认值。
- 解析(Resolution):将静态内部类的符号引用转换为直接引用。
-
初始化(Initialization):
- 执行静态内部类的静态初始化块(如果有)和静态变量的初始化赋值语句。
3. 特点和注意事项
- 独立性:静态内部类与外部类是相对独立的。虽然静态内部类可以访问外部类的静态成员,但它们的类加载过程是独立的。
- 访问权限:静态内部类可以访问外部类的所有静态成员,包括私有静态成员。
- 外部类的引用:静态内部类不持有对外部类实例的引用,因为它不依赖于外部类的实例。这与非静态内部类(成员内部类)不同,后者需要持有外部类的实例引用。
代码示例
以下是一个包含静态内部类的简单Java代码示例:
public class OuterClass {private static String outerStaticField = "Outer Static Field";static class StaticNestedClass {static {System.out.println("Static Nested Class Initialized");}void display() {System.out.println("Accessing: " + outerStaticField);}}public static void main(String[] args) {System.out.println("Main Method Start");StaticNestedClass nestedObject = new StaticNestedClass();nestedObject.display();}
}
说明
- 在上述代码中,静态内部类
StaticNestedClass
在其静态成员被访问或者实例被创建时才会被加载和初始化。 StaticNestedClass
的静态初始化块会在第一次使用时执行,打印出"Static Nested Class Initialized"。- 外部类的静态字段
outerStaticField
可以被静态内部类直接访问。
总结来说,静态内部类的类加载过程与普通类的加载过程类似,只有在静态内部类的成员被访问或实例被创建时,静态内部类才会被加载和初始化。
枚举,最优解
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {SINGLETON;public void show() {System.out.println("枚举类实现单例");}
}