双亲委派机制
JVM可识别文件是一个个.class(字节码)文件,而这些.class需要运行起来需要JVM的类加载器将这些.class文件加载到内存,并对数据进行校检、转换、解析、初始化并最终形成可以被JVM直接执行的指令。
而JVM的类加载器去加载.class文件的时候所需要遵循的原则就是双亲委派原则。
双亲委派原则就是:一个类加载器在收到类的加载请求后不会立刻加载自己,而是先去让父类的加载器去查缓存中是否已经加载,层层迭代,到最顶层都没有加载,会往下进行委派,去加载指定的类。
双亲委派机制的好处:
- 避免重复加载造成的资源浪费问题,父类已经加载了,子类就不需要再加载了
- 保证了类加载器的安全性:解决了各个类加载器的基础类统一问题,如果不使用这种方式,那么用户可以随意定义类加载器加载核心API,会带来安全隐患。
什么是反射?
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用他的任意一个属性和方法,这种动态获取信息以及动态调用对象的方法,称为Java的反射机制
反射的优缺点
优点: 可以动态执行,在运行期间根据业务功能动态的执行方法,访问属性,最大限度的发挥了Java的灵活性
缺点: 对性能有影响,这类操作总是慢于Java代码
反射的应用场景
- JDBC连接数据库,用到反射动态加载数据库驱动程序
- Web服务器利用反射调用SevLet的服务方法
- 很多框架都用反射机制,注入属性,调用方法
Session和Cookie的区别
- cookie和session都是用来跟踪浏览器用户身份的会话方式
- 存放数据的位置不同,cookie存放在客户端浏览器上,Session存放在服务器上
- cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,如果考虑到安全因素应该使用session
- session会在一定时间内保存在服务器上。当访问增多,会比较占用服务器的性能,如果考虑到减轻服务器的性能应该使用cookie
- 单个cookie在客户端的限制是3k,就是说一个站点在客户端存放的cookie不能超过3k
- 所以将登录信息等重要的信息存放为session,其他信息如果需要保留,放在cookie中
HashMap的底层原理
HashMap在JDK 1.7和JDK 1.8的主要区别包括以下几个方面:
数据结构的变化:
JDK 1.7:HashMap是基于数组+链表的形式实现,当哈希冲突严重时,多个键映射到同一个索引位置时,它们会形成一个链表。这种结构在哈希冲突较多时,查询效率较低。12
JDK 1.8:HashMap引入了红黑树来优化数据结构。当链表长度超过8时,链表会转换为红黑树,从而提高查询效率。这种变化使得HashMap在处理大量哈希冲突时性能更好。
扩容条件的变化:
JDK 1.7:当HashMap中的元素数量达到容量乘以负载因子(默认为0.75)时,会触发扩容操作,扩容后数组大小变为原来的两倍。13
JDK 1.8:扩容条件有所改进,不仅在初始化或size超过阈值时会触发扩容,还会在链表长度超过8时调用treeifyBin()方法进行扩容。
插入方式的变化:
JDK 1.7:HashMap使用头插法插入数据,这种方式在并发插入时可能导致循环链表的问题。13
JDK 1.8:改为尾插法插入数据,这种方式在并发场景下减少了线程间的竞争,提高了并发性能。
哈希冲突解决方式的变化:
JDK 1.7:当多个键映射到同一个索引位置时,它们会形成一个链表。查找键时,HashMap会遍历该链表,通过hashCode和equals方法判断是否为目标键。34
JDK 1.8:引入红黑树后,当链表长度超过8时,链表会转换为红黑树,从而提高查询效率。查找时间从O(n)变为O(logn)。
post和get请求的区别
1、GET请求,请求的数据会附加在URL之后,以?分割URL和传输数据,多个参数用&连接。URL的编码格式采用的是ASCII编码,而不是uniclde,即是说所有的非ASCII字符都要编码之后再传输。
POST请求:POST请求会把请求的数据放置在HTTP请求包的包体中。上面的item=bandsaw就是实际的传输数据。
因此,GET请求的数据会暴露在地址栏中,而POST请求则不会。
2、传输数据的大小
在HTTP规范中,没有对URL的长度和传输的数据大小进行限制。但是在实际开发过程中,对于GET,特定的浏览器和服务器对URL的长度有限制。因此,在使用GET请求时,传输数据会受到URL长度的限制。
对于POST,由于不是URL传值,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,Apache、IIS都有各自的配置。
3、安全性
POST的安全性比GET的高。这里的安全是指真正的安全,而不同于上面GET提到的安全方法中的安全,上面提到的安全仅仅是不修改服务器的数据。比如,在进行登录操作,通过GET请求,用户名和密码都会暴露再URL上,因为登录页面有可能被浏览器缓存以及其他人查看浏览器的历史记录的原因,此时的用户名和密码就很容易被他人拿到了。除此之外,GET请求提交的数据还可能会造成Cross-site request frogery(跨站请求伪造)攻击
序列化和反序列化
1、序列化:将Java对象转化为字节序列,可以存储在硬盘上,当JVM停机的时候,字节序列会在硬盘上继续等待
2、反序列化:将字节序列恢复为Java对象的过程,当JVM重启后,把序列化的字节序列通过反序列化恢复为Java对象。
核心就是:对象状态的保存和重建
序列化成字节流形式的对象可以进行网络传输, 通过序列化可以在进程之间传递对象。
- serialVersionUID是序列化前后的唯一标识符
- 默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!
- 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。在完成序列化后,java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要文件发生任何改变,得到的UID就会不一样。那么可能由于我们对序列化对象的修改,比如增加某个属性,就会导致反序列化报错。
- 要解决这样的问题,serialVersionUID 就派上用场了,只要在javaBean对象中增加一个serialVersionUID 字段,用来标识这个类,而不是由编译器自动生成,这样当我们修改这个类时,反序列化就不会报错了。
两种特殊情况
凡是被static修饰的字段是不会被序列化的
凡是被transient修饰符修饰的字段也是不会被序列化的
对于第一个,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域。
对于第二个,就需要了解transient修饰符的作用了
如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。
实现序列化反序列化
- 使用JDK自带的API
java.io.ObjectOutputStream对象输出流,调用writeObject(Object obj)方法可以将obj对象进行序列化,把得到的字节序列写到一个目标输出流中。下面是以文件输出流为例子。
java.io.ObjectInputStream对象输入流,调用readObject(Object obj)方法从一个源输入流中读取字节序列,再把字节序列反序列为一个对象,并将对象返回。
- 实现序列化和反序列化的工具类:ObjSerializeAndDeserialize
- 使用第三方工具Gson
接口幂等性
就是用户进行同一操作发送多次请求返回的结果是一致的,不会因为多次点击产生副作用。
解决方案:
1.token+redis 机制
比如订单支付场景:
该支付分为两个步骤:
1.1 获取全局唯一token
接口处理生成唯一标识(token) 存储到redis中,并返回给调用客户端。
1.2 发起支付操作并附带token
接口处理:
1.2.1 获得分布式锁(处理并发情况)
1.2.2 判断redis中是否存在token
1.2.3 存在 执行支付业务逻辑,否则返回该订单已经支付
1.2.4 释放分布式锁
思考:为什么要加分布式锁?
原因1:在高并发请求中 ,token判断是否存在是非线程安全的,所以要加分布式锁来保证 该条件的判断为线程安全
注释:也可redis用删除操作来判断token,删除成功代表token校验通过 这个删除是原子操作的
原因2:在支付业务中,判断支付订单是否已经存在,存在说明该订单已经支付过了,不存在就执行扣款操作,如果相同操作并发两个请求来到判断条件可能两个请求都能判断支付订单不存在,造成重复扣款。 所以也要加分布式锁保证线程的安全。
2.CAS 保证接口幂等性
2.1 状态机制幂等(状态不可逆)
针对更新操作:
例如 电商订单,订单支付状态 0 待支付 , 1 支付中 , 3 支付成功 4 支付失败。
update order set status = 1 where status =0 and orderId = “201251487987”
该sql语句利用状态CAS 保证该操作的幂等。
eg:比如要进行订单支付,上来先用CAS更新订单状态,
返回影响说为1 代表修改成功,可以支付,继续执行支付业务代码
返回影响数 0 代表修改失败,该订单已经不是待支付订单了。
注释:实际这里是利用CAS原理
3 乐观锁实现幂等
背景由来:
为什么要有幂等这种场景?因为在大的系统中,都是分布式部署,如:订单业务 和 库存业务有可能都是独立部署的,都是单独的服务。用户下订单,会调用到订单服务和库存服务。
比如:订单系统:
订单服务 —> 库存服务 (PRC远程调用(服务接口))
因为分布式部署,很有可能在调用库存服务时,因为网络等原因,订单服务调用失败,但其实库存服务已经处理完成,只是返回给订单服务处理结果时出现了异常。这个时候一般系统会作补偿方案,也就是订单服务再此放起库存服务的调用,库存减1
update t_goods set count = count -1 where good_id=2
这样就出现了问题,其实上一次调用已经减了1,只是订单服务没有收到处理结果。现在又调用一次,又要减1,这样就不符合业务了,多扣了。
幂等这个概念就是,不管库存服务在相同条件下调用几次,处理结果都一样。这样才能保证补偿方案的可行性。
乐观锁方案
借鉴数据库的乐观锁机制,如:
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
4 防重表
1.利用数据库建一张防重表(加唯一索引)
比如订单支付,
反正重复支付:订单号插入防重表 成功 执行支付业务逻辑,失败说明已经支付过。
防重表支付成功是否要删除:
1.可定期清除数据
2.也可结合 订单状态 ,在支付前查询订单状态为待支付 执行支付操作 ,操作后删除订单号 若 第二个请求插入防重表成功,但是这个时候查询订单状态失败。
(实际这个防重表就是实现了分布式锁)
5. 缓存队列
将请求放入队列,后续使用异步任务处理队列中的数据,过滤掉重复的消息。 和防止重复消费道理是一样。
class Sale{
private int number=30;
public synchronized void saleTicket(){
while(number!=0){
System.out.println(Thread.currentThread().getName());
}
}
}
public class ThreadTest{
public static void main(String[] args){
Sale sale = new Sale();
new Thread(()->{
for(int i = 0; i < 40; i++){
sale.saleTicket();
}
},"AA").start();
new Thread(()->{
for(int i = 0; i < 40; i++){
sale.saleTicket();
}
},"BB").start();
new Thread(()->{
for(int i = 0; i < 40; i++){
sale.saleTicket();
}
},"CC").start();
}
}
class Sale{
private int number = 30;
public void saleTicket(){
Lock lock = new Lock();
lock.lock();
try{
while(number != 0){
System.out.printlln(Thread.currentThread().getName());
}
}finally{
lock.unlock();
}
}
}
public class ThreadTest{
public static void main(String[] args){
Sale sale = new Sale();
new Thread(()->{
for(int i = 0; i < 40;i++){
sale.saleTicket();
}
},"AA").start();
new Thread(()->{
for(int i = 0; i < 40;i++){
sale.saleTicket();
}
},"BB").start();
new Thread(()->{
for(int i = 0; i < 40;i++){
sale.saleTicket();
}
},"CC").start();
}
}
Lock锁在资源竞争激烈的情况下性能比synchronize锁好。
Lock锁和synchronize锁各有其适用场景和优势。Lock锁提供了更多的灵活性和可扩展性,允许以非阻塞的方式获取锁,并且在资源竞争激烈的情况下表现出更好的性能。相比之下,synchronize锁虽然编码更简单,锁机制由JVM维护,在竞争不激烈的情况下性能更好,但它会根据锁的竞争情况从偏向锁升级到轻量级锁再到重量级锁。这种升级过程在高度竞争的环境中可能会导致性能下降12。
Lock锁需要手动调用lock()方法获取锁,并在finally块中调用unlock()方法释放锁,这种机制在资源竞争激烈时能够提供更好的性能,因为它允许更精细的控制和避免不必要的等待。而synchronize锁的获取和释放由JVM自动完成,虽然简化了编程,但在高并发场景下可能不如Lock锁高效23。
总的来说,选择使用哪种同步机制应根据具体的并发需求和性能要求来决定。如果预计会有高并发和激烈的资源竞争,Lock锁可能是更合适的选择。如果并发需求不高,或者希望代码编写更简洁,synchronize锁可能更适合12。
标签:总结,调用,请求,订单,--,面试,支付,序列化,加载 From: https://blog.csdn.net/qq_43583691/article/details/142555132