目录
- 1.rabbitmq如何避免消息丢失 (三个阶段)
- 2.如何保证消息的顺序性
- 3.如何保证消息不被重复消费
- 4.缓存与数据库的不一致性
- 5.redis的过期策略和内存淘汰策略
- 6.简单说下LRU的实现
- 7.用原生redis实现分布式锁,锁误删的情况
- 8.锁续期如何去考量
- 9.缓存击穿的解决方案
- 10.Java里常用的锁
- 11.为什么要使用线程池,有什么好处?
- 12.线程池核心参数
- 13.线程池常用的拒绝策略
- 14.MySQL慢查询怎么去捞出来,然后去进一步优化
- 15.explain字段,type有几种,从高到低,扩展字段了解吗?
- 16.类加载机制,clinit方法主要做什么?
- 17.双亲委派机制的优点,为什么要用双亲委派机制?
- 18.jvm内存区域中栈帧的内部结构
- 19.设计模式,为什么要用单例模式?
- 20.volatile关键字的作用
1.rabbitmq如何避免消息丢失 (三个阶段)
首先明确一点一条消息的传送流程:生产者->MQ->消费者
我们根据这三个依次讨论
1.生产者没有成功把消息发送到MQ
丢失的原因:因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有成功接收到消息,但是生产者却以为MQ成功接收到了消息,不会再次重复发送该消息,从而导致消息的丢失。
解决办法 :有两个解决办法:事务机制和confirm机制,最常用的是confirm机制。
事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。
我们就谈谈confirm return(扩展)
rabbitmq 整个消息投递的路径为:
producer—>rabbitmq broker—>exchange—>queue—>consumer
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。
confirm:使用 rabbitTemplate.setConfirmCallback 设置回调函数。当消息发送到 exchange 后回调 confirm 方法。在方法中判断 ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
return:使用 rabbitTemplate.setReturnCallback 设置退回函数,当消息从exchange 路由到 queue 失败后,如果设置了 rabbitTemplate.setMandatory(true) 参数,则会将消息退回给 producer并执行回调函数returnedMessage
2.RabbitMQ接收到消息之后丢失了消息
丢失的原因:RabbitMQ接收到生产者发送过来的消息,是存在内存中的,如果没有被消费完,此时RabbitMQ宕机了,那么再次启动的时候,原来内存中的那些消息都丢失了。
解决方案:开启RabbitMQ的持久化。当生产者把消息成功写入RabbitMQ之后,RabbitMQ就把消息持久化到磁盘。(持久化要起作用必须同时设置这两个持久化才行)
- 持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
- 若生产者那边的confirm机制未开启的情况下,哪怕是你给rabbitmq开启了持久化机制,也有一种可能,就是这个消息写到了rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitmq挂了,就会导致内存里的一点点数据会丢失。
3.消费者弄丢了消息
丢失的原因:如果RabbitMQ成功的把消息发送给了消费者,那么RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但如果就在此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。
解决的办法:简单来说,就是必须关闭 RabbitMQ 的自动 ack
none:自动确认,manual:手动确认
如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,true);方法确认签收消息
如果出现异常,则在catch中调用 basicNack,拒绝消息,让MQ重新发送消息。
2.如何保证消息的顺序性
RabbitMQ 消息顺序错乱
对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息,如消费者 A 执行了增加,消费者 B 执行了修改,消费者 C 执行了删除,但是消费者 C 执行比消费者 B 快,消费者 B 又比消费者 A 快,就会导致消费 binlog 执行到数据库的时候顺序错乱,本该顺序是增加、修改、删除,变成了删除、修改、增加。
RabbitMQ 可能出现顺序错乱的问题示意图:
RabbitMQ 保证消息的顺序性
RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,生产者发送消息的时候,同一个订单号的消息发送到同一个 queue 中,由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。
RabbitMQ 保证消息顺序性的方案:
3.如何保证消息不被重复消费
首先,比如rabbitmq、rocketmq、kafka,都有可能会出现消息重复消费的问题。因为这个问题通常不是由mq来保证的,而是消费方自己来保证的。
那就要想到幂等性
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
(1)可在内存中维护一个set,只要从消息队列里面获取到一个消息,先查询这个消息在不在set里面,如果在表示已消费过,直接丢弃;如果不在,则在消费后将其加入set当中。
(2)如何要写数据库,可以拿唯一键先去数据库查询一下,如果不存在在写,如果存在直接更新或者丢弃消息。
(3)如果是写redis那没有问题,每次都是set,天然的幂等性。
(4)让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。
(5)数据库操作可以设置唯一键,防止重复数据的插入,这样插入只会报错而不会插入重复数据。
4.缓存与数据库的不一致性
如果不熟悉可以看这篇
深入理解redis_缓存双写一致性之更新策略探讨
考点:redis和数据库双写一致性问题
跟着这三点去答
先更新数据库,再更新缓存
先删除缓存,再更新数据库
先更新数据库,再删除缓存
先更新数据库,再更新缓存
- 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
先更新mysql修改为99成功,然后更新redis。
此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
上述发生,会让数据库里面和缓存redis里面数据不一致,读到脏数据 - 举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?
反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。
先删除缓存,再更新数据库
如果数据库更新失败,导致B线程请求再次访问缓存时,发现redis里面没数据,缓存缺失,再去读取mysql时,从数据库中读取到旧值
解决方式:采用延时双删策略
先更新数据库,再删除缓存
假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
解决方案:canal
方案2和方案3用那个?
个人建议是,优先使用先更新数据库,再删除缓存的方案。理由如下:
1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,严重导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
5.redis的过期策略和内存淘汰策略
如果不熟悉可以看这篇
Redis过期删除策略和内存淘汰策略
1.过期策略
立即删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。。
这会产生大量的性能消耗,同时也会影响数据的读取操作。
总结:对CPU不友好,用处理器性能换取存储空间
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,
如果未过期,返回数据 ;
发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息
定期删除
定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间 (随机抽查,重点抽查)
总结
Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
2.内存淘汰策略
有哪些
- noeviction: 不会驱逐任何key
- allkeys-lru: 对所有key使用LRU算法进行删除
- volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除
- allkeys-random: 对所有key随机删除
- volatile-random: 对所有设置了过期时间的key随机删除
- volatile-ttl: 删除马上要过期的key
- allkeys-lfu: 对所有key使用LFU算法进行删除
- volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
总结一下怎么记住这8个
2 * 4 得8
2个维度
过期键中筛选
所有键中筛选
4个方面
LRU
LFU
random
ttl
你平时用哪一种
平常使用allkeys-lru
系统默认的是noeviction
6.简单说下LRU的实现
如果不熟悉可以看这篇
LeetCode-146. LRU 缓存
其实就是问你写过leetcode上146. LRU 缓存吗?
LRU 算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
需要用到一个哈希表和一个双向链表。
可能会延伸你在哪里见过LRU算法
Redis的内存淘汰策略
7.用原生redis实现分布式锁,锁误删的情况
首先要知道一个靠谱分布式锁需要具备的条件和刚需
- 1.独占性
OnlyOne,任何时刻只能有且仅有一个线程持有 - 2.高可用
若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况 - 3.防死锁
杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案 - 4.不乱抢
防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。 - 5.重入性
同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
锁误删的情况
张冠李戴,删除了别人的锁
设置了过期时间,但是过期时间过了,锁被释放了,但是还没有被删除(线程A延迟了),这时候线程B进入获取了锁,结果线程A又好了,把锁删除了,这时候B线程访问的时候就发现自己的锁没了
解决方案:自己的锁设置唯一标识
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static final String Key = "donglinlock";
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public synchronized String buy_Goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(Key,value,10L,TimeUnit.SECONDS);
if (!flag){
return "抢锁失败,try again";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
stringRedisTemplate.delete(Key);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
}else{
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
} finally {
stringRedisTemplate.delete(Key);
}
}
}
根据下面的几点依次去答
- 1.synchronized单机版OK,上分布式
- 2.nginx分布式微服务单机锁不行
- 3.取消单机锁,上redis分布式锁setnx
- 4.只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
- 5.宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
- 6.为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行
- 7.必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
- 8.redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现
8.锁续期如何去考量
Redis 分布式锁过期了,但是业务逻辑还没处理完怎么办
守护线程“续命”
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
在获取锁成功后,给锁加一个 watchdog,watchdog 会起一个定时任务,在锁没有被释放且快要过期的时候会续期
缓存续命
通过redisson新建出来的锁key,默认是30秒
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。
watch dog自动延期机制
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
- 流程解释
通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了lockzzyy这个锁key的剩余生存时间),加锁失败
- 加锁查看
加锁成功后,在redis的内存数据中,就有一条hash结构的数据。
Key为锁的名称;field为随机字符串+线程ID;值为1。见下
如果同一线程多次调用lock方法,值递增1。----------可重入锁见后
- 可重入锁查看
- ttl续命的演示
加大业务逻辑处理时间,看超过10秒钟后,redisson的续命加时
解锁
9.缓存击穿的解决方案
大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql
方案1:对于访问频繁的热点key,干脆就不设置过期时间
互斥独占锁防止击穿
方案2:互斥独占锁防止击穿
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
方案3:定时轮询,互斥更新,差异失效时间
举一个案例例子
QPS上1000后导致可怕的缓存击穿
解决方法
定时轮询,互斥更新,差异失效时间
相当于B穿了一件防弹衣
我们先缓存B,然后在缓存A,设置B的过期时间要比A的长
我们先查询A,如果A为空,我们再查询B
10.Java里常用的锁
乐观锁和悲观锁
- 悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。 - 乐观锁
乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
公平锁和非公平锁
根据线程获取锁的抢占机制,锁又可以分为公平锁和非公平锁。
-
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。 -
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。ReentrantLock 提供了公平锁和非公平锁的实现。
公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock(false)
如果构造函数不传任何参数的时候,默认提供的是非公平锁。
CAS
CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 来实现的。
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
解决方案
Java 提供了一个 AtomicStampedReference 原子引用变量
11.为什么要使用线程池,有什么好处?
线程池做的主要工作就是控制运行的线程的数量,处理过程中,将任务放入到队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用、控制最大并发数、管理线程
使用多线程有下列的好处
1.降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
3.提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池常用的三个方式
Executors.newFixedThreadPool(10); 初始化线程池的大小.
Executors.newSingleThreadExecutor(); 一池,一线程!
Executors.newCachedThreadPool(); 执行异步短期任务,可扩容.
12.线程池核心参数
1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
4、unit:keepAliveTime的单位
5、workQueue:任务队列,被提交但尚未被执行的任务
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
线程池的底层工作原理(扩展)
1.在创建了线程池后,等待提交过来的任务请求
2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程立刻运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
3.当一个线程完成任务时,它会从队列中取下一个任务来执行
4.当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
13.线程池常用的拒绝策略
1.AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
2.CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
3.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
4.DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
14.MySQL慢查询怎么去捞出来,然后去进一步优化
如果不熟悉可以看这篇
Mysql查询截取分析_慢查询日志
开启慢查询
set global slow_query_log=1
设置慢查询的时间
SET GLOBAL long_query_time=0.1;
假如运行时间正好等于long_query_time的情况,并不会被记录下来。也就是说,在mysql源码里是判断大于long_query_time,而非大于等于。
使用mysqldumpslow去分析,查找sql,然后在使用explain进行分析
15.explain字段,type有几种,从高到低,扩展字段了解吗?
如果不熟悉可以看这篇
Mysql索引优化分析_explain查看执行计划
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
system>const>eq_ref>ref>range>index>ALL
Extra
Using filesort *
出现filesort的情况:order by 没有用上索引。
Using temporary *
出现Using temporary情况:分组没有用上索引。产生临时表。
Using index *
表示使用了覆盖索引 [content是一个索引]
扩展
什么是覆盖索引?
id age name sex
age -> index
select * from user where age >20 ;
第一次 取回id,第二次(回表)根据id拿到完整数据
age,name -> index
select age from user where age >20 and name like"张%" ;
覆盖索引不会回表查询,查询效率也是比较高的
using join buffer *
如果有它则表明关联字段没有使用索引!
16.类加载机制,clinit方法主要做什么?
初始化阶段的重要工作是执行类的初始化方法:< clinit >()方法。
子类加载前先加载父类?
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 < clinit >总是在子类< clinit >之前被调用。也就是说,父类的static块优先级高于子类。
口诀:由父及子,静态先行。
哪些类不会生成方法?
一个类中并没有声明任何的类变量,也没有静态代码块时
一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
代码举例:static与final的搭配问题
* 总结:
* 使用static + final 修饰的成员变量,称为:全局常量。
* 什么时候在链接阶段的准备环节:给此全局常量附的值是字面量或常量。不涉及到方法或构造器的调用。
* 除此之外,都是在初始化环节赋值的。
17.双亲委派机制的优点,为什么要用双亲委派机制?
定义: 如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
优点
- 避免类的重复加载, 确保一个类的全局唯一性(防止重复加载同一个.class)
Java 类随着它的类加载器一起具备了一种带有优先级的层级关系, 通过这种层级关系可以避免类的重复加载, 当父亲已经加载了该类时, 就没有必要子ClassLoader 再加载一次 - 保护程序安全, 防止核心 API 被随意篡改
因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。
缺点
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即项层的ClassLoader无法访问底层的ClassLoader所加载的类。
为什么Tomcat要破坏双亲委派
我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。
如多个应用都要依赖hali.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hali.Test.class。
如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
18.jvm内存区域中栈帧的内部结构
1.局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。 局部变量表的最大容量在编译期间就已经确定,保存在字节码文件的Code属性的locals里面,并且在运行期间也不会改变。
2.操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
3.动态链接,也称为指向运行时常量池的方法引用
4.方法返回地址,也称为方法退出或者异常退出的定义
5.一些附加信息
19.设计模式,为什么要用单例模式?
单例模式的五种实现方式
优点:保证了内存中全局的唯一性,避免了对象实例的重复创建,节约了系统资源。
缺点:没有接口,扩展困难
20.volatile关键字的作用
1.说说volatile关键字的特性
可见性(保证了不同线程对该变量操作的内存可见性;)
有序性(禁止指令重排序)
2.什么是内存可见性?能否举例说明?
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
3.指令重排,能举例说明吗?
在单线程程序中,重排序并不会影响程序的运行结果,而在多线程场景下就不一定了。
class ReorderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 操作1
flag = true; // 操作2
}
public void reader(){
if(flag){ // 操作3
int i = a + a; // 操作4
}
}
}
假设线程1先执行writer()方法,随后线程2执行reader()方法,最后程序一定会得到正确的结果吗?
答案是不一定的,如果代码按照下图的执行顺序执行代码则会出现问题。
操作1和操作2进行了重排序,线程1先执行flag=true,然后线程2执行操作3和操作4,线程2执行操作4时不能正确读取到a的值,导致最终程序运行结果出问题。这也说明了在多线程代码中,重排序会破坏多线程程序的语义。
1.当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
2.当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
3.当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
4.volatile能保证原子性吗?
volatile不能保证原子性,它只是对单个volatile变量的读/写具有原子性,但是对于类似i++这样的复合操作就无法保证了。
volatile变量的读写过程
read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用