零碎的知识点-5
AOP的实现方式
AOP是Aspect Oriented Programming的缩写,意思是面向方面编程。
Java可以通过三个层面来实现AOP:
- 在编译期修改源代码
- 在运行期字节码加载前修改字节码 0
- 在运行期字节码加载后动态创建代理类的字节码
各种实现机制的比较:
类别 | 机制 | 原理 | 优点 | 缺点 | 实现的代表 |
---|---|---|---|---|---|
静态AOP | 静态织入 | 在编译期,切面直接以字节码的形式编译到目标字节码文件中 | 对系统无性能影响 | 灵活性不够 | AspectJ |
动态AOP | 动态代理 | 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 | 相对于静态AOP更加灵活 | 切入的关注点需要实现接口。对系统有一点性能影响 | JDK动态代理,Spring |
动态字节码生成 | CGLIB | 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 没有接口也可以织入 扩展类的实例方法为final时,则无法进行织入 | CGLIB,Spring | ||
自定义类加载器 | 在运行期,目标加载前,将切面逻辑加到目标字节码里 | 可以对绝大部分类进行织入 | 代码中如果使用了其他类加载器,则这些类将不会被织入 | ||
字节码转换 | 在运行期,所有类加载器加载字节码前进行拦截 | 可以对所有类进行织入 |
AOP里的成员:
- Joinpoint:拦截点,如某个业务方法
- Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint
- Advice:要切入的逻辑
- Before Advice:在方法前切入
- After Advice:在方法后切入,抛出异常则不会切入
- After Returning Advice:在方法返回后切入,抛出异常则不会切入
- After Throwing Advice:在方法抛出异常时切入
- Around Advice:在方法执行前后切入,可以中断或忽略原有流程的执行
AOP实现的功能
- 性能监控:在方法调用前后记录调用时间,方法执行太长或超时报警
- 缓存代理:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取
- 软件破解:使用AOP修改软件的验证类的判断逻辑
记录日志:在方法执行前后记录系统日志 - 工作流系统:工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务
- 权限验证:方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉
Spring的AOP
Spring默认采取动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用cglib机制
但Spring的AOP有一定的缺点:
- 第一,只能对方法进行切入,不能对接口、字段、静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法都将被切入)
- 第二,同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean
- 第三,性能不是最好的。从前面几节得知,我们自定义的类加载器,性能优于动态代理和cglib
用双向链表实现一个栈
java里默认的LinkedList很适合包装成栈,因为栈不需要随机访问。
public class DLStack implements Stack<Integer> {
private LinkedList<Integer> storage = new LinkedList<>();
/**
* 压出元素
*
* @param v 元素
*/
@Override
public void push(Integer v) {
storage.addFirst(v);
}
/**
* 获取栈顶元素,但并不将其从栈顶移除。
*
* @return 栈顶元素
*/
@Override
public Integer peek() {
return storage.getFirst();
}
/**
* 移除并返回栈顶元素
*
* @return 栈顶元素
*/
@Override
public Integer pop() {
return storage.removeFirst();
}
/**
* 判断栈是否为空
*
* @return 是否为空
*/
@Override
public boolean empty() {
return storage.isEmpty();
}
/**
* 获取栈的大小
*
* @return 栈的大小
*/
@Override
public long size() {
return storage.size();
}
public String toString(){
return storage.toString();
}
}
countdownlatch和cyclicbarrier的作用和区别
countdownlatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
他是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
CountDownLatch的使用场景:
在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作。 这个时候就可以使用CountDownLatch。CountDownLatch最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。
总结:CountDownLatch实质上就是一个AQS计数器,通过AQS来实现线程的等待与唤醒。
CyclicBarrier
CyclicBarrier,让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier 使用场景:
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
CyclicBarrier和CountDownLatch的区别
- 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
- CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
- CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
4, CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。
Spring IOC循环依赖解决方案
循环依赖就是N个类中循环嵌套引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错。下面说一下Spring是如果解决循环依赖的。
Spring为了解决单例的循环依赖问题,使用了三级缓存。
- singletonFactories : 单例对象工厂的cache
- earlySingletonObjects :提前暴光的单例对象的Cache
- singletonObjects:单例对象的cache
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
所以,Spring容器循环依赖包括: 构造器循环依赖和setter循环依赖。
构造器参数循环依赖
通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖
原理:Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符创建过程中将一直保持在这个池中,因为如果在创建bean过程中发现自己已经在“当前创建bean池”中时,将会抛出BeanCurrentlyInCreationException
异常表示循环依赖;而对于创建完毕的bean将从“当前创建bean池”中清除掉。
setter方式单例,默认方式
Spring的单例对象的初始化主要分为三步:
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充
- initializeBean:调用spring xml等配置文件中的init 方法。
Spring是先将Bean对象实例化之后再设置对象属性的
原理:对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(比如setter注入)的bean来完成的,而且只能解决单例作用域的bean循环依赖。
通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean addSingletonFactory()
方法。Spring先是用构造实例化Bean对象 ,此时Spring会将这个实例化结束的对象放到一个Map中,并且Spring提供了获取这个未设置属性的实例化对象引用的方法。 结合我们的实例来看,,当Spring实例化了StudentA、StudentB、StudentC后,紧接着会去设置对象的属性,此时StudentA依赖StudentB,就会去Map中取出存在里面的单例StudentB对象,以此类推,不会出来循环的问题喽、
setter方式原型,prototype
对于scope为
prototype
范围的bean,Spring容器无法完成依赖注入,因为Spring容器不进行缓存prototype
作用域的bean,因此无法提前暴露一个创建中的bean,所以检测到循环依赖会直接抛出BeanCurrentlyInCreationException
异常对于单例作用域的bean,可以通过
setAllowCircularReferences(false)
来禁用循环引用,这样如果存在循环引用就会抛出异常来通知用户。
利用classloader实现热部署
对于Java应用程序来说,热部署就是在运行时更新Java类文件。
为了实现热部署,可以自定义类加载器,并重写ClassLoader的findClass方法。想要实现热部署可以分以下三个步骤:
1、销毁该自定义ClassLoader
2、更新class类文件
3、创建新的ClassLoader去加载更新后的class类文件。
热部署与热加载
Java热部署与热加载的联系:
- 不重启服务器编译/部署项目
- 基于Java的类加载器实现
Java热部署与热加载的区别:
部署方式:
- 热部署在服务器运行时重新部署项目
- 热加载在运行时重新加载class
实现原理 :
- 热部署直接重新加载整个应用
- 热加载在运行时重新加载class
使用场景:
- 热部署更多的是在生产环境使用
- 热加载则更多的实在开发环境使用
Tomcat热部署机制
Tomcat的容器实现热部署使用了两种机制:
- Classloader重写,通过自定义classloader加载相应的jsp编译后的class到JVM中。 通过动态修改内存中的字节码,将修改过的class再次装载到JVM中。
Tomcat通过org.apache.jasper.servlet.JasperLoader
实现了对jsp的加载。
- 通过代理修改内存中class的字节码
Tomcat中的class文件是通过org.apache.catalina.loader. WebappClassLoader
装载的。在热部署的情况下,对于被该classloader 加载的class文件,它的classloader始终是同一个WebappClassLoader,除非容器重启了。
HashMap的一些知识点:
如何处理null
当key为null时,HashMap执行putForNullKey()
方法。在方法里有个for循环,是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。
如果上面for循环没找到则将这个元素添加到talbe[0]
链表的表头。
HashMap在JDK1.7与JDK1.8的区别
JDK1.7中
使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个桶里,这些key会形成一个链表。
在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表,也就是说时间复杂度在最差情况下会退化到O(n)
DK1.8中
使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构。如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个桶里。如果同一个桶里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销,也就是说put/get的操作的时间复杂度最差只有O(log n)。
但是真正想要利用JDK1.8的好处,有一个限制:key的对象,必须正确的实现了Compare接口。如果没有实现Compare接口,或者实现得不正确(比方说所有Compare方法都返回0),那JDK1.8的HashMap其实还是慢于JDK1.7的。
解决hash冲突的办法
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法
- 建立一个公共溢出区
Java中hashmap的解决办法就是采用的链地址法。
B树与B+树简要区别
B树
每个节点都储存key和data,所有节点组成这棵树,并且叶子节点指针为null。
B+树
只有叶子节点存储data,叶子节点包含这棵树的所有键值,字节节点不储存指针。
后来,在B+树上增加了顺序访问指针,也就是每个叶子节点增加一个指向相邻叶子节点的指针,这样一棵树成了数据库系统实现索引的首选数据结构。
因为B+树内节点不存储data,这样一个节点就可以存储更多的key,更适合做数据库的索引。
MyISAM
data存的是数据地址。索引是索引,数据是数据。
InnoDB
data存的是数据本身。索引也是数据。
MyISAM和InnoDB的主要区别和应用场景
主要区别:
- MyISAM是非事务安全型的,而InnoDB是事务安全型的。
- MyISAM锁的粒度是表级,而InnoDB支持行级锁定。
- MyISAM支持全文类型索引,而InnoDB不支持全文索引。(MySQL 5.7开始支持全文索引)
- MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。
- MyISAM表是保存成文件的形式,在跨平台的数据转移中使用MyISAM存储会省去不少的麻烦。
- InnoDB表比MyISAM表更安全,可以在保证数据不会丢失的情况下,切换非事务表到事务表(alter table tablename type=innodb)。
应用场景:
1. MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择。
2. InnoDB用于事务处理应用程序,具有众多特性,包括ACID事务支持。如果应用中需要执行大量的INSERT或UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。
拦截器和过滤器的区别
过滤器:
依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据,比如:在过滤器中修改字符编码;在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等。
拦截器:
依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理
主要区别:
- 拦截器
interceptor
是基于Java的反射机制的,而过滤器Filter是基于函数回调,实现的filter接口中doFilter方法就是回调函数。 - 拦截器interceptor不依赖与servlet容器,过滤器Filter依赖与servlet容器,没有servlet容器就无法来回调
doFilter
方法。 - 拦截器interceptor只能对action请求起作用,而过滤器Filter则可以对几乎所有的请求起作用,Filter的过滤范围比Interceptor大,Filter除了过滤请求外通过通配符可以保护页面,图片,文件等等。
- 拦截器interceptor可以访问action上下文、值栈里的对象,而过滤器Filter不能访问。
- 在action的生命周期中,拦截器interceptor可以多次被调用,而过滤器Filter只能在容器初始化时被调用一次。
- 拦截器interceptor可以获取IOC容器中的各个bean,而过滤器Filter就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
CMS收集器和G1收集器优缺点与区别
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于“”标记–清理”算法实现的,整个过程分为四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清理
优点:并发收集,低停顿
缺点:
- CMS收集器对CPU资源非常敏感
- CMS处理器无法处理浮动垃圾(即标记过程产生的垃圾)
- CMS是基于“标记–清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。
G1收集器
Java8之后广泛使用,G1 将整个对区域划分为若干个Region,每个Region的大小是2的倍数(1M,2M,4M,8M,16M,32M,通过设置堆的大小和Region数量计算得出。
Region区域划分与其他收集类似,不同的是单独将大对象分配到了单独的region中,会分配一组连续的Region区域(Humongous start 和 humonous Contoinue 组成),所以一共有四类Region(Eden,Survior,Humongous和Old),
G1 作用于整个堆内存区域,设计的目的就是减少Full GC的产生。在Full GC过程中由于G1 是单线程进行,会产生较长时间的停顿。
G1的OldGc标记过程可以和yongGc并行执行,但是OldGc一定在YongGc之后执行,即MixedGc在yongGC之后执行。
名词解释(JDK1.8):
- MetaSpace
在Java8之后取代永久代方法区的内存部分,NativeMemory - Mixed GC Event
所有Young Region和一部分Old Region的混合GC时间。 - Reclaimable
G1 为了能够回收,创建了一系列专门用于对象回收的Region,存放在链表中,只包含存活率小于-XX:G1MixedGCLIveThresholdPercent(默认是85%)的region, region的值除以整个Java堆区,如果大于-XX:G1HeapWastePercent(默认5%),则启动回收机制 - Minor GC
年轻代GC - Major GC(Full GC)
发生在老年代的GC
G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:
- 并行于并发
- 分代收集
- 空间整合(,G1从整体来看是基于“标记整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的。)
- 可预测的停顿
G1运行步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1相对于CMS的主要区别在:
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
- G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用、而CMS只能在O区使用