某种场景下想使用已有对象的属性,由于new出来和反射出来的新对象是全新的对象,直接赋值又会影响到原有对象,克隆就是为了解决此类问题的。克隆又分为浅克隆和深克隆
谈谈对Spring IOC的理解
AOP(Aspect Oriented Programming) 面向切面编程。在编程中,我们希望将日志记录,性能统计,事务处理,异常处理等代码逻辑相似又不影响正常业务流程的代码提取出来,然后通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现。
参考:SpringAOP原理分析
SpringCloud的核心组件有:Eureka、Ribbon、Feign、Hystrix、Zuul。
微服务将模块服务化,他们之间会相互调用,随着业务增多,服务增多服务间管理逐渐复杂,Eureka Server提供服务注册功能,服务启动后会将自己的服务名、ip、端口信息注册到Eureka Server上,Eureka Client进行服务调用时就会从Eureka Server上拉取服务信息。
Eureka Client调用某个具有多个实例服务时,应该从众多服务中进行选择,Ribbon提供服务的负载均衡,Ribbon内置了一些负载均衡算法(轮询、随机等),用户也可以自定义算法。
SpringCloud的服务调用可以直接通过自行封装Http发送请求,但是每次服务调用都需要大量代码去封装发送和解释返回结果。Java都推崇面向接口编程,使用Feign发送远程请求就像SpringMVC的前端请求后端一样简单,原理如下
- 在启动时Feign会对使用了@FeignClient注解的接口进行扫描生成动态代理类注册到Spring容器中。
- 然后当调用Feign中的接口时,代理类根据接口上的@RequestMapping等注解,来动态封装HTTP请求,发送请求
- 请求结果返回后,代理类会对结果进行解码返回给调用者
当某个服务在被调用时发生网络故障或者宕机时,服务调用者由于等不到响应会阻塞直到超时,如果有很多服务调用该服务那么所有的服务都将被阻塞。Hystrix会为每个服务提供独立的线程池,服务调用先打到Hystrix中,某个服务发生故障不会影响到其它服务调用,并且Hystix提供服务降级功能,某个服务挂掉时Hystix可以通过fallback直接构造返回结果,并且处理失败结果,比如说将失败信息保存起来以便进行恢复。
随着服务的增多,几十个、几百个甚至是几千个服务,每次调用服务都需要记住服务名。在前后端分离开发的应用中,前端工程师就需要知道每个服务名,这是不切实际的。所有的服务通过zuul配置路径后,发送的请求都通过zuul向服务转发,实现服务访问统一管理。zuul还可以实现统一服务降级、身份权限认证、限流等功能
#{}是预编译处理,MyBatis会将#{}替换为?,配合PreparedStatement的set方法赋值,防止SQL注入。${}直接是字符串替换,不推荐使用。
参考:简述MyBatis的一级缓存、二级缓存原理
开启延迟加载
<setting name="lazyLoadingEnabled" value="true"/>
默认是侵入式延迟加载机制:如果只查询主表数据而不进行使用,级联表的数据不会被查询;如果使用了主表数据,即使级联表的数据没有使用,也会查询
关闭侵入式延迟加载机制:使用到数据才会去查找相关表
<setting name="aggressiveLazyLoading" value="false"/>
- 将从DB查询出来的空值进行缓存“null”
- 使用布隆过滤器。(有一定的误判率,谷歌guava的默认误判率为0.03)
- 设置热点数据key永不过期
- 设置过期时间不要集中在一起
参考:《深入理解Java虚拟机》
JDK1.2之前Java中引用只有引用和没被引用两种状态,过于狭隘,对于“食之无味弃之可惜”的对象无能为力。我们希望某些对象在内存足够时保留,内存不足时抛弃。JDK1.2之后对引用进行了扩充,由以下4中以此减弱:
参考:《深入理解Java虚拟机》
- 引用计数算法:给对象添加一个引用计数器,每当一个地方引用它计数器就加1,每当一个引用失效时计数器减1,任意时刻计数器为0时,该对象不可用。缺点是:无法解决循环引用问题。
- 可达性分析算法:通过一系列成为“GC Roots”对象作为起点向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,改对象不可用。Java中,可作为GC Roots对象包括:
① 虚拟机栈(栈帧中本地变量表)中引用的对象
②方法区中静态属性引用的对象
③方法区中常量引用的对象
④本地方法栈中JNI引用的对象
- 如果对象不可达那么将会被第一次标记,并且进行一次筛选,筛选条件是是否需要执行finalize()方法(对象没有覆盖finalize()方法,或者执行过了finalize()就没必要执行,任何对象的finalize()只会执行一次)
- 如果对象有必要执行finalize()方法,将会被放进F-Queue队列中。然后GC将对该对象进行第二次标记,对象如果在执行finalize()方法时成功自救(重新与引用链上任意对象建立关联),将被移除即将回收的集合,否则就离死不远了
参考:《深入理解Java虚拟机》
- 标记和清除效率低下
- 标记清除后产生大量不连续的内存碎片
现在的商业虚拟机使用这种方式回收新生代,新生代百分之98是朝生夕死对象,所以不需要1:1分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survior(Survior from、Survior to)空间,回收时将Eden和Servior From存活对象复制到Servior to上,然后清理掉自己。HotSpot默认Eden和Servior为8:1,我们没法保证每次回收存活对象不多于10%,当内存不够时需要依赖老年代进行分配担保,也就是当Servior to没有足够内存存放上一次新生代的存活对象时,这些对象将通过分配担保机制进入老年代。
Java应用随着用户量增大等原因会导致需要的内存不断增大,一旦所需内存大于物理机可分配内存就会导致系统崩溃,因此就需要对JVM内存进行配置限制,一旦到达临界点就会进行内存回收释放,系统永远不会因为内存问题而导致崩溃。
加载、验证、准备、初始化、卸载这5个阶段顺序是固定的。为了支持Java运行时绑定,解析阶段可以在初始化之后进行。以下5种情况必须初始化:
获取二进制流途径:
- 从ZIP包获取,是JAR、EAR、WAR格式基础
- 网络中获取,如:Applet
- 运行时计算机生成,这种场景使用最多的是动态代理技术,在java.lang.reflect.Proxy就是使用ProxyGenerator.generateProxyClass来为特定的接口生成二进制字节流。
- 其他文件生成,如JSP
- 从数据库中读取,如某些中间件服务器将代码安装到数据库中来实现代码在集群中分发
a.是否以魔数0xCAFEBABE开头
b.主、次版本号是否在当前虚拟机处理范围内
c.常量池的常量数据类型是否被支持(检查常亮tag标志)
d. 指向常量的各种索引值是否有指向不存在或者复合类型的常亮
e. CONSTANT_utf8_info型常量中是否有不符合utf8编码的数据
f. Class文件中各个部分以及文件本身是否有被删除或者附件的其他信息
…
a. 是否有父类
b. 是否继承了不被允许继承的类
c. 如果该类不是抽象类,是否实现了其父类或接口要求实现的所有方法
d. 类中的字段、方法是否与父类产生矛盾(类如覆盖父类final字段,或者错误方法重载)
…
a. 保证任何时候操作数栈的数据类型与指令代码序列的一致性,不会出现这种情况:在操作栈中放置了一个int类型数据,使用时却按照long类型加载入本地变量表中;
b.跳转指令不会跳转到方法体以外的字节码指令上
…
a. 符号引用的类、字段、方法的访问性(public、private等)是否可被当前类访问
b. 指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
c. 符号引用中的类、字段、方法的访问性(private、protected…)是否可被当前类访问
…
数组是直接由Java虚拟机创建的,但是数组组件是由类加载器创建的,一个数组创建遵循以下规则:
为了让应用程序自己去决定如何获取自己需要的类,将通过一个全类名来获取类的二进制字节流的这个动作放到Java虚拟机外部去实现。实现这个动作的代码块就是类加载器。
参考:《深入理解Java虚拟机》
Java多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任何时刻,一个处理器只能执行一条线程中的指令
jdk1.8中为线程设置了6个状态:
参考:线程间通信的几种实现方式
lock(锁定)、unclock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(储存)、write(写入)
- 虚拟机未将lock、unlock直接开放给用户,但是提供了更高层次的字节码指令monitorenter、monitorexit来隐式使用这两个指定,反映到Java就是synchronized关键字,因此synchronized修饰的代码块具备原子性
- 我们可以认为基本数据类型的访问读写是具有原子性的(long、double例外,但是大部分商用虚拟机都将它们读写当做原子性对待,平时在写long、double变量时不需要声明为volatile)
当一个变量定义为 volaiile 之后,它将具备两种特性:
第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,在各线程的工作内存中变量也存在不一致的情况,但是由于每次使用变量前都需要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在不一致的情况。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程 A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。
不过,无法保证非原子性操作的变量线程安全,例如i++问题,对以下代码进行反编译:
private static int i = 0;
public static void increase() {
i++;
}
反编译结果:
public static void increase();
Code:
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
getstatic将i的值取到操作栈顶时,volatile保证此时变量是正确的的,但是当执行iconst_1、iadd这些操作时,其它线程已经对i的值进行了修改,putstatic就会将较小的值同步回主内存
禁止指令重排:指令重排是指CPU在正确处理指令依赖情况以保证程序得出正确结果的前提下,不按程序规定的顺序将多条指令分开发给不同的电路单元处理。被volatile修饰的变量,会在赋值后多执行一步相当于添加内存屏障的操作,指令重排时不能将后面的指令重排到内存屏障之前。
程序编译后会在添加synchronized关键字代码块的前后分别添加monitorenter和monitorexit字节码指令,这两个指令都需要同一个reference类型的参数来指明要锁定和解锁的对象。执行monitorenter指令是就会尝试获取对象的锁。如果对象没有被锁定或者当前线程已经拥有对象的锁,就把锁的计数器加1,因此对同一个线程来说在synchronized中是可重入的,不会自己把自己锁死。相应的,在执行monitorexit指令时就将锁计数器减1,当计数器为0时释放锁。
ABA问题,假设有个变量a,线程1读到的值为2,然后进行修改3操作,线程b将a修改为4然后又改回为2,线程1提交时发现数据还是2,提交成功,这就是ABA问题,线程1读取了脏数据。
解决办法就是添加版本号,每次提交时获取最新版本号和之前版本号进行对比,一致就提交。
JUC包通过提供一个带有标记的原子引用类“AomicStampedReference”来解决ABA问题,它可以通过控制变量值的版本来保证CAS正确性,不过目前来说这个类比较鸡肋,大部分情况ABA问题不会影响并发正确性,要解决ABA问题改用互斥同步更高效
由于需要保证操作和冲突检测两个步骤具备原子性,如果依靠互斥同步就失去了意义,只能依靠硬件指令集的发展,硬件保证一个从语义上看起来需要多次操作的行为通过一条处理器指令就能完成,常用的指令有:
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap,CAS)
- 加载链接/条件储存(Load-linked/Store-Conditional,LL/SC)
CAS:Compare and Swap,即比较再交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
ReenterantLock需要lock()和unlock()配合try catch finally使用,相比synchronized关键字ReenterantLock增加了以下高级功能:
代理方式 | 特点 | 缺点 |
---|---|---|
静态代理 | 需要定义父类或者接口,代理对象和被代理对象需要同时继承父类或者实现该接口,一次代理一个类 | 随着代理类增多,出现大量重复代码,难维护,造成类膨胀 |
jdk动态代理 | 目标类需要实现至少一个接口,代理对象通过JAVA的API动态生成,可以代理一个借口的多个实现 | 只能够代理实现了接口的目标类 |
cglib动态代理 | 代理类要实现MethodInterceptor接口,通过Enhancer创建目标类的子类为代理对象,所有也是通过继承关系创建代理类的,然后通过实现intercept(Object o, Method method, Object[] objects, MethodProxy proxy)方法对所有的方法进行拦截,添加增强处理,注意该方法中要通过代理类的invokeSuper调用父类的方法 | 不能代理final修饰的类 |
参考:设计模式-代理模式(Proxy Pattern)
public interface Sort {
void sort();
}
public class ConcreteSort1 implements Sort {
@Override
public void sort() {
System.out.println("使用快速排序");
}
}
public class ConcreteSort2 implements Sort {
@Override
public void sort() {
System.out.println("使用归并排序");
}
}
public class Context {
public AbstractSort method;
public Context(AbstractSort abstractSort) {
this.method = abstractSort;
}
public void contextSort() {
method.sort();
}
}
public class Main {
public static void main(String[] args) {
//传入不同的具体策略即可
Context context = new Context(new ConcreteSort2());
context.contextSort();
}
}
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
参考:设计模式之–策略模式及其在JDK中的应用
数据库索引设计的初衷是可以通过索引快速查找表中数据。索引是建立某种数据结构和表中数据一种关系,这种数据结构必须能够快速查找到目标值,然后通过这种关系定位到所需的数据行。索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。以MySQL的MyISAM为例,每个表对应的数据库文件有三个,一个保存表信息,一个保存索引信息,一个保存数据。保存索引的数据结构是B+Tree,如果根据某个字段获取数据MySQL首先判断该字段是否建立了索引,如果有索引,就先通过在B+Tree上快速查找目标值,如果找到目标值,则会通过该索引对应的物理地址定位到数据文件中的数据,获得查询结果。
聚集索引就是将索引和数据存在一个文件中,查找数据时找到索引值后直接能获取到数据。非聚集索引是将索引和数据分开储存,索引文件储存的时索引值和对应数据的物理地址,找到索引后还需要根据物理地址找到对应数据。对比直线聚集索引比非聚集索引效率要高。
参考一次web请求过程
参考:TCP/IP协议
参考:一文搞懂TCP与UDP的区别
- SYN(Synchronize Sequence Numbers):同步序列编号
- SEQ:初始序号
https=http+SSL / TLS,https相当于安全的http,https工作过程如下
只有知道解密私钥才能对内容进行解密。所以只要算法足够高深和解密私钥足够复杂数据就很安全。
- 确定该区间的中间位置K
- 将查找的值T与array[k]比较。若相等,查找成功返回此位置;否则确定新的查找区域,继续二分查找。
- 区域确定如下:如果a.array[k] > T 由数组的有序性可知array[k,k+1,……,high] > T,故新的区间为array[low,……,K-1];如果b.array[k]
public static int indexedBinarySearch(List<Integer> list, int key) {
if (list.isEmpty()) {
throw new RuntimeException("List can`t be empty !");
}
//排序
list.sort(Integer::compareTo);
System.out.println(Arrays.toString(list.toArray()));
int low = 0;
int high = list.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
Integer midVal = list.get(mid);
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
局部变量表存放编译器可知的各种基本变量类型、对象引用、返回类型(指向一条字节码指令的地址)。其中64位的long和double占两个局部变量空间(slot)。局部变量表所需内存空间编译期间完成分配,当进入某方法时这个方法需要在栈帧中分配的局部变量表空间是完全确定的,方法运行时不会改变。 ↩︎
随着JIT编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换优化技术将会导致一些微妙变化,所有对象实例在堆上分配内存变得不“绝对”了。 ↩︎