一、代理(功能增强)
代理:为其他对象提供一个代理以控制对某个对象的访问。代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。
简记:我现在有个登录的功能(被代理类,旧了,不想要了),但我现在又不想完全修改以前的代码,就写个代理类,在代理类里添加了对地区的验证、对登录次数的验证等等功能,再调用被代理类,这样就实现了代理!
在不改变原来的目标方法前提下,通过代理实现自己的功能代码!
静态代理
需要:接口、被代理类、代理类
关系:被代理类 + 代理类 —>实现了接口
举例:
接口:
public interface HelloInterface {
void sayHello();
}
被代理类:(旧功能,希望更新的)
public class Hello implements HelloInterface{
@Override
public void sayHello() {
System.out.println("Hello zhanghao!");
}
}
代理类:
public class HelloProxy implements HelloInterface{
里面存在 被代理类的对象
private HelloInterface helloInterface = new Hello();
@Override
public void sayHello() {
System.out.println("Before invoke sayHello" );
helloInterface.sayHello();
System.out.println("After invoke sayHello");
}
}
静态代理:很容易完成对类的代理,但缺点是只能对一个类进行代理,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。
动态代理
为了解决静态代理的“一对一”的问题,利用反射实现动态代理。
被代理类(公司)和接口(卖U盘)都和上面一样
代理类(微商)需要实现InvocationHandler接口。
InvocationHandler接口:
仅一个方法invoke()
重写invoke()方法:代理对象需要执行的功能代码,代理类要完成的功能就写在invoke方法中
代理类要完成的功能:调用代理类的方法+功能增强(比如中间商加价)
public Object invoke(Object proxy, Method method, Object[] args) :
三个参数:
Object proxy:创建的代理对象:newProxyInstance()这个静态方法返回代理对象,等同于静态代理中的new factory();
Method method:目标类的方法
Object[] args:目标类的方法的参数
public static Object newProxyInstance(ClassLoader loader,class<?>[] interfaces, InvocationHandler h)
三个参数:
ClassLoader loader:类加载器,使用反射获取
class<?>[] interfaces:目标对象实现的接口,也是反射获取
**InvocationHandler h:自己写的,代理类要完成的功能**
- jdk动态代理:必须有接口,目标类(被代理类)必须实现接口
- cglib动态代理:可以没有接口
JDK动态代理过程
public class ProxyHandler implements InvocationHandler{
private Object object;
//动态代理,目标对象是活动的,需要传入进来
public ProxyHandler(Object object){
//传入对象
this.object = object;
}
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke " + method.getName());
method.invoke(object, args);
System.out.println("After invoke " + method.getName());
return null;
}
}
主函数这样:
其中的语句都是用了多态技术,即父类的引用指向子类的对象,接口也同理。
public static void main(String[] args) {
//创建目标对象
HelloInterface hello = new Hello();
//创建invocationHandler对象
InvocationHandler handler = new ProxyHandler(hello);
//创建代理对象
HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);
//通过动态代理执行方法
proxyHello.sayHello();
}
输出:
Before invoke sayHello
Hello zhanghao!
After invoke sayHello
这时又来了一个接口(功能)+被代理类(公司):
public interface ByeInterface {
void sayBye();
}
public class Bye implements ByeInterface {
@Override
public void sayBye() {
System.out.println("Bye zhanghao!");
}
}
无需像静态代理一样重新去写,只需在主程序中这样:
public static void main(String[] args) {
//被代理类创建对象
HelloInterface hello = new Hello();
ByeInterface bye = new Bye();
//InvocationHandler 接口:写好新加的业务逻辑
InvocationHandler handler = new ProxyHandler(hello);
InvocationHandler handler1 = new ProxyHandler(bye);
HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);
ByeInterface proxyBye = (ByeInterface) Proxy.newProxyInstance(bye.getClass().getClassLoader(), bye.getClass().getInterfaces(), handler1);
proxyHello.sayHello();
proxyBye.sayBye();
}
输出:
Before invoke sayHello
Hello zhanghao!
After invoke sayHello
Before invoke sayBye
Bye zhanghao!
After invoke sayBye
好处:不用再写代理类(静态代理中的)了,当接口增加方法时,影响的比较小,不像静态代理一样所有的代理类都要重新写“override”。
四、设计模式
1. 适配器模式
便于复用。版本向下兼容时
使用继承
即,当我们有一个旧版本的代码
,又想实现新接口的功能
时,这时可以继承旧版本代码
,并实现新接口
,这样就既能保证旧版本的兼容,又能开启新功能。
使用委托
思想是相同的,改变的是,这时Print不是接口
而是类
,只需在新代码中继承这个类,再在新代码中生成一个旧代码的对象
,再调取他的功能。
2. 单例模式
确保某个类只有一个实例。Spring下默认的bean均为singleton,可以通过singleton=“true|false” 或者 scope="?"
来指定。
- 懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 懒汉式,线程安全,但是效率不高
```java
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 饿汉式,线程安全,but不是懒加载
public class Singleton{
//类加载时就直接初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
- 枚举类,默认线程安全,且能防止反序列化造对象
public enum EasySingleton{
INSTANCE;
}
运用jad
进行反编译可以看到:
我们可以看到,枚举单例的本质就是饿汉式单例,即在 static 代码块完成了实例的创建及初始化。
3. 代理模式
面向切面编程AOP:在方法执行前后可以创建其他有意义的操作。
看第一部分!
spring的Proxy模式在aop中有体现,比如JdkDynamicAopProxy和Cglib2AopProxy。
4. 模板方法模式
在父类中定义处理流程的框架,在子类中实现具体处理
- 抽象类:定义抽象方法不给出具体的实现
- 具体类:负责实现抽象类中的抽象方法
好处:
①逻辑处理通用:父类中模板方法编写了算法,无需在每个子类中编写,后续修改也容易。
②使用父类类型变量保存子类实例:即使没有指定子类的种类,程序也能正常工作。
举例:Java中的inputStream使用了模板模式。
5. 工厂方法模式(模板方法的典型应用)
类似模板模式,父类决定实例的生成方式,但并不决定所要生成的具体的类,子类负责具体
应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
不用new关键字来生成实例,而是调用生成实例的专用方法来生成实例,防止父类与子类耦合
举例:
spring的工厂方法模式使用BeanFactory 或 ApplicationContext 创建 bean 对象。
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("config.xml"));
假设有这样一个factory对象,可以直接factory.getBean("xxxxxxx")
就返回工厂xxxxxxx的Bean的实例,也就是所谓的工厂方法模式!
6. 原型模式(克隆模式)
① new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。
分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
② clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
深拷贝or浅拷贝
以上的Person类是这样的
public class Person implements Cloneable{
private int age ;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public Person() {}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person)super.clone();
}
}
clone方法属于浅拷贝
如果想实现深拷贝就需要实现clonable接口并重写clone方法。
但是并没有实现真正的深拷贝,因为对于head来说还是浅拷贝的,head中若存在其他对象,那么其他对象也是浅拷贝的
7. 装饰者模式(decorator模式)
StringDisplay就是源代码,Border是对于此源代码的所有装饰类的抽象类,其内部保存了被装饰的对象,所有这些类的构造器内都是(Display display)。
优点:
①接口的透明性——由于装饰类与原类都继承了同一抽象类,这样即使被包装起来,其API也依旧能被调用。由于接口的透明性,也可以采用递归结构多层装饰。new FullBoarder (newStringDisplay(new ...))
。
②可以在不改变被装饰物的前提下增加功能——被装饰物与装饰物的API相同。
举例:
java.io包中的new BufferedReader(new FileReader())
,都继承了Read抽象类。
继承与委托:
继承即Parent obj = new Child()
,可以用Parent的API。
委托是现在存在两个类,其中一个类中创建了另一个类的对象,利用此对象的方法完成自己的方法。
8. 观察者模式
当观察对象发生变化时,会通知给观察者。
成员:
①被观察者subject:用一个List保存观察者,notifyObservers()
方法调用所有观察者.update
②观察者observers:所有观察者实现Observer接口(重写其update方法),注意Observer接口的update传进去的参数是被观察者类型。
由于被观察者的notifyObservers()会将this传给观察者的update()方法,做到将被观察者通知给观察者。
注意:
①如果observers的update的方法又更改了subject,这样就会产生循环调用。
②java中提供了Observer接口(observers必须实现这个)和Observable类(subject必须继承这个),
存在问题,由于Java只能单一继承,不方便,最好不用这个。解决方法:自己将subject和observers都定义为接口。
9. 责任链模式
多个对象组成一条责任链,当某人被要求做什么,自己能做就做,做不了就转给另一个人,下一个人也这样
优点:
①弱化了发出请求的人和处理请求的人的关系——如果不用这个,就必须有个人专门为了分配任务。
②可动态改变责任链。
缺点:
带来一点点的延迟,如果请求和处理者之间的关系是确定的,而且需要非常快的处理速度时,这模式不太行。
责任链模式举例:
拦截器模式
- 调度器:
调度器调度N个拦截器 - 拦截器:
只是负责拦截业务代码,在业务代码前后做一些操作,拦截器不会直接调用业务代码(不然会耦合),拦截器只是在处理完自己的逻辑后通知调度器,由调度器负责后续的操作(执行业务逻辑或其他拦截器)。 - 业务逻辑:
原本的代码
10. 抽象工厂模式
工厂模式:简单工厂模式、工厂方法模式、抽象工厂模式。
工厂方法模式产出的是产出是一个产品(实例),抽象工厂产出是一个抽象(接口)。区别在于,若添加一个新的产品,前者是修改工厂,后者是创建新工厂(符合“闭合原则”)
五、String不可变类型
JDK7中String类型:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
String的成员变量是private final 的,也就是初始化之后不可改变。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗——只能通过反射改变!
public static void testReflection() throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
六、IO多路复用
1)BIO
服务端处理客户端的请求
listenfd = socket(); // 打开一个网络通信端口
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(1) {
connfd = accept(listenfd); // 阻塞建立连接
int n = read(connfd, buf); // 阻塞读数据
doSomeThing(buf); // 利用读到的数据做些什么
close(connfd); // 关闭连接,循环等待下一个连接
}
这个连接的客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接。
这肯定是不行的。
2)NIO
需要操作系统提供一个read函数的非阻塞形式的函数。这个 read 函数的效果是,如果没有数据到达时(到达网卡并拷贝到了内核缓冲区),立刻返回一个错误值(-1),而不是阻塞地等待。
3)IO多路复用
用一个线程就可以监控多个文件描述符。
select
select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:
- select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
- select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
- select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
poll
与select的区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。为什么?底层是链表。其余无区别。
epoll
针对select的三个缺陷:
- 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
- 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
- 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
七、线程池ThreadPoolExecutor
1)线程池流程
2)线程池的五种状态
RUNNING状态可以接受新进来的任务,同时也会执行队列里的任务。
SHUTDOWN 状态已经不会再接受新任务,但仍旧会处理队列中的任务。
STOP状态在之前的基础上,不会处理队列中的任务,在执行的任务也会直接被打断。
TIDYING状态在之前的基础上,所有任务都已经终止,池中的Worker线程都已经为0,也就是stop状态在清理完所有工作线程之后就会进入该状态,同时在shutdown状态在队列空以及工作线程清理完毕之后也会直接进入这个阶段,这一阶段会循环执行terminated()方法。
TERMINATED 状态作为最后的状态,在之前的基础上terminated()方法也业已执行完毕,才会从上个状态进入这个状态,代表线程池已经完全停止。
3)状态底层采用的是AtomicInteger
通过比较大小得知线程池的状态。
4)初始化线程池需要的变量
corePoolSize:线程池的基本大小
maximumPoolSize:代表线程池中最大的工作线程数量
keepAliveTime为线程池中工作线程数量大于corePoolSize时,每个工作线程的在空闲时最长的等待时间。
workQueue作为线程池的任务等待队列
Workers作为存放工作线程的HashSet。
Handler作为线程池中在不能接受任务的时候的拒绝策略,我们可以实现自己的拒绝策略,在实现了RejectedExecutionHandler接口。
八、线程安全
JVM中的线程安全
jvm有一个main memory,而每个线程有自己的working
memory,一个线程对一个variable进行操作时,都要在自己的working
memory里面建立一个copy,操作完之后再写入main
memory;而用synchronized的关键是建立一个monitor,这个monitor可以是要修改的variable也可以其他你认为合适的object比如method,然后通过给这个monitor加锁来实现线程安全,每个线程在获得这个锁之后,要执行完
load到workingmemory -> use&assign -> store到mainmemory
的过程,才会释放它得到的锁。这样就实现了所谓的线程安全。
- list接口
Vector、ArrayList、LinkedList
Vector线程安全,synchronized修饰;ArrayList、LinkedList线程不安全
ArrayList:数组结构,查询方便,增加删除需要后移
LinkedList:链表结构,查询不方便,增加删除简单
- Map和Set接口:
HashTable,HashMap,HashSet
HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized,HashTable不允许null值(put的key如果null直接抛出异常)。
HashSet:
1、HashSet基于HashMap实现,无容量限制。
2、HashSet是非线程安全的。
3、HashSet不保证有序。
HashMap:
1、HashMap采用数组方式存储key,value构成的Entry对象,无容量限制。
2、HashMap基于Key hash查找Entry对象存放到数组的位置,对于hash冲突采用链表的方式来解决。
3、HashMap在插入元素时可能会要扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中。
4、HashMap是非线程安全的。
5、HashMap遍历使用的是Iterator。
HashTable
1、HashTable是线程安全的。
2、HashTable中无论是Key,还是Value都不允许为null。
3、HashTable遍历使用的是Enumeration。
TreeMap:
1、TreeMap是一个典型的基于红黑树的Map实现,因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。
2、TreeMap是非线程安全的。
TreeSet:
1、TreeSet基于TreeMap实现,支持排序。
2、TreeSet是非线程安全的。
线程安全的实现:
比如i++操作在多线程时不是线程安全的。
public class TLS {
public static void main(String[] args) {
ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// 设置当前线程的本地线程变量
threadLocal1.set("thread1");
threadLocal2.set(1);
System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
// 使用完毕后要删除,避免内存泄露
threadLocal1.remove();
threadLocal2.remove();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal1.set("thread2");
threadLocal2.set(2);
System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
threadLocal1.remove();
threadLocal2.remove();
}
});
thread1.start();
thread2.start();
// 没有通过ThreadLocal为主线程添加过本地线程变量,获取到的内容都是null
System.out.println(threadLocal1.get()+": "+threadLocal2.get());
}
}
怎么理解ThreadLocal?
一个hashmap,key会自动帮你填好,因此直接set(value)就可以,不同的线程会提取到不同的value,即,将不同线程的数值都保存起来了。
九、枚举类
十、IO流
IO流用了装饰者模式。