From cd8e022404bce31d87dd44263a091ff7d4d5e4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 1 Oct 2019 21:02:39 +0800 Subject: [PATCH 01/97] =?UTF-8?q?Create=20=E7=A7=92=E6=9D=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\347\247\222\346\235\200.md" | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 "\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" diff --git "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" new file mode 100644 index 00000000..d71a5429 --- /dev/null +++ "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" @@ -0,0 +1,2 @@ +https://gongfukangee.github.io/2019/06/09/SecondsKill/#%E4%BB%A3%E7%A0%81%E4%BC%98%E5%8C%96 +收割阿里百度华为大佬的秒杀项目,可以参考下。 From c024f48279110afc58e550cdbf73870fa0cb518b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 1 Oct 2019 21:03:40 +0800 Subject: [PATCH 02/97] =?UTF-8?q?Create=20=E7=89=9B=E5=AE=A2=E7=BD=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\347\211\233\345\256\242\347\275\221.md" | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 "\351\241\271\347\233\256\346\216\250\350\215\220/\347\211\233\345\256\242\347\275\221.md" diff --git "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\211\233\345\256\242\347\275\221.md" "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\211\233\345\256\242\347\275\221.md" new file mode 100644 index 00000000..c4ab1741 --- /dev/null +++ "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\211\233\345\256\242\347\275\221.md" @@ -0,0 +1,3 @@ +牛客网叶神的项目也不错 + +淘宝都可以找到,当然还是建议购买正版支持哈。 From 9fedba30ce9b905eedadf2f3baa5db385fb39639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 1 Oct 2019 21:06:29 +0800 Subject: [PATCH 03/97] =?UTF-8?q?Update=20=E7=A7=92=E6=9D=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\347\247\222\346\235\200.md" | 440 +++++++++++++++++- 1 file changed, 438 insertions(+), 2 deletions(-) diff --git "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" index d71a5429..48052396 100644 --- "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" +++ "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" @@ -1,2 +1,438 @@ -https://gongfukangee.github.io/2019/06/09/SecondsKill/#%E4%BB%A3%E7%A0%81%E4%BC%98%E5%8C%96 -收割阿里百度华为大佬的秒杀项目,可以参考下。 + +--- +layout: post +title: "如何设计一个秒杀系统" +categories: [秒杀, 并发, 架构] +tags: 秒杀 并发 架构 +By:gongfukangee.github.io +author: G.Fukang +--- +如何设计一个秒杀系统 + +## 系统的特点 + +- 高性能:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键 +- 一致性:秒杀商品减库存的实现方式同样关键,有限数量的商品在同一时刻被很多倍的请求同时来减库存,在大并发更新的过程中都要保证数据的准确性。 +- 高可用:秒杀时会在一瞬间涌入大量的流量,为了避免系统宕机,保证高可用,需要做好流量限制 + +## 优化思路 + +- 后端优化:将请求尽量拦截在系统上游 + - 限流:屏蔽掉无用的流量,允许少部分流量走后端。假设现在库存为 10,有 1000 个购买请求,最终只有 10 个可以成功,99% 的请求都是无效请求 + - 削峰:秒杀请求在时间上高度集中于某一个时间点,瞬时流量容易压垮系统,因此需要对流量进行削峰处理,缓冲瞬时流量,尽量让服务器对资源进行平缓处理 + - 异步:将同步请求转换为异步请求,来提高并发量,本质也是削峰处理 + - 利用缓存:创建订单时,每次都需要先查询判断库存,只有少部分成功的请求才会创建订单,因此可以将商品信息放在缓存中,减少数据库查询 + - 负载均衡:利用 Nginx 等使用多个服务器并发处理请求,减少单个服务器压力 +- 前端优化: + - 限流:前端答题或验证码,来分散用户的请求 + - 禁止重复提交:限定每个用户发起一次秒杀后,需等待才可以发起另一次请求,从而减少用户的重复请求 + - 本地标记:用户成功秒杀到商品后,将提交按钮置灰,禁止用户再次提交请求 + - 动静分离:将前端静态数据直接缓存到离用户最近的地方,比如用户浏览器、CDN 或者服务端的缓存中 +- 防作弊优化: + - 隐藏秒杀接口:如果秒杀地址直接暴露,在秒杀开始前可能会被恶意用户来刷接口,因此需要在没到秒杀开始时间不能获取秒杀接口,只有秒杀开始了,才返回秒杀地址 url 和验证 MD5,用户拿到这两个数据才可以进行秒杀 + - 同一个账号多次发出请求:在前端优化的禁止重复提交可以进行优化;也可以使用 Redis 标志位,每个用户的所有请求都尝试在 Redis 中插入一个 `userId_secondsKill` 标志位,成功插入的才可以执行后续的秒杀逻辑,其他被过滤掉,执行完秒杀逻辑后,删除标志位 + - 多个账号一次性发出多个请求:一般这种请求都来自同一个 IP 地址,可以检测 IP 的请求频率,如果过于频繁则弹出一个验证码 + - 多个账号不同 IP 发起不同请求:这种一般都是僵尸账号,检测账号的活跃度或者等级等信息,来进行限制。比如微博抽奖,用 iphone 的年轻女性用户中奖几率更大。通过用户画像限制僵尸号无法参与秒杀或秒杀不能成功 + +## 代码优化 + +### Jmeter 压测并发量变化图 + +![]() + +### 0. 基本秒杀逻辑 + +```java +@Override +public int createWrongOrder(int sid) throws Exception { + // 数据库校验库存 + Stock stock = checkStock(sid); + // 扣库存(无锁) + saleStock(stock); + // 生成订单 + int res = createOrder(stock); + return res; +} +private Stock checkStock(int sid) throws Exception { + Stock stock = stockService.getStockById(sid); + if (stock.getCount() < 1) { + throw new RuntimeException("库存不足"); + } + return stock; +} +private int saleStock(Stock stock) { + stock.setSale(stock.getSale() + 1); + stock.setCount(stock.getCount() - 1); + return stockService.updateStockById(stock); +} +private int createOrder(Stock stock) throws Exception { + StockOrder order = new StockOrder(); + order.setSid(stock.getId()); + order.setName(stock.getName()); + order.setCreateTime(new Date()); + int res = orderMapper.insertSelective(order); + if (res == 0) { + throw new RuntimeException("创建订单失败"); + } + return res; +} +// 扣库存 Mapper 文件 +@Update("UPDATE stock SET count = #{count, jdbcType = INTEGER}, name = #{name, jdbcType = VARCHAR}, " + "sale = #{sale,jdbcType = INTEGER},version = #{version,jdbcType = INTEGER} " + "WHERE id = #{id, jdbcType = INTEGER}") +``` + +### 1. 乐观锁更新库存,解决超卖问题 + +超卖问题出现的场景 + +![](https://github.com/gongfukangEE/gongfukangEE.github.io/raw/master/_pic/%E5%88%86%E5%B8%83%E5%BC%8F/%E7%A7%92%E6%9D%80%E8%B6%85%E5%8D%96.png) + +悲观锁虽然可以解决超卖问题,但是加锁的时间可能会很长,会长时间的限制其他用户的访问,导致很多请求等待锁,卡死在这里,如果这种请求很多就会耗尽连接,系统出现异常。乐观锁默认不加锁,更失败就直接返回抢购失败,可以承受较高并发 + +![](https://github.com/gongfukangEE/gongfukangEE.github.io/raw/master/_pic/%E5%88%86%E5%B8%83%E5%BC%8F/%E4%B9%90%E8%A7%82%E9%94%81%E6%89%A3%E5%BA%93%E5%AD%98.png) + +```java +@Override +public int createOptimisticOrder(int sid) throws Exception { + // 校验库存 + Stock stock = checkStock(sid); + // 乐观锁更新 + saleStockOptimstic(stock); + // 创建订单 + int id = createOrder(stock); + return id; +} +// 乐观锁 Mapper 文件 +@Update("UPDATE stock SET count = count - 1, sale = sale + 1, version = version + 1 WHERE " + + "id = #{id, jdbcType = INTEGER} AND version = #{version, jdbcType = INTEGER}") +``` + +### 2. Redis 计数限流 + +根据前面的优化分析,假设现在有 10 个商品,有 1000 个并发秒杀请求,最终只有 10 个订单会成功创建,也就是说有 990 的请求是无效的,这些无效的请求也会给数据库带来压力,因此可以在在请求落到数据库之前就将无效的请求过滤掉,将并发控制在一个可控的范围,这样落到数据库的压力就小很多 + +关于限流的方法,可以看这篇博客[浅析限流算法](),由于计数限流实现起来比较简单,因此采用计数限流,限流的实现可以直接使用 Guava 的 RateLimit 方法,但是由于后续需要将实例通过 Nginx 实现负载均衡,这里选用 Redis 实现分布式限流 + +在 `RedisPool` 中对 `Jedis` 线程池进行了简单的封装,封装了初始化和关闭方法,同时在 `RedisPoolUtil` 中对 Jedis 常用 API 进行简单封装,每个方法调用完毕则关闭 Jedis 连接。 + +限流要保证写入 Redis 操作的原子性,因此利用 Redis 的单线程机制,通过 LUA 脚本来完成。 + +![](https://github.com/gongfukangEE/gongfukangEE.github.io/raw/master/_pic/%E5%88%86%E5%B8%83%E5%BC%8F/%E7%A7%92%E6%9D%80%E9%99%90%E6%B5%81.png) + +```java +@Slf4j +public class RedisLimit { + + private static final int FAIL_CODE = 0; + + private static Integer limit = 5; + + /** + * Redis 限流 + */ + public static Boolean limit() { + Jedis jedis = null; + Object result = null; + try { + // 获取 jedis 实例 + jedis = RedisPool.getJedis(); + // 解析 Lua 文件 + String script = ScriptUtil.getScript("limit.lua"); + // 请求限流 + String key = String.valueOf(System.currentTimeMillis() / 1000); + // 计数限流 + result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit))); + if (FAIL_CODE != (Long) result) { + log.info("成功获取令牌"); + return true; + } + } catch (Exception e) { + log.error("Limit 获取 Jedis 实例失败:", e); + } finally { + RedisPool.jedisPoolClose(jedis); + } + return false; + } +} +// 在 Controller 中,每个请求到来先取令牌,获取到令牌再执行后续操作,获取不到直接返回 ERROR +public String createOptimisticLimitOrder(HttpServletRequest request, int sid) { + int res = 0; + try { + if (RedisLimit.limit()) { + res = orderService.createOptimisticOrder(sid); + } + } catch (Exception e) { + log.error("Exception: " + e); + } + return res == 1 ? success : error; +} +``` + +### 3. Redis 缓存商品库存信息 + +虽然限流能够过滤掉一些无效的请求,但是还是会有很多请求落在数据库上,通过 `Druid` 监控可以看出,实时查询库存的语句被大量调用,对于每个没有被过滤掉的请求,都会去数据库查询库存来判断库存是否充足,对于这个查询可以放在缓存 Redis 中,Redis 的数据是存放在内存中的,速度快很多。 + +![]() + +#### 缓存预热 + +在秒杀开始前,需要将秒杀商品信息提前缓存到 Redis 中,这么秒杀开始时则直接从 Redis 中读取,也就是缓存预热,Springboot 中开发者通过 `implement ApplicationRunner` 来设定 SpringBoot 启动后立即执行的方法 + +```java +@Component +public class RedisPreheatRunner implements ApplicationRunner { + + @Autowired + private StockService stockService; + + @Override + public void run(ApplicationArguments args) throws Exception { + // 从数据库中查询热卖商品,商品 id 为 1 + Stock stock = stockService.getStockById(1); + // 删除旧缓存 + RedisPoolUtil.del(RedisKeysConstant.STOCK_COUNT + stock.getCount()); + RedisPoolUtil.del(RedisKeysConstant.STOCK_SALE + stock.getSale()); + RedisPoolUtil.del(RedisKeysConstant.STOCK_VERSION + stock.getVersion()); + //缓存预热 + int sid = stock.getId(); + RedisPoolUtil.set(RedisKeysConstant.STOCK_COUNT + sid, String.valueOf(stock.getCount())); + RedisPoolUtil.set(RedisKeysConstant.STOCK_SALE + sid, String.valueOf(stock.getSale())); + RedisPoolUtil.set(RedisKeysConstant.STOCK_VERSION + sid, String.valueOf(stock.getVersion())); + } +} +``` + +#### 缓存和数据一致性 + +缓存和 DB 的一致性是一个讨论很多的问题,推荐看参考中的 [使用缓存的正确姿势](),首先看下先更新数据库,再更新缓存策略,假设 A、B 两个线程,A 成功更新数据,在要更新缓存时,A 的时间片用完了,B 更新了数据库接着更新了缓存,这是 CPU 再分配给 A,则 A 又更新了缓存,这种情况下缓存中就是脏数据,具体逻辑如下图所示: + +![]() + +那么,如果避免这个问题呢?就是缓存不做更新,仅做删除,先更新数据库再删除缓存。对于上面的问题,A 更新了数据库,还没来得及删除缓存,B 又更新了数据库,接着删除了缓存,然后 A 删除了缓存,这样只有下次缓存未命中时,才会从数据库中重建缓存,避免了脏数据。但是,也会有极端情况出现脏数据,A 做查询操作,没有命中缓存,从数据库中查询,但是还没来得及更新缓存,B 就更新了数据库,接着删除了缓存,然后 A 又重建了缓存,这时 A 中的就是脏数据,如下图所示。但是这种极端情况需要数据库的写操作前进入数据库,又晚于写操作删除缓存来更新缓存,发生的概率极其小,不过为了避免这种情况,可以为缓存设置过期时间。 + +![]() + +安装先更新数据库再删除缓存的策略来执行,代码如下所示: + +```java +@Override +public int createOrderWithLimitAndRedis(int sid) throws Exception { + // 校验库存,从 Redis 中获取 + Stock stock = checkStockWithRedis(sid); + // 乐观锁更新库存和Redis + saleStockOptimsticWithRedis(stock); + // 创建订单 + int res = createOrder(stock); + return res; +} +// Redis 校验库存 +private Stock checkStockWithRedisWithDel(int sid) throws Exception { + Integer count = null; + Integer sale = null; + Integer version = null; + List data = RedisPoolUtil.listGet(RedisKeysConstant.STOCK + sid); + if (data.size() == 0) { + // Redis 不存在,先从数据库中获取,再放到 Redis 中 + Stock newStock = stockService.getStockById(sid); + RedisPoolUtil.listPut(RedisKeysConstant.STOCK + newStock.getId(), String.valueOf(newStock.getCount()), + String.valueOf(newStock.getSale()), String.valueOf(newStock.getVersion())); + count = newStock.getCount(); + sale = newStock.getSale(); + version = newStock.getVersion(); + } else { + count = Integer.parseInt(data.get(0)); + sale = Integer.parseInt(data.get(1)); + version = Integer.parseInt(data.get(2)); + } + if (count < 1) { + log.info("库存不足"); + throw new RuntimeException("库存不足 Redis currentCount: " + sale); + } + Stock stock = new Stock(); + stock.setId(sid); + stock.setCount(count); + stock.setSale(sale); + stock.setVersion(version); + // 此处应该是热更新,但是在数据库中只有一个商品,所以直接赋值 + stock.setName("手机"); + return stock; +} +private void saleStockOptimsticWithRedisWithDel(Stock stock) throws Exception { + // 乐观锁更新数据库 + int res = stockService.updateStockByOptimistic(stock); + // 删除缓存,应该使用 Redis 事务 + RedisPoolUtil.del(RedisKeysConstant.STOCK + stock.getId()); + log.info("删除缓存成功"); + if (res == 0) { + throw new RuntimeException("并发更新库存失败"); + } +} +``` + +在 Jmeter 压力测试中,并发效果并不好,跟前面的限流并发差不多,观察 Redis 中的数据看出,由于每次都删除缓存,因此导致多次缓存都不能命中,能命中缓存的次数很少,因此这种方案并不可取。 + +考虑到使用乐观锁更新数据库,因此在使用先更新数据库再更新缓存的策略中,实际情况如下所示 + +![]() + +在 A 未更新缓存阶段,虽然 B 从缓存中获取到的库存信息脏数据,但是,乐观锁使得 B 在更新数据库时失败,这时 A 又更新了缓存,则保证了数据的最终一致性,并且由于缓存一直都可以命中,对并发量的提升也是很显著的。 + +```java +@Override +public int createOrderWithLimitAndRedis(int sid) throws Exception { + // 校验库存,从 Redis 中获取 + Stock stock = checkStockWithRedis(sid); + // 乐观锁更新库存和Redis + saleStockOptimsticWithRedis(stock); + // 创建订单 + int res = createOrder(stock); + return res; +} +// Redis 中校验库存 +private Stock checkStockWithRedis(int sid) throws Exception { + Integer count = Integer.parseInt(RedisPoolUtil.get(RedisKeysConstant.STOCK_COUNT + sid)); + Integer sale = Integer.parseInt(RedisPoolUtil.get(RedisKeysConstant.STOCK_SALE + sid)); + Integer version = Integer.parseInt(RedisPoolUtil.get(RedisKeysConstant.STOCK_VERSION + sid)); + if (count < 1) { + log.info("库存不足"); + throw new RuntimeException("库存不足 Redis currentCount: " + sale); + } + Stock stock = new Stock(); + stock.setId(sid); + stock.setCount(count); + stock.setSale(sale); + stock.setVersion(version); + // 此处应该是热更新,但是在数据库中只有一个商品,所以直接赋值 + stock.setName("手机"); + + return stock; +} +// 更新 DB 和 Redis +private void saleStockOptimsticWithRedis(Stock stock) throws Exception { + int res = stockService.updateStockByOptimistic(stock); + if (res == 0){ + throw new RuntimeException("并发更新库存失败") ; + } + // 更新 Redis + StockWithRedis.updateStockWithRedis(stock); +} +// Redis 多个写入操作的事务 +public static void updateStockWithRedis(Stock stock) { + Jedis jedis = null; + try { + jedis = RedisPool.getJedis(); + // 开始事务 + Transaction transaction = jedis.multi(); + // 事务操作 + RedisPoolUtil.decr(RedisKeysConstant.STOCK_COUNT + stock.getId()); + RedisPoolUtil.incr(RedisKeysConstant.STOCK_SALE + stock.getId()); + RedisPoolUtil.incr(RedisKeysConstant.STOCK_VERSION + stock.getId()); + // 结束事务 + List list = transaction.exec(); + } catch (Exception e) { + log.error("updateStock 获取 Jedis 实例失败:", e); + } finally { + RedisPool.jedisPoolClose(jedis); + } +} +``` + +#### 发现热点数据 + +热点数据就是用户的热点请求对应的数据,分成静态热点数据和动态热点数据。 + +静态热点数据就是能够提前预测的数据,比如约定商品 A、B、C 参与秒杀,则可以提前对商品进行标记处理。动态热点数据就是不能被提前预测的,比如在商家在抖音上投放广告,导致商品短时间内被大量购买,临时产生热点数据。对于动态热点数据,最主要的就是能够提前预测和发现,以便于及时处理,这里给出[极客时间:许令波 - 如何设计一个秒杀系统]()中对于热点数据发现系统的实现: + +1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key +2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。 +3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。 + +![]() + +我们通过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到 Cache 中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据。 + +对于热点数据,除了上文所提到的缓存,还要进行隔离和限制,比如把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源;将这种热点数据隔离出来,不要让 1% 的请求影响到另外的 99% + +### 4. Kafka 异步 + +服务器的资源是恒定的,你用或者不用它的处理能力都是一样的,所以出现峰值的话,很容易导致忙到处理不过来,闲的时候却又没有什么要处理,因此可以通过削峰来延缓用户请求的发出,让服务端处理变得更加平稳。 + +项目中采用的是用消息队列 Kafka 来缓冲瞬时流量,将同步的直接调用转成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。 + +![]() + +关于 Kafka 的学习,推荐[朱小厮的博客]()和博主的书《深入理解 Kafka:核心设计与实践原理》,向 Kafka 发送消息和从 Kafka 拉取消息需要对消息进行序列化处理,这里采用的是`Gson`框架 + +```java +// 向 Kafka 发送消息 +public void createOrderWithLimitAndRedisAndKafka(int sid) throws Exception { + // 校验库存 + Stock stock = checkStockWithRedis(sid); + // 下单请求发送至 kafka,需要序列化 stock + kafkaTemplate.send(kafkaTopic, gson.toJson(stock)); + log.info("消息发送至 Kafka 成功"); +} +// 监听器从 Kafka 拉取消息 +public class ConsumerListen { + + private Gson gson = new GsonBuilder().create(); + + @Autowired + private OrderService orderService; + + @KafkaListener(topics = "SECONDS-KILL-TOPIC") + public void listen(ConsumerRecord record) throws Exception { + Optional kafkaMessage = Optional.ofNullable(record.value()); + // Object -> String + String message = (String) kafkaMessage.get(); + // 反序列化 + Stock stock = gson.fromJson((String) message, Stock.class); + // 创建订单 + orderService.consumerTopicToCreateOrderWithKafka(stock); + } +} +// Kafka 消费消息执行创建订单业务 +public int consumerTopicToCreateOrderWithKafka(Stock stock) throws Exception { + // 乐观锁更新库存和 Redis + saleStockOptimsticWithRedis(stock); + int res = createOrder(stock); + if (res == 1) { + log.info("Kafka 消费 Topic 创建订单成功"); + } else { + log.info("Kafka 消费 Topic 创建订单失败"); + } + + return res; +} +``` + +## Github + +完整代码已经放在 [Github]() + +## 数据库建表 + +```mysql +CREATE TABLE `stock` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', + `count` int(11) NOT NULL COMMENT '库存', + `sale` int(11) NOT NULL COMMENT '已售', + `version` int(11) NOT NULL COMMENT '乐观锁,版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; +CREATE TABLE `stock_order` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `sid` int(11) NOT NULL COMMENT '库存ID', + `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8; +``` + +## 参考 + +>- [极客时间:许令波 - 如何设计一个秒杀系统]() +>- [crossoverjie:SSM(十八)秒杀架构实践]() +>- [秒杀系统优化方案(下)吐血整理]() +>- [电商网站秒杀与抢购的系统架构](http://www.codeceo.com/article/spike-system-artch.html) +>- [使用缓存的正确姿势]() From 8c05552432369055a1b90f5735156b3b06b9fd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 1 Oct 2019 21:07:02 +0800 Subject: [PATCH 04/97] =?UTF-8?q?Update=20=E7=A7=92=E6=9D=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\347\247\222\346\235\200.md" | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" index 48052396..2b0c0251 100644 --- "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" +++ "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" @@ -3,8 +3,7 @@ layout: post title: "如何设计一个秒杀系统" categories: [秒杀, 并发, 架构] -tags: 秒杀 并发 架构 -By:gongfukangee.github.io +By: gongfukangee.github.io author: G.Fukang --- 如何设计一个秒杀系统 From 0edd0bfc9b11a3aab75e9390176d6a97e61c5b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Tue, 1 Oct 2019 21:07:55 +0800 Subject: [PATCH 05/97] =?UTF-8?q?Update=20=E7=A7=92=E6=9D=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\347\247\222\346\235\200.md" | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" index 2b0c0251..5f148136 100644 --- "a/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" +++ "b/\351\241\271\347\233\256\346\216\250\350\215\220/\347\247\222\346\235\200.md" @@ -1,9 +1,8 @@ - --- layout: post title: "如何设计一个秒杀系统" categories: [秒杀, 并发, 架构] -By: gongfukangee.github.io +By: gongfukangee.github.io author: G.Fukang --- 如何设计一个秒杀系统 From 899e51a3309a43abaa15b398ae0098a49cb55a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 16:36:42 +0800 Subject: [PATCH 06/97] Create 1.md --- docs/1.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/1.md diff --git a/docs/1.md b/docs/1.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/docs/1.md @@ -0,0 +1 @@ + From d3a3cd80db956adabc3a60f6b46b78b538c329d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 17:08:33 +0800 Subject: [PATCH 07/97] =?UTF-8?q?Create=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 "Java\351\233\206\345\220\210.md" diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" new file mode 100644 index 00000000..5533a8dc --- /dev/null +++ "b/Java\351\233\206\345\220\210.md" @@ -0,0 +1,10 @@ +说出所有java集合类 +一.HashMap +https://blog.csdn.net/jiary5201314/article/details/51439982 +(1)hashMap的原理 +hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap +里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点 +2.hashcode的计算 +https://www.zhihu.com/question/20733617/answer/111577937 +https://blog.csdn.net/justloveyou_/article/details/62893086 +Key.hashcode是key的自带的hascode函数是一个int值32位 From cc7468235d1e10ee9f85eba590004e7ec1e9d8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 17:39:36 +0800 Subject: [PATCH 08/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 5533a8dc..afd17da7 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -1,10 +1,18 @@ -说出所有java集合类 -一.HashMap + +# 一.HashMap https://blog.csdn.net/jiary5201314/article/details/51439982 -(1)hashMap的原理 +## 1.hashMap的原理 hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap 里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点 -2.hashcode的计算 +## 2.hashcode的计算 https://www.zhihu.com/question/20733617/answer/111577937 https://blog.csdn.net/justloveyou_/article/details/62893086 Key.hashcode是key的自带的hascode函数是一个int值32位 + +hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或低 16 位 +>return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); +首先确认:当 length 总是 2 的n 次方时, h & (length - 1) 等价于 hash 对length 取模 , 但是&比%具有更高的效率; +>Jdk1.7 之前:h & (length - 1);//第三步,取模运算 + + + From 920e24120fe128f547b83aed6b2885dc3a5aca11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 18:01:00 +0800 Subject: [PATCH 09/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 6 ++++++ 1 file changed, 6 insertions(+) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index afd17da7..da6f9db9 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -14,5 +14,11 @@ hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或 首先确认:当 length 总是 2 的n 次方时, h & (length - 1) 等价于 hash 对length 取模 , 但是&比%具有更高的效率; >Jdk1.7 之前:h & (length - 1);//第三步,取模运算 +![](https://github.com/gzc426/picts/blob/master/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191002175517.jpg) + +## 3.hashMap参数以及扩容机制 +初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的n 次方 +扩容机制: +使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 From 9fe0ad83c07b68c7f7a750df2ed525d1e059e6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 18:22:26 +0800 Subject: [PATCH 10/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index da6f9db9..bbe82e90 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -20,5 +20,38 @@ hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或 初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的n 次方 扩容机制: 使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 +## 4.get()方法 +## 5.put()方法 +这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现 + +## 6.HashMap问题 jdk1.8优化 +(1)HashMap如果有很多相同key,后面的链很长的话,你会怎么优化?或者你会用什么数据结构来存储?针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),怎么优化? +Java8 做的改变: +- HashMap 是数组+链表+红黑树(JDK1.8 增加了红黑树部分),当链表长度>=8 时转化为红黑树 +在 JDK1.8 版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能,其中会用到红黑树的插入、删除、查找等算法。 +- java8 中对 hashmap 扩容不是重新计算所有元素在数组的位置,而是我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash, 只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。 + + +## 7.一些面试题 + +### 7.1 HashMap 和 TreeMap 的区别 +Hashmap 使用的是数组+链表,treemap 是红黑树 + +### 7.2 hashmap 为什么可以插入空值? +HashMap 中添加 key==null 的 Entry 时会调用 putForNullKey 方法直接去遍历 +table[0]Entry 链表,寻找 e.key==null 的 Entry 或者没有找到遍历结束 + +如果找到了 e.key==null,就保存 null 值对应的原值 oldValue,然后覆盖原值,并返回oldValue +如果在 table[0]Entry 链表中没有找到就调用addEntry 方法添加一个 key 为 null 的 Entry +### 7.3 Hashmap 为什么线程不安全:(hash 碰撞和扩容导致) +- HashMap 底层是一个Entry 数组,当发生 hash 冲突的时候,hashmap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。假如 A 线程和B 线程同时对同一个数组位置调用 addEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖A 的写入操作造成A 的写入操作丢失 +- 删除键值对的代码如上:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去, 其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改 +- 当多个线程同时检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。 +### 7.4 Hashmap 碰撞严重 +可以自定义重写 hash——多 hash 函数 +### 7.4 HashMap 高并发情况下会出现什么问题? +扩容问题 +### 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法? +答:hashCode 和equals。通过 hash(hashCode)然后模运算(其实是与的位操作)定位在Entry 数组中的下标,然后遍历这之后的链表,通过 equals 比较有没有相同的 key,如果有直接覆盖 value,如果没有就重新创建一个 Entry。 From 46529444eaac9a6d42c7b8577d2c4437f4febc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 18:37:14 +0800 Subject: [PATCH 11/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index bbe82e90..81c61da9 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -21,8 +21,10 @@ hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或 扩容机制: 使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 ## 4.get()方法 - +**整个过程都不需要加锁** +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%871.png) ## 5.put()方法 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%872.png) 这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现 ## 6.HashMap问题 jdk1.8优化 From 0164c6fc2e3e05b9c6f132350f66af04bcf982cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 18:38:03 +0800 Subject: [PATCH 12/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 81c61da9..879f3a6c 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -1,10 +1,10 @@ -# 一.HashMap +# 一. HashMap https://blog.csdn.net/jiary5201314/article/details/51439982 -## 1.hashMap的原理 +## 1. hashMap的原理 hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap 里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点 -## 2.hashcode的计算 +## 2. hashcode的计算 https://www.zhihu.com/question/20733617/answer/111577937 https://blog.csdn.net/justloveyou_/article/details/62893086 Key.hashcode是key的自带的hascode函数是一个int值32位 @@ -16,18 +16,18 @@ hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或 ![](https://github.com/gzc426/picts/blob/master/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191002175517.jpg) -## 3.hashMap参数以及扩容机制 +## 3. hashMap参数以及扩容机制 初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的n 次方 扩容机制: 使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 -## 4.get()方法 +## 4. get()方法 **整个过程都不需要加锁** ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%871.png) -## 5.put()方法 +## 5. put()方法 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%872.png) 这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现 -## 6.HashMap问题 jdk1.8优化 +## 6. HashMap问题 jdk1.8优化 (1)HashMap如果有很多相同key,后面的链很长的话,你会怎么优化?或者你会用什么数据结构来存储?针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),怎么优化? Java8 做的改变: - HashMap 是数组+链表+红黑树(JDK1.8 增加了红黑树部分),当链表长度>=8 时转化为红黑树 @@ -35,7 +35,7 @@ Java8 做的改变: - java8 中对 hashmap 扩容不是重新计算所有元素在数组的位置,而是我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash, 只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。 -## 7.一些面试题 +## 7. 一些面试题 ### 7.1 HashMap 和 TreeMap 的区别 Hashmap 使用的是数组+链表,treemap 是红黑树 From 9910f1fae5c05c813216f65fe1cf2aac0cb39827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 18:48:00 +0800 Subject: [PATCH 13/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 5 +++++ 1 file changed, 5 insertions(+) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 879f3a6c..c5a89639 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -57,3 +57,8 @@ table[0]Entry 链表,寻找 e.key==null 的 Entry 或者没有找到遍历结 ### 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法? 答:hashCode 和equals。通过 hash(hashCode)然后模运算(其实是与的位操作)定位在Entry 数组中的下标,然后遍历这之后的链表,通过 equals 比较有没有相同的 key,如果有直接覆盖 value,如果没有就重新创建一个 Entry。 +### 7.6 Hashmap为什么线程不安全 +hash碰撞和扩容导致,HashMap扩容的的时候可能会形成环形链表,造成死循环。 +### 7.7 Hashmap中的key可以为任意对象或数据类型吗? +可以为null但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode也进行相应的改变,导致下次无法查找到己存在Map中的效据 +如果可变对象在 HashMap中被用作键,时就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可. From 701904e53737759380d209bbf910016ce078b5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 19:33:46 +0800 Subject: [PATCH 14/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index c5a89639..7a45730b 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -62,3 +62,20 @@ hash碰撞和扩容导致,HashMap扩容的的时候可能会形成环形链表 ### 7.7 Hashmap中的key可以为任意对象或数据类型吗? 可以为null但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode也进行相应的改变,导致下次无法查找到己存在Map中的效据 如果可变对象在 HashMap中被用作键,时就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可. + +# 二. CurrentHashMap +http://www.importnew.com/21781.html +https://blog.csdn.net/dingji_ping/article/details/51005799 +https://www.cnblogs.com/chengxiao/p/6842045.html +http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C%E8%BF%99%E7%AF%87%E6%B2%A1%E4%BA%BA%E8%83%BD%E9%9A%BE%E4%BD%8F%E4%BD%A0%EF%BC%81/ +## 1. 概述 + +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%873.png) +一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。 +## 2. JDK1.7 ConCurrentHashMap原理 +其中 Segment 继承于 ReentrantLock +ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问, 能够实现真正的并发访问。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%874.png) +Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%875.png) +## 3. JDK1.7 Get From c9982e47fb9ee94923c544437cdcff124db00f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 19:59:10 +0800 Subject: [PATCH 15/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 7a45730b..5f87a262 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -79,3 +79,32 @@ ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储 Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%875.png) ## 3. JDK1.7 Get +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%876.png) +CurrentHashMap是否使用了锁??? +它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。 这里可以看出并没有使用锁,但是value的值为null时候才是使用了加锁!!! +Get原理: +第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?看一下count变量的定义: transient volatile int count; +它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%877.png) + +1)在get代码的①和②之间,另一个线程新增了一个entry +如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。下图大致描述了put 一个新的entry的过程。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%878.png) +因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。 如果在这个new的对象的后面,则完全不影响,如果刚好是这个new的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的value值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值! + +2)在get代码的①和②之间,另一个线程修改了一个entry的value +value是用volitale修饰的,可以保证读取时获取到的是修改后的值。 +3)在get代码的①之后,另一个线程删除了一个entry +假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: + +如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了,也就是说没办法看到最新的。 +我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 +不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 + +2. JDK1.7 PUT +1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 +2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 +3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 +4.最后会解除在 1 中所获取当前 Segment 的锁。 + +可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. From 78783f29cdffefa9c38caefcbeef0e6b6ce24ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 20:10:54 +0800 Subject: [PATCH 16/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 5f87a262..751f3a99 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -75,26 +75,32 @@ http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C% ## 2. JDK1.7 ConCurrentHashMap原理 其中 Segment 继承于 ReentrantLock ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问, 能够实现真正的并发访问。 + ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%874.png) + Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%875.png) ## 3. JDK1.7 Get ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%876.png) -CurrentHashMap是否使用了锁??? +- CurrentHashMap是否使用了锁??? + 它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。 这里可以看出并没有使用锁,但是value的值为null时候才是使用了加锁!!! -Get原理: + +- Get原理: 第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?看一下count变量的定义: transient volatile int count; 它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%877.png) -1)在get代码的①和②之间,另一个线程新增了一个entry +### 3.1 在get代码的①和②之间,另一个线程新增了一个entry 如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。下图大致描述了put 一个新的entry的过程。 + ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%878.png) + 因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。 如果在这个new的对象的后面,则完全不影响,如果刚好是这个new的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的value值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值! -2)在get代码的①和②之间,另一个线程修改了一个entry的value +### 3.2 在get代码的①和②之间,另一个线程修改了一个entry的value value是用volitale修饰的,可以保证读取时获取到的是修改后的值。 -3)在get代码的①之后,另一个线程删除了一个entry +### 3.3 在get代码的①之后,另一个线程删除了一个entry 假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: 如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了,也就是说没办法看到最新的。 From b92babc78f53be9ad0a6d2ff83095b880164af8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 20:13:09 +0800 Subject: [PATCH 17/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 751f3a99..e3a0a7ba 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -97,12 +97,12 @@ Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%878.png) 因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。 如果在这个new的对象的后面,则完全不影响,如果刚好是这个new的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的value值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值! - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%879.png) ### 3.2 在get代码的①和②之间,另一个线程修改了一个entry的value value是用volitale修饰的,可以保证读取时获取到的是修改后的值。 ### 3.3 在get代码的①之后,另一个线程删除了一个entry 假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8710.png) 如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了,也就是说没办法看到最新的。 我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 From 19ddb26bf7e791fc016fff2281745760e433c946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 20:16:21 +0800 Subject: [PATCH 18/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index e3a0a7ba..d7ad015f 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -107,10 +107,9 @@ value是用volitale修饰的,可以保证读取时获取到的是修改后的 我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 -2. JDK1.7 PUT -1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 -2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 -3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 -4.最后会解除在 1 中所获取当前 Segment 的锁。 - -可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. +# 4. JDK1.7 PUT +- 1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 +- 2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 +- 3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 +- 4.最后会解除在 1 中所获取当前 Segment 的锁。 +- 5.可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. From 7f4a8e3d847212503e6219b2a7dca8381560ed1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 20:40:23 +0800 Subject: [PATCH 19/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index d7ad015f..d700148e 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -107,9 +107,40 @@ value是用volitale修饰的,可以保证读取时获取到的是修改后的 我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 -# 4. JDK1.7 PUT +### 4. JDK1.7 PUT - 1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 - 2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 - 3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 - 4.最后会解除在 1 中所获取当前 Segment 的锁。 - 5.可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8711.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8712.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8713.png) +### 5. JDK1.7 Remove +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8714.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8715.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8716.png) +### 6. JDK1.7 & JDK1.8 size() +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8720.png) +``` +public int size() { + long n = sumCount(); + return ((n < 0L) ? 0 : + (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int)n); +} +``` +volatile 保证内存可见,最大是65535. + +### 5.JDK 1.8 CurrentHashMap概述 + +1.其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 +2.大于8的时候才去红黑树链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。 +6.JDK1.8 put +- 根据 key 计算出 hashcode 。 +- 判断是否需要进行初始化。 +- f即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 +- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。 +- 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。 +- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 + From 7ee1674f95c5514603c9445aef098426339f1ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Wed, 2 Oct 2019 23:45:48 +0800 Subject: [PATCH 20/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index d700148e..5cc225ca 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -133,10 +133,11 @@ public int size() { volatile 保证内存可见,最大是65535. ### 5.JDK 1.8 CurrentHashMap概述 - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8717.png) 1.其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 2.大于8的时候才去红黑树链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。 -6.JDK1.8 put +### 6. JDK1.8 put +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8718.png) - 根据 key 计算出 hashcode 。 - 判断是否需要进行初始化。 - f即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 @@ -144,3 +145,14 @@ volatile 保证内存可见,最大是65535. - 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。 - 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 +### 7. JDK1.8 get方法 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8719.png) +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 就不满足那就按照链表的方式遍历获取值。 +### 8.rehash过程 + Redis rehash :dictRehash每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[0]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。 +可以看到,redis对dict的rehash是分批进行的,这样不会阻塞请求,设计的比较优雅。 +但是在调用dictFind的时候,可能需要对两张dict表做查询。唯一的优化判断是,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。 +dictAdd的时候,如果状态是rehashing,则把值插入到ht[1],否则ht[0] + From b34b7d8b33cd946323718a8c493a3e0b1f30396a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 11:29:15 +0800 Subject: [PATCH 21/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 165 +++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 5cc225ca..560d0650 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -151,8 +151,165 @@ volatile 保证内存可见,最大是65535. - 如果是红黑树那就按照树的方式获取值。 - 就不满足那就按照链表的方式遍历获取值。 ### 8.rehash过程 - Redis rehash :dictRehash每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[0]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。 -可以看到,redis对dict的rehash是分批进行的,这样不会阻塞请求,设计的比较优雅。 -但是在调用dictFind的时候,可能需要对两张dict表做查询。唯一的优化判断是,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。 -dictAdd的时候,如果状态是rehashing,则把值插入到ht[1],否则ht[0] +- Redis rehash :dictRehash每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[0]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。 +- 可以看到,redis对dict的rehash是分批进行的,这样不会阻塞请求,设计的比较优雅。 +- 但是在调用dictFind的时候,可能需要对两张dict表做查询。唯一的优化判断是,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。 +- dictAdd的时候,如果状态是rehashing,则把值插入到ht[1],否则ht[0] +# 三 Hashtable +https://blog.csdn.net/ns_code/article/details/36191279 +## 1.参数 +-(1)table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 +-(2)count是Hashtable的大小,它是Hashtable保存的键值对的数量。 +- (3)threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。 +- (4)loadFactor就是加载因子。 +- (5)modCount是用来实现fail-fast机制的 +## 1.put +从下面的代码中我们可以看出,Hashtable中的key和value是不允许为空的,当我们想要想Hashtable中添加元素的时候,首先计算key的hash值,然 +后通过hash值确定在table数组中的索引位置,最后将value值替换或者插入新的元素,如果容器的数量达到阈值,就会进行扩充。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8721.png) +## 2.get +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8722.png) +## 3.Remove + 在下面代码中,如果prev为null了,那么说明第一个元素就是要删除的元素,那么就直接指向第一个元素的下一个即可。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8723.png) +## 4.扩容 +- 默认初始容量为11 +- 线程安全,但是速度慢,不允许key/value为null +- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容 +- 扩容增量:2*原数组长度+1如 HashTable的容量为11,一次扩容后是容量为23 + +2.4 hashtable和hashmap的区别 + +2.5 HashMap和ConCurrentHashMap区别 + +2.6 ConcurrentHashMap和HashTable区别 +ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 +hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 +concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 +2.7 linkedHashMap + https://blog.csdn.net/justloveyou_/article/details/71713781 + +2.8 Linkedhashmap与 hashmap的区别 +1.LinkedHashMap是HashMap的子类 +2.LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。 +3.在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 +4.在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写 +5.在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 +2.9 HashSet + 对于HashSet而言,它是基于HashMap实现的 + Hashset源码 http://zhangshixi.iteye.com/blog/673143 + Hashset 如何保证集合的没有重复元素? + 可以看出hashset底层是hashmap但是存储的是一个对象,hashset实际将该元素e作为key放入hashmap,当key值(该元素e)相同时,只是进行更新value,并不会新增加,所以set中的元素不会进行改变。 + + +2.10 hashmap与hashset区别 + + +2.11 Collections.sort 内部原理 + +重写 Collections.sort() +import java.util.*; +class xd{ + int a; + int b; + xd(int a,int b){ + this.a = a; + this.b = b; + } +} +public class Main { + public static void main(String[] arg) { + xd a = new xd(2,3); + xd b = new xd(4,1); + xd c = new xd(1,2); + ArrayList array = new ArrayList<>(); + array.add(a); + array.add(b); + array.add(c); + Collections.sort(array, new Comparator() { + @Override + public int compare(xd o1, xd o2) { + if(o1.a > o2.a) + return 1; + else if(o1.a < o2.a) + return -1; + return 0; + } + }); + for(int i=0;i Date: Thu, 3 Oct 2019 15:35:40 +0800 Subject: [PATCH 22/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 157 +++++++++++++++--------------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 560d0650..06102872 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -179,36 +179,40 @@ https://blog.csdn.net/ns_code/article/details/36191279 - 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容 - 扩容增量:2*原数组长度+1如 HashTable的容量为11,一次扩容后是容量为23 -2.4 hashtable和hashmap的区别 - -2.5 HashMap和ConCurrentHashMap区别 - -2.6 ConcurrentHashMap和HashTable区别 +# 四. 一些面试题 +## 4.1 hashtable和hashmap的区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8724.png) +## 4.2 HashMap和ConCurrentHashMap区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8725.png) +## 4.3 ConcurrentHashMap和HashTable区别 ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 -2.7 linkedHashMap - https://blog.csdn.net/justloveyou_/article/details/71713781 - -2.8 Linkedhashmap与 hashmap的区别 -1.LinkedHashMap是HashMap的子类 -2.LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。 -3.在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 -4.在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写 -5.在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 -2.9 HashSet - 对于HashSet而言,它是基于HashMap实现的 - Hashset源码 http://zhangshixi.iteye.com/blog/673143 - Hashset 如何保证集合的没有重复元素? - 可以看出hashset底层是hashmap但是存储的是一个对象,hashset实际将该元素e作为key放入hashmap,当key值(该元素e)相同时,只是进行更新value,并不会新增加,所以set中的元素不会进行改变。 - - -2.10 hashmap与hashset区别 - - -2.11 Collections.sort 内部原理 - +## 4.4 linkedHashMap +https://blog.csdn.net/justloveyou_/article/details/71713781 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8728.png) +## 4.5 Linkedhashmap与 hashmap的区别 +- 1.LinkedHashMap是HashMap的子类 +-2.LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。 +- 3.在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 +- 4.在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写 +- 5.在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 +## 4.6 HashSet +>对于HashSet而言,它是基于HashMap实现的 +Hashset源码 http://zhangshixi.iteye.com/blog/673143 + +**Hashset 如何保证集合的没有重复元素?** +可以看出hashset底层是hashmap但是存储的是一个对象,hashset实际将该元素e作为key放入hashmap,当key值(该元素e)相同时,只是进行更新value,并不会新增加,所以set中的元素不会进行改变。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8729.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8730.png) + +## 4.7 hashmap与hashset区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8731.png) + +## 4.8 Collections.sort 内部原理 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8732.png) 重写 Collections.sort() +``` import java.util.*; class xd{ int a; @@ -242,27 +246,16 @@ public class Main { for(int i=0;i快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败 From 8c514f428178cbd3718ce9ac74786f168d6ff768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 15:44:03 +0800 Subject: [PATCH 23/97] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" index 06102872..c55e05bd 100644 --- "a/Java\351\233\206\345\220\210.md" +++ "b/Java\351\233\206\345\220\210.md" @@ -185,9 +185,10 @@ https://blog.csdn.net/ns_code/article/details/36191279 ## 4.2 HashMap和ConCurrentHashMap区别 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8725.png) ## 4.3 ConcurrentHashMap和HashTable区别 -ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 -hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 -concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 +- ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 +- hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 +- concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 +- concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 ## 4.4 linkedHashMap https://blog.csdn.net/justloveyou_/article/details/71713781 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8728.png) @@ -255,15 +256,6 @@ public class Main { - -2.13 迭代器 Iterator Enumeration - 1. Iterator和ListIterator的区别是什么? - 答:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 -ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 -4.快速失败(fail-fast)和安全失败(fail-safe)的区别是什么? -答:Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。 -5.Enumeration接口和Iterator接口的区别有哪些? -答:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。 # 五.ArrayList,LinkedList和Vector的区别和实现原理 Vector : https://blog.csdn.net/chenssy/article/details/37520981 @@ -318,3 +310,13 @@ ArrayList和LinkedList的使用场景,其中add方法的实现ArrayList,Linked - 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。 >快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败 + +### Iterator和ListIterator的区别是什么? +答:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 +ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 + +### 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么? +答:Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。 + +### Enumeration接口和Iterator接口的区别有哪些? +答:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。 From 9a9e5ee408b833af8e9bfac734ce4a627f31ea12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 15:45:37 +0800 Subject: [PATCH 24/97] =?UTF-8?q?Create=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\351\233\206\345\220\210.md" | 322 +++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 "docs/Java\351\233\206\345\220\210.md" diff --git "a/docs/Java\351\233\206\345\220\210.md" "b/docs/Java\351\233\206\345\220\210.md" new file mode 100644 index 00000000..c55e05bd --- /dev/null +++ "b/docs/Java\351\233\206\345\220\210.md" @@ -0,0 +1,322 @@ + +# 一. HashMap +https://blog.csdn.net/jiary5201314/article/details/51439982 +## 1. hashMap的原理 +hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap +里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点 +## 2. hashcode的计算 +https://www.zhihu.com/question/20733617/answer/111577937 +https://blog.csdn.net/justloveyou_/article/details/62893086 +Key.hashcode是key的自带的hascode函数是一个int值32位 + +hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或低 16 位 +>return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); +首先确认:当 length 总是 2 的n 次方时, h & (length - 1) 等价于 hash 对length 取模 , 但是&比%具有更高的效率; +>Jdk1.7 之前:h & (length - 1);//第三步,取模运算 + +![](https://github.com/gzc426/picts/blob/master/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191002175517.jpg) + +## 3. hashMap参数以及扩容机制 +初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的n 次方 +扩容机制: +使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 +## 4. get()方法 +**整个过程都不需要加锁** +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%871.png) +## 5. put()方法 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%872.png) +这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现 + +## 6. HashMap问题 jdk1.8优化 +(1)HashMap如果有很多相同key,后面的链很长的话,你会怎么优化?或者你会用什么数据结构来存储?针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),怎么优化? +Java8 做的改变: +- HashMap 是数组+链表+红黑树(JDK1.8 增加了红黑树部分),当链表长度>=8 时转化为红黑树 +在 JDK1.8 版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能,其中会用到红黑树的插入、删除、查找等算法。 +- java8 中对 hashmap 扩容不是重新计算所有元素在数组的位置,而是我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash, 只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。 + + +## 7. 一些面试题 + +### 7.1 HashMap 和 TreeMap 的区别 +Hashmap 使用的是数组+链表,treemap 是红黑树 + +### 7.2 hashmap 为什么可以插入空值? +HashMap 中添加 key==null 的 Entry 时会调用 putForNullKey 方法直接去遍历 +table[0]Entry 链表,寻找 e.key==null 的 Entry 或者没有找到遍历结束 + +如果找到了 e.key==null,就保存 null 值对应的原值 oldValue,然后覆盖原值,并返回oldValue +如果在 table[0]Entry 链表中没有找到就调用addEntry 方法添加一个 key 为 null 的 Entry +### 7.3 Hashmap 为什么线程不安全:(hash 碰撞和扩容导致) +- HashMap 底层是一个Entry 数组,当发生 hash 冲突的时候,hashmap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。假如 A 线程和B 线程同时对同一个数组位置调用 addEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖A 的写入操作造成A 的写入操作丢失 +- 删除键值对的代码如上:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去, 其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改 +- 当多个线程同时检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。 +### 7.4 Hashmap 碰撞严重 +可以自定义重写 hash——多 hash 函数 +### 7.4 HashMap 高并发情况下会出现什么问题? +扩容问题 +### 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法? +答:hashCode 和equals。通过 hash(hashCode)然后模运算(其实是与的位操作)定位在Entry 数组中的下标,然后遍历这之后的链表,通过 equals 比较有没有相同的 key,如果有直接覆盖 value,如果没有就重新创建一个 Entry。 + +### 7.6 Hashmap为什么线程不安全 +hash碰撞和扩容导致,HashMap扩容的的时候可能会形成环形链表,造成死循环。 +### 7.7 Hashmap中的key可以为任意对象或数据类型吗? +可以为null但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode也进行相应的改变,导致下次无法查找到己存在Map中的效据 +如果可变对象在 HashMap中被用作键,时就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可. + +# 二. CurrentHashMap +http://www.importnew.com/21781.html +https://blog.csdn.net/dingji_ping/article/details/51005799 +https://www.cnblogs.com/chengxiao/p/6842045.html +http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C%E8%BF%99%E7%AF%87%E6%B2%A1%E4%BA%BA%E8%83%BD%E9%9A%BE%E4%BD%8F%E4%BD%A0%EF%BC%81/ +## 1. 概述 + +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%873.png) +一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。 +## 2. JDK1.7 ConCurrentHashMap原理 +其中 Segment 继承于 ReentrantLock +ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问, 能够实现真正的并发访问。 + +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%874.png) + +Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%875.png) +## 3. JDK1.7 Get +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%876.png) +- CurrentHashMap是否使用了锁??? + +它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。 这里可以看出并没有使用锁,但是value的值为null时候才是使用了加锁!!! + +- Get原理: +第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?看一下count变量的定义: transient volatile int count; +它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%877.png) + +### 3.1 在get代码的①和②之间,另一个线程新增了一个entry +如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。下图大致描述了put 一个新的entry的过程。 + +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%878.png) + +因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。 如果在这个new的对象的后面,则完全不影响,如果刚好是这个new的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的value值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值! +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%879.png) +### 3.2 在get代码的①和②之间,另一个线程修改了一个entry的value +value是用volitale修饰的,可以保证读取时获取到的是修改后的值。 +### 3.3 在get代码的①之后,另一个线程删除了一个entry +假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8710.png) +如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了,也就是说没办法看到最新的。 +我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 +不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 + +### 4. JDK1.7 PUT +- 1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 +- 2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 +- 3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 +- 4.最后会解除在 1 中所获取当前 Segment 的锁。 +- 5.可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8711.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8712.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8713.png) +### 5. JDK1.7 Remove +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8714.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8715.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8716.png) +### 6. JDK1.7 & JDK1.8 size() +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8720.png) +``` +public int size() { + long n = sumCount(); + return ((n < 0L) ? 0 : + (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int)n); +} +``` +volatile 保证内存可见,最大是65535. + +### 5.JDK 1.8 CurrentHashMap概述 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8717.png) +1.其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 +2.大于8的时候才去红黑树链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。 +### 6. JDK1.8 put +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8718.png) +- 根据 key 计算出 hashcode 。 +- 判断是否需要进行初始化。 +- f即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 +- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。 +- 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。 +- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 + +### 7. JDK1.8 get方法 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8719.png) +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 就不满足那就按照链表的方式遍历获取值。 +### 8.rehash过程 +- Redis rehash :dictRehash每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[0]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。 +- 可以看到,redis对dict的rehash是分批进行的,这样不会阻塞请求,设计的比较优雅。 +- 但是在调用dictFind的时候,可能需要对两张dict表做查询。唯一的优化判断是,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。 +- dictAdd的时候,如果状态是rehashing,则把值插入到ht[1],否则ht[0] + +# 三 Hashtable +https://blog.csdn.net/ns_code/article/details/36191279 +## 1.参数 +-(1)table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 +-(2)count是Hashtable的大小,它是Hashtable保存的键值对的数量。 +- (3)threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。 +- (4)loadFactor就是加载因子。 +- (5)modCount是用来实现fail-fast机制的 +## 1.put +从下面的代码中我们可以看出,Hashtable中的key和value是不允许为空的,当我们想要想Hashtable中添加元素的时候,首先计算key的hash值,然 +后通过hash值确定在table数组中的索引位置,最后将value值替换或者插入新的元素,如果容器的数量达到阈值,就会进行扩充。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8721.png) +## 2.get +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8722.png) +## 3.Remove + 在下面代码中,如果prev为null了,那么说明第一个元素就是要删除的元素,那么就直接指向第一个元素的下一个即可。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8723.png) +## 4.扩容 +- 默认初始容量为11 +- 线程安全,但是速度慢,不允许key/value为null +- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容 +- 扩容增量:2*原数组长度+1如 HashTable的容量为11,一次扩容后是容量为23 + +# 四. 一些面试题 +## 4.1 hashtable和hashmap的区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8724.png) +## 4.2 HashMap和ConCurrentHashMap区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8725.png) +## 4.3 ConcurrentHashMap和HashTable区别 +- ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 +- hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 +- concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 +- concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 +## 4.4 linkedHashMap +https://blog.csdn.net/justloveyou_/article/details/71713781 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8728.png) +## 4.5 Linkedhashmap与 hashmap的区别 +- 1.LinkedHashMap是HashMap的子类 +-2.LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。 +- 3.在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 +- 4.在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写 +- 5.在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 +## 4.6 HashSet +>对于HashSet而言,它是基于HashMap实现的 +Hashset源码 http://zhangshixi.iteye.com/blog/673143 + +**Hashset 如何保证集合的没有重复元素?** +可以看出hashset底层是hashmap但是存储的是一个对象,hashset实际将该元素e作为key放入hashmap,当key值(该元素e)相同时,只是进行更新value,并不会新增加,所以set中的元素不会进行改变。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8729.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8730.png) + +## 4.7 hashmap与hashset区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8731.png) + +## 4.8 Collections.sort 内部原理 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8732.png) +重写 Collections.sort() +``` +import java.util.*; +class xd{ + int a; + int b; + xd(int a,int b){ + this.a = a; + this.b = b; + } +} +public class Main { + public static void main(String[] arg) { + xd a = new xd(2,3); + xd b = new xd(4,1); + xd c = new xd(1,2); + ArrayList array = new ArrayList<>(); + array.add(a); + array.add(b); + array.add(c); + Collections.sort(array, new Comparator() { + @Override + public int compare(xd o1, xd o2) { + if(o1.a > o2.a) + return 1; + else if(o1.a < o2.a) + return -1; + return 0; + } + }); + for(int i=0;i快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败 + +### Iterator和ListIterator的区别是什么? +答:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 +ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 + +### 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么? +答:Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。 + +### Enumeration接口和Iterator接口的区别有哪些? +答:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。 From 754f90cf1542f5a6c3455da7db33f263f6c539b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 15:47:21 +0800 Subject: [PATCH 25/97] =?UTF-8?q?Delete=20Java=E9=9B=86=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\351\233\206\345\220\210.md" | 322 ------------------------------ 1 file changed, 322 deletions(-) delete mode 100644 "Java\351\233\206\345\220\210.md" diff --git "a/Java\351\233\206\345\220\210.md" "b/Java\351\233\206\345\220\210.md" deleted file mode 100644 index c55e05bd..00000000 --- "a/Java\351\233\206\345\220\210.md" +++ /dev/null @@ -1,322 +0,0 @@ - -# 一. HashMap -https://blog.csdn.net/jiary5201314/article/details/51439982 -## 1. hashMap的原理 -hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap -里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点 -## 2. hashcode的计算 -https://www.zhihu.com/question/20733617/answer/111577937 -https://blog.csdn.net/justloveyou_/article/details/62893086 -Key.hashcode是key的自带的hascode函数是一个int值32位 - -hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或低 16 位 ->return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); -首先确认:当 length 总是 2 的n 次方时, h & (length - 1) 等价于 hash 对length 取模 , 但是&比%具有更高的效率; ->Jdk1.7 之前:h & (length - 1);//第三步,取模运算 - -![](https://github.com/gzc426/picts/blob/master/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191002175517.jpg) - -## 3. hashMap参数以及扩容机制 -初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的n 次方 -扩容机制: -使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。 -## 4. get()方法 -**整个过程都不需要加锁** -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%871.png) -## 5. put()方法 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%872.png) -这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现 - -## 6. HashMap问题 jdk1.8优化 -(1)HashMap如果有很多相同key,后面的链很长的话,你会怎么优化?或者你会用什么数据结构来存储?针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),怎么优化? -Java8 做的改变: -- HashMap 是数组+链表+红黑树(JDK1.8 增加了红黑树部分),当链表长度>=8 时转化为红黑树 -在 JDK1.8 版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能,其中会用到红黑树的插入、删除、查找等算法。 -- java8 中对 hashmap 扩容不是重新计算所有元素在数组的位置,而是我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash, 只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。 - - -## 7. 一些面试题 - -### 7.1 HashMap 和 TreeMap 的区别 -Hashmap 使用的是数组+链表,treemap 是红黑树 - -### 7.2 hashmap 为什么可以插入空值? -HashMap 中添加 key==null 的 Entry 时会调用 putForNullKey 方法直接去遍历 -table[0]Entry 链表,寻找 e.key==null 的 Entry 或者没有找到遍历结束 - -如果找到了 e.key==null,就保存 null 值对应的原值 oldValue,然后覆盖原值,并返回oldValue -如果在 table[0]Entry 链表中没有找到就调用addEntry 方法添加一个 key 为 null 的 Entry -### 7.3 Hashmap 为什么线程不安全:(hash 碰撞和扩容导致) -- HashMap 底层是一个Entry 数组,当发生 hash 冲突的时候,hashmap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。假如 A 线程和B 线程同时对同一个数组位置调用 addEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖A 的写入操作造成A 的写入操作丢失 -- 删除键值对的代码如上:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去, 其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改 -- 当多个线程同时检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。 -### 7.4 Hashmap 碰撞严重 -可以自定义重写 hash——多 hash 函数 -### 7.4 HashMap 高并发情况下会出现什么问题? -扩容问题 -### 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法? -答:hashCode 和equals。通过 hash(hashCode)然后模运算(其实是与的位操作)定位在Entry 数组中的下标,然后遍历这之后的链表,通过 equals 比较有没有相同的 key,如果有直接覆盖 value,如果没有就重新创建一个 Entry。 - -### 7.6 Hashmap为什么线程不安全 -hash碰撞和扩容导致,HashMap扩容的的时候可能会形成环形链表,造成死循环。 -### 7.7 Hashmap中的key可以为任意对象或数据类型吗? -可以为null但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode也进行相应的改变,导致下次无法查找到己存在Map中的效据 -如果可变对象在 HashMap中被用作键,时就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可. - -# 二. CurrentHashMap -http://www.importnew.com/21781.html -https://blog.csdn.net/dingji_ping/article/details/51005799 -https://www.cnblogs.com/chengxiao/p/6842045.html -http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C%E8%BF%99%E7%AF%87%E6%B2%A1%E4%BA%BA%E8%83%BD%E9%9A%BE%E4%BD%8F%E4%BD%A0%EF%BC%81/ -## 1. 概述 - -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%873.png) -一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。 -## 2. JDK1.7 ConCurrentHashMap原理 -其中 Segment 继承于 ReentrantLock -ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问, 能够实现真正的并发访问。 - -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%874.png) - -Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%875.png) -## 3. JDK1.7 Get -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%876.png) -- CurrentHashMap是否使用了锁??? - -它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。 这里可以看出并没有使用锁,但是value的值为null时候才是使用了加锁!!! - -- Get原理: -第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?看一下count变量的定义: transient volatile int count; -它使用了volatile来修改。我们前文说过,Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%877.png) - -### 3.1 在get代码的①和②之间,另一个线程新增了一个entry -如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。下图大致描述了put 一个新的entry的过程。 - -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%878.png) - -因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。 如果在这个new的对象的后面,则完全不影响,如果刚好是这个new的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的value值为null,就出现了如下所示的代码,需要重新加锁再次读取这个值! -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%879.png) -### 3.2 在get代码的①和②之间,另一个线程修改了一个entry的value -value是用volitale修饰的,可以保证读取时获取到的是修改后的值。 -### 3.3 在get代码的①之后,另一个线程删除了一个entry -假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8710.png) -如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。这里没有办法实时保证了,也就是说没办法看到最新的。 -我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了,这时候的get的实际上是未更新过的!!!。 -不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。 - -### 4. JDK1.7 PUT -- 1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 -- 2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 -- 3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 -- 4.最后会解除在 1 中所获取当前 Segment 的锁。 -- 5.可以说是首先找到segment,确定是哪一个segment,然后在这个segment中遍历查找 key值是要查找的key值得entry,如果找到,那么就修改该key,如果没找到,那么就在头部新加一个entry. -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8711.png) -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8712.png) -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8713.png) -### 5. JDK1.7 Remove -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8714.png) -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8715.png) -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8716.png) -### 6. JDK1.7 & JDK1.8 size() -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8720.png) -``` -public int size() { - long n = sumCount(); - return ((n < 0L) ? 0 : - (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : - (int)n); -} -``` -volatile 保证内存可见,最大是65535. - -### 5.JDK 1.8 CurrentHashMap概述 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8717.png) -1.其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 -2.大于8的时候才去红黑树链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。 -### 6. JDK1.8 put -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8718.png) -- 根据 key 计算出 hashcode 。 -- 判断是否需要进行初始化。 -- f即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 -- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。 -- 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。 -- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 - -### 7. JDK1.8 get方法 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8719.png) -- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 -- 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 -### 8.rehash过程 -- Redis rehash :dictRehash每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[0]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。 -- 可以看到,redis对dict的rehash是分批进行的,这样不会阻塞请求,设计的比较优雅。 -- 但是在调用dictFind的时候,可能需要对两张dict表做查询。唯一的优化判断是,当key在ht[0]不存在且不在rehashing状态时,可以速度返回空。如果在rehashing状态,当在ht[0]没值的时候,还需要在ht[1]里查找。 -- dictAdd的时候,如果状态是rehashing,则把值插入到ht[1],否则ht[0] - -# 三 Hashtable -https://blog.csdn.net/ns_code/article/details/36191279 -## 1.参数 --(1)table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 --(2)count是Hashtable的大小,它是Hashtable保存的键值对的数量。 -- (3)threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。 -- (4)loadFactor就是加载因子。 -- (5)modCount是用来实现fail-fast机制的 -## 1.put -从下面的代码中我们可以看出,Hashtable中的key和value是不允许为空的,当我们想要想Hashtable中添加元素的时候,首先计算key的hash值,然 -后通过hash值确定在table数组中的索引位置,最后将value值替换或者插入新的元素,如果容器的数量达到阈值,就会进行扩充。 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8721.png) -## 2.get -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8722.png) -## 3.Remove - 在下面代码中,如果prev为null了,那么说明第一个元素就是要删除的元素,那么就直接指向第一个元素的下一个即可。 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8723.png) -## 4.扩容 -- 默认初始容量为11 -- 线程安全,但是速度慢,不允许key/value为null -- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容 -- 扩容增量:2*原数组长度+1如 HashTable的容量为11,一次扩容后是容量为23 - -# 四. 一些面试题 -## 4.1 hashtable和hashmap的区别 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8724.png) -## 4.2 HashMap和ConCurrentHashMap区别 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8725.png) -## 4.3 ConcurrentHashMap和HashTable区别 -- ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 -- hashtable(同一把锁):使用synchronized来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。 -- concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 -- concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。 -## 4.4 linkedHashMap -https://blog.csdn.net/justloveyou_/article/details/71713781 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8728.png) -## 4.5 Linkedhashmap与 hashmap的区别 -- 1.LinkedHashMap是HashMap的子类 --2.LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。 -- 3.在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 -- 4.在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写 -- 5.在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 -## 4.6 HashSet ->对于HashSet而言,它是基于HashMap实现的 -Hashset源码 http://zhangshixi.iteye.com/blog/673143 - -**Hashset 如何保证集合的没有重复元素?** -可以看出hashset底层是hashmap但是存储的是一个对象,hashset实际将该元素e作为key放入hashmap,当key值(该元素e)相同时,只是进行更新value,并不会新增加,所以set中的元素不会进行改变。 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8729.png) -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8730.png) - -## 4.7 hashmap与hashset区别 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8731.png) - -## 4.8 Collections.sort 内部原理 -![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8732.png) -重写 Collections.sort() -``` -import java.util.*; -class xd{ - int a; - int b; - xd(int a,int b){ - this.a = a; - this.b = b; - } -} -public class Main { - public static void main(String[] arg) { - xd a = new xd(2,3); - xd b = new xd(4,1); - xd c = new xd(1,2); - ArrayList array = new ArrayList<>(); - array.add(a); - array.add(b); - array.add(c); - Collections.sort(array, new Comparator() { - @Override - public int compare(xd o1, xd o2) { - if(o1.a > o2.a) - return 1; - else if(o1.a < o2.a) - return -1; - return 0; - } - }); - for(int i=0;i快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败 - -### Iterator和ListIterator的区别是什么? -答:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 -ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 - -### 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么? -答:Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。 - -### Enumeration接口和Iterator接口的区别有哪些? -答:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。 From 6a0c23e623a5ad0454a4e8857ac5074ab93510e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 15:48:07 +0800 Subject: [PATCH 26/97] =?UTF-8?q?Create=20Java=E5=89=91=E6=8C=87offer.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\211\221\346\214\207offer.md" | 4087 +++++++++++++++++++ 1 file changed, 4087 insertions(+) create mode 100644 "docs/Java\345\211\221\346\214\207offer.md" diff --git "a/docs/Java\345\211\221\346\214\207offer.md" "b/docs/Java\345\211\221\346\214\207offer.md" new file mode 100644 index 00000000..22d58c90 --- /dev/null +++ "b/docs/Java\345\211\221\346\214\207offer.md" @@ -0,0 +1,4087 @@ +--- +title: 剑指offer解析(Java实现) +date: 2019-01-18 18:32:16 +updated_at:github.com/zanwen/my-offer-to-java +comments: true +photos: "" +categories: 数据结构与算法 +tags: 数据结构与算法 +--- + +> 以下题目按照牛客网在线编程排序,所有代码示例代码均已通过牛客网OJ。 + +### 二维数组的查找 + +#### 题目描述 + +在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 + +```java +public boolean Find(int target, int [][] arr) { + +} +``` + + + +#### 解析 + +暴力方法是遍历一遍二维数组,找到`target`就返回`true`,时间复杂度为`O(M * N)`(对于M行N列的二维数组)。 + +由题可知输入的数据样本具有高度规律性(单独一行的数据来看是有序的,单独一列的数据来看也是有序的),因此考虑能否有一个比较基准在一次的比较中根据有序性淘汰不必再进行遍历比较的数。**有序**、**查找**,由此不难联想到二分查找,我们可以借鉴二分查找的思路,每次选出一个数作为比较基准进而淘汰掉一些不必比较的数。二分是选取数组的中位数作为比较基准的,因此能够保证每次都淘汰掉二分之一的数,那么此题中有没有这种特性的数呢?我们不妨举例观察一下: + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzaxy3978uj304x04mt8m.jpg) + +不难发现上图中对角线上的数是其所在行和所在列形成的序列的中位数,不妨选取右上角的数作为比较基准,如果不相等,那么我们可以淘汰掉所有它左边的数或者它所有下面的,比如对于`target = 6`,因为`(0,3)`位置上的`4 < 6`,因此`(0,3)`位置及其同一行的左边的所有数都小于6因此可以直接淘汰掉,淘汰掉之后问题就变为了从剩下的三行中找`target`,这与原始问题是相似的,也就是说每一次都选取右上角的数据为比较基准然后淘汰掉一行或一列,直到某一轮被选取的数就是`target`或者已经淘汰得只剩下一个数的时候就一定能得出结果了,因此时间复杂度为被淘汰掉的行数和列数之和,即`O(M + N)`,经过分析后不难写出如下代码: + +```java +public boolean Find(int target, int [][] arr) { + //input check + if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){ + return false; + } + int i = 0, j = arr[0].length - 1; + while(i != arr.length - 1 && j != 0){ + if(target > arr[i][j]){ + i++; + }else if(target < arr[i][j]){ + j--; + }else{ + return true; + } + } + + return target == arr[i][j]; +} +``` + +值得注意的是每次选取的数都是第一行最后一个数,因此前提是第一行有数,那么就对应着输入检查的`arr[0] == null || arr[0].length == 0`,这点比较容易忽略。 + +> 总结:经过分析其实不难发现,此题是在一维有序数组使用二分查找元素的一个变种,我们应该充分利用数据本身的规律性来寻找解题思路。 + +### 替换空格 + +#### 题目描述 + +请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 + +```java +public String replaceSpace(StringBuffer str) { + +} +``` + + + +> 此题考查的是字符串这个数据结构的数组实现(对应的还有链表实现)的相关操作。 + +#### 解析 + +##### String.replace简单粗暴 + +如果可以使用`API`,那么可以很容易地写出如下代码: + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + for(int i = 0 ; i < str.length() ; i++){ + if(str.charAt(i) == ' '){ + str.replace(i, i + 1, "%20"); + } + } + + return str.toString(); +} +``` + +##### 时间O(n),空间O(n) + +但是如果面试官告诉我们不许使用封装好的替换函数,那么目的就是在考查我们对字符串**数组实现**方式的相关操作。由于是连续空间存储,因此需要在创建实例时指定大小,由于每个空格都使用`%20`替换,因此替换之后的字符串应该比原串多出`空格数 * 2`个长度,实现如下: + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + char[] source = str.toString().toCharArray(); + int blankCount = 0; + for(int i = 0 ; i < source.length ; i++){ + blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount; + } + char[] dest = new char[source.length + blankCount * 2]; + for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){ + if(source[i] == ' '){ + dest[j--] = '0'; + dest[j--] = '2'; + dest[j] = '%'; + continue; + }else{ + dest[j] = source[i]; + } + } + + return new String(dest); +} +``` + +##### 时间O(n),空间O(1) + +如果还要求不能有额外空间,那我们就要考虑如何复用输入的字符串,如果我们从前往后遇到空格就将空格及其之后的两个位置替换为`%20`,势必会覆盖空格之后的两个字符,比如`hello world`会被替换成`hello%20rld`,因此我们需要在长度被扩展后的新串中从后往前确定每个索引上的字符。比如使用一个`originalIndex`指向原串中的最后一个字符索引,使用`newIndex`指向新串的最后一个索引,每次将`originalIndex`上的字符复制到`newIndex`上并且两个指针前移,如果`originalIndex`上的字符是空格,则将`newIndex`依次填充`0,2,%`,然后两者再前移,直到两者都到首索引位置。 + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzb0mknemyj30ng0df74w.jpg) + +```java +public String replaceSpace(StringBuffer str) { + //input check + //null pointer + if(str == null){ + return null; + } + //empty str or not exist blank + if(str.length() == 0 || str.indexOf(" ") == -1){ + return str.toString(); + } + + int blankCount = 0; + for(int i = 0 ; i < str.length() ; i++){ + blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount; + } + int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2; + str.setLength(newIndex + 1); //需要重新设置一下字符串的长度,否则会报越界错误 + while(originalIndex >= 0 && newIndex >= 0){ + if(str.charAt(originalIndex) == ' '){ + str.setCharAt(newIndex--, '0'); + str.setCharAt(newIndex--, '2'); + str.setCharAt(newIndex, '%'); + }else{ + str.setCharAt(newIndex, str.charAt(originalIndex)); + } + originalIndex--; + newIndex--; + } + + return str.toString(); +} +``` + +> 总结:要把思维打开,对于数组的操作我们习惯性的以`for(int i = 0 ; i < arr.length ; i++)`的形式从头到尾来操作数组,但是不要忽略了从尾到头遍历也有它的独到之处。 + +### 反转链表 + +#### 题目描述 + +输入一个链表,反转链表后,输出新链表的表头。 + +```java +public ListNode ReverseList(ListNode head) { + +} +``` + + + +#### 解析 + +此题的难点在于无法通过一个单链表结点获取其前驱结点,因此我们不仅要在反转指针之前保存当前结点的前驱结点,还要保存当前结点的后继结点,并在下一次反转之前更新这两个指针。 + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ +public ListNode ReverseList(ListNode head) { + if(head == null || head.next == null){ + return head; + } + ListNode pre = null, p = head, next; + while(p != null){ + next = p.next; + p.next = pre; + pre = p; + p = next; + } + + return pre; +} +``` + + + +### 从尾到头打印链表 + +#### 题目描述 + +输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + +} +``` + + + +#### 解析 + +此题的难点在于单链表只有指向后继结点的指针,因此我们无法通过当前结点获取前驱结点,因此不要妄想先遍历一遍链表找到尾结点然后再依次从后往前打印。 + +##### 递归,简洁优雅 + +由于我们通常是从头到尾遍历链表的,而题目要求从尾到头打印结点,这与前进后出的逻辑是相符的,因此你可以使用一个栈来保存遍历时走过的结点,再通过后进先出的特性实现从尾到头打印结点,但是我们也可以利用递归来帮我们压栈,由于递归简洁不易出错,因此面试中能用递归尽量用递归:只要当前结点不为空,就递归遍历后继结点,当后继结点为空时,递归结束,在递归回溯时将“当前结点”依次添加到集合中 + +```java +/** +* public class ListNode { +* int val; +* ListNode next = null; +* +* ListNode(int val) { +* this.val = val; +* } +* } +* +*/ +import java.util.ArrayList; +public class Solution { + public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList res = new ArrayList(); + //input check + if(listNode == null){ + return res; + } + recursively(res, listNode); + return res; + } + + public void recursively(ArrayList res, ListNode node){ + //base case + if(node == null){ + return; + } + //node not null + recursively(res, node.next); + res.add(node.val); + return; + } +} +``` + +##### 反转链表 + +还有一种方法就是将链表指针都反转,这样将反转后的链表从头到尾打印就是结果了。需要注意的是我们不应该在访问用户数据时更改存储数据的结构,因此最后要记得反转回来: + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList res = new ArrayList(); + //input check + if(listNode == null){ + return res; + } + return unrecursively(listNode); +} + +public ArrayList unrecursively(ListNode node){ + ArrayList res = new ArrayList(); + ListNode newHead = reverse(node); + ListNode p = newHead; + while(p != null){ + res.add(p.val); + p = p.next; + } + reverse(newHead); + return res; +} + +public ListNode reverse(ListNode node){ + ListNode pre = null, cur = node, next; + while(cur != null){ + //save predecessor + next = cur.next; + //reverse pointer + cur.next = pre; + //move to next + pre = cur; + cur = next; + } + //cur is null + return pre; +} +``` + +> 总结:面试时能用递归就用递归,当然了如果面试官就是要考查你的指针功底那你也能`just so so`不是 + +### 重建二叉树 + +#### 题目描述 + +输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,2,7,1,5,3,8,6},则重建二叉树并返回。 + +```java +public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + +} +``` + + + +#### 解析 + +先序序列的特点是第一个数就是根结点而后是左子树的先序序列和右子树的先序序列,而中序序列的特点是先是左子树的中序序列,然后是根结点,最后是右子树的中序序列。因此我们可以通过先序序列得到根结点,然后通过在中序序列中查找根结点的索引从而得到左子树和右子树的结点数。然后可以将两序列都一分为三,对于其中的根结点能够直接重建,然后根据对应子序列分别递归重建根结点的左子树和右子树。这是一个典型的将复杂问题划分成子问题分步解决的过程。 + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzb43dddiej30f70azjrt.jpg) + +递归体的定义,如上图先序序列的左子树序列是`2,3,4`对应下标`1,2,3`,而中序序列的左子树序列是`3,2,4`对应下标`0,1,2`,因此递归体接收的参数除了保存两个序列的数组之外,还需要指明需要递归重建的子序列分别在两个数组中的索引范围:`TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)`。然后递归体根据`pre`的`i~j`索引范围形成的先序序列和`in`的`m~n`索引范围形成的中序序列重建一棵树并返回根结点。 + +首先根结点就是先序序列的第一个数,即`pre[i]`,因此`TreeNode root = new TreeNode(pre[i])`可以直接确定,然后通过在`in`的`m~n`中查找出`pre[i]`的索引`index`可以求得左子树结点数`leftNodes = index - m`,右子树结点数`rightNodes = n - index`,如果左(右)子树结点数为0则表明左(右)子树为`null`,否则通过`root.left = rebuild(pre, i' ,j' ,in ,m' ,n')`来重建左(右)子树即可。 + +这个题的难点也就在这里,即`i',j',m',n'`的值的确定,笔者曾在此困惑许久,建议通过`leftNodes,rightNodes`和`i,j,m,n`来确定:(这个时候了前往不要在脑子里面想这些下标对应关系!!一定要在纸上画,确保准确性和概括性) + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbmo2052rj309v088dfy.jpg) + +于是容易得出如下代码: + +```java +if(leftNodes == 0){ + root.left = null; +}else{ + root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); +} +if(rightNodes == 0){ + root.right = null; +}else{ + root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); +} +``` + +笔者曾以中序序列的根节点索引来确定`i',j',m',n'`的对应关系写出如下**错误代码**: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbmvcv9yej306b07adfv.jpg) + +```java +if(leftNodes == 0){ + root.left = null; +}else{ + root.left = rebuild(pre, i + 1, index, in, m, index - 1); +} +if(rightNodes == 0){ + root.right = null; +}else{ + root.right = rebuild(pre, index + 1, j, in, index + 1, n); +} +``` + +这种对应关系乍一看没错,但是不具有概括性(即囊括所有情况),比如对序列`2,3,4`、`3,2,4`重建时: + +![image](https://wx1.sinaimg.cn/large/006zweohgy1fzbn2rz5n5j30cv07v3yh.jpg) + +你看这种情况,上述错误代码还适用吗?原因就在于`index`是在`in`的`m~n`中选取的,与数组`in`是绑定的,和`pre`没有直接的关系,因此如果用`index`来表示`i',j'`自然是不合理的。 + +此题的正确完整代码如下: + +```java +/** + * Definition for binary tree + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){ + return null; + } + return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1); + } + + public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){ + int rootVal = pre[i], index = findIndex(rootVal, in, m, n); + if(index < 0){ + return null; + } + int leftNodes = index - m, rightNodes = n - index; + TreeNode root = new TreeNode(rootVal); + if(leftNodes == 0){ + root.left = null; + }else{ + root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); + } + if(rightNodes == 0){ + root.right = null; + }else{ + root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); + } + return root; + } + + public int findIndex(int target, int arr[], int from, int to){ + for(int i = from ; i <= to ; i++){ + if(arr[i] == target){ + return i; + } + } + return -1; + } +} +``` + +> 总结: +> +> 1. 对于复杂问题,一定要划分成若干子问题,逐一求解。比如二叉树问题,我们通常将其划分成头结点、左子树、右子树。 +> 2. 对于递归过程的参数对应关系,尽量使用和数据样本本身没有直接关系的变量来表示。比如此题应该选取`leftNodes`和`rightNodes`来计算`i',j',m',n'`而不应该使用头结点在中序序列的下标`index`(它和`in`是绑定的,那么可能对`pre`就不适用了)。 + +### 用两个栈实现队列 + +#### 题目描述 + +用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 + +```java +Stack stack1 = new Stack(); +Stack stack2 = new Stack(); + +public void push(int node) { + +} + +public int pop() { + +} +``` + + + +#### 解析 + +这道题只要记住以下几点即可: + +1. 一个栈(如`stack1`)只能用来存,另一个栈(如`stack2`)只能用来取 +2. 当取元素时首先检查`stack2`是否为空,如果不空直接`stack2.pop()`,否则将`stack1`中的元素**全部倒入**`stack2`,如果倒入之后`stack2`仍为空则需要抛异常,否则`stack2.pop()`。 + +代码示例如下: + +```java +import java.util.Stack; + +public class Solution { + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + + public void push(int node) { + stack1.push(node); + } + + public int pop() { + if(stack2.empty()){ + while(!stack1.empty()){ + stack2.push(stack1.pop()); + } + } + if(stack2.empty()){ + throw new IllegalStateException("no more element!"); + } + return stack2.pop(); + } +} +``` + +> 总结:只要取元素的栈不为空,取元素时直接弹出其栈顶元素即可,只有当其为空时才考虑将存元素的栈倒入进来,并且要一次性倒完。 + +### 旋转数组的最小数字 + +#### 题目描述 + +把一个数组最开始的**若干**个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个**非减排序**的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 + +```java +public int minNumberInRotateArray(int [] arr) { + +} +``` + + + +#### 解析 + +此题需先认真审题: + +1. 若干,涵盖了一个元素都不搬的情况,此时数组是一个非减排序序列,因此首元素就是数组的最小元素。 +2. 非减排序,并不代表是递增的,可能会出现若干相邻元素相同的情况,极端的例子是整个数组的所有元素都相同 + +由此不难得出如下`input check`: + +```java +public int minNumberInRotateArray(int [] arr) { + //input check + if(arr == null || arr.length == 0){ + return 0; + } + //if only one element or no rotate + if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ + return arr[0]; + } + + //TODO +} +``` + +上述的`arr[0] < arr[arr.length - 1]`不能写成`arr[0] <= arr[arr.length - 1]`,比如可能会有`[1,2,3,3,4] -> [3,4,1,2,3]` 的情况,这时你不能返回`arr[0]=3`。 + +如果走到了程序中的`TODO`,就可以考虑普遍情况下的推敲,数组可以被分成两部分:大于等于`arr[0]`的左半部分和小于等于`arr[arr.length - 1]`右半部分,我们不妨借助两个指针从数组的头、尾向中间靠近,这样就能利用二分的思想快速移动指针从而淘汰一些不在考虑范围之内的数。 + +![image](https://wx2.sinaimg.cn/large/006zweohgy1fzbpp2dx1dj30a0063aa1.jpg) + +如图,我们不能直接通过`arr[mid]`和`arr[l]`(或`arr[r]`)的比较(`arr[mid] >= arr[l]`)来决定移动`l`还是`r`到`mid`上,因为数组可能存在若干相同且相邻的数,因此我们还需要加上一个限制条件:`arr[l + 1] >= arr[l] && arr[mid] >= arr[l]`(对于`r`来说则是`arr[r - 1] <= arr[r] && arr[mid] <= arr[r]`),即当左半部分(右半部分)不止一个数时,我们才可能去移动`l`(`r`)指针。完整代码如下: + +```java +import java.util.ArrayList; +public class Solution { + public int minNumberInRotateArray(int [] arr) { + //input check + if(arr == null || arr.length == 0){ + return 0; + } + //if only one element or no rotate + if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ + return arr[0]; + } + + //has rotate, left part is big than right part + int l = 0, r = arr.length - 1, mid; + //l~r has more than 3 elements + while(r > l && r - l != 1){ + //r-l >= 2 -> mid > l + mid = l + ((r - l) >> 1); + if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){ + l = mid; + }else{ + r = mid; + } + } + + return arr[r]; + } +} +``` + +> 总结:审题时要充分考虑数据样本的极端情况,以写出鲁棒性较强的代码。 + +### 斐波那契数列 + +#### 题目描述 + +大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39 + +```java +public int Fibonacci(int n) { + +} +``` + + + +#### 解析 + +##### 递归方式 + +对于公式`f(n) = f(n-1) + f(n-2)`,明显就是一个递归调用,因此根据`f(0) = 0`和`f(1) = 1`我们不难写出如下代码: + +```java +public int Fibonacci(int n) { + if(n == 0 || n == 1){ + return n; + } + return Fibonacci(n - 1) + Fibonacci(n - 2); +} +``` + +##### 动态规划 + +在上述递归过程中,你会发现有很多计算过程是重复的: + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbq4avws3j30b507b74a.jpg) + +**动态规划就在使用递归调用自上而下分析过程中发现有很多重复计算的子过程,于是采用自下而上的方式将每个子状态缓存下来,这样对于上层而言只有当需要的子过程结果不在缓存中时才会计算一次,因此每个子过程都只会被计算一次**。 + +```java +public int Fibonacci(int n) { + if(n == 0 || n == 1){ + return n; + } + //n1 -> f(n-1), n2 -> f(n-2) + int n1 = 1, n2 = 0; + //从f(2)开始算起 + int N = 2, res = 0; + while(N++ <= n){ + //每次计算后更新缓存,当然你也可以使用一个一维数组保存每次的计算结果,只额外空间复杂度就变为O(n)了 + res = n1 + n2; + n2 = n1; + n1 = res; + } + return res; +} +``` + +上述代码很多人都能写出来,只是没有意识到这就是动态规划。 + +> 总结:当你自上而下分析递归时发现有很多子过程被重复计算,那么就应该考虑能否通过自下而上将每个子过程的计算结果缓存下来。 + +### 跳台阶 + +#### 题目描述 + +一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 + +```java +public int JumpFloor(int target) { + +} +``` + + + +#### 解析 + +##### 递归版本 + +将复杂问题分解:复杂问题就是不断地将`target`减1或减2(对应跳一级和跳两级台阶)直到`target`变为1或2(对应只剩下一层或两层台阶)时我们能够很容易地得出结果。因此对于当前的青蛙而言,它能够选择的就是跳一级或跳二级,剩下的台阶有多少种跳法交给子过程来解决: + +```java +public int JumpFloor(int target) { + //input check + if(target <= 0){ + return 0; + } + //base case + if(target == 1){ + return 1; + } + if(target == 2){ + return 2; + } + return JumpFloor(target - 1) + JumpFloor(target - 2); +} +``` + +你会发现这其实就是一个斐波那契数列,只不过是从`f(1) = 1,f(2) = 2`开始的斐波那契数列罢了。自然你也应该能够写出动态规划版本。 + +#### 进阶问题 + +一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 + +#### 解析 + +##### 递归版本 + +本质上还是分解,只不过上一个是分解成两步,而这个是分解成n步: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + //base case,当target=0时表示某个分解分支跳完了所有台阶,这个分支就是一种跳法 + if(target == 0){ + return 1; + } + + //本过程要收集的跳法的总数 + int res = 0; + for(int i = 1 ; i <= target ; i++){ + //本次选择,选择跳i阶台阶,剩下的台阶交给子过程,每个选择就代表一个分解分支 + res += JumpFloorII(target - i); + } + return res; +} +``` + +##### 动态规划 + +这个动态规划就有一点难度了,**首先我们要确定缓存目标**,斐波那契数列中由于`f(n)`只依赖于`f(n-1)`和`f(n-2)`因此我们仅用两个缓存变量实现了动态规划,但是这里`f(n)`依赖的是`f(0),f(1),f(2),...,f(n-1)`,因此我们需要通过长度量级为`n`的表缓存前`n`个状态(`int arr[] = new int[target + 1]`,`arr[target]`表示`f(n)`)。**然后根据递归版本(通常是`base case`)确定哪些状态的值是可以直接确定的**,比如由`if(target == 0){ return 1 }`可知`arr[0] = 1`,从`f(N = 1)`开始的所有状态都需要依赖之前(`f(n < N)`)的所有状态: + +```java +int res = 0; +for(int i = 1 ; i <= target ; i++){ + res += JumpFloorII(target - i); +} +return res +``` + +因此我们可以据此自下而上计算出每个子状态的值: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + for(int i = 1 ; i < arr.length ; i++){ + for(int j = 0 ; j < i ; j++){ + arr[i] += arr[j]; + } + } + + return arr[target]; +} +``` + +但这仍不是最优解,因为观察循环体你会发现,每次`f(n)`的计算都要从`f(0)`累加到`f(n-1)`,我们完全可以将这个累加值缓存起来`preSum`,每计算出一次`f(N)`之后都将缓存更新为`preSum += f(N)`。如此得到最优解: + +```java +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + int preSum = arr[0]; + for(int i = 1 ; i < arr.length ; i++){ + arr[i] = preSum; + preSum += arr[i]; + } + + return arr[target]; +} +``` + +### 矩形覆盖 + +#### 题目描述 + +我们可以用`2*1`的小矩形横着或者竖着去覆盖更大的矩形。请问用n个`2*1`的小矩形无重叠地覆盖一个`2*n`的大矩形,总共有多少种方法? + +```java +public int RectCover(int target) { + +} +``` + + + +#### 解析 + +##### 递归版本 + +有了之前的历练,我们能很快的写出递归版本:先竖着放一个或者先横着放两个,剩下的交给递归处理: + +```java +//target 大矩形的边长,也是剩余小矩形的个数 +public int RectCover(int target) { + if(target <= 0){ + return 0; + } + if(target == 1 || target == 2){ + return target; + } + return RectCover(target - 1) + RectCover(target - 2); +} +``` + +##### 动态规划 + +这仍然是个以`f(1)=1,f(2)=2`开头的斐波那契数列: + +```java +//target 大矩形的边长,也是剩余小矩形的个数 +public int RectCover(int target) { + if(target <= 0){ + return 0; + } + if(target == 1 || target == 2){ + return target; + } + //n_1->f(n-1), n_2->f(n-2),从f(N=3)开始算起 + int n_1 = 2, n_2 = 1, N = 3, res = 0; + while(N++ <= target){ + res = n_1 + n_2; + n_2 = n_1; + n_1 = res; + } + + return res; +} +``` + +### 二进制中1的个数 + +#### 题目描述 + +输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 + +```java +public int NumberOf1(int n) { + +} +``` + + + +#### 解析 + +题目已经给我们降低了难度:负数用补码(取反加1)表示表明输入的参数为均为正数,我们只需统计其二进制表示中1的个数、运算时只考虑无符号移位即可。 + +典型的判断某个二进制位上是否为1的方法是将该二进制数右移至该二进制位为最低位然后与1相与`&`,由于1的二进制表示中只有最低位为1其余位均为0,因此相与后的结果与该二进制位上的数相同。据此不难写出如下代码: + +```java +public int NumberOf1(int n) { + int count = 0; + for(int i = 0 ; i < 32 ; i++){ + count += ((n >> i) & 1); + } + return count; +} +``` + +当然了,还有一种比较秀的解法就是利用`n = n & (n - 1)`将`n`的二进制位中为1的最低位置为0(只要`n`不为0就说明含有二进位制为1的位,如此这样的操作能做多少次就说明有多少个二进制位为1的位): + +```java +public int NumberOf1(int n) { + int count = 0; + while(n != 0){ + count++; + n &= (n - 1); + } + return count; +} +``` + +### 数值的整数次方 + +#### 题目描述 + +给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 + +```java +public double Power(double base, int exponent) { + +} +``` + + + +#### 解析 + +这是一道充满危险色彩的题,求职者可能会内心窃喜不假思索的写出如下代码: + +```java +public double Power(double base, int exponent) { + double res = 1; + for(int i = 1 ; i <= exponent ; i++){ + res *= base; + } + return res; +} +``` + +但是你有没有想过底数`base`和幂`exponent`都是可正、可负、可为0的。如果幂为负数,那么底数就不能为0,否则应该抛出算术异常: + +```java +//是否是负数 +boolean minus = false; +//如果存在分母 +if(exponent < 0){ + minus = true; + exponent = -exponent; + if(base == 0){ + throw new ArithmeticException("/ by zero"); + } +} +``` + +如果幂为0,那么根据任何不为0的数的0次方为1,0的0次方未定义,应该有如下判断: + +```java +//如果指数为0 +if(exponent == 0){ + if(base != 0){ + return 1; + }else{ + throw new ArithmeticException("0^0 is undefined"); + } +} +``` + +剩下的就是计算乘方结果,但是不要忘了如果幂为负需要将结果取倒数: + +```java +//指数不为0且分母也不为0,正常计算并返回整数或分数 +double res = 1; +for(int i = 1 ; i <= exponent ; i++){ + res *= base; +} + +if(minus){ + return 1/res; +}else{ + return res; +} +``` + +也许你还可以锦上添花为幂乘方的计算引入二分计算(当幂为偶数时`2^n = 2^(n/2) * 2^(n/2)`): + +```java +public double binaryPower(double base, int exp){ + if(exp == 1){ + return base; + } + double res = 1; + res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2)); + return exp % 2 == 0 ? res : res * base; +} +``` + +### 调整数组顺序使奇数位于偶数前面 + +#### 题目描述 + +输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的**相对位置不变**。 + +```java +public void reOrderArray(int [] arr) { + +} +``` + + + +#### 解析 + +读题之后发现这个跟快排的`partition`思路很像,都是选取一个比较基准将数组分成两部分,当然你也可以以`arr[i] % 2 == 0`为基准将奇数放前半部分,将偶数放有半部分,但是虽然只需`O(n)`的时间复杂度但不能保证调整后奇数之间、偶数之间的相对位置: + +```java +public void reOrderArray(int [] arr) { + if(arr == null || arr.length == 0){ + return; + } + + int odd = -1; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] % 2 == 1){ + swap(arr, ++odd, i); + } + } +} + +public void swap(int[] arr, int i, int j){ + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; +} +``` + +涉及到排序稳定性,我们自然能够想到插入排序,从数组的第二个元素开始向后依次确定每个元素应处的位置,确定的逻辑是:将该数与前一个数比较,如果比前一个数小则与前一个数交换位置并在交换位置后继续与前一个数比较直到前一个数小于等于该数或者已达数组首部停止。 + +此题不过是将比较的逻辑由数值的大小改为:当前的数是否是奇数并且前一个数是偶数,是则递归向前交换位置。代码示例如下: + +```java +public void reOrderArray(int [] arr) { + if(arr == null || arr.length == 0){ + return; + } + + int odd = -1; + for(int i = 1 ; i < arr.length ; i++){ + for(int j = i ; j >= 1 ; j--){ + if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){ + swap(arr, j, j - 1); + } + } + } +} +``` + +### 链表中倒数第K个结点 + +#### 题目描述 + +输入一个链表,输出该链表中倒数第k个结点。 + +```java +public ListNode FindKthToTail(ListNode head,int k) { + +} +``` + + + +#### 解析 + +**倒数**,这又是一个从尾到头的遍历逻辑,而链表对从尾到头遍历是敏感的,前面我们有通过压栈/递归、反转链表的方式实现这个遍历逻辑,自然对于此题同样适用,但是那样未免太麻烦了,我们可以通过两个间距为(k-1)个结点的链表指针来达到此目的。 + +```java +public ListNode FindKthToTail(ListNode head,int k) { + //input check + if(head == null || k <= 0){ + return null; + } + ListNode tmp = new ListNode(0); + tmp.next = head; + ListNode p1 = tmp, p2 = tmp; + while(k > 0 && p1.next != null){ + p1 = p1.next; + k--; + } + //length < k + if(k != 0){ + return null; + } + while(p1 != null){ + p1 = p1.next; + p2 = p2.next; + } + + tmp = null; //help gc + + return p2; +} +``` + +这里使用了一个技巧,就是创建一个临时结点`tmp`作为两个指针的初始指向,以模拟`p1`先走`k`步之后,`p2`才开始走,没走时停留在初始位置的逻辑,有利于帮我们梳理指针在对应位置上的意义,这样当`p1`走到头时(`p1=null`),`p2`就是倒数第`k`个结点。 + +这里还有一个坑就是,笔者层试图为了简化代码将上述的`9 ~ 12`行写成如下偷懒模式而导致排错许久: + +```java +while(k-- > 0 && p1.next != null){ + p1 = p1.next; +} +``` + +原因是将`k--`写在`while()`中,无论判断是否通过都会执行`k = k - 1`,因此代码总是会在`if(k != 0)`处返回`null`,希望读者不要和笔者一样粗心。 + +> 总结:当遇到复杂的指针操作时,我们不妨试图多引入几个指针或者临时结点,以方便梳理我们的思路,加强代码的逻辑化,这些空间复杂度`O(1)`的操作通常也不会影响性能。 + +### 合并两个排序的链表 + +#### 题目描述 + +输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 + +```java +public ListNode Merge(ListNode list1,ListNode list2) { + +} +``` + + + +#### 解析 + +![image](https://ws3.sinaimg.cn/large/006zweohgy1fzbx9j54uuj30jg0ak3yz.jpg) + +```java +public ListNode Merge(ListNode list1,ListNode list2) { + if(list1 == null || list2 == null){ + return list1 == null ? list2 : list1; + } + ListNode newHead = list1.val < list2.val ? list1 : list2; + ListNode p1 = (newHead == list1) ? list1.next : list1; + ListNode p2 = (newHead == list2) ? list2.next : list2; + ListNode p = newHead; + while(p1 != null && p2 != null){ + if(p1.val <= p2.val){ + p.next = p1; + p1 = p1.next; + }else{ + p.next = p2; + p2 = p2.next; + } + p = p.next; + } + + while(p1 != null){ + p.next = p1; + p = p.next; + p1 = p1.next; + } + while(p2 != null){ + p.next = p2; + p = p.next; + p2 = p2.next; + } + + return newHead; +} +``` + +### 树的子结构 + +#### 题目描述 + +输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) + +```java +/** +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } + +}*/ +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1 == null || root2 == null){ + return false; + } + + return process(root1, root2); +} +``` + + + +#### 解析 + +这是一道典型的分解求解的复杂问题。典型的二叉树分解:遍历头结点、遍历左子树、遍历右子树。首先按照`root1`和`root2`的值是否相等划分为两种情况: + +1. 两个头结点的值相等,并且`root2.left`也是`roo1.left`的子结构(递归)、`root2.right`也是`root1.right`的子结构(递归),那么可返回`true`。 +2. 否则,要看只有当`root2`为`root1.left`的子结构或者`root2`为`root1.right`的子结构时,才能返回`true` + +据上述两点很容易得出如下递归逻辑: + +```java +if(root1.val == root2.val){ + if(process(root1.left, root2.left) && process(root1.right, root2.right)){ + return true; + } +} + +return process(root1.left, root2) || process(root1.right, root2); +``` + +接下来确定递归的终止条件,如果某个子过程`root2=null`那么说明在自上而下的比较过程中`root2`的结点已被罗列比较完了,这时无论`root1`是否为`null`,该子过程都应该返回`true`: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbyis1e3oj30dg04qaa5.jpg) + +```java +if(root2 == null){ + return true; +} +``` + +但是如果`root2 != null`而`root1 = null`,则应返回`false` + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbym9fv0bj30bv05974e.jpg) + +```java +if(root1 == null && root2 != null){ + return false; +} +``` + +完整代码如下: + +```java +public class Solution { + public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1 == null || root2 == null){ + return false; + } + + return process(root1, root2); + } + + public boolean process(TreeNode root1, TreeNode root2){ + if(root2 == null){ + return true; + } + if(root1 == null && root2 != null){ + return false; + } + + if(root1.val == root2.val){ + if(process(root1.left, root2.left) && process(root1.right, root2.right)){ + return true; + } + } + + return process(root1.left, root2) || process(root1.right, root2); + } +} +``` + +### 二叉树的镜像 + +#### 题目描述 + +操作给定的二叉树,将其变换为源二叉树的镜像。 + +![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbyup8oq3j306b08kjra.jpg) + +```java +public void Mirror(TreeNode root) { + +} +``` + + + +#### 解析 + +由图可知获取二叉树的镜像就是将原树的每个结点的左右孩子交换一下位置(这个规律一定要会找),也就是说我们只需遍历每个结点并交换`left,right`的引用指向就可以了,而我们有成熟的先序遍历: + +```java +public void Mirror(TreeNode root) { + if(root == null){ + return; + } + + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + Mirror(root.left); + Mirror(root.right); +} +``` + +### 顺时针打印矩阵 + +#### 题目描述 + +输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. + +```java +public ArrayList printMatrix(int [][] matrix) { + +} +``` + + + +#### 解析 + +![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbzo7qyu0j30gr093q3a.jpg) + +只要分析清楚了打印思路(左上角和右下角即可确定一条打印轨迹)后,此题主要考查条件控制的把握。只要给我一个左上角的点`(i,j)`和右下角的点`(m,n)`,就可以将这一圈的打印分解为四步: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzc01b7bpij309107hweh.jpg) + +但是如果左上角和右下角的点在一行或一列上那就没必要分解,直接打印改行或该列即可,打印的逻辑如下: + +```java +public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList res){ + if(i == m && j == n){ + res.add(matrix[i][j]); + return; + } + + if(i == m || j == n){ + //only one while will be execute + while(i < m){ + res.add(matrix[i++][j]); + } + while(j < n){ + res.add(matrix[i][j++]); + } + res.add(matrix[m][n]); + return; + } + + int p = i, q = j; + while(q < n){ + res.add(matrix[p][q++]); + } + //q == n + while(p < m){ + res.add(matrix[p++][q]); + } + //p == m + while(q > j){ + res.add(matrix[p][q--]); + } + //q == j + while(p > i){ + res.add(matrix[p--][q]); + } + //p == i +} +``` + +接着我们将每个圈的左上角和右下角传入该函数即可: + +```java +public ArrayList printMatrix(int [][] matrix) { + ArrayList res = new ArrayList(); + if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){ + return res; + } + int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1; + while(i <= m && j <= n){ + printEdge(matrix, i++, j++, m--, n--, res); + } + return res; +} +``` + +### 包含min函数的栈 + +#### 题目描述 + +定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。 + +```java +public class Solution { + + + public void push(int node) { + + } + + public void pop() { + + } + + public int top() { + + } + + public int min() { + + } +} +``` + + + +#### 解析 + +最直接的思路是使用一个变量保存栈中现有元素的最小值,但这只对只存不取的栈有效,当弹出的值不是最小值时还没什么影响,但当弹出最小值后我们就无法获取当前栈中的最小值。解决思路是使用一个最小值栈,栈顶总是保存当前栈中的最小值,每次数据栈存入数据时最小值栈就要相应的将存入后的最小值压入栈顶: + +```java +private Stack dataStack = new Stack(); +private Stack minStack = new Stack(); + +public void push(int node) { + dataStack.push(node); + if(!minStack.empty() && minStack.peek() < node){ + minStack.push(minStack.peek()); + }else{ + minStack.push(node); + } +} + +public void pop() { + if(!dataStack.empty()){ + dataStack.pop(); + minStack.pop(); + } +} + +public int top() { + if(!dataStack.empty()){ + return dataStack.peek(); + } + throw new IllegalStateException("stack is empty"); +} + +public int min() { + if(!dataStack.empty()){ + return minStack.peek(); + } + throw new IllegalStateException("stack is empty"); +} +``` + +### 栈的压入、弹出序列 + +#### 题目描述 + +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的**所有数字均不相等**。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的**长度是相等**的) + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + +} +``` + + + +#### 解析 + +可以使用两个指针`i,j`,初始时`i`指向压入序列的第一个,`j`指向弹出序列的第一个,试图将压入序列按照顺序压入栈中: + +1. 如果`arr1[i] != arr2[j]`,那么将`arr1[i]`压入栈中并后移`i`(表示`arr1[i]`还没到该它弹出的时刻) +2. 如果某次后移`i`之后发现`arr1[i] == arr2[j]`,那么说明此刻的`arr1[i]`被压入后应该被立即弹出才会产生给定的弹出序列,于是不压入`arr1[i]`(表示压入并弹出了)并后移`i`,`j`也要后移(表示弹出序列的`arr2[j]`记录已产生,接着产生或许的弹出记录即可)。 +3. 因为步骤2和3都会后移`i`,因此循环的终止条件是`i`到达`arr1.length`,此时若栈中还有元素,那么从栈顶到栈底形成的序列必须与`arr2`中`j`之后的序列相同才能返回`true`。 + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + //input check + if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){ + return false; + } + Stack stack = new Stack(); + int length = arr1.length; + int i = 0, j = 0; + while(i < length && j < length){ + if(arr1[i] != arr2[j]){ + stack.push(arr1[i++]); + }else{ + i++; + j++; + } + } + + while(j < length){ + if(arr2[j] != stack.peek()){ + return false; + }else{ + stack.pop(); + j++; + } + } + + return stack.empty() && j == length; +} +``` + +### 从上往下打印二叉树 + +#### 题目描述 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + +} +``` + + + +#### 解析 + +使用一个队列来保存当前遍历结点的孩子结点,首先将根节点加入队列中,然后进行队列非空循环: + +1. 从队列头取出一个结点,将该结点的值打印 +2. 如果取出的结点左孩子不空,则将其左孩子放入队列尾部 +3. 如果取出的结点右孩子不空,则将其右孩子放入队列尾部 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + ArrayList res = new ArrayList(); + if(root == null){ + return res; + } + LinkedList queue = new LinkedList(); + queue.addLast(root); + while(queue.size() > 0){ + TreeNode node = queue.pollFirst(); + res.add(node.val); + if(node.left != null){ + queue.addLast(node.left); + } + if(node.right != null){ + queue.addLast(node.right); + } + } + + return res; +} +``` + +### 二叉搜索树的后序遍历序列 + +#### 题目描述 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 + +```java +public boolean VerifySquenceOfBST(int [] sequence) { + +} +``` + + + +#### 解析 + +对于二叉树的后序序列,我们能够确定最后一个数就是根结点,还能确定的是前一半部分是左子树的后序序列,后一部分是右子树的后序序列。 + +遇到这种复杂问题,我们仍能采用三步走战略(根结点、左子树、右子树): + +1. 如果当前根结点的左子树**是BST**且其右子树也是BST,那么才可能是BST +2. 在1的条件下,如果左子树的**最大值**小于根结点且右子树的**最小值**大于根结点,那么这棵树就是BST + +据此我们需要定义一个递归体,该递归体需要收集的信息如下:下层需要向我返回其最大值、最小值、以及是否是BST + +```java +class Info{ + boolean isBST; + int max; + int min; + Info(boolean isBST, int max, int min){ + this.isBST = isBST; + this.max = max; + this.min = min; + } +} +``` + +递归体的定义如下: + +```java +public Info process(int[] arr, int start, int end){ + if(start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException("invalid input"); + } + //base case : only one node + if(start == end){ + return new Info(true, arr[end], arr[end]); + } + + int root = arr[end]; + Info left, right; + //not exist left child + if(arr[start] > root){ + right = process(arr, start, end - 1); + return new Info(root < right.min && right.isBST, + Math.max(root, right.max), Math.min(root, right.min)); + } + //not exist right child + if(arr[end - 1] < root){ + left = process(arr, start, end - 1); + return new Info(root > left.max && left.isBST, + Math.max(root, left.max), Math.min(root, left.min)); + } + + int l = 0, r = end - 1; + while(r > l && r - l != 1){ + int mid = l + ((r - l) >> 1); + if(arr[mid] > root){ + r = mid; + }else{ + l = mid; + } + } + left = process(arr, start, l); + right = process(arr, r, end - 1); + return new Info(left.isBST && right.isBST && root > left.max && root < right.min, + right.max, left.min); +} +``` + +> 总结:二叉树相关的信息收集问题分步走: +> +> 1. 分析当前状态需要收集的信息 +> 2. 根据下层传来的信息加工出当前状态的信息 +> 3. 确定递归终止条件 + +### 二叉树中和为某一值的路径 + +#### 题目描述 + +输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下**一直到叶结点**所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前) + +```java +public ArrayList> FindPath(TreeNode root,int target) { + +} +``` + + + +#### 解析 + +审题可知,我们需要有一个自上而下从根结点到每个叶子结点的遍历思路,而先序遍历刚好可以拿来用,我们只需在来到当前结点时将当前结点值加入到栈中,在离开当前结点时再将栈中保存的当前结点的值弹出即可使用栈模拟保存自上而下经过的结点,从而实现在来到每个叶子结点时只需判断栈中数值之和是否为`target`即可。 + +```java +public ArrayList> FindPath(TreeNode root,int target) { + ArrayList> res = new ArrayList(); + if(root == null){ + return res; + } + Stack stack = new Stack(); + preOrder(root, stack, 0, target, res); + return res; +} + +public void preOrder(TreeNode root, Stack stack, int sum, int target, + ArrayList> res){ + if(root == null){ + return; + } + + stack.push(root.val); + sum += root.val; + //leaf node + if(root.left == null && root.right == null && sum == target){ + ArrayList one = new ArrayList(); + one.addAll(stack); + res.add(one); + } + + preOrder(root.left, stack, sum, target, res); + preOrder(root.right, stack, sum, target, res); + + sum -= stack.pop(); +} +``` + +### 复杂链表的复制 + +#### 题目描述 + +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) + +```java +/* +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +*/ +public class Solution { + public RandomListNode Clone(RandomListNode pHead) + { + + } +} +``` + +#### 解析 + +此题主要的难点在于`random`指针的处理。 + +##### 方法一:使用哈希表,额外空间O(n) + +可以将链表中的结点都复制一份,用一个哈希表来保存,`key`是源结点,`value`就是副本结点,然后遍历`key`取出每个对应的`value`将副本结点的`next`指针和`random`指针设置好: + +```java +public RandomListNode Clone(RandomListNode pHead){ + if(pHead == null){ + return null; + } + HashMap map = new HashMap(); + RandomListNode p = pHead; + //copy + while(p != null){ + RandomListNode cp = new RandomListNode(p.label); + map.put(p, cp); + p = p.next; + } + //link + p = pHead; + while(p != null){ + RandomListNode cp = map.get(p); + cp.next = (p.next == null) ? null : map.get(p.next); + cp.random = (p.random == null) ? null : map.get(p.random); + p = p.next; + } + + return map.get(pHead); +} +``` + +##### 方法二:追加结点,额外空间O(1) + +首先将每个结点复制一份并插入到对应结点之后,然后遍历链表将副本结点的`random`指针设置好,最后将源结点和副本结点分离成两个链表 + +```java +public RandomListNode Clone(RandomListNode pHead){ + if(pHead == null){ + return null; + } + + RandomListNode p = pHead; + while(p != null){ + RandomListNode cp = new RandomListNode(p.label); + cp.next = p.next; + p.next = cp; + p = p.next.next; + } + + //more than two node + //link random pointer + p = pHead; + RandomListNode cp; + while(p != null){ + cp = p.next; + cp.random = (p.random == null) ? null : p.random.next; + p = p.next.next; + } + + //split source and copy + p = pHead; + RandomListNode newHead = p.next; + //p != null -> p.next != null + while(p != null){ + cp = p.next; + p.next = p.next.next; + p = p.next; + cp.next = (p == null) ? null : p.next; + } + + return newHead; +} +``` + +### 二叉搜索树与双向链表 + +#### 题目描述 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +```java +public TreeNode Convert(TreeNode root) { +} +``` + +#### 解析 + +典型的二叉树分解问题,我们可以定义一个黑盒`transform`,它的目的是将二叉树转换成双向链表,那么对于一个当前结点`root`,首先将其前驱结点(BST中前驱结点指中序序列的前一个数值,也就是当前结点的左子树上最右的结点,如果左子树为空则没有前驱结点)和后继结点(当前结点的右子树上的最左结点,如果右子树为空则没有后继结点),然后使用黑盒`transform`将左子树和右子树转换成双向链表,最后将当前结点和左子树形成的链表链起来(通过之前保存的前驱结点)和右子树形成的链表链起来(通过之前保存的后继结点),整棵树的转换完毕。 + +```java +public TreeNode Convert(TreeNode root) { + if(root == null){ + return null; + } + + //head is the most left node + TreeNode head = root; + while(head.left != null){ + head = head.left; + } + transform(root); + return head; +} + +//transform a tree to a double-link list +public void transform(TreeNode root){ + if(root == null){ + return; + } + TreeNode pre = root.left, next = root.right; + while(pre != null && pre.right != null){ + pre = pre.right; + } + while(next != null && next.left != null){ + next = next.left; + } + + transform(root.left); + transform(root.right); + //asume the left and right has transformed and what's remaining is link the root + root.left = pre; + if(pre != null){ + pre.right = root; + } + root.right = next; + if(next != null){ + next.left = root; + } +} +``` + +### 字符串全排列 + +#### 题目描述 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 + +#### 解析 + +定义一个递归体`generate(char[] arr, int index, TreeSet res)`,其中`char[] arr`和`index`组合表示上层状态给当前状态传递的信息,即`arr`中`0 ~ index-1`是已生成好的串,现在你(当前状态)要确定`index`位置上应该放什么字符(你可以从`index ~ arr.length - 1`上任选一个字符),然后将`index + 1`应该放什么字符递归交给子过程处理,当某个状态要确定`arr.length`上应该放什么字符时说明`0 ~ arr.length-1`位置上的字符已经生成好了,因此递归终止,将生成好的字符串记录下来(这里由于要求不能重复且按字典序排列,因此我们可以使用JDK中红黑树的实现`TreeSet`来做容器) + +```java +public ArrayList Permutation(String str) { + ArrayList res = new ArrayList(); + if(str == null || str.length() == 0){ + return res; + } + TreeSet set = new TreeSet(); + generate(str.toCharArray(), 0, set); + res.addAll(set); + return res; +} + +public void generate(char[] arr, int index, TreeSet res){ + if(index == arr.length){ + res.add(new String(arr)); + } + for(int i = index ; i < arr.length ; i++){ + swap(arr, index, i); + generate(arr, index + 1, res); + swap(arr, index, i); + } +} + +public void swap(char[] arr, int i, int j){ + if(arr == null || arr.length == 0 || i < 0 || j > arr.length - 1){ + return; + } + char tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} +``` + +> 注意:上述代码的第`19`行有个坑,笔者曾因忘记写第19行而排错许久,由于你任选一个`index ~ arr.length - 1`位置上的字符与`index`位置上的交换并将交换生成的结果交给了子过程(第`17,18`行),但你不应该影响后续选取其他字符放到`index`位置上而形成的结果,因此需要再交换回来(第`19`行) + +### 数组中出现次数超过一半的数 + +#### 题目描述 + +数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { +} +``` + +#### 解析 + +##### 方法一:基于partition查找数组中第k大的数 + +如果我们将数组排序,最快也要`O(nlogn)`,排序后的中位数自然就是出现次数超过长度一半的数。 + +我们知道快排的`partition`操作能够将数组按照一个基准划分成小于部分和大于等于部分并返回这个基准在数组中的下标,虽然一次`partition`并不能使数组整体有序,但是能够返回随机选择的数在`partition`之后的下标`index`,这个下标标识了它是第`index`大的数,这也意味着我们要求数组中第`k`大的数不一定要求数组整体有序。 + +于是我们在首次对整个数组`partition`之后将返回的`index`与`n/2`进行比较,并调整下一次`partition`的范围直到`index = n/2`为止我们就找到了。 + +这个时间复杂度需要使用`Master`公式计算(计算过程参见 http://www.zhenganwen.top/62859a9a.html#Master%E5%85%AC%E5%BC%8F),**使用`partition`查找数组中第k大的数时间复杂度为`O(n)`**,最后不要忘了验证一下`index = n/2`上的数出现的次数是否超过了长度的一半。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + if(arr.length == 1){ + return arr[0]; + } + + int index = partition(arr, 0, arr.length - 1); + int half = arr.length >> 1;// 0 <= half <= arr.length - 1 + while(index != half){ + index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); + } + + int count = 0; + for(int i = 0 ; i < arr.length ; i++){ + count = (arr[i] == arr[index]) ? ++count : count; + } + + return (count > arr.length / 2) ? arr[index] : 0; +} + +public int partition(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return end; + } + int random = start + (int)(Math.random() * (end - start + 1)); + swap(arr, random, end); + int small = start - 1; + for(int i = start ; i < end ; i++){ + if(arr[i] < arr[end]){ + swap(arr, ++small, i); + } + } + + swap(arr, ++small, end); + + return small; +} + +public void swap(int[] arr, int i, int j){ + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +##### 方法二 + +1. 使用一个`target`记录一个数,并使用`count`记录它出现的次数 +2. 初始时`target = arr[0]`,`count = 1`,表示`arr[0]`出现了1次 +3. 从第二个元素开始遍历数组,如果遇到的数不等于`target`就将`count`减1,否则加1 +4. 如果遍历到某个数时,`count`为0了,那么就将`target`设置为该数,并将`count`置1,继续向后遍历 + +如果存在出现次数超过一半的数,那么必定是`target`最后一次被设置时的数。 + +```java +public int MoreThanHalfNum_Solution(int [] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + //此题需要抓住出现次数超过数组长度的一半这个点来想 + //使用一个计数器,如果这个数出现一次就自增,否则自减,如果自减为0则更新被记录的数 + //如果存在出现次数大于一半的数,那么最后一次被记录的数就是所求之数 + int target = arr[0], count = 1; + for(int i = 1 ; i < arr.length ; i++){ + if(count == 0){ + target = arr[i]; + count = 1; + }else{ + count = (arr[i] == target) ? ++count : --count; + } + } + + if(count == 0){ + return 0; + } + + //不要忘了验证!!! + count = 0; + for(int i = 0 ; i < arr.length ; i++){ + count = (arr[i] == target) ? ++count : count; + } + + return (count > arr.length / 2) ? target : 0; +} +``` + +### 最小的k个数 + +#### 题目描述 + +输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 + +```java +public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { + +} +``` + +#### 解析 + +与上一题的求数组第k大的数如出一辙,如果某次`partition`之后你得到了第k大的数的下标,那么根据`partitin`规则该下标左边的数均比该下标上的数小,最小的k个数自然就是此时的`0~k-1`下标上的数 + +```java +public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { + ArrayList res = new ArrayList(); + if(arr == null || arr.length == 0 || k <= 0 || k > arr.length){ + //throw new IllegalArgumentException(); + return res; + } + + int index = partition(arr, 0, arr.length - 1); + k = k - 1; + while(index != k){ + index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); + } + + for(int i = 0 ; i <= k ; i++){ + res.add(arr[i]); + } + + return res; +} + +public int partition(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return end; + } + + int random = start + (int)(Math.random() * (end - start + 1)); + swap(arr, random, end); + int small = start - 1; + for(int i = start ; i < end ; i++){ + if(arr[i] < arr[end]){ + swap(arr, ++small, i); + } + } + + swap(arr, ++small, end); + return small; +} + +public void swap(int[] arr, int i, int j){ + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +### 连续子数组的最大和 + +#### 题目描述 + +HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + +} +``` + +#### 解析 + +##### 暴力解 + +暴力法是找出所有子数组,然后遍历求和,时间复杂度为`O(n^3)` + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + int max = Integer.MIN_VALUE; + + //start + for(int i = 0 ; i < arr.length ; i++){ + //end + for(int j = i ; j < arr.length ; j++){ + //sum + int sum = 0; + for(int k = i ; k <= j ; k++){ + sum += arr[k]; + } + max = Math.max(max, sum); + } + } + + return max; +} +``` + +##### 最优解 + +使用一个`sum`记录累加和,初始时为0,遍历数组: + +1. 如果遍历到`i`时,发现`sum`小于0,那么丢弃这个累加和,将`sum`重置为`0` +2. 将当前元素累加到`sum`上,并更新最大和`maxSum` + +```java +public int FindGreatestSumOfSubArray(int[] arr) { + if(arr == null || arr.length == 0){ + return 0; + } + int sum = 0, max = Integer.MIN_VALUE; + for(int i = 0 ; i < arr.length ; i++){ + if(sum < 0){ + sum = 0; + } + sum += arr[i]; + max = Math.max(max, sum); + } + + return max; +} +``` + +### 整数中1出现的次数(从1到n整数中1出现的次数) + +#### 题目描述 + +求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。 + +#### 解析 + +##### 遍历一遍不就完了吗 + +当然,你可从1遍历到n,然后将当前被遍历的到的数中1出现的次数累加到结果中可以很容易地写出如下代码: + +```java +public int NumberOf1Between1AndN_Solution(int n) { + if(n < 1){ + return 0; + } + int res = 0; + for(int i = 1 ; i <= n ; i++){ + res += count(i); + } + return res; +} + +public int count(int n){ + int count = 0; + while(n != 0){ + //取个位 + count = (n % 10 == 1) ? ++count : count; + //去掉个位 + n /= 10; + } + return count; +} +``` + +但n多大就会循环多少次,这并不是面试官所期待的,这时我们就需要找规律看是否有捷径可走 + +##### 不用数我也知道 + +以`51234`这个数为例,我们可以先将`51234`划分成`1~1234`(去掉最高位)和`1235~51234`两部分来求解。下面先分析`1235~51234`这个区间的结果: + +1. 所有的数中,1在最高位(万位)出现的次数 + + 对于`1235~51234`,最高位为1时(即万位为1时)的数有`10000~19999`这10000个数,也就是说1在最高位(万位)出现的次数为10000,因此我们可以得出结论:如果最高位大于1,那么在最高位上1出现的次数为最高位对应的单位(本例中为一万次);但如果最高位为1,比如`1235~11234`,那么次数就为去掉最高位之后的数了,`11234`去掉最高位后是`1234`,即1在最高位上出现的次数为`1234` + +2. 所有的数中,1在非最高位上出现的次数 + + 我们可以进一步将`1235~51234`按照最高位的单位划分成4个区间(能划分成几个区间由最高位上的数决定,这里最高位为5,所以能划分5个大小为一万子区间): + + - `1235~11234` + - `11235~21234` + - `21235~31234` + - `31235~41234` + - `41235~51234` + + 而每个数不考虑万位(因为1在万位出现的总次数在步骤1中已统计好了),其余四位(个、十、百、千)取一位放1(比如千位),剩下的3位从`0~9`中任意选(`10 * 10 * 10`),那么仅统计1在千位上出现的次数之和就是:`5(子区间数) * 10 * 10 * 10`,还有百位、十位、个位,结果为:`4 * 10 * 10 * 10 * 5`。 + + 因此非高位上1出现的总次数的计算通式为:`(n-1) * 10^(n-2) * 十进制最高位上的数 `(其中`n`为十进制的总位数) + + 于是`1235 ~ 51234`之间所有的数的所有的位上1出现的次数的综合我们就计算出来了 + +剩下`1 ~ 1234`,你会发现这与`1 ~ 51234`的问题是一样的,因此可以做递归处理,即子过程也会将`1 ~ 1234`也分成`1 ~ 234`和`235 ~ 1234`两部分,并计算`235~1234`而将`1~234`又进行递归处理。 + +而递归的终止条件如下: + +1. 如果`1~n`中的`n`:`1 <= n <= 9`,那么就可以直接返回1了,因为只有数1出现了一次1 +2. 如果`n == 0`,比如将`10000`划分成的两部分是`0 ~ 0(10000去掉最高位后的结果)`和`1 ~ 10000`,那么就返回0 + +```java +public int NumberOf1Between1AndN_Solution(int n) { + if(n < 1){ + return 0; + } + return process(n); +} + +public int process(int n){ + if(n == 0){ + return 0; + } + if(n < 10 && n > 0){ + return 1; + } + int res = 0; + //得到十进制位数 + int bitCount = bitCount(n); + //十进制最高位上的数 + int highestBit = numOfBit(n, bitCount); + //1、统计最高位为1时,共有多少个数 + if(highestBit > 1){ + res += powerOf10(bitCount - 1); + }else{ + //highestBit == 1 + res += n - powerOf10(bitCount - 1) + 1; + } + //2、统计其它位为1的情况 + res += powerOf10(bitCount - 2) * (bitCount - 1) * highestBit; + //3、剩下的部分交给递归 + res += process(n % powerOf10(bitCount - 1)); + return res; +} + +//返回10的n次方 +public int powerOf10(int n){ + if(n == 0){ + return 1; + } + boolean minus = false; + if(n < 0){ + n = -n; + minus = true; + } + int res = 1; + for(int i = 1 ; i <= n ; i++){ + res *= 10; + } + return minus ? 1 / res : res; +} + +public int bitCount(int n){ + int count = 1; + while((n /= 10) != 0){ + count++; + } + return count; +} + +public int numOfBit(int n, int bit){ + while(bit-- > 1){ + n /= 10; + } + return n % 10; +} +``` + +笔者曾纠结,对于一个四位数,每个位上出现1时都统计了一遍会不会有重复,比如`11111`这个数在最高位为1时的`10000 ~ 19999`统计了一遍,在统计非最高位的其他位上为1时又统计了4次,总共被统计了5次,而这个数1出现的次数也确实是5次,因此没有重复。 + +### 把数组排成最小的数 + +#### 题目描述 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 + +#### 解析 + +这是一个贪心问题,你发现将数组按递增排序之后依次连接起来的结果并不是最优的结果,于是需要寻求贪心策略,对于这类最小数和最小字典序的问题而言,贪心策略是:如果`3`和`32`相连的结果大于`32`和`3`相连的结果,那么视作`3`比`32`大,最后我们需要按照按照这种策略将数组进行升序排序,以得到首尾相连之后的结果是最小数字(最小字典序)。 + +```java +public String PrintMinNumber(int [] numbers) { + if(numbers == null || numbers.length == 0){ + return ""; + } + List list = new ArrayList(); + for(int num : numbers){ + list.add(num); + } + Collections.sort(list, new MyComparator()); + StringBuilder res = new StringBuilder(""); + for(Integer integer : list){ + res.append(integer.toString()); + } + return res.toString(); +} + +class MyComparator implements Comparator{ + public int compare(Integer i1, Integer i2){ + String s1 = i1.toString() + i2.toString(); + String s2 = i2.toString() + i1.toString(); + return Integer.parseInt(s1) - Integer.parseInt(s2); + } +} +``` + +### 丑数 + +#### 题目描述 + +把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 + +#### 解析 + +老实说,在《剑指offer》上看这道题的时候每太看懂,以至于第一遍在牛客网OJ这道题的时候都是背下来写上去的,直到这第二遍总结时才弄清整个思路,思路的核心就是第一个丑数是1(题目给的),此后的每一个丑数都是由之前的某个丑数与2或3或5的乘积得来 + +![image](https://wx4.sinaimg.cn/large/006zweohgy1fzdddzfdfnj30pm0d4jtb.jpg) + +```java +public int GetUglyNumber_Solution(int index) { + if(index < 1){ + //throw new IllegalArgumentException("index must bigger than one"); + return 0; + } + if(index == 1){ + return 1; + } + + int[] arr = new int[index]; + arr[0] = 1; + int indexOf2 = 0, indexOf3 = 0, indexOf5 = 0; + for(int i = 1 ; i < index ; i++){ + arr[i] = Math.min(arr[indexOf2] * 2, Math.min(arr[indexOf3] * 3, arr[indexOf5] * 5)); + indexOf2 = (arr[indexOf2] * 2 <= arr[i]) ? ++indexOf2 : indexOf2; + indexOf3 = (arr[indexOf3] * 3 <= arr[i]) ? ++indexOf3 : indexOf3; + indexOf5 = (arr[indexOf5] * 5 <= arr[i]) ? ++indexOf5 : indexOf5; + } + + return arr[index - 1]; +} +``` + +### 第一个只出现一次的字符 + +#### 题目描述 + +在一个字符串(0<=字符串长度<=10000,**全部由字母组成**)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写). + +#### 解析 + +可以从头遍历字符串,并使用一个表记录每个字符第一次出现的位置(初始时表中记录的位置均为-1),如果记录当前被遍历字符出现的位置时发现之前已经记录过了(通过查表,该字符的位置不是-1而是大于等于0的一个有效索引),那么当前字符不在答案的考虑范围内,通过将表中该字符的出现索引标记为`-2`来标识。 + +遍历一遍字符串并更新表之后,再遍历一遍字符串,如果发现某个字符在表中对应的记录是一个有效索引(大于等于0),那么该字符就是整个串中第一个只出现一次的字符。 + +由于题目标注字符串全都由字母组成,而字母可以使用`ASCII`码表示且`ASCII`范围为`0~255`,因此使用了一个长度为`256`的数组来实现这张表。用字母的`ASCII`值做索引,索引对应的值就是字母在字符串中第一次出现的位置(初始时为-1,第一次遇到时设置为出现的位置,重复遇到时置为-2)。 + +```java +public int FirstNotRepeatingChar(String str) { + if(str == null || str.length() == 0){ + return -1; + } + //全部由字母组成 + int[] arr = new int[256]; + for(int i = 0 ; i < arr.length ; i++){ + arr[i] = -1; + } + for(int i = 0 ; i < str.length() ; i++){ + int ascii = (int)str.charAt(i); + if(arr[ascii] == -1){ + //set index of first apearance + arr[ascii] = i; + }else if(arr[ascii] >= 0){ + //repeated apearance, don't care + arr[ascii] = -2; + } + //arr[ascii] == -2 -> do not care + } + + for(int i = 0 ; i < str.length() ; i++){ + int ascii = (int)str.charAt(i); + if(arr[ascii] >= 0){ + return arr[ascii]; + } + } + + return -1; +} +``` + +### 数组中的逆序对 + +#### 题目描述 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007 + +```java +public int InversePairs(int [] arr) { + if(arr == null || arr.length <= 1){ + return 0; + } + return mergeSort(arr, 0, arr.length - 1).pairs; +} +``` + +#### 输入描述 + +1. 题目保证输入的数组中没有相同的数字 +2. 数据范围:对于%50的数据,size<=10^4;对于%75的数据,size<=10^5;对于%100的数据,size<=2*10^5 + + + +#### 解析 + +借助归并排序的流程,将归并流程中前一个数组的数比后一个数组的数小的情况记录下来。 + +归并的原始逻辑是根据输入的无序数组返回一个新建的排好序的数组: + +```java +public int[] mergeSort(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return new int[]{ arr[end] }; + } + + int[] arr1 = mergeSort(arr, start, mid); + int[] arr2 = Info right = mergeSort(arr, mid + 1, end); + int[] copy = new int[arr1.length + arr2.length]; + int p1 = 0, p2 = 0, p = 0; + + while(p1 < arr1.length && p2 < arr2.length){ + if(arr1[p1] > arr2[p2]){ + copy[p++] = arr1[p1++]; + }else{ + copy[p++] = arr2[p2++]; + } + } + while(p1 < arr1.length){ + copy[p++] = arr1[p1++]; + } + while(p2 < arr2.length){ + copy[p++] = arr2[p2++]; + } + return copy; +} +``` + +而我们需要再此基础上对子状态收集的信息进行改造,假设左右两半部分分别有序了,那么进行`merge`的时候,不应是从前往后复制了,这样当`arr1[p1] > arr2[p2]`的时候并不知道`arr2`的`p2`后面还有多少元素是比`arr1[p1]`小的,要想一次比较就统计出`arr2`中所有比`arr1[p1]`小的数需要将`p1,p2`从`arr1,arr2`的尾往前遍历: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdg2nzuzkj30n006odg2.jpg) + +而将比较后较大的数移入辅助数组的逻辑还是一样。这样当前递归状态需要收集左半子数组和右半子数组的变成有序过程中记录的逆序对数和自己`merge`记录的逆序对数之和就是当前状态要返回的信息,并且`merge`后形成的有序辅助数组也要返回。 + +```java +public int InversePairs(int [] arr) { + if(arr == null || arr.length <= 1){ + return 0; + } + return mergeSort(arr, 0, arr.length - 1).pairs; +} + +class Info{ + int arr[]; + int pairs; + Info(int[] arr, int pairs){ + this.arr = arr; + this.pairs = pairs; + } +} + +public Info mergeSort(int[] arr, int start, int end){ + if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ + throw new IllegalArgumentException(); + } + if(start == end){ + return new Info(new int[]{arr[end]}, 0); + } + + int pairs = 0; + int mid = start + ((end - start) >> 1); + Info left = mergeSort(arr, start, mid); + Info right = mergeSort(arr, mid + 1, end); + pairs += (left.pairs + right.pairs) % 1000000007; + + int[] arr1 = left.arr, arr2 = right.arr, copy = new int[arr1.length + arr2.length]; + int p1 = arr1.length - 1, p2 = arr2.length - 1, p = copy.length - 1; + + while(p1 >= 0 && p2 >= 0){ + if(arr1[p1] > arr2[p2]){ + pairs += (p2 + 1); + pairs %= 1000000007; + copy[p--] = arr1[p1--]; + }else{ + copy[p--] = arr2[p2--]; + } + } + + while(p1 >= 0){ + copy[p--] = arr1[p1--]; + } + while(p2 >= 0){ + copy[p--] = arr2[p2--]; + } + + return new Info(copy, pairs % 1000000007); +} +``` + +### 两个链表的第一个公共结点 + +#### 题目描述 + +输入两个链表,找出它们的第一个公共结点。 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + +} +``` + +#### 解析 + +首先我们要分析两个链表的组合状态,根据有环、无环相互组合只可能会出现如下几种情况: + +![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdz1wxjy8j30pc0cmmzb.jpg) + +于是我们首先要判断两个链表是否有环,判断链表是否有环以及有环链表的入环结点在哪已有前人给我们总结好了经验: + +1. 使用一个快指针和一个慢指针同时从首节点出发,快指针一次走两步而慢指针一次走一步,如果两指针相遇则说明有环,否则无环 +2. 如果两指针相遇,先将快指针重新指向首节点,然后两指针均一次走一步,再次相遇时的结点就是入环结点 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + //若其中一个链表为空则不存在相交问题 + if(pHead1 == null || pHead2 == null){ + return null; + } + ListNode ringNode1 = ringNode(pHead1); + ListNode ringNode2 = ringNode(pHead2); + //如果一个有环,另一个无环 + if((ringNode1 == null && ringNode2 != null) || + (ringNode1 != null && ringNode2 == null)){ + return null; + } + //如果两者都无环,判断是否共用尾结点 + else if(ringNode1 == null && ringNode2 == null){ + return firstCommonNode(pHead1, pHead2, null); + } + //剩下的情况就是两者都有环了 + else{ + //如果入环结点相同,那么第一个相交的结点肯定在入环结点之前 + if(ringNode1 == ringNode2){ + return firstCommonNode(pHead1, pHead2, ringNode1); + } + //如果入环结点不同,看能否通过ringNode1的后继找到ringNode2 + else{ + ListNode p = ringNode1; + while(p.next != ringNode1){ + p = p.next; + if(p == ringNode2){ + break; + } + } + //如果能找到,那么第一个相交的结点既可以是ringNode1也可以是ringNode2 + return (p == ringNode2) ? ringNode1 : null; + } + } +} + +//查找两链表的第一个公共结点,如果两链表无环,则传入common=null,如果都有环且入环结点相同,那么传入common=入环结点 +public ListNode firstCommonNode(ListNode pHead1, ListNode pHead2, ListNode common){ + ListNode p1 = pHead1, p2 = pHead2; + int len1 = 1, len2 = 1, gap = 0; + while(p1.next != common){ + p1 = p1.next; + len1++; + } + while(p2.next != common){ + p2 = p2.next; + len2++; + } + //如果是两个无环链表,要判断一下是否有公共尾结点 + if(common == null && p1 != p2){ + return null; + } + gap = len1 > len2 ? len1 - len2 : len2 - len1; + //p1指向长链表,p2指向短链表 + p1 = len1 > len2 ? pHead1 : pHead2; + p2 = len1 > len2 ? pHead2 : pHead1; + while(gap-- > 0){ + p1 = p1.next; + } + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + } + return p1; +} + +//判断链表是否有环,没有返回null,有则返回入环结点(整个链表是一个环时入环结点就是头结点) +public ListNode ringNode(ListNode head){ + if(head == null){ + return null; + } + ListNode p1 = head, p2 = head; + while(p1.next != null && p1.next.next != null){ + p1 = p1.next.next; + p2 = p2.next; + if(p1 == p2){ + break; + } + } + + if(p1.next == null || p1.next.next == null){ + return null; + } + + p1 = head; + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + } + //可能整个链表就是一个环,这时入环结点就是头结点!!! + return p1 == p2 ? p1 : head; +} +``` + +### 数字在排序数组中出现的次数 + +#### 题目描述 + +统计一个数字在排序数组中出现的次数。 + +```java +public int GetNumberOfK(int [] array , int k) { + +} +``` + +#### 解析 + +我们可以分两步解决,先找出数值为k的连续序列的左边界,再找右边界。可以采用二分的方式,以查找左边界为例:如果`arr[mid]`小于`k`那么移动左指针,否则移动右指针(初始时左指针指向`-1`,而右指针指向尾元素`arr.length`),当两个指针相邻时,左指针及其左边的数均小于`k`而右指针及其右边的数均大于或等于`k`,因此此时右指针就是要查找的左边界,同样的方式可以求得右边界。 + +值得注意的是,笔者曾将左指针初始化为`0`而右指针初始化为`arr.length - 1`,这与指针指向的含义是相悖的,因为左指针指向的元素必须是小于`k`的,而我们并不能保证`arr[0]`一定小于`k`,同样的我们也不能保证`arr[arr.length - 1]`一定大于等于`k`。 + +还有一点就是如果数组中没有`k`这个算法是否依然会返回一个正确的值(0),这也是需要验证的。 + +```java +public int GetNumberOfK(int [] arr , int k) { + if(arr == null || arr.length == 0){ + return 0; + } + if(arr.length == 1){ + return (arr[0] == k) ? 1 : 0; + } + + int start, end, left, right; + for(start = -1, end = arr.length ; end > start && end - start != 1 ;){ + int mid = start + ((end - start) >> 1); + if(arr[mid] >= k){ + end = mid; + }else{ + start = mid; + } + } + left = end; + for(start = -1, end = arr.length; end > start && end - start != 1 ;){ + int mid = start + ((end - start) >> 1); + if(arr[mid] > k){ + end = mid; + }else{ + start = mid; + } + } + right = start; + return right - left + 1; +} +``` + +### 二叉树的深度 + +#### 题目描述 + +输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 + +```java +public int TreeDepth(TreeNode root) { +} +``` + +#### 解析 + +1. 将`TreeDepth`看做一个黑盒,假设利用这个黑盒收集到了左子树和右子树的深度,那么当前这棵树的深度就是前面两者的最大值加1 +2. `base case`,如果当前是一棵空树,那么深度为0 + +```java +public class Solution { + public int TreeDepth(TreeNode root) { + if(root == null){ + return 0; + } + return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1; + } +} +``` + +### 平衡二叉树 + +#### 题目描述 + +输入一棵二叉树,判断该二叉树是否是平衡二叉树。 + +```java +public boolean IsBalanced_Solution(TreeNode root) { + +} +``` + +#### 解析 + +判断当前这棵树是否是平衡二叉所需要收集的信息: + +1. 左子树、右子树各自是平衡二叉树吗(需要收集子树是否是平衡二叉树) +2. 如果1成立,还需要收集左子树和右子树的高度,如果高度相差不超过1那么当前这棵树才是平衡二叉树(需要收集子树的高度) + +```java +class Info{ + boolean isBalanced; + int height; + Info(boolean isBalanced, int height){ + this.isBalanced = isBalanced; + this.height = height; + } +} +``` + +递归体的定义:(这里高度之差不超过1中的`left.height - right.height == 0`容易被忽略) + +```java +public boolean IsBalanced_Solution(TreeNode root) { + return process(root).isBalanced; +} + +public Info process(TreeNode root){ + if(root == null){ + return new Info(true, 0); + } + Info left = process(root.left); + Info right = process(root.right); + if(!left.isBalanced || !right.isBalanced){ + //如果左子树或右子树不是平衡二叉树,那么当前这棵树肯定也不是,树高度信息也就没用了 + return new Info(false, 0); + } + //高度之差不超过1 + if(left.height - right.height == 1 || left.height - right.height == -1 || + left.height - right.height == 0){ + return new Info(true, Math.max(left.height, right.height) + 1); + } + return new Info(false, 0); +} +``` + +### 数组中只出现一次的数字 + +#### 题目描述 + +一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。 + +#### 解析 + +如果没有解过类似的题目,思路比较难打开。面试官可能会提醒你,如果是让你求一个整型数组里只有一个数只出现了一次而其它数出现了偶数次呢?你应该联想到: + +1. **偶数次相同的数异或的结果是0** +2. **任何数与0异或的结果是它本身** + +于是将数组从头到尾求异或和便可得知结果。那么对于此题,能否将数组分成这样的两部分呢:每个部分只有一个数出现了一次,其他的数都出现偶数次。 + +如果我们仍将整个数组从头到尾求异或和,那结果应该和这两个只出现一次的数的异或结果相同,目前我们所能依仗的也就是这个结果了,能否靠这个结果将数组分成想要的两部分? + +由于两个只出现一次的数(用A和B表示)异或结果`A ^ B`肯定不为0,那么`A ^ B`的二进制表示中肯定包含数值为1的bit位,而这个位上的1肯定是由A或B提供的,也就是说我们能根据**这个bit位上的数是否为1**来区分A和B,那剩下的数呢? + +由于剩下的数都出现偶数次,因此相同的数都会被分到一边(按照某个bit位上是否为1来分)。 + +```java +public void FindNumsAppearOnce(int [] arr,int num1[] , int num2[]) { + if(arr == null || arr.length <= 1){ + return; + } + int xorSum = 0; + for(int num : arr){ + xorSum ^= num; + } + //取xorSum二进制表示中低位为1的bit位,将其它的bit位 置0 + //比如:xorSum = 1100,那么 (1100 ^ 1011) & 1100 = 0100,只剩下一个为1的bit位 + xorSum = (xorSum ^ (xorSum - 1)) & xorSum; + + for(int num : arr){ + num1[0] = (num & xorSum) == 0 ? num1[0] ^ num : num1[0]; + num2[0] = (num & xorSum) != 0 ? num2[0] ^ num : num2[0]; + } +} +``` + +### 和为S的连续正数序列 + +#### 题目描述 + +小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck! + +```java +public ArrayList > FindContinuousSequence(int sum) { + +} +``` + +#### 输出描述 + +输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序 + +#### 解析 + +将`1 ~ (S / 2 + 1)`区间的数`n`依次加入到队列中(因为从`S/2 + 1`之后的任意两个正数之和都大于`S`): + +1. 将`n`加入到队列`queue`中并将队列元素之和`queueSum`更新,更新`queueSum`之后如果发现等于`sum`,那么将此时的队列快照加入到返回结果`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**) +2. 更新`queueSum`之后如果发现大于`sum`,那么循环弹出队首元素直到`queueSum <= Sum`,如果循环弹出之后发现`queueSum == sum`那么将队列快照加入到`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**);如果`queueSum < sum`那么入队下一个`n` + +于是有如下代码: + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> res = new ArrayList(); + if(sum <= 1){ + return res; + } + LinkedList queue = new LinkedList(); + int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; + while(n <= halfSum){ + queue.addLast(n); + queueSum += n; + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + }else if(queueSum > sum){ + while(queueSum > sum){ + queueSum -= queue.pollFirst(); + } + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + } + } + n++; + } + + return res; +} +``` + +我们发现`11~15`和`20~24`行的代码是重复的,于是可以稍微优化一下: + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> res = new ArrayList(); + if(sum <= 1){ + return res; + } + LinkedList queue = new LinkedList(); + int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; + while(n <= halfSum){ + queue.addLast(n); + queueSum += n; + if(queueSum > sum){ + while(queueSum > sum){ + queueSum -= queue.pollFirst(); + } + } + if(queueSum == sum){ + ArrayList one = new ArrayList(); + one.addAll(queue); + res.add(one); + queueSum -= queue.pollFirst(); + } + n++; + } + + return res; +} +``` + +### 和为S的两个数字 + +#### 题目描述 + +输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 + +```java +public ArrayList FindNumbersWithSum(int [] arr,int sum) { + +} +``` + +#### 输出描述 + +对应每个测试案例,输出查找到的两个数,如果有多对,输出乘积最小的两个。 + +#### 解析 + +使用指针`l,r`,初始时`l`指向首元素,`r`指向尾元素,当两指针元素之和不等于`sum`且`r`指针在`l`指针右侧时循环: + +1. 如果两指针元素之和大于`sum`,那么将`r`指针左移,试图减小两指针之和 +2. 如果两指针元素之和小于`sum`,那么将`l`右移,试图增大两指针之和 +3. 如果两指针元素之和等于`sum`那么就可以返回了,或者`r`跑到了`l`的左边表名没有和`sum`的两个数,也可以返回了。 + +```java +public ArrayList FindNumbersWithSum(int [] arr,int sum) { + ArrayList res = new ArrayList(); + if(arr == null || arr.length <= 1 ){ + return res; + } + int l = 0, r = arr.length - 1; + while(arr[l] + arr[r] != sum && r > l){ + if(arr[l] + arr[r] > sum){ + r--; + }else{ + l++; + } + } + if(arr[l] + arr[r] == sum){ + res.add(arr[l]); + res.add(arr[r]); + } + return res; +} +``` + +### 旋转字符串 + +#### 题目描述 + +汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! + +```java +public String LeftRotateString(String str,int n) { + +} +``` + +#### 解析 + +将开头的一段子串移到串尾:将开头的子串翻转一下、将剩余的子串翻转一下,最后将整个子串翻转一下。按理来说应该输入`char[] str`的,这样的话这种算法不会使用额外空间。 + +```java +public String LeftRotateString(String str,int n) { + if(str == null || str.length() == 0 || n <= 0){ + return str; + } + char[] arr = str.toCharArray(); + reverse(arr, 0, n - 1); + reverse(arr, n, arr.length - 1); + reverse(arr, 0, arr.length - 1); + return new String(arr); +} + +public void reverse(char[] str, int start, int end){ + if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ + return; + } + for(int i = start, j = end ; j > i ; i++, j--){ + char tmp = str[i]; + str[i] = str[j]; + str[j] = tmp; + } +} +``` + +### 翻转单词顺序列 + +#### 题目描述 + +牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? + +```java +public String LeftRotateString(String str,int n) { + +} +``` + +#### 解析 + +先将整个字符串翻转,最后按照标点符号或空格一次将句中的单词翻转。注意:由于最后一个单词后面没有空格,因此需要单独处理!!! + +```java +public String ReverseSentence(String str) { + if(str == null || str.length() <= 1){ + return str; + } + char[] arr = str.toCharArray(); + reverse(arr, 0, arr.length - 1); + int start = -1; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] != ' '){ + //初始化start + start = (start == -1) ? i : start; + }else{ + //如果是空格,不用担心start>i-1,reverse会忽略它 + reverse(arr, start, i - 1); + start = i + 1; + } + } + //最后一个单词,这里比较容易忽略!!! + reverse(arr, start, arr.length - 1); + + return new String(arr); +} + +public void reverse(char[] str, int start, int end){ + if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ + return ; + } + for(int i = start, j = end ; j > i ; i++, j--){ + char tmp = str[i]; + str[i] = str[j]; + str[j] = tmp; + } +} +``` + +### 扑克牌顺子 + +#### 题目描述 + +LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。 + +#### 解析 + +先将数组排序(5个元素排序时间复杂O(1)),然后遍历数组统计王的数量和相邻非王牌之间的缺口数(需要用几个王来填)。还有一点值得注意:如果发现两种相同的非王牌,则不可能组成五张不同的顺子。 + +```java +public boolean isContinuous(int [] arr) { + if(arr == null || arr.length != 5){ + return false; + } + //5 numbers -> O(1) + Arrays.sort(arr); + int zeroCount = 0, slots = 0; + for(int i = 0 ; i < arr.length ; i++){ + //如果遇到两张相同的非王牌则不可能组成顺子,这点很容易忽略!!! + if(i > 0 && arr[i - 1] != 0){ + if(arr[i] == arr[i - 1]){ + return false; + }else{ + slots += arr[i] - arr[i - 1] - 1; + } + + } + zeroCount = (arr[i] == 0) ? ++zeroCount : zeroCount; + } + + return zeroCount >= slots; +} +``` + +### 孩子们的游戏(圆圈中剩下的数) + +#### 题目描述 + +每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) + +#### 解析 + +1. 报数时,在报到`m-1`之前,可通过报数求得报数的结点编号: + + ![image](https://ws1.sinaimg.cn/large/006zweohgy1fze7t11d3mj30j309ewey.jpg) + +2. 在某个结点(小朋友)出列后的重新编号过程中,可通过新编号求结点的就编号 + + ![image](https://wx1.sinaimg.cn/large/006zweohgy1fze8z9c5dcj30o40eg0u7.jpg) + + 因此在某轮重新编号时,我们能在已知新编号`x`的情况下通过公式`y = (x + S + 1) % n`求得结点重新标号之前的旧编号,上述两步分析的公式整理如下: + + 1. 某一轮报数出列前:`编号 = (报数 - 1)% 出列前结点个数` + 2. 某一轮报数出列后:`旧编号 = (新编号 + 出列编号 + 1)% 出列前结点个数`,因为出列结点是因为报数`m`才出列的,所以有:`出列编号 = (m - 1)% 出列前结点个数` + 3. 由2可推出:`旧编号 = (新编号 + (m - 1)% 出列前结点个数 + 1)% 出列前结点个数` ,若用`n`表示**出列后**结点个数:`y = (x + (m - 1) % n + 1) % n = (x + m - 1) % n + 1` + +经过上面3步的复杂分析之后,我们得出这么一个通式:`旧编号 = (新编号 + m - 1 )% 出列前结点个数 + 1`,于是我们就可以自下而上(用链表模拟出列过程是自上而下),求出**最后一轮重新编号为`1`**的小朋友(只剩他一个了)在倒数第二轮重新编号时的旧编号,自下而上可倒推出这个小朋友在第一轮编号时(这时还没有任何一个小朋友出列过)的原始编号,即目标答案。 + +> 注意:式子`y = (x + m - 1) % n + 1`的计算结果不可能为`0`,因此我们可以按小朋友从`1`开始编号,将最后的计算结果应题目的要求(小朋友从0开始编号)减一个1即可。 + +```java +public int LastRemaining_Solution(int n, int m) { + if(n <= 0){ + //throw new IllegalArgumentException(); + return -1; + } + //最后一次重新编号:最后一个结点编号为1,出列前结点数为2 + return orginalNumber(2, 0, n, m); +} + +//根据出列后的重新编号(newNumber)推导出列前的旧编号(返回值) +//n:出列前有多少小朋友,N:总共有多少个小朋友 +public int orginalNumber(int n, int newNumber, int N, int m){ + int lastNumber = (newNumber + m - 1) % n + 1; + if(n == N){ + return lastNumber; + } + return orginalNumber(n + 1, lastNumber, N, m); +} +``` + +### 求1+2+3+…+n + +#### 题目描述 + +求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 + +```java +public int Sum_Solution(int n) { + +} +``` + +#### 解析 + +##### 递归轻松解决 + +既然不允许遍历求和,不如将计算分解,如果知道了`f(n - 1)`,`f(n)`则可以通过`f(n - 1) + n`算出: + +```java +public int Sum_Solution(int n) { + if(n == 1){ + return 1; + } + return n + Sum_Solution(n - 1); +} +``` + +### 不用加减乘除做加法 + +#### 题目描述 + +写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 + +#### 解析 + +不要忘了加减乘除是人类熟悉的运算方法,而计算机只知道位运算哦! + +我们可以将两数的二进制表示写出来,然后按位与得出进位信息、按位或得出非进位信息,如果进位信息不为0,则循环计算直到进位信息为0,此时异或信息就是两数之和: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzeb2umgekj30hf0cvdgs.jpg) + +```java +public int Add(int num1,int num2) { + if(num1 == 0 || num2 == 0){ + return num1 == 0 ? num2 : num1; + } + int and = 0, xor = 0; + do{ + and = num1 & num2; + xor = num1 ^ num2; + num1 = and << 1; + num2 = xor; + }while(and != 0); + + return xor; +} +``` + +### 把字符串转换成整数 + +#### 题目描述 + +将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 + +```java +public int StrToInt(String str) { + +} +``` + +#### 输入描述 + +输入一个字符串,包括数字字母符号,可以为空 + +#### 输出描述 + +如果是合法的数值表达则返回该数字,否则返回0 + +#### 示例 + +输入:`+2147483647`,输出:`2147483647` +输入:`1a33`,输出`0` + +#### 解析 + +1. 只有第一个位置上的字符可以是`+`或`-`或数字,其他位置上的字符必须是数字 +2. 如果第一个字符是`-`,返回结果必须是负数 +3. 如果字符串只有一个字符,且为`+`或`-`,这情况很容易被忽略 +4. 在对字符串解析转换时,如果发现溢出(包括正数向负数溢出,负数向正数溢出),必须有所处理(此时可以和面试官交涉),但不能视而不见 + +```java +public int StrToInt(String str) { + if(str == null || str.length() == 0){ + return 0; + } + boolean minus = false; + int index = 0; + if(str.charAt(0) == '-'){ + minus = true; + index = 1; + }else if(str.charAt(0) == '+'){ + index = 1; + } + //如果只有一个正负号 + if(index == str.length()){ + return 0; + } + + if(checkInteger(str, index, str.length() - 1)){ + return transform(str, index, str.length() - 1, minus); + } + + return 0; +} + +public boolean checkInteger(String str, int start, int end){ + if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ + return false; + } + for(int i = start ; i <= end ; i++){ + if(str.charAt(i) < '0' || str.charAt(i) > '9'){ + return false; + } + } + return true; +} + +public int transform(String str, int start, int end, boolean minus){ + if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ + throw new IllegalArgumentException(); + } + int res = 0; + for(int i = start ; i <= end ; i++){ + int num = str.charAt(i) - '0'; + res = minus ? (res * 10 - num) : (res * 10 + num); + if((minus && res > 0) || (!minus && res < 0)){ + throw new ArithmeticException("the str is overflow int"); + } + } + return res; +} +``` + +### 数组中重复的数字 + +#### 题目描述 + +在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 + +```java +// Parameters: +// numbers: an array of integers +// length: the length of array numbers +// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation; +// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++ +// 这里要特别注意~返回任意重复的一个,赋值duplication[0] +// Return value: true if the input is valid, and there are some duplications in the array number +// otherwise false +public boolean duplicate(int numbers[],int length,int [] duplication) { + +} +``` + +#### 解析 + +认真审题发现输入数据是有特征的,即数组长度为`n`,数组中的元素都在`0~n-1`范围内,如果数组中没有重复的元素,那么排序后每个元素和其索引值相同,这就意味着数组中如果有重复的元素,那么数组排序后肯定有元素和它对应的索引是不等的。 + +顺着这个思路,我们可以将每个元素放到与它相等的索引上,如果某次放之前发现对应的索引上已有了和索引相同的元素,那么说明这个元素是重复的,由于每个元素最多会被调整两次,因此时间复杂`O(n)` + +```java +public boolean duplicate(int arr[],int length,int [] duplication) { + if(arr == null || arr.length == 0){ + return false; + } + int index = 0; + while(index < arr.length){ + if(arr[index] == arr[arr[index]]){ + if(index != arr[index]){ + duplication[0] = arr[index]; + return true; + }else{ + index++; + } + }else{ + int tmp = arr[index]; + arr[index] = arr[tmp]; + arr[tmp] = tmp; + } + } + + return false; +} +``` + +### 构建乘积数组 + +#### 题目描述 + +给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。 + +```java +public int[] multiply(int[] arr) { + +} +``` + + + +#### 分析 + +规律题: + +![image](https://ws2.sinaimg.cn/large/006zweohgy1fzee5lql6fj30ie0513ys.jpg) + +```java +public int[] multiply(int[] arr) { + if(arr == null || arr.length == 0){ + return arr; + } + int len = arr.length; + int[] arr1 = new int[len], arr2 = new int[len]; + arr1[0] = 1; + arr2[len - 1] = 1; + for(int i = 1 ; i < len ; i++){ + arr1[i] = arr1[i - 1] * arr[i - 1]; + arr2[len - 1 - i] = arr2[len - i] * arr[len - i]; + } + int[] res = new int[len]; + for(int i = 0 ; i < len ; i++){ + res[i] = arr1[i] * arr2[i]; + } + + return res; +} +``` + +### 正则表达式匹配 + +#### 题目描述 + +请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配 + +```java +public boolean match(char[] str, char[] pattern){ + +} +``` + + + +#### 解析 + +使用`p1`指向`str`中下一个要匹配的字符,使用`p2`指向`pattern`中剩下的模式串的首字符 + +1. 如果`p2 >= pattern.length`,表示模式串消耗完了,这时如果`p1`仍有字符要匹配那么返回`false`否则返回`true` +2. 如果`p1 >= str.length`,表示要匹配的字符都匹配完了,但模式串还没消耗完,这时剩下的模式串必须符合`a*b*c*`这样的范式以能够作为空串处理,否则返回`false` +3. `p1`和`p2`都未越界,按照`p2`后面是否是`*`来讨论 + 1. `p2`后面如果是`*`,又可按照`pattern[p2]`是否能够匹配`str[p1]`分析: + 1. `pattern[p2] == ‘.’ || pattern[p2] == str[p1]`,这时可以选择匹配一个`str[p1]`并继续向后匹配(不用跳过`p2`和其后面的`*`),也可以选择将`pattern[p2]`和其后面的`*`作为匹配空串处理,这时要跳过`p2`和 其后面的`*` + 2. `pattern[p2] != str[p1]`,只能作为匹配空串处理,跳过`p2` + 2. `p2`后面如果不是`*`: + 1. `pattern[p2] == str[p1] || pattern[p2] == ‘.’`,`p1,p2`同时后移一个继续匹配 + 2. `pattern[p2] == str[p1]`,直接返回`false` + +```java +public boolean match(char[] str, char[] pattern){ + if(str == null || pattern == null){ + return false; + } + if(str.length == 0 && pattern.length == 0){ + return true; + } + return matchCore(str, 0, pattern, 0); +} + +public boolean matchCore(char[] str, int p1, char[] pattern, int p2){ + //模式串用完了 + if(p2 >= pattern.length){ + return p1 >= str.length; + } + if(p1 >= str.length){ + if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ + return matchCore(str, p1, pattern, p2 + 2); + }else{ + return false; + } + } + + //如果p2的后面是“*” + if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ + if(pattern[p2] == '.' || pattern[p2] == str[p1]){ + //匹配一个字符,接着还可以向后匹配;或者将当前字符和后面的星合起来做空串 + return matchCore(str, p1 + 1, pattern, p2) || matchCore(str, p1, pattern, p2 + 2); + }else{ + return matchCore(str, p1, pattern, p2 + 2); + } + } + //如果p2的后面不是* + else{ + if(pattern[p2] == '.' || pattern[p2] == str[p1]){ + return matchCore(str, p1 + 1, pattern, p2 + 1); + }else{ + return false; + } + } +} +``` + +### 表示数值的字符串 + +#### 题目描述 + +请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。 + +```java +public boolean isNumeric(char[] str) { + +} +``` + +#### 解析 + +由题式可得出如下约束: + +1. 正负号只能出现在第一个位置或者`e/E`后一个位置 +2. `e/E`后面有且必须有整数 +3. 字符串中只能包含数字、小数点、正负号、`e/E`,其它的都是非法字符 +4. `e/E`的前面最多只能出现一次小数点,而`e/E`的后面不能出现小数点 + +```java +public boolean isNumeric(char[] str) { + if(str == null || str.length == 0){ + return false; + } + + boolean signed = false; //标识是否以正负号开头 + boolean decimal = false; //标识是否有小数点 + boolean existE = false; //是否含有e/E + int start = -1; //一段连续数字的开头 + int index = 0; //从0开始遍历字符 + + if(existSignAtIndex(str, 0)){ + signed = true; + index++; + } + + while(index < str.length){ + //以下按照index上可能出现的字符进行分支判断 + if(str[index] >= '0' && str[index] <= '9'){ + start = (start == -1) ? index : start; + index++; + + }else if(str[index] == '+' || str[index] == '-'){ + //首字符的+-我们已经判断过了,因此+-只可能出现在e/E的后面 + if(!existEAtIndex(str, index - 1)){ + return false; + } + index++; + + }else if(str[index] == '.'){ + //小数点只可能出现在e/E前面,且只可能出现一次 + //如果出现过小数点了,或者小数点前一段连续数字的前面是e/E + if(decimal || existEAtIndex(str, start - 1) + || existEAtIndex(str, start - 2) ){ + return false; + } + decimal = true;//出现了小数点 + index++; + //下一段连续数字的开始 + start = index; + + }else if(existEAtIndex(str, index)){ + if(existE){ + //如果已出现过e/E + return false; + } + existE = true; + index++; + //由于e/E后面可能是正负号也可能是数字,所以下一段连续数字的开始不确定 + start = !existSignAtIndex(str, index) ? index : index + 1; + + }else{ + return false; + } + } + + //如果最后一段连续数字的开始不存在 -> e/E后面没有数字 + if(start >= str.length){ + return false; + } + + return true; +} + +//在index上的字符是否是e或者E +public boolean existEAtIndex(char[] str, int index){ + if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ + return false; + } + return str[index] == 'e' || str[index] == 'E'; +} + +//在index上的字符是否是正负号 +public boolean existSignAtIndex(char[] str, int index){ + if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ + return false; + } + return str[index] == '+' || str[index] == '-'; +} +``` + + + +### 字符流中第一个只出现一次的字符 + +#### 题目描述 + +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 + +#### 输出描述 + +如果当前字符流没有存在出现一次的字符,返回#字符。 + +#### 解析 + +首先要选取一个容器来保存字符,并且要记录字符进入容器的顺序。如果不考虑中文字符,那么可以使用一张大小为`256`(对应`ASCII`码值范围)的表来保存字符,用字符的`ASCII`**码值**作为索引,用字符进入容器的**次序**作为索引对应的记录,表内部维护了一个计数器`position`,每当有字符进入时以该计数器的值作为该字符的次序(初始时,每个字符对应的次序为-1),如果设置该字符的次序时发现之前已设置过(次序不为-1,而是大于等于0),那么将该字符的次序置为-2,表示以后从容器取第一个只出现一次的字符时不考虑该字符。 + +当从容器取第一个只出现一次的字符时,考虑次序大于等于0的字符,在这个前提下找出次序最小的字符并返回。 + +```java +//不算中文,保存所有ascii码对应的字符只需256字节,记录ascii码为index的字符首次出现的位置 +int[] arr = new int[256]; +int position = 0; +{ + for(int i = 0 ; i < arr.length ; i++){ + //初始时所有字符的首次出现的位置为-1 + arr[i] = -1; + } +} +//Insert one char from stringstream +public void Insert(char ch){ + int ascii = (int)ch; + if(arr[ascii] == -1){ + arr[ascii] = position++; + }else if(arr[ascii] >= 0){ + arr[ascii] = -2; + } +} +//return the first appearence once char in current stringstream +public char FirstAppearingOnce(){ + int minPosi = Integer.MAX_VALUE; + char res = '#'; + for(int i = 0 ; i < arr.length ; i++){ + if(arr[i] >= 0 && arr[i] < minPosi){ + minPosi = arr[i]; + res = (char)i; + } + } + + return res; +} +``` + +### 删除链表中重复的结点 + +#### 题目描述 + +在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 + +```java +public ListNode deleteDuplication(ListNode pHead){ + +} +``` + +#### 解析 + +此题处理起来棘手的有两个地方: + +1. 如果某个结点的后继结点与其重复,那么删除该结点的一串连续重复的结点之后如何删除该结点本身,这就要求我们需要保留当前遍历结点的前驱指针。 + + 但是如果从头结点开始就出现一连串的重复呢?我们又如何删除删除头结点,因此我们需要新建一个辅助结点作为头结点的前驱结点。 + +2. 在遍历过程中如何区分当前结点是不重复的结点,还是在删除了它的若干后继结点之后最后也要删除它本身的重复结点?这就需要我们使用一个布尔变量记录是否开启了删除模式(`deleteMode`) + +经过上述两步分析,我们终于可以安心遍历结点了: + +```java +public ListNode deleteDuplication(ListNode pHead){ + if(pHead == null){ + return null; + } + ListNode node = new ListNode(Integer.MIN_VALUE); + node.next = pHead; + ListNode pre = node, p = pHead; + boolean deletedMode = false; + while(p != null){ + if(p.next != null && p.next.val == p.val){ + p.next = p.next.next; + deletedMode = true; + }else if(deletedMode){ + pre.next = p.next; + p = pre.next; + deletedMode = false; + }else{ + pre = p; + p = p.next; + } + } + + return node.next; +} +``` + +### 二叉树的下一个结点 + +#### 题目描述 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +#### 解析 + +由于中序遍历来到某个结点后,首先会接着遍历它的右子树,如果它没有右子树则会回到祖先结点中将它当做左子树上的结点的那一个,因此有如下分析: + +1. 如果当前结点有右子树,那么其后继结点就是其右子树上的最左结点 +2. 如果当前结点没有右子树,那么其后继结点就是其祖先结点中,将它当做左子树上的结点的那一个。 + +```java +public TreeLinkNode GetNext(TreeLinkNode pNode){ + if(pNode == null){ + return null; + } + //如果有右子树,后继结点是右子树上最左的结点 + if(pNode.right != null){ + TreeLinkNode p = pNode.right; + while(p.left != null){ + p = p.left; + } + return p; + }else{ + //如果没有右子树,向上查找第一个当前结点是父结点的左孩子的结点 + TreeLinkNode p = pNode.next; + while(p != null && pNode != p.left){ + pNode = p; + p = p.next; + } + + if(p != null && pNode == p.left){ + return p; + } + return null; + } +} +``` + +​ + +### 对称的二叉树 + +#### 题目描述 + +请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 + +```java +boolean isSymmetrical(TreeNode pRoot){ + +} +``` + +#### 解析 + +判断一棵树是否是镜像二叉树,只需将经典的先序遍历序列和变种的**先根再右再左**的先序遍历序列比较,如果相同则为镜像二叉树。 + +```java +boolean isSymmetrical(TreeNode pRoot){ + if(pRoot == null){ + return true; + } + StringBuffer str1 = new StringBuffer(""); + StringBuffer str2 = new StringBuffer(""); + preOrder(pRoot, str1); + preOrder2(pRoot, str2); + return str1.toString().equals(str2.toString()); +} + +public void preOrder(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder(root.left, str); + preOrder(root.right, str); +} + +public void preOrder2(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder2(root.right, str); + preOrder2(root.left, str); +} +``` + +### 按之字形打印二叉树 + +#### 题目描述 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +#### 解析 + +注意下述代码的第`14`行,笔者曾写为`stack2 = stack1 == empty ? stack1 : stack2`,你能发现错误在哪儿吗? + +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> res = new ArrayList(); + if(pRoot == null){ + return res; + } + + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + stack1.push(pRoot); + boolean flag = true;//先加左孩子,再加右孩子 + while(!stack1.empty() || !stack2.empty()){ + Stack empty = stack1.empty() ? stack1 : stack2; + stack1 = stack1 == empty ? stack2 : stack1; + stack2 = empty; + ArrayList row = new ArrayList(); + while(!stack1.empty()){ + TreeNode p = stack1.pop(); + row.add(p.val); + if(flag){ + if(p.left != null){ + stack2.push(p.left); + } + if(p.right != null){ + stack2.push(p.right); + } + }else{ + if(p.right != null){ + stack2.push(p.right); + } + if(p.left != null){ + stack2.push(p.left); + } + } + } + res.add(row); + flag = !flag; + } + + return res; +} +``` + +### 序列化二叉树 + +#### 题目描述 + +请实现两个函数,分别用来序列化和反序列化二叉树 + +#### 解析 + +怎么序列化的,就怎么反序列化。这里`deserialize`反序列化时对于序列化到`String[] arr`的哪个结点值来了的变量`index`有两个坑(都是笔者亲自踩的): + +1. 将`index`声明为成员的`int`,`Java`中函数调用时不会改变基本类型参数的值的,因此不要企图使用`int`表示当前序列化哪个结点的值来了 +2. 之后笔者想用`Integer`代替,但是`Integer`和`String`一样,都是不可变对象,所有的值更改操作在底层都是拆箱和装箱生成新的`Integer`,因此也不要使用`Integer`做序列化到哪一个结点数值来了的计数器 +3. 最好使用数组或者自定义的类(在类中声明一个`int`变量) + +```java +String Serialize(TreeNode root) { + if(root == null){ + return "#_"; + } + //处理头结点、左子树、右子树 + String res = root.val + "_"; + res += Serialize(root.left); + res += Serialize(root.right); + return res; +} + +TreeNode Deserialize(String str) { + if(str == null || str.length() == 0){ + return null; + } + Integer index = 0; + return deserialize(str.split("_"), new int[]{0}); +} + +//怎么序列化的,就怎么反序列化 +TreeNode deserialize(String[] arr, int[] index){ + if("#".equals(arr[index[0]])){ + index[0]++; + return null; + } + //头结点、左子树、右子树 + TreeNode root = new TreeNode(Integer.parseInt(arr[index[0]])); + index[0]++; + root.left = deserialize(arr, index); + root.right = deserialize(arr, index); + return root; +} +``` + +### 二叉搜索树的第k个结点 + +#### 题目描述 + +给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。 + +```java +TreeNode KthNode(TreeNode pRoot, int k){ + +} +``` + +#### 解析 + +二叉搜索树的特点是,它的中序序列是有序的,因此我们可以借助中序遍历在递归体中第二次来到当前结点时更新一下计数器,直到遇到第k个结点保存并返回即可。 + +值得注意的地方是: + +1. 由于计数器在递归中传来传去,因此你需要保证每个递归引用的是同一个计数器,这里使用的是一个`int[]`的第一个元素来保存 +2. 我们写中序遍历是不需要返回值的,可以在找到第k小的结点时将其保存在传入的数组中以返回给调用方 + +```java +TreeNode KthNode(TreeNode pRoot, int k){ + if(pRoot == null){ + return null; + } + TreeNode[] res = new TreeNode[1]; + inOrder(pRoot, new int[]{ k }, res); + return res[0]; +} + +public void inOrder(TreeNode root, int[] count, TreeNode[] res){ + if(root == null){ + return; + } + inOrder(root.left, count, res); + count[0]--; + if(count[0] == 0){ + res[0] = root; + return; + } + inOrder(root.right, count, res); +} +``` + +> 如果可以利用我们熟知的算法,比如本题中的中序遍历。管它三七二十一先将熟知方法写出来,然后再按具体的业务需求对其进行改造(包括返回值、参数列表,但一般不会更改遍历算法的返回值) + +### 数据流的中位数 + +#### 题目描述 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。 + +```java +public void Insert(Integer num) { + +} + +public Double GetMedian() { + +} +``` + +#### 解析 + +由于中位数只与排序后位于数组中间的一个数或两个数相关,而与数组两边的其它数无关,因此我们可以用一个大根堆保存数组左半边的数的最大值,用一个小根堆保存数组右半边的最小值,插入元素`O(logn)`,取中位数`O(1)`。 + +```java +public class Solution { + + //小根堆、大根堆 + PriorityQueue minHeap = new PriorityQueue(new MinRootHeadComparator()); + PriorityQueue maxHeap = new PriorityQueue(new MaxRootHeadComparator()); + int count = 0; + + class MaxRootHeadComparator implements Comparator{ + //返回值大于0则认为逻辑上i2大于i1(无关对象包装的数值) + public int compare(Integer i1, Integer i2){ + return i2.intValue() - i1.intValue(); + } + } + + class MinRootHeadComparator implements Comparator{ + public int compare(Integer i1, Integer i2){ + return i1.intValue() - i2.intValue(); + } + } + + public void Insert(Integer num) { + count++;//当前这个数是第几个进来的 + //编号是奇数就放入小根堆(右半边),否则放入大根堆 + if(count % 2 != 0){ + //如果要放入右半边的数比左半边的最大值要小则需调整左半边的最大值放入右半边并将当前这个数放入左半边,这样才能保证右半边的数都比左半边的大 + if(maxHeap.size() > 0 && num < maxHeap.peek()){ + maxHeap.add(num); + num = maxHeap.poll(); + } + minHeap.add(num); + }else{ + if(minHeap.size() > 0 && num > minHeap.peek()){ + minHeap.add(num); + num = minHeap.poll(); + } + maxHeap.add(num); + } + } + + public Double GetMedian() { + if(count == 0){ + return 0.0; + } + if(count % 2 != 0){ + return minHeap.peek().doubleValue(); + }else{ + return (minHeap.peek().doubleValue() + maxHeap.peek().doubleValue()) / 2; + } + } +} +``` + +### 滑动窗口的最大值 + +#### 题目描述 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。 + +```java +public ArrayList maxInWindows(int [] num, int size){ + +} +``` + +#### 解析 + +使用一个单调非增队列,队头保存当前窗口的最大值,后面保存在窗口移动过程中导致队头失效(出窗口)后的从而晋升为窗口最大值的候选值。 + +```java +public ArrayList maxInWindows(int [] num, int size){ + ArrayList res = new ArrayList(); + if(num == null || num.length == 0 || size <= 0 || size > num.length){ + return res; + } + + //用队头元素保存窗口最大值,队列中元素只能是单调递减的,窗口移动可能导致队头元素失效 + LinkedList queue = new LinkedList(); + int start = 0, end = size - 1; + for(int i = start ; i <= end ; i++){ + addLast(queue, num[i]); + } + res.add(queue.getFirst()); + //移动窗口 + while(end < num.length - 1){ + addLast(queue, num[++end]); + if(queue.getFirst() == num[start]){ + queue.pollFirst(); + } + start++; + res.add(queue.getFirst()); + } + + return res; +} + +public void addLast(LinkedList queue, int num){ + if(queue == null){ + return; + } + //加元素之前要确保该元素小于等于队尾元素 + while(queue.size() != 0 && num > queue.getLast()){ + queue.pollLast(); + } + queue.addLast(num); +} +``` + +### 矩形中的路径 + +#### 题目描述 + +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 + +#### 解析 + +定义一个黑盒`hasPathCorechar(matrix, rows, cols, int i, int j, str, index)`,表示从`rows`行`cols`列的矩阵`matrix`中的`(i,j)`位置开始走是否能走出一条与`str`的子串`index ~ str.length-1`相同的路径。那么对于当前位置`(i,j)`,需要关心的只有一下三点: + +1. `(i,j)`是否越界了 +2. `(i,j)`上的字符是否和`str[index]`匹配 +3. `(i,j)`是否已在之前走过的路径上 + +如果通过了上面三点检查,那么认为`(i,j)`这个位置是可以走的,剩下的就是`(i,j)`上下左右四个方向能否走出`str`的`index+1 ~ str.length-1`,这个交给黑盒就好了。 + +还有一点要注意,如果确定了可以走当前位置`(i,j)`,那么需要将该位置的`visited`标记为`true`,表示该位置在已走过的路径上,而退出`(i,j)`的时候(对应下面第`32`行)又要将他的`visited`重置为`false`。 + +```java +public boolean hasPath(char[] matrix, int rows, int cols, char[] str){ + if(matrix == null || matrix.length != rows * cols || str == null){ + return false; + } + boolean[] visited = new boolean[matrix.length]; + for(int i = 0 ; i < rows ; i++){ + for(int j = 0 ; j < cols ; j++){ + //以矩阵中的每个点作为起点尝试走出str对应的路径 + if(hasPathCore(matrix, rows, cols, i, j, str, 0, visited)){ + return true; + } + } + } + return false; +} + +//当前在矩阵的(i,j)位置上 +//index -> 匹配到了str中的第几个字符 +private boolean hasPathCore(char[] matrix, int rows, int cols, int i, int j, + char[] str, int index, boolean[] visited){ + if(index == str.length){ + return true; + } + //越界或字符不匹配或该位置已在路径上 + if(!match(matrix, rows, cols, i, j, str[index]) || visited[i * cols + j] == true){ + return false; + } + visited[i * cols + j] = true; + boolean res = hasPathCore(matrix, rows, cols, i + 1, j, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i - 1, j, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i, j + 1, str, index + 1, visited) || + hasPathCore(matrix, rows, cols, i, j - 1, str, index + 1, visited); + visited[i * cols + j] = false; + return res; +} + +//矩阵matrix中的(i,j)位置上是否是c字符 +private boolean match(char[] matrix, int rows, int cols, int i, int j, char c){ + if(i < 0 || i > rows - 1 || j < 0 || j > cols - 1){ + return false; + } + return matrix[i * cols + j] == c; +} +``` + +### 机器人的运动范围 + +#### 题目描述 + +地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? + +#### 解析 + +定义一个黑盒`walk(int threshold, int rows, int cols, int i, int j, boolean[] visited)`,它表示统计从`rows`行`cols`列的矩阵中的`(i,j)`开始所能到达的格子并返回,对于当前位置`(i,j)`有如下判断: + +1. `(i,j)`是否越界矩阵了 +2. `(i,j)`是否已被统计过了 +3. `(i,j)`的行坐标和列坐标的数位之和是否大于`k` + +如果通过了上面三重检查,则认为`(i,j)`是可以到达的(`res=1`),并标记`(i,j)`的`visited`为`true`表示已被统计过了,然后对`(i,j)`的上下左右的格子调用黑盒进行统计。 + +这里要注意的是,与上一题不同,`visited`不会在递归计算完子状态后被重置回`false`,因为每个格子只能被统计一次。`visited`的含义不一样 + +```java +public int movingCount(int threshold, int rows, int cols){ + if(threshold < 0 || rows < 0 || cols < 0){ + return 0; + } + boolean[] visited = new boolean[rows * cols]; + return walk(threshold, rows, cols, 0, 0, visited); +} + +private int walk(int threshold, int rows, int cols, int i, int j, boolean[] visited){ + if(!isLegalPosition(rows, cols, i, j) || visited[i * cols + j] == true + || bitSum(i) + bitSum(j) > threshold){ + return 0; + } + int res = 1; + visited[i * cols + j] = true; + res += walk(threshold, rows, cols, i + 1, j, visited) + + walk(threshold, rows, cols, i - 1, j, visited) + + walk(threshold, rows, cols, i, j + 1, visited) + + walk(threshold, rows, cols, i, j - 1, visited); + return res; +} + +private boolean isLegalPosition(int rows, int cols, int i, int j){ + if(i < 0 || j < 0 || i > rows - 1 || j > cols - 1){ + return false; + } + return true; +} + +public int bitSum(int num){ + int res = num % 10; + while((num /= 10) != 0){ + res += num % 10; + } + return res; +} +``` + From 480d5f02150ab690d53baf5d6677d9c3e2767668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 15:48:25 +0800 Subject: [PATCH 27/97] =?UTF-8?q?Delete=20Java=E5=89=91=E6=8C=87offer.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "Java\345\211\221\346\214\207offer.md" | 4087 ------------------------ 1 file changed, 4087 deletions(-) delete mode 100644 "Java\345\211\221\346\214\207offer.md" diff --git "a/Java\345\211\221\346\214\207offer.md" "b/Java\345\211\221\346\214\207offer.md" deleted file mode 100644 index 22d58c90..00000000 --- "a/Java\345\211\221\346\214\207offer.md" +++ /dev/null @@ -1,4087 +0,0 @@ ---- -title: 剑指offer解析(Java实现) -date: 2019-01-18 18:32:16 -updated_at:github.com/zanwen/my-offer-to-java -comments: true -photos: "" -categories: 数据结构与算法 -tags: 数据结构与算法 ---- - -> 以下题目按照牛客网在线编程排序,所有代码示例代码均已通过牛客网OJ。 - -### 二维数组的查找 - -#### 题目描述 - -在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 - -```java -public boolean Find(int target, int [][] arr) { - -} -``` - - - -#### 解析 - -暴力方法是遍历一遍二维数组,找到`target`就返回`true`,时间复杂度为`O(M * N)`(对于M行N列的二维数组)。 - -由题可知输入的数据样本具有高度规律性(单独一行的数据来看是有序的,单独一列的数据来看也是有序的),因此考虑能否有一个比较基准在一次的比较中根据有序性淘汰不必再进行遍历比较的数。**有序**、**查找**,由此不难联想到二分查找,我们可以借鉴二分查找的思路,每次选出一个数作为比较基准进而淘汰掉一些不必比较的数。二分是选取数组的中位数作为比较基准的,因此能够保证每次都淘汰掉二分之一的数,那么此题中有没有这种特性的数呢?我们不妨举例观察一下: - -![image](https://ws1.sinaimg.cn/large/006zweohgy1fzaxy3978uj304x04mt8m.jpg) - -不难发现上图中对角线上的数是其所在行和所在列形成的序列的中位数,不妨选取右上角的数作为比较基准,如果不相等,那么我们可以淘汰掉所有它左边的数或者它所有下面的,比如对于`target = 6`,因为`(0,3)`位置上的`4 < 6`,因此`(0,3)`位置及其同一行的左边的所有数都小于6因此可以直接淘汰掉,淘汰掉之后问题就变为了从剩下的三行中找`target`,这与原始问题是相似的,也就是说每一次都选取右上角的数据为比较基准然后淘汰掉一行或一列,直到某一轮被选取的数就是`target`或者已经淘汰得只剩下一个数的时候就一定能得出结果了,因此时间复杂度为被淘汰掉的行数和列数之和,即`O(M + N)`,经过分析后不难写出如下代码: - -```java -public boolean Find(int target, int [][] arr) { - //input check - if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){ - return false; - } - int i = 0, j = arr[0].length - 1; - while(i != arr.length - 1 && j != 0){ - if(target > arr[i][j]){ - i++; - }else if(target < arr[i][j]){ - j--; - }else{ - return true; - } - } - - return target == arr[i][j]; -} -``` - -值得注意的是每次选取的数都是第一行最后一个数,因此前提是第一行有数,那么就对应着输入检查的`arr[0] == null || arr[0].length == 0`,这点比较容易忽略。 - -> 总结:经过分析其实不难发现,此题是在一维有序数组使用二分查找元素的一个变种,我们应该充分利用数据本身的规律性来寻找解题思路。 - -### 替换空格 - -#### 题目描述 - -请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 - -```java -public String replaceSpace(StringBuffer str) { - -} -``` - - - -> 此题考查的是字符串这个数据结构的数组实现(对应的还有链表实现)的相关操作。 - -#### 解析 - -##### String.replace简单粗暴 - -如果可以使用`API`,那么可以很容易地写出如下代码: - -```java -public String replaceSpace(StringBuffer str) { - //input check - //null pointer - if(str == null){ - return null; - } - //empty str or not exist blank - if(str.length() == 0 || str.indexOf(" ") == -1){ - return str.toString(); - } - - for(int i = 0 ; i < str.length() ; i++){ - if(str.charAt(i) == ' '){ - str.replace(i, i + 1, "%20"); - } - } - - return str.toString(); -} -``` - -##### 时间O(n),空间O(n) - -但是如果面试官告诉我们不许使用封装好的替换函数,那么目的就是在考查我们对字符串**数组实现**方式的相关操作。由于是连续空间存储,因此需要在创建实例时指定大小,由于每个空格都使用`%20`替换,因此替换之后的字符串应该比原串多出`空格数 * 2`个长度,实现如下: - -```java -public String replaceSpace(StringBuffer str) { - //input check - //null pointer - if(str == null){ - return null; - } - //empty str or not exist blank - if(str.length() == 0 || str.indexOf(" ") == -1){ - return str.toString(); - } - - char[] source = str.toString().toCharArray(); - int blankCount = 0; - for(int i = 0 ; i < source.length ; i++){ - blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount; - } - char[] dest = new char[source.length + blankCount * 2]; - for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){ - if(source[i] == ' '){ - dest[j--] = '0'; - dest[j--] = '2'; - dest[j] = '%'; - continue; - }else{ - dest[j] = source[i]; - } - } - - return new String(dest); -} -``` - -##### 时间O(n),空间O(1) - -如果还要求不能有额外空间,那我们就要考虑如何复用输入的字符串,如果我们从前往后遇到空格就将空格及其之后的两个位置替换为`%20`,势必会覆盖空格之后的两个字符,比如`hello world`会被替换成`hello%20rld`,因此我们需要在长度被扩展后的新串中从后往前确定每个索引上的字符。比如使用一个`originalIndex`指向原串中的最后一个字符索引,使用`newIndex`指向新串的最后一个索引,每次将`originalIndex`上的字符复制到`newIndex`上并且两个指针前移,如果`originalIndex`上的字符是空格,则将`newIndex`依次填充`0,2,%`,然后两者再前移,直到两者都到首索引位置。 - -![image](https://ws1.sinaimg.cn/large/006zweohgy1fzb0mknemyj30ng0df74w.jpg) - -```java -public String replaceSpace(StringBuffer str) { - //input check - //null pointer - if(str == null){ - return null; - } - //empty str or not exist blank - if(str.length() == 0 || str.indexOf(" ") == -1){ - return str.toString(); - } - - int blankCount = 0; - for(int i = 0 ; i < str.length() ; i++){ - blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount; - } - int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2; - str.setLength(newIndex + 1); //需要重新设置一下字符串的长度,否则会报越界错误 - while(originalIndex >= 0 && newIndex >= 0){ - if(str.charAt(originalIndex) == ' '){ - str.setCharAt(newIndex--, '0'); - str.setCharAt(newIndex--, '2'); - str.setCharAt(newIndex, '%'); - }else{ - str.setCharAt(newIndex, str.charAt(originalIndex)); - } - originalIndex--; - newIndex--; - } - - return str.toString(); -} -``` - -> 总结:要把思维打开,对于数组的操作我们习惯性的以`for(int i = 0 ; i < arr.length ; i++)`的形式从头到尾来操作数组,但是不要忽略了从尾到头遍历也有它的独到之处。 - -### 反转链表 - -#### 题目描述 - -输入一个链表,反转链表后,输出新链表的表头。 - -```java -public ListNode ReverseList(ListNode head) { - -} -``` - - - -#### 解析 - -此题的难点在于无法通过一个单链表结点获取其前驱结点,因此我们不仅要在反转指针之前保存当前结点的前驱结点,还要保存当前结点的后继结点,并在下一次反转之前更新这两个指针。 - -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -}*/ -public ListNode ReverseList(ListNode head) { - if(head == null || head.next == null){ - return head; - } - ListNode pre = null, p = head, next; - while(p != null){ - next = p.next; - p.next = pre; - pre = p; - p = next; - } - - return pre; -} -``` - - - -### 从尾到头打印链表 - -#### 题目描述 - -输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 - -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - -} -``` - - - -#### 解析 - -此题的难点在于单链表只有指向后继结点的指针,因此我们无法通过当前结点获取前驱结点,因此不要妄想先遍历一遍链表找到尾结点然后再依次从后往前打印。 - -##### 递归,简洁优雅 - -由于我们通常是从头到尾遍历链表的,而题目要求从尾到头打印结点,这与前进后出的逻辑是相符的,因此你可以使用一个栈来保存遍历时走过的结点,再通过后进先出的特性实现从尾到头打印结点,但是我们也可以利用递归来帮我们压栈,由于递归简洁不易出错,因此面试中能用递归尽量用递归:只要当前结点不为空,就递归遍历后继结点,当后继结点为空时,递归结束,在递归回溯时将“当前结点”依次添加到集合中 - -```java -/** -* public class ListNode { -* int val; -* ListNode next = null; -* -* ListNode(int val) { -* this.val = val; -* } -* } -* -*/ -import java.util.ArrayList; -public class Solution { - public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList res = new ArrayList(); - //input check - if(listNode == null){ - return res; - } - recursively(res, listNode); - return res; - } - - public void recursively(ArrayList res, ListNode node){ - //base case - if(node == null){ - return; - } - //node not null - recursively(res, node.next); - res.add(node.val); - return; - } -} -``` - -##### 反转链表 - -还有一种方法就是将链表指针都反转,这样将反转后的链表从头到尾打印就是结果了。需要注意的是我们不应该在访问用户数据时更改存储数据的结构,因此最后要记得反转回来: - -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList res = new ArrayList(); - //input check - if(listNode == null){ - return res; - } - return unrecursively(listNode); -} - -public ArrayList unrecursively(ListNode node){ - ArrayList res = new ArrayList(); - ListNode newHead = reverse(node); - ListNode p = newHead; - while(p != null){ - res.add(p.val); - p = p.next; - } - reverse(newHead); - return res; -} - -public ListNode reverse(ListNode node){ - ListNode pre = null, cur = node, next; - while(cur != null){ - //save predecessor - next = cur.next; - //reverse pointer - cur.next = pre; - //move to next - pre = cur; - cur = next; - } - //cur is null - return pre; -} -``` - -> 总结:面试时能用递归就用递归,当然了如果面试官就是要考查你的指针功底那你也能`just so so`不是 - -### 重建二叉树 - -#### 题目描述 - -输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,2,7,1,5,3,8,6},则重建二叉树并返回。 - -```java -public TreeNode reConstructBinaryTree(int [] pre,int [] in) { - -} -``` - - - -#### 解析 - -先序序列的特点是第一个数就是根结点而后是左子树的先序序列和右子树的先序序列,而中序序列的特点是先是左子树的中序序列,然后是根结点,最后是右子树的中序序列。因此我们可以通过先序序列得到根结点,然后通过在中序序列中查找根结点的索引从而得到左子树和右子树的结点数。然后可以将两序列都一分为三,对于其中的根结点能够直接重建,然后根据对应子序列分别递归重建根结点的左子树和右子树。这是一个典型的将复杂问题划分成子问题分步解决的过程。 - -![image](https://ws2.sinaimg.cn/large/006zweohgy1fzb43dddiej30f70azjrt.jpg) - -递归体的定义,如上图先序序列的左子树序列是`2,3,4`对应下标`1,2,3`,而中序序列的左子树序列是`3,2,4`对应下标`0,1,2`,因此递归体接收的参数除了保存两个序列的数组之外,还需要指明需要递归重建的子序列分别在两个数组中的索引范围:`TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)`。然后递归体根据`pre`的`i~j`索引范围形成的先序序列和`in`的`m~n`索引范围形成的中序序列重建一棵树并返回根结点。 - -首先根结点就是先序序列的第一个数,即`pre[i]`,因此`TreeNode root = new TreeNode(pre[i])`可以直接确定,然后通过在`in`的`m~n`中查找出`pre[i]`的索引`index`可以求得左子树结点数`leftNodes = index - m`,右子树结点数`rightNodes = n - index`,如果左(右)子树结点数为0则表明左(右)子树为`null`,否则通过`root.left = rebuild(pre, i' ,j' ,in ,m' ,n')`来重建左(右)子树即可。 - -这个题的难点也就在这里,即`i',j',m',n'`的值的确定,笔者曾在此困惑许久,建议通过`leftNodes,rightNodes`和`i,j,m,n`来确定:(这个时候了前往不要在脑子里面想这些下标对应关系!!一定要在纸上画,确保准确性和概括性) - -![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbmo2052rj309v088dfy.jpg) - -于是容易得出如下代码: - -```java -if(leftNodes == 0){ - root.left = null; -}else{ - root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); -} -if(rightNodes == 0){ - root.right = null; -}else{ - root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); -} -``` - -笔者曾以中序序列的根节点索引来确定`i',j',m',n'`的对应关系写出如下**错误代码**: - -![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbmvcv9yej306b07adfv.jpg) - -```java -if(leftNodes == 0){ - root.left = null; -}else{ - root.left = rebuild(pre, i + 1, index, in, m, index - 1); -} -if(rightNodes == 0){ - root.right = null; -}else{ - root.right = rebuild(pre, index + 1, j, in, index + 1, n); -} -``` - -这种对应关系乍一看没错,但是不具有概括性(即囊括所有情况),比如对序列`2,3,4`、`3,2,4`重建时: - -![image](https://wx1.sinaimg.cn/large/006zweohgy1fzbn2rz5n5j30cv07v3yh.jpg) - -你看这种情况,上述错误代码还适用吗?原因就在于`index`是在`in`的`m~n`中选取的,与数组`in`是绑定的,和`pre`没有直接的关系,因此如果用`index`来表示`i',j'`自然是不合理的。 - -此题的正确完整代码如下: - -```java -/** - * Definition for binary tree - * public class TreeNode { - * int val; - * TreeNode left; - * TreeNode right; - * TreeNode(int x) { val = x; } - * } - */ -public class Solution { - public TreeNode reConstructBinaryTree(int [] pre,int [] in) { - if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){ - return null; - } - return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1); - } - - public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){ - int rootVal = pre[i], index = findIndex(rootVal, in, m, n); - if(index < 0){ - return null; - } - int leftNodes = index - m, rightNodes = n - index; - TreeNode root = new TreeNode(rootVal); - if(leftNodes == 0){ - root.left = null; - }else{ - root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1); - } - if(rightNodes == 0){ - root.right = null; - }else{ - root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n); - } - return root; - } - - public int findIndex(int target, int arr[], int from, int to){ - for(int i = from ; i <= to ; i++){ - if(arr[i] == target){ - return i; - } - } - return -1; - } -} -``` - -> 总结: -> -> 1. 对于复杂问题,一定要划分成若干子问题,逐一求解。比如二叉树问题,我们通常将其划分成头结点、左子树、右子树。 -> 2. 对于递归过程的参数对应关系,尽量使用和数据样本本身没有直接关系的变量来表示。比如此题应该选取`leftNodes`和`rightNodes`来计算`i',j',m',n'`而不应该使用头结点在中序序列的下标`index`(它和`in`是绑定的,那么可能对`pre`就不适用了)。 - -### 用两个栈实现队列 - -#### 题目描述 - -用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 - -```java -Stack stack1 = new Stack(); -Stack stack2 = new Stack(); - -public void push(int node) { - -} - -public int pop() { - -} -``` - - - -#### 解析 - -这道题只要记住以下几点即可: - -1. 一个栈(如`stack1`)只能用来存,另一个栈(如`stack2`)只能用来取 -2. 当取元素时首先检查`stack2`是否为空,如果不空直接`stack2.pop()`,否则将`stack1`中的元素**全部倒入**`stack2`,如果倒入之后`stack2`仍为空则需要抛异常,否则`stack2.pop()`。 - -代码示例如下: - -```java -import java.util.Stack; - -public class Solution { - Stack stack1 = new Stack(); - Stack stack2 = new Stack(); - - public void push(int node) { - stack1.push(node); - } - - public int pop() { - if(stack2.empty()){ - while(!stack1.empty()){ - stack2.push(stack1.pop()); - } - } - if(stack2.empty()){ - throw new IllegalStateException("no more element!"); - } - return stack2.pop(); - } -} -``` - -> 总结:只要取元素的栈不为空,取元素时直接弹出其栈顶元素即可,只有当其为空时才考虑将存元素的栈倒入进来,并且要一次性倒完。 - -### 旋转数组的最小数字 - -#### 题目描述 - -把一个数组最开始的**若干**个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个**非减排序**的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 - -```java -public int minNumberInRotateArray(int [] arr) { - -} -``` - - - -#### 解析 - -此题需先认真审题: - -1. 若干,涵盖了一个元素都不搬的情况,此时数组是一个非减排序序列,因此首元素就是数组的最小元素。 -2. 非减排序,并不代表是递增的,可能会出现若干相邻元素相同的情况,极端的例子是整个数组的所有元素都相同 - -由此不难得出如下`input check`: - -```java -public int minNumberInRotateArray(int [] arr) { - //input check - if(arr == null || arr.length == 0){ - return 0; - } - //if only one element or no rotate - if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ - return arr[0]; - } - - //TODO -} -``` - -上述的`arr[0] < arr[arr.length - 1]`不能写成`arr[0] <= arr[arr.length - 1]`,比如可能会有`[1,2,3,3,4] -> [3,4,1,2,3]` 的情况,这时你不能返回`arr[0]=3`。 - -如果走到了程序中的`TODO`,就可以考虑普遍情况下的推敲,数组可以被分成两部分:大于等于`arr[0]`的左半部分和小于等于`arr[arr.length - 1]`右半部分,我们不妨借助两个指针从数组的头、尾向中间靠近,这样就能利用二分的思想快速移动指针从而淘汰一些不在考虑范围之内的数。 - -![image](https://wx2.sinaimg.cn/large/006zweohgy1fzbpp2dx1dj30a0063aa1.jpg) - -如图,我们不能直接通过`arr[mid]`和`arr[l]`(或`arr[r]`)的比较(`arr[mid] >= arr[l]`)来决定移动`l`还是`r`到`mid`上,因为数组可能存在若干相同且相邻的数,因此我们还需要加上一个限制条件:`arr[l + 1] >= arr[l] && arr[mid] >= arr[l]`(对于`r`来说则是`arr[r - 1] <= arr[r] && arr[mid] <= arr[r]`),即当左半部分(右半部分)不止一个数时,我们才可能去移动`l`(`r`)指针。完整代码如下: - -```java -import java.util.ArrayList; -public class Solution { - public int minNumberInRotateArray(int [] arr) { - //input check - if(arr == null || arr.length == 0){ - return 0; - } - //if only one element or no rotate - if(arr.length == 1 || arr[0] < arr[arr.length - 1]){ - return arr[0]; - } - - //has rotate, left part is big than right part - int l = 0, r = arr.length - 1, mid; - //l~r has more than 3 elements - while(r > l && r - l != 1){ - //r-l >= 2 -> mid > l - mid = l + ((r - l) >> 1); - if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){ - l = mid; - }else{ - r = mid; - } - } - - return arr[r]; - } -} -``` - -> 总结:审题时要充分考虑数据样本的极端情况,以写出鲁棒性较强的代码。 - -### 斐波那契数列 - -#### 题目描述 - -大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39 - -```java -public int Fibonacci(int n) { - -} -``` - - - -#### 解析 - -##### 递归方式 - -对于公式`f(n) = f(n-1) + f(n-2)`,明显就是一个递归调用,因此根据`f(0) = 0`和`f(1) = 1`我们不难写出如下代码: - -```java -public int Fibonacci(int n) { - if(n == 0 || n == 1){ - return n; - } - return Fibonacci(n - 1) + Fibonacci(n - 2); -} -``` - -##### 动态规划 - -在上述递归过程中,你会发现有很多计算过程是重复的: - -![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbq4avws3j30b507b74a.jpg) - -**动态规划就在使用递归调用自上而下分析过程中发现有很多重复计算的子过程,于是采用自下而上的方式将每个子状态缓存下来,这样对于上层而言只有当需要的子过程结果不在缓存中时才会计算一次,因此每个子过程都只会被计算一次**。 - -```java -public int Fibonacci(int n) { - if(n == 0 || n == 1){ - return n; - } - //n1 -> f(n-1), n2 -> f(n-2) - int n1 = 1, n2 = 0; - //从f(2)开始算起 - int N = 2, res = 0; - while(N++ <= n){ - //每次计算后更新缓存,当然你也可以使用一个一维数组保存每次的计算结果,只额外空间复杂度就变为O(n)了 - res = n1 + n2; - n2 = n1; - n1 = res; - } - return res; -} -``` - -上述代码很多人都能写出来,只是没有意识到这就是动态规划。 - -> 总结:当你自上而下分析递归时发现有很多子过程被重复计算,那么就应该考虑能否通过自下而上将每个子过程的计算结果缓存下来。 - -### 跳台阶 - -#### 题目描述 - -一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 - -```java -public int JumpFloor(int target) { - -} -``` - - - -#### 解析 - -##### 递归版本 - -将复杂问题分解:复杂问题就是不断地将`target`减1或减2(对应跳一级和跳两级台阶)直到`target`变为1或2(对应只剩下一层或两层台阶)时我们能够很容易地得出结果。因此对于当前的青蛙而言,它能够选择的就是跳一级或跳二级,剩下的台阶有多少种跳法交给子过程来解决: - -```java -public int JumpFloor(int target) { - //input check - if(target <= 0){ - return 0; - } - //base case - if(target == 1){ - return 1; - } - if(target == 2){ - return 2; - } - return JumpFloor(target - 1) + JumpFloor(target - 2); -} -``` - -你会发现这其实就是一个斐波那契数列,只不过是从`f(1) = 1,f(2) = 2`开始的斐波那契数列罢了。自然你也应该能够写出动态规划版本。 - -#### 进阶问题 - -一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 - -#### 解析 - -##### 递归版本 - -本质上还是分解,只不过上一个是分解成两步,而这个是分解成n步: - -```java -public int JumpFloorII(int target) { - if(target <= 0){ - return 0; - } - //base case,当target=0时表示某个分解分支跳完了所有台阶,这个分支就是一种跳法 - if(target == 0){ - return 1; - } - - //本过程要收集的跳法的总数 - int res = 0; - for(int i = 1 ; i <= target ; i++){ - //本次选择,选择跳i阶台阶,剩下的台阶交给子过程,每个选择就代表一个分解分支 - res += JumpFloorII(target - i); - } - return res; -} -``` - -##### 动态规划 - -这个动态规划就有一点难度了,**首先我们要确定缓存目标**,斐波那契数列中由于`f(n)`只依赖于`f(n-1)`和`f(n-2)`因此我们仅用两个缓存变量实现了动态规划,但是这里`f(n)`依赖的是`f(0),f(1),f(2),...,f(n-1)`,因此我们需要通过长度量级为`n`的表缓存前`n`个状态(`int arr[] = new int[target + 1]`,`arr[target]`表示`f(n)`)。**然后根据递归版本(通常是`base case`)确定哪些状态的值是可以直接确定的**,比如由`if(target == 0){ return 1 }`可知`arr[0] = 1`,从`f(N = 1)`开始的所有状态都需要依赖之前(`f(n < N)`)的所有状态: - -```java -int res = 0; -for(int i = 1 ; i <= target ; i++){ - res += JumpFloorII(target - i); -} -return res -``` - -因此我们可以据此自下而上计算出每个子状态的值: - -```java -public int JumpFloorII(int target) { - if(target <= 0){ - return 0; - } - - int arr[] = new int[target + 1]; - arr[0] = 1; - for(int i = 1 ; i < arr.length ; i++){ - for(int j = 0 ; j < i ; j++){ - arr[i] += arr[j]; - } - } - - return arr[target]; -} -``` - -但这仍不是最优解,因为观察循环体你会发现,每次`f(n)`的计算都要从`f(0)`累加到`f(n-1)`,我们完全可以将这个累加值缓存起来`preSum`,每计算出一次`f(N)`之后都将缓存更新为`preSum += f(N)`。如此得到最优解: - -```java -public int JumpFloorII(int target) { - if(target <= 0){ - return 0; - } - - int arr[] = new int[target + 1]; - arr[0] = 1; - int preSum = arr[0]; - for(int i = 1 ; i < arr.length ; i++){ - arr[i] = preSum; - preSum += arr[i]; - } - - return arr[target]; -} -``` - -### 矩形覆盖 - -#### 题目描述 - -我们可以用`2*1`的小矩形横着或者竖着去覆盖更大的矩形。请问用n个`2*1`的小矩形无重叠地覆盖一个`2*n`的大矩形,总共有多少种方法? - -```java -public int RectCover(int target) { - -} -``` - - - -#### 解析 - -##### 递归版本 - -有了之前的历练,我们能很快的写出递归版本:先竖着放一个或者先横着放两个,剩下的交给递归处理: - -```java -//target 大矩形的边长,也是剩余小矩形的个数 -public int RectCover(int target) { - if(target <= 0){ - return 0; - } - if(target == 1 || target == 2){ - return target; - } - return RectCover(target - 1) + RectCover(target - 2); -} -``` - -##### 动态规划 - -这仍然是个以`f(1)=1,f(2)=2`开头的斐波那契数列: - -```java -//target 大矩形的边长,也是剩余小矩形的个数 -public int RectCover(int target) { - if(target <= 0){ - return 0; - } - if(target == 1 || target == 2){ - return target; - } - //n_1->f(n-1), n_2->f(n-2),从f(N=3)开始算起 - int n_1 = 2, n_2 = 1, N = 3, res = 0; - while(N++ <= target){ - res = n_1 + n_2; - n_2 = n_1; - n_1 = res; - } - - return res; -} -``` - -### 二进制中1的个数 - -#### 题目描述 - -输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 - -```java -public int NumberOf1(int n) { - -} -``` - - - -#### 解析 - -题目已经给我们降低了难度:负数用补码(取反加1)表示表明输入的参数为均为正数,我们只需统计其二进制表示中1的个数、运算时只考虑无符号移位即可。 - -典型的判断某个二进制位上是否为1的方法是将该二进制数右移至该二进制位为最低位然后与1相与`&`,由于1的二进制表示中只有最低位为1其余位均为0,因此相与后的结果与该二进制位上的数相同。据此不难写出如下代码: - -```java -public int NumberOf1(int n) { - int count = 0; - for(int i = 0 ; i < 32 ; i++){ - count += ((n >> i) & 1); - } - return count; -} -``` - -当然了,还有一种比较秀的解法就是利用`n = n & (n - 1)`将`n`的二进制位中为1的最低位置为0(只要`n`不为0就说明含有二进位制为1的位,如此这样的操作能做多少次就说明有多少个二进制位为1的位): - -```java -public int NumberOf1(int n) { - int count = 0; - while(n != 0){ - count++; - n &= (n - 1); - } - return count; -} -``` - -### 数值的整数次方 - -#### 题目描述 - -给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 - -```java -public double Power(double base, int exponent) { - -} -``` - - - -#### 解析 - -这是一道充满危险色彩的题,求职者可能会内心窃喜不假思索的写出如下代码: - -```java -public double Power(double base, int exponent) { - double res = 1; - for(int i = 1 ; i <= exponent ; i++){ - res *= base; - } - return res; -} -``` - -但是你有没有想过底数`base`和幂`exponent`都是可正、可负、可为0的。如果幂为负数,那么底数就不能为0,否则应该抛出算术异常: - -```java -//是否是负数 -boolean minus = false; -//如果存在分母 -if(exponent < 0){ - minus = true; - exponent = -exponent; - if(base == 0){ - throw new ArithmeticException("/ by zero"); - } -} -``` - -如果幂为0,那么根据任何不为0的数的0次方为1,0的0次方未定义,应该有如下判断: - -```java -//如果指数为0 -if(exponent == 0){ - if(base != 0){ - return 1; - }else{ - throw new ArithmeticException("0^0 is undefined"); - } -} -``` - -剩下的就是计算乘方结果,但是不要忘了如果幂为负需要将结果取倒数: - -```java -//指数不为0且分母也不为0,正常计算并返回整数或分数 -double res = 1; -for(int i = 1 ; i <= exponent ; i++){ - res *= base; -} - -if(minus){ - return 1/res; -}else{ - return res; -} -``` - -也许你还可以锦上添花为幂乘方的计算引入二分计算(当幂为偶数时`2^n = 2^(n/2) * 2^(n/2)`): - -```java -public double binaryPower(double base, int exp){ - if(exp == 1){ - return base; - } - double res = 1; - res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2)); - return exp % 2 == 0 ? res : res * base; -} -``` - -### 调整数组顺序使奇数位于偶数前面 - -#### 题目描述 - -输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的**相对位置不变**。 - -```java -public void reOrderArray(int [] arr) { - -} -``` - - - -#### 解析 - -读题之后发现这个跟快排的`partition`思路很像,都是选取一个比较基准将数组分成两部分,当然你也可以以`arr[i] % 2 == 0`为基准将奇数放前半部分,将偶数放有半部分,但是虽然只需`O(n)`的时间复杂度但不能保证调整后奇数之间、偶数之间的相对位置: - -```java -public void reOrderArray(int [] arr) { - if(arr == null || arr.length == 0){ - return; - } - - int odd = -1; - for(int i = 0 ; i < arr.length ; i++){ - if(arr[i] % 2 == 1){ - swap(arr, ++odd, i); - } - } -} - -public void swap(int[] arr, int i, int j){ - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; -} -``` - -涉及到排序稳定性,我们自然能够想到插入排序,从数组的第二个元素开始向后依次确定每个元素应处的位置,确定的逻辑是:将该数与前一个数比较,如果比前一个数小则与前一个数交换位置并在交换位置后继续与前一个数比较直到前一个数小于等于该数或者已达数组首部停止。 - -此题不过是将比较的逻辑由数值的大小改为:当前的数是否是奇数并且前一个数是偶数,是则递归向前交换位置。代码示例如下: - -```java -public void reOrderArray(int [] arr) { - if(arr == null || arr.length == 0){ - return; - } - - int odd = -1; - for(int i = 1 ; i < arr.length ; i++){ - for(int j = i ; j >= 1 ; j--){ - if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){ - swap(arr, j, j - 1); - } - } - } -} -``` - -### 链表中倒数第K个结点 - -#### 题目描述 - -输入一个链表,输出该链表中倒数第k个结点。 - -```java -public ListNode FindKthToTail(ListNode head,int k) { - -} -``` - - - -#### 解析 - -**倒数**,这又是一个从尾到头的遍历逻辑,而链表对从尾到头遍历是敏感的,前面我们有通过压栈/递归、反转链表的方式实现这个遍历逻辑,自然对于此题同样适用,但是那样未免太麻烦了,我们可以通过两个间距为(k-1)个结点的链表指针来达到此目的。 - -```java -public ListNode FindKthToTail(ListNode head,int k) { - //input check - if(head == null || k <= 0){ - return null; - } - ListNode tmp = new ListNode(0); - tmp.next = head; - ListNode p1 = tmp, p2 = tmp; - while(k > 0 && p1.next != null){ - p1 = p1.next; - k--; - } - //length < k - if(k != 0){ - return null; - } - while(p1 != null){ - p1 = p1.next; - p2 = p2.next; - } - - tmp = null; //help gc - - return p2; -} -``` - -这里使用了一个技巧,就是创建一个临时结点`tmp`作为两个指针的初始指向,以模拟`p1`先走`k`步之后,`p2`才开始走,没走时停留在初始位置的逻辑,有利于帮我们梳理指针在对应位置上的意义,这样当`p1`走到头时(`p1=null`),`p2`就是倒数第`k`个结点。 - -这里还有一个坑就是,笔者层试图为了简化代码将上述的`9 ~ 12`行写成如下偷懒模式而导致排错许久: - -```java -while(k-- > 0 && p1.next != null){ - p1 = p1.next; -} -``` - -原因是将`k--`写在`while()`中,无论判断是否通过都会执行`k = k - 1`,因此代码总是会在`if(k != 0)`处返回`null`,希望读者不要和笔者一样粗心。 - -> 总结:当遇到复杂的指针操作时,我们不妨试图多引入几个指针或者临时结点,以方便梳理我们的思路,加强代码的逻辑化,这些空间复杂度`O(1)`的操作通常也不会影响性能。 - -### 合并两个排序的链表 - -#### 题目描述 - -输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 - -```java -public ListNode Merge(ListNode list1,ListNode list2) { - -} -``` - - - -#### 解析 - -![image](https://ws3.sinaimg.cn/large/006zweohgy1fzbx9j54uuj30jg0ak3yz.jpg) - -```java -public ListNode Merge(ListNode list1,ListNode list2) { - if(list1 == null || list2 == null){ - return list1 == null ? list2 : list1; - } - ListNode newHead = list1.val < list2.val ? list1 : list2; - ListNode p1 = (newHead == list1) ? list1.next : list1; - ListNode p2 = (newHead == list2) ? list2.next : list2; - ListNode p = newHead; - while(p1 != null && p2 != null){ - if(p1.val <= p2.val){ - p.next = p1; - p1 = p1.next; - }else{ - p.next = p2; - p2 = p2.next; - } - p = p.next; - } - - while(p1 != null){ - p.next = p1; - p = p.next; - p1 = p1.next; - } - while(p2 != null){ - p.next = p2; - p = p.next; - p2 = p2.next; - } - - return newHead; -} -``` - -### 树的子结构 - -#### 题目描述 - -输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) - -```java -/** -public class TreeNode { - int val = 0; - TreeNode left = null; - TreeNode right = null; - - public TreeNode(int val) { - this.val = val; - - } - -}*/ -public boolean HasSubtree(TreeNode root1,TreeNode root2) { - if(root1 == null || root2 == null){ - return false; - } - - return process(root1, root2); -} -``` - - - -#### 解析 - -这是一道典型的分解求解的复杂问题。典型的二叉树分解:遍历头结点、遍历左子树、遍历右子树。首先按照`root1`和`root2`的值是否相等划分为两种情况: - -1. 两个头结点的值相等,并且`root2.left`也是`roo1.left`的子结构(递归)、`root2.right`也是`root1.right`的子结构(递归),那么可返回`true`。 -2. 否则,要看只有当`root2`为`root1.left`的子结构或者`root2`为`root1.right`的子结构时,才能返回`true` - -据上述两点很容易得出如下递归逻辑: - -```java -if(root1.val == root2.val){ - if(process(root1.left, root2.left) && process(root1.right, root2.right)){ - return true; - } -} - -return process(root1.left, root2) || process(root1.right, root2); -``` - -接下来确定递归的终止条件,如果某个子过程`root2=null`那么说明在自上而下的比较过程中`root2`的结点已被罗列比较完了,这时无论`root1`是否为`null`,该子过程都应该返回`true`: - -![image](https://ws4.sinaimg.cn/large/006zweohgy1fzbyis1e3oj30dg04qaa5.jpg) - -```java -if(root2 == null){ - return true; -} -``` - -但是如果`root2 != null`而`root1 = null`,则应返回`false` - -![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbym9fv0bj30bv05974e.jpg) - -```java -if(root1 == null && root2 != null){ - return false; -} -``` - -完整代码如下: - -```java -public class Solution { - public boolean HasSubtree(TreeNode root1,TreeNode root2) { - if(root1 == null || root2 == null){ - return false; - } - - return process(root1, root2); - } - - public boolean process(TreeNode root1, TreeNode root2){ - if(root2 == null){ - return true; - } - if(root1 == null && root2 != null){ - return false; - } - - if(root1.val == root2.val){ - if(process(root1.left, root2.left) && process(root1.right, root2.right)){ - return true; - } - } - - return process(root1.left, root2) || process(root1.right, root2); - } -} -``` - -### 二叉树的镜像 - -#### 题目描述 - -操作给定的二叉树,将其变换为源二叉树的镜像。 - -![image](https://ws1.sinaimg.cn/large/006zweohgy1fzbyup8oq3j306b08kjra.jpg) - -```java -public void Mirror(TreeNode root) { - -} -``` - - - -#### 解析 - -由图可知获取二叉树的镜像就是将原树的每个结点的左右孩子交换一下位置(这个规律一定要会找),也就是说我们只需遍历每个结点并交换`left,right`的引用指向就可以了,而我们有成熟的先序遍历: - -```java -public void Mirror(TreeNode root) { - if(root == null){ - return; - } - - TreeNode tmp = root.left; - root.left = root.right; - root.right = tmp; - Mirror(root.left); - Mirror(root.right); -} -``` - -### 顺时针打印矩阵 - -#### 题目描述 - -输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. - -```java -public ArrayList printMatrix(int [][] matrix) { - -} -``` - - - -#### 解析 - -![image](https://wx3.sinaimg.cn/large/006zweohgy1fzbzo7qyu0j30gr093q3a.jpg) - -只要分析清楚了打印思路(左上角和右下角即可确定一条打印轨迹)后,此题主要考查条件控制的把握。只要给我一个左上角的点`(i,j)`和右下角的点`(m,n)`,就可以将这一圈的打印分解为四步: - -![image](https://ws2.sinaimg.cn/large/006zweohgy1fzc01b7bpij309107hweh.jpg) - -但是如果左上角和右下角的点在一行或一列上那就没必要分解,直接打印改行或该列即可,打印的逻辑如下: - -```java -public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList res){ - if(i == m && j == n){ - res.add(matrix[i][j]); - return; - } - - if(i == m || j == n){ - //only one while will be execute - while(i < m){ - res.add(matrix[i++][j]); - } - while(j < n){ - res.add(matrix[i][j++]); - } - res.add(matrix[m][n]); - return; - } - - int p = i, q = j; - while(q < n){ - res.add(matrix[p][q++]); - } - //q == n - while(p < m){ - res.add(matrix[p++][q]); - } - //p == m - while(q > j){ - res.add(matrix[p][q--]); - } - //q == j - while(p > i){ - res.add(matrix[p--][q]); - } - //p == i -} -``` - -接着我们将每个圈的左上角和右下角传入该函数即可: - -```java -public ArrayList printMatrix(int [][] matrix) { - ArrayList res = new ArrayList(); - if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){ - return res; - } - int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1; - while(i <= m && j <= n){ - printEdge(matrix, i++, j++, m--, n--, res); - } - return res; -} -``` - -### 包含min函数的栈 - -#### 题目描述 - -定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。 - -```java -public class Solution { - - - public void push(int node) { - - } - - public void pop() { - - } - - public int top() { - - } - - public int min() { - - } -} -``` - - - -#### 解析 - -最直接的思路是使用一个变量保存栈中现有元素的最小值,但这只对只存不取的栈有效,当弹出的值不是最小值时还没什么影响,但当弹出最小值后我们就无法获取当前栈中的最小值。解决思路是使用一个最小值栈,栈顶总是保存当前栈中的最小值,每次数据栈存入数据时最小值栈就要相应的将存入后的最小值压入栈顶: - -```java -private Stack dataStack = new Stack(); -private Stack minStack = new Stack(); - -public void push(int node) { - dataStack.push(node); - if(!minStack.empty() && minStack.peek() < node){ - minStack.push(minStack.peek()); - }else{ - minStack.push(node); - } -} - -public void pop() { - if(!dataStack.empty()){ - dataStack.pop(); - minStack.pop(); - } -} - -public int top() { - if(!dataStack.empty()){ - return dataStack.peek(); - } - throw new IllegalStateException("stack is empty"); -} - -public int min() { - if(!dataStack.empty()){ - return minStack.peek(); - } - throw new IllegalStateException("stack is empty"); -} -``` - -### 栈的压入、弹出序列 - -#### 题目描述 - -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的**所有数字均不相等**。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的**长度是相等**的) - -```java -public boolean IsPopOrder(int [] arr1,int [] arr2) { - -} -``` - - - -#### 解析 - -可以使用两个指针`i,j`,初始时`i`指向压入序列的第一个,`j`指向弹出序列的第一个,试图将压入序列按照顺序压入栈中: - -1. 如果`arr1[i] != arr2[j]`,那么将`arr1[i]`压入栈中并后移`i`(表示`arr1[i]`还没到该它弹出的时刻) -2. 如果某次后移`i`之后发现`arr1[i] == arr2[j]`,那么说明此刻的`arr1[i]`被压入后应该被立即弹出才会产生给定的弹出序列,于是不压入`arr1[i]`(表示压入并弹出了)并后移`i`,`j`也要后移(表示弹出序列的`arr2[j]`记录已产生,接着产生或许的弹出记录即可)。 -3. 因为步骤2和3都会后移`i`,因此循环的终止条件是`i`到达`arr1.length`,此时若栈中还有元素,那么从栈顶到栈底形成的序列必须与`arr2`中`j`之后的序列相同才能返回`true`。 - -```java -public boolean IsPopOrder(int [] arr1,int [] arr2) { - //input check - if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){ - return false; - } - Stack stack = new Stack(); - int length = arr1.length; - int i = 0, j = 0; - while(i < length && j < length){ - if(arr1[i] != arr2[j]){ - stack.push(arr1[i++]); - }else{ - i++; - j++; - } - } - - while(j < length){ - if(arr2[j] != stack.peek()){ - return false; - }else{ - stack.pop(); - j++; - } - } - - return stack.empty() && j == length; -} -``` - -### 从上往下打印二叉树 - -#### 题目描述 - -从上往下打印出二叉树的每个节点,同层节点从左至右打印。 - -```java -public ArrayList PrintFromTopToBottom(TreeNode root) { - -} -``` - - - -#### 解析 - -使用一个队列来保存当前遍历结点的孩子结点,首先将根节点加入队列中,然后进行队列非空循环: - -1. 从队列头取出一个结点,将该结点的值打印 -2. 如果取出的结点左孩子不空,则将其左孩子放入队列尾部 -3. 如果取出的结点右孩子不空,则将其右孩子放入队列尾部 - -```java -public ArrayList PrintFromTopToBottom(TreeNode root) { - ArrayList res = new ArrayList(); - if(root == null){ - return res; - } - LinkedList queue = new LinkedList(); - queue.addLast(root); - while(queue.size() > 0){ - TreeNode node = queue.pollFirst(); - res.add(node.val); - if(node.left != null){ - queue.addLast(node.left); - } - if(node.right != null){ - queue.addLast(node.right); - } - } - - return res; -} -``` - -### 二叉搜索树的后序遍历序列 - -#### 题目描述 - -输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 - -```java -public boolean VerifySquenceOfBST(int [] sequence) { - -} -``` - - - -#### 解析 - -对于二叉树的后序序列,我们能够确定最后一个数就是根结点,还能确定的是前一半部分是左子树的后序序列,后一部分是右子树的后序序列。 - -遇到这种复杂问题,我们仍能采用三步走战略(根结点、左子树、右子树): - -1. 如果当前根结点的左子树**是BST**且其右子树也是BST,那么才可能是BST -2. 在1的条件下,如果左子树的**最大值**小于根结点且右子树的**最小值**大于根结点,那么这棵树就是BST - -据此我们需要定义一个递归体,该递归体需要收集的信息如下:下层需要向我返回其最大值、最小值、以及是否是BST - -```java -class Info{ - boolean isBST; - int max; - int min; - Info(boolean isBST, int max, int min){ - this.isBST = isBST; - this.max = max; - this.min = min; - } -} -``` - -递归体的定义如下: - -```java -public Info process(int[] arr, int start, int end){ - if(start < 0 || end > arr.length - 1 || start > end){ - throw new IllegalArgumentException("invalid input"); - } - //base case : only one node - if(start == end){ - return new Info(true, arr[end], arr[end]); - } - - int root = arr[end]; - Info left, right; - //not exist left child - if(arr[start] > root){ - right = process(arr, start, end - 1); - return new Info(root < right.min && right.isBST, - Math.max(root, right.max), Math.min(root, right.min)); - } - //not exist right child - if(arr[end - 1] < root){ - left = process(arr, start, end - 1); - return new Info(root > left.max && left.isBST, - Math.max(root, left.max), Math.min(root, left.min)); - } - - int l = 0, r = end - 1; - while(r > l && r - l != 1){ - int mid = l + ((r - l) >> 1); - if(arr[mid] > root){ - r = mid; - }else{ - l = mid; - } - } - left = process(arr, start, l); - right = process(arr, r, end - 1); - return new Info(left.isBST && right.isBST && root > left.max && root < right.min, - right.max, left.min); -} -``` - -> 总结:二叉树相关的信息收集问题分步走: -> -> 1. 分析当前状态需要收集的信息 -> 2. 根据下层传来的信息加工出当前状态的信息 -> 3. 确定递归终止条件 - -### 二叉树中和为某一值的路径 - -#### 题目描述 - -输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下**一直到叶结点**所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前) - -```java -public ArrayList> FindPath(TreeNode root,int target) { - -} -``` - - - -#### 解析 - -审题可知,我们需要有一个自上而下从根结点到每个叶子结点的遍历思路,而先序遍历刚好可以拿来用,我们只需在来到当前结点时将当前结点值加入到栈中,在离开当前结点时再将栈中保存的当前结点的值弹出即可使用栈模拟保存自上而下经过的结点,从而实现在来到每个叶子结点时只需判断栈中数值之和是否为`target`即可。 - -```java -public ArrayList> FindPath(TreeNode root,int target) { - ArrayList> res = new ArrayList(); - if(root == null){ - return res; - } - Stack stack = new Stack(); - preOrder(root, stack, 0, target, res); - return res; -} - -public void preOrder(TreeNode root, Stack stack, int sum, int target, - ArrayList> res){ - if(root == null){ - return; - } - - stack.push(root.val); - sum += root.val; - //leaf node - if(root.left == null && root.right == null && sum == target){ - ArrayList one = new ArrayList(); - one.addAll(stack); - res.add(one); - } - - preOrder(root.left, stack, sum, target, res); - preOrder(root.right, stack, sum, target, res); - - sum -= stack.pop(); -} -``` - -### 复杂链表的复制 - -#### 题目描述 - -输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) - -```java -/* -public class RandomListNode { - int label; - RandomListNode next = null; - RandomListNode random = null; - - RandomListNode(int label) { - this.label = label; - } -} -*/ -public class Solution { - public RandomListNode Clone(RandomListNode pHead) - { - - } -} -``` - -#### 解析 - -此题主要的难点在于`random`指针的处理。 - -##### 方法一:使用哈希表,额外空间O(n) - -可以将链表中的结点都复制一份,用一个哈希表来保存,`key`是源结点,`value`就是副本结点,然后遍历`key`取出每个对应的`value`将副本结点的`next`指针和`random`指针设置好: - -```java -public RandomListNode Clone(RandomListNode pHead){ - if(pHead == null){ - return null; - } - HashMap map = new HashMap(); - RandomListNode p = pHead; - //copy - while(p != null){ - RandomListNode cp = new RandomListNode(p.label); - map.put(p, cp); - p = p.next; - } - //link - p = pHead; - while(p != null){ - RandomListNode cp = map.get(p); - cp.next = (p.next == null) ? null : map.get(p.next); - cp.random = (p.random == null) ? null : map.get(p.random); - p = p.next; - } - - return map.get(pHead); -} -``` - -##### 方法二:追加结点,额外空间O(1) - -首先将每个结点复制一份并插入到对应结点之后,然后遍历链表将副本结点的`random`指针设置好,最后将源结点和副本结点分离成两个链表 - -```java -public RandomListNode Clone(RandomListNode pHead){ - if(pHead == null){ - return null; - } - - RandomListNode p = pHead; - while(p != null){ - RandomListNode cp = new RandomListNode(p.label); - cp.next = p.next; - p.next = cp; - p = p.next.next; - } - - //more than two node - //link random pointer - p = pHead; - RandomListNode cp; - while(p != null){ - cp = p.next; - cp.random = (p.random == null) ? null : p.random.next; - p = p.next.next; - } - - //split source and copy - p = pHead; - RandomListNode newHead = p.next; - //p != null -> p.next != null - while(p != null){ - cp = p.next; - p.next = p.next.next; - p = p.next; - cp.next = (p == null) ? null : p.next; - } - - return newHead; -} -``` - -### 二叉搜索树与双向链表 - -#### 题目描述 - -输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 - -```java -public TreeNode Convert(TreeNode root) { -} -``` - -#### 解析 - -典型的二叉树分解问题,我们可以定义一个黑盒`transform`,它的目的是将二叉树转换成双向链表,那么对于一个当前结点`root`,首先将其前驱结点(BST中前驱结点指中序序列的前一个数值,也就是当前结点的左子树上最右的结点,如果左子树为空则没有前驱结点)和后继结点(当前结点的右子树上的最左结点,如果右子树为空则没有后继结点),然后使用黑盒`transform`将左子树和右子树转换成双向链表,最后将当前结点和左子树形成的链表链起来(通过之前保存的前驱结点)和右子树形成的链表链起来(通过之前保存的后继结点),整棵树的转换完毕。 - -```java -public TreeNode Convert(TreeNode root) { - if(root == null){ - return null; - } - - //head is the most left node - TreeNode head = root; - while(head.left != null){ - head = head.left; - } - transform(root); - return head; -} - -//transform a tree to a double-link list -public void transform(TreeNode root){ - if(root == null){ - return; - } - TreeNode pre = root.left, next = root.right; - while(pre != null && pre.right != null){ - pre = pre.right; - } - while(next != null && next.left != null){ - next = next.left; - } - - transform(root.left); - transform(root.right); - //asume the left and right has transformed and what's remaining is link the root - root.left = pre; - if(pre != null){ - pre.right = root; - } - root.right = next; - if(next != null){ - next.left = root; - } -} -``` - -### 字符串全排列 - -#### 题目描述 - -输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 - -#### 解析 - -定义一个递归体`generate(char[] arr, int index, TreeSet res)`,其中`char[] arr`和`index`组合表示上层状态给当前状态传递的信息,即`arr`中`0 ~ index-1`是已生成好的串,现在你(当前状态)要确定`index`位置上应该放什么字符(你可以从`index ~ arr.length - 1`上任选一个字符),然后将`index + 1`应该放什么字符递归交给子过程处理,当某个状态要确定`arr.length`上应该放什么字符时说明`0 ~ arr.length-1`位置上的字符已经生成好了,因此递归终止,将生成好的字符串记录下来(这里由于要求不能重复且按字典序排列,因此我们可以使用JDK中红黑树的实现`TreeSet`来做容器) - -```java -public ArrayList Permutation(String str) { - ArrayList res = new ArrayList(); - if(str == null || str.length() == 0){ - return res; - } - TreeSet set = new TreeSet(); - generate(str.toCharArray(), 0, set); - res.addAll(set); - return res; -} - -public void generate(char[] arr, int index, TreeSet res){ - if(index == arr.length){ - res.add(new String(arr)); - } - for(int i = index ; i < arr.length ; i++){ - swap(arr, index, i); - generate(arr, index + 1, res); - swap(arr, index, i); - } -} - -public void swap(char[] arr, int i, int j){ - if(arr == null || arr.length == 0 || i < 0 || j > arr.length - 1){ - return; - } - char tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; -} -``` - -> 注意:上述代码的第`19`行有个坑,笔者曾因忘记写第19行而排错许久,由于你任选一个`index ~ arr.length - 1`位置上的字符与`index`位置上的交换并将交换生成的结果交给了子过程(第`17,18`行),但你不应该影响后续选取其他字符放到`index`位置上而形成的结果,因此需要再交换回来(第`19`行) - -### 数组中出现次数超过一半的数 - -#### 题目描述 - -数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。 - -```java -public int MoreThanHalfNum_Solution(int [] arr) { -} -``` - -#### 解析 - -##### 方法一:基于partition查找数组中第k大的数 - -如果我们将数组排序,最快也要`O(nlogn)`,排序后的中位数自然就是出现次数超过长度一半的数。 - -我们知道快排的`partition`操作能够将数组按照一个基准划分成小于部分和大于等于部分并返回这个基准在数组中的下标,虽然一次`partition`并不能使数组整体有序,但是能够返回随机选择的数在`partition`之后的下标`index`,这个下标标识了它是第`index`大的数,这也意味着我们要求数组中第`k`大的数不一定要求数组整体有序。 - -于是我们在首次对整个数组`partition`之后将返回的`index`与`n/2`进行比较,并调整下一次`partition`的范围直到`index = n/2`为止我们就找到了。 - -这个时间复杂度需要使用`Master`公式计算(计算过程参见 http://www.zhenganwen.top/62859a9a.html#Master%E5%85%AC%E5%BC%8F),**使用`partition`查找数组中第k大的数时间复杂度为`O(n)`**,最后不要忘了验证一下`index = n/2`上的数出现的次数是否超过了长度的一半。 - -```java -public int MoreThanHalfNum_Solution(int [] arr) { - if(arr == null || arr.length == 0){ - return 0; - } - if(arr.length == 1){ - return arr[0]; - } - - int index = partition(arr, 0, arr.length - 1); - int half = arr.length >> 1;// 0 <= half <= arr.length - 1 - while(index != half){ - index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); - } - - int count = 0; - for(int i = 0 ; i < arr.length ; i++){ - count = (arr[i] == arr[index]) ? ++count : count; - } - - return (count > arr.length / 2) ? arr[index] : 0; -} - -public int partition(int[] arr, int start, int end){ - if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ - throw new IllegalArgumentException(); - } - if(start == end){ - return end; - } - int random = start + (int)(Math.random() * (end - start + 1)); - swap(arr, random, end); - int small = start - 1; - for(int i = start ; i < end ; i++){ - if(arr[i] < arr[end]){ - swap(arr, ++small, i); - } - } - - swap(arr, ++small, end); - - return small; -} - -public void swap(int[] arr, int i, int j){ - int t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; -} -``` - -##### 方法二 - -1. 使用一个`target`记录一个数,并使用`count`记录它出现的次数 -2. 初始时`target = arr[0]`,`count = 1`,表示`arr[0]`出现了1次 -3. 从第二个元素开始遍历数组,如果遇到的数不等于`target`就将`count`减1,否则加1 -4. 如果遍历到某个数时,`count`为0了,那么就将`target`设置为该数,并将`count`置1,继续向后遍历 - -如果存在出现次数超过一半的数,那么必定是`target`最后一次被设置时的数。 - -```java -public int MoreThanHalfNum_Solution(int [] arr) { - if(arr == null || arr.length == 0){ - return 0; - } - //此题需要抓住出现次数超过数组长度的一半这个点来想 - //使用一个计数器,如果这个数出现一次就自增,否则自减,如果自减为0则更新被记录的数 - //如果存在出现次数大于一半的数,那么最后一次被记录的数就是所求之数 - int target = arr[0], count = 1; - for(int i = 1 ; i < arr.length ; i++){ - if(count == 0){ - target = arr[i]; - count = 1; - }else{ - count = (arr[i] == target) ? ++count : --count; - } - } - - if(count == 0){ - return 0; - } - - //不要忘了验证!!! - count = 0; - for(int i = 0 ; i < arr.length ; i++){ - count = (arr[i] == target) ? ++count : count; - } - - return (count > arr.length / 2) ? target : 0; -} -``` - -### 最小的k个数 - -#### 题目描述 - -输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 - -```java -public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { - -} -``` - -#### 解析 - -与上一题的求数组第k大的数如出一辙,如果某次`partition`之后你得到了第k大的数的下标,那么根据`partitin`规则该下标左边的数均比该下标上的数小,最小的k个数自然就是此时的`0~k-1`下标上的数 - -```java -public ArrayList GetLeastNumbers_Solution(int [] arr, int k) { - ArrayList res = new ArrayList(); - if(arr == null || arr.length == 0 || k <= 0 || k > arr.length){ - //throw new IllegalArgumentException(); - return res; - } - - int index = partition(arr, 0, arr.length - 1); - k = k - 1; - while(index != k){ - index = index > k ? partition(arr, 0, index - 1) : partition(arr, index + 1, arr.length - 1); - } - - for(int i = 0 ; i <= k ; i++){ - res.add(arr[i]); - } - - return res; -} - -public int partition(int[] arr, int start, int end){ - if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ - throw new IllegalArgumentException(); - } - if(start == end){ - return end; - } - - int random = start + (int)(Math.random() * (end - start + 1)); - swap(arr, random, end); - int small = start - 1; - for(int i = start ; i < end ; i++){ - if(arr[i] < arr[end]){ - swap(arr, ++small, i); - } - } - - swap(arr, ++small, end); - return small; -} - -public void swap(int[] arr, int i, int j){ - int t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; -} -``` - -### 连续子数组的最大和 - -#### 题目描述 - -HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) - -```java -public int FindGreatestSumOfSubArray(int[] arr) { - -} -``` - -#### 解析 - -##### 暴力解 - -暴力法是找出所有子数组,然后遍历求和,时间复杂度为`O(n^3)` - -```java -public int FindGreatestSumOfSubArray(int[] arr) { - if(arr == null || arr.length == 0){ - return 0; - } - int max = Integer.MIN_VALUE; - - //start - for(int i = 0 ; i < arr.length ; i++){ - //end - for(int j = i ; j < arr.length ; j++){ - //sum - int sum = 0; - for(int k = i ; k <= j ; k++){ - sum += arr[k]; - } - max = Math.max(max, sum); - } - } - - return max; -} -``` - -##### 最优解 - -使用一个`sum`记录累加和,初始时为0,遍历数组: - -1. 如果遍历到`i`时,发现`sum`小于0,那么丢弃这个累加和,将`sum`重置为`0` -2. 将当前元素累加到`sum`上,并更新最大和`maxSum` - -```java -public int FindGreatestSumOfSubArray(int[] arr) { - if(arr == null || arr.length == 0){ - return 0; - } - int sum = 0, max = Integer.MIN_VALUE; - for(int i = 0 ; i < arr.length ; i++){ - if(sum < 0){ - sum = 0; - } - sum += arr[i]; - max = Math.max(max, sum); - } - - return max; -} -``` - -### 整数中1出现的次数(从1到n整数中1出现的次数) - -#### 题目描述 - -求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。 - -#### 解析 - -##### 遍历一遍不就完了吗 - -当然,你可从1遍历到n,然后将当前被遍历的到的数中1出现的次数累加到结果中可以很容易地写出如下代码: - -```java -public int NumberOf1Between1AndN_Solution(int n) { - if(n < 1){ - return 0; - } - int res = 0; - for(int i = 1 ; i <= n ; i++){ - res += count(i); - } - return res; -} - -public int count(int n){ - int count = 0; - while(n != 0){ - //取个位 - count = (n % 10 == 1) ? ++count : count; - //去掉个位 - n /= 10; - } - return count; -} -``` - -但n多大就会循环多少次,这并不是面试官所期待的,这时我们就需要找规律看是否有捷径可走 - -##### 不用数我也知道 - -以`51234`这个数为例,我们可以先将`51234`划分成`1~1234`(去掉最高位)和`1235~51234`两部分来求解。下面先分析`1235~51234`这个区间的结果: - -1. 所有的数中,1在最高位(万位)出现的次数 - - 对于`1235~51234`,最高位为1时(即万位为1时)的数有`10000~19999`这10000个数,也就是说1在最高位(万位)出现的次数为10000,因此我们可以得出结论:如果最高位大于1,那么在最高位上1出现的次数为最高位对应的单位(本例中为一万次);但如果最高位为1,比如`1235~11234`,那么次数就为去掉最高位之后的数了,`11234`去掉最高位后是`1234`,即1在最高位上出现的次数为`1234` - -2. 所有的数中,1在非最高位上出现的次数 - - 我们可以进一步将`1235~51234`按照最高位的单位划分成4个区间(能划分成几个区间由最高位上的数决定,这里最高位为5,所以能划分5个大小为一万子区间): - - - `1235~11234` - - `11235~21234` - - `21235~31234` - - `31235~41234` - - `41235~51234` - - 而每个数不考虑万位(因为1在万位出现的总次数在步骤1中已统计好了),其余四位(个、十、百、千)取一位放1(比如千位),剩下的3位从`0~9`中任意选(`10 * 10 * 10`),那么仅统计1在千位上出现的次数之和就是:`5(子区间数) * 10 * 10 * 10`,还有百位、十位、个位,结果为:`4 * 10 * 10 * 10 * 5`。 - - 因此非高位上1出现的总次数的计算通式为:`(n-1) * 10^(n-2) * 十进制最高位上的数 `(其中`n`为十进制的总位数) - - 于是`1235 ~ 51234`之间所有的数的所有的位上1出现的次数的综合我们就计算出来了 - -剩下`1 ~ 1234`,你会发现这与`1 ~ 51234`的问题是一样的,因此可以做递归处理,即子过程也会将`1 ~ 1234`也分成`1 ~ 234`和`235 ~ 1234`两部分,并计算`235~1234`而将`1~234`又进行递归处理。 - -而递归的终止条件如下: - -1. 如果`1~n`中的`n`:`1 <= n <= 9`,那么就可以直接返回1了,因为只有数1出现了一次1 -2. 如果`n == 0`,比如将`10000`划分成的两部分是`0 ~ 0(10000去掉最高位后的结果)`和`1 ~ 10000`,那么就返回0 - -```java -public int NumberOf1Between1AndN_Solution(int n) { - if(n < 1){ - return 0; - } - return process(n); -} - -public int process(int n){ - if(n == 0){ - return 0; - } - if(n < 10 && n > 0){ - return 1; - } - int res = 0; - //得到十进制位数 - int bitCount = bitCount(n); - //十进制最高位上的数 - int highestBit = numOfBit(n, bitCount); - //1、统计最高位为1时,共有多少个数 - if(highestBit > 1){ - res += powerOf10(bitCount - 1); - }else{ - //highestBit == 1 - res += n - powerOf10(bitCount - 1) + 1; - } - //2、统计其它位为1的情况 - res += powerOf10(bitCount - 2) * (bitCount - 1) * highestBit; - //3、剩下的部分交给递归 - res += process(n % powerOf10(bitCount - 1)); - return res; -} - -//返回10的n次方 -public int powerOf10(int n){ - if(n == 0){ - return 1; - } - boolean minus = false; - if(n < 0){ - n = -n; - minus = true; - } - int res = 1; - for(int i = 1 ; i <= n ; i++){ - res *= 10; - } - return minus ? 1 / res : res; -} - -public int bitCount(int n){ - int count = 1; - while((n /= 10) != 0){ - count++; - } - return count; -} - -public int numOfBit(int n, int bit){ - while(bit-- > 1){ - n /= 10; - } - return n % 10; -} -``` - -笔者曾纠结,对于一个四位数,每个位上出现1时都统计了一遍会不会有重复,比如`11111`这个数在最高位为1时的`10000 ~ 19999`统计了一遍,在统计非最高位的其他位上为1时又统计了4次,总共被统计了5次,而这个数1出现的次数也确实是5次,因此没有重复。 - -### 把数组排成最小的数 - -#### 题目描述 - -输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 - -#### 解析 - -这是一个贪心问题,你发现将数组按递增排序之后依次连接起来的结果并不是最优的结果,于是需要寻求贪心策略,对于这类最小数和最小字典序的问题而言,贪心策略是:如果`3`和`32`相连的结果大于`32`和`3`相连的结果,那么视作`3`比`32`大,最后我们需要按照按照这种策略将数组进行升序排序,以得到首尾相连之后的结果是最小数字(最小字典序)。 - -```java -public String PrintMinNumber(int [] numbers) { - if(numbers == null || numbers.length == 0){ - return ""; - } - List list = new ArrayList(); - for(int num : numbers){ - list.add(num); - } - Collections.sort(list, new MyComparator()); - StringBuilder res = new StringBuilder(""); - for(Integer integer : list){ - res.append(integer.toString()); - } - return res.toString(); -} - -class MyComparator implements Comparator{ - public int compare(Integer i1, Integer i2){ - String s1 = i1.toString() + i2.toString(); - String s2 = i2.toString() + i1.toString(); - return Integer.parseInt(s1) - Integer.parseInt(s2); - } -} -``` - -### 丑数 - -#### 题目描述 - -把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 - -#### 解析 - -老实说,在《剑指offer》上看这道题的时候每太看懂,以至于第一遍在牛客网OJ这道题的时候都是背下来写上去的,直到这第二遍总结时才弄清整个思路,思路的核心就是第一个丑数是1(题目给的),此后的每一个丑数都是由之前的某个丑数与2或3或5的乘积得来 - -![image](https://wx4.sinaimg.cn/large/006zweohgy1fzdddzfdfnj30pm0d4jtb.jpg) - -```java -public int GetUglyNumber_Solution(int index) { - if(index < 1){ - //throw new IllegalArgumentException("index must bigger than one"); - return 0; - } - if(index == 1){ - return 1; - } - - int[] arr = new int[index]; - arr[0] = 1; - int indexOf2 = 0, indexOf3 = 0, indexOf5 = 0; - for(int i = 1 ; i < index ; i++){ - arr[i] = Math.min(arr[indexOf2] * 2, Math.min(arr[indexOf3] * 3, arr[indexOf5] * 5)); - indexOf2 = (arr[indexOf2] * 2 <= arr[i]) ? ++indexOf2 : indexOf2; - indexOf3 = (arr[indexOf3] * 3 <= arr[i]) ? ++indexOf3 : indexOf3; - indexOf5 = (arr[indexOf5] * 5 <= arr[i]) ? ++indexOf5 : indexOf5; - } - - return arr[index - 1]; -} -``` - -### 第一个只出现一次的字符 - -#### 题目描述 - -在一个字符串(0<=字符串长度<=10000,**全部由字母组成**)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写). - -#### 解析 - -可以从头遍历字符串,并使用一个表记录每个字符第一次出现的位置(初始时表中记录的位置均为-1),如果记录当前被遍历字符出现的位置时发现之前已经记录过了(通过查表,该字符的位置不是-1而是大于等于0的一个有效索引),那么当前字符不在答案的考虑范围内,通过将表中该字符的出现索引标记为`-2`来标识。 - -遍历一遍字符串并更新表之后,再遍历一遍字符串,如果发现某个字符在表中对应的记录是一个有效索引(大于等于0),那么该字符就是整个串中第一个只出现一次的字符。 - -由于题目标注字符串全都由字母组成,而字母可以使用`ASCII`码表示且`ASCII`范围为`0~255`,因此使用了一个长度为`256`的数组来实现这张表。用字母的`ASCII`值做索引,索引对应的值就是字母在字符串中第一次出现的位置(初始时为-1,第一次遇到时设置为出现的位置,重复遇到时置为-2)。 - -```java -public int FirstNotRepeatingChar(String str) { - if(str == null || str.length() == 0){ - return -1; - } - //全部由字母组成 - int[] arr = new int[256]; - for(int i = 0 ; i < arr.length ; i++){ - arr[i] = -1; - } - for(int i = 0 ; i < str.length() ; i++){ - int ascii = (int)str.charAt(i); - if(arr[ascii] == -1){ - //set index of first apearance - arr[ascii] = i; - }else if(arr[ascii] >= 0){ - //repeated apearance, don't care - arr[ascii] = -2; - } - //arr[ascii] == -2 -> do not care - } - - for(int i = 0 ; i < str.length() ; i++){ - int ascii = (int)str.charAt(i); - if(arr[ascii] >= 0){ - return arr[ascii]; - } - } - - return -1; -} -``` - -### 数组中的逆序对 - -#### 题目描述 - -在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007 - -```java -public int InversePairs(int [] arr) { - if(arr == null || arr.length <= 1){ - return 0; - } - return mergeSort(arr, 0, arr.length - 1).pairs; -} -``` - -#### 输入描述 - -1. 题目保证输入的数组中没有相同的数字 -2. 数据范围:对于%50的数据,size<=10^4;对于%75的数据,size<=10^5;对于%100的数据,size<=2*10^5 - - - -#### 解析 - -借助归并排序的流程,将归并流程中前一个数组的数比后一个数组的数小的情况记录下来。 - -归并的原始逻辑是根据输入的无序数组返回一个新建的排好序的数组: - -```java -public int[] mergeSort(int[] arr, int start, int end){ - if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ - throw new IllegalArgumentException(); - } - if(start == end){ - return new int[]{ arr[end] }; - } - - int[] arr1 = mergeSort(arr, start, mid); - int[] arr2 = Info right = mergeSort(arr, mid + 1, end); - int[] copy = new int[arr1.length + arr2.length]; - int p1 = 0, p2 = 0, p = 0; - - while(p1 < arr1.length && p2 < arr2.length){ - if(arr1[p1] > arr2[p2]){ - copy[p++] = arr1[p1++]; - }else{ - copy[p++] = arr2[p2++]; - } - } - while(p1 < arr1.length){ - copy[p++] = arr1[p1++]; - } - while(p2 < arr2.length){ - copy[p++] = arr2[p2++]; - } - return copy; -} -``` - -而我们需要再此基础上对子状态收集的信息进行改造,假设左右两半部分分别有序了,那么进行`merge`的时候,不应是从前往后复制了,这样当`arr1[p1] > arr2[p2]`的时候并不知道`arr2`的`p2`后面还有多少元素是比`arr1[p1]`小的,要想一次比较就统计出`arr2`中所有比`arr1[p1]`小的数需要将`p1,p2`从`arr1,arr2`的尾往前遍历: - -![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdg2nzuzkj30n006odg2.jpg) - -而将比较后较大的数移入辅助数组的逻辑还是一样。这样当前递归状态需要收集左半子数组和右半子数组的变成有序过程中记录的逆序对数和自己`merge`记录的逆序对数之和就是当前状态要返回的信息,并且`merge`后形成的有序辅助数组也要返回。 - -```java -public int InversePairs(int [] arr) { - if(arr == null || arr.length <= 1){ - return 0; - } - return mergeSort(arr, 0, arr.length - 1).pairs; -} - -class Info{ - int arr[]; - int pairs; - Info(int[] arr, int pairs){ - this.arr = arr; - this.pairs = pairs; - } -} - -public Info mergeSort(int[] arr, int start, int end){ - if(arr == null || arr.length == 0 || start < 0 || end > arr.length - 1 || start > end){ - throw new IllegalArgumentException(); - } - if(start == end){ - return new Info(new int[]{arr[end]}, 0); - } - - int pairs = 0; - int mid = start + ((end - start) >> 1); - Info left = mergeSort(arr, start, mid); - Info right = mergeSort(arr, mid + 1, end); - pairs += (left.pairs + right.pairs) % 1000000007; - - int[] arr1 = left.arr, arr2 = right.arr, copy = new int[arr1.length + arr2.length]; - int p1 = arr1.length - 1, p2 = arr2.length - 1, p = copy.length - 1; - - while(p1 >= 0 && p2 >= 0){ - if(arr1[p1] > arr2[p2]){ - pairs += (p2 + 1); - pairs %= 1000000007; - copy[p--] = arr1[p1--]; - }else{ - copy[p--] = arr2[p2--]; - } - } - - while(p1 >= 0){ - copy[p--] = arr1[p1--]; - } - while(p2 >= 0){ - copy[p--] = arr2[p2--]; - } - - return new Info(copy, pairs % 1000000007); -} -``` - -### 两个链表的第一个公共结点 - -#### 题目描述 - -输入两个链表,找出它们的第一个公共结点。 - -```java -public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { - -} -``` - -#### 解析 - -首先我们要分析两个链表的组合状态,根据有环、无环相互组合只可能会出现如下几种情况: - -![image](https://ws4.sinaimg.cn/large/006zweohgy1fzdz1wxjy8j30pc0cmmzb.jpg) - -于是我们首先要判断两个链表是否有环,判断链表是否有环以及有环链表的入环结点在哪已有前人给我们总结好了经验: - -1. 使用一个快指针和一个慢指针同时从首节点出发,快指针一次走两步而慢指针一次走一步,如果两指针相遇则说明有环,否则无环 -2. 如果两指针相遇,先将快指针重新指向首节点,然后两指针均一次走一步,再次相遇时的结点就是入环结点 - -```java -public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { - //若其中一个链表为空则不存在相交问题 - if(pHead1 == null || pHead2 == null){ - return null; - } - ListNode ringNode1 = ringNode(pHead1); - ListNode ringNode2 = ringNode(pHead2); - //如果一个有环,另一个无环 - if((ringNode1 == null && ringNode2 != null) || - (ringNode1 != null && ringNode2 == null)){ - return null; - } - //如果两者都无环,判断是否共用尾结点 - else if(ringNode1 == null && ringNode2 == null){ - return firstCommonNode(pHead1, pHead2, null); - } - //剩下的情况就是两者都有环了 - else{ - //如果入环结点相同,那么第一个相交的结点肯定在入环结点之前 - if(ringNode1 == ringNode2){ - return firstCommonNode(pHead1, pHead2, ringNode1); - } - //如果入环结点不同,看能否通过ringNode1的后继找到ringNode2 - else{ - ListNode p = ringNode1; - while(p.next != ringNode1){ - p = p.next; - if(p == ringNode2){ - break; - } - } - //如果能找到,那么第一个相交的结点既可以是ringNode1也可以是ringNode2 - return (p == ringNode2) ? ringNode1 : null; - } - } -} - -//查找两链表的第一个公共结点,如果两链表无环,则传入common=null,如果都有环且入环结点相同,那么传入common=入环结点 -public ListNode firstCommonNode(ListNode pHead1, ListNode pHead2, ListNode common){ - ListNode p1 = pHead1, p2 = pHead2; - int len1 = 1, len2 = 1, gap = 0; - while(p1.next != common){ - p1 = p1.next; - len1++; - } - while(p2.next != common){ - p2 = p2.next; - len2++; - } - //如果是两个无环链表,要判断一下是否有公共尾结点 - if(common == null && p1 != p2){ - return null; - } - gap = len1 > len2 ? len1 - len2 : len2 - len1; - //p1指向长链表,p2指向短链表 - p1 = len1 > len2 ? pHead1 : pHead2; - p2 = len1 > len2 ? pHead2 : pHead1; - while(gap-- > 0){ - p1 = p1.next; - } - while(p1 != p2){ - p1 = p1.next; - p2 = p2.next; - } - return p1; -} - -//判断链表是否有环,没有返回null,有则返回入环结点(整个链表是一个环时入环结点就是头结点) -public ListNode ringNode(ListNode head){ - if(head == null){ - return null; - } - ListNode p1 = head, p2 = head; - while(p1.next != null && p1.next.next != null){ - p1 = p1.next.next; - p2 = p2.next; - if(p1 == p2){ - break; - } - } - - if(p1.next == null || p1.next.next == null){ - return null; - } - - p1 = head; - while(p1 != p2){ - p1 = p1.next; - p2 = p2.next; - } - //可能整个链表就是一个环,这时入环结点就是头结点!!! - return p1 == p2 ? p1 : head; -} -``` - -### 数字在排序数组中出现的次数 - -#### 题目描述 - -统计一个数字在排序数组中出现的次数。 - -```java -public int GetNumberOfK(int [] array , int k) { - -} -``` - -#### 解析 - -我们可以分两步解决,先找出数值为k的连续序列的左边界,再找右边界。可以采用二分的方式,以查找左边界为例:如果`arr[mid]`小于`k`那么移动左指针,否则移动右指针(初始时左指针指向`-1`,而右指针指向尾元素`arr.length`),当两个指针相邻时,左指针及其左边的数均小于`k`而右指针及其右边的数均大于或等于`k`,因此此时右指针就是要查找的左边界,同样的方式可以求得右边界。 - -值得注意的是,笔者曾将左指针初始化为`0`而右指针初始化为`arr.length - 1`,这与指针指向的含义是相悖的,因为左指针指向的元素必须是小于`k`的,而我们并不能保证`arr[0]`一定小于`k`,同样的我们也不能保证`arr[arr.length - 1]`一定大于等于`k`。 - -还有一点就是如果数组中没有`k`这个算法是否依然会返回一个正确的值(0),这也是需要验证的。 - -```java -public int GetNumberOfK(int [] arr , int k) { - if(arr == null || arr.length == 0){ - return 0; - } - if(arr.length == 1){ - return (arr[0] == k) ? 1 : 0; - } - - int start, end, left, right; - for(start = -1, end = arr.length ; end > start && end - start != 1 ;){ - int mid = start + ((end - start) >> 1); - if(arr[mid] >= k){ - end = mid; - }else{ - start = mid; - } - } - left = end; - for(start = -1, end = arr.length; end > start && end - start != 1 ;){ - int mid = start + ((end - start) >> 1); - if(arr[mid] > k){ - end = mid; - }else{ - start = mid; - } - } - right = start; - return right - left + 1; -} -``` - -### 二叉树的深度 - -#### 题目描述 - -输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 - -```java -public int TreeDepth(TreeNode root) { -} -``` - -#### 解析 - -1. 将`TreeDepth`看做一个黑盒,假设利用这个黑盒收集到了左子树和右子树的深度,那么当前这棵树的深度就是前面两者的最大值加1 -2. `base case`,如果当前是一棵空树,那么深度为0 - -```java -public class Solution { - public int TreeDepth(TreeNode root) { - if(root == null){ - return 0; - } - return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1; - } -} -``` - -### 平衡二叉树 - -#### 题目描述 - -输入一棵二叉树,判断该二叉树是否是平衡二叉树。 - -```java -public boolean IsBalanced_Solution(TreeNode root) { - -} -``` - -#### 解析 - -判断当前这棵树是否是平衡二叉所需要收集的信息: - -1. 左子树、右子树各自是平衡二叉树吗(需要收集子树是否是平衡二叉树) -2. 如果1成立,还需要收集左子树和右子树的高度,如果高度相差不超过1那么当前这棵树才是平衡二叉树(需要收集子树的高度) - -```java -class Info{ - boolean isBalanced; - int height; - Info(boolean isBalanced, int height){ - this.isBalanced = isBalanced; - this.height = height; - } -} -``` - -递归体的定义:(这里高度之差不超过1中的`left.height - right.height == 0`容易被忽略) - -```java -public boolean IsBalanced_Solution(TreeNode root) { - return process(root).isBalanced; -} - -public Info process(TreeNode root){ - if(root == null){ - return new Info(true, 0); - } - Info left = process(root.left); - Info right = process(root.right); - if(!left.isBalanced || !right.isBalanced){ - //如果左子树或右子树不是平衡二叉树,那么当前这棵树肯定也不是,树高度信息也就没用了 - return new Info(false, 0); - } - //高度之差不超过1 - if(left.height - right.height == 1 || left.height - right.height == -1 || - left.height - right.height == 0){ - return new Info(true, Math.max(left.height, right.height) + 1); - } - return new Info(false, 0); -} -``` - -### 数组中只出现一次的数字 - -#### 题目描述 - -一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。 - -#### 解析 - -如果没有解过类似的题目,思路比较难打开。面试官可能会提醒你,如果是让你求一个整型数组里只有一个数只出现了一次而其它数出现了偶数次呢?你应该联想到: - -1. **偶数次相同的数异或的结果是0** -2. **任何数与0异或的结果是它本身** - -于是将数组从头到尾求异或和便可得知结果。那么对于此题,能否将数组分成这样的两部分呢:每个部分只有一个数出现了一次,其他的数都出现偶数次。 - -如果我们仍将整个数组从头到尾求异或和,那结果应该和这两个只出现一次的数的异或结果相同,目前我们所能依仗的也就是这个结果了,能否靠这个结果将数组分成想要的两部分? - -由于两个只出现一次的数(用A和B表示)异或结果`A ^ B`肯定不为0,那么`A ^ B`的二进制表示中肯定包含数值为1的bit位,而这个位上的1肯定是由A或B提供的,也就是说我们能根据**这个bit位上的数是否为1**来区分A和B,那剩下的数呢? - -由于剩下的数都出现偶数次,因此相同的数都会被分到一边(按照某个bit位上是否为1来分)。 - -```java -public void FindNumsAppearOnce(int [] arr,int num1[] , int num2[]) { - if(arr == null || arr.length <= 1){ - return; - } - int xorSum = 0; - for(int num : arr){ - xorSum ^= num; - } - //取xorSum二进制表示中低位为1的bit位,将其它的bit位 置0 - //比如:xorSum = 1100,那么 (1100 ^ 1011) & 1100 = 0100,只剩下一个为1的bit位 - xorSum = (xorSum ^ (xorSum - 1)) & xorSum; - - for(int num : arr){ - num1[0] = (num & xorSum) == 0 ? num1[0] ^ num : num1[0]; - num2[0] = (num & xorSum) != 0 ? num2[0] ^ num : num2[0]; - } -} -``` - -### 和为S的连续正数序列 - -#### 题目描述 - -小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck! - -```java -public ArrayList > FindContinuousSequence(int sum) { - -} -``` - -#### 输出描述 - -输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序 - -#### 解析 - -将`1 ~ (S / 2 + 1)`区间的数`n`依次加入到队列中(因为从`S/2 + 1`之后的任意两个正数之和都大于`S`): - -1. 将`n`加入到队列`queue`中并将队列元素之和`queueSum`更新,更新`queueSum`之后如果发现等于`sum`,那么将此时的队列快照加入到返回结果`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**) -2. 更新`queueSum`之后如果发现大于`sum`,那么循环弹出队首元素直到`queueSum <= Sum`,如果循环弹出之后发现`queueSum == sum`那么将队列快照加入到`res`中,并弹出队首元素(**保证下次入队操作时队列元素之和是小于sum的**);如果`queueSum < sum`那么入队下一个`n` - -于是有如下代码: - -```java -public ArrayList> FindContinuousSequence(int sum) { - ArrayList> res = new ArrayList(); - if(sum <= 1){ - return res; - } - LinkedList queue = new LinkedList(); - int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; - while(n <= halfSum){ - queue.addLast(n); - queueSum += n; - if(queueSum == sum){ - ArrayList one = new ArrayList(); - one.addAll(queue); - res.add(one); - queueSum -= queue.pollFirst(); - }else if(queueSum > sum){ - while(queueSum > sum){ - queueSum -= queue.pollFirst(); - } - if(queueSum == sum){ - ArrayList one = new ArrayList(); - one.addAll(queue); - res.add(one); - queueSum -= queue.pollFirst(); - } - } - n++; - } - - return res; -} -``` - -我们发现`11~15`和`20~24`行的代码是重复的,于是可以稍微优化一下: - -```java -public ArrayList> FindContinuousSequence(int sum) { - ArrayList> res = new ArrayList(); - if(sum <= 1){ - return res; - } - LinkedList queue = new LinkedList(); - int n = 1, halfSum = (sum >> 1) + 1, queueSum = 0; - while(n <= halfSum){ - queue.addLast(n); - queueSum += n; - if(queueSum > sum){ - while(queueSum > sum){ - queueSum -= queue.pollFirst(); - } - } - if(queueSum == sum){ - ArrayList one = new ArrayList(); - one.addAll(queue); - res.add(one); - queueSum -= queue.pollFirst(); - } - n++; - } - - return res; -} -``` - -### 和为S的两个数字 - -#### 题目描述 - -输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 - -```java -public ArrayList FindNumbersWithSum(int [] arr,int sum) { - -} -``` - -#### 输出描述 - -对应每个测试案例,输出查找到的两个数,如果有多对,输出乘积最小的两个。 - -#### 解析 - -使用指针`l,r`,初始时`l`指向首元素,`r`指向尾元素,当两指针元素之和不等于`sum`且`r`指针在`l`指针右侧时循环: - -1. 如果两指针元素之和大于`sum`,那么将`r`指针左移,试图减小两指针之和 -2. 如果两指针元素之和小于`sum`,那么将`l`右移,试图增大两指针之和 -3. 如果两指针元素之和等于`sum`那么就可以返回了,或者`r`跑到了`l`的左边表名没有和`sum`的两个数,也可以返回了。 - -```java -public ArrayList FindNumbersWithSum(int [] arr,int sum) { - ArrayList res = new ArrayList(); - if(arr == null || arr.length <= 1 ){ - return res; - } - int l = 0, r = arr.length - 1; - while(arr[l] + arr[r] != sum && r > l){ - if(arr[l] + arr[r] > sum){ - r--; - }else{ - l++; - } - } - if(arr[l] + arr[r] == sum){ - res.add(arr[l]); - res.add(arr[r]); - } - return res; -} -``` - -### 旋转字符串 - -#### 题目描述 - -汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! - -```java -public String LeftRotateString(String str,int n) { - -} -``` - -#### 解析 - -将开头的一段子串移到串尾:将开头的子串翻转一下、将剩余的子串翻转一下,最后将整个子串翻转一下。按理来说应该输入`char[] str`的,这样的话这种算法不会使用额外空间。 - -```java -public String LeftRotateString(String str,int n) { - if(str == null || str.length() == 0 || n <= 0){ - return str; - } - char[] arr = str.toCharArray(); - reverse(arr, 0, n - 1); - reverse(arr, n, arr.length - 1); - reverse(arr, 0, arr.length - 1); - return new String(arr); -} - -public void reverse(char[] str, int start, int end){ - if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ - return; - } - for(int i = start, j = end ; j > i ; i++, j--){ - char tmp = str[i]; - str[i] = str[j]; - str[j] = tmp; - } -} -``` - -### 翻转单词顺序列 - -#### 题目描述 - -牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? - -```java -public String LeftRotateString(String str,int n) { - -} -``` - -#### 解析 - -先将整个字符串翻转,最后按照标点符号或空格一次将句中的单词翻转。注意:由于最后一个单词后面没有空格,因此需要单独处理!!! - -```java -public String ReverseSentence(String str) { - if(str == null || str.length() <= 1){ - return str; - } - char[] arr = str.toCharArray(); - reverse(arr, 0, arr.length - 1); - int start = -1; - for(int i = 0 ; i < arr.length ; i++){ - if(arr[i] != ' '){ - //初始化start - start = (start == -1) ? i : start; - }else{ - //如果是空格,不用担心start>i-1,reverse会忽略它 - reverse(arr, start, i - 1); - start = i + 1; - } - } - //最后一个单词,这里比较容易忽略!!! - reverse(arr, start, arr.length - 1); - - return new String(arr); -} - -public void reverse(char[] str, int start, int end){ - if(str == null || str.length == 0 || start < 0 || end > str.length - 1 || start >= end){ - return ; - } - for(int i = start, j = end ; j > i ; i++, j--){ - char tmp = str[i]; - str[i] = str[j]; - str[j] = tmp; - } -} -``` - -### 扑克牌顺子 - -#### 题目描述 - -LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。 - -#### 解析 - -先将数组排序(5个元素排序时间复杂O(1)),然后遍历数组统计王的数量和相邻非王牌之间的缺口数(需要用几个王来填)。还有一点值得注意:如果发现两种相同的非王牌,则不可能组成五张不同的顺子。 - -```java -public boolean isContinuous(int [] arr) { - if(arr == null || arr.length != 5){ - return false; - } - //5 numbers -> O(1) - Arrays.sort(arr); - int zeroCount = 0, slots = 0; - for(int i = 0 ; i < arr.length ; i++){ - //如果遇到两张相同的非王牌则不可能组成顺子,这点很容易忽略!!! - if(i > 0 && arr[i - 1] != 0){ - if(arr[i] == arr[i - 1]){ - return false; - }else{ - slots += arr[i] - arr[i - 1] - 1; - } - - } - zeroCount = (arr[i] == 0) ? ++zeroCount : zeroCount; - } - - return zeroCount >= slots; -} -``` - -### 孩子们的游戏(圆圈中剩下的数) - -#### 题目描述 - -每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) - -#### 解析 - -1. 报数时,在报到`m-1`之前,可通过报数求得报数的结点编号: - - ![image](https://ws1.sinaimg.cn/large/006zweohgy1fze7t11d3mj30j309ewey.jpg) - -2. 在某个结点(小朋友)出列后的重新编号过程中,可通过新编号求结点的就编号 - - ![image](https://wx1.sinaimg.cn/large/006zweohgy1fze8z9c5dcj30o40eg0u7.jpg) - - 因此在某轮重新编号时,我们能在已知新编号`x`的情况下通过公式`y = (x + S + 1) % n`求得结点重新标号之前的旧编号,上述两步分析的公式整理如下: - - 1. 某一轮报数出列前:`编号 = (报数 - 1)% 出列前结点个数` - 2. 某一轮报数出列后:`旧编号 = (新编号 + 出列编号 + 1)% 出列前结点个数`,因为出列结点是因为报数`m`才出列的,所以有:`出列编号 = (m - 1)% 出列前结点个数` - 3. 由2可推出:`旧编号 = (新编号 + (m - 1)% 出列前结点个数 + 1)% 出列前结点个数` ,若用`n`表示**出列后**结点个数:`y = (x + (m - 1) % n + 1) % n = (x + m - 1) % n + 1` - -经过上面3步的复杂分析之后,我们得出这么一个通式:`旧编号 = (新编号 + m - 1 )% 出列前结点个数 + 1`,于是我们就可以自下而上(用链表模拟出列过程是自上而下),求出**最后一轮重新编号为`1`**的小朋友(只剩他一个了)在倒数第二轮重新编号时的旧编号,自下而上可倒推出这个小朋友在第一轮编号时(这时还没有任何一个小朋友出列过)的原始编号,即目标答案。 - -> 注意:式子`y = (x + m - 1) % n + 1`的计算结果不可能为`0`,因此我们可以按小朋友从`1`开始编号,将最后的计算结果应题目的要求(小朋友从0开始编号)减一个1即可。 - -```java -public int LastRemaining_Solution(int n, int m) { - if(n <= 0){ - //throw new IllegalArgumentException(); - return -1; - } - //最后一次重新编号:最后一个结点编号为1,出列前结点数为2 - return orginalNumber(2, 0, n, m); -} - -//根据出列后的重新编号(newNumber)推导出列前的旧编号(返回值) -//n:出列前有多少小朋友,N:总共有多少个小朋友 -public int orginalNumber(int n, int newNumber, int N, int m){ - int lastNumber = (newNumber + m - 1) % n + 1; - if(n == N){ - return lastNumber; - } - return orginalNumber(n + 1, lastNumber, N, m); -} -``` - -### 求1+2+3+…+n - -#### 题目描述 - -求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 - -```java -public int Sum_Solution(int n) { - -} -``` - -#### 解析 - -##### 递归轻松解决 - -既然不允许遍历求和,不如将计算分解,如果知道了`f(n - 1)`,`f(n)`则可以通过`f(n - 1) + n`算出: - -```java -public int Sum_Solution(int n) { - if(n == 1){ - return 1; - } - return n + Sum_Solution(n - 1); -} -``` - -### 不用加减乘除做加法 - -#### 题目描述 - -写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 - -#### 解析 - -不要忘了加减乘除是人类熟悉的运算方法,而计算机只知道位运算哦! - -我们可以将两数的二进制表示写出来,然后按位与得出进位信息、按位或得出非进位信息,如果进位信息不为0,则循环计算直到进位信息为0,此时异或信息就是两数之和: - -![image](https://ws2.sinaimg.cn/large/006zweohgy1fzeb2umgekj30hf0cvdgs.jpg) - -```java -public int Add(int num1,int num2) { - if(num1 == 0 || num2 == 0){ - return num1 == 0 ? num2 : num1; - } - int and = 0, xor = 0; - do{ - and = num1 & num2; - xor = num1 ^ num2; - num1 = and << 1; - num2 = xor; - }while(and != 0); - - return xor; -} -``` - -### 把字符串转换成整数 - -#### 题目描述 - -将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 - -```java -public int StrToInt(String str) { - -} -``` - -#### 输入描述 - -输入一个字符串,包括数字字母符号,可以为空 - -#### 输出描述 - -如果是合法的数值表达则返回该数字,否则返回0 - -#### 示例 - -输入:`+2147483647`,输出:`2147483647` -输入:`1a33`,输出`0` - -#### 解析 - -1. 只有第一个位置上的字符可以是`+`或`-`或数字,其他位置上的字符必须是数字 -2. 如果第一个字符是`-`,返回结果必须是负数 -3. 如果字符串只有一个字符,且为`+`或`-`,这情况很容易被忽略 -4. 在对字符串解析转换时,如果发现溢出(包括正数向负数溢出,负数向正数溢出),必须有所处理(此时可以和面试官交涉),但不能视而不见 - -```java -public int StrToInt(String str) { - if(str == null || str.length() == 0){ - return 0; - } - boolean minus = false; - int index = 0; - if(str.charAt(0) == '-'){ - minus = true; - index = 1; - }else if(str.charAt(0) == '+'){ - index = 1; - } - //如果只有一个正负号 - if(index == str.length()){ - return 0; - } - - if(checkInteger(str, index, str.length() - 1)){ - return transform(str, index, str.length() - 1, minus); - } - - return 0; -} - -public boolean checkInteger(String str, int start, int end){ - if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ - return false; - } - for(int i = start ; i <= end ; i++){ - if(str.charAt(i) < '0' || str.charAt(i) > '9'){ - return false; - } - } - return true; -} - -public int transform(String str, int start, int end, boolean minus){ - if(str == null || str.length() == 0 || start < 0 || end > str.length() - 1 || start > end){ - throw new IllegalArgumentException(); - } - int res = 0; - for(int i = start ; i <= end ; i++){ - int num = str.charAt(i) - '0'; - res = minus ? (res * 10 - num) : (res * 10 + num); - if((minus && res > 0) || (!minus && res < 0)){ - throw new ArithmeticException("the str is overflow int"); - } - } - return res; -} -``` - -### 数组中重复的数字 - -#### 题目描述 - -在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 - -```java -// Parameters: -// numbers: an array of integers -// length: the length of array numbers -// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation; -// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++ -// 这里要特别注意~返回任意重复的一个,赋值duplication[0] -// Return value: true if the input is valid, and there are some duplications in the array number -// otherwise false -public boolean duplicate(int numbers[],int length,int [] duplication) { - -} -``` - -#### 解析 - -认真审题发现输入数据是有特征的,即数组长度为`n`,数组中的元素都在`0~n-1`范围内,如果数组中没有重复的元素,那么排序后每个元素和其索引值相同,这就意味着数组中如果有重复的元素,那么数组排序后肯定有元素和它对应的索引是不等的。 - -顺着这个思路,我们可以将每个元素放到与它相等的索引上,如果某次放之前发现对应的索引上已有了和索引相同的元素,那么说明这个元素是重复的,由于每个元素最多会被调整两次,因此时间复杂`O(n)` - -```java -public boolean duplicate(int arr[],int length,int [] duplication) { - if(arr == null || arr.length == 0){ - return false; - } - int index = 0; - while(index < arr.length){ - if(arr[index] == arr[arr[index]]){ - if(index != arr[index]){ - duplication[0] = arr[index]; - return true; - }else{ - index++; - } - }else{ - int tmp = arr[index]; - arr[index] = arr[tmp]; - arr[tmp] = tmp; - } - } - - return false; -} -``` - -### 构建乘积数组 - -#### 题目描述 - -给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。 - -```java -public int[] multiply(int[] arr) { - -} -``` - - - -#### 分析 - -规律题: - -![image](https://ws2.sinaimg.cn/large/006zweohgy1fzee5lql6fj30ie0513ys.jpg) - -```java -public int[] multiply(int[] arr) { - if(arr == null || arr.length == 0){ - return arr; - } - int len = arr.length; - int[] arr1 = new int[len], arr2 = new int[len]; - arr1[0] = 1; - arr2[len - 1] = 1; - for(int i = 1 ; i < len ; i++){ - arr1[i] = arr1[i - 1] * arr[i - 1]; - arr2[len - 1 - i] = arr2[len - i] * arr[len - i]; - } - int[] res = new int[len]; - for(int i = 0 ; i < len ; i++){ - res[i] = arr1[i] * arr2[i]; - } - - return res; -} -``` - -### 正则表达式匹配 - -#### 题目描述 - -请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配 - -```java -public boolean match(char[] str, char[] pattern){ - -} -``` - - - -#### 解析 - -使用`p1`指向`str`中下一个要匹配的字符,使用`p2`指向`pattern`中剩下的模式串的首字符 - -1. 如果`p2 >= pattern.length`,表示模式串消耗完了,这时如果`p1`仍有字符要匹配那么返回`false`否则返回`true` -2. 如果`p1 >= str.length`,表示要匹配的字符都匹配完了,但模式串还没消耗完,这时剩下的模式串必须符合`a*b*c*`这样的范式以能够作为空串处理,否则返回`false` -3. `p1`和`p2`都未越界,按照`p2`后面是否是`*`来讨论 - 1. `p2`后面如果是`*`,又可按照`pattern[p2]`是否能够匹配`str[p1]`分析: - 1. `pattern[p2] == ‘.’ || pattern[p2] == str[p1]`,这时可以选择匹配一个`str[p1]`并继续向后匹配(不用跳过`p2`和其后面的`*`),也可以选择将`pattern[p2]`和其后面的`*`作为匹配空串处理,这时要跳过`p2`和 其后面的`*` - 2. `pattern[p2] != str[p1]`,只能作为匹配空串处理,跳过`p2` - 2. `p2`后面如果不是`*`: - 1. `pattern[p2] == str[p1] || pattern[p2] == ‘.’`,`p1,p2`同时后移一个继续匹配 - 2. `pattern[p2] == str[p1]`,直接返回`false` - -```java -public boolean match(char[] str, char[] pattern){ - if(str == null || pattern == null){ - return false; - } - if(str.length == 0 && pattern.length == 0){ - return true; - } - return matchCore(str, 0, pattern, 0); -} - -public boolean matchCore(char[] str, int p1, char[] pattern, int p2){ - //模式串用完了 - if(p2 >= pattern.length){ - return p1 >= str.length; - } - if(p1 >= str.length){ - if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ - return matchCore(str, p1, pattern, p2 + 2); - }else{ - return false; - } - } - - //如果p2的后面是“*” - if(p2 + 1 < pattern.length && pattern[p2 + 1] == '*'){ - if(pattern[p2] == '.' || pattern[p2] == str[p1]){ - //匹配一个字符,接着还可以向后匹配;或者将当前字符和后面的星合起来做空串 - return matchCore(str, p1 + 1, pattern, p2) || matchCore(str, p1, pattern, p2 + 2); - }else{ - return matchCore(str, p1, pattern, p2 + 2); - } - } - //如果p2的后面不是* - else{ - if(pattern[p2] == '.' || pattern[p2] == str[p1]){ - return matchCore(str, p1 + 1, pattern, p2 + 1); - }else{ - return false; - } - } -} -``` - -### 表示数值的字符串 - -#### 题目描述 - -请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。 - -```java -public boolean isNumeric(char[] str) { - -} -``` - -#### 解析 - -由题式可得出如下约束: - -1. 正负号只能出现在第一个位置或者`e/E`后一个位置 -2. `e/E`后面有且必须有整数 -3. 字符串中只能包含数字、小数点、正负号、`e/E`,其它的都是非法字符 -4. `e/E`的前面最多只能出现一次小数点,而`e/E`的后面不能出现小数点 - -```java -public boolean isNumeric(char[] str) { - if(str == null || str.length == 0){ - return false; - } - - boolean signed = false; //标识是否以正负号开头 - boolean decimal = false; //标识是否有小数点 - boolean existE = false; //是否含有e/E - int start = -1; //一段连续数字的开头 - int index = 0; //从0开始遍历字符 - - if(existSignAtIndex(str, 0)){ - signed = true; - index++; - } - - while(index < str.length){ - //以下按照index上可能出现的字符进行分支判断 - if(str[index] >= '0' && str[index] <= '9'){ - start = (start == -1) ? index : start; - index++; - - }else if(str[index] == '+' || str[index] == '-'){ - //首字符的+-我们已经判断过了,因此+-只可能出现在e/E的后面 - if(!existEAtIndex(str, index - 1)){ - return false; - } - index++; - - }else if(str[index] == '.'){ - //小数点只可能出现在e/E前面,且只可能出现一次 - //如果出现过小数点了,或者小数点前一段连续数字的前面是e/E - if(decimal || existEAtIndex(str, start - 1) - || existEAtIndex(str, start - 2) ){ - return false; - } - decimal = true;//出现了小数点 - index++; - //下一段连续数字的开始 - start = index; - - }else if(existEAtIndex(str, index)){ - if(existE){ - //如果已出现过e/E - return false; - } - existE = true; - index++; - //由于e/E后面可能是正负号也可能是数字,所以下一段连续数字的开始不确定 - start = !existSignAtIndex(str, index) ? index : index + 1; - - }else{ - return false; - } - } - - //如果最后一段连续数字的开始不存在 -> e/E后面没有数字 - if(start >= str.length){ - return false; - } - - return true; -} - -//在index上的字符是否是e或者E -public boolean existEAtIndex(char[] str, int index){ - if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ - return false; - } - return str[index] == 'e' || str[index] == 'E'; -} - -//在index上的字符是否是正负号 -public boolean existSignAtIndex(char[] str, int index){ - if(str == null || str.length == 0 || index < 0 || index > str.length - 1){ - return false; - } - return str[index] == '+' || str[index] == '-'; -} -``` - - - -### 字符流中第一个只出现一次的字符 - -#### 题目描述 - -请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 - -#### 输出描述 - -如果当前字符流没有存在出现一次的字符,返回#字符。 - -#### 解析 - -首先要选取一个容器来保存字符,并且要记录字符进入容器的顺序。如果不考虑中文字符,那么可以使用一张大小为`256`(对应`ASCII`码值范围)的表来保存字符,用字符的`ASCII`**码值**作为索引,用字符进入容器的**次序**作为索引对应的记录,表内部维护了一个计数器`position`,每当有字符进入时以该计数器的值作为该字符的次序(初始时,每个字符对应的次序为-1),如果设置该字符的次序时发现之前已设置过(次序不为-1,而是大于等于0),那么将该字符的次序置为-2,表示以后从容器取第一个只出现一次的字符时不考虑该字符。 - -当从容器取第一个只出现一次的字符时,考虑次序大于等于0的字符,在这个前提下找出次序最小的字符并返回。 - -```java -//不算中文,保存所有ascii码对应的字符只需256字节,记录ascii码为index的字符首次出现的位置 -int[] arr = new int[256]; -int position = 0; -{ - for(int i = 0 ; i < arr.length ; i++){ - //初始时所有字符的首次出现的位置为-1 - arr[i] = -1; - } -} -//Insert one char from stringstream -public void Insert(char ch){ - int ascii = (int)ch; - if(arr[ascii] == -1){ - arr[ascii] = position++; - }else if(arr[ascii] >= 0){ - arr[ascii] = -2; - } -} -//return the first appearence once char in current stringstream -public char FirstAppearingOnce(){ - int minPosi = Integer.MAX_VALUE; - char res = '#'; - for(int i = 0 ; i < arr.length ; i++){ - if(arr[i] >= 0 && arr[i] < minPosi){ - minPosi = arr[i]; - res = (char)i; - } - } - - return res; -} -``` - -### 删除链表中重复的结点 - -#### 题目描述 - -在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 - -```java -public ListNode deleteDuplication(ListNode pHead){ - -} -``` - -#### 解析 - -此题处理起来棘手的有两个地方: - -1. 如果某个结点的后继结点与其重复,那么删除该结点的一串连续重复的结点之后如何删除该结点本身,这就要求我们需要保留当前遍历结点的前驱指针。 - - 但是如果从头结点开始就出现一连串的重复呢?我们又如何删除删除头结点,因此我们需要新建一个辅助结点作为头结点的前驱结点。 - -2. 在遍历过程中如何区分当前结点是不重复的结点,还是在删除了它的若干后继结点之后最后也要删除它本身的重复结点?这就需要我们使用一个布尔变量记录是否开启了删除模式(`deleteMode`) - -经过上述两步分析,我们终于可以安心遍历结点了: - -```java -public ListNode deleteDuplication(ListNode pHead){ - if(pHead == null){ - return null; - } - ListNode node = new ListNode(Integer.MIN_VALUE); - node.next = pHead; - ListNode pre = node, p = pHead; - boolean deletedMode = false; - while(p != null){ - if(p.next != null && p.next.val == p.val){ - p.next = p.next.next; - deletedMode = true; - }else if(deletedMode){ - pre.next = p.next; - p = pre.next; - deletedMode = false; - }else{ - pre = p; - p = p.next; - } - } - - return node.next; -} -``` - -### 二叉树的下一个结点 - -#### 题目描述 - -给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 - -#### 解析 - -由于中序遍历来到某个结点后,首先会接着遍历它的右子树,如果它没有右子树则会回到祖先结点中将它当做左子树上的结点的那一个,因此有如下分析: - -1. 如果当前结点有右子树,那么其后继结点就是其右子树上的最左结点 -2. 如果当前结点没有右子树,那么其后继结点就是其祖先结点中,将它当做左子树上的结点的那一个。 - -```java -public TreeLinkNode GetNext(TreeLinkNode pNode){ - if(pNode == null){ - return null; - } - //如果有右子树,后继结点是右子树上最左的结点 - if(pNode.right != null){ - TreeLinkNode p = pNode.right; - while(p.left != null){ - p = p.left; - } - return p; - }else{ - //如果没有右子树,向上查找第一个当前结点是父结点的左孩子的结点 - TreeLinkNode p = pNode.next; - while(p != null && pNode != p.left){ - pNode = p; - p = p.next; - } - - if(p != null && pNode == p.left){ - return p; - } - return null; - } -} -``` - -​ - -### 对称的二叉树 - -#### 题目描述 - -请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 - -```java -boolean isSymmetrical(TreeNode pRoot){ - -} -``` - -#### 解析 - -判断一棵树是否是镜像二叉树,只需将经典的先序遍历序列和变种的**先根再右再左**的先序遍历序列比较,如果相同则为镜像二叉树。 - -```java -boolean isSymmetrical(TreeNode pRoot){ - if(pRoot == null){ - return true; - } - StringBuffer str1 = new StringBuffer(""); - StringBuffer str2 = new StringBuffer(""); - preOrder(pRoot, str1); - preOrder2(pRoot, str2); - return str1.toString().equals(str2.toString()); -} - -public void preOrder(TreeNode root, StringBuffer str){ - if(root == null){ - str.append("#"); - return; - } - str.append(String.valueOf(root.val)); - preOrder(root.left, str); - preOrder(root.right, str); -} - -public void preOrder2(TreeNode root, StringBuffer str){ - if(root == null){ - str.append("#"); - return; - } - str.append(String.valueOf(root.val)); - preOrder2(root.right, str); - preOrder2(root.left, str); -} -``` - -### 按之字形打印二叉树 - -#### 题目描述 - -请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 - -#### 解析 - -注意下述代码的第`14`行,笔者曾写为`stack2 = stack1 == empty ? stack1 : stack2`,你能发现错误在哪儿吗? - -```java -public ArrayList> Print(TreeNode pRoot) { - ArrayList> res = new ArrayList(); - if(pRoot == null){ - return res; - } - - Stack stack1 = new Stack(); - Stack stack2 = new Stack(); - stack1.push(pRoot); - boolean flag = true;//先加左孩子,再加右孩子 - while(!stack1.empty() || !stack2.empty()){ - Stack empty = stack1.empty() ? stack1 : stack2; - stack1 = stack1 == empty ? stack2 : stack1; - stack2 = empty; - ArrayList row = new ArrayList(); - while(!stack1.empty()){ - TreeNode p = stack1.pop(); - row.add(p.val); - if(flag){ - if(p.left != null){ - stack2.push(p.left); - } - if(p.right != null){ - stack2.push(p.right); - } - }else{ - if(p.right != null){ - stack2.push(p.right); - } - if(p.left != null){ - stack2.push(p.left); - } - } - } - res.add(row); - flag = !flag; - } - - return res; -} -``` - -### 序列化二叉树 - -#### 题目描述 - -请实现两个函数,分别用来序列化和反序列化二叉树 - -#### 解析 - -怎么序列化的,就怎么反序列化。这里`deserialize`反序列化时对于序列化到`String[] arr`的哪个结点值来了的变量`index`有两个坑(都是笔者亲自踩的): - -1. 将`index`声明为成员的`int`,`Java`中函数调用时不会改变基本类型参数的值的,因此不要企图使用`int`表示当前序列化哪个结点的值来了 -2. 之后笔者想用`Integer`代替,但是`Integer`和`String`一样,都是不可变对象,所有的值更改操作在底层都是拆箱和装箱生成新的`Integer`,因此也不要使用`Integer`做序列化到哪一个结点数值来了的计数器 -3. 最好使用数组或者自定义的类(在类中声明一个`int`变量) - -```java -String Serialize(TreeNode root) { - if(root == null){ - return "#_"; - } - //处理头结点、左子树、右子树 - String res = root.val + "_"; - res += Serialize(root.left); - res += Serialize(root.right); - return res; -} - -TreeNode Deserialize(String str) { - if(str == null || str.length() == 0){ - return null; - } - Integer index = 0; - return deserialize(str.split("_"), new int[]{0}); -} - -//怎么序列化的,就怎么反序列化 -TreeNode deserialize(String[] arr, int[] index){ - if("#".equals(arr[index[0]])){ - index[0]++; - return null; - } - //头结点、左子树、右子树 - TreeNode root = new TreeNode(Integer.parseInt(arr[index[0]])); - index[0]++; - root.left = deserialize(arr, index); - root.right = deserialize(arr, index); - return root; -} -``` - -### 二叉搜索树的第k个结点 - -#### 题目描述 - -给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。 - -```java -TreeNode KthNode(TreeNode pRoot, int k){ - -} -``` - -#### 解析 - -二叉搜索树的特点是,它的中序序列是有序的,因此我们可以借助中序遍历在递归体中第二次来到当前结点时更新一下计数器,直到遇到第k个结点保存并返回即可。 - -值得注意的地方是: - -1. 由于计数器在递归中传来传去,因此你需要保证每个递归引用的是同一个计数器,这里使用的是一个`int[]`的第一个元素来保存 -2. 我们写中序遍历是不需要返回值的,可以在找到第k小的结点时将其保存在传入的数组中以返回给调用方 - -```java -TreeNode KthNode(TreeNode pRoot, int k){ - if(pRoot == null){ - return null; - } - TreeNode[] res = new TreeNode[1]; - inOrder(pRoot, new int[]{ k }, res); - return res[0]; -} - -public void inOrder(TreeNode root, int[] count, TreeNode[] res){ - if(root == null){ - return; - } - inOrder(root.left, count, res); - count[0]--; - if(count[0] == 0){ - res[0] = root; - return; - } - inOrder(root.right, count, res); -} -``` - -> 如果可以利用我们熟知的算法,比如本题中的中序遍历。管它三七二十一先将熟知方法写出来,然后再按具体的业务需求对其进行改造(包括返回值、参数列表,但一般不会更改遍历算法的返回值) - -### 数据流的中位数 - -#### 题目描述 - -如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。 - -```java -public void Insert(Integer num) { - -} - -public Double GetMedian() { - -} -``` - -#### 解析 - -由于中位数只与排序后位于数组中间的一个数或两个数相关,而与数组两边的其它数无关,因此我们可以用一个大根堆保存数组左半边的数的最大值,用一个小根堆保存数组右半边的最小值,插入元素`O(logn)`,取中位数`O(1)`。 - -```java -public class Solution { - - //小根堆、大根堆 - PriorityQueue minHeap = new PriorityQueue(new MinRootHeadComparator()); - PriorityQueue maxHeap = new PriorityQueue(new MaxRootHeadComparator()); - int count = 0; - - class MaxRootHeadComparator implements Comparator{ - //返回值大于0则认为逻辑上i2大于i1(无关对象包装的数值) - public int compare(Integer i1, Integer i2){ - return i2.intValue() - i1.intValue(); - } - } - - class MinRootHeadComparator implements Comparator{ - public int compare(Integer i1, Integer i2){ - return i1.intValue() - i2.intValue(); - } - } - - public void Insert(Integer num) { - count++;//当前这个数是第几个进来的 - //编号是奇数就放入小根堆(右半边),否则放入大根堆 - if(count % 2 != 0){ - //如果要放入右半边的数比左半边的最大值要小则需调整左半边的最大值放入右半边并将当前这个数放入左半边,这样才能保证右半边的数都比左半边的大 - if(maxHeap.size() > 0 && num < maxHeap.peek()){ - maxHeap.add(num); - num = maxHeap.poll(); - } - minHeap.add(num); - }else{ - if(minHeap.size() > 0 && num > minHeap.peek()){ - minHeap.add(num); - num = minHeap.poll(); - } - maxHeap.add(num); - } - } - - public Double GetMedian() { - if(count == 0){ - return 0.0; - } - if(count % 2 != 0){ - return minHeap.peek().doubleValue(); - }else{ - return (minHeap.peek().doubleValue() + maxHeap.peek().doubleValue()) / 2; - } - } -} -``` - -### 滑动窗口的最大值 - -#### 题目描述 - -给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。 - -```java -public ArrayList maxInWindows(int [] num, int size){ - -} -``` - -#### 解析 - -使用一个单调非增队列,队头保存当前窗口的最大值,后面保存在窗口移动过程中导致队头失效(出窗口)后的从而晋升为窗口最大值的候选值。 - -```java -public ArrayList maxInWindows(int [] num, int size){ - ArrayList res = new ArrayList(); - if(num == null || num.length == 0 || size <= 0 || size > num.length){ - return res; - } - - //用队头元素保存窗口最大值,队列中元素只能是单调递减的,窗口移动可能导致队头元素失效 - LinkedList queue = new LinkedList(); - int start = 0, end = size - 1; - for(int i = start ; i <= end ; i++){ - addLast(queue, num[i]); - } - res.add(queue.getFirst()); - //移动窗口 - while(end < num.length - 1){ - addLast(queue, num[++end]); - if(queue.getFirst() == num[start]){ - queue.pollFirst(); - } - start++; - res.add(queue.getFirst()); - } - - return res; -} - -public void addLast(LinkedList queue, int num){ - if(queue == null){ - return; - } - //加元素之前要确保该元素小于等于队尾元素 - while(queue.size() != 0 && num > queue.getLast()){ - queue.pollLast(); - } - queue.addLast(num); -} -``` - -### 矩形中的路径 - -#### 题目描述 - -请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 - -#### 解析 - -定义一个黑盒`hasPathCorechar(matrix, rows, cols, int i, int j, str, index)`,表示从`rows`行`cols`列的矩阵`matrix`中的`(i,j)`位置开始走是否能走出一条与`str`的子串`index ~ str.length-1`相同的路径。那么对于当前位置`(i,j)`,需要关心的只有一下三点: - -1. `(i,j)`是否越界了 -2. `(i,j)`上的字符是否和`str[index]`匹配 -3. `(i,j)`是否已在之前走过的路径上 - -如果通过了上面三点检查,那么认为`(i,j)`这个位置是可以走的,剩下的就是`(i,j)`上下左右四个方向能否走出`str`的`index+1 ~ str.length-1`,这个交给黑盒就好了。 - -还有一点要注意,如果确定了可以走当前位置`(i,j)`,那么需要将该位置的`visited`标记为`true`,表示该位置在已走过的路径上,而退出`(i,j)`的时候(对应下面第`32`行)又要将他的`visited`重置为`false`。 - -```java -public boolean hasPath(char[] matrix, int rows, int cols, char[] str){ - if(matrix == null || matrix.length != rows * cols || str == null){ - return false; - } - boolean[] visited = new boolean[matrix.length]; - for(int i = 0 ; i < rows ; i++){ - for(int j = 0 ; j < cols ; j++){ - //以矩阵中的每个点作为起点尝试走出str对应的路径 - if(hasPathCore(matrix, rows, cols, i, j, str, 0, visited)){ - return true; - } - } - } - return false; -} - -//当前在矩阵的(i,j)位置上 -//index -> 匹配到了str中的第几个字符 -private boolean hasPathCore(char[] matrix, int rows, int cols, int i, int j, - char[] str, int index, boolean[] visited){ - if(index == str.length){ - return true; - } - //越界或字符不匹配或该位置已在路径上 - if(!match(matrix, rows, cols, i, j, str[index]) || visited[i * cols + j] == true){ - return false; - } - visited[i * cols + j] = true; - boolean res = hasPathCore(matrix, rows, cols, i + 1, j, str, index + 1, visited) || - hasPathCore(matrix, rows, cols, i - 1, j, str, index + 1, visited) || - hasPathCore(matrix, rows, cols, i, j + 1, str, index + 1, visited) || - hasPathCore(matrix, rows, cols, i, j - 1, str, index + 1, visited); - visited[i * cols + j] = false; - return res; -} - -//矩阵matrix中的(i,j)位置上是否是c字符 -private boolean match(char[] matrix, int rows, int cols, int i, int j, char c){ - if(i < 0 || i > rows - 1 || j < 0 || j > cols - 1){ - return false; - } - return matrix[i * cols + j] == c; -} -``` - -### 机器人的运动范围 - -#### 题目描述 - -地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? - -#### 解析 - -定义一个黑盒`walk(int threshold, int rows, int cols, int i, int j, boolean[] visited)`,它表示统计从`rows`行`cols`列的矩阵中的`(i,j)`开始所能到达的格子并返回,对于当前位置`(i,j)`有如下判断: - -1. `(i,j)`是否越界矩阵了 -2. `(i,j)`是否已被统计过了 -3. `(i,j)`的行坐标和列坐标的数位之和是否大于`k` - -如果通过了上面三重检查,则认为`(i,j)`是可以到达的(`res=1`),并标记`(i,j)`的`visited`为`true`表示已被统计过了,然后对`(i,j)`的上下左右的格子调用黑盒进行统计。 - -这里要注意的是,与上一题不同,`visited`不会在递归计算完子状态后被重置回`false`,因为每个格子只能被统计一次。`visited`的含义不一样 - -```java -public int movingCount(int threshold, int rows, int cols){ - if(threshold < 0 || rows < 0 || cols < 0){ - return 0; - } - boolean[] visited = new boolean[rows * cols]; - return walk(threshold, rows, cols, 0, 0, visited); -} - -private int walk(int threshold, int rows, int cols, int i, int j, boolean[] visited){ - if(!isLegalPosition(rows, cols, i, j) || visited[i * cols + j] == true - || bitSum(i) + bitSum(j) > threshold){ - return 0; - } - int res = 1; - visited[i * cols + j] = true; - res += walk(threshold, rows, cols, i + 1, j, visited) + - walk(threshold, rows, cols, i - 1, j, visited) + - walk(threshold, rows, cols, i, j + 1, visited) + - walk(threshold, rows, cols, i, j - 1, visited); - return res; -} - -private boolean isLegalPosition(int rows, int cols, int i, int j){ - if(i < 0 || j < 0 || i > rows - 1 || j > cols - 1){ - return false; - } - return true; -} - -public int bitSum(int num){ - int res = num % 10; - while((num /= 10) != 0){ - res += num % 10; - } - return res; -} -``` - From e4cd1a54bbaf495e292e265d494bb0bea11d7fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 16:36:23 +0800 Subject: [PATCH 28/97] =?UTF-8?q?Create=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 5611 ++++++++++++++++++++++++ 1 file changed, 5611 insertions(+) create mode 100644 "docs/Java\345\271\266\345\217\221.md" diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" new file mode 100644 index 00000000..7dd275c3 --- /dev/null +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -0,0 +1,5611 @@ +并发框架 +Doug Lea +如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。 +说他是这个世界上对Java影响力最大的个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。 +值得一提的是: Doug Lea也是JCP (Java社区项目)中的一员。 +Doug是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出不一样的火花。《Effective JAVA》这本Java经典之作的作者Joshua Bloch便在书中特别感谢Doug Lea是此书中许多构想的共鸣板,感谢Doug Lea大方分享丰富而又宝贵的知识。 + +线程 +线程的状态 +线程的几种实现方式 + +三个线程轮流打印ABC十次 +判断线程是否销毁 +yield功能 +给定三个线程t1,t2,t3,如何保证他们依次执行 +基本概念 + + + + +线程的启动 +1)实现Runnable接口 +1.自定义一个线程,实现Runnable接口的run方法 +run方法就是要执行的内容,会在另一个分支上进行 +Thread类本身也实现了Runnable接口 +2.主方法中new一个自定义线程对象,然后new一个Thread类对象,其构造方法的参数是自定义线程对象 +3.执行Thread类的start方法,线程开始执行 +自此产生了分支,一个分支会执行run方法,在主方法中不会等待run方法调用完毕返回才继续执行,而是直接继续执行,是第二个分支。这两个分支并行运行 + +这里运用了静态代理模式: +Thread类和自定义线程类都实现了Runnable接口 +Thread类是代理Proxy,自定义线程类是被代理类 +通过调用Thread的start方法,实际上调用了自定义线程类的start方法(当然除此之外还有其他的代码) +2)继承Thread类 +①自定义一个类MyThread,继承Thread类,重写run方法 +②在main方法中new一个自定义类,然后直接调用start方法 +两个方法比较而言第二个方法代码量较少 +但是第一个方法比较灵活,自定义线程类还可以继承其他的类,而不限于Thread类 +3)实现Callable接口 + + + +线程的状态 +初始态:NEW +创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。 + +运行态:RUNNABLE +在Java中,运行态包括就绪态 和 运行态。 + +就绪态 READY +该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 +所有就绪态的线程存放在就绪队列中。 + +运行态 RUNNING +获得CPU执行权,正在执行的线程。 +由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。 + +阻塞态 BLOCKED +阻塞态专指请求排它锁失败时进入的状态。 + +等待态 WAITING +当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 +进入等待态的线程会释放CPU执行权,并释放资源(如:锁),它们要等待被其他线程显式地唤醒。 + +超时等待态 TIME_WAITING +当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态; +进入该状态后释放CPU执行权 和 占有的资源。 +与等待态的区别:无需等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。 + +终止态 +线程执行结束后的状态。 + +线程的方法 + +getName +Thread类的构造方法1 + +Thread类的构造方法2 + +new 一个子类对象的同时也new了其父类的对象,只是如果不显式调用父类的构造方法super(),那么会自动调用无参数的父类的构造方法。 +可以在自定义类MyThread中(继承自Thread类)中写一个构造方法,显式调用父类的构造方法,其参数为一个字符串,表示创建一个以该字符串为名字的Thread对象。 +效果是创建了一个MyThread对象,并且其父类Thread对象的名字是给定的字符串。 +如果不显式调用父类的构造方法super(参数),那么默认父类Thread是没有名字的。 +isAlive +isAlive活着的定义是就绪、运行、阻塞状态 +线程是有优先级的,优先级高的获得Cpu执行时间长,并不代表优先级低的就得不到执行 +sleep(当前线程.sleep) +sleep时持有的锁不会自动释放,sleep时可能会抛出InterruptedException。 +Thread.sleep(long millis) +一定是当前线程调用此方法,当前线程进入TIME_WAIT状态,但不释放对象锁,millis后线程自动苏醒进入READY状态。作用:给其它线程执行机会的最佳方式。 +join(其他线程.join) +t.join()/t.join(long millis) +当前线程里调用线程1的join方法,当前线程进入WAIT状态,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。 +join方法的作用是将分出来的线程合并回去,等待分出来的线程执行完毕后继续执行原有线程。类似于方法调用。(相当于调用thead.run()) +yield(当前线程.yield) +Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。 +interrupt(其他线程.interrupt) +调用Interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志; +中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简单来说就是由其他线程通过执行interrupt方法对该线程打个招呼,让起中断标志位为true,从而实现中断线程执行的目的。 + +其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用Thread.currentThread().isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。 + +那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后抛出InterruptedException,这个时候调用isInterrupted方法返回的也是false。 + +interrupt一个其他线程t时 +1)如果线程t中调用了可以抛出InterruptedException的方法,那么会在t中抛出InterruptedException并清除中断标志位。 +2)如果t没有调用此类方法,那么会正常地将设置中断标志位。 + +如何停止线程? +1)在catch InterruptedException异常时可以关闭当前线程; +2)循环调用isInterrupted方法检测是否被中断,如果被中断,要么调用interrupted方法清除中断标志位,要么就关闭当前线程。 +3)无论1)还是2),都可以通过一个volatile的自定义标志位来控制循环是否继续执行 + +但是注意! +如果线程中有阻塞操作,在阻塞时是无法去检测中断标志位或自定义标志位的,只能使用1)的interrupt方法才能中断线程,并且在线程停止前关闭引起阻塞的资源(比如Socket)。 + + + +wait(对象.wait) +调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。 +obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。 +调用wait()方法的线程,如果其他线程调用该线程的interrupt()方法,则会重新尝试获取对象锁。只有当获取到对象锁,才开始抛出相应的InterruptedException异常,从wait中返回。 +notify(对象.notify) +obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。 +wait¬ify 最佳实践 +等待方(消费者)和通知方(生产者) +等待方: +synchronized(obj){ + while(条件不满足){ + obj.wait(); +} +消费; +} + +通知方: +synchonized(obj){ + 改变条件; + obj.notifyAll(); +} + +1)条件谓词: +将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档。 +在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。 +当线程从wait方法中被唤醒时,它在重新请求锁时不具有任何特殊的优先性,而要去其他尝试进入同步代码块的线程一起正常地在锁上进行竞争。 +每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。 + +2)过早唤醒: +虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但wait方法的返回并不一定意味着线程正在等待的条件谓词已经变成真了。 +当执行控制重新进入调用wait的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变成假。在线程被唤醒到wait重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用wait起根本就没有变成真。你并不知道另一个线程为什么调用notify或notifyAll,也许是因为与同一条件队列相关的另一个条件谓词变成了真。一个条件队列与多个条件谓词相关是一种很常见的情况。 +基于所有这些原因,每当线程从wait中唤醒时,都必须再次测试条件谓词。 + +3)notify与notifyAll: +由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号地址(线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词)的问题。 + +只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll: +1)所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。 +2)单进单出:在对象状态上的每次改变,最多只能唤醒一个线程来执行。 + +suspend resume stop destroy(废弃方法) +线程的暂停、恢复、停止对应的就是suspend、resume和stop/destroy。 +suspend会使当前线程进入阻塞状态并不释放占有的资源,容易引起死锁; +stop在结束一个线程时不会去释放占用的资源。它会直接终止run方法的调用,并且会抛出一个ThreadDeath错误。 +destroy只是抛出一个NoSuchMethodError。 +suspend和resume已被wait、notify取代。 + +线程的优先级 + +判断当前线程是否正在执行 +注意优先级是概率而非先后顺序(优先级高可能会执行时间长,但也不一定) + +线程优先级特性: +继承性 +比如A线程启动B线程,则B线程的优先级与A是一样的。 +规则性 +高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。 +随机性 +优先级较高的线程不一定每一次都先执行完。 +注意,在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。 +守护线程 + +将线程转换为守护线程 +守护线程的唯一用途是为其他线程提供服务。比如计时线程,它定时发送信号给其他线程; +当只剩下守护线程时,JVM就退出了。 +守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。 +注意!Java虚拟机退出时Daemon线程中的finally块并不一定会被执行。 + + +未捕获异常处理器 +在Runnable的run方法中不能抛出异常,如果某个异常没有被捕获,则会导致线程终止。 + +要求异常处理器实现Thread.UncaughtExceptionHandler接口。 +可以使用setUncaughtExceptionHandler方法为任何一个线程安装一个处理器, +也可以使用Thread.setDefaultUncaughtExceptionHandler方法为所有线程安装一个默认的处理器; + +如果不安装默认的处理器,那么默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象 +ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作: +1)如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。 +2)否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。 +3)否则,如果Throwable是ThreadDeath的一个实例(ThreadDeath对象由stop方法产生,而该方法已过时),什么都不做。 +4)否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。 + +如果是由线程池ThreadPoolExecutor执行任务,只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出的未检测异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。 + + +并发编程的问题 +线程引入开销:上下文切换与内存同步 +使用多线程编程时影响性能的首先是线程的上下文切换。每个线程占有一个CPU的时间片,然后会保存该线程的状态,切换到下一个线程执行。线程的状态的保存与加载就是上下文切换。 +减少上下文切换的方法有:无锁并发编程、CAS、使用最少线程、协程。 +1)无锁并发:通过某种策略(比如hash分隔任务)使得每个线程不共享资源,避免锁的使用。 +2)CAS:是比锁更轻量级的线程同步方式 +3)避免创建不需要的线程,避免线程一直处于等待状态 +4)协程:单线程实现多任务调度,单线程维持多任务切换 + +vmstat可以查看上下文切换次数 +jstack 可以dump 线程信息,查看一个进程中各个线程的状态 + +内存同步:在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏。内存栅栏可以刷新缓存,使缓存失效,刷新硬件的写缓冲,以及停止执行管道。内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序。 +不要担心非竞争同步带来的开销,这个基本的机制已经非常快了,并且JVM还能进行额外的优化以进一步降低或消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。 + +死锁 +死锁后会陷入循环等待中。 +如何避免死锁? +1)避免一个线程同时获取多个锁 +2)避免一个线程在锁内占用多个资源,尽量保证每个锁只占用一个资源 +3)尝试使用定时锁tryLock替代阻塞式的锁 +4)对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会解锁失败 + + + +线程安全性(原子性+可见性) +1、对象的状态:对象的状态是指存储在状态变量中的数据,对象的状态可能包括其他依赖对象的域。在对象的状态中包含了任何可能影响其外部可见行为的数据。 + +2、一个对象是否是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。同步机制包括synchronized、volatile变量、显式锁、原子变量。 + +3、有三种方式可以修复线程安全问题: +1)不在线程之间共享该状态变量 +2)将状态变量修改为不可变的变量 +3)在访问状态变量时使用同步 + +4、线程安全性的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 + +5、无状态变量一定是线程安全的,比如局部变量。 + +6、读取-修改-写入操作序列,如果是后续操作是依赖于之前读取的值,那么这个序列必须是串行执行的。在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它称为竞态条件(Race Condition)。最常见的竞态条件类型就是先检查后执行的操作,通过一个可能失效的观测结果来决定下一步的操作。 + +7、复合操作:要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。假定有两个操作A和B,如果从执行A的线程看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。原子操作是指,对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的操作。 +为了确保线程安全性,读取-修改-写入序列必须是原子的,将其称为复合操作。复合操作包含了一组必须以原子方式执行的接口以确保线程安全性。 + +8、在无状态的类中添加一个状态时,如果这个状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。(比如原子变量) + +9、如果多个状态是相关的,需要同时被修改,那么对多个状态的操作必须是串行的,需要进行同步。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。 + +10、内置锁:synchronized(object){同步块} +Java的内置锁相当于一种互斥体,这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等待下去。 + +11、重入:当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置1。如果一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数值会相应递减。当计数值为0时,这个锁将被释放。 + +12、对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。 +每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。 +一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并提供对象的内置锁(this)对所有访问可变状态的代码路径进行同步。在这种情况下,对象状态中的所有变量都由对象的内置锁保护起来。 + +13、不良并发:要保证同步代码块不能过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。 + +14、可见性:为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。 + +15、加锁与可见性:当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步,那么就无法实现上述保证。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。 + +16、volatile变量:当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他对处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。volatile的语义不足以确保递增操作的原子性,除非你能确保只有一个线程对变量执行写操作。原子变量提供了“读-改-写”的原子操作,并且常常用做一种更好的volatile变量。 + +17、加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。 + +18、当且仅当满足以下的所有条件时,才应该使用volatile变量: +1)对变量的写入操作不依赖变量的当前值(不存在读取-判断-写入序列),或者你能确保只有单个线程更新变量的值。 +2)该变量不会与其他状态变量一起纳入不可变条件中 +3)在访问变量时不需要加锁 + +19、栈封闭:在栈封闭中,只能通过局部变量才能访问对象。维护线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程的某个值与保存值的对象关联起来,ThreadLocal通过了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。 + +20、在并发程序中使用和共享对象时,可以使用一些使用的策略,包括: +1)线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。 +2)只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象(从技术上来说是可变的,但其状态在发布之后不会再改变)。 +3)线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。 +4)保护对象。被保护的对象只能通过持有对象的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布并且由某个特定锁保护的对象。 + +21、饥饿:当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿(某线程永远等待)。引发饥饿的最常见资源就是CPU时钟周期。比如线程的优先级问题。在Thread API中定义的线程优先级只是作为线程调度的参考。在Thread API中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射是与特定平台相关的,因此在某个操作系统中两个不同的Java优先级可能被映射到同一优先级,而在另一个操作系统中则可能被映射到另一个不同的优先级。 +当提高某个线程的优先级时,可能不会起到任何作用,或者也可能使得某个线程的调度优先级高于其他线程,从而导致饥饿。 +通常,我们尽量不要改变线程的优先级,只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。 + +事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T的请求......T2可能永远等待 + +22、活锁 +活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发生在处理事务消息的应用程序中。如果不能成功处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。虽然处理消息的线程并没有阻塞,但也无法继续执行下去。这种形式的活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。 + +当多个相互协作的线程都对彼此进行响从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。要解决这种活锁问题,需要在重试机制中引入随机性。在并发应用程序中,通过等待随机长度的时间和回退可以有效地避免活锁的发生。 + +23、当在锁上发生竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以采用自旋等待(Spin-Waiting,指通过循环不断地尝试获取锁,直到成功),或者通过操作系统挂起被阻塞的线程。这两种方式的效率高低,取决于上下文切换的开销以及在成功获取锁之前需要等待的时间。如果等待时间较短,则适合采用自旋等待的方式,而如果等待时间较长,则适合采用线程挂起方式。 + +24、有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。如果二者的乘积很小,那么大多数获取锁的操作都不会发生竞争,会因此在该锁上的竞争不会对可伸缩性造成严重影响。然而,如果在锁上的请求量很高,那么需要获取该锁的线程将被阻塞并等待。在极端情况下,即使仍有大量工作等待完成,处理器也会被闲置。 +有3种方式可以降低锁的竞争程度: +1)减少锁的持有时间: +缩小锁的范围,将与锁无关的代码移出同步代码块,尤其是开销较大的操作以及可能被阻塞的操作(IO操作)。 +当把一个同步代码块分解为多个同步代码块时,反而会对性能提升产生负面影响。在分解同步代码块时,理想的平衡点将与平台相关,但在实际情况中,仅可以将一些大量的计算或阻塞操作从同步代码块移出时,才应该考虑同步代码块的大小。 +减小锁的粒度:锁分解和锁分段 +锁分解是采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况。这些技术能减小锁操作的粒度,并能实现更高的可伸缩性,然而,使用的锁越多,那么发生死锁的风险也就越高。 +锁分段:比如JDK1.7及之前的ConcurrentHashMap采用的方式就是分段锁的方式。 +2)降低锁的请求频率 +3)使用带有协调机制的独占锁,这些机制允许更高的并发性 +比如读写锁,并发容器等 + + + +线程间通信/线程同步 工具使用 +synchronized +synchronized锁定的是对象而非代码,所处的位置是代码块或方法 +一种使用方法是对代码块使用synchronized关键字 +public void fun(){ + synchronized (this){ } +} +括号中锁定的是普通对象或Class对象 +如果是this,表示在执行该代码块时锁定当前对象,其他线程不能调用该对象的其他锁定代码块,但可以调用其他对象的所有方法(包括锁定的代码块),也可以调用该对象的未锁定的代码块或方法。 +如果是Object o1,表示执行该代码块的时候锁定该对象,其他线程不能访问该对象(该对象是空的,没有方法,自然不能调用) +如果是类.class,那么锁住了该类的Class对象,只对静态方法生效。 + +另一种写法是将synchronized作为方法的修饰符 +public synchronized void fun() {} //这个方法执行的时候锁定该当前对象 +每个类的对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的一个对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。 + +如果synchronized修饰的是静态方法,那么锁住的是这个类的Class对象,没有其他线程可以调用该类的这个方法或其他的同步静态方法。 + +实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的这个代码段。 +synchronized锁住的是括号里的对象,而不是代码。对于非静态的synchronized方法,锁的就是对象本身也就是this。 + +获取锁的线程释放锁只会有两种情况: +1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; +2)线程执行发生异常,此时JVM会让线程自动释放锁。 + + +Lock +锁是可重入的(reentrant),因为线程可以重复获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。 +public class TestReentrantLock { + public static void main(String[] args) { + Ticket ticket = new Ticket(); + new Thread(ticket, "一号窗口").start(); + new Thread(ticket, "二号窗口").start(); + new Thread(ticket, "三号窗口").start(); + } +} + +class Ticket implements Runnable { + private int tickets = 100; + private Lock lock = new ReentrantLock(); + + @Override + public void run() { + while (true) { + lock.lock(); + try { + Thread.sleep(50); + if(tickets > 0){ + System.out.println(Thread.currentThread().getName() + "正在售票,余票为:" + (--tickets)); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + } +} + + + +volatile +(作用是为成员变量的同步访问提供了一种免锁机制,如果声明一个成员变量是volatile的,那么会通知编译器和虚拟机这个成员变量是可能其他线程并发更新的 +对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。 + +Java内存模型简要介绍(后面会详细介绍): +多线程环境下,会共享同一份数据(线程公共的内存空间)。为了提高效率,JVM会为每一个线程设置一个线程私有的内存空间(线程工作内存),并将共享数据拷贝过来。写操作实际上写的是线程私有的数据。当写操作完毕后,将线程私有的数据写回到线程公共的内存空间。 +如果在写回之前其他线程读取该数据,那么返回的可能是修改前的数据,视读取线程的执行效率而定。 +jvm运行时刻内存的分配:其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,(从线程内存中读值) +在修改完之后的某一个时刻(线程退出之前),把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 +) + +final修饰的变量是线程安全的 + +内存可见性问题是,当多个线程操作共享数据时,彼此不可见。 +解决这个问题有两种方法: +1、加锁:加锁会保证读取的数据一定是写回之后的,内存刷新。但是效率较低 +2、volatile:会保证数据在读操作之前,上一次写操作必须生效,即写回。 +1)修改volatile变量时会强制将修改后的值刷新到主内存中。 +2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。 + 相较于synchronized是一种较为轻量级的同步策略,但是volatile不具备互斥性;不能保证修改变量时的原子性。 +public class TestVolatile { + public static void main(String[] args) { + MyThread myThread = new MyThread(); + new Thread(myThread).start(); + while(true){ + synchronized (myThread) { + if(myThread.isFlag()){ + System.out.println("flag被设置为true"); + break; + } + } + } + } +} + +class MyThread implements Runnable{ + private volatile boolean flag = false; + public boolean isFlag() { + return flag; + } + public void setFlag(boolean flag) { + this.flag = flag; + } + @Override + public void run() { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + flag = true; + System.out.println("flag="+flag); + } +} + +Atomic +原子变量 +可以实现原子性+可见性 + + + +Lock使用 深入 +可重入锁 ReentrantLock +在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,绑定多个条件以及非块结构的锁。否则,还是应该优先使用synchronized。 + +1)可中断:lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。 +2)可定时:tryLock(time) +3)可轮询:tryLock() +4)可公平:公平锁与非公平锁 +5)绑定多个条件:一个锁可以对应多个条件,而Object锁只能对应一个条件 +6)非块结构:加锁与解锁不是块结构的 + +Condition(与wait¬ify区别) +BlockingQueue就是基于Condition实现的。 + +一个Condition对象和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样。要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。 + +Condition与wait¬ify区别 + +1)Condition比内置条件等待队列提供了更丰富的功能:在每个锁上可存在 可不响应中断、可等待至某个时间点、可公平的队列操作。 +wait¬ify一定响应中断并抛出遗产;Condition可以响应中断也可以不响应中断 + +2)与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象。 +await() awaitUninterruptibly() await(time) +Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会按照FIFO顺序从Condition.await中释放。 + + +await&signal +await被中断会抛出InterruptedException。 + +Condition区分开了不同的条件谓词,更容易满足单次通知的需求。signal比signalAll更高效,它能极大地减少在每次缓存操作中发生的上下文切换与锁请求的次数。 + +线程进入临界区(同步块)时,发现必须要满足一定条件才能执行。要使用一个条件对象来管理那些已经获得一个锁但是不能做有用工作的线程 +条件对象也称为条件变量 +一个锁对象可以有多个相关的条件对象,newCondition方法可以获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。 +当发现条件不满足时,调用Condition对象的await方法 +此时线程被阻塞,并放弃了锁。等待其他线程的相关操作使得条件达到满足 + +等待获得锁的线程和调用await方法的线程有本质区别。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞,相反,它处于阻塞状态,直到另一个线程调用同一个条件的signalAll方法为止 +await方法和signalAll方法是配套使用的 +await进入等待,signalAll解除等待 + +signalAll方法会重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的,线程调度器将再次激活它们。同时它们将试图重新进入该对象。一旦锁可用,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行 + +线程应该再次测试该条件。由于无法确保该条件被满足,signalAll方法仅仅是通知正在等待的线程,此时有可能满足条件,值得再次去检测该条件 +对于await方法的调用应该用在这种形式: +while(!(ok to continue)){ + condition.await(); +} +最重要的是需要其他某个线程调用signalAll方法。当一个线程调用await方法,它没有办法去激活自身,只能寄希望于其他线程。如果没有其他线程来激活等待的线程,那么就会一直等待,出现死锁。 +如果所有其他线程都被阻塞,且最后一个线程也调用了await方法,那么它也被阻塞,此时程序挂起。 + +signalAll方法不会立刻激活一个等待的线程,仅仅是解除等待线程的阻塞,以便这些线程可以在当前线程(调用signalAll方法的线程)退出时,通过竞争来实现对对象的方法 +这个await和signalAll方法的组合类似于Object对象的wait和notifyAll方法的组合 + +public class ConditionBoundedBuffer { + private static final int BUFFER_SIZE = 20; + protected final Lock lock = new ReentrantLock(); + private final Condition notFull = lock.newCondition(); + + private final Condition notEmpty = lock.newCondition(); + + private final T[] items = (T[]) new Object[BUFFER_SIZE]; + private int tail; + private int head; + private int count; + + public void put(T t) throws InterruptedException { + lock.lock(); + try { + while(count == items.length){ + notFull.await(); + } + items[tail] = t; + if(++tail == items.length){ + tail = 0; + } + ++count; + notEmpty.signal(); + } finally { + lock.unlock(); + } + } + + public T take() throws InterruptedException { + lock.lock(); + try{ + while(count == 0){ + notEmpty.await(); + } + T t = items[head]; + items[head] = null; + if(++head == items.length){ + head = 0; + } + --count; + notFull.signal(); + return t; + }finally { + lock.unlock(); + } + } +} + + + + +公平锁 +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} + +在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许‘插队’:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 +非公平的ReentrantLock 并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。 + +非公平锁性能高于公平锁性能的原因: +在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。 + +假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。 + +当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。 + +非公平锁可能会引起线程饥饿,但是线程切换更少,吞吐量更大 + + +读写锁 ReentrantReadWriteLock +读写锁是一种性能优化措施,在一些特定的情况下能实现更高的并发性。在实际情况中,对于在多处理器系统上被频繁读取的数据结构,读写锁能够提高性能。而在其他情况下,读写锁的性能比独占锁的性能要略差一些,这是因为它们的复杂性更高。如果要判断在某种情况下使用读写锁是否会带来性能提升,最好对程序进行分析。由于ReadWriteLock使用Lock来实现锁的读写部分,因此如果分析结果表明读写锁没有提高性能,那么可以很容易地将读写锁换为独占锁。 + +ReentrantReadWriteLock 如果很多线程从一个数据结构中读取数据而很少线程修改其中数据,那么允许对读的线程共享访问是合适的。 + +读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁! + +特性: + (a).重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。 + (b).写锁可以降级为读锁,顺序是:先获得写锁再获得读锁,然后释放写锁,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不可能,为什么?参看(a) + (c) 读锁不能升级为写锁,目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。 + (d).读锁可以被多个线程持有并且在作用时排斥任何的写锁,而写锁则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 + (e).不管是读锁还是写锁都支持Interrupt,语义与ReentrantLock一致。 + (f).写锁支持Condition并且与ReentrantLock语义一致,而读锁则不能使用Condition,否则抛出UnsupportedOperationException异常。 + +public class TestReadWriteLock { + public static void main(String[] args) { + ReadWriteLockDemo demo = new ReadWriteLockDemo(); + for (int i = 0; i < 100; ++i) { + new Thread(new Runnable() { + + @Override + public void run() { + demo.get(); + } + }, "Read" + i).start(); + } + new Thread(new Runnable() { + + @Override + public void run() { + demo.set(222); + } + }, "Write").start(); + + } +} + +class ReadWriteLockDemo { + private ReadWriteLock lock = new ReentrantReadWriteLock(); + private int data = 2; + + public void get() { + lock.readLock().lock(); + try { + System.out.println("读操作 " + Thread.currentThread().getName() + ":" + data); + } finally { + lock.readLock().unlock(); + } + } + + public void set(int data) { + lock.writeLock().lock(); + try { + System.out.println("写操作 " + Thread.currentThread().getName() + ":" + data); + this.data = data; + } finally { + lock.writeLock().unlock(); + } + } +} + +LockSupport(锁住的是线程,synchronized锁住的是对象) +当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。 +LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(thread)方法来唤醒一个被阻塞的线程。 + +park等方法还可以传入阻塞对象,有阻塞对象的park方法在dump线程时可以给开发人员更多的现场信息。 + +park对于中断只会设置中断标志位,不会抛出InterruptedException。 +LockSupport是可不重入的,如果一个线程连续2次调用 LockSupport .park(),那么该线程一定会一直阻塞下去 +unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。 + +LockSupport.park()和unpark(),与object.wait()和notify()的区别? +主要的区别应该说是它们面向的对象不同。阻塞和唤醒是对于线程来说的,LockSupport的park/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。 +park/unpark模型真正解耦了线程之间的同步。线程之间不再须要一个Object或者其他变量来存储状态。不再须要关心对方的状态。 +synchronized与Lock的区别 +1)层次:前者是JVM实现,后者是JDK实现 +2)功能:前者仅能实现互斥与重入,后者可以实现 可中断、可轮询、可定时、可公平、绑定多个条件、非块结构 +synchronized在阻塞时不会响应中断,Lock会响应中断,并抛出InterruptedException异常。 +3)异常:前者线程中抛出异常时JVM会自动释放锁,后者必须手工释放 +4)性能:synchronized性能已经大幅优化,如果synchronized能够满足需求,则尽量使用synchronized +原子操作类使用 +1、近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(compareAndSwap)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于在操作系统和JVM中实现进程/线程调度机制、垃圾回收机制以及锁和其他并发数据结构。 +非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。非阻塞算法常见应用是原子变量类。 +即使原子变量没有用于非阻塞算法的开发,它们也可以用作一个更好的volatile类型变量。原子变量提供了与volatile类型变量相同的内存语义,此外还支持原子的更新操作,从而使它们更加适用于实现计数器、序列发生器和统计数据收集等,同时还能比基于锁的方法提供更高的可伸缩性。 + +2、锁的缺点: +1)在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。 +2)volatile变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的操作。 +3)当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。 +4)总之,锁定方式对于细粒度的操作(比如递增计数器)来说仍然是一种高开销的机制。在管理线程之间的竞争应该有一种粒度更细的技术,比如CAS。 + +3、独占锁是一种悲观技术。对于细粒度的操作,还有另外一个更高效的办法,也是一种乐观的办法,通过这种方法可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试。在针对多处理器操作而设计的处理器中提供了一些特殊的指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-increment)以及交换(swap)指令。现在几乎所有的现代处理器都包含了某种形式的原子读-改-写指令,比如比较并交换(compare-and-swap)等。 + +4、CAS包含了三个操作数——需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会以原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。 +CAS的含义是:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。 + + +上面这段代码模拟了CAS操作(但实际上不是基于synchronized实现的原子操作,而是由操作系统支持的)。 +当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会被阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。这种灵活性就大大减少了与锁相关的活跃性风险。 + +5、基于CAS实现的非阻塞计数器 + + +6、初看起来,基于CAS的计数器似乎比基于锁的计数器在性能上更差一些,因为它需要执行更多的操作和更复杂的控制流,并且还依赖看似复杂的CAS操作。但实际上,当竞争程序不高时,基于CAS的计数器在性能上远远超过了基于锁的计数器,而在没有竞争时甚至更高。如果要快速获取无竞争的锁,那么至少需要一次CAS操作再加上与其他锁相关操作,因此基于锁的计数器即使在更好的情况下也会比基于CAS的计数器在一般情况下能执行更多的操作。 +CAS的主要缺点是,它将使调用者处理竞争问题,而在锁中能自动处理竞争问题。 + +7、原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最细的情况。更新原子变量的快速路径不会比获取锁的路径慢,并且通常会更快,而它的慢速路径肯定比锁的慢速路径快,因为它不需要挂起或重新调度线程。 +原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。 +共用12个原子变量类,可分为4组:标量类、更新器类、数组类以及复合变量类。原子数组类中的元素可以实现原子更新。 +原子数组类中的元素可以实现原子更新。原子数组类为数组的元素提供了volatile类型的访问语义,这是普通数组锁不具备的特性——volatile类型的数组仅在数组引用上具有volatile语义,而在其元素上则没有。 + + + +8、ABA问题 +有时候还需要知道“自从上次看到V的值为A以来,这个值是否发生了变化”,在某些算法中,如果V的值首先由A变为B,再由B变为A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。 +有一个相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,然后又变为A,版本号也将是不同的。AtomicStampedReference支持在两个变量上执行原子的条件更新。 +AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上:“版本号”,从而避免ABA问题。类似地,AtomicMarkableRefernce将更新一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的节点”。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作(变量合并,AtomicReference)三个问题。 + + +Java内存模型 线程同步工具原理 +JMM Java Memory Model +JMM抽象结构 +在Java中,堆内存在线程之间共享,线程之间的通信由Java内存模型JMM控制。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储了线程读写共享变量的副本。 + + +如果线程A与线程B之间要通信的话,必须要经历下面2个步骤: +1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。 +2)线程B到主内存中去读取线程A之前已更新过的共享变量。 + +指令重排序 +在执行程序时,为了提高性能,编译器和CPU常常会对指令进行重排序,分为以下3种类型: +1、编译优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。 +2、指令级并行的重排序。CPU采用了指令级并行技术将多条指令重叠执行。 +3、内存系统的重排序。由于CPU使用cache和读/写缓冲区,因此加载和存储操作可能在乱序执行。 + + +1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。 +对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。 +对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。 +内存屏障 +JMM把内存屏障分为四类: +LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 +StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 +LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 +StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 +happens-before(抽象概念,基于内存屏障) +JDK1.5后,Java采用JSR133内存模型,通过happens-before概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果要对另一个操作可见,那么这两个操作之间必须要有happens-before关系。 + +定义: +1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 +2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。 + +上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证! + +上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序)编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。 + +与程序员密切相关的happens-before规则如下。 +1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。(单线程顺序执行) +2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。(先解锁后加锁) +比如: +lock.unlock(); +lock.lock(); +那么不会重排序,因为重排序后肯定会发生死锁 + +3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。(先写后读) +4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 +5)start规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start() happens-before于线程B的任意操作。 +6)join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 + +happens-before与JMM的关系如下: + + +指令重排序 +重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 +数据依赖性 +对于单个CPU和单个线程中所执行的操作而言,如果两个操作都访问了同一个变量,且两个操作中有写操作,那么这两个操作就具有数据依赖性。 +(RW,WW,WR)这三种操作只要重排序对操作的执行顺序,程序的执行结果就会被改变,因此,编译器和处理器在进行重排序的时候会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。 +as-if-serial +as-if-serial:无论如何重排序,(单线程)程序的执行结果不能被改变。 +编译器,runtime,CPU都必须遵守as-if-serial语义,因此,编译器和CPU不会对存在数据依赖关系的操作进行重排序。 + +在单线程中,对存在控制依赖性的操作进行重排序,不会改变执行结果,而在多线程中则可能会改变结果。 +顺序一致性 +程序未正确同步的时候,就可能存在数据竞争。 +数据竞争: +1)在一个线程中写一个变量 +2)在另一个线程中读同一个变量 +3)而且写和读没有通过同步来排序。 + +JMM对正确同步的多线程程序的内存一致性做了如下保证: +如果程序是正确同步的,程序的执行将具有顺序一致性——即程序的执行结果与该程序的顺序一致性内存模型的执行结果相同。 +顺序一致性模型有两大特性: +1)一个线程中的所有操作必须按照程序的顺序来执行 +2)不管程序是否同步,所有线程都只能看到单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。 + +JMM中,临界区内的代码可以重排序。 + +而对于未正确同步的多线程程序,JMM只提供最小的安全性:线程执行时所读取到的值,要么是之前某个线程所写入的值,要么是默认值。 +volatile原理 +汇编上的实现 +volatile修饰的共享变量在转换为汇编语言后,会出现Lock前缀指令,该指令在多核处理器下引发了两件事: +1、将当前处理器缓存行(CPU cache中可以分配的最小存储单位)的数据写回到系统内存。 + +2、这个写回内存的操作使得其他CPU里缓存了该内存地址的数据无效。 +(当前CPU该变量的缓存回写;其他CPU该变量的缓存失效) +内存语义 +volatile写的内存语义: +当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存 +volatile读的内存语义: +当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量 + + +一个volatile变量的单个读写操作,与一个普通变量的读写操作使用同一个锁来同步,它们的执行效果相同。锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这也意味着对一个volatile变量的读操作,总是能看到任意线程对该变量最后的写入。 + +对于volatile变量本身的单个读写操作具有原子性,但是与锁不同的是,多个对于volatile变量的复合操作不具有原子性。而锁的语义保证了临界区代码的执行具有原子性。 + +JAVA1.5后,JSR-133增强了volatile的内存语义,严格限制编译器和CPU对于volatile变量与普通变量的重排序,从而确保volatile变量的写-读操作可以实现线程之间的通信,提供了一种比锁更轻量级的线程通信机制。从内存语义的角度而言,volatile的写-读与锁的释放-获取有相同的内存效果:写操作=锁的释放;读操作=锁的获取。 + +A线程写一个volatile变量x后,B线程读取x以及其他共享变量。 + +1. 当A线程对x进行写操作时,JMM会把该线程A对应的cache中的共享变量值刷新到主存中.(实质上是线程A向接下来要读变量x的线程发出了其对共享变量修改的消息) + +2.当B线程对x进行读取时,JMM会把该线程对应的cache值设置为无效,而从主存中读取x。(实质上是线程B接收了某个线程发出的对共享变量修改的消息) + +两个步骤综合起来看,在线程B读取一个volatile变量x后,线程A本地cache中在写这个变量x之前所有其他可见的共享变量的值都立即变得对B可见。线程A写volatile变量x,B读x的过程实质上是线程A通过主存向B发送消息。 + +需要注意的是,由于volatile仅仅保证对单个volatile变量的读写操作具有原子性,而锁的互斥则可以确保整个临界区代码执行的原子性。 + +内存语义的实现(内存屏障) +在每个volatile写操作的前面插入一个StoreStore屏障 +在每个volatile写操作的后面插入一个StoreLoad屏障 +在每个volatile读操作的后面插入一个LoadLoad屏障 +在每个volatile读操作的后面插入一个LoadStore屏障 + +StoreStore屏障;(禁止上面的普通写和下面的volatile写重排序,保证上面所有的普通写在volatile写之前刷新到主内存) +volatile写; +StoreLoad屏障;(禁止上面的volatile写和下面的volatile读/写重排序) + +volatile读; +LoadLoad屏障; +LoadStore屏障; + +从汇编入手分析volatile多线程问题 +1、普通方式int i,执行i++: + +普通方式没有任何与锁有关的指令;其他方式都出现了与锁相关的汇编指令lock。 +解释指令:其中edi为32位寄存器。如果是long则为64位的rdi寄存器。 +2、volatile方式volatile int i,执行i++: + +指令“lock; addl $0,0(%%esp)”表示加锁,把0加到栈顶的内存单元,该指令操作本身无意义,但这些指令起到内存屏障的作用,让前面的指令执行完成。具有XMM2特征的CPU已有内存屏障指令,就直接使用该指令。 +volatile字节码为: + + + + +synchronized原理 +monitor +代码块同步是使用monitorenter和monitorexit指令实现。monitorenter和monitorexit指令是在编译后插入到同步代码块开始和结束的的位置。任何一个对象都有一个monitor与之关联,当一个monitor被某个线程持有之后,该对象将处于锁定状态。线程执行到monitorenter指令时,会尝试获取该对象对应的monitor所有权,也即获得对象的锁。 + +monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: +1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 +2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. +3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 + +monitorexit:执行monitorexit的线程必须是对象所对应的monitor的所有者。 +指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 +其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因 +在HotSpotJVM实现中,锁有个专门的名字:对象监视器。 +汇编上的实现(cmpxchg) +synchronizied方式实现i++ +字节码: + +汇编 + +monitorenter与monitorexit包裹了getstatic i及putstatic i,等相关代码执行指令。中间值的交换采用了原子操作lock cmpxchg %rsi,(%rdi),如果交换成功,则执行goto直接退出当前函数return。如果失败,执行jne跳转指令,继续循环执行,直到成功为止。 + +cmpxchg指令:比较rsi和目的操作数rdi(第一个操作数)的值,如果相同,ZF标志被设置,同时源操作数(第二个操作)的值被写到目的操作数,否则,清ZF标志为0,并且把目的操作数的值写回rsi,则执行jne跳转指令。 +Java对象头 +synchronized用的锁放在java对象头里。 +有两种情况: +数组对象,虚拟机使用3个字宽存储对象头。 +非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32字节。 +长度 内容 说明 +32/64bit mark word 存储对象的hashCode或者锁信息 +32/64bit Class metadata address 存储对象描述数据的指针 +32/64bit Array length 数组的长度 +Mark Word 的存储结构: + +锁的分类 +synchronized是重量级锁,效率较低。 +synchronized所用到的锁是存在Java对象头中。在Java1.6中,锁一共有4种状态,由低到高依次是:无锁,偏向锁,轻量级锁,重量级锁,这几种状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 +monitorenter和monitorexit是上层指令,底层实现可能是偏向锁、轻量级锁、重量级锁等。 + +偏向锁(只有一个线程进入临界区) +引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。 + +加锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(此时会引发竞争,偏向锁会升级为轻量级锁)。 + +膨胀过程:当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将Object设置LightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。 + +偏向锁,顾名思义,它会偏向于第一个获取锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程尝试获取,则持有偏向锁的线程将永远不需要触发同步。 +在锁对象的对象头中有个偏向锁的线程ID字段,这个字段如果是空的,第一次获取锁的时候,就CAS将自身的线程ID写入到MarkWord的偏向锁线程ID字段内,将MarkWord中的偏向锁的标识置1。这样下次获取锁的时候,直接检查偏向锁线程ID是否和自身线程ID一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁; +如果不一致,则表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的) + + + +轻量级锁(多个线程交替进入临界区) +引入背景:轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。 + +轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。 +然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋重试。重试一定次数后膨胀为重量级锁(修改MarkWord,改为指向重量级锁的指针),阻塞当前线程。 + +轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示有其他线程尝试获得锁,则释放锁,并唤醒被阻塞的线程。 + + +重量级锁(多个线程同时进入临界区) +在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。 + +锁的比较 + + + +锁的优化 +Java1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 +自旋锁 +线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 +何谓自旋锁? +所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 +自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了规定的时间仍然没有获取到锁,则应该被挂起。 +适应性自旋锁 +JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要获得这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 +有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。 +锁消除 +为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。 +锁粗化 + public void vectorTest(){ + Vector vector = new Vector(); + for(int i = 0 ; i < 10 ; i++){ + vector.add(i + ""); + } + + System.out.println(vector); + } +我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 +在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。 +锁粗化是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。 + + + + +原子操作原理 +CAS操作的意思是比较并交换,它需要两个数值,一个旧值(期望操作前的值)和新值。操作之前比较两个旧值是否变化,如无变化才交换为新值。 +CPU如何实现原子操作 +1)在硬件层面,CPU依靠总线加锁和缓存锁定机制来实现原子操作。 + +使用总线锁保证原子性。如果多个CPU同时对共享变量进行写操作(i++),通常无法得到期望的值。CPU使用总线锁来保证对共享变量写操作的原子性,当CPU在总线上输出LOCK信号时,其他CPU的请求将被阻塞住,于是该CPU可以独占共享内存。 + +使用缓存锁保证原子性。频繁使用的内存地址的数据会缓存于CPU的cache中,那么原子操作只需在CPU内部执行即可,不需要锁住整个总线。缓存锁是指在内存中的数据如果被缓存于CPU的cache中,并且在LOCK操作期间被锁定,那么当它执行锁操作写回到内存时,CPU修改内部的内存地址,并允许它的缓存一致性来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器 缓存的 内存区域数据。当其他CPU回写被锁定的cache行数据时候,会使cache行无效。 +Java如何实现原子操作 +2)Java使用了锁和循环CAS的方式来实现原子操作。 +使用循环CAS实现原子操作。JVM的CAS操作使用了CPU提供的CMPXCHG指令来实现,自旋式CAS操作的基本思路是循环进行CAS操作直到成功为止。1.5之后的并发包中提供了诸如AtomicBoolean, AtomicInteger等包装类来支持原子操作。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作三个问题。 + +cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。 + +使用锁机制实现原子操作。锁机制保证了只有获得锁的线程才能给操作锁定的区域。JVM的内部实现了多种锁机制。除了偏向锁,其他锁的方式都使用了循环CAS,也就是当一个线程想进入同步块的时候,使用循环CAS方式来获取锁,退出时使用CAS来释放锁。 + +CAS在OpenJDK中的实现 +以compareAndSwapInt为例: +UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) + UnsafeWrapper("Unsafe_CompareAndSwapInt"); + oop p = JNIHandles::resolve(obj); + jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); + return (jint)( (x, addr, e)) == e; +UNSAFE_END + +linux下 +inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { + int mp = os::is_MP(); + __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" + : "=a" (exchange_value) + : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) + : "cc", "memory"); + return exchange_value; +} + +程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpchg指令加上lock前缀;如果是在单处理器上运行,就省略lock前缀。 +Intel对lock前缀的说明如下: +1)确保对内存的读-改-写操作原子执行(基于总线锁或缓存锁) +2)禁止该指令,与 之前 和 之后 的读写指令重排序 +3)把写缓冲区中的所有数据刷新到内存中。 + + +同步容器 +同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作包括:迭代,跳跃,以及条件运算。 +ConcurrentHashMap +它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要再迭代过程中对容器加锁。ConcurrentHasMap返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当修改迭代器会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。 + +CopyOnWriteArrayList +用于替代同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。写入时复制容器返回的迭代器不会抛出 +ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。 +显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时,仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器。 + +BlockingQueue +阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上的put方法也永远不会阻塞。 +在构建高可靠的应用程序时,有界队列ArrayBlockingQueue是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。 + +ThreadLocal +在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。 +例如有一个静态变量 + +public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”); +如果两个线程同时调用sdf.format(…) +那么可能会很混乱,因为sdf使用的内部数据结构可能会被并发的访问所破坏。当然可以使用线程同步,但是开销很大;或者也可以在需要时构造一个局部SImpleDateFormat对象。但这很浪费 + +同步工具使用 +Semaphore(信号量) +信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池,或者对容器施加边界。 +Semaphore中管理着一组虚拟的许可permit,许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。 +可以用于实现资源池,当池为空时,请求资源将会阻塞,直至存在资源。将资源返回给池之后会调用release释放许可。 +public class BoundedHashSet { + private final Set set; + private final Semaphore semaphore; + + public BoundedHashSet(int bound) { + this.set = Collections.synchronizedSet(new HashSet()); + this.semaphore = new Semaphore(bound); + } + public boolean add(T t) throws InterruptedException { + semaphore.acquire(); + boolean wasAdded = false; + try { + wasAdded = set.add(t); + return wasAdded; + } finally { + if(!wasAdded){ + semaphore.release(); + } + } + } + + public boolean remove(Object o){ + boolean wasRemoved = set.remove(o); + if(wasRemoved){ + semaphore.release(); + } + return wasRemoved; + } + +} + +CyclicBarrier(可循环使用的屏障/栅栏) +CountDownLatch CyclicBarrier +减计数方式 加计数方式 +计算为0时释放所有等待的线程 计数达到指定值时释放所有等待线程 +计数为0时,无法重置 计数达到指定值时,计数置为0重新开始 +调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 +不可重复利用 可重复利用 +线程在countDown()之后,会继续执行自己的任务,而CyclicBarrier会在所有线程任务结束之后,才会进行后续任务。 + +Barrier类似于闭锁,它能阻塞一组线程直到某个线程发生。栅栏与闭锁的关键区别在于,前者未达到条件时每个线程都会阻塞在await上,直至条件满足所有线程解除阻塞,后者未达到条件时countDown不会阻塞,条件满足时会解除await线程的阻塞。 + +CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用;这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。 +如果对await方法的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都被终止并抛出BrokenBarrierException。如果成功通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来选举产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会在一个子任务线程中执行它。 +可以用于多线程计算数据,最后合并计算结果的场景。CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset方法重置。 +/** + * 通过CyclicBarrier协调细胞自动衍生系统中的计算 + */ +public class CellularAutomata { + private final Board mainBoard; + private final CyclicBarrier cyclicBarrier; + private final Worker[] workers; + + public CellularAutomata(Board board){ + this.mainBoard = board; + int count = Runtime.getRuntime().availableProcessors(); + this.cyclicBarrier = new CyclicBarrier(count, new Runnable() { + public void run() { + mainBoard.commitNewValues(); + } + }); + this.workers = new Worker[count]; + for (int i = 0; i < count; i++) { + workers[i] = new Worker(mainBoard.getSubBoard(count,i)); + } + } + + private class Worker implements Runnable{ + private final Board board; + + public Worker(Board board){ + this.board = board; + } + + public void run() { + while (!board.hasConverged()) { + for (int x = 0; x < board.getMaxX(); x++) { + for (int y = 0; y < board.getMaxY(); y++) { + board.setNewValue(x, y, computeValue(x, y)); + } + } + try { + cyclicBarrier.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + return; + } + } + } + } + + private int computeValue(int x, int y) { + return x+y; + } + + + public void start(){ + for (int i = 0; i < workers.length; i++) { + new Thread(workers[i]).start(); + } + mainBoard.waitForConvergence(); + } +} + + +Exchanger(两个线程交换数据) +另一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用。 +Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。 +public class TestExchanger { + + private Exchanger exchanger = new Exchanger(); + + private ExecutorService threadPool = Executors.newFixedThreadPool(2); + + public void start() { + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + String A = "银行流水A";// A录入银行流水数据 + exchanger.exchange(A); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + String B = "银行流水B";// B录入银行流水数据 + String A = exchanger.exchange("B"); + System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:" + + A + ",B录入是:" + B); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + threadPool.shutdown(); + } + + public static void main(String[] args) { + new TestExchanger().start(); + } +} + +CountDownLatch(闭锁) +闭锁可以延迟线程的进度直到其达到终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁达到结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。 +闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件发生了,而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。 +public class TestCountDownLatch { + public static void main(String[] args) { + CountDownLatch latch = new CountDownLatch(5); + LatchDemo latchDemo = new LatchDemo(latch); + long begin = System.currentTimeMillis(); + for (int i = 0; i < 5; ++i) { + new Thread(latchDemo).start(); + } + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + System.out.println("总计耗时:" + (end - begin)); + } +} + +class LatchDemo implements Runnable { + private CountDownLatch latch; + + public LatchDemo(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void run() { + try { + for (int i = 0; i < 50000; i++) { + if (i % 2 == 0) { + System.out.println(i); + } + } + } finally { + latch.countDown(); + } + } +} + + +FutureTask(Future实现类) +FurureTask是Future接口的唯一实现类。 +FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。 +Future.get方法的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则会阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask的规范确保了这种传递过程能实现结果的安全发布。 +Callable表示的任务可以抛出受检查的或未受检查的异常,并且任何代码都可能抛出一个Error。无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在future.get中被重新抛出。 +当get方法抛出ExecutionException,可能是以下三种情况之一:Callable抛出的受检查异常,RuntimeException,以及Error。 + + + +Future +Future接口设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。 在Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要等待耗时的操作完成。 +示例 +public void future() { + ExecutorService executor = Executors.newCachedThreadPool(); + Future future = executor.submit(new Callable() { + @Override + public Double call() throws Exception { + return doSomethingComputation(); + } + }); + // 在另一个线程执行耗时操作的同时,去执行一些其他的任务。 + // 这些任务不依赖于future的结果,可以与future并发执行。 + // 如果下面的任务马上依赖于future的结果,那异步操作是没有意义的。 + doSomethingElse(); + try { + // 如果不设置超时时间,那么线程会阻塞在这里。 + Double result = future.get(1, TimeUnit.SECONDS); + System.out.println("result is " + result); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } +} + +private void doSomethingElse() { + System.out.println("doSomethingElse"); +} + +private double doSomethingComputation() { + System.out.println("doSomethingComputation"); + return 0.1; +} + +局限性 +Future无法实现以下的功能。 +1) 将两个异步操作计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的记过 +2)等待Future集合中的所有任务都完成 +3)仅等待Future集合中最快结束的任务完成,并返回它的结果 +4)通过编程方式完成一个Future任务的执行(以手工设定异步操作结果) +5)应对Future的完成事件(完成回调) +CompletableFuture +实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果) +1)Shop +public class Shop { + private ThreadLocalRandom random; + private ExecutorService executorService = Executors.newCachedThreadPool(); + + public Future getPriceAsync(String product){ + CompletableFuture future = new CompletableFuture<>(); + // 另一个线程计算 + executorService.submit(() -> { + try { + double price = calculatePrice(product); + future.complete(price); + } catch (Exception e) { + // 处理异常 + future.completeExceptionally(e); + e.printStackTrace(); + } + }); + return future; + } + + private double calculatePrice(String product){ + random = ThreadLocalRandom.current(); + // 模拟耗时操作 + delay(); + // 随机 + return random.nextDouble() * product.charAt(0) + product.charAt(1); + } + + public static void delay(){ + try { + Thread.sleep(1000); + throw new RuntimeException("product is not available"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Shop shop = new Shop(); + + Future price = shop.getPriceAsync("my favorite product"); + // 计算price和doSomethingElse是并发执行的 + doSomethingElse(); + try { + // 如果此时已经计算完毕,则立即返回;如果没有计算完毕,则会阻塞 + Double result = price.get(); + System.out.println("result is " + result); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + } + + private static void doSomethingElse() { + System.out.println("doSomethingElse"); + } +} + +2) GracefulShop +工厂方法创建的Future自己内部维护了一个线程池。 +public class GracefulShop { + private ThreadLocalRandom random; + public Future getPriceAsync(String product){ + // 接收一个Supplier,该Supplier会交由ForkJoinPool池中的某个执行线程执行 + return CompletableFuture.supplyAsync(() -> calculatePrice(product)); + } + + private double calculatePrice(String product){ + random = ThreadLocalRandom.current(); + // 模拟耗时操作 + delay(); + // 随机 + return random.nextDouble() * product.charAt(0) + product.charAt(1); + } + + public static void delay(){ + try { + Thread.sleep(1000); + throw new RuntimeException("product is not available"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + GracefulShop shop = new GracefulShop(); + + Future price = shop.getPriceAsync("my favorite product"); + // 计算price和doSomethingElse是并发执行的 + doSomethingElse(); + try { + // 如果此时已经计算完毕,则立即返回;如果没有计算完毕,则会阻塞 + Double result = price.get(); + System.out.println("result is " + result); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + } + + private static void doSomethingElse() { + System.out.println("doSomethingElse"); + } +} + + + +将批量同步操作转为异步操作(并行流/CompletableFuture) +如果原本的getPrice是同步方法的话,那么如果想批量调用getPrice,提高效率的方法要么使用并行流,要么使用CompletableFuture。 +public class SyncShop { + private String name; + + public SyncShop(String name) { + this.name = name; + } + + public static void delay() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public double getPrice(String product) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + delay(); + return random.nextDouble() * product.charAt(0) + product.charAt(1); + } + + public String getName() { + return name; + } +} + +public class BestProductPriceCalculator { + private List shops = Arrays.asList( + new SyncShop("BestPrice"), + new SyncShop("LetsSaveBig"), + new SyncShop("MyFavoriteShop"), + new SyncShop("BuyItAll") + ); + + public List findPricesWithParallelStream(String product) { + return shops + .parallelStream() + .map(shop -> shop.getName() + ":" + shop.getPrice(product)) + .collect(Collectors.toList()); + } + + public List findPricesWithCompletableFuture(String product) { + List> futures = shops + .stream() + .map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + ":" + shop.getPrice(product))) + .collect(Collectors.toList()); + // join方法和Future的get方法有相同的含义,并且也声明在Future接口中,它们唯一的不同就是join不会抛出任何检测到的异常。 + return futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); + } +} + + +public class FutureTest { + private BestProductPriceCalculator calculator = new BestProductPriceCalculator(); + // 1s + @Test + public void testParallelStream(){ + calculator.findPricesWithParallelStream("my favorite product"); + } + //2s + @Test + public void testCompletableFuture(){ + calculator.findPricesWithCompletableFuture("my favorite product"); + } +} + + +使用并行流还是CompletableFuture? +前者是无法调整线程池的大小的(处理器个数),而后者可以。 +如果是计算密集型应用,且没有IO,那么推荐使用并行流 +如果是IO密集型,需要等待IO,那么使用CompletableFuture灵活性更高,比如根据《Java并发编程实战》中给出的公式计算线程池合适的大小。 + + + +多个异步任务合并 +逻辑如下: +从每个商店获取price,price以某种格式返回。拿到price后解析price,然后调用远程API根据折扣计算最终price。 +可以分为三个任务,每个商店都要执行这三个任务。 +public class PipelineShop { + private String name; + + public PipelineShop(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getPrice(String product) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + double price = calculatePrice(product); + Discount.DiscountCode code = Discount.DiscountCode.values()[random.nextInt(Discount.DiscountCode.values().length)]; + return String.format("%s:%.2f:%s", name, price, code); + } + + private double calculatePrice(String product) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + // 模拟耗时操作 + delay(); + // 随机 + return random.nextDouble() * product.charAt(0) + product.charAt(1); + } + + public static void delay() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} + + +public class Discount { + public enum DiscountCode{ + NONE(0),SILVER(5),GOLD(10),PLATINUM(15),DIAMOND(20); + private int percentage; + DiscountCode(int percentage){ + this.percentage = percentage; + } + } + + public static String applyDiscount(Quote quote){ + return quote.getShopName()+ " price is " + apply(quote.getPrice(), quote.getDiscountCode()); + } + + private static double apply(double price, DiscountCode discountCode) { + // 模拟调用远程服务的延迟 + delay(); + return price * ( 100 - discountCode.percentage ) / 100 ; + } + +} + +public class Quote { + private String shopName; + private double price; + private Discount.DiscountCode discountCode; + + public Quote(String shopName, double price, Discount.DiscountCode discountCode) { + this.shopName = shopName; + this.price = price; + this.discountCode = discountCode; + } + + public static Quote parse(String str){ + String [] slices = str.split(":"); + return new Quote(slices[0],Double.parseDouble(slices[1]),Discount.DiscountCode.valueOf(slices[2])); + } + + public String getShopName() { + return shopName; + } + + public double getPrice() { + return price; + } + + public Discount.DiscountCode getDiscountCode() { + return discountCode; + } +} + + +public class BestProductPriceWithDiscountCalculator { + private List shops = Arrays.asList( + new PipelineShop("BestPrice"), + new PipelineShop("LetsSaveBig"), + new PipelineShop("MyFavoriteShop"), + new PipelineShop("BuyItAll") + ); + + public List findPricesWithPipeline(String product) { + List> futures = shops + .stream() + .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product))) + .map(future -> future.thenApply(Quote::parse)) + .map(future -> future.thenCompose( + quote -> CompletableFuture.supplyAsync( + () -> Discount.applyDiscount(quote) + ) + )) + .collect(Collectors.toList()); + return futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); + } +} + + +回调 +public class CallbackBestProductPriceCalculator { + private List shops = Arrays.asList( + new PipelineShop("BestPrice"), + new PipelineShop("LetsSaveBig"), + new PipelineShop("MyFavoriteShop"), + new PipelineShop("BuyItAll") + ); + + public Stream> findPricesStream(String product) { + return shops + .stream() + .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product))) + .map(future -> future.thenApply(Quote::parse)) + .map(future -> future.thenCompose( + quote -> CompletableFuture.supplyAsync( + () -> Discount.applyDiscount(quote) + ) + )); + } +} + + +@Test +public void testCallback(){ + CompletableFuture[] futures = callbackBestProductPriceCalculator.findPricesStream("my favorite product").map( + future -> future.thenAccept(System.out::println) + ).toArray(size -> new CompletableFuture[size]); + CompletableFuture.allOf(futures).join(); +} + + + + +API +CompletableFuture类实现了CompletionStage和Future接口。Future是Java 5添加的类,用来描述一个异步计算的结果,但是获取一个结果时方法较少,要么通过轮询isDone,确认完成后,调用get()获取值,要么调用get()设置一个超时时间。但是这个get()方法会阻塞住调用线程,这种阻塞的方式显然和我们的异步编程的初衷相违背。 +为了解决这个问题,JDK吸收了guava的设计思想,加入了Future的诸多扩展功能形成了CompletableFuture。 + +CompletionStage是一个接口,从命名上看得知是一个完成的阶段,它里面的方法也标明是在某个运行阶段得到了结果之后要做的事情。、 +supplyAsync 提交任务 +public static CompletableFuture supplyAsync(Supplier supplier); +public static CompletableFuture supplyAsync(Supplier supplier, Executor executor); + +thenApply 变换(等待前一个任务返回后执行,处于同一个CompletableFuture) +public CompletionStage thenApply(Function fn); +public CompletionStage thenApplyAsync(Function fn); +public CompletionStage thenApplyAsync(Function fn,Executor executor); +首先说明一下以Async结尾的方法都是可以异步执行的,如果指定了线程池,会在指定的线程池中执行,如果没有指定,默认会在ForkJoinPool.commonPool()中执行,下文中将会有好多类似的,都不详细解释了。关键的入参只有一个Function,它是函数式接口,所以使用Lambda表示起来会更加优雅。它的入参是上一个阶段计算后的结果,返回值是经过转化后结果。 +不带Async的方法会在和前一个任务相同的线程中处理; +以Async的方法会将任务提交到一个线程池,所有每个任务是由不同的线程处理的。 +public void thenApply() { + String result = CompletableFuture.supplyAsync(() -> "hello").thenApply(s -> s + " world").join(); + System.out.println(result); +} +thenAccept 消耗 +public CompletionStage thenAccept(Consumer action); +public CompletionStage thenAcceptAsync(Consumer action); +public CompletionStage thenAcceptAsync(Consumer action,Executor executor); + +public void thenAccept() { + CompletableFuture.supplyAsync(() -> "hello").thenAccept(s -> System.out.println(s + " world")); +} + +thenRun 执行下一步操作,不关心上一步结果 +public CompletionStage thenRun(Runnable action); +public CompletionStage thenRunAsync(Runnable action); +public CompletionStage thenRunAsync(Runnable action,Executor executor); + +thenRun它的入参是一个Runnable的实例,表示当得到上一步的结果时的操作。 +public void thenRun() { + CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "hello"; + }).thenRun(() -> System.out.println("hello world")); +} + + + + +thenCombine 结合两个CompletionStage的结果,进行转化后返回 +public CompletionStage thenCombine(CompletionStage other,BiFunction fn); +public CompletionStage thenCombineAsync(CompletionStage other,BiFunction fn); +public CompletionStage thenCombineAsync(CompletionStage other,BiFunction fn,Executor executor); + +它需要原来的处理返回值,并且other代表的CompletionStage也要返回值之后,利用这两个返回值,进行转换后返回指定类型的值。 +public void thenCombine() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "hello"; + }).thenCombine(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "world"; + }), (s1, s2) -> s1 + " " + s2).join(); + System.out.println(result); +} + + +thenCompose(合并多个CompletableFuture,流水线执行,在调用外部接口返回CompletableFuture类型时更方便) + CompletableFuture thenCompose(Function> fn); +thenCompose方法允许对两个异步操作(supplyAsync)进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。 +创建两个CompletableFuture,对第一个CompletableFuture对象调用thenCompose,并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。 + +thenAccptBoth 结合两个CompletionStage的结果,进行消耗 +public CompletionStage thenAcceptBoth(CompletionStage other,BiConsumer action); +public CompletionStage thenAcceptBothAsync(CompletionStage other,BiConsumer action); +public CompletionStage thenAcceptBothAsync(CompletionStage other,BiConsumer action, Executor executor); +它需要原来的处理返回值,并且other代表的CompletionStage也要返回值之后,利用这两个返回值,进行消耗。 +public void thenAcceptBoth() { + CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "hello"; + }).thenAcceptBoth(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "world"; + }), (s1, s2) -> System.out.println(s1 + " " + s2)); +} + + + +runAfterBoth 在两个CompletionStage都运行完执行,不关心上一步结果 +public CompletionStage runAfterBoth(CompletionStage other,Runnable action); +public CompletionStage runAfterBothAsync(CompletionStage other,Runnable action); +public CompletionStage runAfterBothAsync(CompletionStage other,Runnable action,Executor executor); +不关心这两个CompletionStage的结果,只关心这两个CompletionStage执行完毕,之后在进行操作(Runnable)。 +public void runAfterBoth() { + CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s1"; + }).runAfterBothAsync(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s2"; + }), () -> System.out.println("hello world")); +} + + +applyToEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的转化操作 +public CompletionStage applyToEither(CompletionStage other,Function fn); +public CompletionStage applyToEitherAsync(CompletionStage other,Function fn); +public CompletionStage applyToEitherAsync(CompletionStage other,Function fn,Executor executor); +我们现实开发场景中,总会碰到有两种渠道完成同一个事情,所以就可以调用这个方法,找一个最快的结果进行处理。 +public void applyToEither() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s1"; + }).applyToEither(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "hello world"; + }), s -> s).join(); + System.out.println(result); +} + + + +acceptEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的消耗操作 +public CompletionStage acceptEither(CompletionStage other,Consumer action); +public CompletionStage acceptEitherAsync(CompletionStage other,Consumer action); +public CompletionStage acceptEitherAsync(CompletionStage other,Consumer action,Executor executor); + +public void acceptEither() { + CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s1"; + }).acceptEither(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "hello world"; + }), System.out::println); + while (true) { + } +} + + +runAfterEither 两个CompletionStage,任何一个完成了都会执行下一步的操作,不关心上一步结果 +public CompletionStage runAfterEither(CompletionStage other,Runnable action); +public CompletionStage runAfterEitherAsync(CompletionStage other,Runnable action); +public CompletionStage runAfterEitherAsync(CompletionStage other,Runnable action,Executor executor); +public void runAfterEither() { + CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s1"; + }).runAfterEither(CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s2"; + }), () -> System.out.println("hello world")); +} + + +exceptionally 当运行时出现了异常,可以进行补偿 +public CompletionStage exceptionally(Function fn); +public void exceptionally() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (1 == 1) { + throw new RuntimeException("测试一下异常情况"); + } + return "s1"; + }).exceptionally(e -> { + System.out.println(e.getMessage()); + return "hello world"; + }).join(); + System.out.println(result); +} + + +whenComplete 当运行完成时,若有异常则改变返回值,否则返回原值 +public CompletionStage whenComplete(BiConsumer action); +public CompletionStage whenCompleteAsync(BiConsumer action); +public CompletionStage whenCompleteAsync(BiConsumer action,Executor executor); + +public void whenComplete() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (1 == 1) { + throw new RuntimeException("测试一下异常情况"); + } + return "s1"; + }).whenComplete((s, t) -> { + System.out.println(s); + System.out.println(t.getMessage()); + }).exceptionally(e -> { + System.out.println(e.getMessage()); + return "hello world"; + }).join(); + System.out.println(result); +} + + +null +java.lang.RuntimeException: 测试一下异常情况 +java.lang.RuntimeException: 测试一下异常情况 +hello world +这里也可以看出,如果使用了exceptionally,就会对最终的结果产生影响,它无法影响如果没有异常时返回的正确的值,这也就引出下面我们要介绍的handle。 + +handle 当运行完成时,无论有无异常均可转换 +public CompletionStage handle(BiFunction fn); +public CompletionStage handleAsync(BiFunction fn); +public CompletionStage handleAsync(BiFunction fn,Executor executor); + +public void handle() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //出现异常 + if (1 == 1) { + throw new RuntimeException("测试一下异常情况"); + } + return "s1"; + }).handle((s, t) -> { + if (t != null) { + return "hello world"; + } + return s; + }).join(); + System.out.println(result); +} + +hello world + +public void handle() { + String result = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "s1"; + }).handle((s, t) -> { + if (t != null) { + return "hello world"; + } + return s; + }).join(); + System.out.println(result); +} + +s1 + + +allOf +allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有CompletableFuture对象执行完成之后,它返回一个CompletableFuture对象。这意味着,如果你需要等待最初Stream中的所有CompletableFuture对象执行完毕,对allOf方法返回的CompletableFuture执行join操作是个不错的注意。 +anyOf +只要CompletableFuture对象数组中有一个执行完毕,便不再等待。 + + +ForkJoin +双端队列LinkedBlockingDeque适用于另一种相关模式,即工作密取(work stealing)。在生产者——消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列头部秘密地获取工作。密取工作模式比传统的生产者——消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的头部而不是从尾部获取工作,因此进一步降低了队列上的竞争程度。 +第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。 +第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。 +Fork/Join使用两个类来完成以上两件事情: +ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类: +oRecursiveAction:用于没有返回结果的任务。 +oRecursiveTask :用于有返回结果的任务。 +ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。 + +threshold 临界值 + +RecursiveTask有两个方法:fork和join +fork是执行子任务,join是取得子任务的结果,用于合并 +public class TestForkJoin { + public static void main(String[] args) throws InterruptedException, ExecutionException { + ForkJoinPool pool = new ForkJoinPool(); + ForkJoinCalculator calculator = new ForkJoinCalculator(0, 10000000L); + Long result = pool.invoke(calculator); + System.out.println(result); + pool.shutdown(); + } +} + +class ForkJoinCalculator extends RecursiveTask { + + private static final long serialVersionUID = -6682191224530210391L; + + private long start; + private long end; + private static final long THRESHOLD = 10000L; + + public ForkJoinCalculator(long start, long end) { + this.start = start; + this.end = end; + } + + @Override + protected Long compute() { + long length = end - start; + if (length < THRESHOLD) { + long sum = 0L; + for (long i = start; i < end; ++i) { + sum += i; + } + return sum; + } else { + long middle = (start + end) / 2; + ForkJoinCalculator left = new ForkJoinCalculator(start, middle); + left.fork(); + ForkJoinCalculator right = new ForkJoinCalculator(middle, end); + right.fork(); + return left.join() + right.join(); + } + } +} +原理浅析 +1. 每个Worker线程都维护一个任务队列,即ForkJoinWorkerThread中的任务队列。 + +2. 任务队列是双向队列,这样可以同时实现LIFO和FIFO。 + +3. 子任务会被加入到原先任务所在Worker线程的任务队列。 + +4. Worker线程用LIFO的方法取出任务,也就后进队列的任务先取出来(子任务总是后加入队列,但是需要先执行)。 + +5. Worker线程的任务队列为空,会随机从其他的线程的任务队列中拿走一个任务执行(所谓偷任务:steal work,FIFO的方式)。 + +6. 如果一个Worker线程遇到了join操作,而这时候正在处理其他任务,会等到这个任务结束。否则直接返回。 + +7. 如果一个Worker线程偷任务失败,它会用yield或者sleep之类的方法休息一会儿,再尝试偷任务(如果所有线程都是空闲状态,即没有任务运行,那么该线程也会进入阻塞状态等待新任务的到来)。 + +与MapReduce的区别 +MapReduce是把大数据集切分成小数据集,并行分布计算后再合并。 + +ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。 + +二者共同点:都是用于执行并行任务的。基本思想都是把问题分解为一个个子问题分别计算,再合并结果。应该说并行计算都是这种思想,彼此独立的或可分解的。从名字上看Fork和Map都有切分的意思,Join和Reduce都有合并的意思,比较类似。 + +区别: + +1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。 + +2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。 + +线程池使用 +引入原因 +1)任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接,使得任务在完成前面的请求之前可以接受新的请求,从而提高响应性。 +2)任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,程序的吞吐量将得到提高。 +3)任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。 + +无限制创建线程的不足: +1)线程生命周期的开销非常高 +2)资源消耗 +3)稳定性 + +解决方式:线程池 Executor框架 + +使用线程池的好处: +1)降低资源消耗 +2)提高响应速度 +3)提高线程的可管理性 + + + +Executor ExecutorService ScheduledExecutorService +继承体系 + + + + + + +ExecutorService + + +ScheduledExecutorService + +返回值 + + + + +示例 +public class QuoteTask implements Callable { + private final TravelCompany company; + private final TravelInfo travelInfo; + private ExecutorService exec; + public QuoteTask(TravelCompany company, TravelInfo travelInfo) { + this.company = company; + this.travelInfo = travelInfo; + } + + public TravelQuote call() throws Exception { + return company.solicitQuote(travelInfo); + } + + public List getRankedTravelQuotes(TravelInfo travelInfo, Set companies, Comparator ranking, long time, TimeUnit unit) throws InterruptedException { + //任务 + List tasks = new ArrayList(); + for (TravelCompany company : companies) { + tasks.add(new QuoteTask(company,travelInfo)); + } + //执行 + List> futures = exec.invokeAll(tasks,time,unit); + List quotes = new ArrayList(tasks.size()); + Iterator taskIterator = tasks.iterator(); + //取出结果 + for(Future future:futures){ + QuoteTask task = taskIterator.next(); + try { + quotes.add(future.get()); + } catch (ExecutionException e) { + quotes.add(task.getFailureQuote(e.getCause())); + e.printStackTrace(); + }catch(CancellationException e){ + quotes.add(task.getTimeOutQuote(e)); + } + } + Collections.sort(quotes,ranking); + return quotes; + } +} + + + +ThreadPoolExecutor +创建线程池 + +线程动态变化 +1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 +2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 +3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 +4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理 +5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 +6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 + +创建一个线程池时需要以下几个参数: +1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 +2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列: + a)ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO + b) LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO,吞吐量高于ArrayBlockingQueue,Executors.newFixedThreadPoll()使用了这个队列 + c)SynchronousQueue:一个只存储一个元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,Executors#newCachedThreadPoll()使用了这个队列 + d)PriorityBlockingQueue:具有优先级的无界阻塞队列 + +3)maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果使用了无界队列该参数就没有意义了。 + +4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 + +5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,或者当线程池已关闭时,会采用一种策略处理提交的新任务。这个策略默认是AbortPolicy,表示无法处理新任务时抛出异常。有以下四种饱和策略: + a)AbortPolicy:直接抛出异常 + b)CallerRunsPolicy:使用调用者所在线程来运行任务 + c)DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务 + d)DiscardPolicy:不处理,直接丢弃 + +也可以自定义饱和策略。 + +6)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。出现timeout情况下,而且线程数超过了核心线程数,会销毁销毁线程。保持在corePoolSize数。除非设置了allowCoreThreadTimeOut和超时时间,这种情况线程数可能减少到0,最大可能是Integer.MAX_VALUE。 +如果任务很多,每个任务执行的时间比较短,可以调大时间,提高线程的利用率。 + +allowCoreThreadTimeOut为true +该值为true,则线程池数量最后销毁到0个。 + +allowCoreThreadTimeOut为false +销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。 + +7)TimeUnit(线程活动保持时间的单位) +使用注意 +1、只有当任务都是同类型并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成拥塞。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——web服务器、邮件服务器、文件服务器等,它们的请求通常都是同类型的并且相互独立的。 + +2、设置线程池的大小: +基于Runtime.getRuntime().avialableprocessors() 进行动态计算 +对于计算密集型的任务,在N个处理器的系统上,当线程池为N+1时,通过能实现最优的利用率(缺页故障等暂停时额外的线程也能确保CPU时钟周期不被浪费)。 +对于包含IO操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大,比如2*N。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。这种估算不需要很精确,而且可以通过一些分析或监控工具来获得。你还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率。 +最佳线程数目 = (线程等待时间与线程计算时间之比 + 1)* CPU数目 +3、线程的创建与销毁 +基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。 + +4、管理队列任务 +ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列、有界队列和同步移交。 +一种稳妥的资源管理策略是使用有界队列,有界队列有助于避免资源耗尽的情况发生,但又带来了新的问题:当队列填满后,新的任务该怎么办? + +5、饱和策略 +当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler来修改。JDK提供了几种不同的RejectedExecutionHandler的实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。 + +1)中止策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 +2)当新提交的任务无法保存到队列中执行时,抛弃策略会悄悄抛弃该任务。 +3)抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试重新提交下一个将被执行的任务(如果工作队列是一个优先级队列,那么抛弃最旧的将抛弃优先级最高的任务) + +4)调用者运行策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。为什么好?因为当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。 + +6、线程工厂 +在许多情况下都需要使用定制的线程工厂方法。例如,你希望为线程池中的线程指定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录,你还可能希望修改线程的优先级(虽然不提倡这样做),或者只是给线程取一个更有意义的名字,用来解释线程的转储信息和错误日志。 + +7、在调用构造函数后再定制ThreadPoolExecutor + +扩展ThreadPoolExecutor +public class TimingThreadPool extends ThreadPoolExecutor { + public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + private final ThreadLocal startTime = new ThreadLocal(); + private final Logger log = Logger.getLogger("TimingThreadPool"); + private final AtomicLong numTasks = new AtomicLong(); + private final AtomicLong totalTime = new AtomicLong(); + + @Override + protected void beforeExecute(Thread t, Runnable r) { + super.beforeExecute(t,r); + log.fine(String.format("Thread %s :start %s",t,r)); + startTime.set(System.nanoTime()); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + try{ + long endTime = System.nanoTime(); + long taskTime = endTime - startTime.get(); + numTasks.incrementAndGet(); + totalTime.addAndGet(taskTime); + log.fine(String.format("Thread %s : end %s ,time = %dns",t,r,taskTime)); + }finally { + super.afterExecute(r, t); + } + } + + @Override + protected void terminated() { + try { + log.info(String.format("Terminated : avg time = %dns",totalTime.get() / numTasks.get())); + } finally { + super.terminated(); + } + } +} + +任务时限 +Future的get方法可以限时,如果超时会抛出TimeOutException,那么此时可以通过cancel方法来取消任务。如果编写的任务是可取消的,那么可以提前中止它,以免消耗过多的资源。 +创建n个任务,将其提交到一个线程池,保留n个Future,并使用限时的get方法通过Future串行地获取每一个结果,这一切都很简单。但还有一个更简单的实现:invokeAll。 +将多个任务提交到一个ExecutorService并获得结果。invokeAll方法的参数是一组任务,并返回一组Future。这两个集合有着相同的结构。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable关联起来。当所有任务执行完毕时,或者调用线程被中断时,又或者超时,invokeAll将返回。当超时时,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。 +public class QuoteTask implements Callable { + private final TravelCompany company; + private final TravelInfo travelInfo; + private ExecutorService exec; + public QuoteTask(TravelCompany company, TravelInfo travelInfo) { + this.company = company; + this.travelInfo = travelInfo; + } + + public TravelQuote call() throws Exception { + return company.solicitQuote(travelInfo); + } + + public List getRankedTravelQuotes(TravelInfo travelInfo, Set companies, Comparator ranking, long time, TimeUnit unit) throws InterruptedException { + //任务 + List tasks = new ArrayList(); + for (TravelCompany company : companies) { + tasks.add(new QuoteTask(company,travelInfo)); + } + //执行 + List> futures = exec.invokeAll(tasks,time,unit); + List quotes = new ArrayList(tasks.size()); + Iterator taskIterator = tasks.iterator(); + //取出结果 + for(Future future:futures){ + QuoteTask task = taskIterator.next(); + try { + quotes.add(future.get()); + } catch (ExecutionException e) { + quotes.add(task.getFailureQuote(e.getCause())); + e.printStackTrace(); + }catch(CancellationException e){ + quotes.add(task.getTimeOutQuote(e)); + } + } + Collections.sort(quotes,ranking); + return quotes; + } +} + + +任务关闭 +线程有一个相应的所有者,即创建该线程的类,因此线程池是工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。 +ExecutorService中提供了shutdown和shutdownNow方法。 +前者是正常关闭,后者是强行关闭。 +1)它们都会阻止新任务的提交 +2)正常关闭是停止空闲线程,正在执行的任务继续执行并完成所有未执行的任务 +3)强行关闭是停止所有(空闲+工作)线程,关闭当前正在执行的任务,然后返回所有尚未执行的任务。 + +通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用 +shutdownNow方法。 + +但是我们无法通过常规方法来找出哪些任务已经开始但尚未结束,这意味着我们无法在关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查。要知道哪些任务还没有完成,你不仅需要知道哪些任务还没有开始,而且还需要知道当Executor关闭时哪些任务正在执行。 +-------------------------------- +处理非正常的线程终止(只对execute提交的任务有效,submit提交的话会在future.get时将受检异常直接抛出) +要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally块来接收通知,因此当线程结束时,将有新的线程来代替它。如果没有提供捕获异常处理器或者其他的故障通知机制,那么任务会悄悄失败,从而导致很大的混乱。如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。 +只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。 +public class QuoteTask implements Callable { + private final TravelCompany company; + private final TravelInfo travelInfo; + private ExecutorService exec; + public QuoteTask(TravelCompany company, TravelInfo travelInfo) { + this.company = company; + this.travelInfo = travelInfo; + } + + public TravelQuote call() throws Exception { + return company.solicitQuote(travelInfo); + } + + public List getRankedTravelQuotes(TravelInfo travelInfo, Set companies, Comparator ranking, long time, TimeUnit unit) throws InterruptedException { + //任务 + List tasks = new ArrayList(); + for (TravelCompany company : companies) { + tasks.add(new QuoteTask(company,travelInfo)); + } + //执行 + List> futures = exec.invokeAll(tasks,time,unit); + List quotes = new ArrayList(tasks.size()); + Iterator taskIterator = tasks.iterator(); + //取出结果 + for(Future future:futures){ + QuoteTask task = taskIterator.next(); + try { + quotes.add(future.get()); + } catch (ExecutionException e) { + quotes.add(task.getFailureQuote(e.getCause())); + e.printStackTrace(); + }catch(CancellationException e){ + quotes.add(task.getTimeOutQuote(e)); + } + } + Collections.sort(quotes,ranking); + return quotes; + } +} + + + +ScheduledThreadPoolExecutor +它继承自ThreadPoolExecutor,主要用来在给定的延迟之后运行任务,或者定期执行任务。Timer是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。 +public ScheduledThreadPoolExecutor(int corePoolSize, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue(), threadFactory, handler); +} + +内部工作队列是DelayedWorkQueue,它是一个无界队列,maxPoolSize这个参数没有意义。 +static class DelayedWorkQueue extends AbstractQueue + implements BlockingQueue + +public class TestScheduledThreadPool { + public static void main(String[] args) throws InterruptedException, ExecutionException { + ScheduledExecutorService pool = Executors.newScheduledThreadPool(5); + for(int i = 0; i < 10 ;++i){ + Future result = pool.schedule(new ThreadPoolDemo2(), 800, TimeUnit.MILLISECONDS); + System.out.println(result.get()); + } + pool.shutdown(); + } +} + +class ThreadPoolDemo2 implements Callable { + @Override + public Integer call() throws Exception { + int sum = 0; + for (int i = 0; i < 100; ++i) { + sum += i; + System.out.println(Thread.currentThread().getName() + "\t" + i); + } + return sum; + } +} + + +Executors +Executors是一个工厂类,可以创建3种类型的ThreadPoolExecutor和2种类型的ScheduledThreadPool。 + +FixedThreadPool +创建固定线程数的FixedThreadPool,适用于负载比较重的服务器。 +public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); +} +corePoolSize和maxPoolSize都被设置为创建FixedThreadPoolExecutor时指定的参数nThreads。 +keepAliveTime为0表示多余的空闲线程将会被立即终止。 +使用无界队列LinkedBlockingQueue来作为线程池的工作队列,并且默认容量为Integer.MAX_VALUE。使用无界队列会带来以下影响: +1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize +2)maximumPoolSize是一个无效的参数 +3)keepAliveTime是一个无效参数 +4)运行中的FixedThreadPool(未执行shutdown或shutdownNow)不会拒绝任务。 + +SingleThreadExecutor +适用于需要保证顺序地执行各个任务,并且在任意时间点不会有多个线程活动的应用场景。 +public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); +} +它也是使用无界队列,corePoolSize和maxPoolSize都为1。 + +CachedThreadPool +大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。 +public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); +} +使用没有缓冲区、只能存储一个元素的SynchronousQueue作为工作队列。 +maxPoolSize是无界的,如果主线程提交任务的速度高于maxPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存。 + +任务执行过程: +1)首先执行SynchronousQueue#offer(Runnable) 。如果当前maxPool中有空闲线程正在执行SynchronousQueue#poll,那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行;否则执行2) +2)当初始maxPool为空,或者maxPool中没有空闲线程时,此时CachedThreadPool会创建一个新线程执行任务 +3)在2)中新创建的线程执行任务完毕后,会执行SynchronousQueue#poll,这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒。如果60秒内主线程提交了一个新任务,那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。 + +ScheduledThreadPoolExecutor +固定线程个数,适用于多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的梳理的应用场景。 +SingleThreadScheduledExecutor +public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { + return new DelegatedScheduledExecutorService + (new ScheduledThreadPoolExecutor(1, threadFactory)); +} + +适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。 + + +CompletionService +CompletionService将Executor和BlockingQueue的概念融合在一起,你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时封装为Future。ExecutorCompletionService实现了CompletionService并将计算任务委托给一个Executor。 + +ExecutorCompletionService的实现非常简单,在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用FutureTask的done方法。当提交某个任务时,该任务将首先包装为一个QueueingFuture,这是FutureTask的一个子类,然后再改写子类的done方法,并将结果放入BlockingQueue中。take和poll方法委托给了BlockingQueue,这些方法会在得出结果之前阻塞。 + +多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。 +public class CompletionServiceTest { + public void test() throws InterruptedException, ExecutionException { + ExecutorService exec = Executors.newCachedThreadPool(); + CompletionService completionService = new ExecutorCompletionService(exec); + for (int i = 0; i < 10; i++) { + completionService.submit(new Task()); + } + int sum = 0; + for (int i = 0; i < 10; i++) { + //检索并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。 + Future future = completionService.take(); + sum += future.get(); + } + System.out.println("总数为:" + sum); + exec.shutdown(); + } +} + + +J.U.C 源码解析 +实现整个并发体系的真正底层是CPU提供的lock前缀+cmpxchg指令和POSIX的同步原语(mutex&condition) +synchronized和wait¬ify基于JVM的monitor,monitor底层又是基于POSIX同步原语。 +volatile基于CPU的lock前缀指令实现内存屏障。 +而J.U.C是基于LockSupport,底层基于POSIX同步原语。 +AbstractQueuedSynchronizer(AQS) +在ReentrantLock和Semaphore这两个接口之间存在许多共同点,这两个类都可以用作一个阀门,即每次只允许一定数量的线程通过,并当线程到达阀门时,可以通过(在调用lock或acquire时成功返回),也可以等待(在调用lock或acquire时阻塞),还可以取消(在调用tryLock或tryAcquire时返回假,表示在指定的时间内锁是不可用的或无法得到许可)。 +可以通过锁来实现计数信号量。 +事实上,它们在实现时都使用了一个共同的基类,即AbstractQueuedSynchronizer(AQS),这个类也是其他许多同步类的基类。AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask,都是基于AQS构造的。 +在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。在设计AQS时充分考虑了可伸缩性,因此java.util.concurrent中所有基于AQS构建的同步器都能获得这个优势。 +在基于AQS构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。当使用锁或信号量时,获取操作的含义就很直观,即获取的是锁或许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。 + +AQS负责管理同步器类中的状态,它管理了一个整数类型的状态信息,可以通过getState、setState以及compareAndSetState等protected类型方法来进行操作。这个整数可以用于表示任意状态。 + + +它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。 +子类通过继承AQS并实现它的抽象方法来管理同步状态,修改同步状态依赖于AQS的getState、setState、compareAndSetState来进行操作,它们能够保证状态的改变是安全的。 +子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。AQS既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。 + +AQS的接口 +AQS的设计是基于模板方法模式的,使用者需要继承同步器并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用使用者重写的方法。 + +同步器可重写的方法: + + +同步器提供的模板方法: + + +AQS使用实例(互斥锁,tryAcquire只需一次CAS) +public class Mutex implements Lock { + private final Sync sync = new Sync(); + + @Override + public void lock() { + sync.acquire(1); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + sync.acquireInterruptibly(1); + } + + @Override + public boolean tryLock() { + return sync.tryAcquire(1); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return sync.tryAcquireNanos(1, unit.toNanos(time)); + } + + @Override + public void unlock() { + sync.release(1); + } + + @Override + public Condition newCondition() { + return sync.newCondition(); + } + + private static class Sync extends AbstractQueuedSynchronizer { + @Override + protected boolean tryAcquire(int arg) { + if (super.compareAndSetState(0, 1)) { + super.setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + + @Override + protected boolean tryRelease(int arg) { + if (super.getState() == 0) { + throw new IllegalMonitorStateException(); + } + super.setExclusiveOwnerThread(null); + super.setState(0); + return true; + } + + @Override + protected boolean isHeldExclusively() { + return super.getState() == 1; + } + + Condition newCondition() { + return new ConditionObject(); + } + } +} +AQS实现 +主要工作基于CLH队列,voliate关键字修饰的状态state,线程去修改状态成功了就是获取成功,失败了就进队列等待,等待唤醒。在等待唤醒的时候,很多时候会使用自旋(while(!cas()))的方式,不停的尝试获取锁,直到被其他线程获取成功。 +AQS#state getState setState +/** + * The synchronization state. + */ +private volatile int state; + +/** + * Returns the current value of synchronization state. + * This operation has memory semantics of a {@code volatile} read. + * @return current state value + */ +protected final int getState() { + return state; +} + +/** + * Sets the value of synchronization state. + * This operation has memory semantics of a {@code volatile} write. + * @param newState the new state value + */ +protected final void setState(int newState) { + state = newState; +} + +同步队列 +AQS依赖内部的CLH同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造为一个Node并将其加入同步队列,并阻塞当前线程。当同步状态释放时,会把后继节点线程唤醒,使其再次尝试获取同步状态。后继节点将会在获取同步状态成功时将自己设置为头节点。 +AQS#Node + +static final class Node { + /** Marker to indicate a node is waiting in shared mode */ + static final Node SHARED = new Node(); + /** Marker to indicate a node is waiting in exclusive mode */ + static final Node EXCLUSIVE = null; + + /** waitStatus value to indicate thread has cancelled */ + static final int CANCELLED = 1; + /** waitStatus value to indicate successor's thread needs unparking */ + static final int SIGNAL = -1; + /** waitStatus value to indicate thread is waiting on condition */ + static final int CONDITION = -2; + /** + * waitStatus value to indicate the next acquireShared should + * unconditionally propagate + */ + static final int PROPAGATE = -3; + + /** + * Status field, taking on only the values: + * SIGNAL: The successor of this node is (or will soon be) + * blocked (via park), so the current node must + * unpark its successor when it releases or + * cancels. To avoid races, acquire methods must + * first indicate they need a signal, + * then retry the atomic acquire, and then, + * on failure, block. + * CANCELLED: This node is cancelled due to timeout or interrupt. + * Nodes never leave this state. In particular, + * a thread with cancelled node never again blocks. + * CONDITION: This node is currently on a condition queue. + * It will not be used as a sync queue node + * until transferred, at which time the status + * will be set to 0. (Use of this value here has + * nothing to do with the other uses of the + * field, but simplifies mechanics.) + * PROPAGATE: A releaseShared should be propagated to other + * nodes. This is set (for head node only) in + * doReleaseShared to ensure propagation + * continues, even if other operations have + * since intervened. + * 0: None of the above + * + * The values are arranged numerically to simplify use. + * Non-negative values mean that a node doesn't need to + * signal. So, most code doesn't need to check for particular + * values, just for sign. + * + * The field is initialized to 0 for normal sync nodes, and + * CONDITION for condition nodes. It is modified using CAS + * (or when possible, unconditional volatile writes). + */ + volatile int waitStatus; + + /** + * Link to predecessor node that current node/thread relies on + * for checking waitStatus. Assigned during enqueuing, and nulled + * out (for sake of GC) only upon dequeuing. Also, upon + * cancellation of a predecessor, we short-circuit while + * finding a non-cancelled one, which will always exist + * because the head node is never cancelled: A node becomes + * head only as a result of successful acquire. A + * cancelled thread never succeeds in acquiring, and a thread only + * cancels itself, not any other node. + */ + volatile Node prev; + + /** + * Link to the successor node that the current node/thread + * unparks upon release. Assigned during enqueuing, adjusted + * when bypassing cancelled predecessors, and nulled out (for + * sake of GC) when dequeued. The enq operation does not + * assign next field of a predecessor until after attachment, + * so seeing a null next field does not necessarily mean that + * node is at end of queue. However, if a next field appears + * to be null, we can scan prev's from the tail to + * double-check. The next field of cancelled nodes is set to + * point to the node itself instead of null, to make life + * easier for isOnSyncQueue. + */ + volatile Node next; + + /** + * The thread that enqueued this node. Initialized on + * construction and nulled out after use. + */ + volatile Thread thread; + + /** + * Link to next node waiting on condition, or the special + * value SHARED. Because condition queues are accessed only + * when holding in exclusive mode, we just need a simple + * linked queue to hold nodes while they are waiting on + * conditions. They are then transferred to the queue to + * re-acquire. And because conditions can only be exclusive, + * we save a field by using special value to indicate shared + * mode. + */ + Node nextWaiter; + + /** + * Returns true if node is waiting in shared mode. + */ + final boolean isShared() { + return nextWaiter == SHARED; + } + + /** + * Returns previous node, or throws NullPointerException if null. + * Use when predecessor cannot be null. The null check could + * be elided, but is present to help the VM. + * + * @return the predecessor of this node + */ + final Node predecessor() throws NullPointerException { + Node p = prev; + if (p == null) + throw new NullPointerException(); + else + return p; + } + + Node() { // Used to establish initial head or SHARED marker + } + + Node(Thread thread, Node mode) { // Used by addWaiter + this.nextWaiter = mode; + this.thread = thread; + } + + Node(Thread thread, int waitStatus) { // Used by Condition + this.waitStatus = waitStatus; + this.thread = thread; + } +} + + + +独占式同步状态 +在获取同步状态时,AQS调用tryAcquire获取同步状态。AQS维护一个同步队列,获取同步状态失败的线程都会被加入到队列中并在队列进行自旋(等待);移出队列的条件是前驱节点是头结点且成功获取了同步状态; +在释放同步状态时,AQS调用tryRelease释放同步状态,然后唤醒头节点的后继节点,使其尝试获取同步状态。 +AQS#acquire +acquire(int)可以获取同步状态,对中断不敏感。 + +1)调用自定义同步器实现的tryAcquire +2)如果成功,那么结束 +3)如果失败,那么调用addWaiter加入同步队列尾部,并调用acquireQueued获取同步状态(前提是前驱节点为head) +3.1)如果获取到了,那么将自己设置为头节点,返回 +3,2)如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL, + 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 + 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +addWaiter(新Node添加到同步队列尾部,初始状态下head是一个空节点) +获取同步状态失败的线程会被构造成Node加入到同步队列尾部,这个过程必须是线程安全的,AQS基于CAS来设置同步队列的尾节点compareAndSetTail。 + + +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; +} +可能tail为null,或者tail不为null,但CAS添加node至尾部失败,此时会enq + +如果tail为null,则设置head和tail都指向一个空节点 +然后循环CAS添加node至尾部,直至成功。 +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } +} +acquireQueued + +设置首节点是由获取同步状态成功的线程来完成的,因为只有一个线程能够成功获取同步状态,因此设置头节点的方法并不需要CAS的包装。 + +如果自己是第二个结点,那么尝试获取同步状态,如果成功,那么将自己设置为头节点,并返回。 +如果自己不是第二个结点或者CAS获取失败,那么判断是否应该阻塞,如果应该,那么阻塞,否则自旋重新尝试获取同步状态。 +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 如果前驱是head,即该结点是第二个结点,那么便有资格去尝试获取资源(可能是head释放完资源唤醒自己的,当然也可能被interrupt了) + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} + +/** + * Sets head of queue to be node, thus dequeuing. Called only by + * acquire methods. Also nulls out unused fields for sake of GC + * and to suppress unnecessary signals and traversals. + * + * @param node the node + */ +private void setHead(Node node) { + head = node; + node.thread = null; + node.prev = null; +} + +shouldParkAfterFailedAcquire +1)如果前一个节点状态是SIGNAL,那么表示已经设置了前驱节点在获取到同步状态时会唤醒自己,就可以放心的去阻塞了。 +2)否则会检查前一个节点状态是否是Cancelled +2.1)如果是,那么就删除前一个节点,直至状态不是Cancelled。 +2.2)如果不是,那么将其状态设置为SIGNAL。 +/** + * Checks and updates status for a node that failed to acquire. + * Returns true if thread should block. This is the main signal + * control in all acquire loops. Requires that pred == node.prev. + * + * @param pred node's predecessor holding status + * @param node the node + * @return {@code true} if thread should block + */ +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + if (ws == Node.SIGNAL) + /* + * This node has already set status asking a release + * to signal it, so it can safely park. + */ + return true; + if (ws > 0) { + /* + * Predecessor was cancelled. Skip over predecessors and + * indicate retry. + */ + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + /* + * waitStatus must be 0 or PROPAGATE. Indicate that we + * need a signal, but don't park yet. Caller will need to + * retry to make sure it cannot acquire before parking. + */ + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} +parkAndCheckInterrupt +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +为什么只有前驱节点是头节点才能尝试获取同步状态? +1)头节点是成功获取到同步状态的节点,头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头结点 +2)维护同步队列的FIFO原则。 + + + + +AQS#release +在释放同步状态之后,会唤醒其后继节点,使后继节点继续尝试获取同步状态。 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} + +unparkSuccessor +private void unparkSuccessor(Node node) { + /* + * If status is negative (i.e., possibly needing signal) try + * to clear in anticipation of signalling. It is OK if this + * fails or if status is changed by waiting thread. + */ + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + /* + * Thread to unpark is held in successor, which is normally + * just the next node. But if cancelled or apparently null, + * traverse backwards from tail to find the actual + * non-cancelled successor. + */ + Node s = node.next; + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + LockSupport.unpark(s.thread); +} + +共享式同步状态 +共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。 + +左半部分:共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞 +右半部分:独占式访问资源时,同一时刻其他访问均被阻塞。 + + +AQS#acquireShared +AQS会调用tryAcquireShared方法尝试获取同步状态,该方法返回值为int,当返回值大于等于0时,表示能够获取到同步状态。 +如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。 + +1)调用自定义同步器实现的tryAcquireShared +2)如果成功,那么结束 +3)如果失败,那么调用addWaiter加入SHARED节点至同步队列尾部,并调用再次尝试获取同步状态(前提是前驱节点为head) +3.1)如果获取到了,那么将自己设置为头节点,并向后唤醒共享节点(如果还有剩余acquire),返回 +3.2) 如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL + 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 + 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 +public final void acquireShared(int arg) { + if (tryAcquireShared(arg) < 0) + doAcquireShared(arg); +} +doAcquiredShared +构造一个当前线程对应的共享节点,如果前驱节点是head并且尝试获取同步状态成功,那么将当前节点设置为head +private void doAcquireShared(int arg) { + final Node node = addWaiter(Node.SHARED); + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head) { + int r = tryAcquireShared(arg); + if (r >= 0) { + setHeadAndPropagate(node, r); + p.next = null; // help GC + if (interrupted) + selfInterrupt(); + failed = false; + return; + } + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} +setHeadAndPropagate +如果获取了同步状态,仍有剩余的acquire,那么继续向后唤醒 +/** + * Sets head of queue, and checks if successor may be waiting + * in shared mode, if so propagating if either propagate > 0 or + * PROPAGATE status was set. + * + * @param node the node + * @param propagate the return value from a tryAcquireShared + */ +private void setHeadAndPropagate(Node node, long propagate) { + Node h = head; // Record old head for check below + setHead(node); + /* + * Try to signal next queued node if: + * Propagation was indicated by caller, + * or was recorded (as h.waitStatus either before + * or after setHead) by a previous operation + * (note: this uses sign-check of waitStatus because + * PROPAGATE status may transition to SIGNAL.) + * and + * The next node is waiting in shared mode, + * or we don't know, because it appears null + * + * The conservatism in both of these checks may cause + * unnecessary wake-ups, but only when there are multiple + * racing acquires/releases, so most need signals now or soon + * anyway. + */ + if (propagate > 0 || h == null || h.waitStatus < 0 || + (h = head) == null || h.waitStatus < 0) { + Node s = node.next; +// 如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒 +// 这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒 + if (s == null || s.isShared()) + doReleaseShared(); + } +} + +AQS#releaseShared +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); + return true; + } + return false; +} +doReleaseShared +private void doReleaseShared() { + /* + * Ensure that a release propagates, even if there are other + * in-progress acquires/releases. This proceeds in the usual + * way of trying to unparkSuccessor of head if it needs + * signal. But if it does not, status is set to PROPAGATE to + * ensure that upon release, propagation continues. + * Additionally, we must loop in case a new node is added + * while we are doing this. Also, unlike other uses of + * unparkSuccessor, we need to know if CAS to reset status + * fails, if so rechecking. + */ + for (;;) { + Node h = head; + if (h != null && h != tail) { + int ws = h.waitStatus; +// 表示后继节点需要被唤醒 + if (ws == Node.SIGNAL) { + if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) + continue; // loop to recheck cases + unparkSuccessor(h); + } +//如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去 + else if (ws == 0 && + !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) + continue; // loop on failed CAS + } +//如果头结点没有发生变化,表示设置完成,退出循环 +//如果头结点发生变化,比如说其他线程获取到了锁,将自己设置为了头节点。为了使自己的唤醒动作可以传递给之后的节点,就需要重新进入循环 + if (h == head) // loop if head changed + break; + } +} + + + +独占式超时获取同步状态 +AQS#tryAcquireNanos + +public final boolean tryAcquireNanos(int arg, long nanosTimeout) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + return tryAcquire(arg) || + doAcquireNanos(arg, nanosTimeout); +} +该方法可以超时获取同步状态,即在指定上的时间段内获取同步状态,如果成功返回true,失败则返回false。 +对比另一个获取同步状态的方法acquireInterruptibly,该方法等待时如果被中断,那么会立即返回并抛出InterruptedException;而synchronized即使被中断也仅仅是设置中断标志位,并不会立即返回。 +而tryAcquireNanos不仅支持响应中断,还增加了超时获取的特性。 + +针对超时获取,主要需要计算出需要等待的时间间隔nanosTImeout,为了防止过早通知,nanosTimeout的计算公式为:nanosTimeout -= now – lastTime。now是当前唤醒时间,lastTime为上次唤醒时间。 +如果nanosTimeout大于0,则表示超时时间未到,需要继续等待nanosTimeout纳秒;反之已经超时。 +AQS#doAcquireNanos + +private boolean doAcquireNanos(int arg, long nanosTimeout) + throws InterruptedException { + if (nanosTimeout <= 0L) + return false; + final long deadline = System.nanoTime() + nanosTimeout; + final Node node = addWaiter(Node.EXCLUSIVE); + boolean failed = true; + try { + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return true; + } + nanosTimeout = deadline - System.nanoTime(); + if (nanosTimeout <= 0L) + return false; + if (shouldParkAfterFailedAcquire(p, node) && + nanosTimeout > spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanosTimeout); + if (Thread.interrupted()) + throw new InterruptedException(); + } + } finally { + if (failed) + cancelAcquire(node); + } +} + + + + + + +ReentrantLock +锁是Java编程中最重要的同步机制,除了让临界区互斥执行之外,还可以让释放锁的线程向获取锁的线程发送消息。当线程释放锁时,JMM会把该线程对应的本地cache中的共享变量刷新到主存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得临界区的代码必须从主存中读取共享变量。 + +对比锁和volatile的内存语义可以看出:锁的释放与volatile的写操作有相同的内存语义,锁的获取与volatile的读操作有相同的内存语义。 +公平锁加锁 +ReentrantLock#lock +public void lock() { + sync.lock(); +} + +FairSync#lock +final void lock() { + acquire(1); +} + +FairSync#tryAcquire(重入) +状态值在没有线程持有锁时为0,有线程持有锁时大于0 + +获取状态 +1)如果为0,表示是首次获取,判断同步队列中当前节点是否有前驱节点 + 1.1)如果有前驱节点,那么说明锁已被其他线程占有,返回失败 + 1.2) 如果没有前驱节点,那么说明当前节点为head,CAS将状态设置为1 + 1.2.1) 如果设置成功,那么获取锁成功,将独占锁持有者设置为当前线程 + 1.2.2) 如果设置失败,那么说明锁竞争失败,返回失败 +2)如果不为0,判断独占锁持有者是否是当前线程 + 2.1)如果是,那么说明出现了重入,则将状态++ + 2.2)如果不是,那么说明锁已被其他线程占有,返回失败 + +与非公平的tryAcquire相比,多了一个方法调用hasQueuedPredecessors,即加入了同步队列中当前节点是否有前驱节点的判断。如果有前驱节点,那么有线程比当前线程更早地请求锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 +protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); +// 首次获取 + if (c == 0) { + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } +// 重入 + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } +// 已被其他线程占有 + return false; +} +hasQueuedPredecessors +public final boolean hasQueuedPredecessors() { + // The correctness of this depends on head being initialized + // before tail and on head.next being accurate if the current + // thread is first in queue. + Node t = tail; // Read fields in reverse initialization order + Node h = head; + Node s; + return h != t && + ((s = h.next) == null || s.thread != Thread.currentThread()); +} + +公平锁解锁 +ReentrantLock#unlock +public void unlock() { + sync.release(1); +} + +Sync#tryRelease +1)如果当前线程不是独占锁持有者,则抛出异常。 +2)获取状态,将状态值减一,如果减为0,则将独占锁持有者设置为null,表示当前线程不再持有这个锁 +3)更新状态值 + +protected final boolean tryRelease(int releases) { + int c = getState() - releases; + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; +} + +公平锁总结 +在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享内存,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。 + +非公平锁加锁 +NonfairSync#lock +CAS同时具有volatile读和volatile写的内存语义。 +底层是基于CPU的cmpxchg指令实现的,Intel会规定该指令:禁止该指令与 之前 和 之后 的读写指令重排序;把写缓冲区中的所有数据刷新到内存中。 +这一点就足以同时实现volatile读和volatile写的内存语义了。 + +final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); +} + +AQS#acquire +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} + +NonfairLock#tryAcquire +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} +NonfairLock#nonfairTryAcquire(重入) +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + if (compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} + + + +ReentrantReadWriteLock +读写状态的设计 +读写锁的自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。 +如果在一个整型变量上维护多种状态就需要按位切割使用该变量,读写锁把变量切成了两个部分,高16位表示读,低16位表示写。 + +当前同步状态为S,则写状态=S&0x0000FFFF,读状态=S>>>16。 +写状态加一,就是S+1;读状态加一,就是S+(1<<16)。 +S不为0时,若写状态为0,则读状态大于0,读锁已被获取。 +写锁的获取与释放 +写锁是一个支持重入的排它锁。如果当前线程已经获取了写锁,则增加写状态,如果当前线程在获取写锁时,读锁已经被获取,或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。 +WriteLock#lock +public void lock() { + sync.acquire(1); +} +Sync#tryAcquire +如果存在读锁,那么写锁不能被获取。因为读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都是释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。 + +获取状态 +1)如果状态不为0,计算写状态 + 如果写状态为0(存在读锁)或者独占锁持有者不是当前线程,则返回失败 + 否则是写线程重入的情况,更新写状态++,返回成功 +2)如果状态为0,如果是公平锁,那么判断当前节点是否有后继节点 + 2.1)如果有,则返回失败 + 2.2)如果没有,或者是非公平锁,则CAS更新写状态++ + 2.2.1)如果更新失败,则返回失败 + 2.2.2) 如果更新成功,则将独占锁持有者设置为当前线程,返回成功 +protected final boolean tryAcquire(int acquires) { + Thread current = Thread.currentThread(); + int c = getState(); + int w = exclusiveCount(c); + if (c != 0) { + // (Note: if c != 0 and w == 0 then shared count != 0) + if (w == 0 || current != getExclusiveOwnerThread()) + return false; + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // Reentrant acquire + setState(c + acquires); + return true; + } + if (writerShouldBlock() || + !compareAndSetState(c, c + acquires)) + return false; + setExclusiveOwnerThread(current); + return true; +} + +writerShouldBlock +如果是公平,那么返回当前节点是否有后继节点,即hasQueuedPredecessors;如果是非公平,则直接返回false +WriteLock#unlock +public void unlock() { + sync.release(1); +} + +Sync#tryRelease +protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + int nextc = getState() - releases; + boolean free = exclusiveCount(nextc) == 0; + if (free) + setExclusiveOwnerThread(null); + setState(nextc); + return free; +} +读锁的获取与释放(放弃) +读锁是一个支持重入的共享锁,它能够被多个线程同时获取,在写状态为0时,读锁总会成功地获取,而所做的也只是线程安全地增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已经被其他线程获取,则进入等待状态。 +在Java6中除了保存所有线程获取读锁次数的总和(state的高16位),也保存了每个线程各自获取读锁的次数(ThreadLocal)。 + +ReadLock#lock +public void lock() { + sync.acquireShared(1); +} + + +Sync#tryAcquireShared + +1)如果有线程持有写锁并且不是当前线程,直接返回失败; +2)获取读状态 +2.1)如果是公平锁,那么判断当前节点是否有后继节点 + 2.1.1)如果有,则执行fullTryAcquireShared + 2.1.2) 如果没有,继续执行 +2.2)如果是非公平锁,那么判断 +CAS设置state成功,则设置读锁count的值。这一步并没有检查读锁重入的情况,被延迟到fullTryAcquireShared里了,因为大多数情况下不是重入的; +3.如果步骤2失败了,或许是队列策略返回false或许是CAS设置失败了等,则执行fullTryAcquireShared。 + +protected final int tryAcquireShared(int unused) { + Thread current = Thread.currentThread(); + int c = getState(); +// 有其他写线程,则失败 + if (exclusiveCount(c) != 0 && + getExclusiveOwnerThread() != current) + return -1; + int r = sharedCount(c); + if (!readerShouldBlock() && + r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + cachedHoldCounter = rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + } + return 1; + } + return fullTryAcquireShared(current); +} +readerShouldBlock +如果是公平,那么返回当前节点是否有后继节点,即hasQueuedPredecessors;如果是非公平,则调用apparentlyFirstQueuedIsExclusive +/** + * Returns {@code true} if the apparent first queued thread, if one + * exists, is waiting in exclusive mode. If this method returns + * {@code true}, and the current thread is attempting to acquire in + * shared mode (that is, this method is invoked from {@link + * #tryAcquireShared}) then it is guaranteed that the current thread + * is not the first queued thread. Used only as a heuristic in + * ReentrantReadWriteLock. + */ +final boolean apparentlyFirstQueuedIsExclusive() { + Node h, s; + return (h = head) != null && + (s = h.next) != null && + !s.isShared() && + s.thread != null; +} + +fullTryAcquireShared +final int fullTryAcquireShared(Thread current) { + /* + * This code is in part redundant with that in + * tryAcquireShared but is simpler overall by not + * complicating tryAcquireShared with interactions between + * retries and lazily reading hold counts. + */ + HoldCounter rh = null; + for (;;) { + int c = getState(); + if (exclusiveCount(c) != 0) { +// 有线程持有写锁且不是当前线程,直接失败 + if (getExclusiveOwnerThread() != current) + return -1; +// 如果队列策略不允许,需要检查是否是读锁重入的情况。队列策略是否允许,分两种情况: +// 1.公平模式:如果当前AQS队列前面有等待的结点,返回false;2.非公平模式:如果 +// AQS前面有线程在等待写锁,返回false(这样做的原因是为了防止写饥饿)。 + } else if (readerShouldBlock()) { + // 如果当前线程是第一个获取读锁的线程,则有资格获取读锁 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + } else { +// 优先赋值成上一次获取读锁成功的cache,如果发现线程tid和当前线程不相等,再从ThreadLocal里获取 + if (rh == null) { + rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) { + rh = readHolds.get(); + if (rh.count == 0) + readHolds.remove(); + } + } +// 说明不是读锁重入的情况,直接返回失败了 + if (rh.count == 0) + return -1; + } + } + if (sharedCount(c) == MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + if (compareAndSetState(c, c + SHARED_UNIT)) { +// 设置当前线程为第一个获取读锁的线程 + if (sharedCount(c) == 0) { + firstReader = current; + firstReaderHoldCount = 1; +// 读锁重入 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { +// 其他获取读锁成功的情况 + if (rh == null) + rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + cachedHoldCounter = rh; // cache for release + } + return 1; + } + } +} + +锁降级 +锁降级是指把持有写锁,再获取到读锁,随后释放写锁的过程。 + +锁降级中读锁的获取是否必要?主要是为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞。 + + +LockSupport +public static void park() { + UNSAFE.park(false, 0L); +} + +/** + * Block current thread, returning when a balancing + * unpark occurs, or a balancing unpark has + * already occurred, or the thread is interrupted, or, if not + * absolute and time is not zero, the given time nanoseconds have + * elapsed, or if absolute, the given deadline in milliseconds + * since Epoch has passed, or spuriously (i.e., returning for no + * "reason"). Note: This operation is in the Unsafe class only + * because unpark is, so it would be strange to place it + * elsewhere. + */ +public native void park(boolean isAbsolute, long time); + +Unsafe_Park +UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) + UnsafeWrapper("Unsafe_Park"); + EventThreadPark event; +#ifndef USDT2 + HS_DTRACE_PROBE3(hotspot, thread__park__begin, thread->parker(), (int) isAbsolute, time); +#else /* USDT2 */ + HOTSPOT_THREAD_PARK_BEGIN( + (uintptr_t) thread->parker(), (int) isAbsolute, time); +#endif /* USDT2 */ + JavaThreadParkedState jtps(thread, time != 0); + thread->parker()->park(isAbsolute != 0, time); +#ifndef USDT2 + HS_DTRACE_PROBE1(hotspot, thread__park__end, thread->parker()); +#else /* USDT2 */ + HOTSPOT_THREAD_PARK_END( + (uintptr_t) thread->parker()); +#endif /* USDT2 */ + if (event.should_commit()) { + oop obj = thread->current_park_blocker(); + event.set_klass((obj != NULL) ? obj->klass() : NULL); + event.set_timeout(time); + event.set_address((obj != NULL) ? (TYPE_ADDRESS) cast_from_oop(obj) : 0); + event.commit(); + } +UNSAFE_END +Parker +定义私有属性_counter:可以理解为是否可以调用park的一个许可证,只有_count > 0的时候才能调用; +提供public方法park和unpark支撑阻塞/唤醒线程; +Parker继承PlatformParker +class Parker : public os::PlatformParker { +private: + volatile int _counter ; + Parker * FreeNext ; + JavaThread * AssociatedWith ; // Current association + +public: + Parker() : PlatformParker() { + _counter = 0 ; + FreeNext = NULL ; + AssociatedWith = NULL ; + } +protected: + ~Parker() { ShouldNotReachHere(); } +public: + // For simplicity of interface with Java, all forms of park (indefinite, + // relative, and absolute) are multiplexed into one call. + void park(bool isAbsolute, jlong time); + void unpark(); + + // Lifecycle operators + static Parker * Allocate (JavaThread * t) ; + static void Release (Parker * e) ; +private: + static Parker * volatile FreeList ; + static volatile int ListLock ; + +}; + + +Linux#PlatformParker +linux下的PlatformParker,基于POSIX的线程编写的。 +POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32   +class PlatformParker : public CHeapObj { + protected: + enum { + REL_INDEX = 0, + ABS_INDEX = 1 + }; + int _cur_index; // which cond is in use: -1, 0, 1 + pthread_mutex_t _mutex [1] ; + pthread_cond_t _cond [2] ; // one for relative times and one for abs. + + public: // TODO-FIXME: make dtor private + ~PlatformParker() { guarantee (0, "invariant") ; } + + public: + PlatformParker() { + int status; + status = pthread_cond_init (&_cond[REL_INDEX], os::Linux::condAttr()); + assert_status(status == 0, status, "cond_init rel"); + status = pthread_cond_init (&_cond[ABS_INDEX], NULL); + assert_status(status == 0, status, "cond_init abs"); + status = pthread_mutex_init (_mutex, NULL); + assert_status(status == 0, status, "mutex_init"); + _cur_index = -1; // mark as unused + } +}; +Parker#park +用mutex和condition保护了一个_counter的变量,当park时,这个变量置为了0,当unpark时,这个变量置为1。 + +1、先尝试使用Atomic的xchg,CAS查看counter是否大于0,如果是,那么更新为0,返回 +2、构造一个ThreadBlockInVM,判断如果_counter > 0,可以调用,将_counter置为0,,unlock mutex,返回 +3、根据等待时间调用不同的等待函数等待,如果等待返回正确,将_counter置为0,unlock mutex,返回,park调用成功。 +void Parker::park(bool isAbsolute, jlong time) { + // Ideally we'd do something useful while spinning, such + // as calling unpackTime(). + + // Optional fast-path check: + // Return immediately if a permit is available. + // We depend on Atomic::xchg() having full barrier semantics + // since we are doing a lock-free update to _counter. + if (Atomic::xchg(0, &_counter) > 0) return; + + Thread* thread = Thread::current(); + assert(thread->is_Java_thread(), "Must be JavaThread"); + JavaThread *jt = (JavaThread *)thread; + + // Optional optimization -- avoid state transitions if there's an interrupt pending. + // Check interrupt before trying to wait + if (Thread::is_interrupted(thread, false)) { + return; + } + + // Next, demultiplex/decode time arguments + timespec absTime; + if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all + return; + } + if (time > 0) { + unpackTime(&absTime, isAbsolute, time); + } + + + // Enter safepoint region + // Beware of deadlocks such as 6317397. + // The per-thread Parker:: mutex is a classic leaf-lock. + // In particular a thread must never block on the Threads_lock while + // holding the Parker:: mutex. If safepoints are pending both the + // the ThreadBlockInVM() CTOR and DTOR may grab Threads_lock. + ThreadBlockInVM tbivm(jt); + + // Don't wait if cannot get lock since interference arises from + // unblocking. Also. check interrupt before trying wait + if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) { + return; + } + + int status ; + if (_counter > 0) { // no wait needed + _counter = 0; + status = pthread_mutex_unlock(_mutex); + assert (status == 0, "invariant") ; + // Paranoia to ensure our locked and lock-free paths interact + // correctly with each other and Java-level accesses. + OrderAccess::fence(); + return; + } + +#ifdef ASSERT + // Don't catch signals while blocked; let the running threads have the signals. + // (This allows a debugger to break into the running thread.) + sigset_t oldsigs; + sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals(); + pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs); +#endif + + OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */); + jt->set_suspend_equivalent(); + // cleared by handle_special_suspend_equivalent_condition() or java_suspend_self() + + assert(_cur_index == -1, "invariant"); + if (time == 0) { + _cur_index = REL_INDEX; // arbitrary choice when not timed + status = pthread_cond_wait (&_cond[_cur_index], _mutex) ; + } else { + _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX; + status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ; + if (status != 0 && WorkAroundNPTLTimedWaitHang) { + pthread_cond_destroy (&_cond[_cur_index]) ; + pthread_cond_init (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr()); + } + } + _cur_index = -1; + assert_status(status == 0 || status == EINTR || + status == ETIME || status == ETIMEDOUT, + status, "cond_timedwait"); + +#ifdef ASSERT + pthread_sigmask(SIG_SETMASK, &oldsigs, NULL); +#endif + + _counter = 0 ; + status = pthread_mutex_unlock(_mutex) ; + assert_status(status == 0, status, "invariant") ; + // Paranoia to ensure our locked and lock-free paths interact + // correctly with each other and Java-level accesses. + OrderAccess::fence(); + + // If externally suspended while waiting, re-suspend + if (jt->handle_special_suspend_equivalent_condition()) { + jt->java_suspend_self(); + } +} + +Parker#unpark +将_counter置为1; +判断之前_counter的值: +小于1时,调用pthread_cond_signal唤醒在park中等待的线程,unlock mutex; +等于1时,unlock mutex,返回。 +void Parker::unpark() { + int s, status ; + status = pthread_mutex_lock(_mutex); + assert (status == 0, "invariant") ; + s = _counter; + _counter = 1; + if (s < 1) { + // thread might be parked + if (_cur_index != -1) { + // thread is definitely parked + if (WorkAroundNPTLTimedWaitHang) { + status = pthread_cond_signal (&_cond[_cur_index]); + assert (status == 0, "invariant"); + status = pthread_mutex_unlock(_mutex); + assert (status == 0, "invariant"); + } else { + status = pthread_mutex_unlock(_mutex); + assert (status == 0, "invariant"); + status = pthread_cond_signal (&_cond[_cur_index]); + assert (status == 0, "invariant"); + } + } else { + pthread_mutex_unlock(_mutex); + assert (status == 0, "invariant") ; + } + } else { + pthread_mutex_unlock(_mutex); + assert (status == 0, "invariant") ; + } +} + +wait¬ify(忽略) +1)使用wait、notify、notifyAll时需要先对调用对象加锁 +2)调用wait方法后,会放弃对象的锁,线程状态由运行态转为等待态,并将当前线程放置到对象的等待队列 +3)notify或notifyAll方法调用后,等待线程不会从wait返回,需要调用notify或notifyAll的线程释放锁后,等待线程才有机会从wait返回 +4)notify方法是将等待队列中的一个等待线程能从等待队列移至同步队列中,notifyAll方法是将等待队列中的所有线程全部移至同步队列中,被移动的线程状态由等待态转为阻塞态 +5)从wait方法返回的前提是获得了调用对象的锁 + +wait¬ify前提也是基于monitorenter、monitorexit指令实现的(对应1))。 + +WaitThread首先获取了对象的锁,然后调用对象的wait方法,从而放弃了锁,并进入了对象的等待队列WaitQueue中,进入等待状态。 +由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify方法,将WaitThrad从等待队列WaitQueue移到同步队列SynchronizedQueue中,此时WaitThread状态变为阻塞态。NotifyThread释放了锁之后,WaitThread再次获取到了锁,并从wait方法返回继续执行。 +ObjectMonitor#wait +在HotSpot虚拟机中,monitor采用ObjectMonitor实现。 +void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { + Thread * const Self = THREAD ; + assert(Self->is_Java_thread(), "Must be Java thread!"); + JavaThread *jt = (JavaThread *)THREAD; + + DeferredInitialize () ; + + // Throw IMSX or IEX. + CHECK_OWNER(); + + EventJavaMonitorWait event; + + // check for a pending interrupt + if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) { + // post monitor waited event. Note that this is past-tense, we are done waiting. + if (JvmtiExport::should_post_monitor_waited()) { + // Note: 'false' parameter is passed here because the + // wait was not timed out due to thread interrupt. + JvmtiExport::post_monitor_waited(jt, this, false); + } + if (event.should_commit()) { + post_monitor_wait_event(&event, 0, millis, false); + } + TEVENT (Wait - Throw IEX) ; + THROW(vmSymbols::java_lang_InterruptedException()); + return ; + } + + TEVENT (Wait) ; + + assert (Self->_Stalled == 0, "invariant") ; + Self->_Stalled = intptr_t(this) ; + jt->set_current_waiting_monitor(this); + + // create a node to be put into the queue + // Critically, after we reset() the event but prior to park(), we must check + // for a pending interrupt. + ObjectWaiter node(Self); + node.TState = ObjectWaiter::TS_WAIT ; + Self->_ParkEvent->reset() ; + OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag + + // Enter the waiting queue, which is a circular doubly linked list in this case + // but it could be a priority queue or any data structure. + // _WaitSetLock protects the wait queue. Normally the wait queue is accessed only + // by the the owner of the monitor *except* in the case where park() + // returns because of a timeout of interrupt. Contention is exceptionally rare + // so we use a simple spin-lock instead of a heavier-weight blocking lock. + + Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ; + AddWaiter (&node) ; + Thread::SpinRelease (&_WaitSetLock) ; + + if ((SyncFlags & 4) == 0) { + _Responsible = NULL ; + } + intptr_t save = _recursions; // record the old recursion count + _waiters++; // increment the number of waiters + _recursions = 0; // set the recursion level to be 1 + exit (true, Self) ; // exit the monitor + guarantee (_owner != Self, "invariant") ; + + // As soon as the ObjectMonitor's ownership is dropped in the exit() + // call above, another thread can enter() the ObjectMonitor, do the + // notify(), and exit() the ObjectMonitor. If the other thread's + // exit() call chooses this thread as the successor and the unpark() + // call happens to occur while this thread is posting a + // MONITOR_CONTENDED_EXIT event, then we run the risk of the event + // handler using RawMonitors and consuming the unpark(). + // + // To avoid the problem, we re-post the event. This does no harm + // even if the original unpark() was not consumed because we are the + // chosen successor for this monitor. + if (node._notified != 0 && _succ == Self) { + node._event->unpark(); + } + + // The thread is on the WaitSet list - now park() it. + // On MP systems it's conceivable that a brief spin before we park + // could be profitable. + // + // TODO-FIXME: change the following logic to a loop of the form + // while (!timeout && !interrupted && _notified == 0) park() + + int ret = OS_OK ; + int WasNotified = 0 ; + { // State transition wrappers + OSThread* osthread = Self->osthread(); + OSThreadWaitState osts(osthread, true); + { + ThreadBlockInVM tbivm(jt); + // Thread is in thread_blocked state and oop access is unsafe. + jt->set_suspend_equivalent(); + + if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) { + // Intentionally empty + } else + if (node._notified == 0) { + if (millis <= 0) { + Self->_ParkEvent->park () ; + } else { + ret = Self->_ParkEvent->park (millis) ; + } + } + + // were we externally suspended while we were waiting? + if (ExitSuspendEquivalent (jt)) { + // TODO-FIXME: add -- if succ == Self then succ = null. + jt->java_suspend_self(); + } + + } // Exit thread safepoint: transition _thread_blocked -> _thread_in_vm + + + // Node may be on the WaitSet, the EntryList (or cxq), or in transition + // from the WaitSet to the EntryList. + // See if we need to remove Node from the WaitSet. + // We use double-checked locking to avoid grabbing _WaitSetLock + // if the thread is not on the wait queue. + // + // Note that we don't need a fence before the fetch of TState. + // In the worst case we'll fetch a old-stale value of TS_WAIT previously + // written by the is thread. (perhaps the fetch might even be satisfied + // by a look-aside into the processor's own store buffer, although given + // the length of the code path between the prior ST and this load that's + // highly unlikely). If the following LD fetches a stale TS_WAIT value + // then we'll acquire the lock and then re-fetch a fresh TState value. + // That is, we fail toward safety. + + if (node.TState == ObjectWaiter::TS_WAIT) { + Thread::SpinAcquire (&_WaitSetLock, "WaitSet - unlink") ; + if (node.TState == ObjectWaiter::TS_WAIT) { + DequeueSpecificWaiter (&node) ; // unlink from WaitSet + assert(node._notified == 0, "invariant"); + node.TState = ObjectWaiter::TS_RUN ; + } + Thread::SpinRelease (&_WaitSetLock) ; + } + + // The thread is now either on off-list (TS_RUN), + // on the EntryList (TS_ENTER), or on the cxq (TS_CXQ). + // The Node's TState variable is stable from the perspective of this thread. + // No other threads will asynchronously modify TState. + guarantee (node.TState != ObjectWaiter::TS_WAIT, "invariant") ; + OrderAccess::loadload() ; + if (_succ == Self) _succ = NULL ; + WasNotified = node._notified ; + + // Reentry phase -- reacquire the monitor. + // re-enter contended monitor after object.wait(). + // retain OBJECT_WAIT state until re-enter successfully completes + // Thread state is thread_in_vm and oop access is again safe, + // although the raw address of the object may have changed. + // (Don't cache naked oops over safepoints, of course). + + // post monitor waited event. Note that this is past-tense, we are done waiting. + if (JvmtiExport::should_post_monitor_waited()) { + JvmtiExport::post_monitor_waited(jt, this, ret == OS_TIMEOUT); + } + + if (event.should_commit()) { + post_monitor_wait_event(&event, node._notifier_tid, millis, ret == OS_TIMEOUT); + } + + OrderAccess::fence() ; + + assert (Self->_Stalled != 0, "invariant") ; + Self->_Stalled = 0 ; + + assert (_owner != Self, "invariant") ; + ObjectWaiter::TStates v = node.TState ; + if (v == ObjectWaiter::TS_RUN) { + enter (Self) ; + } else { + guarantee (v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant") ; + ReenterI (Self, &node) ; + node.wait_reenter_end(this); + } + + // Self has reacquired the lock. + // Lifecycle - the node representing Self must not appear on any queues. + // Node is about to go out-of-scope, but even if it were immortal we wouldn't + // want residual elements associated with this thread left on any lists. + guarantee (node.TState == ObjectWaiter::TS_RUN, "invariant") ; + assert (_owner == Self, "invariant") ; + assert (_succ != Self , "invariant") ; + } // OSThreadWaitState() + + jt->set_current_waiting_monitor(NULL); + + guarantee (_recursions == 0, "invariant") ; + _recursions = save; // restore the old recursion count + _waiters--; // decrement the number of waiters + + // Verify a few postconditions + assert (_owner == Self , "invariant") ; + assert (_succ != Self , "invariant") ; + assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; + + if (SyncFlags & 32) { + OrderAccess::fence() ; + } + + // check if the notification happened + if (!WasNotified) { + // no, it could be timeout or Thread.interrupt() or both + // check for interrupt event, otherwise it is timeout + if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) { + TEVENT (Wait - throw IEX from epilog) ; + THROW(vmSymbols::java_lang_InterruptedException()); + } + } + + // NOTE: Spurious wake up will be consider as timeout. + // Monitor notify has precedence over thread interrupt. +} + + + +Condition +每个Condition对象都包含着一个等待队列,该队列是Condition对象实现等待/通知功能的关键。 +Condition的实现类是AQS的内部类ConditionObject。 +ConditionObject +/** First node of condition queue. */ +private transient Node firstWaiter; +/** Last node of condition queue. */ +private transient Node lastWaiter; +等待队列 +等待队列是一个FIFO的队列,在队列的每个节点上都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。 +Condition拥有等待队列的首节点firstWaiter和尾节点lastWaiter。 +每个Node都持有同一个队列中下一个Node的引用。 + +Condition拥有首尾节点的引用,新增节点时仅需将原有的尾节点的nextWaiter指针指向它, 并更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,因为调用await方法的线程必定时获取了锁的线程。 +在Object的监视器模型上,一个对象拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。 + +Condition是AQS同步器的内部类,所以每个Conditions实例都能访问同步器提供的方法。 + +AQS维护了一个同步队列,一个AQS对应多个Condition,每个Condition维护了一个等待队列。 + + + +ConditionObject#await + + +public final void await() throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); +// 当前线程构造为Node,加入到等待队列 + Node node = addConditionWaiter(); +// 释放锁,唤醒同步队列中的后继节点 + long savedState = fullyRelease(node); + int interruptMode = 0; +// 阻塞,直至被其他线程唤醒或中断 + while (!isOnSyncQueue(node)) { + LockSupport.park(this); + if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) + break; + } +// 重新获取锁 + if (acquireQueued(node, savedState) && interruptMode != THROW_IE) + interruptMode = REINTERRUPT; +// 移出等待队列 +if (node.nextWaiter != null) // clean up if cancelled + unlinkCancelledWaiters(); + if (interruptMode != 0) +// 被其他线程中断,抛出InterruptedException异常 + reportInterruptAfterWait(interruptMode); +} +ConditionObject#addConditionWaiter +private Node addConditionWaiter() { + Node t = lastWaiter; + // If lastWaiter is cancelled, clean out. + if (t != null && t.waitStatus != Node.CONDITION) { + unlinkCancelledWaiters(); + t = lastWaiter; + } + Node node = new Node(Thread.currentThread(), Node.CONDITION); + if (t == null) + firstWaiter = node; + else + t.nextWaiter = node; + lastWaiter = node; + return node; +} +AQS#fullyRelease +/** + * Invokes release with current state value; returns saved state. + * Cancels node and throws exception on failure. + * @param node the condition node for this wait + * @return previous sync state + */ +final long fullyRelease(Node node) { + boolean failed = true; + try { + long savedState = getState(); + if (release(savedState)) { + failed = false; + return savedState; + } else { + throw new IllegalMonitorStateException(); + } + } finally { + if (failed) + node.waitStatus = Node.CANCELLED; + } +} +AQS#isOnSyncQueue +/** + * Returns true if a node, always one that was initially placed on + * a condition queue, is now waiting to reacquire on sync queue. + * @param node the node + * @return true if is reacquiring + */ +final boolean isOnSyncQueue(Node node) { + if (node.waitStatus == Node.CONDITION || node.prev == null) + return false; + if (node.next != null) // If has successor, it must be on queue + return true; + /* + * node.prev can be non-null, but not yet on queue because + * the CAS to place it on queue can fail. So we have to + * traverse from tail to make sure it actually made it. It + * will always be near the tail in calls to this method, and + * unless the CAS failed (which is unlikely), it will be + * there, so we hardly ever traverse much. + */ + return findNodeFromTail(node); +} + +/** + * Returns true if node is on sync queue by searching backwards from tail. + * Called only when needed by isOnSyncQueue. + * @return true if present + */ +private boolean findNodeFromTail(Node node) { + Node t = tail; + for (;;) { + if (t == node) + return true; + if (t == null) + return false; + t = t.prev; + } +} + +ConditionObject#checkInterruptWhileWaiting +/** + * Checks for interrupt, returning THROW_IE if interrupted + * before signalled, REINTERRUPT if after signalled, or + * 0 if not interrupted. + */ +private int checkInterruptWhileWaiting(Node node) { + return Thread.interrupted() ? + (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : + 0; +} + +AQS#acquireQueued +/** + * Acquires in exclusive uninterruptible mode for thread already in + * queue. Used by condition wait methods as well as acquire. + * + * @param node the node + * @param arg the acquire argument + * @return {@code true} if interrupted while waiting + */ +final boolean acquireQueued(final Node node, long arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} + +ConditionObject#unlinkCancelledWaiters +/** + * Unlinks cancelled waiter nodes from condition queue. + * Called only while holding lock. This is called when + * cancellation occurred during condition wait, and upon + * insertion of a new waiter when lastWaiter is seen to have + * been cancelled. This method is needed to avoid garbage + * retention in the absence of signals. So even though it may + * require a full traversal, it comes into play only when + * timeouts or cancellations occur in the absence of + * signals. It traverses all nodes rather than stopping at a + * particular target to unlink all pointers to garbage nodes + * without requiring many re-traversals during cancellation + * storms. + */ +private void unlinkCancelledWaiters() { + Node t = firstWaiter; + Node trail = null; + while (t != null) { + Node next = t.nextWaiter; + if (t.waitStatus != Node.CONDITION) { + t.nextWaiter = null; + if (trail == null) + firstWaiter = next; + else + trail.nextWaiter = next; + if (next == null) + lastWaiter = trail; + } + else + trail = t; + t = next; + } +} + +ConditionObject#reportInterruptAfterWait +private void reportInterruptAfterWait(int interruptMode) + throws InterruptedException { + if (interruptMode == THROW_IE) + throw new InterruptedException(); + else if (interruptMode == REINTERRUPT) + selfInterrupt(); +} + +ConditionObject#signal +首先判断当前线程是否获取了锁,然后获取等待队列的首节点,将其移动到同步队列,并使用LockSupport.unpark唤醒节点中的线程。 + +public final void signal() { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + Node first = firstWaiter; + if (first != null) + doSignal(first); +} + +/** + * Removes and transfers nodes until hit non-cancelled one or + * null. Split out from signal in part to encourage compilers + * to inline the case of no waiters. + * @param first (non-null) the first node on condition queue + */ +private void doSignal(Node first) { + do { + if ( (firstWaiter = first.nextWaiter) == null) + lastWaiter = null; + first.nextWaiter = null; + } while (!transferForSignal(first) && + (first = firstWaiter) != null); +} + +AQS#transferForSignal +/** + * Transfers a node from a condition queue onto sync queue. + * Returns true if successful. + * @param node the node + * @return true if successfully transferred (else the node was + * cancelled before signal) + */ +final boolean transferForSignal(Node node) { + /* + * If cannot change waitStatus, the node has been cancelled. + */ + if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) + return false; + + /* + * Splice onto queue and try to set waitStatus of predecessor to + * indicate that thread is (probably) waiting. If cancelled or + * attempt to set waitStatus fails, wake up to resync (in which + * case the waitStatus can be transiently and harmlessly wrong). + */ +// 等待队列中的头节点移动到了同步队列 + Node p = enq(node); + int ws = p.waitStatus; + if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) +// 唤醒该节点的线程 + LockSupport.unpark(node.thread); + return true; +} + + + +Semaphore(暂缓) +CyclicBarrier(暂缓) +CountDownLatch(暂缓) +Exchanger(暂缓) +AtomicInteger +/** + * Atomically increments by one the current value. + * + * @return the updated value + */ +public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; +} + +/** + * Atomically adds the given value to the current value of a field + * or array element within the given object o + * at the given offset. + * + * @param o object/array to update the field/element in + * @param offset field/element offset + * @param delta the value to add + * @return the previous value + * @since 1.8 + */ +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + return v; +} + +/** Volatile version of {@link #getInt(Object, long)} */ +public native int getIntVolatile(Object o, long offset); + +/** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ +public final native boolean compareAndSwapInt(Object o, long offset, + int expected, + int x); + +ThreadPoolExeuctor +线程池中持有一组Runnable,称为Worker,包装为Thread,调用Thread#start(作为一个线程去启动)。它们的run方法是一个循环,不断获取用户提交的Runnable并调用Runnable#run(不是启动线程,仅仅是方法调用)。 +状态转换 + +成员变量 +private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); +private static final int COUNT_BITS = Integer.SIZE - 3; +private static final int CAPACITY = (1 << COUNT_BITS) - 1; + +// runState is stored in the high-order bits +private static final int RUNNING = -1 << COUNT_BITS; +private static final int SHUTDOWN = 0 << COUNT_BITS; +private static final int STOP = 1 << COUNT_BITS; +private static final int TIDYING = 2 << COUNT_BITS; +private static final int TERMINATED = 3 << COUNT_BITS; +private final BlockingQueue workQueue; + +private final ReentrantLock mainLock = new ReentrantLock(); + +private final HashSet workers = new HashSet(); + +private final Condition termination = mainLock.newCondition(); + +private int largestPoolSize; + +private long completedTaskCount; + +private volatile ThreadFactory threadFactory; + +private volatile RejectedExecutionHandler handler; + +private volatile long keepAliveTime; + +private volatile boolean allowCoreThreadTimeOut; + +private volatile int corePoolSize; + +private volatile int maximumPoolSize; + +private static final RejectedExecutionHandler defaultHandler = + new AbortPolicy(); +private static final RuntimePermission shutdownPerm = + new RuntimePermission("modifyThread"); + +/* The context to be used when executing the finalizer, or null. */ +private final AccessControlContext acc; + +一个 ctl 变量可以包含两部分信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount). 由于 int 型的变量是由32位二进制的数构成, 所以用 ctl 的高3位来表示线程池的运行状态, 用低29位来表示线程池内有效线程的数量. 由于这两部分信息在该类中很多地方都会使用到, 所以我们也经常会涉及到要获取其中一个信息的操作, 通常来说, 代表这两个信息的变量的名称直接用他们各自英文单词首字母的组合来表示, 所以, 表示线程池运行状态的变量通常命名为 rs, 表示线程池中有效线程数量的变量通常命名为 wc, 另外, ctl 也通常会简写作 c。 +由于 ctl 变量是由线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)这两个信息组合而成, 所以, 如果知道了这两部分信息各自的数值, 就可以调用下面的 ctlOf() 方法来计算出 ctl 的数值: + +// rs: 表示线程池的运行状态 (rs 是 runState中各单词首字母的简写组合) +// wc: 表示线程池内有效线程的数量 (wc 是 workerCount中各单词首字母的简写组合) +private static int ctlOf(int rs, int wc) { return rs | wc; } +反过来, 如果知道了 ctl 的值, 那么也可以通过如下的 runStateOf() 和 workerCountOf() 两个方法来分别获取线程池的运行状态和线程池内有效线程的数量. +private static int runStateOf(int c) { return c & ~CAPACITY; } +private static int workerCountOf(int c) { return c & CAPACITY; } +其中, CAPACITY 等于 (2^29)-1, 也就是高3位是0, 低29位是1的一个int型的数, +private static final int COUNT_BITS = Integer.SIZE - 3; // 29 +private static final int CAPACITY = (1 << COUNT_BITS) - 1; // COUNT_BITS == 29 +执行任务 + +当提交一个新任务到线程池时,线程池的处理流程如下: +1)线程池判断当前运行的线程是否少于corePoolSize(需要获取全局锁),如果不是,则创建一个新的工作线程来执行任务(分配线程)。如果是,则进入2) +2)线程池判断工作队列是否已经满了,如果没有满,则将新提交的任务存储到这个工作队列BlockingQueue里。如果满了,则进入3) +3)线程池判断创建新的线程是否会使当前运行的线程超过maxPoolSize(需要获取全局锁),如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 + +ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute方法时,尽可能地避免获取全局锁。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute方法都是执行步骤2,步骤2不需要获取全局锁。 +execute(Runnable不进行任何封装) +public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + int c = ctl.get(); +// 如果当前运行线程数小于corePoolSize + if (workerCountOf(c) < corePoolSize) { +// 创建线程并执行当前任务 + if (addWorker(command, true)) + return; + c = ctl.get(); + } +// 如果线程池处于运行状态,且(当前运行线程数大于等于corePoolSize或线程创建失败),则将当前任务放入工作队列 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); +// 再次检查线程池的状态,如果线程池没有运行,且成功从工作队列中删除任务,则执行reject处理任务 + if (!isRunning(recheck) && remove(command)) + reject(command); + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } +// 如果线程池不处于运行中或任务无法放进队列,并且当前线程数小于maxPoolSize,则创建一个线程执行任务 + else if (!addWorker(command, false)) +// 创建线程失败,则执行reject处理任务 + reject(command); +} + + + +1) addWorker +addWorker主要负责创建新的线程并执行任务 +private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + int wc = workerCountOf(c); + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + if (compareAndIncrementWorkerCount(c)) + break retry; + c = ctl.get(); // Re-read ctl + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + + boolean workerStarted = false; + boolean workerAdded = false; + Worker w = null; + try { + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // Recheck while holding lock. + // Back out on ThreadFactory failure or if + // shut down before lock acquired. + int rs = runStateOf(ctl.get()); + + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + workerAdded = true; + } + } finally { + mainLock.unlock(); + } + if (workerAdded) { + t.start(); + workerStarted = true; + } + } + } finally { + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; +} + + +1.1) Worker(基于AQS) +当addWorker中调用t.start()时,这个t是Worker构造方法中使用ThreadFactory创建出来的Thread,且将this作为Runnable传入,启动t时会调用Worker#run方法。 + +private final class Worker + extends AbstractQueuedSynchronizer + implements Runnable +{ + /** + * This class will never be serialized, but we provide a + * serialVersionUID to suppress a javac warning. + */ + private static final long serialVersionUID = 6138294804551838833L; + + /** Thread this worker is running in. Null if factory fails. */ + final Thread thread; + /** Initial task to run. Possibly null. */ + Runnable firstTask; + /** Per-thread task counter */ + volatile long completedTasks; + + /** + * Creates with given first task and thread from ThreadFactory. + * @param firstTask the first task (null if none) + */ + Worker(Runnable firstTask) { + setState(-1); // inhibit interrupts until runWorker + this.firstTask = firstTask; + this.thread = getThreadFactory().newThread(this); + } + + /** Delegates main run loop to outer runWorker */ + public void run() { + runWorker(this); + } + // ... +} + + +1.2) Worker#runWorker +final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + Runnable task = w.firstTask; + w.firstTask = null; + w.unlock(); // allow interrupts + boolean completedAbruptly = true; + try { + while (task != null || (task = getTask()) != null) { + w.lock(); + // If pool is stopping, ensure thread is interrupted; + // if not, ensure thread is not interrupted. This + // requires a recheck in second case to deal with + // shutdownNow race while clearing interrupt + if ((runStateAtLeast(ctl.get(), STOP) || + (Thread.interrupted() && + runStateAtLeast(ctl.get(), STOP))) && + !wt.isInterrupted()) + wt.interrupt(); + try { + beforeExecute(wt, task); + Throwable thrown = null; + try { + task.run(); + } catch (RuntimeException x) { + thrown = x; throw x; + } catch (Error x) { + thrown = x; throw x; + } catch (Throwable x) { + thrown = x; throw new Error(x); + } finally { + afterExecute(task, thrown); + } + } finally { + task = null; + w.completedTasks++; + w.unlock(); + } + } + completedAbruptly = false; + } finally { + processWorkerExit(w, completedAbruptly); + } +} + +1.2.1) ThreadPoolExecutor#getTask +1)判断线程池是否已经关闭,如果关闭,则退出循环 +2)根据当前Worker是否超时,对工作队列调用poll或take方法,进行阻塞。阻塞中的线程就是空闲线程。 + +当调用shutdown方法时,首先设置了线程池的状态为ShutDown,此时1阶段的worker进入到状态判断时会返回null,此时Worker退出。 +因为getTask的时候是不加锁的,所以在shutdown时可以调用worker.Interrupt.此时会中断退出,Loop到状态判断时,同时workQueue为empty。那么抛出中断异常,导致重新Loop,在检测线程池状态时,Worker退出。如果workQueue不为null就不会退出,此处有些疑问,因为没有看见中断标志位清除的逻辑,那么这里就会不停的循环直到workQueue为Empty退出。 +这里也能看出来SHUTDOWN只是清除一些空闲Worker,并且拒绝新Task加入,对于workQueue中的线程还是继续处理的。 +对于shutdown中获取mainLock而addWorker中也做了mainLock的获取,这么做主要是因为Works是HashSet类型的,是线程不安全的,我们也看到在addWorker后面也是对线程池状态做了判断,将Worker添加和中断逻辑分离开。 + +timed变量主要是标识着当前Worker超时是否要退出。wc > corePoolSize时需要减小空闲的Worker数,那么timed为true,但是wc <= corePoolSize时,不能减小核心线程数timed为false。 +timedOut初始为false,如果timed为true那么使用poll取线程。如果正常返回,那么返回取到的task。如果超时,证明worker空闲,同时worker超过了corePoolSize,需要删除。返回r=null。则 timedOut = true。此时循环到wc <= maximumPoolSize && ! (timedOut && timed)时,减小worker数,并返回null,导致worker退出。如果线程数<= corePoolSize,那么此时调用 workQueue.take(),没有线程获取到时将一直阻塞,直到获取到线程或者中断。 +private Runnable getTask() { + boolean timedOut = false; // Did the last poll() time out? + + for (;;) { + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { + decrementWorkerCount(); + return null; + } + + int wc = workerCountOf(c); + // Worker是否要减少 + // Are workers subject to culling? + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + + if ((wc > maximumPoolSize || (timed && timedOut)) + && (wc > 1 || workQueue.isEmpty())) { + if (compareAndDecrementWorkerCount(c)) + return null; + continue; + } + // 如果要减少Worker的话,如果在keepAliveTime内没有拿到任务,那么设置为超时,下次循环被会移除;如果不需要减少Worker,那么阻塞获取任务 + try { + Runnable r = timed ? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : + workQueue.take(); + if (r != null) + return r; + timedOut = true; + } catch (InterruptedException retry) { + timedOut = false; + } + } +} + + + +2) reject +final void reject(Runnable command) { + handler.rejectedExecution(command, this); +} + +2.1) AbortPolicy#rejectedExecution +public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + throw new RejectedExecutionException("Task " + r.toString() + + " rejected from " + + e.toString()); +} + +2.2) DiscardPolicy#rejectedExecution +public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { +} + +2.3) DiscardOldestPolicy#rejectedExecution +public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + e.getQueue().poll(); + e.execute(r); + } +} + +2.4) CallerRunsPolicy#rejectedExecution +public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + r.run(); + } +} + + + +submit(Callable包装为FutureTask) +public Future submit(Callable task) { + if (task == null) throw new NullPointerException(); + RunnableFuture ftask = newTaskFor(task); + execute(ftask); + return ftask; +} +1) newTaskFor(将Callable包装成Runnable+Future,Runnable可以放在ThreadPoolExecutor中执行) +protected RunnableFuture newTaskFor(Callable callable) { + return new FutureTask(callable); +} + + +public FutureTask(Callable callable) { + if (callable == null) + throw new NullPointerException(); + this.callable = callable; + this.state = NEW; // ensure visibility of callable +} + +1.1) FutureTask#run +public void run() { + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, + null, Thread.currentThread())) + return; + try { + Callable c = callable; + if (c != null && state == NEW) { + V result; + boolean ran; + try { + result = c.call(); + ran = true; + } catch (Throwable ex) { + result = null; + ran = false; + setException(ex); + } + if (ran) + set(result); + } + } finally { + // runner must be non-null until state is settled to + // prevent concurrent calls to run() + runner = null; + // state must be re-read after nulling runner to prevent + // leaked interrupts + int s = state; + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } +} +1.1.1) FutureTask#set +protected void set(V v) { + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + outcome = v; + UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state + finishCompletion(); + } +} +1.1.2) FutureTask#setException +protected void setException(Throwable t) { + if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { + outcome = t; + UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state + finishCompletion(); + } +} + + + +1.1.1.1) FutureTask#finishCompletion(唤醒Waiter) +private void finishCompletion() { + // assert state > COMPLETING; + for (WaitNode q; (q = waiters) != null;) { + if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { + for (;;) { + Thread t = q.thread; + if (t != null) { + q.thread = null; + LockSupport.unpark(t); + } + WaitNode next = q.next; + if (next == null) + break; + q.next = null; // unlink to help gc + q = next; + } + break; + } + } + + done(); + + callable = null; // to reduce footprint +} + +2) FutureTask#get +public V get() throws InterruptedException, ExecutionException { + int s = state; + if (s <= COMPLETING) +// 如果尚未执行完毕,则等待 + s = awaitDone(false, 0L); + return report(s); +} + +/** + * Simple linked list nodes to record waiting threads in a Treiber + * stack. See other classes such as Phaser and SynchronousQueue + * for more detailed explanation. + */ +static final class WaitNode { + volatile Thread thread; + volatile WaitNode next; + WaitNode() { thread = Thread.currentThread(); } +} + +2.1) FutureTask#awaitDone(添加并阻塞Waiter) +private int awaitDone(boolean timed, long nanos) + throws InterruptedException { + final long deadline = timed ? System.nanoTime() + nanos : 0L; + WaitNode q = null; + boolean queued = false; + for (;;) { + if (Thread.interrupted()) { + removeWaiter(q); + throw new InterruptedException(); + } + + int s = state; + if (s > COMPLETING) { + if (q != null) + q.thread = null; + return s; + } + else if (s == COMPLETING) // cannot time out yet + Thread.yield(); + else if (q == null) + q = new WaitNode(); + else if (!queued) +// 添加Waiter,以待被唤醒 + queued = UNSAFE.compareAndSwapObject(this, waitersOffset, + q.next = waiters, q); + else if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + removeWaiter(q); + return state; + } + LockSupport.parkNanos(this, nanos); + } + else + LockSupport.park(this); + } +} + +2.2) FutureTask#report(抛出执行时的异常) +private V report(int s) throws ExecutionException { + Object x = outcome; + if (s == NORMAL) + return (V)x; + if (s >= CANCELLED) + throw new CancellationException(); + throw new ExecutionException((Throwable)x); +} + +3) FutureTask#cancel(实际上还是中断线程) +public boolean cancel(boolean mayInterruptIfRunning) { + if (!(state == NEW && + UNSAFE.compareAndSwapInt(this, stateOffset, NEW, + mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) + return false; + try { // in case call to interrupt throws exception + if (mayInterruptIfRunning) { + try { + Thread t = runner; + if (t != null) + t.interrupt(); + } finally { // final state + UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); + } + } + } finally { + finishCompletion(); + } + return true; +} + + + +关闭线程池 +shutdown方法会遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。 +shutdownNow方法会首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有在执行任务的线程。 +只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这时调用isTerminated方法会返回true。 +shutdown + +public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { +// 判断是否可以操作目标线程 + checkShutdownAccess(); +// 设置线程池状态为SHUTDOWN,以后线程池不会执行新的任务 + advanceRunState(SHUTDOWN); +// 中断所有的空闲线程 + interruptIdleWorkers(); + onShutdown(); // hook for ScheduledThreadPoolExecutor + } finally { + mainLock.unlock(); + } +// 转到Terminate + tryTerminate(); +} + +1) interruptIdleWorkers +private void interruptIdleWorkers() { + interruptIdleWorkers(false); +} +中断worker,但是中断之前需要先获取锁,这就意味着正在运行的Worker不能中断。但是上面的代码有w.tryLock(),那么获取不到锁就不会中断,shutdown的Interrupt只是对所有的空闲Worker(正在从workQueue中取Task,此时Worker没有加锁)发送中断信号。 +private void interruptIdleWorkers(boolean onlyOne) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + for (Worker w : workers) { + Thread t = w.thread; + if (!t.isInterrupted() && w.tryLock()) { +//tryLock能获取到的Worker都是空闲的Worker,因为Worker在执行任务时是要拿到Worker的Lock的 + try { +// 让阻塞在工作队列中的Worker中断 + t.interrupt(); + } catch (SecurityException ignore) { + } finally { + w.unlock(); + } + } + if (onlyOne) + break; + } + } finally { + mainLock.unlock(); + } +} + +shutdownNow +public List shutdownNow() { + List tasks; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + advanceRunState(STOP); + interruptWorkers(); +// 工作队列中没有执行的任务全部抛弃 + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + tryTerminate(); + return tasks; +} +1) interruptWorkers +private void interruptWorkers() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + for (Worker w : workers) + w.interruptIfStarted(); + } finally { + mainLock.unlock(); + } +} + +Worker#interruptIfStarted +void interruptIfStarted() { + Thread t; + if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { + try { + t.interrupt(); + } catch (SecurityException ignore) { + } + } +} + +1.1) drainQueue +*/ +private List drainQueue() { + BlockingQueue q = workQueue; + ArrayList taskList = new ArrayList(); + q.drainTo(taskList); + if (!q.isEmpty()) { + for (Runnable r : q.toArray(new Runnable[0])) { + if (q.remove(r)) + taskList.add(r); + } + } + return taskList; +} + + +1.2) tryTerminate(两种关闭都会调用)TIDYING和TERMINATED的转化 +有几种状态是不能转化到TIDYING(整理中)的: +RUNNING状态 +TIDYING或TERMINATED +SHUTDOWN状态,但是workQueue不为空 + +也说明了两点: +1. SHUTDOWN想转化为TIDYING,需要workQueue为空,同时workerCount为0。 +2. STOP转化为TIDYING,需要workerCount为0 +如果满足上面的条件(一般一定时间后都会满足的),那么CAS成TIDYING,TIDYING也只是个过渡状态,最终会转化为TERMINATED。 +final void tryTerminate() { + for (;;) { + int c = ctl.get(); + if (isRunning(c) || + runStateAtLeast(c, TIDYING) || + (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) + return; + if (workerCountOf(c) != 0) { // Eligible to terminate + interruptIdleWorkers(ONLY_ONE); + return; + } + + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { + try { + terminated(); + } finally { + ctl.set(ctlOf(TERMINATED, 0)); + termination.signalAll(); + } + return; + } + } finally { + mainLock.unlock(); + } + // else retry on failed CAS + } +} + +ScheduledThreadPoolExecutor(继承ThreadPoolExecutor) +与ThreadPoolExecutor的区别: +1)使用DelayedWorkQueue作为任务队列 +2)获取任务的方式不同 +3)执行周期任务后,增加了额外的处理 +成员变量 +/** + * False if should cancel/suppress periodic tasks on shutdown. + */ +private volatile boolean continueExistingPeriodicTasksAfterShutdown; + +/** + * False if should cancel non-periodic tasks on shutdown. + */ +private volatile boolean executeExistingDelayedTasksAfterShutdown = true; + +/** + * True if ScheduledFutureTask.cancel should remove from queue + */ +private volatile boolean removeOnCancel = false; + +/** + * Sequence number to break scheduling ties, and in turn to + * guarantee FIFO order among tied entries. + */ +private static final AtomicLong sequencer = new AtomicLong(); + + +构造方法 +/** + * Creates a new {@code ScheduledThreadPoolExecutor} with the + * given core pool size. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @throws IllegalArgumentException if {@code corePoolSize < 0} + */ +public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); +} + +/** + * Creates a new {@code ScheduledThreadPoolExecutor} with the + * given initial parameters. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param threadFactory the factory to use when the executor + * creates a new thread + * @throws IllegalArgumentException if {@code corePoolSize < 0} + * @throws NullPointerException if {@code threadFactory} is null + */ +public ScheduledThreadPoolExecutor(int corePoolSize, + ThreadFactory threadFactory) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue(), threadFactory); +} + +/** + * Creates a new ScheduledThreadPoolExecutor with the given + * initial parameters. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param handler the handler to use when execution is blocked + * because the thread bounds and queue capacities are reached + * @throws IllegalArgumentException if {@code corePoolSize < 0} + * @throws NullPointerException if {@code handler} is null + */ +public ScheduledThreadPoolExecutor(int corePoolSize, + RejectedExecutionHandler handler) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue(), handler); +} + +/** + * Creates a new ScheduledThreadPoolExecutor with the given + * initial parameters. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param threadFactory the factory to use when the executor + * creates a new thread + * @param handler the handler to use when execution is blocked + * because the thread bounds and queue capacities are reached + * @throws IllegalArgumentException if {@code corePoolSize < 0} + * @throws NullPointerException if {@code threadFactory} or + * {@code handler} is null + */ +public ScheduledThreadPoolExecutor(int corePoolSize, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue(), threadFactory, handler); +} + + + +DelayedWorkQueue(底层是堆,无界阻塞队列,存放RunnableScheduledFuture) +static class DelayedWorkQueue extends AbstractQueue + implements BlockingQueue {} + +put +public void put(Runnable e) { + offer(e); +} + +public boolean offer(Runnable x) { + if (x == null) + throw new NullPointerException(); + RunnableScheduledFuture e = (RunnableScheduledFuture)x; + final ReentrantLock lock = this.lock; + lock.lock(); + try { + int i = size; + if (i >= queue.length) + grow(); + size = i + 1; + if (i == 0) { + queue[0] = e; + setIndex(e, 0); + } else { + siftUp(i, e); + } + if (queue[0] == e) { + leader = null; + available.signal(); + } + } finally { + lock.unlock(); + } + return true; +} + +private void siftUp(int k, RunnableScheduledFuture key) { + while (k > 0) { + int parent = (k - 1) >>> 1; + RunnableScheduledFuture e = queue[parent]; + if (key.compareTo(e) >= 0) + break; + queue[k] = e; + setIndex(e, k); + k = parent; + } + queue[k] = key; + setIndex(key, k); +} + +ScheduledFutureTask是RunnableScheduledFuture的唯一实现类,它实现了Comparable接口。先按照time(下一次运行的时间)比较,然后按seq比较,最后按delay比较。 +public int compareTo(Delayed other) { + if (other == this) // compare zero if same object + return 0; + if (other instanceof ScheduledFutureTask) { + ScheduledFutureTask x = (ScheduledFutureTask)other; + long diff = time - x.time; + if (diff < 0) + return -1; + else if (diff > 0) + return 1; + else if (sequenceNumber < x.sequenceNumber) + return -1; + else + return 1; + } + long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); + return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; +} + +take(阻塞获取) +public RunnableScheduledFuture take() throws InterruptedException { + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + for (;;) { + RunnableScheduledFuture first = queue[0]; + if (first == null) +// 队列为空,则阻塞 + available.await(); + else { + long delay = first.getDelay(NANOSECONDS); +// 已经过期,则移除 + if (delay <= 0) + return finishPoll(first); + first = null; // don't retain ref while waiting + if (leader != null) + available.await(); + else { + Thread thisThread = Thread.currentThread(); + leader = thisThread; + try { +// 未过期,则等待相应时间 + available.awaitNanos(delay); + } finally { + if (leader == thisThread) + leader = null; + } + } + } + } + } finally { + if (leader == null && queue[0] != null) + available.signal(); + lock.unlock(); + } +} + + + +定期执行任务 +执行主要分为两大部分: +1)当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或 +scheduleAtFixedDelay()方法时,会向DelayedWorkQueue添加一个实现了RunnableScheduledFuture接口的ScheduledFutureTask。 +2)线程池中的线程从DelayedWorkQueue中获取ScheduledFutureTask,然后执行任务。 + + + + + +scheduleAtFixedRate(Runnable包装为ScheduledFutureTask) +public ScheduledFuture scheduleAtFixedRate(Runnable command, + long initialDelay, + long period, + TimeUnit unit) { + if (command == null || unit == null) + throw new NullPointerException(); + if (period <= 0) + throw new IllegalArgumentException(); +// 将Runnable包装成ScheduledFutureTask,它实现了RunnableScheduledFuture接口 + ScheduledFutureTask sft = + new ScheduledFutureTask(command, + null, + triggerTime(initialDelay, unit), + unit.toNanos(period)); + RunnableScheduledFuture t = decorateTask(command, sft); + sft.outerTask = t; + delayedExecute(t); + return t; +} +1) ScheduledFutureTask +private class ScheduledFutureTask + extends FutureTask implements RunnableScheduledFuture {} +成员变量 +/** Sequence number to break ties FIFO */ +private final long sequenceNumber; + +/** The time the task is enabled to execute in nanoTime units */ +private long time; + +/** + * Period in nanoseconds for repeating tasks. A positive + * value indicates fixed-rate execution. A negative value + * indicates fixed-delay execution. A value of 0 indicates a + * non-repeating task. + */ +private final long period; + +/** The actual task to be re-enqueued by reExecutePeriodic */ +RunnableScheduledFuture outerTask = this; + +/** + * Index into delay queue, to support faster cancellation. + */ +int heapIndex; +构造方法 +ScheduledFutureTask(Runnable r, V result, long ns, long period) { + super(r, result); + this.time = ns; + this.period = period; + this.sequenceNumber = sequencer.getAndIncrement(); +} + +public FutureTask(Runnable runnable, V result) { + this.callable = Executors.callable(runnable, result); + this.state = NEW; // ensure visibility of callable +} + +public static Callable callable(Runnable task, T result) { + if (task == null) + throw new NullPointerException(); + return new RunnableAdapter(task, result); +} + +static final class RunnableAdapter implements Callable { + final Runnable task; + final T result; + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + public T call() { + task.run(); + return result; + } +} + + +2) delayedExecute(入队) +private void delayedExecute(RunnableScheduledFuture task) { + if (isShutdown()) + reject(task); + else { +// 加入到任务队列中 + super.getQueue().add(task); + if (isShutdown() && + !canRunInCurrentRunState(task.isPeriodic()) && + remove(task)) + task.cancel(false); + else +// 确保至少一个线程在处理任务,即使核心线程数corePoolSize为0 + ensurePrestart(); + } +} +2.1) ensurePrestart(添加工作线程直至corePoolSize) +void ensurePrestart() { + int wc = workerCountOf(ctl.get()); + if (wc < corePoolSize) + addWorker(null, true); + else if (wc == 0) + addWorker(null, false); +} + +3) ScheduledFutureTask#run +public void run() { +// 判断是不是定时周期调度的 + boolean periodic = isPeriodic(); + if (!canRunInCurrentRunState(periodic)) + cancel(false); + else if (!periodic) +//调用FutureTask的run方法 + ScheduledFutureTask.super.run(); + else if (ScheduledFutureTask.super.runAndReset()) { +//计算下一次执行时间 + setNextRunTime(); +// 重新入队 + reExecutePeriodic(outerTask); + } +} + +private void setNextRunTime() { + long p = period; + if (p > 0) + time += p; + else + time = triggerTime(-p); +} + +void reExecutePeriodic(RunnableScheduledFuture task) { + if (canRunInCurrentRunState(true)) { + super.getQueue().add(task); + if (!canRunInCurrentRunState(true) && remove(task)) + task.cancel(false); + else + ensurePrestart(); + } +} + + + +练习题 +生产者消费者几种实现方式 + +wait¬ify +public class TestProducerConsumer { + public static void main(String[] args) { + SyncStack ss = new SyncStack(); + Producer p = new Producer(ss); + Consumer c = new Consumer(ss); + new Thread(p, "A").start(); + new Thread(p, "B").start(); + new Thread(c).start(); + } +} + +class Food { + private String id; + + public Food(String id) { + this.id = id; + } + + public String toString() { + return "产品" + id; + } +} + +class SyncStack { + private int index = 0; + private Food[] foods = new Food[6]; + + public SyncStack() { + } + + public synchronized void push(Food f) { + while (index == foods.length) { + try { + System.out.println("容器已满"); + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + foods[index] = f; + index++; + this.notifyAll(); + } + + public synchronized Food pop() { + while (index == 0) { + try { + System.out.println("容器已空"); + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + index--; + this.notifyAll(); + return foods[index]; + } +} + +class Producer implements Runnable { + private SyncStack ss; + + public Producer(SyncStack ss) { + this.ss = ss; + } + + public void run() { + for (int i = 0; i < 10; i++) { + Food f = new Food(Thread.currentThread().getName() + i); + ss.push(f); + System.out.println("生产者"+Thread.currentThread().getName() + "生产了 " + f); + try { + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +class Consumer implements Runnable { + private SyncStack ss; + + public Consumer(SyncStack ss) { + this.ss = ss; + } + + public void run() { + for (int i = 0; i < 20; i++) { + Food f = ss.pop(); + System.out.println("消费了 " + f); + try { + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + + +Lock&Condition +public class TestProducerConsumer { + public static void main(String[] args) { + SyncStack ss = new SyncStack(); +Producer p = new Producer(ss); +Consumer c = new Consumer(ss); + new Thread(p, "A").start(); + new Thread(p, "B").start(); + new Thread(c, "C").start(); + new Thread(c, "D").start(); + } +} + +class Food { + private String id; + + public Food(String id) { + this.id = id; + } + + public String toString() { + return "产品" + id; + } +} + +class SyncStack { + private int index = 0; + private Food[] foods = new Food[6]; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public SyncStack() { + } + + public void push(Food f) { + lock.lock(); + try { + while (index == foods.length) { + try { + System.out.println("容器已满"); + condition.await(); + //相当于this.wait() + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + foods[index] = f; + index++; + condition.signalAll(); + //相当于this.notifyAll() + } finally { + lock.unlock(); + } + } + + public Food pop() { + lock.lock(); + try { + while (index == 0) { + try { + System.out.println("容器已空"); + condition.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + index--; + condition.signalAll(); + } finally { + lock.unlock(); + } + return foods[index]; + } +} + +class Producer implements Runnable { + private SyncStack ss; + + public Producer(SyncStack ss) { + this.ss = ss; + } + + public void run() { + for (int i = 0; i < 10; i++) { + Food f = new Food(Thread.currentThread().getName() + i); + ss.push(f); + System.out.println("生产者" + Thread.currentThread().getName() + "生产了 " + f); + try { + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +class Consumer implements Runnable { + private SyncStack ss; + + public Consumer(SyncStack ss) { + this.ss = ss; + } + + public void run() { + for (int i = 0; i < 10; i++) { + Food f = ss.pop(); + System.out.println("消费者" + Thread.currentThread().getName() + "消费了 " + f); + try { + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + + + +BlockingQueue +public class TestProducerConsumer { + public static void main(String[] args) { + BlockingQueue queue = new ArrayBlockingQueue(6); +Producer p = new Producer(queue); +Consumer c = new Consumer(queue); + new Thread(p, "A").start(); + new Thread(p, "B").start(); + new Thread(c, "C").start(); + new Thread(c, "D").start(); + } +} + +class Food { + private String id; + + public Food(String id) { + this.id = id; + } + + public String toString() { + return "产品" + id; + } +} + +class Producer implements Runnable { + private BlockingQueue foods; + + public Producer(BlockingQueue foods) { + this.foods = foods; + } + + public void run() { + for (int i = 0; i < 10; i++) { + Food f = new Food(Thread.currentThread().getName() + i); + try { + foods.put(f); + System.out.println("生产者" + Thread.currentThread().getName() + "生产了 " + f); + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +class Consumer implements Runnable { + + private BlockingQueue foods; + + public Consumer(BlockingQueue foods) { + this.foods = foods; + } + + public void run() { + for (int i = 0; i < 10; i++) { + try { + Food f = foods.take(); + System.out.println("消费者" + Thread.currentThread().getName() + "消费了 " + f); + Thread.sleep((int) (Math.random() * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + + + +线程按序交替执行 +设置3个线程,线程名分别为123,按照123的顺序打印,重复20遍。 +public class TestAlternate { + public static void main(String[] args) { + int threadNum = 3; + int loopTimes = 20; + AlternativeDemo atomicDemo = new AlternativeDemo(threadNum, loopTimes); + for (int i = 1; i <= threadNum; ++i) { + new Thread(atomicDemo, String.valueOf(i)).start(); + } + } + // 所有线程共享lock和conditions + private static class AlternativeDemo implements Runnable { + private int nextThread = 1; + private Lock lock = new ReentrantLock(); + private Condition[] conditions; + private int totalTimes; + + public AlternativeDemo(int threadNum, int totalTimes) { + this.totalTimes = totalTimes; + this.conditions = new Condition[threadNum]; + for (int i = 0; i < threadNum; ++i) { + conditions[i] = lock.newCondition(); + } + } + + public void run() { + for (int i = 1; i <= totalTimes; ++i) { + lock.lock(); + // currentThread 取值为1,2,3 + // currentThread-1为当前线程对应的Condition + int currentThread = Thread.currentThread().getName().charAt(0) - '0'; + try { +// 下一个不是自己,则等待 + if (currentThread != nextThread) { + conditions[currentThread - 1].await(); + } + System.out.println("线程" + currentThread + ":" + currentThread); +// 计算下一个要打印的线程 + // 3 % 3 + 1 = 1 线程3后面的是线程1 + nextThread = nextThread % conditions.length + 1; +// 唤醒下一个要打印的线程 + conditions[nextThread - 1].signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + } + } +} + + +线程同步的基本使用习题 +public class Test8Questions { + + public static void main(String[] args) { + Number number = new Number(); + Number number2 = new Number(); + + new Thread(new Runnable() { + @Override + public void run() { + number.getOne(); + } + }).start(); + + new Thread(new Runnable() { + @Override + public void run() { +// number.getTwo(); + number2.getTwo(); + } + }).start(); + + /*new Thread(new Runnable() { + @Override + public void run() { + number.getThree(); + } + }).start();*/ + + } + +} + +class Number{ + + public static synchronized void getOne(){//Number.class + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + } + + System.out.println("one"); + } + + public synchronized void getTwo(){//this + System.out.println("two"); + } + + public void getThree(){ + System.out.println("three"); + } + +} + +1、持有同一个Number对象,并都加了锁,按调用顺序打印 +2、sleep方法不会释放锁 +3、普通方法不需要锁定对象,直接调用,最先调用 +4、两个Number对象,实际上并没有并发访问资源 +5、静态同步方法锁定的是类的Class对象,非静态同步方法锁定的是类的实例,同4,没有并发访问资源 +6、两个方法均为静态同步方法,此时构成并发访问,因为它们锁定的是类的同一个Class对象 +7、同4,没有并发访问资源 +8、同6,虽然是不同实例,但对应着同一个Class对象 + + + +直击灵魂的Interrupt七问 +1.Thread.interrupt()方法和InterruptedException异常的关系?是由interrupt触发产生了InterruptedException异常? +2.Thread.interrupt()会中断线程什么状态的工作? RUNNING or BLOCKING? +3.一般Thread编程需要关注interrupt中断不?一般怎么处理?可以用来做什么? +4.LockSupport.park()和unpark(),与object.wait()和notify()的区别? +5.LockSupport.park(Object blocker)传递的blocker对象做什么用? +6.LockSupport能响应Thread.interrupt()事件不?会抛出InterruptedException异常? +7.Thread.interrupt()处理是否有对应的回调函数?类似于钩子调用? + +1. Thread.interrupt()只是在Object.wait() .Object.join(), Object.sleep()几个方法会主动抛出InterruptedException异常。而在其他的block场景,只是通过设置了Thread的一个标志位信息,需要程序自己进行处理。 +在J.U.C里面的ReentrantLock、Condition等源码都是自己去检测中断标志位,然后抛出InterruptedException。 +if (Thread.interrupted()) // Clears interrupted status! +throw new InterruptedException(); + +2. Thread.interrupt设计的目的主要是用于处理线程处于block状态,比如wait(),sleep()状态就是个例子。但可以在程序设计时为支持task cancel,同样可以支持RUNNING状态。比如Object.join()和一些支持interrupt的一些nio channel设计。 + +3. interrupt用途: unBlock操作,支持任务cancel, 数据清理等。 + +4. +1) 面向的主体不一样。LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。 +2) 实现机制不同。虽然LockSuport可以指定monitor的object对象,但和object.wait(),两者的阻塞队列并不交叉。可以看下测试例子。object.notifyAll()不能唤醒LockSupport的阻塞Thread. + +5. 对应的blcoker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象. + +6. 能响应interrupt事件,但不会抛出InterruptedException异常 From 62b06e5c4013138294eea172db1a819df7a91825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 17:11:38 +0800 Subject: [PATCH 29/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 63 ++++++++++++++------------ 1 file changed, 33 insertions(+), 30 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 7dd275c3..295b33c1 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -1,75 +1,78 @@ -并发框架 -Doug Lea +# 一. 并发框架 +## Doug Lea 如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。 说他是这个世界上对Java影响力最大的个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。 值得一提的是: Doug Lea也是JCP (Java社区项目)中的一员。 Doug是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出不一样的火花。《Effective JAVA》这本Java经典之作的作者Joshua Bloch便在书中特别感谢Doug Lea是此书中许多构想的共鸣板,感谢Doug Lea大方分享丰富而又宝贵的知识。 -线程 -线程的状态 -线程的几种实现方式 +# 一.线程 +## 0.关于线程你需要搞懂这些: +- 线程的状态 +- 线程的几种实现方式 +- 三个线程轮流打印ABC十次 +- 判断线程是否销毁 +- yield功能 +- 给定三个线程t1,t2,t3,如何保证他们依次执行 -三个线程轮流打印ABC十次 -判断线程是否销毁 -yield功能 -给定三个线程t1,t2,t3,如何保证他们依次执行 -基本概念 +## 1. 基本概念 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8734.png) +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8735.png) - -线程的启动 -1)实现Runnable接口 -1.自定义一个线程,实现Runnable接口的run方法 +## 2. 线程的启动 +### 2.1 实现Runnable接口 +- 1.自定义一个线程,实现Runnable接口的run方法 run方法就是要执行的内容,会在另一个分支上进行 Thread类本身也实现了Runnable接口 -2.主方法中new一个自定义线程对象,然后new一个Thread类对象,其构造方法的参数是自定义线程对象 -3.执行Thread类的start方法,线程开始执行 +- 2.主方法中new一个自定义线程对象,然后new一个Thread类对象,其构造方法的参数是自定义线程对象 +- 3.执行Thread类的start方法,线程开始执行 自此产生了分支,一个分支会执行run方法,在主方法中不会等待run方法调用完毕返回才继续执行,而是直接继续执行,是第二个分支。这两个分支并行运行 这里运用了静态代理模式: Thread类和自定义线程类都实现了Runnable接口 Thread类是代理Proxy,自定义线程类是被代理类 通过调用Thread的start方法,实际上调用了自定义线程类的start方法(当然除此之外还有其他的代码) -2)继承Thread类 -①自定义一个类MyThread,继承Thread类,重写run方法 -②在main方法中new一个自定义类,然后直接调用start方法 +### 2.2 继承Thread类 +- 自定义一个类MyThread,继承Thread类,重写run方法 +- 在main方法中new一个自定义类,然后直接调用start方法 两个方法比较而言第二个方法代码量较少 但是第一个方法比较灵活,自定义线程类还可以继承其他的类,而不限于Thread类 -3)实现Callable接口 - +### 2.3 实现Callable接口 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8736.png) -线程的状态 -初始态:NEW +## 3. 线程的状态 +### 初始态:NEW 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。 -运行态:RUNNABLE +### 运行态:RUNNABLE 在Java中,运行态包括就绪态 和 运行态。 -就绪态 READY +### 就绪态 READY 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 所有就绪态的线程存放在就绪队列中。 -运行态 RUNNING +### 运行态 RUNNING 获得CPU执行权,正在执行的线程。 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。 -阻塞态 BLOCKED +### 阻塞态 BLOCKED 阻塞态专指请求排它锁失败时进入的状态。 -等待态 WAITING +### 等待态 WAITING 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 进入等待态的线程会释放CPU执行权,并释放资源(如:锁),它们要等待被其他线程显式地唤醒。 -超时等待态 TIME_WAITING +### 超时等待态 TIME_WAITING 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态; 进入该状态后释放CPU执行权 和 占有的资源。 与等待态的区别:无需等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。 -终止态 +### 终止态 线程执行结束后的状态。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8737.png) 线程的方法 getName From 4684042a2c6a753194266303bb249762f5d0cdad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 17:33:37 +0800 Subject: [PATCH 30/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 102 ++++++++++++------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 295b33c1..1c90abb6 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -73,60 +73,60 @@ Thread类是代理Proxy,自定义线程类是被代理类 线程执行结束后的状态。 ![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8737.png) -线程的方法 +## 4. 线程的方法 -getName +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8738.png) +### getName Thread类的构造方法1 - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8739.png) Thread类的构造方法2 - -new 一个子类对象的同时也new了其父类的对象,只是如果不显式调用父类的构造方法super(),那么会自动调用无参数的父类的构造方法。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8740.png) +- new 一个子类对象的同时也new了其父类的对象,只是如果不显式调用父类的构造方法super(),那么会自动调用无参数的父类的构造方法。 可以在自定义类MyThread中(继承自Thread类)中写一个构造方法,显式调用父类的构造方法,其参数为一个字符串,表示创建一个以该字符串为名字的Thread对象。 -效果是创建了一个MyThread对象,并且其父类Thread对象的名字是给定的字符串。 -如果不显式调用父类的构造方法super(参数),那么默认父类Thread是没有名字的。 -isAlive -isAlive活着的定义是就绪、运行、阻塞状态 +- 效果是创建了一个MyThread对象,并且其父类Thread对象的名字是给定的字符串。 +- 如果不显式调用父类的构造方法super(参数),那么默认父类Thread是没有名字的。 +### isAlive +**isAlive活着的定义是就绪、运行、阻塞状态** 线程是有优先级的,优先级高的获得Cpu执行时间长,并不代表优先级低的就得不到执行 -sleep(当前线程.sleep) + +### sleep(当前线程.sleep) sleep时持有的锁不会自动释放,sleep时可能会抛出InterruptedException。 Thread.sleep(long millis) 一定是当前线程调用此方法,当前线程进入TIME_WAIT状态,但不释放对象锁,millis后线程自动苏醒进入READY状态。作用:给其它线程执行机会的最佳方式。 -join(其他线程.join) +### join(其他线程.join) t.join()/t.join(long millis) 当前线程里调用线程1的join方法,当前线程进入WAIT状态,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。 join方法的作用是将分出来的线程合并回去,等待分出来的线程执行完毕后继续执行原有线程。类似于方法调用。(相当于调用thead.run()) -yield(当前线程.yield) +### yield(当前线程.yield) Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。 -interrupt(其他线程.interrupt) -调用Interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志; +### interrupt(其他线程.interrupt) +- 调用Interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志; 中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简单来说就是由其他线程通过执行interrupt方法对该线程打个招呼,让起中断标志位为true,从而实现中断线程执行的目的。 +- 其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用Thread.currentThread().isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。 +- 那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为**Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后抛出InterruptedException,这个时候调用isInterrupted方法返回的也是false**。 -其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用Thread.currentThread().isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。 - -那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后抛出InterruptedException,这个时候调用isInterrupted方法返回的也是false。 +**interrupt一个其他线程t时** +- 1)如果线程t中调用了可以抛出InterruptedException的方法,那么会在t中抛出InterruptedException并清除中断标志位。 +- 2)如果t没有调用此类方法,那么会正常地将设置中断标志位。 -interrupt一个其他线程t时 -1)如果线程t中调用了可以抛出InterruptedException的方法,那么会在t中抛出InterruptedException并清除中断标志位。 -2)如果t没有调用此类方法,那么会正常地将设置中断标志位。 +**如何停止线程?** +- 1)在catch InterruptedException异常时可以关闭当前线程; +- 2)循环调用isInterrupted方法检测是否被中断,如果被中断,要么调用interrupted方法清除中断标志位,要么就关闭当前线程。 +- 3)无论1)还是2),都可以通过一个volatile的自定义标志位来控制循环是否继续执行 -如何停止线程? -1)在catch InterruptedException异常时可以关闭当前线程; -2)循环调用isInterrupted方法检测是否被中断,如果被中断,要么调用interrupted方法清除中断标志位,要么就关闭当前线程。 -3)无论1)还是2),都可以通过一个volatile的自定义标志位来控制循环是否继续执行 +**但是注意! +如果线程中有阻塞操作,在阻塞时是无法去检测中断标志位或自定义标志位的,只能使用1)的interrupt方法才能中断线程,并且在线程停止前关闭引起阻塞的资源(比如Socket)。** -但是注意! -如果线程中有阻塞操作,在阻塞时是无法去检测中断标志位或自定义标志位的,只能使用1)的interrupt方法才能中断线程,并且在线程停止前关闭引起阻塞的资源(比如Socket)。 +### wait(对象.wait) +- **调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。** +- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。 +- 调用wait()方法的线程,如果其他线程调用该线程的interrupt()方法,则会重新尝试获取对象锁。只有当获取到对象锁,才开始抛出相应的InterruptedException异常,从wait中返回。 - - -wait(对象.wait) -调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。 -obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。 -调用wait()方法的线程,如果其他线程调用该线程的interrupt()方法,则会重新尝试获取对象锁。只有当获取到对象锁,才开始抛出相应的InterruptedException异常,从wait中返回。 -notify(对象.notify) +### notify(对象.notify) obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。 -wait¬ify 最佳实践 +### wait¬ify 最佳实践 等待方(消费者)和通知方(生产者) +``` 等待方: synchronized(obj){ while(条件不满足){ @@ -140,33 +140,33 @@ synchonized(obj){ 改变条件; obj.notifyAll(); } +``` +- 1)条件谓词: +- 将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档。 +- 在条件等待中存在一种重要的三元关系,包括**加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。** +- 当线程从wait方法中被唤醒时,它在重新请求锁时不具有任何特殊的优先性,而要去其他尝试进入同步代码块的线程一起正常地在锁上进行竞争。 +- 每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。 -1)条件谓词: -将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档。 -在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。 -当线程从wait方法中被唤醒时,它在重新请求锁时不具有任何特殊的优先性,而要去其他尝试进入同步代码块的线程一起正常地在锁上进行竞争。 -每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。 - -2)过早唤醒: +- 2)过早唤醒: 虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但wait方法的返回并不一定意味着线程正在等待的条件谓词已经变成真了。 当执行控制重新进入调用wait的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变成假。在线程被唤醒到wait重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用wait起根本就没有变成真。你并不知道另一个线程为什么调用notify或notifyAll,也许是因为与同一条件队列相关的另一个条件谓词变成了真。一个条件队列与多个条件谓词相关是一种很常见的情况。 基于所有这些原因,每当线程从wait中唤醒时,都必须再次测试条件谓词。 -3)notify与notifyAll: +- 3)notify与notifyAll: 由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号地址(线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词)的问题。 只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll: -1)所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。 -2)单进单出:在对象状态上的每次改变,最多只能唤醒一个线程来执行。 +- 1)所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。 +- 2)单进单出:在对象状态上的每次改变,最多只能唤醒一个线程来执行。 -suspend resume stop destroy(废弃方法) -线程的暂停、恢复、停止对应的就是suspend、resume和stop/destroy。 -suspend会使当前线程进入阻塞状态并不释放占有的资源,容易引起死锁; -stop在结束一个线程时不会去释放占用的资源。它会直接终止run方法的调用,并且会抛出一个ThreadDeath错误。 -destroy只是抛出一个NoSuchMethodError。 -suspend和resume已被wait、notify取代。 +### suspend resume stop destroy(废弃方法) +- 线程的暂停、恢复、停止对应的就是suspend、resume和stop/destroy。 +- suspend会使当前线程进入阻塞状态并不释放占有的资源,容易引起死锁; +- stop在结束一个线程时不会去释放占用的资源。它会直接终止run方法的调用,并且会抛出一个ThreadDeath错误。 +- destroy只是抛出一个NoSuchMethodError。 +- suspend和resume已被wait、notify取代。 -线程的优先级 +### 线程的优先级 判断当前线程是否正在执行 注意优先级是概率而非先后顺序(优先级高可能会执行时间长,但也不一定) From 3c5555a7e95df5d7572ace0ca9b4a0be340b3b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 18:05:40 +0800 Subject: [PATCH 31/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 154 +++++++++++-------------- 1 file changed, 70 insertions(+), 84 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 1c90abb6..4d37deb8 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -167,28 +167,29 @@ synchonized(obj){ - suspend和resume已被wait、notify取代。 ### 线程的优先级 - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8741.png) 判断当前线程是否正在执行 注意优先级是概率而非先后顺序(优先级高可能会执行时间长,但也不一定) 线程优先级特性: -继承性 +- 继承性 比如A线程启动B线程,则B线程的优先级与A是一样的。 -规则性 +- 规则性 高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。 -随机性 +- 随机性 优先级较高的线程不一定每一次都先执行完。 -注意,在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。 -守护线程 +**注意,在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。** -将线程转换为守护线程 -守护线程的唯一用途是为其他线程提供服务。比如计时线程,它定时发送信号给其他线程; -当只剩下守护线程时,JVM就退出了。 -守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。 -注意!Java虚拟机退出时Daemon线程中的finally块并不一定会被执行。 +### 守护线程 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8742.png) +- 将线程转换为守护线程 +- 守护线程的唯一用途是为其他线程提供服务。比如计时线程,它定时发送信号给其他线程; +- 当只剩下守护线程时,JVM就退出了。 +- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。 +- 注意!Java虚拟机退出时Daemon线程中的finally块并不一定会被执行。 -未捕获异常处理器 +### 未捕获异常处理器 在Runnable的run方法中不能抛出异常,如果某个异常没有被捕获,则会导致线程终止。 要求异常处理器实现Thread.UncaughtExceptionHandler接口。 @@ -197,118 +198,103 @@ synchonized(obj){ 如果不安装默认的处理器,那么默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象 ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作: -1)如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。 -2)否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。 -3)否则,如果Throwable是ThreadDeath的一个实例(ThreadDeath对象由stop方法产生,而该方法已过时),什么都不做。 -4)否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。 +- 1)如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。 +- 2)否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。 +- 3)否则,如果Throwable是ThreadDeath的一个实例(ThreadDeath对象由stop方法产生,而该方法已过时),什么都不做。 +- 4)否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。 如果是由线程池ThreadPoolExecutor执行任务,只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出的未检测异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。 -并发编程的问题 -线程引入开销:上下文切换与内存同步 +# 二.并发编程的问题 +## 线程引入开销:上下文切换与内存同步 使用多线程编程时影响性能的首先是线程的上下文切换。每个线程占有一个CPU的时间片,然后会保存该线程的状态,切换到下一个线程执行。线程的状态的保存与加载就是上下文切换。 减少上下文切换的方法有:无锁并发编程、CAS、使用最少线程、协程。 -1)无锁并发:通过某种策略(比如hash分隔任务)使得每个线程不共享资源,避免锁的使用。 -2)CAS:是比锁更轻量级的线程同步方式 -3)避免创建不需要的线程,避免线程一直处于等待状态 -4)协程:单线程实现多任务调度,单线程维持多任务切换 +- 1)无锁并发:通过某种策略(比如hash分隔任务)使得每个线程不共享资源,避免锁的使用。 +- 2)CAS:是比锁更轻量级的线程同步方式 +- 3)避免创建不需要的线程,避免线程一直处于等待状态 +- 4)协程:单线程实现多任务调度,单线程维持多任务切换 -vmstat可以查看上下文切换次数 -jstack 可以dump 线程信息,查看一个进程中各个线程的状态 +**vmstat可以查看上下文切换次数 +jstack 可以dump 线程信息,查看一个进程中各个线程的状态** -内存同步:在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏。内存栅栏可以刷新缓存,使缓存失效,刷新硬件的写缓冲,以及停止执行管道。内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序。 +- 内存同步:在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏。内存栅栏可以刷新缓存,使缓存失效,刷新硬件的写缓冲,以及停止执行管道。 +- 内存栅栏可能同样会对性能带来间接的影响,因为它们将抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序。 不要担心非竞争同步带来的开销,这个基本的机制已经非常快了,并且JVM还能进行额外的优化以进一步降低或消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。 -死锁 +## 死锁 死锁后会陷入循环等待中。 如何避免死锁? -1)避免一个线程同时获取多个锁 -2)避免一个线程在锁内占用多个资源,尽量保证每个锁只占用一个资源 -3)尝试使用定时锁tryLock替代阻塞式的锁 -4)对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会解锁失败 - - - -线程安全性(原子性+可见性) -1、对象的状态:对象的状态是指存储在状态变量中的数据,对象的状态可能包括其他依赖对象的域。在对象的状态中包含了任何可能影响其外部可见行为的数据。 +- 1)避免一个线程同时获取多个锁 +- 2)避免一个线程在锁内占用多个资源,尽量保证每个锁只占用一个资源 +- 3)尝试使用定时锁tryLock替代阻塞式的锁 +- 4)对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会解锁失败 -2、一个对象是否是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。同步机制包括synchronized、volatile变量、显式锁、原子变量。 -3、有三种方式可以修复线程安全问题: -1)不在线程之间共享该状态变量 -2)将状态变量修改为不可变的变量 -3)在访问状态变量时使用同步 -4、线程安全性的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 +# 线程安全性(原子性+可见性) +- 1、对象的状态:对象的状态是指存储在状态变量中的数据,对象的状态可能包括其他依赖对象的域。在对象的状态中包含了任何可能影响其外部可见行为的数据。 +- 2、一个对象是否是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。同步机制包括synchronized、volatile变量、显式锁、原子变量。 -5、无状态变量一定是线程安全的,比如局部变量。 - -6、读取-修改-写入操作序列,如果是后续操作是依赖于之前读取的值,那么这个序列必须是串行执行的。在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它称为竞态条件(Race Condition)。最常见的竞态条件类型就是先检查后执行的操作,通过一个可能失效的观测结果来决定下一步的操作。 - -7、复合操作:要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。假定有两个操作A和B,如果从执行A的线程看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。原子操作是指,对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的操作。 +- 3、有三种方式可以修复线程安全问题: + - 1)不在线程之间共享该状态变量 + - 2)将状态变量修改为不可变的变量 + - 3)在访问状态变量时使用同步 +- 4、线程安全性的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 +- 5、无状态变量一定是线程安全的,比如局部变量。 +- 6、读取-修改-写入操作序列,如果是后续操作是依赖于之前读取的值,那么这个序列必须是串行执行的。在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它称为竞态条件(Race Condition)。最常见的竞态条件类型就是先检查后执行的操作,通过一个可能失效的观测结果来决定下一步的操作。 +- 7、复合操作:要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。假定有两个操作A和B,如果从执行A的线程看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。原子操作是指,对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的操作。 为了确保线程安全性,读取-修改-写入序列必须是原子的,将其称为复合操作。复合操作包含了一组必须以原子方式执行的接口以确保线程安全性。 -8、在无状态的类中添加一个状态时,如果这个状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。(比如原子变量) +- 8、在无状态的类中添加一个状态时,如果这个状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。(比如原子变量) -9、如果多个状态是相关的,需要同时被修改,那么对多个状态的操作必须是串行的,需要进行同步。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。 +- 9、如果多个状态是相关的,需要同时被修改,那么对多个状态的操作必须是串行的,需要进行同步。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。 -10、内置锁:synchronized(object){同步块} +- 10、内置锁:synchronized(object){同步块} Java的内置锁相当于一种互斥体,这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等待下去。 -11、重入:当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置1。如果一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数值会相应递减。当计数值为0时,这个锁将被释放。 +- 11、重入:当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置1。如果一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数值会相应递减。当计数值为0时,这个锁将被释放。 -12、对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。 +- 12、对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。 每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。 一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并提供对象的内置锁(this)对所有访问可变状态的代码路径进行同步。在这种情况下,对象状态中的所有变量都由对象的内置锁保护起来。 -13、不良并发:要保证同步代码块不能过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。 - -14、可见性:为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。 - -15、加锁与可见性:当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步,那么就无法实现上述保证。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。 - -16、volatile变量:当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他对处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。volatile的语义不足以确保递增操作的原子性,除非你能确保只有一个线程对变量执行写操作。原子变量提供了“读-改-写”的原子操作,并且常常用做一种更好的volatile变量。 - -17、加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。 - -18、当且仅当满足以下的所有条件时,才应该使用volatile变量: -1)对变量的写入操作不依赖变量的当前值(不存在读取-判断-写入序列),或者你能确保只有单个线程更新变量的值。 -2)该变量不会与其他状态变量一起纳入不可变条件中 -3)在访问变量时不需要加锁 - -19、栈封闭:在栈封闭中,只能通过局部变量才能访问对象。维护线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程的某个值与保存值的对象关联起来,ThreadLocal通过了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。 - -20、在并发程序中使用和共享对象时,可以使用一些使用的策略,包括: -1)线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。 -2)只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象(从技术上来说是可变的,但其状态在发布之后不会再改变)。 -3)线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。 -4)保护对象。被保护的对象只能通过持有对象的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布并且由某个特定锁保护的对象。 - -21、饥饿:当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿(某线程永远等待)。引发饥饿的最常见资源就是CPU时钟周期。比如线程的优先级问题。在Thread API中定义的线程优先级只是作为线程调度的参考。在Thread API中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射是与特定平台相关的,因此在某个操作系统中两个不同的Java优先级可能被映射到同一优先级,而在另一个操作系统中则可能被映射到另一个不同的优先级。 +- 13、不良并发:要保证同步代码块不能过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。 +- 14、可见性:为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。 +- 15、加锁与可见性:当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步,那么就无法实现上述保证。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。 +- 16、volatile变量:当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或其他对处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。volatile的语义不足以确保递增操作的原子性,除非你能确保只有一个线程对变量执行写操作。原子变量提供了“读-改-写”的原子操作,并且常常用做一种更好的volatile变量。 +- 17、加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。 +- 18、当且仅当满足以下的所有条件时,才应该使用volatile变量: + - 1)对变量的写入操作不依赖变量的当前值(不存在读取-判断-写入序列),或者你能确保只有单个线程更新变量的值。 + - 2)该变量不会与其他状态变量一起纳入不可变条件中 + - 3)在访问变量时不需要加锁 +- 19、栈封闭:在栈封闭中,只能通过局部变量才能访问对象。维护线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程的某个值与保存值的对象关联起来,ThreadLocal通过了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。 +- 20、在并发程序中使用和共享对象时,可以使用一些使用的策略,包括: + - 1)线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。 + - 2)只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象(从技术上来说是可变的,但其状态在发布之后不会再改变)。 + - 3)线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。 + - 4)保护对象。被保护的对象只能通过持有对象的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布并且由某个特定锁保护的对象。 +- 21、饥饿:当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿(某线程永远等待)。引发饥饿的最常见资源就是CPU时钟周期。比如线程的优先级问题。在Thread API中定义的线程优先级只是作为线程调度的参考。在Thread API中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射是与特定平台相关的,因此在某个操作系统中两个不同的Java优先级可能被映射到同一优先级,而在另一个操作系统中则可能被映射到另一个不同的优先级。 当提高某个线程的优先级时,可能不会起到任何作用,或者也可能使得某个线程的调度优先级高于其他线程,从而导致饥饿。 通常,我们尽量不要改变线程的优先级,只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。 - 事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T的请求......T2可能永远等待 -22、活锁 +- 22、活锁 活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发生在处理事务消息的应用程序中。如果不能成功处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。虽然处理消息的线程并没有阻塞,但也无法继续执行下去。这种形式的活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。 - 当多个相互协作的线程都对彼此进行响从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。要解决这种活锁问题,需要在重试机制中引入随机性。在并发应用程序中,通过等待随机长度的时间和回退可以有效地避免活锁的发生。 -23、当在锁上发生竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以采用自旋等待(Spin-Waiting,指通过循环不断地尝试获取锁,直到成功),或者通过操作系统挂起被阻塞的线程。这两种方式的效率高低,取决于上下文切换的开销以及在成功获取锁之前需要等待的时间。如果等待时间较短,则适合采用自旋等待的方式,而如果等待时间较长,则适合采用线程挂起方式。 +- 23、当在锁上发生竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以采用自旋等待(Spin-Waiting,指通过循环不断地尝试获取锁,直到成功),或者通过操作系统挂起被阻塞的线程。这两种方式的效率高低,取决于上下文切换的开销以及在成功获取锁之前需要等待的时间。如果等待时间较短,则适合采用自旋等待的方式,而如果等待时间较长,则适合采用线程挂起方式。 -24、有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。如果二者的乘积很小,那么大多数获取锁的操作都不会发生竞争,会因此在该锁上的竞争不会对可伸缩性造成严重影响。然而,如果在锁上的请求量很高,那么需要获取该锁的线程将被阻塞并等待。在极端情况下,即使仍有大量工作等待完成,处理器也会被闲置。 +- 24、有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。如果二者的乘积很小,那么大多数获取锁的操作都不会发生竞争,会因此在该锁上的竞争不会对可伸缩性造成严重影响。然而,如果在锁上的请求量很高,那么需要获取该锁的线程将被阻塞并等待。在极端情况下,即使仍有大量工作等待完成,处理器也会被闲置。 有3种方式可以降低锁的竞争程度: -1)减少锁的持有时间: + - 1)减少锁的持有时间: 缩小锁的范围,将与锁无关的代码移出同步代码块,尤其是开销较大的操作以及可能被阻塞的操作(IO操作)。 当把一个同步代码块分解为多个同步代码块时,反而会对性能提升产生负面影响。在分解同步代码块时,理想的平衡点将与平台相关,但在实际情况中,仅可以将一些大量的计算或阻塞操作从同步代码块移出时,才应该考虑同步代码块的大小。 减小锁的粒度:锁分解和锁分段 锁分解是采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况。这些技术能减小锁操作的粒度,并能实现更高的可伸缩性,然而,使用的锁越多,那么发生死锁的风险也就越高。 锁分段:比如JDK1.7及之前的ConcurrentHashMap采用的方式就是分段锁的方式。 -2)降低锁的请求频率 -3)使用带有协调机制的独占锁,这些机制允许更高的并发性 -比如读写锁,并发容器等 + - 2)降低锁的请求频率 + - 3)使用带有协调机制的独占锁,这些机制允许更高的并发性比如读写锁,并发容器等 From b7d9bca89bddb8ae57801ddb48037d6aedf452b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Thu, 3 Oct 2019 19:09:04 +0800 Subject: [PATCH 32/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 154 +++++++++++++------------ 1 file changed, 79 insertions(+), 75 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 4d37deb8..11d3d42b 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -298,34 +298,33 @@ Java的内置锁相当于一种互斥体,这意味着最多只有一个线程 -线程间通信/线程同步 工具使用 -synchronized +# 四.线程间通信/线程同步 工具使用 +## synchronized synchronized锁定的是对象而非代码,所处的位置是代码块或方法 -一种使用方法是对代码块使用synchronized关键字 +### 一种使用方法是对代码块使用synchronized关键字 +``` public void fun(){ synchronized (this){ } } -括号中锁定的是普通对象或Class对象 -如果是this,表示在执行该代码块时锁定当前对象,其他线程不能调用该对象的其他锁定代码块,但可以调用其他对象的所有方法(包括锁定的代码块),也可以调用该对象的未锁定的代码块或方法。 -如果是Object o1,表示执行该代码块的时候锁定该对象,其他线程不能访问该对象(该对象是空的,没有方法,自然不能调用) -如果是类.class,那么锁住了该类的Class对象,只对静态方法生效。 - -另一种写法是将synchronized作为方法的修饰符 -public synchronized void fun() {} //这个方法执行的时候锁定该当前对象 -每个类的对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的一个对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。 - -如果synchronized修饰的是静态方法,那么锁住的是这个类的Class对象,没有其他线程可以调用该类的这个方法或其他的同步静态方法。 - -实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的这个代码段。 -synchronized锁住的是括号里的对象,而不是代码。对于非静态的synchronized方法,锁的就是对象本身也就是this。 - -获取锁的线程释放锁只会有两种情况: -1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; -2)线程执行发生异常,此时JVM会让线程自动释放锁。 - - -Lock +``` +- 括号中锁定的是普通对象或Class对象 +- 如果是this,表示在执行该代码块时锁定当前对象,其他线程不能调用该对象的其他锁定代码块,但可以调用其他对象的所有方法(包括锁定的代码块),也可以调用该对象的未锁定的代码块或方法。 +- 如果是Object o1,表示执行该代码块的时候锁定该对象,其他线程不能访问该对象(该对象是空的,没有方法,自然不能调用) +- 如果是类.class,那么锁住了该类的Class对象,只对静态方法生效。 + +### 另一种写法是将synchronized作为方法的修饰符 +- public synchronized void fun() {} //这个方法执行的时候锁定该当前对象 +- 每个类的对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的一个对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。 +- 如果synchronized修饰的是静态方法,那么锁住的是这个类的Class对象,没有其他线程可以调用该类的这个方法或其他的同步静态方法。 +- 实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的这个代码段。 synchronized锁住的是括号里的对象,而不是代码。对于非静态的synchronized方法,锁的就是对象本身也就是this。 +- 获取锁的线程释放锁只会有两种情况: + - 1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; + - 2)线程执行发生异常,此时JVM会让线程自动释放锁。 + + +## Lock 锁是可重入的(reentrant),因为线程可以重复获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。 +``` public class TestReentrantLock { public static void main(String[] args) { Ticket ticket = new Ticket(); @@ -356,29 +355,29 @@ class Ticket implements Runnable { } } } +``` - -volatile +## volatile (作用是为成员变量的同步访问提供了一种免锁机制,如果声明一个成员变量是volatile的,那么会通知编译器和虚拟机这个成员变量是可能其他线程并发更新的 对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。 Java内存模型简要介绍(后面会详细介绍): -多线程环境下,会共享同一份数据(线程公共的内存空间)。为了提高效率,JVM会为每一个线程设置一个线程私有的内存空间(线程工作内存),并将共享数据拷贝过来。写操作实际上写的是线程私有的数据。当写操作完毕后,将线程私有的数据写回到线程公共的内存空间。 -如果在写回之前其他线程读取该数据,那么返回的可能是修改前的数据,视读取线程的执行效率而定。 -jvm运行时刻内存的分配:其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,(从线程内存中读值) -在修改完之后的某一个时刻(线程退出之前),把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 -) +- 多线程环境下,会共享同一份数据(线程公共的内存空间)。为了提高效率,JVM会为每一个线程设置一个线程私有的内存空间(线程工作内存),并将共享数据拷贝过来。写操作实际上写的是线程私有的数据。当写操作完毕后,将线程私有的数据写回到线程公共的内存空间。 +- 如果在写回之前其他线程读取该数据,那么返回的可能是修改前的数据,视读取线程的执行效率而定。 +- jvm运行时刻内存的分配:其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,(从线程内存中读值) +- 在修改完之后的某一个时刻(线程退出之前),把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 + final修饰的变量是线程安全的 内存可见性问题是,当多个线程操作共享数据时,彼此不可见。 解决这个问题有两种方法: -1、加锁:加锁会保证读取的数据一定是写回之后的,内存刷新。但是效率较低 -2、volatile:会保证数据在读操作之前,上一次写操作必须生效,即写回。 -1)修改volatile变量时会强制将修改后的值刷新到主内存中。 -2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。 - 相较于synchronized是一种较为轻量级的同步策略,但是volatile不具备互斥性;不能保证修改变量时的原子性。 +- 1、加锁:加锁会保证读取的数据一定是写回之后的,内存刷新。但是效率较低 +- 2、volatile:会保证数据在读操作之前,上一次写操作必须生效,即写回。 + - 1)修改volatile变量时会强制将修改后的值刷新到主内存中。 + - 2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。相较于synchronized是一种较为轻量级的同步策略,但是volatile不具备互斥性;不能保证修改变量时的原子性。 +``` public class TestVolatile { public static void main(String[] args) { MyThread myThread = new MyThread(); @@ -413,40 +412,38 @@ class MyThread implements Runnable{ System.out.println("flag="+flag); } } - -Atomic +``` +## Atomic 原子变量 可以实现原子性+可见性 +# 五.Lock使用 深入 +## 可重入锁 ReentrantLock - -Lock使用 深入 -可重入锁 ReentrantLock 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,绑定多个条件以及非块结构的锁。否则,还是应该优先使用synchronized。 -1)可中断:lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。 -2)可定时:tryLock(time) -3)可轮询:tryLock() -4)可公平:公平锁与非公平锁 -5)绑定多个条件:一个锁可以对应多个条件,而Object锁只能对应一个条件 -6)非块结构:加锁与解锁不是块结构的 +- 1)可中断:lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。 +- 2)可定时:tryLock(time) +- 3)可轮询:tryLock() +- 4)可公平:公平锁与非公平锁 +- 5)绑定多个条件:一个锁可以对应多个条件,而Object锁只能对应一个条件 +- 6)非块结构:加锁与解锁不是块结构的 -Condition(与wait¬ify区别) +## Condition(与wait¬ify区别) BlockingQueue就是基于Condition实现的。 一个Condition对象和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样。要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。 -Condition与wait¬ify区别 - -1)Condition比内置条件等待队列提供了更丰富的功能:在每个锁上可存在 可不响应中断、可等待至某个时间点、可公平的队列操作。 +### Condition与wait¬ify区别 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8743.png) +- 1)Condition比内置条件等待队列提供了更丰富的功能:在每个锁上可存在 可不响应中断、可等待至某个时间点、可公平的队列操作。 wait¬ify一定响应中断并抛出遗产;Condition可以响应中断也可以不响应中断 - -2)与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象。 -await() awaitUninterruptibly() await(time) -Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会按照FIFO顺序从Condition.await中释放。 +- 2)与内置条件队列不同的是,对于每个Lock,可以有任意数量的Condition对象。 +- await() awaitUninterruptibly() await(time) Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会按照FIFO顺序从Condition.await中释放。 -await&signal +### await&signal +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8744.png) await被中断会抛出InterruptedException。 Condition区分开了不同的条件谓词,更容易满足单次通知的需求。signal比signalAll更高效,它能极大地减少在每次缓存操作中发生的上下文切换与锁请求的次数。 @@ -465,15 +462,17 @@ signalAll方法会重新激活因为这一条件而等待的所有线程。当 线程应该再次测试该条件。由于无法确保该条件被满足,signalAll方法仅仅是通知正在等待的线程,此时有可能满足条件,值得再次去检测该条件 对于await方法的调用应该用在这种形式: +``` while(!(ok to continue)){ condition.await(); } +``` 最重要的是需要其他某个线程调用signalAll方法。当一个线程调用await方法,它没有办法去激活自身,只能寄希望于其他线程。如果没有其他线程来激活等待的线程,那么就会一直等待,出现死锁。 如果所有其他线程都被阻塞,且最后一个线程也调用了await方法,那么它也被阻塞,此时程序挂起。 signalAll方法不会立刻激活一个等待的线程,仅仅是解除等待线程的阻塞,以便这些线程可以在当前线程(调用signalAll方法的线程)退出时,通过竞争来实现对对象的方法 这个await和signalAll方法的组合类似于Object对象的wait和notifyAll方法的组合 - +``` public class ConditionBoundedBuffer { private static final int BUFFER_SIZE = 20; protected final Lock lock = new ReentrantLock(); @@ -522,14 +521,16 @@ public class ConditionBoundedBuffer { } } } +``` - -公平锁 +## 公平锁 +``` public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } +``` 在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许‘插队’:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 非公平的ReentrantLock 并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。 @@ -544,7 +545,7 @@ public ReentrantLock(boolean fair) { 非公平锁可能会引起线程饥饿,但是线程切换更少,吞吐量更大 -读写锁 ReentrantReadWriteLock +## 读写锁 ReentrantReadWriteLock 读写锁是一种性能优化措施,在一些特定的情况下能实现更高的并发性。在实际情况中,对于在多处理器系统上被频繁读取的数据结构,读写锁能够提高性能。而在其他情况下,读写锁的性能比独占锁的性能要略差一些,这是因为它们的复杂性更高。如果要判断在某种情况下使用读写锁是否会带来性能提升,最好对程序进行分析。由于ReadWriteLock使用Lock来实现锁的读写部分,因此如果分析结果表明读写锁没有提高性能,那么可以很容易地将读写锁换为独占锁。 ReentrantReadWriteLock 如果很多线程从一个数据结构中读取数据而很少线程修改其中数据,那么允许对读的线程共享访问是合适的。 @@ -552,13 +553,15 @@ ReentrantReadWriteLock 如果很多线程从一个数据结构中读取数据而 读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁! 特性: - (a).重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。 - (b).写锁可以降级为读锁,顺序是:先获得写锁再获得读锁,然后释放写锁,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不可能,为什么?参看(a) - (c) 读锁不能升级为写锁,目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。 - (d).读锁可以被多个线程持有并且在作用时排斥任何的写锁,而写锁则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 - (e).不管是读锁还是写锁都支持Interrupt,语义与ReentrantLock一致。 - (f).写锁支持Condition并且与ReentrantLock语义一致,而读锁则不能使用Condition,否则抛出UnsupportedOperationException异常。 - +- (a).重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。 +- (b).写锁可以降级为读锁,顺序是:先获得写锁再获得读锁,然后释放写锁,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不可能,为什么?参看(a) +- (c) 读锁不能升级为写锁,目的是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。 +- (d).读锁可以被多个线程持有并且在作用时排斥任何的写锁,而写锁则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 +- (e).不管是读锁还是写锁都支持Interrupt,语义与ReentrantLock一致。 +- (f).写锁支持Condition并且与ReentrantLock语义一致,而读锁则不能使用Condition,否则抛出UnsupportedOperationException异常。 + +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8745.png) +``` public class TestReadWriteLock { public static void main(String[] args) { ReadWriteLockDemo demo = new ReadWriteLockDemo(); @@ -605,11 +608,12 @@ class ReadWriteLockDemo { } } } - -LockSupport(锁住的是线程,synchronized锁住的是对象) -当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。 +``` +## LockSupport(锁住的是线程,synchronized锁住的是对象) +当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。 +LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。 LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(thread)方法来唤醒一个被阻塞的线程。 - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8745.png) park等方法还可以传入阻塞对象,有阻塞对象的park方法在dump线程时可以给开发人员更多的现场信息。 park对于中断只会设置中断标志位,不会抛出InterruptedException。 @@ -619,12 +623,12 @@ unpark函数可以先于park调用。比如线程B调用unpark函数,给线程 LockSupport.park()和unpark(),与object.wait()和notify()的区别? 主要的区别应该说是它们面向的对象不同。阻塞和唤醒是对于线程来说的,LockSupport的park/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。 park/unpark模型真正解耦了线程之间的同步。线程之间不再须要一个Object或者其他变量来存储状态。不再须要关心对方的状态。 -synchronized与Lock的区别 -1)层次:前者是JVM实现,后者是JDK实现 -2)功能:前者仅能实现互斥与重入,后者可以实现 可中断、可轮询、可定时、可公平、绑定多个条件、非块结构 +## synchronized与Lock的区别 +- 1)层次:前者是JVM实现,后者是JDK实现 +- 2)功能:前者仅能实现互斥与重入,后者可以实现 可中断、可轮询、可定时、可公平、绑定多个条件、非块结构 synchronized在阻塞时不会响应中断,Lock会响应中断,并抛出InterruptedException异常。 -3)异常:前者线程中抛出异常时JVM会自动释放锁,后者必须手工释放 -4)性能:synchronized性能已经大幅优化,如果synchronized能够满足需求,则尽量使用synchronized +- 3)异常:前者线程中抛出异常时JVM会自动释放锁,后者必须手工释放 +- 4)性能:synchronized性能已经大幅优化,如果synchronized能够满足需求,则尽量使用synchronized 原子操作类使用 1、近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(compareAndSwap)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于在操作系统和JVM中实现进程/线程调度机制、垃圾回收机制以及锁和其他并发数据结构。 非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。非阻塞算法常见应用是原子变量类。 From b67a9b4b3b70ff16e957110137f881d800266d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 13:07:14 +0800 Subject: [PATCH 33/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 41 ++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 11d3d42b..2a2f8218 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -629,40 +629,31 @@ park/unpark模型真正解耦了线程之间的同步。线程之间不再须要 synchronized在阻塞时不会响应中断,Lock会响应中断,并抛出InterruptedException异常。 - 3)异常:前者线程中抛出异常时JVM会自动释放锁,后者必须手工释放 - 4)性能:synchronized性能已经大幅优化,如果synchronized能够满足需求,则尽量使用synchronized -原子操作类使用 -1、近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(compareAndSwap)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于在操作系统和JVM中实现进程/线程调度机制、垃圾回收机制以及锁和其他并发数据结构。 +# 六.原子操作类使用 +- 1、近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(compareAndSwap)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于在操作系统和JVM中实现进程/线程调度机制、垃圾回收机制以及锁和其他并发数据结构。 非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。非阻塞算法常见应用是原子变量类。 即使原子变量没有用于非阻塞算法的开发,它们也可以用作一个更好的volatile类型变量。原子变量提供了与volatile类型变量相同的内存语义,此外还支持原子的更新操作,从而使它们更加适用于实现计数器、序列发生器和统计数据收集等,同时还能比基于锁的方法提供更高的可伸缩性。 - -2、锁的缺点: -1)在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。 -2)volatile变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的操作。 -3)当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。 -4)总之,锁定方式对于细粒度的操作(比如递增计数器)来说仍然是一种高开销的机制。在管理线程之间的竞争应该有一种粒度更细的技术,比如CAS。 - -3、独占锁是一种悲观技术。对于细粒度的操作,还有另外一个更高效的办法,也是一种乐观的办法,通过这种方法可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试。在针对多处理器操作而设计的处理器中提供了一些特殊的指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-increment)以及交换(swap)指令。现在几乎所有的现代处理器都包含了某种形式的原子读-改-写指令,比如比较并交换(compare-and-swap)等。 - -4、CAS包含了三个操作数——需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会以原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。 +- 2、锁的缺点: + - 1)在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断。 + - 2)volatile变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的操作。 + - 3)当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。 + - 4)总之,锁定方式对于细粒度的操作(比如递增计数器)来说仍然是一种高开销的机制。在管理线程之间的竞争应该有一种粒度更细的技术,比如CAS。 +- 3、独占锁是一种悲观技术。对于细粒度的操作,还有另外一个更高效的办法,也是一种乐观的办法,通过这种方法可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试。在针对多处理器操作而设计的处理器中提供了一些特殊的指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-increment)以及交换(swap)指令。现在几乎所有的现代处理器都包含了某种形式的原子读-改-写指令,比如比较并交换(compare-and-swap)等。 +- 4、CAS包含了三个操作数——需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会以原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。 CAS的含义是:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。 - - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8747.png) 上面这段代码模拟了CAS操作(但实际上不是基于synchronized实现的原子操作,而是由操作系统支持的)。 当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会被阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。这种灵活性就大大减少了与锁相关的活跃性风险。 - -5、基于CAS实现的非阻塞计数器 - - -6、初看起来,基于CAS的计数器似乎比基于锁的计数器在性能上更差一些,因为它需要执行更多的操作和更复杂的控制流,并且还依赖看似复杂的CAS操作。但实际上,当竞争程序不高时,基于CAS的计数器在性能上远远超过了基于锁的计数器,而在没有竞争时甚至更高。如果要快速获取无竞争的锁,那么至少需要一次CAS操作再加上与其他锁相关操作,因此基于锁的计数器即使在更好的情况下也会比基于CAS的计数器在一般情况下能执行更多的操作。 +- 5、基于CAS实现的非阻塞计数器 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8748.png) +- 6、初看起来,基于CAS的计数器似乎比基于锁的计数器在性能上更差一些,因为它需要执行更多的操作和更复杂的控制流,并且还依赖看似复杂的CAS操作。但实际上,当竞争程序不高时,基于CAS的计数器在性能上远远超过了基于锁的计数器,而在没有竞争时甚至更高。如果要快速获取无竞争的锁,那么至少需要一次CAS操作再加上与其他锁相关操作,因此基于锁的计数器即使在更好的情况下也会比基于CAS的计数器在一般情况下能执行更多的操作。 CAS的主要缺点是,它将使调用者处理竞争问题,而在锁中能自动处理竞争问题。 - -7、原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最细的情况。更新原子变量的快速路径不会比获取锁的路径慢,并且通常会更快,而它的慢速路径肯定比锁的慢速路径快,因为它不需要挂起或重新调度线程。 +- 7、原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最细的情况。更新原子变量的快速路径不会比获取锁的路径慢,并且通常会更快,而它的慢速路径肯定比锁的慢速路径快,因为它不需要挂起或重新调度线程。 原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。 共用12个原子变量类,可分为4组:标量类、更新器类、数组类以及复合变量类。原子数组类中的元素可以实现原子更新。 原子数组类中的元素可以实现原子更新。原子数组类为数组的元素提供了volatile类型的访问语义,这是普通数组锁不具备的特性——volatile类型的数组仅在数组引用上具有volatile语义,而在其元素上则没有。 - - - -8、ABA问题 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8749.png) +- 8、ABA问题 有时候还需要知道“自从上次看到V的值为A以来,这个值是否发生了变化”,在某些算法中,如果V的值首先由A变为B,再由B变为A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。 有一个相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,然后又变为A,版本号也将是不同的。AtomicStampedReference支持在两个变量上执行原子的条件更新。 AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上:“版本号”,从而避免ABA问题。类似地,AtomicMarkableRefernce将更新一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的节点”。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作(变量合并,AtomicReference)三个问题。 From ce8c1011d1a58d1c08802a374c019fd9bb4dfbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 13:23:25 +0800 Subject: [PATCH 34/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 81 +++++++++++++------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 2a2f8218..3a3fedaa 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -659,57 +659,56 @@ CAS的主要缺点是,它将使调用者处理竞争问题,而在锁中能 AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上:“版本号”,从而避免ABA问题。类似地,AtomicMarkableRefernce将更新一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的节点”。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作(变量合并,AtomicReference)三个问题。 -Java内存模型 线程同步工具原理 -JMM Java Memory Model -JMM抽象结构 +# 七.Java内存模型 线程同步工具原理 +## JMM Java Memory Model +### JMM抽象结构 在Java中,堆内存在线程之间共享,线程之间的通信由Java内存模型JMM控制。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储了线程读写共享变量的副本。 - +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8750.png) 如果线程A与线程B之间要通信的话,必须要经历下面2个步骤: -1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。 -2)线程B到主内存中去读取线程A之前已更新过的共享变量。 - -指令重排序 -在执行程序时,为了提高性能,编译器和CPU常常会对指令进行重排序,分为以下3种类型: -1、编译优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。 -2、指令级并行的重排序。CPU采用了指令级并行技术将多条指令重叠执行。 -3、内存系统的重排序。由于CPU使用cache和读/写缓冲区,因此加载和存储操作可能在乱序执行。 +- 1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。 +- 2)线程B到主内存中去读取线程A之前已更新过的共享变量。 + +### 指令重排序 +- 在执行程序时,为了提高性能,编译器和CPU常常会对指令进行重排序,分为以下3种类型: +- 1、编译优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。 +- 2、指令级并行的重排序。CPU采用了指令级并行技术将多条指令重叠执行。 +- 3、内存系统的重排序。由于CPU使用cache和读/写缓冲区,因此加载和存储操作可能在乱序执行。 +![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8751.png) +- 1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。 +- 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。 +- 对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。 + +### 内存屏障 +JMM把内存屏障分为四类: +- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 +- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 +- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 +- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 +### happens-before(抽象概念,基于内存屏障) -1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。 -对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。 -对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。 -内存屏障 -JMM把内存屏障分为四类: -LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 -StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 -LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 -StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 -happens-before(抽象概念,基于内存屏障) -JDK1.5后,Java采用JSR133内存模型,通过happens-before概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果要对另一个操作可见,那么这两个操作之间必须要有happens-before关系。 +JDK1.5后,Java采用JSR133内存模型,通过happens-before概念来阐述操作之间的内存可见性。在JMM中,**如果一个操作执行的结果要对另一个操作可见,那么这两个操作之间必须要有happens-before关系。** 定义: -1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 -2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。 - -上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证! +- 1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 +- 2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。 -上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序)编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。 +- 上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证! +- 上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序)编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。 与程序员密切相关的happens-before规则如下。 -1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。(单线程顺序执行) -2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。(先解锁后加锁) -比如: -lock.unlock(); -lock.lock(); -那么不会重排序,因为重排序后肯定会发生死锁 - -3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。(先写后读) -4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 -5)start规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start() happens-before于线程B的任意操作。 -6)join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 - -happens-before与JMM的关系如下: +- 1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。(单线程顺序执行) +- 2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。(先解锁后加锁)比如: + - lock.unlock(); + - lock.lock(); + - 那么不会重排序,因为重排序后肯定会发生死锁 +- 3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。(先写后读) +- 4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 +- 5)start规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start() happens-before于线程B的任意操作。 +- 6)join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 + +**happens-before与JMM的关系如下:** 指令重排序 From aa3824970956b49ba1b12ff484487ae9429a49e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 13:52:16 +0800 Subject: [PATCH 35/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 93 +++++++++++++------------- 1 file changed, 45 insertions(+), 48 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 3a3fedaa..a902d53e 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -711,44 +711,44 @@ JDK1.5后,Java采用JSR133内存模型,通过happens-before概念来阐述 **happens-before与JMM的关系如下:** -指令重排序 +## 指令重排序 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 -数据依赖性 -对于单个CPU和单个线程中所执行的操作而言,如果两个操作都访问了同一个变量,且两个操作中有写操作,那么这两个操作就具有数据依赖性。 -(RW,WW,WR)这三种操作只要重排序对操作的执行顺序,程序的执行结果就会被改变,因此,编译器和处理器在进行重排序的时候会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。 -as-if-serial -as-if-serial:无论如何重排序,(单线程)程序的执行结果不能被改变。 +### 数据依赖性 +- 对于单个CPU和单个线程中所执行的操作而言,如果两个操作都访问了同一个变量,且两个操作中有写操作,那么这两个操作就具有数据依赖性。 +- **(RW,WW,WR)这三种操作只要重排序对操作的执行顺序,程序的执行结果就会被改变,因此,编译器和处理器在进行重排序的时候会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。** +### as-if-serial +- as-if-serial:无论如何重排序,(单线程)程序的执行结果不能被改变。 编译器,runtime,CPU都必须遵守as-if-serial语义,因此,编译器和CPU不会对存在数据依赖关系的操作进行重排序。 +- 在单线程中,对存在控制依赖性的操作进行重排序,不会改变执行结果,而在多线程中则可能会改变结果。 -在单线程中,对存在控制依赖性的操作进行重排序,不会改变执行结果,而在多线程中则可能会改变结果。 -顺序一致性 +## 顺序一致性 程序未正确同步的时候,就可能存在数据竞争。 数据竞争: -1)在一个线程中写一个变量 -2)在另一个线程中读同一个变量 -3)而且写和读没有通过同步来排序。 +- 1)在一个线程中写一个变量 +- 2)在另一个线程中读同一个变量 +- 3)而且写和读没有通过同步来排序。 JMM对正确同步的多线程程序的内存一致性做了如下保证: 如果程序是正确同步的,程序的执行将具有顺序一致性——即程序的执行结果与该程序的顺序一致性内存模型的执行结果相同。 顺序一致性模型有两大特性: -1)一个线程中的所有操作必须按照程序的顺序来执行 -2)不管程序是否同步,所有线程都只能看到单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。 +- 1)一个线程中的所有操作必须按照程序的顺序来执行 +- 2)不管程序是否同步,所有线程都只能看到单一的操作执行顺序。每个操作都必须原子执行且立即对所有线程可见。 JMM中,临界区内的代码可以重排序。 而对于未正确同步的多线程程序,JMM只提供最小的安全性:线程执行时所读取到的值,要么是之前某个线程所写入的值,要么是默认值。 -volatile原理 -汇编上的实现 -volatile修饰的共享变量在转换为汇编语言后,会出现Lock前缀指令,该指令在多核处理器下引发了两件事: -1、将当前处理器缓存行(CPU cache中可以分配的最小存储单位)的数据写回到系统内存。 +## volatile原理 -2、这个写回内存的操作使得其他CPU里缓存了该内存地址的数据无效。 +### 汇编上的实现 +volatile修饰的共享变量在转换为汇编语言后,会出现Lock前缀指令,该指令在多核处理器下引发了两件事: +- 1、将当前处理器缓存行(CPU cache中可以分配的最小存储单位)的数据写回到系统内存。 +- 2、这个写回内存的操作使得其他CPU里缓存了该内存地址的数据无效。 (当前CPU该变量的缓存回写;其他CPU该变量的缓存失效) -内存语义 +### 内存语义 volatile写的内存语义: -当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存 +- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存 volatile读的内存语义: -当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量 +- 当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量 一个volatile变量的单个读写操作,与一个普通变量的读写操作使用同一个锁来同步,它们的执行效果相同。锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这也意味着对一个volatile变量的读操作,总是能看到任意线程对该变量最后的写入。 @@ -758,20 +758,18 @@ volatile读的内存语义: JAVA1.5后,JSR-133增强了volatile的内存语义,严格限制编译器和CPU对于volatile变量与普通变量的重排序,从而确保volatile变量的写-读操作可以实现线程之间的通信,提供了一种比锁更轻量级的线程通信机制。从内存语义的角度而言,volatile的写-读与锁的释放-获取有相同的内存效果:写操作=锁的释放;读操作=锁的获取。 A线程写一个volatile变量x后,B线程读取x以及其他共享变量。 - -1. 当A线程对x进行写操作时,JMM会把该线程A对应的cache中的共享变量值刷新到主存中.(实质上是线程A向接下来要读变量x的线程发出了其对共享变量修改的消息) - -2.当B线程对x进行读取时,JMM会把该线程对应的cache值设置为无效,而从主存中读取x。(实质上是线程B接收了某个线程发出的对共享变量修改的消息) +- 1. 当A线程对x进行写操作时,JMM会把该线程A对应的cache中的共享变量值刷新到主存中.(实质上是线程A向接下来要读变量x的线程发出了其对共享变量修改的消息) +- 2.当B线程对x进行读取时,JMM会把该线程对应的cache值设置为无效,而从主存中读取x。(实质上是线程B接收了某个线程发出的对共享变量修改的消息) 两个步骤综合起来看,在线程B读取一个volatile变量x后,线程A本地cache中在写这个变量x之前所有其他可见的共享变量的值都立即变得对B可见。线程A写volatile变量x,B读x的过程实质上是线程A通过主存向B发送消息。 需要注意的是,由于volatile仅仅保证对单个volatile变量的读写操作具有原子性,而锁的互斥则可以确保整个临界区代码执行的原子性。 -内存语义的实现(内存屏障) -在每个volatile写操作的前面插入一个StoreStore屏障 -在每个volatile写操作的后面插入一个StoreLoad屏障 -在每个volatile读操作的后面插入一个LoadLoad屏障 -在每个volatile读操作的后面插入一个LoadStore屏障 +### 内存语义的实现(内存屏障) +- 在每个volatile写操作的前面插入一个StoreStore屏障 +- 在每个volatile写操作的后面插入一个StoreLoad屏障 +- 在每个volatile读操作的后面插入一个LoadLoad屏障 +- 在每个volatile读操作的后面插入一个LoadStore屏障 StoreStore屏障;(禁止上面的普通写和下面的volatile写重排序,保证上面所有的普通写在volatile写之前刷新到主内存) volatile写; @@ -781,42 +779,41 @@ volatile读; LoadLoad屏障; LoadStore屏障; -从汇编入手分析volatile多线程问题 -1、普通方式int i,执行i++: - +**从汇编入手分析volatile多线程问题** +- 1、普通方式int i,执行i++: +![图片1.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418aov6j30mv03mmxg.jpg) 普通方式没有任何与锁有关的指令;其他方式都出现了与锁相关的汇编指令lock。 解释指令:其中edi为32位寄存器。如果是long则为64位的rdi寄存器。 -2、volatile方式volatile int i,执行i++: - +- 2、volatile方式volatile int i,执行i++: +![图片2.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418anvhj30my03lmxe.jpg) 指令“lock; addl $0,0(%%esp)”表示加锁,把0加到栈顶的内存单元,该指令操作本身无意义,但这些指令起到内存屏障的作用,让前面的指令执行完成。具有XMM2特征的CPU已有内存屏障指令,就直接使用该指令。 volatile字节码为: +![图片3.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418bl7dj30fv0shtas.jpg) - - - -synchronized原理 -monitor +## synchronized原理 +### monitor 代码块同步是使用monitorenter和monitorexit指令实现。monitorenter和monitorexit指令是在编译后插入到同步代码块开始和结束的的位置。任何一个对象都有一个monitor与之关联,当一个monitor被某个线程持有之后,该对象将处于锁定状态。线程执行到monitorenter指令时,会尝试获取该对象对应的monitor所有权,也即获得对象的锁。 monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: -1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 -2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. -3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 +- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 +- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. +- 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 monitorexit:执行monitorexit的线程必须是对象所对应的monitor的所有者。 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 -其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因 +**其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因** 在HotSpotJVM实现中,锁有个专门的名字:对象监视器。 -汇编上的实现(cmpxchg) + +### 汇编上的实现(cmpxchg) synchronizied方式实现i++ 字节码: - +![图片4.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418bfy2j30f50ihq3z.jpg) 汇编 - +![图片5.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418ciyij30z90xn42b.jpg) monitorenter与monitorexit包裹了getstatic i及putstatic i,等相关代码执行指令。中间值的交换采用了原子操作lock cmpxchg %rsi,(%rdi),如果交换成功,则执行goto直接退出当前函数return。如果失败,执行jne跳转指令,继续循环执行,直到成功为止。 cmpxchg指令:比较rsi和目的操作数rdi(第一个操作数)的值,如果相同,ZF标志被设置,同时源操作数(第二个操作)的值被写到目的操作数,否则,清ZF标志为0,并且把目的操作数的值写回rsi,则执行jne跳转指令。 -Java对象头 +### Java对象头 synchronized用的锁放在java对象头里。 有两种情况: 数组对象,虚拟机使用3个字宽存储对象头。 From 460d9fd3730c745df868e5827e1f1a5a277f11fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:01:51 +0800 Subject: [PATCH 36/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 4 ++++ 1 file changed, 4 insertions(+) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index a902d53e..9ee2f5ec 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -818,6 +818,10 @@ synchronized用的锁放在java对象头里。 有两种情况: 数组对象,虚拟机使用3个字宽存储对象头。 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32字节。 +| 班级 | 男生 | 女生 | +|-----|-----|------| +| 一(7)班 | 30 | 25 | +| 一(8)班 | 25 | 30 | 长度 内容 说明 32/64bit mark word 存储对象的hashCode或者锁信息 32/64bit Class metadata address 存储对象描述数据的指针 From d13107f1546f079ee35d4e2d013a903d01824817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:13:09 +0800 Subject: [PATCH 37/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 9ee2f5ec..7b154c78 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -818,10 +818,15 @@ synchronized用的锁放在java对象头里。 有两种情况: 数组对象,虚拟机使用3个字宽存储对象头。 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32字节。 -| 班级 | 男生 | 女生 | -|-----|-----|------| -| 一(7)班 | 30 | 25 | -| 一(8)班 | 25 | 30 | +--- +title: MySQL优化看这一篇就够了 +date: 2018-12-25 11:04:03 +updated_at: +comments: true +photos: "http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/51481063.jpg" +categories: 数据库 +tags: MySQL +--- 长度 内容 说明 32/64bit mark word 存储对象的hashCode或者锁信息 32/64bit Class metadata address 存储对象描述数据的指针 From 14aab4456acfb10c34ddab6f9afef7db935ec841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:15:58 +0800 Subject: [PATCH 38/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 4 ++++ 1 file changed, 4 insertions(+) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 7b154c78..538f2710 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -1,3 +1,7 @@ + 表头 | 表头 + ------------- | ------------- + 单元格内容 | 单元格内容 + 单元格内容l | 单元格内容 # 一. 并发框架 ## Doug Lea 如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。 From be514ddcf4bdc30502b1118cc02c1963d8cfa399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:18:05 +0800 Subject: [PATCH 39/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 538f2710..7c6a53c7 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -1,7 +1,4 @@ - 表头 | 表头 - ------------- | ------------- - 单元格内容 | 单元格内容 - 单元格内容l | 单元格内容 + # 一. 并发框架 ## Doug Lea 如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。 @@ -822,19 +819,12 @@ synchronized用的锁放在java对象头里。 有两种情况: 数组对象,虚拟机使用3个字宽存储对象头。 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32字节。 ---- -title: MySQL优化看这一篇就够了 -date: 2018-12-25 11:04:03 -updated_at: -comments: true -photos: "http://zanwenblog.oss-cn-beijing.aliyuncs.com/18-12-29/51481063.jpg" -categories: 数据库 -tags: MySQL ---- -长度 内容 说明 -32/64bit mark word 存储对象的hashCode或者锁信息 -32/64bit Class metadata address 存储对象描述数据的指针 -32/64bit Array length 数组的长度 + 长度 | 内容 |说明 + ------------- | ------------- | +32/64bit | mark word | 存储对象的hashCode或者锁信息 +32/64bit | Class metadata address | 存储对象描述数据的指针 +32/64bit | Array length | 数组的长度 + Mark Word 的存储结构: 锁的分类 From 607c2b94f3a5188c2a9e4c7fe0151077eb918081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:19:05 +0800 Subject: [PATCH 40/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 7c6a53c7..db1517e6 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -819,8 +819,9 @@ synchronized用的锁放在java对象头里。 有两种情况: 数组对象,虚拟机使用3个字宽存储对象头。 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32字节。 - 长度 | 内容 |说明 - ------------- | ------------- | + + 长度 | 内容 |说明 + ------------- | ------------- | ------------- 32/64bit | mark word | 存储对象的hashCode或者锁信息 32/64bit | Class metadata address | 存储对象描述数据的指针 32/64bit | Array length | 数组的长度 From 26a49da60feca1bd650cea474f96ccd1d33283a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:33:20 +0800 Subject: [PATCH 41/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 79 +++++++++++++------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index db1517e6..9f053a1c 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -827,54 +827,57 @@ synchronized用的锁放在java对象头里。 32/64bit | Array length | 数组的长度 Mark Word 的存储结构: +![图片6.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418bpmsj30ng06b0u2.jpg) -锁的分类 -synchronized是重量级锁,效率较低。 -synchronized所用到的锁是存在Java对象头中。在Java1.6中,锁一共有4种状态,由低到高依次是:无锁,偏向锁,轻量级锁,重量级锁,这几种状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 -monitorenter和monitorexit是上层指令,底层实现可能是偏向锁、轻量级锁、重量级锁等。 +### 锁的分类 -偏向锁(只有一个线程进入临界区) -引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。 +- synchronized是重量级锁,效率较低。 +- synchronized所用到的锁是存在Java对象头中。在Java1.6中,锁一共有4种状态,由低到高依次是:无锁,偏向锁,轻量级锁,重量级锁,这几种状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 +- monitorenter和monitorexit是上层指令,底层实现可能是偏向锁、轻量级锁、重量级锁等。 +![图片7.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418covtj30wy0fkq4h.jpg) +#### 偏向锁(只有一个线程进入临界区) +>引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。 -加锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(此时会引发竞争,偏向锁会升级为轻量级锁)。 - -膨胀过程:当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将Object设置LightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。 - -偏向锁,顾名思义,它会偏向于第一个获取锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程尝试获取,则持有偏向锁的线程将永远不需要触发同步。 +- 加锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(此时会引发竞争,偏向锁会升级为轻量级锁)。 +- 膨胀过程:当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将Object设置LightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。 +- 偏向锁,顾名思义,它会偏向于第一个获取锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程尝试获取,则持有偏向锁的线程将永远不需要触发同步。 在锁对象的对象头中有个偏向锁的线程ID字段,这个字段如果是空的,第一次获取锁的时候,就CAS将自身的线程ID写入到MarkWord的偏向锁线程ID字段内,将MarkWord中的偏向锁的标识置1。这样下次获取锁的时候,直接检查偏向锁线程ID是否和自身线程ID一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁; -如果不一致,则表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的) - +- 如果不一致,则表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的) +![图片8.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418euloj30o10qxwg1.jpg) -轻量级锁(多个线程交替进入临界区) -引入背景:轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。 +#### 轻量级锁(多个线程交替进入临界区) -轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。 -然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋重试。重试一定次数后膨胀为重量级锁(修改MarkWord,改为指向重量级锁的指针),阻塞当前线程。 +- 引入背景:轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。 +- 轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋重试。重试一定次数后膨胀为重量级锁(修改MarkWord,改为指向重量级锁的指针),阻塞当前线程。 +- 轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示有其他线程尝试获得锁,则释放锁,并唤醒被阻塞的线程。 +![图片9.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418dvcaj30o10nc407.jpg) -轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示有其他线程尝试获得锁,则释放锁,并唤醒被阻塞的线程。 - - -重量级锁(多个线程同时进入临界区) +#### 重量级锁(多个线程同时进入临界区) 在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。 -锁的比较 +### 锁的比较 +![图片10.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418dkizj30hc0au3zd.jpg) +### 锁的优化 +>Java1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 -锁的优化 -Java1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 -自旋锁 +#### 自旋锁 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 -何谓自旋锁? -所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 -自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了规定的时间仍然没有获取到锁,则应该被挂起。 -适应性自旋锁 -JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要获得这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 + +**何谓自旋锁?** + +- 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 +- 自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了规定的时间仍然没有获取到锁,则应该被挂起。 +#### 适应性自旋锁 +- JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。 +- 反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要获得这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。 -锁消除 +#### 锁消除 为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。 -锁粗化 +#### 锁粗化 +``` public void vectorTest(){ Vector vector = new Vector(); for(int i = 0 ; i < 10 ; i++){ @@ -883,14 +886,14 @@ JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适 System.out.println(vector); } -我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 -在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。 -锁粗化是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。 - - +``` +- 我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 +- 在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。 +- 锁粗化是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。 +![图片11.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418dyi2j30o10nc407.jpg) -原子操作原理 +## 原子操作原理 CAS操作的意思是比较并交换,它需要两个数值,一个旧值(期望操作前的值)和新值。操作之前比较两个旧值是否变化,如无变化才交换为新值。 CPU如何实现原子操作 1)在硬件层面,CPU依靠总线加锁和缓存锁定机制来实现原子操作。 From c3eb319d4ccb91eae591564fd732aaa521024515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 14:45:46 +0800 Subject: [PATCH 42/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 9f053a1c..3c129d6e 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -894,31 +894,34 @@ Mark Word 的存储结构: ## 原子操作原理 + CAS操作的意思是比较并交换,它需要两个数值,一个旧值(期望操作前的值)和新值。操作之前比较两个旧值是否变化,如无变化才交换为新值。 -CPU如何实现原子操作 -1)在硬件层面,CPU依靠总线加锁和缓存锁定机制来实现原子操作。 -使用总线锁保证原子性。如果多个CPU同时对共享变量进行写操作(i++),通常无法得到期望的值。CPU使用总线锁来保证对共享变量写操作的原子性,当CPU在总线上输出LOCK信号时,其他CPU的请求将被阻塞住,于是该CPU可以独占共享内存。 +### CPU如何实现原子操作 -使用缓存锁保证原子性。频繁使用的内存地址的数据会缓存于CPU的cache中,那么原子操作只需在CPU内部执行即可,不需要锁住整个总线。缓存锁是指在内存中的数据如果被缓存于CPU的cache中,并且在LOCK操作期间被锁定,那么当它执行锁操作写回到内存时,CPU修改内部的内存地址,并允许它的缓存一致性来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器 缓存的 内存区域数据。当其他CPU回写被锁定的cache行数据时候,会使cache行无效。 -Java如何实现原子操作 -2)Java使用了锁和循环CAS的方式来实现原子操作。 -使用循环CAS实现原子操作。JVM的CAS操作使用了CPU提供的CMPXCHG指令来实现,自旋式CAS操作的基本思路是循环进行CAS操作直到成功为止。1.5之后的并发包中提供了诸如AtomicBoolean, AtomicInteger等包装类来支持原子操作。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作三个问题。 +- **1)在硬件层面,CPU依靠总线加锁和缓存锁定机制来实现原子操作。** + - 使用总线锁保证原子性。如果多个CPU同时对共享变量进行写操作(i++),通常无法得到期望的值。CPU使用总线锁来保证对共享变量写操作的原子性,当CPU在总线上输出LOCK信号时,其他CPU的请求将被阻塞住,于是该CPU可以独占共享内存。 + - 使用缓存锁保证原子性。频繁使用的内存地址的数据会缓存于CPU的cache中,那么原子操作只需在CPU内部执行即可,不需要锁住整个总线。缓存锁是指在内存中的数据如果被缓存于CPU的cache中,并且在LOCK操作期间被锁定,那么当它执行锁操作写回到内存时,CPU修改内部的内存地址,并允许它的缓存一致性来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器 缓存的 内存区域数据。当其他CPU回写被锁定的cache行数据时候,会使cache行无效。 -cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。 +### Java如何实现原子操作 +- **2)Java使用了锁和循环CAS的方式来实现原子操作。** + - 使用循环CAS实现原子操作。JVM的CAS操作使用了CPU提供的CMPXCHG指令来实现,自旋式CAS操作的基本思路是循环进行CAS操作直到成功为止。1.5之后的并发包中提供了诸如AtomicBoolean, AtomicInteger等包装类来支持原子操作。CAS存在ABA,循环时间长开销大,以及只能保证一个共享变量的原子操作三个问题。 + - cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。 + - 使用锁机制实现原子操作。锁机制保证了只有获得锁的线程才能给操作锁定的区域。JVM的内部实现了多种锁机制。除了偏向锁,其他锁的方式都使用了循环CAS,也就是当一个线程想进入同步块的时候,使用循环CAS方式来获取锁,退出时使用CAS来释放锁。 -使用锁机制实现原子操作。锁机制保证了只有获得锁的线程才能给操作锁定的区域。JVM的内部实现了多种锁机制。除了偏向锁,其他锁的方式都使用了循环CAS,也就是当一个线程想进入同步块的时候,使用循环CAS方式来获取锁,退出时使用CAS来释放锁。 +### CAS在OpenJDK中的实现 -CAS在OpenJDK中的实现 以compareAndSwapInt为例: +``` UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)( (x, addr, e)) == e; UNSAFE_END - +``` linux下 +``` inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" @@ -927,15 +930,15 @@ inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* : "cc", "memory"); return exchange_value; } - +``` 程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpchg指令加上lock前缀;如果是在单处理器上运行,就省略lock前缀。 Intel对lock前缀的说明如下: -1)确保对内存的读-改-写操作原子执行(基于总线锁或缓存锁) -2)禁止该指令,与 之前 和 之后 的读写指令重排序 -3)把写缓冲区中的所有数据刷新到内存中。 +- 1)确保对内存的读-改-写操作原子执行(基于总线锁或缓存锁) +- 2)禁止该指令,与 之前 和 之后 的读写指令重排序 +- 3)把写缓冲区中的所有数据刷新到内存中。 -同步容器 +# 同步容器 同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作包括:迭代,跳跃,以及条件运算。 ConcurrentHashMap 它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要再迭代过程中对容器加锁。ConcurrentHasMap返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当修改迭代器会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。 From 4f613798930f3c965e77f666e0c3b7bb19bdaf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 15:47:25 +0800 Subject: [PATCH 43/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 297 ++++++++++++++----------- 1 file changed, 173 insertions(+), 124 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 3c129d6e..7fc5a963 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -939,32 +939,36 @@ Intel对lock前缀的说明如下: # 同步容器 + 同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作包括:迭代,跳跃,以及条件运算。 -ConcurrentHashMap +## ConcurrentHashMap 它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要再迭代过程中对容器加锁。ConcurrentHasMap返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当修改迭代器会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。 -CopyOnWriteArrayList -用于替代同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。写入时复制容器返回的迭代器不会抛出 -ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。 -显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时,仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器。 +## CopyOnWriteArrayList +- 用于替代同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。写入时复制容器返回的迭代器不会抛出 +- ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。 +- 显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时,仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器。 -BlockingQueue -阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上的put方法也永远不会阻塞。 -在构建高可靠的应用程序时,有界队列ArrayBlockingQueue是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。 +## BlockingQueue +- 阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上的put方法也永远不会阻塞。 +- 在构建高可靠的应用程序时,有界队列ArrayBlockingQueue是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。 + +## ThreadLocal -ThreadLocal 在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。 例如有一个静态变量 - +``` public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”); +``` 如果两个线程同时调用sdf.format(…) 那么可能会很混乱,因为sdf使用的内部数据结构可能会被并发的访问所破坏。当然可以使用线程同步,但是开销很大;或者也可以在需要时构造一个局部SImpleDateFormat对象。但这很浪费 -同步工具使用 -Semaphore(信号量) -信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池,或者对容器施加边界。 -Semaphore中管理着一组虚拟的许可permit,许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。 -可以用于实现资源池,当池为空时,请求资源将会阻塞,直至存在资源。将资源返回给池之后会调用release释放许可。 +# 同步工具使用 +## Semaphore(信号量) +- 信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池,或者对容器施加边界。 +- Semaphore中管理着一组虚拟的许可permit,许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。 +- 可以用于实现资源池,当池为空时,请求资源将会阻塞,直至存在资源。将资源返回给池之后会调用release释放许可。 +``` public class BoundedHashSet { private final Set set; private final Semaphore semaphore; @@ -995,21 +999,26 @@ public class BoundedHashSet { } } +``` + +## CyclicBarrier(可循环使用的屏障/栅栏) -CyclicBarrier(可循环使用的屏障/栅栏) -CountDownLatch CyclicBarrier -减计数方式 加计数方式 -计算为0时释放所有等待的线程 计数达到指定值时释放所有等待线程 -计数为0时,无法重置 计数达到指定值时,计数置为0重新开始 -调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 -不可重复利用 可重复利用 -线程在countDown()之后,会继续执行自己的任务,而CyclicBarrier会在所有线程任务结束之后,才会进行后续任务。 +CountDownLatch | CyclicBarrier + ------------- | ------------- +减计数方式 | 加计数方式 +计算为0时释放所有等待的线 | 计数达到指定值时释放所有等待线程 +计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 +计数为0时,无法重置 | 计数达到指定值时,计数置为0重新开始 +调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 | 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 +不可重复利用 | 可重复利用 -Barrier类似于闭锁,它能阻塞一组线程直到某个线程发生。栅栏与闭锁的关键区别在于,前者未达到条件时每个线程都会阻塞在await上,直至条件满足所有线程解除阻塞,后者未达到条件时countDown不会阻塞,条件满足时会解除await线程的阻塞。 -CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用;这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。 -如果对await方法的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都被终止并抛出BrokenBarrierException。如果成功通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来选举产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会在一个子任务线程中执行它。 -可以用于多线程计算数据,最后合并计算结果的场景。CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset方法重置。 +- 线程在countDown()之后,会继续执行自己的任务,而CyclicBarrier会在所有线程任务结束之后,才会进行后续任务。 +- Barrier类似于闭锁,它能阻塞一组线程直到某个线程发生。栅栏与闭锁的关键区别在于,前者未达到条件时每个线程都会阻塞在await上,直至条件满足所有线程解除阻塞,后者未达到条件时countDown不会阻塞,条件满足时会解除await线程的阻塞。 +- CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用;这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都达到栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。 +- 如果对await方法的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都被终止并抛出BrokenBarrierException。如果成功通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来选举产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会在一个子任务线程中执行它。 +- 可以用于多线程计算数据,最后合并计算结果的场景。CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset方法重置。 +``` /** * 通过CyclicBarrier协调细胞自动衍生系统中的计算 */ @@ -1070,11 +1079,12 @@ public class CellularAutomata { mainBoard.waitForConvergence(); } } +``` +## Exchanger(两个线程交换数据) +- 另一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用。 +- Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。 - -Exchanger(两个线程交换数据) -另一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用。 -Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。 +``` public class TestExchanger { private Exchanger exchanger = new Exchanger(); @@ -1114,10 +1124,14 @@ public class TestExchanger { new TestExchanger().start(); } } +``` -CountDownLatch(闭锁) -闭锁可以延迟线程的进度直到其达到终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁达到结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。 -闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件发生了,而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。 +## CountDownLatch(闭锁) + +- 闭锁可以延迟线程的进度直到其达到终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁达到结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。 +- 闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件发生了,而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。 + +``` public class TestCountDownLatch { public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(5); @@ -1156,20 +1170,22 @@ class LatchDemo implements Runnable { } } } +``` +## FutureTask(Future实现类) -FutureTask(Future实现类) -FurureTask是Future接口的唯一实现类。 -FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。 -Future.get方法的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则会阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask的规范确保了这种传递过程能实现结果的安全发布。 -Callable表示的任务可以抛出受检查的或未受检查的异常,并且任何代码都可能抛出一个Error。无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在future.get中被重新抛出。 -当get方法抛出ExecutionException,可能是以下三种情况之一:Callable抛出的受检查异常,RuntimeException,以及Error。 - +- FurureTask是Future接口的唯一实现类。 +- FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。 +- Future.get方法的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则会阻塞直到任务进入完成状态,然后返回结果或者抛出异常。- --- -- FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask的规范确保了这种传递过程能实现结果的安全发布。 +- Callable表示的任务可以抛出受检查的或未受检查的异常,并且任何代码都可能抛出一个Error。无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在future.get中被重新抛出。 +- 当get方法抛出ExecutionException,可能是以下三种情况之一:Callable抛出的受检查异常,RuntimeException,以及Error。 +### Future -Future Future接口设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。 在Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要等待耗时的操作完成。 -示例 + +**示例** +``` public void future() { ExecutorService executor = Executors.newCachedThreadPool(); Future future = executor.submit(new Callable() { @@ -1203,17 +1219,23 @@ private double doSomethingComputation() { System.out.println("doSomethingComputation"); return 0.1; } +``` + +**局限性** -局限性 Future无法实现以下的功能。 -1) 将两个异步操作计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的记过 -2)等待Future集合中的所有任务都完成 -3)仅等待Future集合中最快结束的任务完成,并返回它的结果 -4)通过编程方式完成一个Future任务的执行(以手工设定异步操作结果) -5)应对Future的完成事件(完成回调) -CompletableFuture -实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果) -1)Shop +- 1) 将两个异步操作计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的记过 +- 2)等待Future集合中的所有任务都完成 +- 3)仅等待Future集合中最快结束的任务完成,并返回它的结果 +- 4)通过编程方式完成一个Future任务的执行(以手工设定异步操作结果) +- 5)应对Future的完成事件(完成回调) + +## CompletableFuture + +**实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果)** + +**1)Shop** +``` public class Shop { private ThreadLocalRandom random; private ExecutorService executorService = Executors.newCachedThreadPool(); @@ -1273,8 +1295,10 @@ public class Shop { System.out.println("doSomethingElse"); } } +``` -2) GracefulShop +**2) GracefulShop** +``` 工厂方法创建的Future自己内部维护了一个线程池。 public class GracefulShop { private ThreadLocalRandom random; @@ -1322,11 +1346,12 @@ public class GracefulShop { System.out.println("doSomethingElse"); } } +``` +### 将批量同步操作转为异步操作(并行流/CompletableFuture) - -将批量同步操作转为异步操作(并行流/CompletableFuture) 如果原本的getPrice是同步方法的话,那么如果想批量调用getPrice,提高效率的方法要么使用并行流,要么使用CompletableFuture。 +``` public class SyncShop { private String name; @@ -1392,19 +1417,17 @@ public class FutureTest { calculator.findPricesWithCompletableFuture("my favorite product"); } } - - +``` 使用并行流还是CompletableFuture? 前者是无法调整线程池的大小的(处理器个数),而后者可以。 如果是计算密集型应用,且没有IO,那么推荐使用并行流 如果是IO密集型,需要等待IO,那么使用CompletableFuture灵活性更高,比如根据《Java并发编程实战》中给出的公式计算线程池合适的大小。 - - -多个异步任务合并 +### 多个异步任务合并 逻辑如下: 从每个商店获取price,price以某种格式返回。拿到price后解析price,然后调用远程API根据折扣计算最终price。 可以分为三个任务,每个商店都要执行这三个任务。 +``` public class PipelineShop { private String name; @@ -1514,9 +1537,10 @@ public class BestProductPriceWithDiscountCalculator { return futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); } } +``` - -回调 +### 回调 +``` public class CallbackBestProductPriceCalculator { private List shops = Arrays.asList( new PipelineShop("BestPrice"), @@ -1546,31 +1570,39 @@ public void testCallback(){ ).toArray(size -> new CompletableFuture[size]); CompletableFuture.allOf(futures).join(); } +``` +### API -API CompletableFuture类实现了CompletionStage和Future接口。Future是Java 5添加的类,用来描述一个异步计算的结果,但是获取一个结果时方法较少,要么通过轮询isDone,确认完成后,调用get()获取值,要么调用get()设置一个超时时间。但是这个get()方法会阻塞住调用线程,这种阻塞的方式显然和我们的异步编程的初衷相违背。 为了解决这个问题,JDK吸收了guava的设计思想,加入了Future的诸多扩展功能形成了CompletableFuture。 CompletionStage是一个接口,从命名上看得知是一个完成的阶段,它里面的方法也标明是在某个运行阶段得到了结果之后要做的事情。、 -supplyAsync 提交任务 + +#### supplyAsync 提交任务 +``` public static CompletableFuture supplyAsync(Supplier supplier); public static CompletableFuture supplyAsync(Supplier supplier, Executor executor); - -thenApply 变换(等待前一个任务返回后执行,处于同一个CompletableFuture) +``` +#### thenApply 变换(等待前一个任务返回后执行,处于同一个CompletableFuture) +``` public CompletionStage thenApply(Function fn); public CompletionStage thenApplyAsync(Function fn); public CompletionStage thenApplyAsync(Function fn,Executor executor); +``` 首先说明一下以Async结尾的方法都是可以异步执行的,如果指定了线程池,会在指定的线程池中执行,如果没有指定,默认会在ForkJoinPool.commonPool()中执行,下文中将会有好多类似的,都不详细解释了。关键的入参只有一个Function,它是函数式接口,所以使用Lambda表示起来会更加优雅。它的入参是上一个阶段计算后的结果,返回值是经过转化后结果。 不带Async的方法会在和前一个任务相同的线程中处理; 以Async的方法会将任务提交到一个线程池,所有每个任务是由不同的线程处理的。 +``` public void thenApply() { String result = CompletableFuture.supplyAsync(() -> "hello").thenApply(s -> s + " world").join(); System.out.println(result); } -thenAccept 消耗 +``` +#### thenAccept 消耗 +``` public CompletionStage thenAccept(Consumer action); public CompletionStage thenAcceptAsync(Consumer action); public CompletionStage thenAcceptAsync(Consumer action,Executor executor); @@ -1578,8 +1610,10 @@ public CompletionStage thenAcceptAsync(Consumer action,Executor public void thenAccept() { CompletableFuture.supplyAsync(() -> "hello").thenAccept(s -> System.out.println(s + " world")); } +``` -thenRun 执行下一步操作,不关心上一步结果 +#### thenRun 执行下一步操作,不关心上一步结果 +``` public CompletionStage thenRun(Runnable action); public CompletionStage thenRunAsync(Runnable action); public CompletionStage thenRunAsync(Runnable action,Executor executor); @@ -1595,16 +1629,16 @@ public void thenRun() { return "hello"; }).thenRun(() -> System.out.println("hello world")); } +``` - - - -thenCombine 结合两个CompletionStage的结果,进行转化后返回 +#### thenCombine 结合两个CompletionStage的结果,进行转化后返回 +``` public CompletionStage thenCombine(CompletionStage other,BiFunction fn); public CompletionStage thenCombineAsync(CompletionStage other,BiFunction fn); public CompletionStage thenCombineAsync(CompletionStage other,BiFunction fn,Executor executor); - +``` 它需要原来的处理返回值,并且other代表的CompletionStage也要返回值之后,利用这两个返回值,进行转换后返回指定类型的值。 +``` public void thenCombine() { String result = CompletableFuture.supplyAsync(() -> { try { @@ -1623,18 +1657,24 @@ public void thenCombine() { }), (s1, s2) -> s1 + " " + s2).join(); System.out.println(result); } +``` - -thenCompose(合并多个CompletableFuture,流水线执行,在调用外部接口返回CompletableFuture类型时更方便) +#### thenCompose(合并多个CompletableFuture,流水线执行,在调用外部接口返回CompletableFuture类型时更方便) +``` CompletableFuture thenCompose(Function> fn); -thenCompose方法允许对两个异步操作(supplyAsync)进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。 -创建两个CompletableFuture,对第一个CompletableFuture对象调用thenCompose,并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。 +``` -thenAccptBoth 结合两个CompletionStage的结果,进行消耗 +- thenCompose方法允许对两个异步操作(supplyAsync)进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。 +- 创建两个CompletableFuture,对第一个CompletableFuture对象调用thenCompose,并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。 + +#### thenAccptBoth 结合两个CompletionStage的结果,进行消耗 +``` public CompletionStage thenAcceptBoth(CompletionStage other,BiConsumer action); public CompletionStage thenAcceptBothAsync(CompletionStage other,BiConsumer action); public CompletionStage thenAcceptBothAsync(CompletionStage other,BiConsumer action, Executor executor); +``` 它需要原来的处理返回值,并且other代表的CompletionStage也要返回值之后,利用这两个返回值,进行消耗。 +``` public void thenAcceptBoth() { CompletableFuture.supplyAsync(() -> { try { @@ -1652,14 +1692,16 @@ public void thenAcceptBoth() { return "world"; }), (s1, s2) -> System.out.println(s1 + " " + s2)); } +``` - - -runAfterBoth 在两个CompletionStage都运行完执行,不关心上一步结果 +#### runAfterBoth 在两个CompletionStage都运行完执行,不关心上一步结果 +``` public CompletionStage runAfterBoth(CompletionStage other,Runnable action); public CompletionStage runAfterBothAsync(CompletionStage other,Runnable action); public CompletionStage runAfterBothAsync(CompletionStage other,Runnable action,Executor executor); +``` 不关心这两个CompletionStage的结果,只关心这两个CompletionStage执行完毕,之后在进行操作(Runnable)。 +``` public void runAfterBoth() { CompletableFuture.supplyAsync(() -> { try { @@ -1677,13 +1719,16 @@ public void runAfterBoth() { return "s2"; }), () -> System.out.println("hello world")); } +``` - -applyToEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的转化操作 +#### applyToEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的转化操作 +``` public CompletionStage applyToEither(CompletionStage other,Function fn); public CompletionStage applyToEitherAsync(CompletionStage other,Function fn); public CompletionStage applyToEitherAsync(CompletionStage other,Function fn,Executor executor); +``` 我们现实开发场景中,总会碰到有两种渠道完成同一个事情,所以就可以调用这个方法,找一个最快的结果进行处理。 +``` public void applyToEither() { String result = CompletableFuture.supplyAsync(() -> { try { @@ -1702,10 +1747,10 @@ public void applyToEither() { }), s -> s).join(); System.out.println(result); } +``` - - -acceptEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的消耗操作 +#### acceptEither 两个CompletionStage,谁计算的快,我就用那个CompletionStage的结果进行下一步的消耗操作 +``` public CompletionStage acceptEither(CompletionStage other,Consumer action); public CompletionStage acceptEitherAsync(CompletionStage other,Consumer action); public CompletionStage acceptEitherAsync(CompletionStage other,Consumer action,Executor executor); @@ -1729,9 +1774,9 @@ public void acceptEither() { while (true) { } } - - -runAfterEither 两个CompletionStage,任何一个完成了都会执行下一步的操作,不关心上一步结果 +``` +#### runAfterEither 两个CompletionStage,任何一个完成了都会执行下一步的操作,不关心上一步结果 +``` public CompletionStage runAfterEither(CompletionStage other,Runnable action); public CompletionStage runAfterEitherAsync(CompletionStage other,Runnable action); public CompletionStage runAfterEitherAsync(CompletionStage other,Runnable action,Executor executor); @@ -1752,9 +1797,10 @@ public void runAfterEither() { return "s2"; }), () -> System.out.println("hello world")); } +``` - -exceptionally 当运行时出现了异常,可以进行补偿 +#### exceptionally 当运行时出现了异常,可以进行补偿 +``` public CompletionStage exceptionally(Function fn); public void exceptionally() { String result = CompletableFuture.supplyAsync(() -> { @@ -1773,9 +1819,10 @@ public void exceptionally() { }).join(); System.out.println(result); } +``` - -whenComplete 当运行完成时,若有异常则改变返回值,否则返回原值 +#### whenComplete 当运行完成时,若有异常则改变返回值,否则返回原值 +``` public CompletionStage whenComplete(BiConsumer action); public CompletionStage whenCompleteAsync(BiConsumer action); public CompletionStage whenCompleteAsync(BiConsumer action,Executor executor); @@ -1800,7 +1847,7 @@ public void whenComplete() { }).join(); System.out.println(result); } - +``` null java.lang.RuntimeException: 测试一下异常情况 @@ -1808,7 +1855,8 @@ java.lang.RuntimeException: 测试一下异常情况 hello world 这里也可以看出,如果使用了exceptionally,就会对最终的结果产生影响,它无法影响如果没有异常时返回的正确的值,这也就引出下面我们要介绍的handle。 -handle 当运行完成时,无论有无异常均可转换 +#### handle 当运行完成时,无论有无异常均可转换 +``` public CompletionStage handle(BiFunction fn); public CompletionStage handleAsync(BiFunction fn); public CompletionStage handleAsync(BiFunction fn,Executor executor); @@ -1833,9 +1881,10 @@ public void handle() { }).join(); System.out.println(result); } +``` hello world - +``` public void handle() { String result = CompletableFuture.supplyAsync(() -> { try { @@ -1852,30 +1901,35 @@ public void handle() { }).join(); System.out.println(result); } - +``` s1 -allOf +#### allOf allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有CompletableFuture对象执行完成之后,它返回一个CompletableFuture对象。这意味着,如果你需要等待最初Stream中的所有CompletableFuture对象执行完毕,对allOf方法返回的CompletableFuture执行join操作是个不错的注意。 -anyOf +#### anyOf 只要CompletableFuture对象数组中有一个执行完毕,便不再等待。 +## ForkJoin -ForkJoin 双端队列LinkedBlockingDeque适用于另一种相关模式,即工作密取(work stealing)。在生产者——消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列头部秘密地获取工作。密取工作模式比传统的生产者——消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的头部而不是从尾部获取工作,因此进一步降低了队列上的竞争程度。 -第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。 -第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。 +![图片12.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m418h9wxj30s20fedhj.jpg) +![图片13.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m7q2pz79j30sa0hudiq.jpg) + +- 第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。 +- 第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。 + Fork/Join使用两个类来完成以上两件事情: -ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类: -oRecursiveAction:用于没有返回结果的任务。 -oRecursiveTask :用于有返回结果的任务。 -ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。 +- ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承- ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类: + - oRecursiveAction:用于没有返回结果的任务。 + - oRecursiveTask :用于有返回结果的任务。 +- ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。 threshold 临界值 RecursiveTask有两个方法:fork和join fork是执行子任务,join是取得子任务的结果,用于合并 +``` public class TestForkJoin { public static void main(String[] args) throws InterruptedException, ExecutionException { ForkJoinPool pool = new ForkJoinPool(); @@ -1918,22 +1972,19 @@ class ForkJoinCalculator extends RecursiveTask { } } } -原理浅析 -1. 每个Worker线程都维护一个任务队列,即ForkJoinWorkerThread中的任务队列。 - -2. 任务队列是双向队列,这样可以同时实现LIFO和FIFO。 - -3. 子任务会被加入到原先任务所在Worker线程的任务队列。 - -4. Worker线程用LIFO的方法取出任务,也就后进队列的任务先取出来(子任务总是后加入队列,但是需要先执行)。 - -5. Worker线程的任务队列为空,会随机从其他的线程的任务队列中拿走一个任务执行(所谓偷任务:steal work,FIFO的方式)。 +``` -6. 如果一个Worker线程遇到了join操作,而这时候正在处理其他任务,会等到这个任务结束。否则直接返回。 +### 原理浅析 +- 1. 每个Worker线程都维护一个任务队列,即ForkJoinWorkerThread中的任务队列。 +- . 任务队列是双向队列,这样可以同时实现LIFO和FIFO。 +- . 子任务会被加入到原先任务所在Worker线程的任务队列。 +- 4. Worker线程用LIFO的方法取出任务,也就后进队列的任务先取出来(子任务总是后加入队列,但是需要先执行)。 +- 5. Worker线程的任务队列为空,会随机从其他的线程的任务队列中拿走一个任务执行(所谓偷任务:steal work,FIFO的方式)。 +- 6. 如果一个Worker线程遇到了join操作,而这时候正在处理其他任务,会等到这个任务结束。否则直接返回。 +- 7. 如果一个Worker线程偷任务失败,它会用yield或者sleep之类的方法休息一会儿,再尝试偷任务(如果所有线程都是空闲状态,即没有任务运行,那么该线程也会进入阻塞状态等待新任务的到来)。 -7. 如果一个Worker线程偷任务失败,它会用yield或者sleep之类的方法休息一会儿,再尝试偷任务(如果所有线程都是空闲状态,即没有任务运行,那么该线程也会进入阻塞状态等待新任务的到来)。 +### 与MapReduce的区别 -与MapReduce的区别 MapReduce是把大数据集切分成小数据集,并行分布计算后再合并。 ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。 @@ -1941,12 +1992,10 @@ ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算 二者共同点:都是用于执行并行任务的。基本思想都是把问题分解为一个个子问题分别计算,再合并结果。应该说并行计算都是这种思想,彼此独立的或可分解的。从名字上看Fork和Map都有切分的意思,Join和Reduce都有合并的意思,比较类似。 区别: +- 1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。 +- 2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。 -1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。 - -2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。 - -线程池使用 +# 线程池使用 引入原因 1)任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接,使得任务在完成前面的请求之前可以接受新的请求,从而提高响应性。 2)任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,程序的吞吐量将得到提高。 From 8d6326b544efc35b4dd3a4d4de32ff536590e0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 17:04:45 +0800 Subject: [PATCH 44/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 60 +++++++++++++------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 7fc5a963..e6e91480 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -1996,44 +1996,45 @@ ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算 - 2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。 # 线程池使用 -引入原因 -1)任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接,使得任务在完成前面的请求之前可以接受新的请求,从而提高响应性。 -2)任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,程序的吞吐量将得到提高。 -3)任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。 -无限制创建线程的不足: -1)线程生命周期的开销非常高 -2)资源消耗 -3)稳定性 +## 引入原因 +- 1)任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接,使得任务在完成前面的请求之前可以接受新的请求,从而提高响应性。 +- 2)任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,程序的吞吐量将得到提高。 +- 3)任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。 + +**无限制创建线程的不足:** +- 1)线程生命周期的开销非常高 + 2)资源消耗 +- 3)稳定性 解决方式:线程池 Executor框架 使用线程池的好处: -1)降低资源消耗 -2)提高响应速度 -3)提高线程的可管理性 +- 1)降低资源消耗 +- 2)提高响应速度 +- 3)提高线程的可管理性 -Executor ExecutorService ScheduledExecutorService +## Executor ExecutorService ScheduledExecutorService 继承体系 +![图片14.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr2x9bj30mq0jj0u3.jpg) +![图片15.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr4wnlj30wq0awta0.jpg) +![图片16.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr23h9j30lu0lhwfl.jpg) +### ExecutorService +![图片17.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr1xfdj315g0jrgqq.jpg) - - - - -ExecutorService - - -ScheduledExecutorService +### ScheduledExecutorService +![图片18.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr1a7cj318b09wtbj.jpg) 返回值 - +![图片19.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr0e61j30ew095wf0.jpg) 示例 +``` public class QuoteTask implements Callable { private final TravelCompany company; private final TravelInfo travelInfo; @@ -2073,19 +2074,20 @@ public class QuoteTask implements Callable { return quotes; } } +``` - -ThreadPoolExecutor +## ThreadPoolExecutor 创建线程池 +![图片20.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr0v57j30w8029jrj.jpg) 线程动态变化 -1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 -2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 -3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 -4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理 -5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 -6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 +- 1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 +- 2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 +- 3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 +- 4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理 +- 5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 +- 6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 创建一个线程池时需要以下几个参数: 1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 From c233e0a3cc37a1d641fa757bbf38c7ad9d7e0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 17:30:52 +0800 Subject: [PATCH 45/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 45 +++++++++++--------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index e6e91480..39dbcd92 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -2090,36 +2090,27 @@ public class QuoteTask implements Callable { - 6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 创建一个线程池时需要以下几个参数: -1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 -2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列: - a)ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO - b) LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO,吞吐量高于ArrayBlockingQueue,Executors.newFixedThreadPoll()使用了这个队列 - c)SynchronousQueue:一个只存储一个元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,Executors#newCachedThreadPoll()使用了这个队列 - d)PriorityBlockingQueue:具有优先级的无界阻塞队列 - -3)maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果使用了无界队列该参数就没有意义了。 - -4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 - -5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,或者当线程池已关闭时,会采用一种策略处理提交的新任务。这个策略默认是AbortPolicy,表示无法处理新任务时抛出异常。有以下四种饱和策略: - a)AbortPolicy:直接抛出异常 - b)CallerRunsPolicy:使用调用者所在线程来运行任务 - c)DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务 - d)DiscardPolicy:不处理,直接丢弃 - +- 1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 +- 2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列: + - a)ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO + - b) LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO,吞吐量高于ArrayBlockingQueue,Executors.newFixedThreadPoll()使用了这个队列 + - c)SynchronousQueue:一个只存储一个元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,Executors#newCachedThreadPoll()使用了这个队列 + - d)PriorityBlockingQueue:具有优先级的无界阻塞队列 +- 3)maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果使用了无界队列该参数就没有意义了。 +- 4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 +- 5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,或者当线程池已关闭时,会采用一种策略处理提交的新任务。这个策略默认是AbortPolicy,表示无法处理新任务时抛出异常。有以下四种饱和策略: + - a)AbortPolicy:直接抛出异常 + - b)CallerRunsPolicy:使用调用者所在线程来运行任务 + - c)DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务 + - d)DiscardPolicy:不处理,直接丢弃 也可以自定义饱和策略。 - -6)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。出现timeout情况下,而且线程数超过了核心线程数,会销毁销毁线程。保持在corePoolSize数。除非设置了allowCoreThreadTimeOut和超时时间,这种情况线程数可能减少到0,最大可能是Integer.MAX_VALUE。 +- 6)keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。出现timeout情况下,而且线程数超过了核心线程数,会销毁销毁线程。保持在corePoolSize数。除非设置了allowCoreThreadTimeOut和超时时间,这种情况线程数可能减少到0,最大可能是Integer.MAX_VALUE。 如果任务很多,每个任务执行的时间比较短,可以调大时间,提高线程的利用率。 + - allowCoreThreadTimeOut为true该值为true,则线程池数量最后销毁到0个。 + - allowCoreThreadTimeOut为false销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。 +- 7)TimeUnit(线程活动保持时间的单位) +### 使用注意 -allowCoreThreadTimeOut为true -该值为true,则线程池数量最后销毁到0个。 - -allowCoreThreadTimeOut为false -销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。 - -7)TimeUnit(线程活动保持时间的单位) -使用注意 1、只有当任务都是同类型并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成拥塞。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——web服务器、邮件服务器、文件服务器等,它们的请求通常都是同类型的并且相互独立的。 2、设置线程池的大小: From 01207d057db9a34349dfbdaa7971f7ed2c08d9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 18:57:38 +0800 Subject: [PATCH 46/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 110 +++++++++++++++---------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 39dbcd92..02324967 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -2111,35 +2111,29 @@ public class QuoteTask implements Callable { - 7)TimeUnit(线程活动保持时间的单位) ### 使用注意 -1、只有当任务都是同类型并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成拥塞。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——web服务器、邮件服务器、文件服务器等,它们的请求通常都是同类型的并且相互独立的。 - -2、设置线程池的大小: +- 1、只有当任务都是同类型并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成拥塞。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——web服务器、邮件服务器、文件服务器等,它们的请求通常都是同类型的并且相互独立的。 +- 2、设置线程池的大小: 基于Runtime.getRuntime().avialableprocessors() 进行动态计算 对于计算密集型的任务,在N个处理器的系统上,当线程池为N+1时,通过能实现最优的利用率(缺页故障等暂停时额外的线程也能确保CPU时钟周期不被浪费)。 对于包含IO操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大,比如2*N。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。这种估算不需要很精确,而且可以通过一些分析或监控工具来获得。你还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率。 最佳线程数目 = (线程等待时间与线程计算时间之比 + 1)* CPU数目 -3、线程的创建与销毁 +- 3、线程的创建与销毁 基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。 - -4、管理队列任务 +- 4、管理队列任务 ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列、有界队列和同步移交。 一种稳妥的资源管理策略是使用有界队列,有界队列有助于避免资源耗尽的情况发生,但又带来了新的问题:当队列填满后,新的任务该怎么办? - -5、饱和策略 +- 5、饱和策略 当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler来修改。JDK提供了几种不同的RejectedExecutionHandler的实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。 - -1)中止策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 -2)当新提交的任务无法保存到队列中执行时,抛弃策略会悄悄抛弃该任务。 -3)抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试重新提交下一个将被执行的任务(如果工作队列是一个优先级队列,那么抛弃最旧的将抛弃优先级最高的任务) - -4)调用者运行策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。为什么好?因为当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。 - -6、线程工厂 + - 1)中止策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 + - 2)当新提交的任务无法保存到队列中执行时,抛弃策略会悄悄抛弃该任务。 + - 3)抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试重新提交下一个将被执行的任务(如果工作队列是一个优先级队列,那么抛弃最旧的将抛弃优先级最高的任务) + - 4)调用者运行策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。为什么好?因为当服务器过载时,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。 +- 6、线程工厂 在许多情况下都需要使用定制的线程工厂方法。例如,你希望为线程池中的线程指定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录,你还可能希望修改线程的优先级(虽然不提倡这样做),或者只是给线程取一个更有意义的名字,用来解释线程的转储信息和错误日志。 +- 7、在调用构造函数后再定制ThreadPoolExecutor -7、在调用构造函数后再定制ThreadPoolExecutor - -扩展ThreadPoolExecutor +### 扩展ThreadPoolExecutor +``` public class TimingThreadPool extends ThreadPoolExecutor { public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); @@ -2178,11 +2172,12 @@ public class TimingThreadPool extends ThreadPoolExecutor { } } } - -任务时限 +``` +### 任务时限 Future的get方法可以限时,如果超时会抛出TimeOutException,那么此时可以通过cancel方法来取消任务。如果编写的任务是可取消的,那么可以提前中止它,以免消耗过多的资源。 创建n个任务,将其提交到一个线程池,保留n个Future,并使用限时的get方法通过Future串行地获取每一个结果,这一切都很简单。但还有一个更简单的实现:invokeAll。 将多个任务提交到一个ExecutorService并获得结果。invokeAll方法的参数是一组任务,并返回一组Future。这两个集合有着相同的结构。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable关联起来。当所有任务执行完毕时,或者调用线程被中断时,又或者超时,invokeAll将返回。当超时时,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。 +``` public class QuoteTask implements Callable { private final TravelCompany company; private final TravelInfo travelInfo; @@ -2222,24 +2217,29 @@ public class QuoteTask implements Callable { return quotes; } } +``` +### 任务关闭 -任务关闭 线程有一个相应的所有者,即创建该线程的类,因此线程池是工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。 ExecutorService中提供了shutdown和shutdownNow方法。 前者是正常关闭,后者是强行关闭。 -1)它们都会阻止新任务的提交 -2)正常关闭是停止空闲线程,正在执行的任务继续执行并完成所有未执行的任务 -3)强行关闭是停止所有(空闲+工作)线程,关闭当前正在执行的任务,然后返回所有尚未执行的任务。 +- 1)它们都会阻止新任务的提交 +- 2)正常关闭是停止空闲线程,正在执行的任务继续执行并完成所有未执行的任务 +- 3)强行关闭是停止所有(空闲+工作)线程,关闭当前正在执行的任务,然后返回所有尚未执行的任务。 通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow方法。 但是我们无法通过常规方法来找出哪些任务已经开始但尚未结束,这意味着我们无法在关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查。要知道哪些任务还没有完成,你不仅需要知道哪些任务还没有开始,而且还需要知道当Executor关闭时哪些任务正在执行。 + -------------------------------- 处理非正常的线程终止(只对execute提交的任务有效,submit提交的话会在future.get时将受检异常直接抛出) + 要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally块来接收通知,因此当线程结束时,将有新的线程来代替它。如果没有提供捕获异常处理器或者其他的故障通知机制,那么任务会悄悄失败,从而导致很大的混乱。如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。 + 只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。 +``` public class QuoteTask implements Callable { private final TravelCompany company; private final TravelInfo travelInfo; @@ -2279,19 +2279,22 @@ public class QuoteTask implements Callable { return quotes; } } +``` +## ScheduledThreadPoolExecutor -ScheduledThreadPoolExecutor 它继承自ThreadPoolExecutor,主要用来在给定的延迟之后运行任务,或者定期执行任务。Timer是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。 +``` public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler); } - +``` 内部工作队列是DelayedWorkQueue,它是一个无界队列,maxPoolSize这个参数没有意义。 +``` static class DelayedWorkQueue extends AbstractQueue implements BlockingQueue @@ -2317,68 +2320,87 @@ class ThreadPoolDemo2 implements Callable { return sum; } } +``` - -Executors +## Executors Executors是一个工厂类,可以创建3种类型的ThreadPoolExecutor和2种类型的ScheduledThreadPool。 +![图片21.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jr0gjdj315f0l6tf6.jpg) + +### FixedThreadPool -FixedThreadPool 创建固定线程数的FixedThreadPool,适用于负载比较重的服务器。 +``` public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); } +``` corePoolSize和maxPoolSize都被设置为创建FixedThreadPoolExecutor时指定的参数nThreads。 + keepAliveTime为0表示多余的空闲线程将会被立即终止。 + 使用无界队列LinkedBlockingQueue来作为线程池的工作队列,并且默认容量为Integer.MAX_VALUE。使用无界队列会带来以下影响: -1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize -2)maximumPoolSize是一个无效的参数 -3)keepAliveTime是一个无效参数 -4)运行中的FixedThreadPool(未执行shutdown或shutdownNow)不会拒绝任务。 +- 1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize +- 2)maximumPoolSize是一个无效的参数 +- 3)keepAliveTime是一个无效参数 +- 4)运行中的FixedThreadPool(未执行shutdown或shutdownNow)不会拒绝任务。 -SingleThreadExecutor +### SingleThreadExecutor 适用于需要保证顺序地执行各个任务,并且在任意时间点不会有多个线程活动的应用场景。 +``` public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); } +``` 它也是使用无界队列,corePoolSize和maxPoolSize都为1。 -CachedThreadPool +### CachedThreadPool 大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。 +``` public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); } +``` 使用没有缓冲区、只能存储一个元素的SynchronousQueue作为工作队列。 + maxPoolSize是无界的,如果主线程提交任务的速度高于maxPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存。 +![图片22.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jqzse0j30md0ff0tg.jpg) 任务执行过程: -1)首先执行SynchronousQueue#offer(Runnable) 。如果当前maxPool中有空闲线程正在执行SynchronousQueue#poll,那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行;否则执行2) -2)当初始maxPool为空,或者maxPool中没有空闲线程时,此时CachedThreadPool会创建一个新线程执行任务 -3)在2)中新创建的线程执行任务完毕后,会执行SynchronousQueue#poll,这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒。如果60秒内主线程提交了一个新任务,那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。 +- 1)首先执行SynchronousQueue#offer(Runnable) 。如果当前maxPool中有空闲线程正在执行SynchronousQueue#poll,那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行;否则执行2) +- 2)当初始maxPool为空,或者maxPool中没有空闲线程时,此时CachedThreadPool会创建一个新线程执行任务 +- 3)在2)中新创建的线程执行任务完毕后,会执行SynchronousQueue#poll,这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒。如果60秒内主线程提交了一个新任务,那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。 + +![图片23.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7m9jqznlsj30mb0cq3z4.jpg) + +### ScheduledThreadPoolExecutor -ScheduledThreadPoolExecutor 固定线程个数,适用于多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的梳理的应用场景。 -SingleThreadScheduledExecutor + +### SingleThreadScheduledExecutor +``` public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1, threadFactory)); } - +``` 适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。 -CompletionService +## CompletionService + CompletionService将Executor和BlockingQueue的概念融合在一起,你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时封装为Future。ExecutorCompletionService实现了CompletionService并将计算任务委托给一个Executor。 ExecutorCompletionService的实现非常简单,在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用FutureTask的done方法。当提交某个任务时,该任务将首先包装为一个QueueingFuture,这是FutureTask的一个子类,然后再改写子类的done方法,并将结果放入BlockingQueue中。take和poll方法委托给了BlockingQueue,这些方法会在得出结果之前阻塞。 多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。 +``` public class CompletionServiceTest { public void test() throws InterruptedException, ExecutionException { ExecutorService exec = Executors.newCachedThreadPool(); @@ -2396,7 +2418,7 @@ public class CompletionServiceTest { exec.shutdown(); } } - +``` J.U.C 源码解析 实现整个并发体系的真正底层是CPU提供的lock前缀+cmpxchg指令和POSIX的同步原语(mutex&condition) From 99dd739bad16a2186b5b19ca097755fade6b0f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Fri, 4 Oct 2019 20:43:57 +0800 Subject: [PATCH 47/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 201 +++++++++++++++++-------- 1 file changed, 136 insertions(+), 65 deletions(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 02324967..6772c6b3 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -2420,35 +2420,47 @@ public class CompletionServiceTest { } ``` -J.U.C 源码解析 +# J.U.C 源码解析 实现整个并发体系的真正底层是CPU提供的lock前缀+cmpxchg指令和POSIX的同步原语(mutex&condition) + synchronized和wait¬ify基于JVM的monitor,monitor底层又是基于POSIX同步原语。 + volatile基于CPU的lock前缀指令实现内存屏障。 + 而J.U.C是基于LockSupport,底层基于POSIX同步原语。 -AbstractQueuedSynchronizer(AQS) + +## AbstractQueuedSynchronizer(AQS) + 在ReentrantLock和Semaphore这两个接口之间存在许多共同点,这两个类都可以用作一个阀门,即每次只允许一定数量的线程通过,并当线程到达阀门时,可以通过(在调用lock或acquire时成功返回),也可以等待(在调用lock或acquire时阻塞),还可以取消(在调用tryLock或tryAcquire时返回假,表示在指定的时间内锁是不可用的或无法得到许可)。 + 可以通过锁来实现计数信号量。 + 事实上,它们在实现时都使用了一个共同的基类,即AbstractQueuedSynchronizer(AQS),这个类也是其他许多同步类的基类。AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore,还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask,都是基于AQS构造的。 + 在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。在设计AQS时充分考虑了可伸缩性,因此java.util.concurrent中所有基于AQS构建的同步器都能获得这个优势。 + 在基于AQS构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。当使用锁或信号量时,获取操作的含义就很直观,即获取的是锁或许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。 AQS负责管理同步器类中的状态,它管理了一个整数类型的状态信息,可以通过getState、setState以及compareAndSetState等protected类型方法来进行操作。这个整数可以用于表示任意状态。 - +![图片24.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzddvwj30s60dl0tl.jpg) 它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。 + 子类通过继承AQS并实现它的抽象方法来管理同步状态,修改同步状态依赖于AQS的getState、setState、compareAndSetState来进行操作,它们能够保证状态的改变是安全的。 + 子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。AQS既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。 -AQS的接口 +### AQS的接口 AQS的设计是基于模板方法模式的,使用者需要继承同步器并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用使用者重写的方法。 同步器可重写的方法: - +![图片25.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzcx0oj30pt0c6gna.jpg) 同步器提供的模板方法: +![图片26.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzdomoj30pp0haq5t.jpg) - -AQS使用实例(互斥锁,tryAcquire只需一次CAS) +### AQS使用实例(互斥锁,tryAcquire只需一次CAS) +``` public class Mutex implements Lock { private final Sync sync = new Sync(); @@ -2512,9 +2524,14 @@ public class Mutex implements Lock { } } } -AQS实现 +``` + +### AQS实现 + 主要工作基于CLH队列,voliate关键字修饰的状态state,线程去修改状态成功了就是获取成功,失败了就进队列等待,等待唤醒。在等待唤醒的时候,很多时候会使用自旋(while(!cas()))的方式,不停的尝试获取锁,直到被其他线程获取成功。 -AQS#state getState setState + +#### AQS#state getState setState +``` /** * The synchronization state. */ @@ -2537,11 +2554,16 @@ protected final int getState() { protected final void setState(int newState) { state = newState; } +``` + +#### 同步队列 -同步队列 AQS依赖内部的CLH同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造为一个Node并将其加入同步队列,并阻塞当前线程。当同步状态释放时,会把后继节点线程唤醒,使其再次尝试获取同步状态。后继节点将会在获取同步状态成功时将自己设置为头节点。 -AQS#Node +#### AQS#Node + +![图片27.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzcgvrj30pu0dsac7.jpg) +``` static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); @@ -2677,31 +2699,38 @@ static final class Node { this.thread = thread; } } +``` +![图片28.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzadvcj30pu09l0th.jpg) +#### 独占式同步状态 - -独占式同步状态 在获取同步状态时,AQS调用tryAcquire获取同步状态。AQS维护一个同步队列,获取同步状态失败的线程都会被加入到队列中并在队列进行自旋(等待);移出队列的条件是前驱节点是头结点且成功获取了同步状态; + 在释放同步状态时,AQS调用tryRelease释放同步状态,然后唤醒头节点的后继节点,使其尝试获取同步状态。 -AQS#acquire + +##### AQS#acquire + acquire(int)可以获取同步状态,对中断不敏感。 -1)调用自定义同步器实现的tryAcquire -2)如果成功,那么结束 -3)如果失败,那么调用addWaiter加入同步队列尾部,并调用acquireQueued获取同步状态(前提是前驱节点为head) -3.1)如果获取到了,那么将自己设置为头节点,返回 -3,2)如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL, - 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 - 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 +- 1)调用自定义同步器实现的tryAcquire +- 2)如果成功,那么结束 +- 3)如果失败,那么调用addWaiter加入同步队列尾部,并调用acquireQueued获取同步状态(前提是前驱节点为head) + - 3.1)如果获取到了,那么将自己设置为头节点,返回 + - 3,2)如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL, + - 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 + - 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 +``` public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } -addWaiter(新Node添加到同步队列尾部,初始状态下head是一个空节点) -获取同步状态失败的线程会被构造成Node加入到同步队列尾部,这个过程必须是线程安全的,AQS基于CAS来设置同步队列的尾节点compareAndSetTail。 - +``` +###### addWaiter(新Node添加到同步队列尾部,初始状态下head是一个空节点) +![图片29.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebza85hj30m30ar3z3.jpg) +获取同步状态失败的线程会被构造成Node加入到同步队列尾部,这个过程必须是线程安全的,AQS基于CAS来设置同步队列的尾节点compareAndSetTail。 +``` private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure @@ -2716,10 +2745,14 @@ private Node addWaiter(Node mode) { enq(node); return node; } +``` + 可能tail为null,或者tail不为null,但CAS添加node至尾部失败,此时会enq 如果tail为null,则设置head和tail都指向一个空节点 + 然后循环CAS添加node至尾部,直至成功。 +``` private Node enq(final Node node) { for (;;) { Node t = tail; @@ -2735,12 +2768,15 @@ private Node enq(final Node node) { } } } -acquireQueued +```` +###### acquireQueued +![图片30.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebz9h50j30pi07qjru.jpg) -设置首节点是由获取同步状态成功的线程来完成的,因为只有一个线程能够成功获取同步状态,因此设置头节点的方法并不需要CAS的包装。 +- 设置首节点是由获取同步状态成功的线程来完成的,因为只有一个线程能够成功获取同步状态,因此设置头节点的方法并不需要CAS的包装。 +- 如果自己是第二个结点,那么尝试获取同步状态,如果成功,那么将自己设置为头节点,并返回。 +- 如果自己不是第二个结点或者CAS获取失败,那么判断是否应该阻塞,如果应该,那么阻塞,否则自旋重新尝试获取同步状态。 -如果自己是第二个结点,那么尝试获取同步状态,如果成功,那么将自己设置为头节点,并返回。 -如果自己不是第二个结点或者CAS获取失败,那么判断是否应该阻塞,如果应该,那么阻塞,否则自旋重新尝试获取同步状态。 +```` final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { @@ -2776,12 +2812,15 @@ private void setHead(Node node) { node.thread = null; node.prev = null; } +``` + +###### shouldParkAfterFailedAcquire -shouldParkAfterFailedAcquire -1)如果前一个节点状态是SIGNAL,那么表示已经设置了前驱节点在获取到同步状态时会唤醒自己,就可以放心的去阻塞了。 -2)否则会检查前一个节点状态是否是Cancelled -2.1)如果是,那么就删除前一个节点,直至状态不是Cancelled。 -2.2)如果不是,那么将其状态设置为SIGNAL。 +- 1)如果前一个节点状态是SIGNAL,那么表示已经设置了前驱节点在获取到同步状态时会唤醒自己,就可以放心的去阻塞了。 +- 2)否则会检查前一个节点状态是否是Cancelled +- 2.1)如果是,那么就删除前一个节点,直至状态不是Cancelled。 +- 2.2)如果不是,那么将其状态设置为SIGNAL。 +``` /** * Checks and updates status for a node that failed to acquire. * Returns true if thread should block. This is the main signal @@ -2818,20 +2857,28 @@ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { } return false; } -parkAndCheckInterrupt +``` +###### parkAndCheckInterrupt + +``` private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } +``` + 为什么只有前驱节点是头节点才能尝试获取同步状态? -1)头节点是成功获取到同步状态的节点,头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头结点 -2)维护同步队列的FIFO原则。 +- 1)头节点是成功获取到同步状态的节点,头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头结点 +- 2)维护同步队列的FIFO原则。 +![图片31.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebzay7lj30pm0gq40o.jpg) +##### AQS#release -AQS#release 在释放同步状态之后,会唤醒其后继节点,使后继节点继续尝试获取同步状态。 + +``` public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; @@ -2841,8 +2888,10 @@ public final boolean release(int arg) { } return false; } +``` -unparkSuccessor +###### unparkSuccessor +``` private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try @@ -2869,31 +2918,42 @@ private void unparkSuccessor(Node node) { if (s != null) LockSupport.unpark(s.thread); } +``` + +#### 共享式同步状态 -共享式同步状态 共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。 +![图片32.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebz74cbj30kq0d3gm8.jpg) -左半部分:共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞 -右半部分:独占式访问资源时,同一时刻其他访问均被阻塞。 +- 左半部分:共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞 +- 右半部分:独占式访问资源时,同一时刻其他访问均被阻塞。 -AQS#acquireShared +##### AQS#acquireShared + AQS会调用tryAcquireShared方法尝试获取同步状态,该方法返回值为int,当返回值大于等于0时,表示能够获取到同步状态。 + 如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。 -1)调用自定义同步器实现的tryAcquireShared -2)如果成功,那么结束 -3)如果失败,那么调用addWaiter加入SHARED节点至同步队列尾部,并调用再次尝试获取同步状态(前提是前驱节点为head) -3.1)如果获取到了,那么将自己设置为头节点,并向后唤醒共享节点(如果还有剩余acquire),返回 -3.2) 如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL - 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 - 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 +- 1)调用自定义同步器实现的tryAcquireShared +- 2)如果成功,那么结束 +- 3)如果失败,那么调用addWaiter加入SHARED节点至同步队列尾部,并调用再次尝试获取同步状态(前提是前驱节点为head) +- 3.1)如果获取到了,那么将自己设置为头节点,并向后唤醒共享节点(如果还有剩余acquire),返回 +- 3.2) 如果前驱节点不是head或者没有获取到,那么判断前驱节点状态是否为SIGNAL + - 3.2.1) 如果是,那么阻塞当前线程,阻塞解除后仍自旋获取同步状态 + - 3.2.2) 如果不是,那么删除状态为CANCELLED的前驱节点,将前驱节点状态设置为SIGNAL,继续自旋尝试获取同步状态。 + +``` public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } -doAcquiredShared +``` + +##### doAcquiredShared + 构造一个当前线程对应的共享节点,如果前驱节点是head并且尝试获取同步状态成功,那么将当前节点设置为head +``` private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; @@ -2921,8 +2981,12 @@ private void doAcquireShared(int arg) { cancelAcquire(node); } } -setHeadAndPropagate +``` + +##### setHeadAndPropagate + 如果获取了同步状态,仍有剩余的acquire,那么继续向后唤醒 +``` /** * Sets head of queue, and checks if successor may be waiting * in shared mode, if so propagating if either propagate > 0 or @@ -2959,8 +3023,10 @@ private void setHeadAndPropagate(Node node, long propagate) { doReleaseShared(); } } +``` -AQS#releaseShared +#### AQS#releaseShared +``` public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); @@ -2968,7 +3034,10 @@ public final boolean releaseShared(int arg) { } return false; } -doReleaseShared +``` + +##### doReleaseShared +``` private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other @@ -3002,12 +3071,14 @@ private void doReleaseShared() { break; } } +```` +#### 独占式超时获取同步状态 -独占式超时获取同步状态 -AQS#tryAcquireNanos +##### AQS#tryAcquireNanos +``` public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) @@ -3015,14 +3086,17 @@ public final boolean tryAcquireNanos(int arg, long nanosTimeout) return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } -该方法可以超时获取同步状态,即在指定上的时间段内获取同步状态,如果成功返回true,失败则返回false。 -对比另一个获取同步状态的方法acquireInterruptibly,该方法等待时如果被中断,那么会立即返回并抛出InterruptedException;而synchronized即使被中断也仅仅是设置中断标志位,并不会立即返回。 -而tryAcquireNanos不仅支持响应中断,还增加了超时获取的特性。 +``` -针对超时获取,主要需要计算出需要等待的时间间隔nanosTImeout,为了防止过早通知,nanosTimeout的计算公式为:nanosTimeout -= now – lastTime。now是当前唤醒时间,lastTime为上次唤醒时间。 -如果nanosTimeout大于0,则表示超时时间未到,需要继续等待nanosTimeout纳秒;反之已经超时。 -AQS#doAcquireNanos +- 该方法可以超时获取同步状态,即在指定上的时间段内获取同步状态,如果成功返回true,失败则返回false。 +- 对比另一个获取同步状态的方法acquireInterruptibly,该方法等待时如果被中断,那么会立即返回并抛出InterruptedException;而synchronized即使被中断也仅仅是设置中断标志位,并不会立即返回。 +- 而tryAcquireNanos不仅支持响应中断,还增加了超时获取的特性。 +- 针对超时获取,主要需要计算出需要等待的时间间隔nanosTImeout,为了防止过早通知,nanosTimeout的计算公式为:nanosTimeout -= now – lastTime。now是当前唤醒时间,lastTime为上次唤醒时间。 +- 如果nanosTimeout大于0,则表示超时时间未到,需要继续等待nanosTimeout纳秒;反之已经超时。 +##### AQS#doAcquireNanos +![图片33.jpg](http://ww1.sinaimg.cn/large/007s8HJUly1g7mebz79chj30mw0p7wfx.jpg) +``` private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) @@ -3053,10 +3127,7 @@ private boolean doAcquireNanos(int arg, long nanosTimeout) cancelAcquire(node); } } - - - - +```` ReentrantLock From 7910c9bc043546f4380f6cb0707603f7ae76ac80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 14:49:08 +0800 Subject: [PATCH 48/97] =?UTF-8?q?Create=20Java&=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...76\350\256\241\346\250\241\345\274\217.md" | 6091 +++++++++++++++++ 1 file changed, 6091 insertions(+) create mode 100644 "docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" diff --git "a/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..077fe85b --- /dev/null +++ "b/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,6091 @@ +## Java +- Oracle JDK有部分源码是闭源的,如果确实需要可以查看OpenJDK的源码,可以在该网站获取。 +- http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/ +- http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/73d5bcd0585d/src +- 上面这个还可以查看native方法。 +### JDK&JRE&JVM +- JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具(编译、开发工具)和Java核心类库。 +- Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。 +- JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。 + +- JDK包含JRE和Java编译、开发工具; +- JRE包含JVM和Java核心类库; +- 运行Java仅需要JRE;而开发Java需要JDK。 +### 跨平台 +- 字节码是在虚拟机上运行的,而不是编译器。换而言之,是因为JVM能跨平台安装,所以相应JAVA字节码便可以跟着在任何平台上运行。只要JVM自身的代码能在相应平台上运行,即JVM可行,则JAVA的程序员就可以不用考虑所写的程序要在哪里运行,反正都是在虚拟机上运行,然后变成相应平台的机器语言,而这个转变并不是程序员应该关心的。 +### 基础数据类型 +- 第一类:整型 byte short int long +- 第二类:浮点型 float double +- 第三类:逻辑型 boolean(它只有两个值可取true false) +- 第四类:字符型 char + + - byte(1)的取值范围为-128~127(-2的7次方到2的7次方-1) + - short(2)的取值范围为-32768~32767(-2的15次方到2的15次方-1) + - int(4)的取值范围为(-2147483648~2147483647)(-2的31次方到2的31次方-1) + - long(8)的取值范围为(-9223372036854774808~9223372036854774807)(-2的63次方到2的63次方-1) + - float(4) + - double(8) + - char(2) + - boolean(1/8) + +- 内码是程序内部使用的字符编码,特别是某种语言实现其char或String类型在内存里用的内部编码;外码是程序与外部交互时外部使用的字符编码。“外部”相对“内部”而言;不是char或String在内存里用的内部编码的地方都可以认为是“外部”。例如,外部可以是序列化之后的char或String,或者外部的文件、命令行参数之类的。 +- Java语言规范规定,Java的char类型是UTF-16的code unit,也就是一定是16位(2字节),然后字符串是UTF-16 code unit的序列。 +- Java规定了字符的内码要用UTF-16编码。或者至少要让用户无法感知到String内部采用了非UTF-16的编码。 + +- String.getBytes()是一个用于将String的内码转换为指定的外码的方法。无参数版使用平台的默认编码作为外码,有参数版使用参数指定的编码作为外码;将String的内容用外码编码好,结果放在一个新byte[]返回。调用了String.getBytes()之后得到的byte[]只能表明该外码的性质,而无法碰触到String内码的任何特质。 + + - Java标准库实现的对char与String的序列化规定使用UTF-8作为外码。Java的Class文件中的字符串常量与符号名字也都规定用UTF-8编码。这大概是当时设计者为了平衡运行时的时间效率(采用定长编码的UTF-16)与外部存储的空间效率(采用变长的UTF-8编码)而做的取舍。 + +### 引用类型 +- 类、接口、数组都是引用类型 +#### 四种引用 +- 目的:避免对象长期占用内存, + +##### 强引用 +- StringReference GC时不回收 +- 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 +##### 软引用 +- SoftReference GC时如果JVM内存不足时会回收 +- 软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 +##### 弱引用 +- WeakReference GC时立即回收 +- 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 +- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 +##### 虚引用 +- PhantomReference +- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +- 在Java集合中有一种特殊的Map类型:WeakHashMap, 在这种Map中存放了键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。 +#### 基础数据类型包装类 +##### 为什么需要 +- 由于基本数据类型不是对象,所以java并不是纯面向对象的语言,好处是效率较高(全部包装为对象效率较低)。 +- Java是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。 +- + +##### 有哪些 + 基本类型     包装器类型   +boolean Boolean +char Character +int Integer +byte Byte +short Short +long Long +float Float +double Double +- Number是所有数字包装类的父类 +##### 自动装箱、自动拆箱(编译器行为) +- 自动装箱:可以将基础数据类型包装成对应的包装类 +- Integer i = 10000; // 编译器会改为new Integer(10000) +- 自动拆箱:可以将包装类转为对应的基础数据类型 +- int i = new Integer(1000);//编译器会修改为 int i = new Integer(1000).intValue(); + +- 自动拆箱时如果包装类是null,那么会抛出NPE +##### Integer.valueOf + +``` +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + + +- 调用Integer.valueOf时-128~127的对象被缓存起来。 +- 所以在此访问内的Integer对象使用==和equals结果是一样的。 +- 如果Integer的值一致,且在此范围内,因为是同一个对象,所以==返回true;但此访问之外的对象==比较的是内存地址,值相同,也是返回false。 + +### Object + +#### == 与 equals的区别 +- 如果两个引用类型变量使用==运算符,那么比较的是地址,它们分别指向的是否是同一地址的对象。结果一定是false,因为两个对象不可能存放在同一地址处。 +- 要求是两个对象都不是能空值,与空值比较返回false。 +- ==不能实现比较对象的值是否相同。 +- 所有对象都有equals方法,默认是Object类的equals,其结果与==一样。 +- 如果希望比较对象的值相同,必须重写equals方法。 +#### hashCode与equals的区别 +- Object中的equals: + +``` +public boolean equals(Object obj) { + return (this == obj); +} +``` + +- equals 方法要求满足: +- 自反性 a.equals(a) +- 对称性 x.equals(y) y.equals(x) +- 一致性 x.equals(y) 多次调用结果一致 +- 对于任意非空引用x,x.equals(null) 应该返回false + +- Object中的hashCode: + +``` +public native int hashCode(); +``` + +- 它是一个本地方法,它的实现与本地机器有关,这里我们暂且认为他返回的是对象存储的物理位置。 +- 当equals方法被重写时,通常有必要重写hashCode方法,以维护hashCode方法的常规约定:值相同的对象必须有相同的hashCode。 + - object1.equals(object2)为true,hashCode也相同; + - hashCode不同时,object1.equals(object2)为false; + - hashCode相同时,object1.equals(object2)不一定为true; + +- 当我们向一个Hash结构的集合中添加某个元素,集合会首先调用hashCode方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用equals方法来匹配这两个元素是否相同,相同则不存,不同则链到后面(如果是链地址法)。 +- 先调用hashCode,唯一则存储,不唯一则再调用equals,结果相同则不再存储,结果不同则散列到其他位置。因为hashCode效率更高(仅为一个int值),比较起来更快。 + +- HashMap#put源码 +- hash是key的hash值,当该hash对应的位置已有元素时会执行以下代码(hashCode相同) +- if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; +- 如果equals返回结果相同,则值一定相同,不再存入。 +#### 如果重写equals不重写hashCode会怎样 +- 两个值不同的对象的hashCode一定不一样,那么执行equals,结果为true,HashSet或HashMap的键会放入值相同的对象。 +### String&StringBuffer&StringBuilder +- 都是final类,不允许继承; +- String长度不可变,StringBuffer、StringBuilder长度可变; + +#### String + +``` +public final class String + implements java.io.Serializable, Comparable, CharSequence {} +``` + +##### equals&hashCode +- String重写了Object的hashCode和equals。 + +``` +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +##### 添加功能 +- String是final类,不可被继承,也不可重写一个java.lang.String(类加载机制)。 +- 一般是使用StringUtils来增强String的功能。 + +- 为什么只加载系统通过的java.lang.String类而不加载用户自定义的java.lang.String类呢? +- 双亲委派机制 +- 因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类, +- 加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理, +- AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader, +- 但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap, +- 父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。 +- 所以,用户自定义的java.lang.String不被加载,也就是不会被使用。 +##### + substring +- 会创建一个新的字符串; +- 编译时会将+转为StringBuilder的append方法。 +- 注意新的字符串是在运行时在堆里创建的。 +- String str1 = “ABC”;可能创建一个或者不创建对象,如果”ABC”这个字符串在java String池里不存在,会在java String池里创建一个创建一个String对象(“ABC”),然后str1指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。 +- +- 注意只有字符串常量是共享的,+和substring等操作的结果不是共享的,substring也会在堆中重新创建字符串。 + + +``` +public String substring(int beginIndex, int endIndex) { + if (beginIndex < 0) { + throw new StringIndexOutOfBoundsException(beginIndex); + } + if (endIndex > value.length) { + throw new StringIndexOutOfBoundsException(endIndex); + } + int subLen = endIndex - beginIndex; + if (subLen < 0) { + throw new StringIndexOutOfBoundsException(subLen); + } + return ((beginIndex == 0) && (endIndex == value.length)) ? this + : new String(value, beginIndex, subLen); +} +``` + + + +``` +public String(char value[], int offset, int count) { + if (offset < 0) { + throw new StringIndexOutOfBoundsException(offset); + } + if (count <= 0) { + if (count < 0) { + throw new StringIndexOutOfBoundsException(count); + } + if (offset <= value.length) { + this.value = "".value; + return; + } + } + // Note: offset or count might be near -1>>>1. + if (offset > value.length - count) { + throw new StringIndexOutOfBoundsException(offset + count); + } + this.value = Arrays.copyOfRange(value, offset, offset+count); +} +``` + + +##### 常量池 +- String str = new String(“ABC”); +- 至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。同时如果这个字符串在字符串常量池里不存在,会在池里创建这个String对象“ABC”。 +- String s1= “a”; +- String s2 = “a”; +- 此时s1 == s2 返回true + +- String s1= new String(“a”); +- String s2 = new String(“a”); +- 此时s1 == s2 返回false + +- ""创建的字符串在字符串池中。 +- 如果引号中字符串存在在常量池中,则仅在堆中拷贝一份(new String); +- 如果不在,那么会先在常量池中创建一份("abc"),然后在堆中创建一份(new String),共创建两个对象。 +- + +##### 编译优化 +- 字面量,final 都会在编译期被优化,并且会被直接运算好。 +- + + - 1)注意c和d中,final变量b已经被替换为其字符串常量了。 + - 2)注意f、g中,b被替换为其字符串常量,并且在编译时字符串常量的+运算会被执行,返回拼接后的字符串常量 + - 3)注意j,a1作为final变量,在编译时被替换为其字符串常量 + +- 解释 c == h / d == h/ e== h为false:c是运行时使用+拼接,创建了一个新的堆中的字符串ab,与ab字符串常量不是同一个对象; +- 解释f == h/ g == h为true:f编译时进行优化,其值即为字符串常量ab,h也是,指向字符串常量池中的同一个对象; + + +- String#intern(JDK1.7之后) +- JDK1.7之后JVM里字符串常量池放入了堆中,之前是放在方法区。 + +- intern()方法设计的初衷,就是重用String对象,以节省内存消耗。 +- 一定是new得到的字符串才会调用intern,字符串常量没有必要去intern。 +- 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,常量池中直接存储堆中该字符串的引用(1.7之前是常量池中再保存一份该字符串)。 + +- 源码 + +``` +public native String intern(); +``` + + +- 实例一: + + - String s = new String("1"); +s.intern(); +String s2 = "1"; +System.out.println(s == s2);// false + +String s3 = new String("1") + new String("1"); +s3.intern(); +String s4 = "11"; +System.out.println(s3 == s4);// true + +- String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。 +- s.intern(),这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。 +- String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象。 +- 结果就是 s 和 s2 的引用地址明显不同。因此返回了false。 + +- String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。 +- s3.intern(),这一行代码,是将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一个 "11" 的对象。 +- 但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。 +- String s4 = "11", 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。 + +- 实例二: + + - String s3 = new String("1") + new String("1"); +String s4 = "11"; +s3.intern(); +System.out.println(s3 == s4);// false + +- String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。 +- String s4 = "11", 这一行代码会直接去生成常量池中的"11"。 +- s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。 +- 结果就是 s3 和 s4 的引用地址明显不同。因此返回了false。 + +- 实例三: + + - String str1 = new String("SEU") + new String("Calvin"); +System.out.println(str1.intern() == str1);// true +System.out.println(str1 == "SEUCalvin");// true + +- str1.intern() == str1就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,因此指向了str1。 "SEUCalvin"在常量池中创建时,也就直接指向了str1了。两个都返回true就理所当然啦。 + + +- 实例四: + + - String str2 = "SEUCalvin";//新加的一行代码,其余不变 +String str1 = new String("SEU") + new String("Calvin"); +System.out.println(str1.intern() == str1);// false +System.out.println(str1 == "SEUCalvin");// false + +- 在实例三的基础上加了第一行 +- str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,你可以去验证它们两个是返回的true。后面的"SEUCalvin"也一样指向str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。 +#### StringBuffer&StringBuilder +- StringBuffer是线程安全的,StringBuilder不是线程安全的,但它们两个中的所有方法都是相同的。StringBuffer在StringBuilder的方法之上添加了synchronized,保证线程安全。 +- StringBuilder比StringBuffer性能更好。 + +- + +### 面向对象 +#### 抽象类与接口 +- 区别: + - 1)抽象类中方法可以不是抽象的;接口中的方法必须是抽象方法; + - 2)抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。 + - 3)抽象类只能单继承,接口可以继承多个父接口; + - 4)Java8 中接口中会有 default 方法,即方法可以被实现。 + +- 使用场景: +- 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。 +- 如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。 +#### 三大特性 +- 面向对象的三个特性:封装;继承;多态 +- 封装:将数据与操作数据的方法绑定起来,隐藏实现细节,对外提供接口。 +- 继承:代码重用;可扩展性 +- 多态:允许不同子类对象对同一消息做出不同响应 + +- 多态的三个必要条件:继承、方法的重写、父类引用指向子类对象 +#### 重写和重载 +- 根据对象对方法进行选择,称为分派 +- 编译期的静态多分派:overloading重载 根据调用引用类型和方法参数决定调用哪个方法(编译器) +- 运行期的动态单分派:overriding 重写 根据指向对象的类型决定调用哪个方法(JVM) + +- + +### 关键类 +#### ThreadLocal(线程局部变量) +- 在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。 +- 例如有一个静态变量 + +``` +public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”); +``` + +- 如果两个线程同时调用sdf.format(…) +- 那么可能会很混乱,因为sdf使用的内部数据结构可能会被并发的访问所破坏。当然可以使用线程同步,但是开销很大;或者也可以在需要时构造一个局部SImpleDateFormat对象。但这很浪费。 +- 希望为每一个线程构造一个对象,即使该线程调用多次方法,也只需要构造一次,不必在局部每次都构造。 + +``` +public static final ThreadLocal sdf = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd"); + } +}; +``` + + +- 实现原理: +##### 1)每个线程的变量副本是存储在哪里的 +- ThreadLocal的get方法就是从当前线程的ThreadLocalMap中取出当前线程对应的变量的副本。该Map的key是ThreadLocal对象,value是当前线程对应的变量。 + +``` +public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + + +- ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} +- 【注意,变量是保存在线程中的,而不是保存在ThreadLocal变量中】。当前线程中,有一个变量引用名字是threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。 +- void createMap(Thread t, T firstValue) { + t.threadLocals = new ThreadLocalMap(this, firstValue); +} +- 每个线程都有一个这样的名为threadLocals 的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为key和value。 +- Thread +- ThreadLocal.ThreadLocalMap threadLocals = null; +- 这样,我们所使用的ThreadLocal变量的实际数据,通过get方法取值的时候,就是通过取出Thread中threadLocals引用的map,然后从这个map中根据当前threadLocal作为参数,取出数据。现在,变量的副本从哪里取出来的(本文章提出的第一个问题)已经确认解决了。 + +- 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程; +- Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系; +- Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。 +##### 2)为什么ThreadLocalMap的Key是弱引用 +- 如果是强引用,ThreadLocal将无法被释放内存。 +- 因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。 +##### 3)ThreadLocalMap是何时初始化的(setInitialValue) +- 在get时最后一行调用了setInitialValue,它又调用了我们自己重写的initialValue方法获得要线程局部变量对象。ThreadLocalMap没有被初始化的话,便初始化,并设置firstKey和firstValue;如果已经被初始化,那么将key和value放入map。 + +``` +private T setInitialValue() { + T value = initialValue(); + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + return value; +} +``` + +##### 4)ThreadLocalMap 原理 + +``` +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + + +- 它也是一个类似HashMap的数据结构,但是并没实现Map接口。 +- 也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。 +- ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。 +###### 构造方法 +- ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + - // 表的大小始终为2的幂次 + table = new Entry[INITIAL_CAPACITY]; + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + table[i] = new Entry(firstKey, firstValue); + size = 1; +- // 设定扩容阈值 + setThreshold(INITIAL_CAPACITY); +} + - 在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。 + - 对于& (INITIAL_CAPACITY - 1),相对于2的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。 + +``` +private void setThreshold(int len) { + threshold = len * 2 / 3; +} +``` + + +- getEntry(由ThreadLocal#get调用) + +``` +private Entry getEntry(ThreadLocal key) { + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + if (e != null && e.get() == key) + return e; + else +``` + +- // 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。 + return getEntryAfterMiss(key, i, e); +} + + + +``` +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + while (e != null) { + ThreadLocal k = e.get(); + if (k == key) + return e; + if (k == null) +``` + +- // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry +- expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; +} + +- i是位置 +- 从staleSlot开始遍历,将无效key(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。 +- 另外,在过程中还会对非空的entry作rehash。 +- 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等) + +``` +private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + // expunge entry at staleSlot + tab[staleSlot].value = null; + tab[staleSlot] = null; + size--; + + // Rehash until we encounter null + Entry e; + int i; + for (i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + tab[i] = null; + size--; + } else { +``` + +- // 对于还没有被回收的情况,需要做一次rehash。 +- 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i, + - 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。 + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + tab[i] = null; + + // Unlike Knuth 6.4 Algorithm R, we must scan until + // null because multiple entries could have been stale. + while (tab[h] != null) + h = nextIndex(h, len); + tab[h] = e; + } + } + } + return i; +} + +###### set(线性探测法解决hash冲突) + +``` +private void set(ThreadLocal key, Object value) { + + // We don't use a fast path as with get() because it is at + // least as common to use set() to create new entries as + // it is to replace existing ones, in which case, a fast + // path would fail more often than not. + + Entry[] tab = table; + int len = tab.length; + // 计算key的hash值 +``` + + - int i = key.threadLocalHashCode & (len-1); + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + + if (k == key) { +- // 同一个ThreadLocal赋了新值,则替换原值为新值 + e.value = value; + return; + } + + if (k == null) { +- // 该位置的TheadLocal已经被回收,那么会清理slot并在此位置放入当前key和value(stale:陈旧的) + replaceStaleEntry(key, value, i); + return; + } + } + // 下一个位置为空,那么就放到该位置上 + tab[i] = new Entry(key, value); + int sz = ++size; +- // 启发式地清理一些slot,并判断是否是否需要扩容 + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +} + +- 每个ThreadLocal对象都有一个hash值 threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小 0x61c88647。 + +``` +private final int threadLocalHashCode = nextHashCode(); +``` + + +``` +private static final int HASH_INCREMENT = 0x61c88647; +``` + + +``` +private static int nextHashCode() { + return nextHashCode.getAndAdd(HASH_INCREMENT); +} +``` + +- 由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。 + +``` +private static int nextIndex(int i, int len) { + return ((i + 1 < len) ? i + 1 : 0); +} +``` + + +- 在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下: +- 1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上; +- 2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value; +- 3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置; +- 这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置 + +- 可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。 +###### cleanSomeSlots(启发式地清理slot) +- i是当前位置,n是元素个数 +- i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空) +- n是用于控制控制扫描次数的 +- 正常情况下如果log n次扫描没有发现无效slot,函数就结束了 + +- 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理 +- 再从下一个空的slot开始继续扫描 +- 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用, +- 区别是前者传入的n为元素个数,后者为table的容量 + +``` +private boolean cleanSomeSlots(int i, int n) { + boolean removed = false; + Entry[] tab = table; + int len = tab.length; + do { + i = nextIndex(i, len); + Entry e = tab[i]; + if (e != null && e.get() == null) { + n = len; + removed = true; + i = expungeStaleEntry(i); + } + } while ( (n >>>= 1) != 0); + return removed; +} +``` + + +###### rehash +- 先全量清理,如果清理后现有元素个数超过负载,那么扩容 + +``` +private void rehash() { +``` + + - // 进行一次全量清理 + expungeStaleEntries(); + + // Use lower threshold for doubling to avoid hysteresis + if (size >= threshold - threshold / 4) + resize(); +} + +- 全量清理 + +``` +private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) + expungeStaleEntry(j); + } +} +``` + + +- 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍 + +``` +private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + int count = 0; + + for (int j = 0; j < oldLen; ++j) { + Entry e = oldTab[j]; + if (e != null) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; // Help the GC + } else { + int h = k.threadLocalHashCode & (newLen - 1); + while (newTab[h] != null) + h = nextIndex(h, newLen); + newTab[h] = e; + count++; + } + } + } + + setThreshold(newLen); + size = count; + table = newTab; +} +``` + + +###### remove + +``` +private void remove(ThreadLocal key) { + Entry[] tab = table; + int len = tab.length; + int i = key.threadLocalHashCode & (len-1); + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + if (e.get() == key) { +``` + +- // 显式断开弱引用 + e.clear(); +- // 进行段清理 + expungeStaleEntry(i); + return; + } + } +} + +- Reference#clear + +``` +public void clear() { + this.referent = null; +} +``` + + +###### 内存泄露 +- 只有调用TheadLocal的remove或者get、set时才会采取措施去清理被回收的ThreadLocal对应的value(但也未必会清理所有的需要被回收的value)。假如一个局部的ThreadLocal不再需要,如果没有去调用remove方法清除,那么有可能会发生内存泄露。 + +- 既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。 + +- 如果使用ThreadLocal的set方法之后,没有显式的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。 + + +``` +JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。 +``` + +- + +#### Iterator / ListIterator / Iterable +- 普通for循环时不能删除元素,否则会抛出异常;Iterator可以 + + +``` +public interface Collection extends Iterable {} +``` + +- Collection接口继承了Iterable,Iterable接口定义了iterator抽象方法和forEach default方法。所以ArrayList、LinkedList都可以使用迭代器和forEach,包括增强for循环(编译时转为迭代器)。 + + +``` +public interface Iterable { + Iterator iterator(); + default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } + } +``` + +- default Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } +- } + +- + +- 注意这些具体的容器类返回的迭代器对象是各不相同的,主要是因为不同的容器遍历方式不同,但是这些迭代器对象都实现Iterator接口,都可以使用一个Iterator对象来统一指向这些不同的子类对象。 +- ArrayList#iterator + +``` +public Iterator iterator() { + return new Itr(); +} +``` + + +- ArrayList#Itr + +``` +private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + if (i >= size) + throw new NoSuchElementException(); + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i + 1; + return (E) elementData[lastRet = i]; + } + + public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + + @Override + @SuppressWarnings("unchecked") + public void forEachRemaining(Consumer consumer) { + Objects.requireNonNull(consumer); + final int size = ArrayList.this.size; + int i = cursor; + if (i >= size) { + return; + } + final Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) { + throw new ConcurrentModificationException(); + } + while (i != size && modCount == expectedModCount) { + consumer.accept((E) elementData[i++]); + } + // update once at end of iteration to reduce heap write traffic + cursor = i; + lastRet = i - 1; + checkForComodification(); + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } +} +``` + + +- ArrayList#listIterator + +``` +public ListIterator listIterator() { + return new ListItr(0); +} +``` + +- ArrayList#ListItr + +``` +private class ListItr extends Itr implements ListIterator { + ListItr(int index) { + super(); + cursor = index; + } + + public boolean hasPrevious() { + return cursor != 0; + } + + public int nextIndex() { + return cursor; + } + + public int previousIndex() { + return cursor - 1; + } + + @SuppressWarnings("unchecked") + public E previous() { + checkForComodification(); + int i = cursor - 1; + if (i < 0) + throw new NoSuchElementException(); + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i; + return (E) elementData[lastRet = i]; + } + + public void set(E e) { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.set(lastRet, e); + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + + public void add(E e) { + checkForComodification(); + + try { + int i = cursor; + ArrayList.this.add(i, e); + cursor = i + 1; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } +} +``` + +#### for /增强for/ forEach +For-each loop Equivalent for loop +for (type var : arr) { + body-of-loop +} for (int i = 0; i < arr.length; i++) { + type var = arr[i]; + body-of-loop +} +for (type var : coll) { + body-of-loop +} for (Iterator iter = coll.iterator(); iter.hasNext(); ) { + type var = iter.next(); + body-of-loop +} +- 增强for循环在编译时被修改为for循环:数组会被修改为下标式的循环;集合会被修改为Iterator循环。 + +- 增强for循环不适合以下情况:(过滤、转换、平行迭代) +- 对collection或数组中的元素不能做赋值操作; +- 只能正向遍历,不能反向遍历; +- 遍历过程中,collection或数组中同时只有一个元素可见,即只有“当前遍历到的元素”可见,而前一个或后一个元素是不可见的; + +- forEach +- ArrayList#forEach继承自 +- Iterable接口的default方法 +- default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } + } + +- + +#### Comparable与Comparator + +- 基本数据类型包装类和String类均已实现了Comparable接口。 +- 实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序,默认为升序。 + + +- 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如TreeSet,TreeMap)的顺序。 +- + +### 继承 + +``` +子类继承父类所有的成员变量(即使是private变量,有所有权,但是没有使用权,不能访问父类的private的成员变量)。 +``` + + + +``` +子类中可以直接调用父类非private方法,也可以用super.父类方法的形式调用。 +``` + +- 子类构造方法中如果没有显式使用super(父类构造方法参数)去构造父类对象的话(如果有必须是方法的第一行),编译器会在第一行添加super()。 + +- 子类的构造函数可否不使用super(父类构造方法参数)调用超类的构造方法? +- 可以不用显式的写出super,但前提是“父类中有多个构造方法,且有一个是显式写出的无参的构造方法”。 + +- + +### 内部类 +- 在另一个类的里面定义的类就是内部类 +- 内部类是编译器现象,与虚拟机无关。 +- 编译器会将内部类编译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。 + + +``` +内部类可以是static的,也可用public,default,protected和private修饰。(而外部类即类名和文件名相同的只能使用public和default)。 +``` + + +#### 优点 +- 每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。 +- 接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。 + +- 用内部类还能够为我们带来如下特性: +- 1、内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。 +- 2、在单个外部类中,可以让多个内部类实现不同的接口,或者继承不同的类。外部类想要多继承的类可以分别由内部类继承,并进行Override或者直接复用。然后外部类通过创建内部类的对象来使用该内部对象的方法和成员,从而达到复用的目的,这样外部内就具有多个父类的所有特征。 +- 3、创建内部类对象的时刻并不依赖于外部类对象的创建。 +- 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。 +- 5、内部类提供了更好的封装,除了该外部类,其他类都不能访问 + +- 只有静态内部类可以同时拥有静态成员和非静态成员,其他内部类只有拥有非静态成员。 + +#### 成员内部类:就像外部类的一个成员变量 + + +- 注意内部类的对象总有一个外部类的引用 +- 当创建内部类对象时,会自动将外部类的this引用传递给当前的内部类的构造方法。 + +#### 静态内部类:就像外部类的一个静态成员变量 + + +``` +public class OuterClass { + + private static class StaticInnerClass { + int id; + static int increment = 1; + } +} +//调用方式: +//外部类.内部类 instanceName = new 外部类.内部类(); +``` + + +#### 局部内部类:定义在一个方法或者一个块作用域里面的类 +- 想创建一个类来辅助我们的解决方案,又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。 + +- JDK1.8之前不能访问非final的局部变量! +- 生命周期不一致: +- 方法在栈中,对象在堆中;方法执行完,对象并没有死亡 +- 如果可以使用方法的局部变量,如果方法执行完毕,就会访问一个不存在的内存区域。 +- 而final是常量,就可以将该常量的值复制一份,即使不存在也不影响。 + +``` +public Destination destination(String str) { + class PDestination implements Destination { + private String label; + + private PDestination(String whereTo) { + label = whereTo; + } + public String readLabel() { + return label; + } + } + return new PDestination(str); +} +``` + + +#### 匿名内部类:必须继承一个父类或实现一个接口 + +- 匿名内部类和局部内部类在JDK1.8 之前都不能访问一个非final的局部变量,只能访问final的局部变量,原因是生命周期不同,可能栈中的局部变量已经被销毁,而堆中的对象仍存活,此时会访问一个不存在的内存区域。假如是final的变量,那么编译时会将其拷贝一份,延长其生命周期。 +- 拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。 +- 但在JDK1.8之后可以访问一个非final的局部变量了,前提是非final的局部变量没有修改,表现得和final变量一样才可以! + +``` +interface AnonymousInner { + int add(); +} +public class AnonymousOuter { + public AnonymousInner getAnonymousInner(){ + int x = 100; + return new AnonymousInner() { + int y = 100; + @Override + public int add() { + return x + y; + } + }; + } +} +``` + + +### 关键字 +#### final + +#### try-finally-return +- 1、不管有没有出现异常,finally块中代码都会执行; +- 2、当try和catch中有return时,finally仍然会执行;无论try里执行了return语句、break语句、还是continue语句,finally语句块还会继续执行;如果执行try和catch时JVM退出(比如System.exit(0)),那么finally不会被执行; +- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的; +- 【 +- 如果try语句里有return,那么代码的行为如下: +1.如果有返回值,就把返回值保存到局部变量中 +2.执行jsr指令跳到finally语句里执行 +3.执行完finally语句后,返回之前保存在局部变量表里的值 +- 】 +- 3、当try和finally里都有return时,会忽略try的return,而使用finally的return。 +- 4、如果try块中抛出异常,执行finally块时又抛出异常,此时原始异常信息会丢失,只抛出在finally代码块中的异常。 + +- 实例一: + +``` +public static int test() { + int x = 1; + try { + x++; + return x; // 2 + } finally { + x++; + } +} +``` + + +- 实例二: + +``` +private static int test2() { + try { + System.out.println("try..."); + return 80; + } finally { + System.out.println("finally..."); + return 100; // 100 + } +} +``` + + +#### static +- static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。 +##### 1)修饰成员方法:静态成员方法 +- 在静态方法中不能访问类的非静态成员变量和非静态成员方法; +- 在非静态成员方法中是可以访问静态成员方法/变量的; +- 即使没有显式地声明为static,类的构造器实际上也是静态方法 + +##### 2)修饰成员变量:静态成员变量 +- 静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 + +- 静态成员变量并发下不是线程安全的,并且对象是单例的情况下,非静态成员变量也不是线程安全的。 + +- 怎么保证变量的线程安全? +- 只有一个线程写,其他线程都是读的时候,加volatile;线程既读又写,可以考虑Atomic原子类和线程安全的集合类;或者考虑ThreadLocal +##### 3)修饰代码块:静态代码块 +- 用来构造静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 + +##### 4)修饰内部类:静态内部类 +- 成员内部类和静态内部类的区别: + - 1)前者只能拥有非静态成员;后者既可拥有静态成员,又可拥有非静态成员 + - 2)前者持有外部类的的引用,可以访问外部类的静态成员和非静态成员;后者不持有外部类的引用,只能访问外部类的静态成员 + - 3)前者不能脱离外部类而存在;后者可以 +##### 5)修饰import:静态导包 +- + +#### switch +##### switch字符串实现原理 +- 对比反编译之后的结果: + +- 编译后switch还是基于整数,该整数来自于String的hashCode。 +- 先比较字符串的hashCode,因为hashCode相同未必值相同,又再次检查了equals是否相同。 + +- + +##### 字节码实现原理(tableswitch / lookupswitch) +- 编译器会使用tableswitch和lookupswitch指令来生成switch语句的编译代码。当switch语句中的case分支的条件值比较稀疏时,tableswitch指令的空间使用率偏低。这种情况下将使用lookupswitch指令来替代。lookupswitch指令的索引表由int类型的键(来源于case语句块后面的数值)与对应的目标语句偏移量所构成。当lookupswitch指令执行时,switch语句的条件值将和索引表中的键进行比较,如果某个键和条件值相符,那么将转移到这个键对应的分支偏移量继续执行,如果没有键值符合,执行将在default分支执行。 + +#### abstract +- 只要含有抽象方法,这个类必须添加abstract关键字,定义为抽象类。 +- 只要父类是抽象类,内含抽象方法,那么继承这个类的子类的相对应的方法必须重写。如果不重写,就需要把父类的声明抽象方法再写一遍,留给这个子类的子类去实现。同时将这个子类也定义为抽象类。 +- 注意抽象类中可以有抽象方法,也可以有具体实现方法(当然也可以没有)。 +- 抽象方法须加abstract关键字,而具体方法不可加 +- 只要是抽象类,就不能存在这个类的对象(不可以new一个这个类的对象)。 +#### this & super +- this +- 自身引用;访问成员变量与方法;调用其他构造方法 +- 1. 通过this调用另一个构造方法,用法是this(参数列表),这个仅在类的构造方法中可以使用 +- 2. 函数参数或者函数中的局部变量和成员变量同名的情况下,成员变量被屏蔽,此时要访问成员变量则需要用“this.成员变量名”的方式来引用成员变量。 +- 3. 需要引用当前对象时候,直接用this(自身引用) + +- super +- 父类引用;访问父类成员变量与方法;调用父类构造方法 +- super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 +- super有三种用法: +- 1.普通的直接引用 +- 与this类似,super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员,如果不冲突的话也可以不加super。 +- 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,为了区别,调用父类的成员必须要加super +- 3.调用父类的构造函数 +#### 访问权限 + +### 枚举 +#### JDK实现 +- 实例: + +``` +public enum Labels0 { + + ENVIRONMENT("环保"), TRAFFIC("交通"), PHONE("手机"); + + private String name; + + private Labels0(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} +``` + + +- 编译后生成的字节码反编译: + +- 可以清晰地看到枚举被编译后其实就是一个类,该类被声明成 final,说明其不能被继承,同时它继承了 Enum 类。枚举里面的元素被声明成 static final ,另外生成一个静态代码块 static{},最后还会生成 values 和 valueOf 两个方法。下面以最简单的 Labels 为例,一个一个模块来看。 + +##### Enum 类 +- Enum 类是一个抽象类,主要有 name 和 ordinal 两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引,而构造函数传入的两个变量刚好与之对应。 + +- toString 方法直接返回 name。 +- equals 方法直接用 == 比较两个对象。 +- hashCode 方法调用的是父类的 hashCode 方法。 +- 枚举不支持 clone、finalize 和 readObject 方法。 +- compareTo 方法可以看到就是比较 ordinal 的大小。 +- valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素。 + +##### 静态代码块的实现 +- 在静态代码块中创建对象,对象是单例的! +- 可以看到静态代码块主要完成的工作就是先分别创建 Labels 对象,然后将“ENVIRONMENT”、“TRAFFIC”和“PHONE”字符串作为 name ,按照顺序分别分配位置索引0、1、2作为 ordinal,然后将其值设置给创建的三个 Labels 对象的 name 和 ordinal 属性,此外还会创建一个大小为3的 Labels 数组 ENUM$VALUES,将前面创建出来的 Labels 对象分别赋值给数组。 +##### values的实现 +- 可以看到它是一个静态方法,主要是使用了前面静态代码块中的 Labels 数组 ENUM$VALUES,调用 System.arraycopy 对其进行复制,然后返回该数组。所以通过 Labels.values()[2]就能获取到数组中索引为2的元素。 +##### valueOf 方法 +- 该方法同样是个静态方法,可以看到该方法的实现是间接调用了父类 Enum 类的 valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素,比如可以通过 Labels.valueOf("ENVIRONMENT")获取 Labels.ENVIRONMENT。 + +- 枚举本质其实也是一个类,而且都会继承java.lang.Enum类,同时还会生成一个静态代码块 static{},并且还会生成 values 和 valueOf 两个方法。而上述的工作都需要由编译器来完成,然后我们就可以像使用我们熟悉的类那样去使用枚举了。 +- + +#### 用enum代替int常量 +- 将int枚举常量翻译成可打印的字符串,没有很便利的方法。 +- 要遍历一个枚举组中的所有int 枚举常量,甚至获得int枚举组的大小。 + +- 使用枚举类型的values方法可以获得该枚举类型的数组 +- 枚举类型没有可以访问的构造器,是真正的final;是实例受控的,它们是单例的泛型化;本质上是单元素的枚举;提供了编译时的类型安全。 +- 单元素的枚举是实现单例的最佳方法! + +- 可以在枚举类型中放入这段代码,可以实现String2Enum。 +- 注意Operation是枚举类型名。 + +#### 用实例域代替序数 + +- 这种实现不好,不推荐使用ordinal方法,推荐使用下面这种实现: + + +#### 用EnumSet代替位域 +- 位域是将几个常量合并到一个集合中,我们推荐用枚举代替常量,用EnumSet代替集合 + - EnumSet.of(enum1,enum2) -> Set<枚举> +#### 用EnumMap代替序数索引 + + + +- 将一个枚举类型的值与一个元素(或一组)对应起来,推荐使用EnumMap数据结构 +- 如果是两个维度的变化,那么可以使用EnumMap> + + + + +- + +### 序列化 +#### JDK序列化(Serizalizable) +- 定义:将实现了Serializable接口(标记型接口)的对象转换成一个字节数组,并可以将该字节数组转为原来的对象。 + +- ObjectOutputStream 是专门用来输出对象的输出流; +- ObjectOutputStream 将 Java 对象写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。 + +#### serialVersionUID +- Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)。 + + - 1)如果没有添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),此时会报错。 + - 2)如果添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),那么可能会恢复部分数据,或者恢复不了数据。 + +- 如果设置了serialVersionUID并且一致,那么可能会反序列化部分数据;如果没有设置,那么只要属性不同,那么无法反序列化。 + +#### 其他序列化工具 +- XML/JSON +- Thrift/Protobuf + +#### 对象深拷贝与浅拷贝 +- 当拷贝一个变量时,原始引用和拷贝的引用指向同一个对象,改变一个引用所指向的对象会对另一个引用产生影响。 +- 如果需要创建一个对象的浅拷贝,那么需要调用clone方法。 +- Object 类本身不实现接口 Cloneable,直接调用clone会抛出异常。 + +``` +如果要在自己定义类中调用clone方法,必须实现Cloneable接口(标记型接口),因为Object类中的clone方法为protected,所以需要自己重写clone方法,设置为public。 +``` + +- protected native Object clone() throws CloneNotSupportedException; + + + +``` +public class Person implements Cloneable { + private int age; + private String name; + private Company company; + @Override + public Person clone() throws CloneNotSupportedException { + return (Person) super.clone(); + } +``` + +- } + + +``` +public class Company implements Cloneable{ + private String name; +``` + + +``` + @Override +public Company clone() throws CloneNotSupportedException { + return (Company) super.clone(); +} +``` + +- } +- 使用super(即Object)的clone方法只能进行浅拷贝。 +- 如果希望实现深拷贝,需要修改实现,比如修改为: + +``` +@Override +public Person clone() throws CloneNotSupportedException { + Person person = (Person) super.clone(); + person.setCompany(company.clone()); // 一个新的Company + return person; +} +``` + +- 假如说Company中还有持有其他对象的引用,那么Company中也要像Person这样做。 +- 可以说:想要深拷贝一个子类,那么它的所有父类都必须可以实现深拷贝。 + +- 另一种实现对象深拷贝的方式是序列化。 +- @Override +protected Object clone() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(baos); + os.writeObject(this); + os.close(); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream in = new ObjectInputStream(bais); + Object ret = in.readObject(); + in.close(); + return ret; + }catch(Exception e) { + e.printStackTrace(); + } + return null; +} + +- + +### 异常 + + +#### Error、Exception +- Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 +- Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。 +#### 常见RuntimeException +- IllegalArgumentException - 方法的参数无效 +- NullPointerException - 试图访问一空对象的变量、方法或空数组的元素 +- ArrayIndexOutOfBoundsException - 数组越界访问 +- ClassCastException - 类型转换异常 +- NumberFormatException 继承IllegalArgumentException,字符串转换为数字时出现。比如int i= Integer.parseInt("ab3"); +- + +#### RuntimeException与非Runtime Exception +- RuntimeException是运行时异常,也称为未检查异常; +- 非RuntimeException 也称为CheckedException 受检异常 + +- 前者可以不必进行try-catch,后者必须要进行try-catch或者throw。 +#### 异常包装 +- 在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型 +- try{ +- … +- }catch(SQLException e){ +- throw new ServletException(e.getMessage()); +- } +- 这样的话ServletException会取代SQLException。 + +- 有一种更好的方法,可以保存原有异常的信息,将原始异常设置为新的异常的原因 +- try{ +- … +- }catch(SQLException e){ +- Throwable se = new ServletException(e.getMessage()); +- se.initCause(e); +- throw se; +- } +- 当捕获到异常时,可以使用getCause方法来重新得到原始异常 +- Throwable e = se.getCause(); +- 建议使用这种包装技术,可以抛出系统的高级异常(自己new的),又不会丢失原始异常的细节。 + +- 早抛出,晚捕获。 +- + +### 泛型 +- 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 +#### 泛型接口/类/方法 +#### 泛型继承、实现 + +- 父类使用泛型,子类要么去指定具体类型参数,要么继续使用泛型 + +#### 泛型的约束和局限性 + - 1)只能使用包装器类型,不能使用基本数据类型; + + - 2)运行时类型查询只适用于原始类型,不适用于带类型参数的类型; +- if(a instanceof Pair) //error + + - 3)不能创建带有类型参数的泛型类的数组 +- Pair [] pairs = new Pair[10];//error +- 只能使用反射来创建泛型数组 + +``` +public static T[] minmax(T… a){ +``` + +- T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),个数); +- …复制 +- } +#### 通配符 + +- ? 未知类型 只可以用于声明时,声明类型或方法参数,不能用于定义时(指定类型参数时) +- List unknownList; +- List unknownNumberList; +- List unknownBaseLineIntgerList; + +- 对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素, 因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是null。 + +- 通配符类型 List 与原始类型 List 和具体类型 List都不相同,List表示这个list内的每个元素的类型都相同,但是这种类型具体是什么我们却不知道。注意,List和List可不相同,由于Object是最高层的超类,List表示元素可以是任何类型的对象,但是List可不是这个意思(未知类型)。 + +- + +#### extends 指定类型必为自身或其子类 + +- List +- 这个引用变量如果作为参数,哪些引用可以传入? +- 本身及其子类 +- 以及含有通配符及extends的本身及子类 +- 不可传入只含通配符不含extends+指定类 的引用 +- 或者extends的不是指定类及其子类,而是其父类 + +- // Number "extends" Number (in this context) +- List foo3 = new ArrayList(); +- // Integer extends Number +- List foo3 = new ArrayList(); +- // Double extends Number +- List foo3 = new ArrayList(); + +- 如果实现了多个接口,可以使用&来将接口隔开 +- T extends Comparable & Serializable + +- List list = new ArrayList(); + - list.add(new Integer(1)); //error +- list.add(new Float(1.2f)); //error + +#### super 指定类型必为自身或其父类 + + +- 不能同时声明泛型通配符申明上界和下界 +#### PECS(读extends,写super) +- producer-extends, consumer-super. +- produce是指参数是producer,consumer是指参数是consumer。 +- 要往泛型类写数据时,用extends; +- 要从泛型类读数据时,用super; +- 既要取又要写,就不用通配符(即extends与super都不用)比如List。 + +- 如果参数化类型表示一个T生产者,就是;如果它表示一个T消费者,就使用。 +- Stack的pushAll的参数产生E实例供Stack使用,因此参数类型为Iterable。 +- popAll的参数提供Stack消费E实例,因此参数类型为Collection<? super E>。 + + +``` +public void pushAll(Iterable src) { +``` + +- for (E e : src) +- push(e); +- } + +``` +public void popAll(Collection dst) { +``` + +- while (!isEmpty()) +- dst.add(pop()); +- } +- 在调用pushAll方法时生产了E 实例(produces E instances),在调用popAll方法时dst消费了E 实例(consumes E instances)。Naftalin与Wadler将PECS称为Get and Put Principle。 + +- Collections#copy + +``` +public static void copy(List dest, List src) { + int srcSize = src.size(); + if (srcSize > dest.size()) + throw new IndexOutOfBoundsException("Source does not fit in dest"); + + if (srcSize < COPY_THRESHOLD || + (src instanceof RandomAccess && dest instanceof RandomAccess)) { + for (int i=0; i di=dest.listIterator(); + ListIterator si=src.listIterator(); + for (int i=0; i pair = new Pair<>(); +- 那么Pair 中所有的T都替换为String + +- 泛型擦除带来的问题: + - 1)无法使用具有不同类型参数的泛型进行方法重载 + +``` +public void test(List ls) { +``` + +- System.out.println("Sting"); +- } + +``` +public void test(List li) { +``` + +- System.ut.println("Integer"); +- } // 编译出错 + +- 或者 + +``` +public interface Builder { +``` + +- void add(List keyList); +- void add(List valueList); +- } + + - 2)泛型类的静态变量是共享的 + +- 另外,因为Java泛型的擦除并不是对所有使用泛型的地方都会擦除的,部分地方会保留泛型信息,在运行时可以获得类型参数。 + +- + +### IO +#### Unix IO模型 +- 异步I/O 是指用户程序发起IO请求后,不等待数据,同时操作系统内核负责I/O操作把数据从内核拷贝到用户程序的缓冲区后通知应用程序。数据拷贝是由操作系统内核完成,用户程序从一开始就没有等待数据,发起请求后不参与任何IO操作,等内核通知完成。 +- 同步I/O 就是非异步IO的情况,也就是用户程序要参与把数据拷贝到程序缓冲区(例如java的InputStream读字节流过程)。 +- 同步IO里的非阻塞 是指用户程序发起IO操作请求后不等待数据,而是调用会立即返回一个标志信息告知条件不满足,数据未准备好,从而用户请求程序继续执行其它任务。执行完其它任务,用户程序会主动轮询查看IO操作条件是否满足,如果满足,则用户程序亲自参与拷贝数据动作。 +- Unix IO模型的语境下,同步和异步的区别在于数据拷贝阶段是否需要完全由操作系统处理。阻塞和非阻塞操作是针对发起IO请求操作后是否有立刻返回一个标志信息而不让请求线程等待。 +#### BIO NIO AIO介绍 + +- BIO:同步阻塞,每个客户端的连接会对应服务器的一个线程 +- NIO:同步非阻塞,多路复用器轮询客户端的请求,每个客户端的IO请求会对应服务器的一个线程 +- AIO: 异步非阻塞,客户端的IO请求由OS完成后再通知服务器启动线程处理(需要OS支持) +- 1、进程向操作系统请求数据 +- 2、操作系统把外部数据加载到内核的缓冲区中, +- 3、操作系统把内核的缓冲区拷贝到进程的缓冲区 +- 4、进程获得数据完成自己的功能 + +- Java NIO属于同步非阻塞IO,即IO多路复用,单个线程可以支持多个IO +- 即询问时从IO没有完毕时直接阻塞,变成了立即返回一个是否完成IO的信号。 + +- 异步IO就是指AIO,AIO需要操作系统支持。 +- + +#### Java BIO 使用 + +- Server + +``` +public class ChatServer { +``` + + +- ServerSocket ss = null; +- boolean started = false; +- ArrayList clients = new ArrayList(); + + +``` + public static void main(String[] args) { +``` + +- new ChatServer().start(); +- } + + +``` + public void start() { +``` + +- try { + - ss = new ServerSocket(6666); +- started = true; +- } catch (BindException e) { +- System.out.println("端口使用中...."); // 用于处理两次启动Server端 +- System.out.println("请重新运行服务器"); + - System.exit(-1); +- } catch (IOException e) { +- System.out.println("服务器启动失败"); +- e.printStackTrace(); +- } + +- try { +- while (started) { +- Socket s = ss.accept(); +- Client c = new Client(s); +- clients.add(c); +- c.transmitToAll(c.name + "进入了聊天室"); +- new Thread(c).start(); +- } +- } catch (IOException e) { +- e.printStackTrace(); +- } finally { // 主方法结束时应该关闭服务器ServerSocket +- if (ss != null) +- try { +- ss.close(); +- } catch (IOException e) { +- e.printStackTrace(); +- } +- } +- } + +- class Client implements Runnable { // 包装给一个单独的客户端的线程类,应该保留自己的连接Socket和流 +- // 保留连接一般使用构造方法,将连接传入 +- // 一个客户端就new 一个Client 连接 + +``` + private Socket s = null; +``` + + +``` + private DataInputStream dis = null; +``` + + +``` + private DataOutputStream dos = null;// 每个客户端的线程都有各自的输入输出流,输入流用于读来自当前客户端的数据,输出流用于保存当前客户端的流。 +``` + + +``` + private boolean Connected = false;// 每个客户端都有一个开始结束的标志 +``` + + +``` + private String name; +``` + + +- Client(Socket s) { // new 一个Client对象时,要打开Socket和DataInputStream流 +- this.s = s; +- try { +- dis = new DataInputStream(s.getInputStream()); +- dos = new DataOutputStream(s.getOutputStream()); +- Connected = true; +- this.name = dis.readUTF(); + +- } catch (IOException e) { +- e.printStackTrace(); +- } +- } + +- // 如何实现一个客户端与其他客户端的通信? +- // 可以考虑在每连到一个客户端就保存与其的连接Socket,当要发送给其他客户端信息时,遍历一遍所有其他客户端 + +``` + public void run() { +``` + +- try { +- while (Connected) { +- String read = dis.readUTF(); +- if (read.equals("EXIT")) { +- Connected = false; +- transmitToAll(this.name + "已退出"); +- continue; +- } else if (read.startsWith("@")) { + - String[] msg = read.substring(1).split(":"); +- transmitToPerson(msg[0], msg[1]); +- continue; +- } +- transmitToAll(this.name+":"+read); +- } + - } catch (EOFException e1) { +- System.out.println("Client closed"); + - } catch (IOException e1) { +- e1.printStackTrace(); +- } finally { // 关闭资源应该放在finally中 +- try { +- CloseUtil.close(dis, dos); +- if (s != null) +- s.close(); + - } catch (IOException e1) { +- e1.printStackTrace(); +- } +- } +- } + + +``` + /** +``` + +- * 将消息发送给所有人 +- * +- * @param read +- */ + +``` + public void transmitToAll(String read) { +``` + +- for (int i = 0; i < clients.size(); i++) { +- Client c = clients.get(i); +- if (c.Connected == true) +- c.send(read); // 调用每个客户端线程的send方法,一个对象的输出流与对应的客户端连接 dos --> +- // Client +- } +- } + + +``` + /** +``` + +- * 将消息发送给某个人,私聊 +- * +- * @param read +- * @param clientName +- */ + +``` + public void transmitToPerson(String clientName, String read) { +``` + +- boolean isFind = false; +- for (int i = 0; i < clients.size(); i++) { +- Client client = clients.get(i); +- if (client.name.equals(clientName)) { +- client.send(this.name+":"+read); +- isFind = true; +- } +- } +- send(this.name+":"+read + (isFind ? "" : "\n抱歉,没有找到此用户")); +- } + + +``` + public void send(String str) {// 在哪里出错就在哪里捕获 +``` + +- try { +- dos.writeUTF(str); +- } catch (SocketException e) { +- this.Connected = false; +- clients.remove(this); +- } catch (IOException e) { +- e.printStackTrace(); +- } +- } +- } +- } +-   +- Client(一个线程用于读取,一个线程用于发送) + +``` +public class ChatClient extends Frame { +``` + + + +- Socket s = null; //将某个对象使得在一个类的各个方法可用,将该对象设置为整个类的成员变量 +- DataOutputStream dos = null;//在多个方法中都要使用 +- DataInputStream dis = null; +- TextField tfText = new TextField(); // 设置为成员变量方便其他类进行访问 +- TextArea taContent = new TextArea(); +- boolean started = false; +- Thread recv = null; +- +- ChatClient(String name, int x, int y, int w, int h) { +- super(name); +- this.setBounds(x, y, w, h); +- this.setLayout(new BorderLayout()); +- this.addWindowListener(new MonitorWindow()); +- taContent.setEditable(false); +- this.add(tfText, BorderLayout.SOUTH); +- this.add(taContent, BorderLayout.NORTH); +- tfText.addActionListener(new MonitorText());//对于文本框的监视器必须添加在某个文本框上,只有窗口监视器才能添加到Frame上 +- this.pack(); +- this.setVisible(true); // 必须放在最后一行,否则之下的组件无法显示 +- connect(); +- ClientNameDialog dialog = new ClientNameDialog(this,"姓名提示框",true); +- } +- + +``` + private class ClientNameDialog extends JDialog implements ActionListener{ +``` + +- JLabel jl = null; +- JTextField jf = null; +- JButton jb = null; +- +- ClientNameDialog(Frame owner,String title,boolean model){ +- super(owner,title,model); +- this.setLayout(new BorderLayout()); +- this.setBounds(300, 300, 200, 150); +- jl = new JLabel("请输入您的姓名或昵称:"); +- jf = new JTextField(); +- jb = new JButton("确定"); +- jb.addActionListener(this); +- this.addWindowListener(new WindowAdapter(){ + + +``` + public void windowClosing(WindowEvent arg0) { +``` + +- setVisible(false); +- System.exit(0); +- } +- }); +- this.add(jl,BorderLayout.NORTH); +- this.add(jf,BorderLayout.CENTER); +- this.add(jb, BorderLayout.SOUTH); +- this.setVisible(true); +- } + + +``` + public void actionPerformed(ActionEvent e) { +``` + +- String name = ""; +- name = jf.getText(); +- if((name == null || name.equals(""))){ +- JOptionPane.showMessageDialog(this, "姓名不可为空!"); +- return; +- } +- this.setVisible(false); +- send(name); +- JOptionPane.showMessageDialog(this, "欢迎您,"+name); +- launchThread(); +- } +- +- } + +``` + private class MonitorWindow extends WindowAdapter { +``` + + + +``` + public void windowClosing(WindowEvent e) { +``` + +- setVisible(false); +- disConnect(); +- System.exit(0); +- } +- } +- + +``` + private class MonitorText implements ActionListener { +``` + +- String str = null; +- + +``` + public void actionPerformed(ActionEvent e) { +``` + +- +- str = tfText.getText().trim();//注意这是内部类,要找到事件源对象直接引用外部类的TextField即可,不需要getSource(平行类可用) +- tfText.setText(""); //trim可以去掉开头和结尾的空格 +- send(str); +- } +- } +- + +``` + public void send(String str){//为发送数据单独建立一个方法 +``` + +- try{ +- dos.writeUTF(str); +- dos.flush(); + - }catch(IOException e1){ +- e1.printStackTrace(); +- } +- } +- + +``` + public void connect(){ //应为连接单独建立一个方法 +``` + +- try{ + - s = new Socket("localhost",6666); +- dos = new DataOutputStream(s.getOutputStream());//一连接就打开输出流 +- dis = new DataInputStream(s.getInputStream()); //一连接就打开输入流 +- started = true; +- }catch(IOException e){ +- e.printStackTrace(); +- } +- } +- + +``` + public void launchThread(){ +``` + +- recv = new Thread(new Receive()); +- recv.start(); +- } +- + +``` + public void disConnect() { +``` + +- try{ +- dos.writeUTF("EXIT"); +- started = false; +- //加入到主线程,会等待子线程执行完毕,才会执行下面的语句。这就避免了在读数据的时候将流切断,但是在这里是无效的。但是将线程停止应该先考虑使用join方法 +- CloseUtil.close(dis,dos); +- s.close(); +- } catch (IOException e) { +- e.printStackTrace(); +- } +- } + +- + +``` + private class Receive implements Runnable { //同样原因 readUTF是阻塞式的,处于死循环中,不能执行其他语句,所以为其单独设置一个线程 +``` + +- + +``` + public void run(){ +``` + +- String str = null; +- try{ +- while(started){ +- str = dis.readUTF(); //如果在阻塞状态,程序被关闭,那么一定会报错SocketException。关闭了Socket之后还在调用readUTF方法 +- taContent.setText(taContent.getText()+str+'\n');//解决方法是在关闭程序的同时停止线程,不再读取 +- (如果使用JTextArea可以使用append方法) +- } +- }catch (SocketException e){ //将SocketException视为退出。但这种想法是不好的,将异常视为程序正常的一部分 +- System.out.println("Client has quitted!"); +- }catch (EOFException e){ +- System.out.println("Client has quitted!"); +- }catch(IOException e){ +- e.printStackTrace(); +- } +- } +- } + +``` + public static void main(String[] args) { +``` + +- new ChatClient("Client", 200, 200, 300, 200); +- } +- } + + +#### Java NIO 使用 +- 传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。 +- NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。 +- BIO中的accept是没有客户端连接时阻塞,NIO的accept是没有客户端连接时立即返回。 + +- NIO的三个重要组件:Buffer、Channel、Selector。 +- Buffer是用于容纳数据的缓冲区,Channel是与IO设备之间的连接,类似于流。 +- 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。 +- Selector是Channel的多路复用器。 +##### Buffer(缓冲区) + +- + + +- clear 是将position置为0,limit置为capacity; +- flip是将limit置为position,position置为0; +###### MappedByteBuffer(对应OS中的内存映射文件) +- ByteBuffer有两种模式:直接/间接。间接模式就是HeapByteBuffer,即操作堆内存 (byte[])。 +- 但是内存毕竟有限,如果我要发送一个1G的文件怎么办?不可能真的去分配1G的内存.这时就必须使用"直接"模式,即 MappedByteBuffer。 + +- OS中内存映射文件是将一个文件映射为虚拟内存(文件没有真正加载到内存,只是作为虚存),不需要使用文件系统调用来读写数据,而是直接读写内存。 +- Java中是使用MappedByteBuffer来将文件映射为内存的。通常可以映射整个文件,如果文件比较大的话可以分段进行映射,只要指定文件的那个部分就可以。 +- 优点:减少一次数据拷贝 +- 之前是 进程空间<->内核的IO缓冲区<->文件 +- 现在是 进程空间<->文件 + +- MappedByteBuffer可以使用FileChannel.map方法获取。 + +- 它有更多的优点: +- a. 读取快 +- b. 写入快 +- c. 随机读写 + + +- MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。 +- 那么可用堆外内存到底是多少?,即默认堆外内存有多大: +- ① 如果我们没有通过-XX:MaxDirectMemorySize来指定最大的堆外内存。则 +- ② 如果我们没通过-Dsun.nio.MaxDirectMemorySize指定了这个属性,且它不等于-1。则 +- ③ 那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法。 +- 在我们使用CMS GC的情况下的实现如下:其实是新生代的最大值-一个survivor的大小+老生代的最大值,也就是我们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了。 + +- 如果当文件过大,内存不足时,可以通过position参数重新map文件后面的内容。 +- MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。 +- + +###### DirectByteBuffer(堆外内存) +- DirectByteBuffer继承自MappedByteBuffer,它们都是使用的堆外内存,不受JVM堆大小的限制,只是前者仅仅是分配内存,后者是将文件映射到内存中。 +- 可以通过ByteBuf.allocateDirect方法获取。 + +- 堆外内存的特点(大对象;加快内存拷贝;减轻GC压力) +- 对于大内存有良好的伸缩性(支持分配大块内存) +- 对垃圾回收停顿的改善可以明显感觉到(堆外内存,减少GC对堆内存回收的压力) +- 在进程间可以共享,减少虚拟机间的复制,加快复制速度(减少堆内内存拷贝到堆外内存的过程) +- 还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。( Netty中就使用了该方式 ) + +- 堆外内存的一些问题 + - 1)堆外内存回收问题(不手工回收会导致内存溢出,手工回收就失去了Java的优势); + - 2) 数据结构变得有些别扭。要么就是需要一个简单的数据结构以便于直接映射到堆外内存,要么就使用复杂的数据结构并序列化及反序列化到内存中。很明显使用序列化的话会比较头疼且存在性能瓶颈。使用序列化比使用堆对象的性能还差。 + + +- + +###### 堆外内存的释放 +- java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。 + +- GC方式: +- 存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的Cleaner,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。 + +- 当新生代满了,就会发生minor gc;如果此时对象还没失效,就不会被回收;撑过几次minorgc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。 +- 这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了minor gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。 + +- 这时,就只能靠前面提到的申请额度超限时触发的System.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。 + +- 那为什么System.gc()会释放DirectByteBuffer呢? +- 每个DirectByteBuffer关联着其对应的Cleaner,Cleaner是PhantomReference的子类,虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看ReferenceQueue中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收。 + +- 当GC时发现DirectByteBuffer除了PhantomReference外已不可达,就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,做爱做的事情——这是一种比finalizer更轻量更好的机制。 + + +- 手工方式: +- 如果想立即释放掉一个MappedByteBuffer/DirectByteBuffer,因为JDK没有提供公开API,只能使用反射的方法去unmap; +- 或者使用Cleaner的clean方法。 + +``` +public static void main(String[] args) { + try { + File f = File.createTempFile("Test", null); + f.deleteOnExit(); + RandomAccessFile file = new RandomAccessFile(f, "rw"); + file.setLength(1024); + FileChannel channel = file.getChannel(); + MappedByteBuffer buffer = channel.map( + FileChannel.MapMode.READ_WRITE, 0, 1024); + channel.close(); + file.close(); + // 手动unmap + Method m = FileChannelImpl.class.getDeclaredMethod("unmap", + MappedByteBuffer.class); + m.setAccessible(true); + m.invoke(FileChannelImpl.class, buffer); + if (f.delete()) + System.out.println("Temporary file deleted: " + f); + else + System.err.println("Not yet deleted: " + f); + } catch (Exception ex) { + ex.printStackTrace(); + } +} +``` + + + +- + +##### Channel(通道) +- Channel与IO设备的连接,与Stream是平级的概念。 +###### 流与通道的区别 +- 1、流是单向的,通道是双向的,可读可写。 +- 2、流读写是阻塞的,通道可以异步读写。 +- 3、流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入 + + +- 注意,FileChannel 不能设置为非阻塞模式。 + +###### 分散与聚集 +- 分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 + +- 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。 +###### Pipe + + +``` +public class PipeTest { + public static void main(String[] args) { + Pipe pipe = null; + ExecutorService exec = Executors.newFixedThreadPool(2); + try { + pipe = Pipe.open(); + final Pipe pipeTemp = pipe; + + exec.submit(new Callable() { + @Override + public Object call() throws Exception { + Pipe.SinkChannel sinkChannel = pipeTemp.sink();//向通道中写数据 + while (true) { + TimeUnit.SECONDS.sleep(1); + String newData = "Pipe Test At Time " + System.currentTimeMillis(); + ByteBuffer buf = ByteBuffer.allocate(1024); + buf.clear(); + buf.put(newData.getBytes()); + buf.flip(); + + while (buf.hasRemaining()) { + System.out.println(buf); + sinkChannel.write(buf); + } + } + } + }); + + exec.submit(new Callable() { + @Override + public Object call() throws Exception { + Pipe.SourceChannel sourceChannel = pipeTemp.source();//向通道中读数据 + while (true) { + TimeUnit.SECONDS.sleep(1); + ByteBuffer buf = ByteBuffer.allocate(1024); + buf.clear(); + int bytesRead = sourceChannel.read(buf); + System.out.println("bytesRead=" + bytesRead); + while (bytesRead > 0) { + buf.flip(); + byte b[] = new byte[bytesRead]; + int i = 0; + while (buf.hasRemaining()) { + b[i] = buf.get(); + System.out.printf("%X", b[i]); + i++; + } + String s = new String(b); + System.out.println("=================||" + s); + bytesRead = sourceChannel.read(buf); + } + } + } + }); + } catch (IOException e) { + e.printStackTrace(); + } finally { + exec.shutdown(); + } + } +} +``` + + +- + +###### FileChannel与文件锁 +- 在通道中我们可以对文件或者部分文件进行上锁。上锁和我们了解的线程锁差不多,都是为了保证数据的一致性。在文件通道FileChannel中可以对文件进行上锁,通过FileLock可以对文件进行锁的释放。 +- 文件加锁是建立在文件通道(FileChannel)之上的,套接字通道(SockeChannel)不考虑文件加锁,因为它是不共享的。它对文件加锁有两种方式: +- ①lock +- ②tryLock +- 两种加锁方式默认都是对整个文件加锁,如果自己配置的话就可以控制加锁的文件范围:position是加锁的开始位置,size是加锁长度,shared是用于控制该锁是共享的还是独占的。 +- lock是阻塞式的,当有进程对锁进行读取时会等待锁的释放,在此期间它会一直等待;tryLock是非阻塞式的,它尝试获得锁,如果这个锁不能获得,那么它会立即返回。 +- release可以释放锁。 +- 在一个进程中在锁没有释放之前是无法再次获得锁的 + +- 在java的NIO中,通道包下面有一个FileLock类,它主要是对文件锁工具的一个描述。在上一小节中对文件的锁获取其实是FileChannel获取的(lock与trylock是FileChannel的方法),它们返回一个FileLock对象。这个类的核心方法有如下这些: +- boolean isShared() :判断锁是否为共享类型 +- abstract boolean isValid() :判断锁是否有效 +- boolean overlaps():判断此锁定是否与给定的锁定区域重叠 +- long position():返回文件内锁定区域中第一个字节的位置。 +- abstract void release() :释放锁 +- long size() :返回锁定区域的大小,以字节为单位 + +- 在文件锁中有3种方式可以释放文件锁:①锁类释放锁,调用FileLock的release方法; ②通道类关闭通道,调用FileChannel的close方法;③jvm虚拟机会在特定情况释放锁。 + + +- 锁类型(独占式和共享式) +- 我们先区分一下在文件锁中两种锁的区别:①独占式的锁就想我们上面测试的那样,只要有一个进程获取了独占锁,那么别的进程只能等待。②共享锁在一个进程获取的情况下,别的进程还是可以读取被锁定的文件,但是别的进程不能写只能读。 + +- + +##### Selector (Channel的多路复用器) + +- Selector可以用单线程去管理多个Channel(多个连接)。 +- 放在网络编程的环境下:Selector使用单线程,轮询客户端对应的Channel的请求,如果某个Channel需要进行IO,那么分配一个线程去执行IO操作。 +- Selector可以去监听的请求有以下几类: + - 1、connect:客户端连接服务端事件,对应值为SelectionKey.OPCONNECT(8) + - 2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OPACCEPT(16) + - 3、read:读事件,对应值为SelectionKey.OPREAD(1) + - 4、write:写事件,对应值为SelectionKey.OPWRITE(4) +- 每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。 +- SelectionKey是一个复合事件,绑定到某个selector对应的某个channel上,可能是多个事件的复合或单一事件。 + +#### Java NIO 实例(文件上传) +- 服务器主线程先创建Socket,并注册到selector,然后轮询selector。 + - 1)如果有客户端需要进行连接,那么selector返回ACCEPT事件,主线程建立连接(accept),并将该客户端连接注册到selector,结束,继续轮询selector等待下一个客户端事件; + - 2)如果有已连接的客户端需要进行读写,那么selector返回READ/WRITE事件,主线程将该请求交给IO线程池中的某个线程执行操作,结束,继续轮询selector等待下一个客户端事件。 +##### 服务器 + +``` +public class NIOTCPServer { + private ServerSocketChannel serverSocketChannel; + private final String FILE_PATH = "E:/uploads/"; + private AtomicInteger i; + private final String RESPONSE_MSG = "服务器接收数据成功"; + private Selector selector; + private ExecutorService acceptPool; + private ExecutorService readPool; + + public NIOTCPServer() { + try { + serverSocketChannel = ServerSocketChannel.open(); + //切换为非阻塞模式 + serverSocketChannel.configureBlocking(false); + serverSocketChannel.bind(new InetSocketAddress(9000)); + //获得选择器 + selector = Selector.open(); + //将channel注册到selector上 + //第二个参数是选择键,用于说明selector监控channel的状态 + //可能的取值:SelectionKey.OP_READ OP_WRITE OP_CONNECT OP_ACCEPT + + //监控的是channel的接收状态 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + acceptPool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>()); + readPool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>()); + i = new AtomicInteger(0); + System.out.println("服务器启动"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void receive() { + try { + //如果有一个及以上的客户端的数据准备就绪 + while (selector.select() > 0) { + //获取当前选择器中所有注册的监听事件 + for (Iterator it = selector.selectedKeys().iterator(); it.hasNext(); ) { + SelectionKey key = it.next(); + //如果"接收"事件已就绪 + if (key.isAcceptable()) { + //交由接收事件的处理器处理 + acceptPool.submit(new ReceiveEventHander()); + } else if (key.isReadable()) { + //如果"读取"事件已就绪 + //交由读取事件的处理器处理 + readPool.submit(new ReadEventHandler((SocketChannel) key.channel())); + } + //处理完毕后,需要取消当前的选择键 + it.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + + class ReceiveEventHander implements Runnable { + + public ReceiveEventHander() { + } + + @Override + public void run() { + SocketChannel client = null; + try { + client = serverSocketChannel.accept(); + // 接收的客户端也要切换为非阻塞模式 + client.configureBlocking(false); + // 监控客户端的读操作是否就绪 + client.register(selector, SelectionKey.OP_READ); + System.out.println("服务器连接客户端:" + client.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + class ReadEventHandler implements Runnable { + private ByteBuffer buf; + private SocketChannel client; + + public ReadEventHandler(SocketChannel client) { + this.client = client; + buf = ByteBuffer.allocate(1024); + } + + @Override + public void run() { + + FileChannel fileChannel = null; + try { + int index = 0; + synchronized (client) { + while (client.read(buf) != -1) { + if (fileChannel == null) { + index = i.getAndIncrement(); + fileChannel = FileChannel.open(Paths.get(FILE_PATH, index + ".jpeg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); + } + buf.flip(); + fileChannel.write(buf); + buf.clear(); + } + } + if (fileChannel != null) { + fileChannel.close(); + System.out.println("服务器写来自客户端" + client + " 文件" + index + " 完毕"); + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + } + + public static void main(String[] args) { + NIOTCPServer server = new NIOTCPServer(); + server.receive(); + } +} +``` + +##### 客户端 + +``` +public class NIOTCPClient { + private SocketChannel clientChannel; + private ByteBuffer buf; + + public NIOTCPClient() { + try { + clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9000)); + //设置客户端为非阻塞模式 + clientChannel.configureBlocking(false); + buf = ByteBuffer.allocate(1024); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void send(String fileName) { + try { + FileChannel fileChannel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ); + while (fileChannel.read(buf) != -1) { + buf.flip(); + clientChannel.write(buf); + buf.clear(); + } + System.out.println("客户端已发送文件" + fileName); + fileChannel.close(); + clientChannel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + ExecutorService pool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>()); + Instant begin = Instant.now(); + for (int i = 0; i < 200; i++) { + pool.submit(() -> { + NIOTCPClient client = new NIOTCPClient(); + client.send("E:/1.jpeg"); + }); + } + pool.shutdown(); + Instant end = Instant.now(); + System.out.println(Duration.between(begin,end)); + } +} +``` + + +- + +#### Java AIO 使用 +- 对AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。 +- AIO的四步: +- 1、进程向操作系统请求数据 +- 2、操作系统把外部数据加载到内核的缓冲区中, +- 3、操作系统把内核的缓冲区拷贝到进程的缓冲区 +- 4、进程获得数据完成自己的功能 + +- JDK1.7主要增加了三个新的异步通道: +- AsynchronousFileChannel: 用于文件异步读写; +- AsynchronousSocketChannel: 客户端异步socket; +- AsynchronousServerSocketChannel: 服务器异步socket。 +- 因为AIO的实施需充分调用OS参与,IO需要操作系统支持、并发也同样需要操作系统的 + +- 在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。 + +- 在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。 +- void completed(V result, A attachment); +- void failed(Throwable exc, A attachment); + + +``` +public class AIOServer { + private static int PORT = 8080; + private static int BUFFER_SIZE = 1024; + private static String CHARSET = "utf-8"; //默认编码 + private static CharsetDecoder decoder = Charset.forName(CHARSET).newDecoder(); //解码 + + private AsynchronousServerSocketChannel serverChannel; + + public AIOServer() { + this.decoder = Charset.forName(CHARSET).newDecoder(); + } + + private void listen() throws Exception { + + //打开一个服务通道 + //绑定服务端口 + this.serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT), 100); + this.serverChannel.accept(this, new AcceptHandler()); + } + + + /** + * accept到一个请求时的回调 + */ + private class AcceptHandler implements CompletionHandler { + @Override + public void completed(final AsynchronousSocketChannel client, AIOServer server) { + try { + System.out.println("远程地址:" + client.getRemoteAddress()); + //tcp各项参数 + client.setOption(StandardSocketOptions.TCP_NODELAY, true); + client.setOption(StandardSocketOptions.SO_SNDBUF, 1024); + client.setOption(StandardSocketOptions.SO_RCVBUF, 1024); + + if (client.isOpen()) { + System.out.println("client.isOpen:" + client.getRemoteAddress()); + final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + buffer.clear(); + client.read(buffer, client, new ReadHandler(buffer)); + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + server.serverChannel.accept(server, this);// 监听新的请求,递归调用。 + } + } + + @Override + public void failed(Throwable e, AIOServer attachment) { + try { + e.printStackTrace(); + } finally { + attachment.serverChannel.accept(attachment, this);// 监听新的请求,递归调用。 + } + } + } + + /** + * Read到请求数据的回调 + */ + private class ReadHandler implements CompletionHandler { + + private ByteBuffer buffer; + + public ReadHandler(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void completed(Integer result, AsynchronousSocketChannel client) { + try { + if (result < 0) {// 客户端关闭了连接 + AIOServer.close(client); + } else if (result == 0) { + System.out.println("空数据"); // 处理空数据 + } else { + // 读取请求,处理客户端发送的数据 + buffer.flip(); + CharBuffer charBuffer = AIOServer.decoder.decode(buffer); + System.out.println(charBuffer.toString()); //接收请求 + + //响应操作,服务器响应结果 + buffer.clear(); + String res = "hellworld"; + buffer = ByteBuffer.wrap(res.getBytes()); + client.write(buffer, client, new WriteHandler(buffer));//Response:响应。 + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void failed(Throwable exc, AsynchronousSocketChannel attachment) { + exc.printStackTrace(); + AIOServer.close(attachment); + } + } + + /** + * Write响应完请求的回调 + */ + private class WriteHandler implements CompletionHandler { + private ByteBuffer buffer; + + public WriteHandler(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void completed(Integer result, AsynchronousSocketChannel attachment) { + buffer.clear(); + AIOServer.close(attachment); + } + + @Override + public void failed(Throwable exc, AsynchronousSocketChannel attachment) { + exc.printStackTrace(); + AIOServer.close(attachment); + } + } + + + private static void close(AsynchronousSocketChannel client) { + try { + client.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + try { + System.out.println("正在启动服务..."); + AIOServer AIOServer = new AIOServer(); + AIOServer.listen(); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + } + } + }); + t.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + + +- + +#### Java NIO 源码 +- 关于Selector源码过于难以理解,可以先放过。 +##### Buffer + +``` +public abstract class Buffer { + + /** + * The characteristics of Spliterators that traverse and split elements + * maintained in Buffers. + */ + static final int SPLITERATOR_CHARACTERISTICS = + Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; + + // Invariants: mark <= position <= limit <= capacity + private int mark = -1; + private int position = 0; + private int limit; + private int capacity; + + // Used only by direct buffers + // NOTE: hoisted here for speed in JNI GetDirectBufferAddress + long address; + + // Creates a new buffer with the given mark, position, limit, and capacity, + // after checking invariants. + // + Buffer(int mark, int pos, int lim, int cap) { // package-private + if (cap < 0) + throw new IllegalArgumentException("Negative capacity: " + cap); + this.capacity = cap; + limit(lim); + position(pos); + if (mark >= 0) { + if (mark > pos) + throw new IllegalArgumentException("mark > position: (" + + mark + " > " + pos + ")"); + this.mark = mark; + } + } +``` + +- } +- ByteBuffer有两种实现:HeapByteBuffer和DirectByteBuffer。 +- ByteBuffer#allocate + +``` +public static ByteBuffer allocate(int capacity) { + if (capacity < 0) + throw new IllegalArgumentException(); + return new HeapByteBuffer(capacity, capacity); +} +``` + +- ByteBuffer#allocateDirect + +``` +public static ByteBuffer allocateDirect(int capacity) { + return new DirectByteBuffer(capacity); +} +``` + + +##### HeapByteBuffer(间接模式) +- 底层基于byte数组。 +###### 初始化 + +``` +HeapByteBuffer(int cap, int lim) { // package-private + super(-1, 0, lim, cap, new byte[cap], 0); +} +``` + +- 调用的是ByteBuffer的初始化方法 + +``` +ByteBuffer(int mark, int pos, int lim, int cap, // package-private + byte[] hb, int offset) +{ + super(mark, pos, lim, cap); + this.hb = hb; + this.offset = offset; +} +``` + +- ByteBuffer的独有成员变量: +- final byte[] hb; // Non-null only for heap buffers +final int offset; +boolean isReadOnly; // Valid only for heap buffers + +###### get + +``` +public byte get() { + return hb[ix(nextGetIndex())]; +} +``` + + + +``` +final int nextGetIndex() { // package-private + if (position >= limit) + throw new BufferUnderflowException(); + return position++; +} +``` + + +- protected int ix(int i) { + return i + offset; +} + +###### put + +``` +public ByteBuffer put(byte x) { + hb[ix(nextPutIndex())] = x; + return this; +} +``` + + + +``` +final int nextPutIndex() { // package-private + if (position >= limit) + throw new BufferOverflowException(); + return position++; +} +``` + + +- + +##### DirectByteBuffer(直接模式) +- 底层基于c++的malloc分配的堆外内存,是使用Unsafe类分配的,底层调用了native方法。 +- 在创建DirectByteBuffer的同时,创建一个与其对应的cleaner,cleaner是一个虚引用。 +- 回收堆外内存的几种情况: + - 1)程序员手工释放,需要使用sun的非公开API实现。 + - 2)申请新的堆外内存而内存不足时,会进行调用Cleaner(作为一个Reference)的静态方法tryHandlePending(false),它又会调用cleaner的clean方法释放内存。 + - 3)当DirectByteBuffer失去强引用,只有虚引用时,当等到某一次System.gc(full gc)(比如堆外内存达到XX:MaxDirectMemorySize)时,当DirectByteBuffer对象从pending状态 -> enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。 +###### 初始化 +- 重要成员变量: + +``` +private final Cleaner cleaner; + +``` + +- // Cached unsafe-access object +protected static final Unsafe unsafe = Bits.unsafe(); +- Unsafe中很多都是native方法,底层调用c++代码。 + + +``` +DirectByteBuffer(int cap) { // package-private + + super(-1, 0, cap, cap); +``` + +- // 内存是否按页分配对齐 + boolean pa = VM.isDirectMemoryPageAligned(); +- // 获取每页内存大小 + int ps = Bits.pageSize(); +- // 分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量 + long size = Math.max(1L, (long)cap + (pa ? ps : 0)); +- // 用Bits类保存总分配内存(按页分配)的大小和实际内存的大小 + Bits.reserveMemory(size, cap); + + long base = 0; + try { +- // 在堆外内存的基地址,指定内存大小 + base = unsafe.allocateMemory(size); + } catch (OutOfMemoryError x) { + Bits.unreserveMemory(size, cap); + throw x; + } + unsafe.setMemory(base, size, (byte) 0); + - // 计算堆外内存的基地址 + if (pa && (base % ps != 0)) { + // Round up to page boundary + address = base + ps - (base & (ps - 1)); + } else { + address = base; + } + cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); + att = null; +} + +- 第一行super调用的是其父类MappedByteBuffer的构造方法 + +``` +MappedByteBuffer(int mark, int pos, int lim, int cap) { // package-private + super(mark, pos, lim, cap); + this.fd = null; +} +``` + +- 而它的super又调用了ByteBuffer的构造方法 + +``` +ByteBuffer(int mark, int pos, int lim, int cap) { // package-private + this(mark, pos, lim, cap, null, 0); +} +``` + +- Bits#reserveMemory +- 该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。 + +- 总的来说,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要创建的堆外内存大小时,会实现以下的步骤来尝试完成本次堆外内存的创建: +- ① 触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。 +- ② 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()。System.gc()会触发一个full gc,但你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。 + +- 这里之所以用使用full gc的很重要的一个原因是:System.gc()会对新生代和老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存. +- DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为冰山对象. +- 我们做young gc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题。(并且堆外内存多用于生命期中等或较长的对象) +- 如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc – JVM参数DisableExplicitGC)。 + +- 注意,如果你设置了-XX:+DisableExplicitGC,将会禁用显示GC,这会使System.gc()调用无效。 +- ③ 如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError("Direct buffer memory”)异常。 +- static void reserveMemory(long size, int cap) { + + if (!memoryLimitSet && VM.isBooted()) { + maxMemory = VM.maxDirectMemory(); + memoryLimitSet = true; + } + + // optimist! + if (tryReserveMemory(size, cap)) { + return; + } + + final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); + // 如果系统中内存( 即,堆外内存 )不够的话: +- jlra.tryHandlePendingReference()会触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。 +- + // retry while helping enqueue pending Reference objects + // which includes executing pending Cleaner(s) which includes + // Cleaner(s) that free direct buffer memory + while (jlra.tryHandlePendingReference()) { + if (tryReserveMemory(size, cap)) { + return; + } + } + // 如果在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则 + // trigger VM's Reference processing + System.gc(); + + // a retry loop with exponential back-off delays + // (this gives VM some time to do it's job) + boolean interrupted = false; + try { + long sleepTime = 1; + int sleeps = 0; + while (true) { + if (tryReserveMemory(size, cap)) { + return; + } +- // 9 + if (sleeps >= MAX_SLEEPS) { + break; + } + if (!jlra.tryHandlePendingReference()) { + try { + Thread.sleep(sleepTime); + sleepTime <<= 1; + sleeps++; + } catch (InterruptedException e) { + interrupted = true; + } + } + } + // 如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError("Direct buffer memory”)异常。 + // no luck + throw new OutOfMemoryError("Direct buffer memory"); + + } finally { + if (interrupted) { + // don't swallow interrupts + Thread.currentThread().interrupt(); + } + } +} +- Reference#tryHandlePending +- static boolean tryHandlePending(boolean waitForNotify) { + Reference r; + Cleaner c; + try { + synchronized (lock) { + if (pending != null) { + r = pending; + // 'instanceof' might throw OutOfMemoryError sometimes + // so do this before un-linking 'r' from the 'pending' chain... + c = r instanceof Cleaner ? (Cleaner) r : null; + // unlink 'r' from 'pending' chain + pending = r.discovered; + r.discovered = null; + } else { + // The waiting on the lock may cause an OutOfMemoryError + // because it may try to allocate exception objects. + if (waitForNotify) { + lock.wait(); + } + // retry if waited + return waitForNotify; + } + } + } catch (OutOfMemoryError x) { + // Give other threads CPU time so they hopefully drop some live references + // and GC reclaims some space. + // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above + // persistently throws OOME for some time... + Thread.yield(); + // retry + return true; + } catch (InterruptedException x) { + // retry + return true; + } + + // Fast path for cleaners + if (c != null) { + c.clean(); + return true; + } + + ReferenceQueue q = r.queue; + if (q != ReferenceQueue.NULL) q.enqueue(r); + return true; +} + +###### Deallocator +- 后面是调用unsafe的分配堆外内存的方法,然后初始化了该DirectByteBuffer对应的cleaner。 +- 注:在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。 + + +``` +private static class Deallocator + implements Runnable +{ + + private static Unsafe unsafe = Unsafe.getUnsafe(); + + private long address; + private long size; + private int capacity; + + private Deallocator(long address, long size, int capacity) { + assert (address != 0); + this.address = address; + this.size = size; + this.capacity = capacity; + } + + public void run() { + if (address == 0) { + // Paranoia + return; + } + unsafe.freeMemory(address); + address = 0; + Bits.unreserveMemory(size, capacity); + } + +} +``` + + +###### Cleaner(回收) + +``` +public class Cleaner extends PhantomReference { + private static final ReferenceQueue dummyQueue = new ReferenceQueue(); + private static Cleaner first = null; + private Cleaner next = null; + private Cleaner prev = null; + private final Runnable thunk; + + private static synchronized Cleaner add(Cleaner var0) { + if (first != null) { + var0.next = first; + first.prev = var0; + } + + first = var0; + return var0; + } + + private static synchronized boolean remove(Cleaner var0) { + if (var0.next == var0) { + return false; + } else { + if (first == var0) { + if (var0.next != null) { + first = var0.next; + } else { + first = var0.prev; + } + } + + if (var0.next != null) { + var0.next.prev = var0.prev; + } + + if (var0.prev != null) { + var0.prev.next = var0.next; + } + + var0.next = var0; + var0.prev = var0; + return true; + } + } + + private Cleaner(Object var1, Runnable var2) { + super(var1, dummyQueue); + this.thunk = var2; + } + // var0是DirectByteBuffer,var1是Deallocator线程对象 + public static Cleaner create(Object var0, Runnable var1) { + return var1 == null ? null : add(new Cleaner(var0, var1)); + } + + public void clean() { + if (remove(this)) { + try { +``` + + +``` +// 回收该DirectByteBuffer对应的堆外内存 + this.thunk.run(); + } catch (final Throwable var2) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + if (System.err != null) { + (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); + } + + System.exit(1); + return null; + } + }); + } + + } + } +} +``` + +- Cleaner的构造方法中又调用了父类虚引用的构造方法: + +``` +public PhantomReference(T referent, ReferenceQueue q) { + super(referent, q); +} +``` + + +###### get + +``` +public byte get() { + return ((unsafe.getByte(ix(nextGetIndex())))); +} +``` + +###### put + +``` +public ByteBuffer put(byte x) { + unsafe.putByte(ix(nextPutIndex()), ((x))); + return this; +} +``` + + +- + +##### FileChannel(阻塞式) +- FileChannel的read、write和map通过其实现类FileChannelImpl实现。 +- FileChannelImpl的Oracle JDK没有提供源码,只能在OpenJDK中查看。 +###### open + +``` +public static FileChannel open(Path path, OpenOption... options) + throws IOException +{ + Set set = new HashSet(options.length); + Collections.addAll(set, options); + return open(path, set, NO_ATTRIBUTES); +} +``` + + + +``` +public static FileChannel open(Path path, + Set options, + FileAttribute... attrs) + throws IOException +{ + FileSystemProvider provider = path.getFileSystem().provider(); + return provider.newFileChannel(path, options, attrs); +} +``` + + +- WindowsFileSystemProvider#newFileChannel + +``` +public FileChannel newFileChannel(Path path, + Set options, + FileAttribute... attrs) + throws IOException +{ + if (path == null) + throw new NullPointerException(); + if (!(path instanceof WindowsPath)) + throw new ProviderMismatchException(); + WindowsPath file = (WindowsPath)path; + + WindowsSecurityDescriptor sd = WindowsSecurityDescriptor.fromAttribute(attrs); + try { + return WindowsChannelFactory + .newFileChannel(file.getPathForWin32Calls(), + file.getPathForPermissionCheck(), + options, + sd.address()); + } catch (WindowsException x) { + x.rethrowAsIOException(file); + return null; + } finally { + if (sd != null) + sd.release(); + } +} +``` + + +- WindowsChannelFactory#newFileChannel +- static FileChannel newFileChannel(String pathForWindows, + String pathToCheck, + Set options, + long pSecurityDescriptor) + throws WindowsException +{ + Flags flags = Flags.toFlags(options); + + // default is reading; append => writing + if (!flags.read && !flags.write) { + if (flags.append) { + flags.write = true; + } else { + flags.read = true; + } + } + + // validation + if (flags.read && flags.append) + throw new IllegalArgumentException("READ + APPEND not allowed"); + if (flags.append && flags.truncateExisting) + throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed"); + + FileDescriptor fdObj = open(pathForWindows, pathToCheck, flags, pSecurityDescriptor); + return FileChannelImpl.open(fdObj, pathForWindows, flags.read, flags.write, flags.append, null); +} + + +``` +/** + * Opens file based on parameters and options, returning a FileDescriptor + * encapsulating the handle to the open file. + */ +private static FileDescriptor open(String pathForWindows, + String pathToCheck, + Flags flags, + long pSecurityDescriptor) + throws WindowsException +{ + // set to true if file must be truncated after open + boolean truncateAfterOpen = false; + + // map options + int dwDesiredAccess = 0; + if (flags.read) + dwDesiredAccess |= GENERIC_READ; + if (flags.write) + dwDesiredAccess |= GENERIC_WRITE; + + int dwShareMode = 0; + if (flags.shareRead) + dwShareMode |= FILE_SHARE_READ; + if (flags.shareWrite) + dwShareMode |= FILE_SHARE_WRITE; + if (flags.shareDelete) + dwShareMode |= FILE_SHARE_DELETE; + + int dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; + int dwCreationDisposition = OPEN_EXISTING; + if (flags.write) { + if (flags.createNew) { + dwCreationDisposition = CREATE_NEW; + // force create to fail if file is orphaned reparse point + dwFlagsAndAttributes |= FILE_FLAG_OPEN_REPARSE_POINT; + } else { + if (flags.create) + dwCreationDisposition = OPEN_ALWAYS; + if (flags.truncateExisting) { + // Windows doesn't have a creation disposition that exactly + // corresponds to CREATE + TRUNCATE_EXISTING so we use + // the OPEN_ALWAYS mode and then truncate the file. + if (dwCreationDisposition == OPEN_ALWAYS) { + truncateAfterOpen = true; + } else { + dwCreationDisposition = TRUNCATE_EXISTING; + } + } + } + } + + if (flags.dsync || flags.sync) + dwFlagsAndAttributes |= FILE_FLAG_WRITE_THROUGH; + if (flags.overlapped) + dwFlagsAndAttributes |= FILE_FLAG_OVERLAPPED; + if (flags.deleteOnClose) + dwFlagsAndAttributes |= FILE_FLAG_DELETE_ON_CLOSE; + + // NOFOLLOW_LINKS and NOFOLLOW_REPARSEPOINT mean open reparse point + boolean okayToFollowLinks = true; + if (dwCreationDisposition != CREATE_NEW && + (flags.noFollowLinks || + flags.openReparsePoint || + flags.deleteOnClose)) + { + if (flags.noFollowLinks || flags.deleteOnClose) + okayToFollowLinks = false; + dwFlagsAndAttributes |= FILE_FLAG_OPEN_REPARSE_POINT; + } + + // permission check + if (pathToCheck != null) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + if (flags.read) + sm.checkRead(pathToCheck); + if (flags.write) + sm.checkWrite(pathToCheck); + if (flags.deleteOnClose) + sm.checkDelete(pathToCheck); + } + } + + // open file + long handle = CreateFile(pathForWindows, + dwDesiredAccess, + dwShareMode, + pSecurityDescriptor, + dwCreationDisposition, + dwFlagsAndAttributes); + + // make sure this isn't a symbolic link. + if (!okayToFollowLinks) { + try { + if (WindowsFileAttributes.readAttributes(handle).isSymbolicLink()) + throw new WindowsException("File is symbolic link"); + } catch (WindowsException x) { + CloseHandle(handle); + throw x; + } + } + + // truncate file (for CREATE + TRUNCATE_EXISTING case) + if (truncateAfterOpen) { + try { + SetEndOfFile(handle); + } catch (WindowsException x) { + CloseHandle(handle); + throw x; + } + } + + // make the file sparse if needed + if (dwCreationDisposition == CREATE_NEW && flags.sparse) { + try { + DeviceIoControlSetSparse(handle); + } catch (WindowsException x) { + // ignore as sparse option is hint + } + } + + // create FileDescriptor and return + FileDescriptor fdObj = new FileDescriptor(); + fdAccess.setHandle(fdObj, handle); + return fdObj; +} +``` + + +- static long CreateFile(String path, + int dwDesiredAccess, + int dwShareMode, + long lpSecurityAttributes, + int dwCreationDisposition, + int dwFlagsAndAttributes) + throws WindowsException +{ + NativeBuffer buffer = asNativeBuffer(path); + try { + return CreateFile0(buffer.address(), + dwDesiredAccess, + dwShareMode, + lpSecurityAttributes, + dwCreationDisposition, + dwFlagsAndAttributes); + } finally { + buffer.release(); + } +} + + +``` +private static native long CreateFile0(long lpFileName, + int dwDesiredAccess, + int dwShareMode, + long lpSecurityAttributes, + int dwCreationDisposition, + int dwFlagsAndAttributes) + throws WindowsException; +``` + +- + +###### read + +``` +public int read(ByteBuffer dst) throws IOException { + ensureOpen(); + if (!readable) + throw new NonReadableChannelException(); + synchronized (positionLock) { + int n = 0; + int ti = -1; + try { + begin(); + ti = threads.add(); + if (!isOpen()) + return 0; + do { + n = IOUtil.read(fd, dst, -1, nd); + } while ((n == IOStatus.INTERRUPTED) && isOpen()); + return IOStatus.normalize(n); + } finally { + threads.remove(ti); + end(n > 0); + assert IOStatus.check(n); + } + } +} +``` + +- IOUtil.read +- static int read(FileDescriptor fd, ByteBuffer dst, long position, + NativeDispatcher nd) IOException { + if (dst.isReadOnly()) + throw new IllegalArgumentException("Read-only buffer"); + if (dst instanceof DirectBuffer) + return readIntoNativeBuffer(fd, dst, position, nd); + + // Substitute a native buffer + ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining()); + try { + int n = readIntoNativeBuffer(fd, bb, position, nd); + bb.flip(); + if (n > 0) + dst.put(bb); + return n; + } finally { + Util.offerFirstTemporaryDirectBuffer(bb); + } +} +- 通过上述实现可以看出,基于channel的文件数据读取步骤如下: +- 1、申请一块和缓存同大小的DirectByteBuffer bb。 +- 2、读取数据到缓存bb,底层由NativeDispatcher的read实现。 +- 3、把bb的数据读取到dst(用户定义的缓存,在jvm中分配内存)。 +- read方法导致数据复制了两次。 + +###### write + +``` +public int write(ByteBuffer src) throws IOException { + ensureOpen(); + if (!writable) + throw new NonWritableChannelException(); + synchronized (positionLock) { + int n = 0; + int ti = -1; + try { + begin(); + ti = threads.add(); + if (!isOpen()) + return 0; + do { + n = IOUtil.write(fd, src, -1, nd); + } while ((n == IOStatus.INTERRUPTED) && isOpen()); + return IOStatus.normalize(n); + } finally { + threads.remove(ti); + end(n > 0); + assert IOStatus.check(n); + } + } +} +``` + + +- IOUtil.write +- static int write(FileDescriptor fd, ByteBuffer src, long position, + NativeDispatcher nd) throws IOException { + if (src instanceof DirectBuffer) + return writeFromNativeBuffer(fd, src, position, nd); + // Substitute a native buffer + int pos = src.position(); + int lim = src.limit(); + assert (pos <= lim); + int rem = (pos <= lim ? lim - pos : 0); + ByteBuffer bb = Util.getTemporaryDirectBuffer(rem); + try { + bb.put(src); + bb.flip(); + // Do not update src until we see how many bytes were written + src.position(pos); + int n = writeFromNativeBuffer(fd, bb, position, nd); + if (n > 0) { + // now update src + src.position(pos + n); + } + return n; + } finally { + Util.offerFirstTemporaryDirectBuffer(bb); + } +} +- 基于channel的文件数据写入步骤如下: +- 1、申请一块DirectByteBuffer,bb大小为byteBuffer中的limit - position。 +- 2、复制byteBuffer中的数据到bb中。 +- 3、把数据从bb中写入到文件,底层由NativeDispatcher的write实现,具体如下: + +``` +private static int writeFromNativeBuffer(FileDescriptor fd, + ByteBuffer bb, long position, NativeDispatcher nd) + throws IOException { + int pos = bb.position(); + int lim = bb.limit(); + assert (pos <= lim); + int rem = (pos <= lim ? lim - pos : 0); + + int written = 0; + if (rem == 0) + return 0; + if (position != -1) { + written = nd.pwrite(fd, + ((DirectBuffer) bb).address() + pos, + rem, position); + } else { + written = nd.write(fd, ((DirectBuffer) bb).address() + pos, rem); + } + if (written > 0) + bb.position(pos + written); + return written; +} +``` + +- write方法也导致了数据复制了两次。 +- + +##### ServerSocketChannel +- 它的实现类是ServerSocketChannelImpl,同样是闭源的。 +###### open + +``` +public static ServerSocketChannel open() throws IOException { + return SelectorProvider.provider().openServerSocketChannel(); +} +``` + + +- SelectorProvider.provider()方法在windows平台下返回的是SelectorProvider 的实现类 WindowsSelectorProvider类的实例。 +- WindowsSelectorProvider类的直接父类为SelectorProviderImpl; +- SelectorProviderImpl 的直接父类是 SelectorProvider。 + +- SelectorProviderImpl# openServerSocketChannel + +``` +public ServerSocketChannel openServerSocketChannel() throws IOException { + return new ServerSocketChannelImpl(this); +} +``` + + + - ServerSocketChannelImpl(SelectorProvider var1) throws IOException { + super(var1); + this.fd = Net.serverSocket(true); + this.fdVal = IOUtil.fdVal(this.fd); + this.state = 0; +} + - super(var1)实际上是父类的构造方法 +- protected AbstractSelectableChannel(SelectorProvider provider) { + this.provider = provider; +} + +- Net#serverSocket 创建一个FileDescriptor +- static FileDescriptor serverSocket(boolean stream) { + return IOUtil.newFD(socket0(isIPv6Available(), stream, true, fastLoopback)); +} + +- + +###### bind + +``` +public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException { + synchronized (lock) { + if (!isOpen()) + throw new ClosedChannelException(); + if (isBound()) + throw new AlreadyBoundException(); + InetSocketAddress isa = (local == null) ? new InetSocketAddress(0) : + Net.checkAddress(local); + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + sm.checkListen(isa.getPort()); + NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort()); + Net.bind(fd, isa.getAddress(), isa.getPort()); + Net.listen(fd, backlog < 1 ? 50 : backlog); + synchronized (stateLock) { + localAddress = Net.localAddress(fd); + } + } + return this; +} +``` + + +- + +###### register +- Selector是通过Selector.open方法获得的。 +- 将这个通道channel注册到指定的selector中,返回一个SelectionKey对象实例。 +- register这个方法在实现代码上的逻辑有以下四点: +- 1、首先检查通道channel是否是打开的,如果不是打开的,则抛异常,如果是打开的,则进行 2。 +- 2、检查指定的interest集合是否是有效的。如果没效,则抛异常。否则进行 3。这里要特别强调一下:对于ServerSocketChannel仅仅支持”新的连接”,因此interest集合ops满足ops&~sectionKey.OP_ACCEPT!=0,即对于ServerSocketChannel注册到Selector中时的事件只能包括SelectionKey.OP_ACCEPT。 +- 3、对通道进行了阻塞模式的检查,如果不是阻塞模式,则抛异常,否则进行4. +- 4、得到当前通道在指定Selector上的SelectionKey,假设结果用k表示。下面对k是否为null有不同的处理。如果k不为null,则说明此通道channel已经在Selector上注册过了,则直接将指定的ops添加进SelectionKey中即可。如果k为null,则说明此通道还没有在Selector上注册,则需要先进行注册,然后为其对应的SelectionKey设置给定值ops。 + +- 与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。 + +``` +public final SelectionKey register(Selector sel, int ops, + Object att) + throws ClosedChannelException +{ + synchronized (regLock) { + if (!isOpen()) + throw new ClosedChannelException(); + if ((ops & ~validOps()) != 0) + throw new IllegalArgumentException(); + if (blocking) + throw new IllegalBlockingModeException(); +``` + +- // 得到当前通道在指定Selector上的SelectionKey(复合事件) +- SelectionKey k = findKey(sel); +- 如果k不为null,则说明此通道已经在Selector上注册过了,则直接将指定的ops添加进SelectionKey中即可。 +- 如果k为null,则说明此通道还没有在Selector上注册,则需要先进行注册,然后添加SelectionKey。 + if (k != null) { + k.interestOps(ops); + k.attach(att); + } + if (k == null) { + // New registration + synchronized (keyLock) { + if (!isOpen()) + throw new ClosedChannelException(); + k = ((AbstractSelector)sel).register(this, ops, att); + addKey(k); + } + } + return k; + } + + +``` +private SelectionKey findKey(Selector sel) { + synchronized (keyLock) { + if (keys == null) + return null; + for (int i = 0; i < keys.length; i++) + if ((keys[i] != null) && (keys[i].selector() == sel)) + return keys[i]; + return null; + } +} +``` + + +- + +##### Selector(如何实现Channel多路复用) + +- SocketChannel、ServerSocketChannel和Selector的实例初始化都通过SelectorProvider类实现,其中Selector是整个NIO Socket的核心实现。 +- SelectorProvider在windows和linux下有不同的实现,provider方法会返回对应的实现。 +###### 成员变量 +1.- final class WindowsSelectorImpl extends SelectorImpl   +2.- {   +3. +``` +    private final int INIT_CAP = 8;//选择key集合,key包装集合初始化容量   +``` + +4. +``` +    private static final int MAX_SELECTABLE_FDS = 1024;//最大选择key数量   +``` + +5. +``` +    private SelectionKeyImpl channelArray[];//选择器关联通道集合   +``` + +6. +``` +    private PollArrayWrapper pollWrapper;//存放所有文件描述对象(选择key,唤醒管道的source与sink通道)的集合   +``` + +7. +``` +    private int totalChannels;//注册到选择的通道数量   +``` + +8. +``` +    private int threadsCount;//选择线程数   +``` + +9. +``` +    private final List threads = new ArrayList();//选择操作线程集合   +``` + +10. +``` +    private final Pipe wakeupPipe = Pipe.open();//唤醒等待选择操作的管道   +``` + +11. +``` +    private final int wakeupSourceFd;//唤醒管道源通道文件描述   +``` + +12. +``` +    private final int wakeupSinkFd;//唤醒管道sink通道文件描述   +``` + +13. +``` +    private Object closeLock;//选择器关闭同步锁   +``` + +14. +``` +    private final FdMap fdMap = new FdMap();//存放选择key文件描述与选择key映射关系的Map   +``` + +15. +``` +    private final SubSelector subSelector = new SubSelector();//子选择器   +``` + +16. +``` +    private long timeout;//超时时间,具体什么意思,现在还没明白,在后面在看   +``` + +17. +``` +    private final Object interruptLock = new Object();//中断同步锁,在唤醒选择操作线程时,用于同步   +``` + +18. +``` +    private volatile boolean interruptTriggered;//是否唤醒等待选择操的线程   +``` + +19. +``` +    private final StartLock startLock = new StartLock();//选择操作开始锁   +``` + +20. +``` +    private final FinishLock finishLock = new FinishLock();//选择操作结束锁   +``` + +21. +``` +    private long updateCount;//更新数量,具体什么意思,现在还没明白,在后面在看   +``` + +22.-     static final boolean $assertionsDisabled = !sun/nio/ch/WindowsSelectorImpl.desiredAssertionStatus();   +23.-     static    +24.-     {   +25.-         //加载nio,net资源库   +26.-         Util.load();   +27.-     }    +28.- }   + +###### open + +``` +public static Selector open() throws IOException { + return SelectorProvider.provider().openSelector(); +} +``` + +- WindowsSelectorProvider#openSelector + +``` +public AbstractSelector openSelector() throws IOException { + return new WindowsSelectorImpl(this); +} +``` + +- 初始化一个wakeupPipe + + - WindowsSelectorImpl(SelectorProvider var1) throws IOException { + super(var1); + this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal(); + SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink(); + var2.sc.socket().setTcpNoDelay(true); + this.wakeupSinkFd = var2.getFDVal(); + this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0); +} + +- SelectorImpl#register +- 第一个参数是ServerSocketChannel,第二个参数是复合事件,第三个是附件。 +- 1、以当前channel和selector为参数,初始化SelectionKeyImpl 对象selectionKeyImpl ,并添加附件attachment。 +- 2、如果当前channel的数量totalChannels等于SelectionKeyImpl数组大小,对SelectionKeyImpl数组和pollWrapper进行扩容操作。 +- 3、如果totalChannels % MAXSELECTABLEFDS == 0,则多开一个线程处理selector。 +- 4、pollWrapper.addEntry将把selectionKeyImpl中的socket句柄添加到对应的pollfd。 +- 5、k.interestOps(ops)方法最终也会把event添加到对应的pollfd。 +- 所以,不管serverSocketChannel,还是socketChannel,在selector注册的事件,最终都保存在pollArray中。 + +``` +protected final SelectionKey register(AbstractSelectableChannel ch, + int ops, + Object attachment) { + if (!(ch instanceof SelChImpl)) + throw new IllegalSelectorException(); + SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); + k.attach(attachment); + synchronized (publicKeys) { + implRegister(k); + } + k.interestOps(ops); + return k; +} +``` + + +- WindowsSelectorImpl#implRegister +- protected void implRegister(SelectionKeyImpl ski) { + synchronized (closeLock) { + if (pollWrapper == null) + throw new ClosedSelectorException(); + growIfNeeded(); + channelArray[totalChannels] = ski; + ski.setIndex(totalChannels); + fdMap.put(ski); + keys.add(ski); + pollWrapper.addEntry(totalChannels, ski); + totalChannels++; + } +} + +###### select(返回有事件发生的SelectionKey数量) +- var1是timeout时间,无参数的版本对应的timeout为0. +- select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。 +- 这个方法并不能提供精确时间的保证,和当执行wait(long timeout)方法时并不能保证会延时timeout道理一样。 +- 这里的timeout说明如下: +- 如果 timeout为正,则select(long timeout)在等待有通道被选择时至多会阻塞timeout毫秒 +- 如果timeout为零,则永远阻塞直到有至少一个通道准备就绪。 +- timeout不能为负数。 + +``` +public int select(long timeout) + throws IOException +{ + if (timeout < 0) + throw new IllegalArgumentException("Negative timeout"); + return lockAndDoSelect((timeout == 0) ? -1 : timeout); +} +``` + +- selectNow(非阻塞版本) + +``` +public int selectNow() throws IOException { + return this.lockAndDoSelect(0L); +} +``` + + + + +``` +private int lockAndDoSelect(long timeout) throws IOException { + synchronized (this) { + if (!isOpen()) + throw new ClosedSelectorException(); + synchronized (publicKeys) { + synchronized (publicSelectedKeys) { + return doSelect(timeout); + } + } + } +} +``` + + +- WindowsSelectorImpl#doSelect +- protected int doSelect(long timeout) throws IOException { + if (channelArray == null) + throw new ClosedSelectorException(); + this.timeout = timeout; // set selector timeout + processDeregisterQueue(); + if (interruptTriggered) { + resetWakeupSocket(); + return 0; + } + // Calculate number of helper threads needed for poll. If necessary + // threads are created here and start waiting on startLock + adjustThreadsCount(); + finishLock.reset(); // reset finishLock + // Wakeup helper threads, waiting on startLock, so they start polling. + // Redundant threads will exit here after wakeup. + startLock.startThreads(); + // do polling in the main thread. Main thread is responsible for + // first MAX_SELECTABLE_FDS entries in pollArray. + try { + begin(); + try { + subSelector.poll(); + } catch (IOException e) { + finishLock.setException(e); // Save this exception + } + // Main thread is out of poll(). Wakeup others and wait for them + if (threads.size() > 0) + finishLock.waitForHelperThreads(); + } finally { + end(); + } + // Done with poll(). Set wakeupSocket to nonsignaled for the next run. + finishLock.checkForException(); + processDeregisterQueue(); + int updated = updateSelectedKeys(); + // Done with poll(). Set wakeupSocket to nonsignaled for the next run. + resetWakeupSocket(); + return updated; +} + +- 其中 subSelector.poll() 是select的核心,由native函数poll0实现,readFds、writeFds 和exceptFds数组用来保存底层select的结果,数组的第一个位置都是存放发生事件的socket的总数,其余位置存放发生事件的socket句柄fd。 + +``` +private int poll() throws IOException { + return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout); +} +``` + + + + +``` +private native int poll0(long pollAddress, int numfds, + int[] readFds, int[] writeFds, int[] exceptFds, long timeout); +``` + + +- 在src/windows/native/sun/nio/ch/WindowsSelectorImpl.c中找到了该方法的实现 +- #define FD_SETSIZE 1024 + + +``` +Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject this, + jlong pollAddress, jint numfds, + jintArray returnReadFds, jintArray returnWriteFds, + jintArray returnExceptFds, jlong timeout) +{ + DWORD result = 0; + pollfd *fds = (pollfd *) pollAddress; + int i; + FD_SET readfds, writefds, exceptfds; + struct timeval timevalue, *tv; + static struct timeval zerotime = {0, 0}; + int read_count = 0, write_count = 0, except_count = 0; + +#ifdef _WIN64 + int resultbuf[FD_SETSIZE + 1]; +#endif + + if (timeout == 0) { + tv = &zerotime; + } else if (timeout < 0) { + tv = NULL; + } else { + tv = &timevalue; + tv->tv_sec = (long)(timeout / 1000); + tv->tv_usec = (long)((timeout % 1000) * 1000); + } + + /* Set FD_SET structures required for select */ + for (i = 0; i < numfds; i++) { + if (fds[i].events & POLLIN) { + readfds.fd_array[read_count] = fds[i].fd; + read_count++; + } + if (fds[i].events & (POLLOUT | POLLCONN)) + { + writefds.fd_array[write_count] = fds[i].fd; + write_count++; + } + exceptfds.fd_array[except_count] = fds[i].fd; + except_count++; + } + + readfds.fd_count = read_count; + writefds.fd_count = write_count; + exceptfds.fd_count = except_count; + + /* Call select */ + if ((result = select(0 , &readfds, &writefds, &exceptfds, tv)) + == SOCKET_ERROR) { + /* Bad error - this should not happen frequently */ + /* Iterate over sockets and call select() on each separately */ + FD_SET errreadfds, errwritefds, errexceptfds; + readfds.fd_count = 0; + writefds.fd_count = 0; + exceptfds.fd_count = 0; + for (i = 0; i < numfds; i++) { + /* prepare select structures for the i-th socket */ + errreadfds.fd_count = 0; + errwritefds.fd_count = 0; + if (fds[i].events & POLLIN) { + errreadfds.fd_array[0] = fds[i].fd; + errreadfds.fd_count = 1; + } + if (fds[i].events & (POLLOUT | POLLCONN)) + { + errwritefds.fd_array[0] = fds[i].fd; + errwritefds.fd_count = 1; + } + errexceptfds.fd_array[0] = fds[i].fd; + errexceptfds.fd_count = 1; + + /* call select on the i-th socket */ + if (select(0, &errreadfds, &errwritefds, &errexceptfds, &zerotime) + == SOCKET_ERROR) { + /* This socket causes an error. Add it to exceptfds set */ + exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd; + exceptfds.fd_count++; + } else { + /* This socket does not cause an error. Process result */ + if (errreadfds.fd_count == 1) { + readfds.fd_array[readfds.fd_count] = fds[i].fd; + readfds.fd_count++; + } + if (errwritefds.fd_count == 1) { + writefds.fd_array[writefds.fd_count] = fds[i].fd; + writefds.fd_count++; + } + if (errexceptfds.fd_count == 1) { + exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd; + exceptfds.fd_count++; + } + } + } + } + + /* Return selected sockets. */ + /* Each Java array consists of sockets count followed by sockets list */ + +#ifdef _WIN64 + resultbuf[0] = readfds.fd_count; + for (i = 0; i < (int)readfds.fd_count; i++) { + resultbuf[i + 1] = (int)readfds.fd_array[i]; + } + (*env)->SetIntArrayRegion(env, returnReadFds, 0, + readfds.fd_count + 1, resultbuf); + + resultbuf[0] = writefds.fd_count; + for (i = 0; i < (int)writefds.fd_count; i++) { + resultbuf[i + 1] = (int)writefds.fd_array[i]; + } + (*env)->SetIntArrayRegion(env, returnWriteFds, 0, + writefds.fd_count + 1, resultbuf); + + resultbuf[0] = exceptfds.fd_count; + for (i = 0; i < (int)exceptfds.fd_count; i++) { + resultbuf[i + 1] = (int)exceptfds.fd_array[i]; + } + (*env)->SetIntArrayRegion(env, returnExceptFds, 0, + exceptfds.fd_count + 1, resultbuf); +#else + (*env)->SetIntArrayRegion(env, returnReadFds, 0, + readfds.fd_count + 1, (jint *)&readfds); + + (*env)->SetIntArrayRegion(env, returnWriteFds, 0, + writefds.fd_count + 1, (jint *)&writefds); + (*env)->SetIntArrayRegion(env, returnExceptFds, 0, + exceptfds.fd_count + 1, (jint *)&exceptfds); +#endif + return 0; +} +``` + + +- 执行 selector.select() ,poll0函数把指向socket句柄和事件的内存地址传给底层函数。 +- 1、如果之前没有发生事件,程序就阻塞在select处,当然不会一直阻塞,因为epoll在timeout时间内如果没有事件,也会返回; +- 2、一旦有对应的事件发生,poll0方法就会返回; +- 3、processDeregisterQueue方法会清理那些已经cancelled的SelectionKey; +- 4、updateSelectedKeys方法统计有事件发生的SelectionKey数量,并把符合条件发生事件的SelectionKey添加到selectedKeys哈希表中,提供给后续使用。 + +- 如何判断是否有事件发生?(native) +- poll0()会监听pollWrapper中的FD有没有数据进出,这会造成IO阻塞,直到有数据读写事件发生。 +- 比如,由于pollWrapper中保存的也有ServerSocketChannel的FD,所以只要ClientSocket发一份数据到ServerSocket,那么poll0()就会返回; +- 又由于pollWrapper中保存的也有pipe的write端的FD,所以只要pipe的write端向FD发一份数据,也会造成poll0()返回; +- 如果这两种情况都没有发生,那么poll0()就一直阻塞,也就是selector.select()会一直阻塞;如果有任何一种情况发生,那么selector.select()就会返回。 + +- 在早期的JDK1.4和1.5 update10版本之前,Selector基于select/poll模型实现,是基于IO复用技术的非阻塞IO,不是异步IO。在JDK1.5 update10和linux core2.6以上版本,sun优化了Selctor的实现,底层使用epoll替换了select/poll。 +- epoll是Linux下的一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄。 +- 在Windows下是IOCP + +###### WindowsSelectorImpl.wakeup() + +``` +public Selector wakeup() { + synchronized (interruptLock) { + if (!interruptTriggered) { + setWakeupSocket(); + interruptTriggered = true; + } + } + return this; +} +``` + + + +``` +private void setWakeupSocket() { + setWakeupSocket0(wakeupSinkFd); +} +``` + + + +``` +private native void setWakeupSocket0(int wakeupSinkFd); +``` + + + +``` +Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this, + jint scoutFd) +{ + /* Write one byte into the pipe */ + const char byte = 1; + send(scoutFd, &byte, 1, 0); +} +``` + + +- 这里完成了向最开始建立的pipe的sink端写入了一个字节,source文件描述符就会处于就绪状态,poll方法会返回,从而导致select方法返回。(原来自己建立一个socket链着自己另外一个socket就是为了这个目的) + +- + +#### Java AIO 源码 +##### AsynchronousFileChannle(AIO,基于CompletionHandler回调) +- 在Java 7中,AsynchronousFileChannel被添加到Java NIO。AsynchronousFileChannel使读取数据,并异步地将数据写入文件成为可能。 +###### open +- Path path = Paths.get("data/test.xml"); + +- AsynchronousFileChannel fileChannel = +- AsynchronousFileChannel.open(path, StandardOpenOption.READ); + + +``` +public static AsynchronousFileChannel open(Path file, + Set options, + ExecutorService executor, + FileAttribute... attrs) + throws IOException +{ + FileSystemProvider provider = file.getFileSystem().provider(); + return provider.newAsynchronousFileChannel(file, options, executor, attrs); +} +``` + +- WindowsChannelFactory#newAsynchronousFileChannel +- static AsynchronousFileChannel newAsynchronousFileChannel(String pathForWindows, + String pathToCheck, + Set options, + long pSecurityDescriptor, + ThreadPool pool) + throws IOException +{ + Flags flags = Flags.toFlags(options); + + // Overlapped I/O required + flags.overlapped = true; + + // default is reading + if (!flags.read && !flags.write) { + flags.read = true; + } + + // validation + if (flags.append) + throw new UnsupportedOperationException("APPEND not allowed"); + + // open file for overlapped I/O + FileDescriptor fdObj; + try { + fdObj = open(pathForWindows, pathToCheck, flags, pSecurityDescriptor); + } catch (WindowsException x) { + x.rethrowAsIOException(pathForWindows); + return null; + } + + // create the AsynchronousFileChannel + try { + return WindowsAsynchronousFileChannelImpl.open(fdObj, flags.read, flags.write, pool); + } catch (IOException x) { + // IOException is thrown if the file handle cannot be associated + // with the completion port. All we can do is close the file. + long handle = fdAccess.getHandle(fdObj); + CloseHandle(handle); + throw x; + } + +- WindowsAsynchronousFileChannelImpl#open + +``` +public static AsynchronousFileChannel open(FileDescriptor fdo, + boolean reading, + boolean writing, + ThreadPool pool) + throws IOException +{ + Iocp iocp; + boolean isDefaultIocp; + if (pool == null) { + iocp = DefaultIocpHolder.defaultIocp; + isDefaultIocp = true; + } else { + iocp = new Iocp(null, pool).start(); + isDefaultIocp = false; + } + try { + return new + WindowsAsynchronousFileChannelImpl(fdo, reading, writing, iocp, isDefaultIocp); + } catch (IOException x) { + // error binding to port so need to close it (if created for this channel) + if (!isDefaultIocp) + iocp.implClose(); + throw x; + } +} +``` + + +###### read +###### write + +- + + +- + +#### Netty NIO +- 基于这个语境,Netty目前的版本是没有把IO操作交过操作系统处理的,所以是属于同步的。如果别人说Netty是异步非阻塞,如果要深究,那真要看看Netty新的版本是否把IO操作交过操作系统处理,或者看看有否使用JDK1.7中的AIO API,否则他们说的异步其实是指客户端程序调用Netty的IO操作API“不停顿等待”。 + +- 很多人所讲的异步其实指的是编程模型上的异步(即回调),而非应用程序的异步。 +#### NIO与Epoll +- Linux2.6之后支持epoll +- windows支持select而不支持epoll +- 不同系统下nio的实现是不一样的,包括Sunos linux 和windows +- select的复杂度为O(N) +- select有最大fd限制,默认为1024 +- 修改sys/select.h可以改变select的fd数量限制 + - epoll的事件模型,无fd数量限制,复杂度O(1),不需要遍历fd + +- + +### 动态代理 +- 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。 +- 动态代理:代理类是在运行时生成的。也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。 + +- JDK动态代理是由Java内部的反射机制+动态生成字节码来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:JDK动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,JDK动态代理不能应用。由此可以看出,JDK动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。 + +- 前者必须基于接口,后者不需要接口,是基于继承的,但是不能代理final类和final方法; +- JDK采用反射机制调用委托类的方法,CGLIB采用类似索引的方式直接调用委托类方法; +- 前者效率略低于后者效率,CGLIB效率略高(不是一定的) +#### JDK动态代理 使用 +- Proxy类(代理类)的设计用到代理模式的设计思想,Proxy类对象实现了代理目标的所有接口,并代替目标对象进行实际的操作。代理的目的是在目标对象方法的基础上作增强,这种增强的本质通常就是对目标对象的方法进行拦截。所以,Proxy应该包括一个方法拦截器,来指示当拦截到方法调用时作何种处理。InvocationHandler就是拦截器的接口。 + +- Proxy (代理) 提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。 +- 动态代理类(代理类)是一个实现在创建类时在运行时指定的接口列表的类 ,代理接口是代理类实现的一个接口。代理实例 是代理类的一个实例。 +- 每个代理实例都有一个关联的调用处理程序对象,它可以实现接口 InvocationHandler。(拦截器) + +- 在Java中怎样实现动态代理呢? +- 第一步,我们要有一个接口,还要有一个接口的实现类,而这个实现类呢就是我们要代理的对象, 所谓代理呢也就是在调用实现类的方法时,可以在方法执行前后做额外的工作。 +- 第二步,我们要自己写一个在代理类的方法要执行时,能够做额外工作的类(拦截器),而这个类必须继承InvocationHandler接口, 为什么要继承它呢?因为代理类的实例在调用实现类的方法的时候,不会调用真正的实现类的这个方法, 而是转而调用这个类的invoke方法(继承时必须实现的方法),在这个方法中你可以调用真正的实现类的这个方法。 + +#### JDK动态代理 原理 +- Proxy#newProxyInstance +- 会返回一个实现了指定接口的代理对象,对该对象的所有方法调用都会转发给InvocationHandler.invoke()方法。 + +``` +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException +{ + Objects.requireNonNull(h); + + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } + + /* + * Look up or generate the designated proxy class. + */ +``` + + +``` +// 生成代理类的class + Class cl = getProxyClass0(loader, intfs); + + /* + * Invoke its constructor with the designated invocation handler. + */ + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } + // 获取代理对象的构造方法(也就是$Proxy0(InvocationHandler h))    + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + cons.setAccessible(true); + return null; + } + }); + } +``` + +- // 生成代理类的实例并把InvocationHandlerImpl的实例传给它的构造方法 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) { + throw new InternalError(e.toString(), e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new InternalError(t.toString(), t); + } + } catch (NoSuchMethodException e) { + throw new InternalError(e.toString(), e); + } +} + +##### 1)getProxyClass0(生成代理类的class) +- 最终生成是通过ProxyGenerator的generateProxyClass方法实现的。 + +``` +private static Class getProxyClass0(ClassLoader loader, + Class... interfaces) { + if (interfaces.length > 65535) { + throw new IllegalArgumentException("interface limit exceeded"); + } + + // If the proxy class defined by the given loader implementing + // the given interfaces exists, this will simply return the cached copy; + // otherwise, it will create the proxy class via the ProxyClassFactory + return proxyClassCache.get(loader, interfaces); +} +``` + + + +``` +private static final WeakCache[], Class> + proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); +``` + + +- * @param type of keys + * @param

type of parameters + * @param type of values + */ +final class WeakCache {} + + +``` +public V get(K key, P parameter) { + Objects.requireNonNull(parameter); + + expungeStaleEntries(); + + Object cacheKey = CacheKey.valueOf(key, refQueue); + + // lazily install the 2nd level valuesMap for the particular cacheKey + ConcurrentMap> valuesMap = map.get(cacheKey); + if (valuesMap == null) { + ConcurrentMap> oldValuesMap + = map.putIfAbsent(cacheKey, + valuesMap = new ConcurrentHashMap<>()); + if (oldValuesMap != null) { + valuesMap = oldValuesMap; + } + } + + // create subKey and retrieve the possible Supplier stored by that + // subKey from valuesMap + Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter)); + Supplier supplier = valuesMap.get(subKey); + Factory factory = null; + + while (true) { + if (supplier != null) { + // supplier might be a Factory or a CacheValue instance +``` + +- // supplier是Factory,这个类定义在WeakCache的内部。 + V value = supplier.get(); + if (value != null) { + return value; + } + } + // else no supplier in cache + // or a supplier that returned null (could be a cleared CacheValue + // or a Factory that wasn't successful in installing the CacheValue) + + // lazily construct a Factory + if (factory == null) { + factory = new Factory(key, parameter, subKey, valuesMap); + } + + if (supplier == null) { + supplier = valuesMap.putIfAbsent(subKey, factory); + if (supplier == null) { + // successfully installed Factory + supplier = factory; + } + // else retry with winning supplier + } else { + if (valuesMap.replace(subKey, supplier, factory)) { + // successfully replaced + // cleared CacheEntry / unsuccessful Factory + // with our Factory + supplier = factory; + } else { + // retry with current supplier + supplier = valuesMap.get(subKey); + } + } + } +} + +- Factory + +``` +private final class Factory implements Supplier { + + private final K key; + private final P parameter; + private final Object subKey; + private final ConcurrentMap> valuesMap; + + Factory(K key, P parameter, Object subKey, + ConcurrentMap> valuesMap) { + this.key = key; + this.parameter = parameter; + this.subKey = subKey; + this.valuesMap = valuesMap; + } + + @Override + public synchronized V get() { // serialize access + // re-check + Supplier supplier = valuesMap.get(subKey); + if (supplier != this) { + // something changed while we were waiting: + // might be that we were replaced by a CacheValue + // or were removed because of failure -> + // return null to signal WeakCache.get() to retry + // the loop + return null; + } + // else still us (supplier == this) + + // create new value +``` + +- // 创建新的class + V value = null; + try { + value = Objects.requireNonNull(valueFactory.apply(key, parameter)); + } finally { + if (value == null) { // remove us on failure + valuesMap.remove(subKey, this); + } + } + // the only path to reach here is with non-null value + assert value != null; + + // wrap value with CacheValue (WeakReference) + CacheValue cacheValue = new CacheValue<>(value); + + // try replacing us with CacheValue (this should always succeed) + if (valuesMap.replace(subKey, this, cacheValue)) { + // put also in reverseMap + reverseMap.put(cacheValue, Boolean.TRUE); + } else { + throw new AssertionError("Should not reach here"); + } + + // successfully replaced us with new CacheValue -> return the value + // wrapped by it + return value; + } +} + + + +``` +private static final class ProxyClassFactory + implements BiFunction[], Class> +{ + // prefix for all proxy class names + private static final String proxyClassNamePrefix = "$Proxy"; + + // next number to use for generation of unique proxy class names + private static final AtomicLong nextUniqueNumber = new AtomicLong(); + + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + for (Class intf : interfaces) { + /* + * Verify that the class loader resolves the name of this + * interface to the same Class object. + */ + Class interfaceClass = null; + try { + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + /* + * Verify that the Class object actually represents an + * interface. + */ + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + /* + * Verify that this interface is not a duplicate. + */ + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } + + String proxyPkg = null; // package to define proxy class in + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; + + /* + * Record the package of a non-public proxy interface so that the + * proxy class will be defined in the same package. Verify that + * all non-public proxy interfaces are in the same package. + */ + for (Class intf : interfaces) { + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + String name = intf.getName(); + int n = name.lastIndexOf('.'); + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } + + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + /* + * Choose a name for the proxy class to generate. + */ + long num = nextUniqueNumber.getAndIncrement(); + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + /* + * Generate the specified proxy class. + */ + byte[] proxyClassFile = ProxyGenerator.generateProxyClass( + proxyName, interfaces, accessFlags); + try { + return defineClass0(loader, proxyName, + proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { + /* + * A ClassFormatError here means that (barring bugs in the + * proxy class generation code) there was some other + * invalid aspect of the arguments supplied to the proxy + * class creation (such as virtual machine limitations + * exceeded). + */ + throw new IllegalArgumentException(e.toString()); + } + } +} +``` + + +- 重点! + +``` +/** + * Generate a proxy class given a name and a list of proxy interfaces. + * + * @param name the class name of the proxy class + * @param interfaces proxy interfaces + * @param accessFlags access flags of the proxy class +*/ +public static byte[] generateProxyClass(final String name, + Class[] interfaces, + int accessFlags) +{ + ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags); + final byte[] classFile = gen.generateClassFile(); + + if (saveGeneratedFiles) { + java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Void run() { + try { + int i = name.lastIndexOf('.'); + Path path; + if (i > 0) { + Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar)); + Files.createDirectories(dir); + path = dir.resolve(name.substring(i+1, name.length()) + ".class"); + } else { + path = Paths.get(name + ".class"); + } + Files.write(path, classFile); + return null; + } catch (IOException e) { + throw new InternalError( + "I/O exception saving generated file: " + e); + } + } + }); + } + + return classFile; +} +``` + +- ProxyGenerator#generateClassFIle + + +``` +/** + * Generate a class file for the proxy class. This method drives the + * class file generation process. + */ +private byte[] generateClassFile() { + + /* ============================================================ + * Step 1: Assemble ProxyMethod objects for all methods to + * generate proxy dispatching code for. + */ + + /* + * Record that proxy methods are needed for the hashCode, equals, + * and toString methods of java.lang.Object. This is done before + * the methods from the proxy interfaces so that the methods from + * java.lang.Object take precedence over duplicate methods in the + * proxy interfaces. + */ + addProxyMethod(hashCodeMethod, Object.class); + addProxyMethod(equalsMethod, Object.class); + addProxyMethod(toStringMethod, Object.class); + + /* + * Now record all of the methods from the proxy interfaces, giving + * earlier interfaces precedence over later ones with duplicate + * methods. + */ + for (Class intf : interfaces) { + for (Method m : intf.getMethods()) { + addProxyMethod(m, intf); + } + } + + /* + * For each set of proxy methods with the same signature, + * verify that the methods' return types are compatible. + */ + for (List sigmethods : proxyMethods.values()) { + checkReturnTypes(sigmethods); + } + + /* ============================================================ + * Step 2: Assemble FieldInfo and MethodInfo structs for all of + * fields and methods in the class we are generating. + */ + try { + methods.add(generateConstructor()); + + for (List sigmethods : proxyMethods.values()) { + for (ProxyMethod pm : sigmethods) { + + // add static field for method's Method object + fields.add(new FieldInfo(pm.methodFieldName, + "Ljava/lang/reflect/Method;", + ACC_PRIVATE | ACC_STATIC)); + + // generate code for proxy method and add it + methods.add(pm.generateMethod()); + } + } + + methods.add(generateStaticInitializer()); + + } catch (IOException e) { + throw new InternalError("unexpected I/O Exception", e); + } + + if (methods.size() > 65535) { + throw new IllegalArgumentException("method limit exceeded"); + } + if (fields.size() > 65535) { + throw new IllegalArgumentException("field limit exceeded"); + } + + /* ============================================================ + * Step 3: Write the final class file. + */ + + /* + * Make sure that constant pool indexes are reserved for the + * following items before starting to write the final class file. + */ + cp.getClass(dotToSlash(className)); + cp.getClass(superclassName); + for (Class intf: interfaces) { + cp.getClass(dotToSlash(intf.getName())); + } + + /* + * Disallow new constant pool additions beyond this point, since + * we are about to write the final constant pool table. + */ + cp.setReadOnly(); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream dout = new DataOutputStream(bout); + + try { + /* + * Write all the items of the "ClassFile" structure. + * See JVMS section 4.1. + */ + // u4 magic; + dout.writeInt(0xCAFEBABE); + // u2 minor_version; + dout.writeShort(CLASSFILE_MINOR_VERSION); + // u2 major_version; + dout.writeShort(CLASSFILE_MAJOR_VERSION); + + cp.write(dout); // (write constant pool) + + // u2 access_flags; + dout.writeShort(accessFlags); + // u2 this_class; + dout.writeShort(cp.getClass(dotToSlash(className))); + // u2 super_class; + dout.writeShort(cp.getClass(superclassName)); + + // u2 interfaces_count; + dout.writeShort(interfaces.length); + // u2 interfaces[interfaces_count]; + for (Class intf : interfaces) { + dout.writeShort(cp.getClass( + dotToSlash(intf.getName()))); + } + + // u2 fields_count; + dout.writeShort(fields.size()); + // field_info fields[fields_count]; + for (FieldInfo f : fields) { + f.write(dout); + } + + // u2 methods_count; + dout.writeShort(methods.size()); + // method_info methods[methods_count]; + for (MethodInfo m : methods) { + m.write(dout); + } + + // u2 attributes_count; + dout.writeShort(0); // (no ClassFile attributes for proxy classes) + + } catch (IOException e) { + throw new InternalError("unexpected I/O Exception", e); + } + + return bout.toByteArray(); +} +``` + + +##### 2)getConstructor(获取代理类的构造方法) +##### 3)newInstance(初始化代理对象) + +#### CGLIB动态代理 使用 +- CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理。 +- CGLIB的核心类: +- net.sf.cglib.proxy.Enhancer – 主要的增强类 +- net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现 +- net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用,如使用: + +- Object o = methodProxy.invokeSuper(proxy, args);//虽然第一个参数是被代理对象,也不会出现死循环的问题。 +- net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调(callback)类型,它经常被基于代理的AOP用来实现拦截(intercept)方法的调用。这个接口只定义了一个方法 + +``` +public Object intercept(Object object, java.lang.reflect.Method method, +``` + +- Object[] args, MethodProxy proxy) throws Throwable; +- 第一个参数是代理对像,第二和第三个参数分别是拦截的方法和方法的参数。原来的方法可能通过使用java.lang.reflect.Method对象的一般反射调用,或者使用 net.sf.cglib.proxy.MethodProxy对象调用。net.sf.cglib.proxy.MethodProxy通常被首选使用,因为它更快。 + +``` +public class CglibProxy implements MethodInterceptor { +``` + +- @Override + +``` + public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { +``` + +- System.out.println("++++++before " + methodProxy.getSuperName() + "++++++"); +- System.out.println(method.getName()); +- Object o1 = methodProxy.invokeSuper(o, args); +- System.out.println("++++++before " + methodProxy.getSuperName() + "++++++"); +- return o1; +- } +- } + + +``` +public class Main { +``` + + +``` + public static void main(String[] args) { +``` + +- CglibProxy cglibProxy = new CglibProxy(); +- +- Enhancer enhancer = new Enhancer(); +- enhancer.setSuperclass(UserServiceImpl.class); +- enhancer.setCallback(cglibProxy); +- +- UserService o = (UserService)enhancer.create(); + - o.getName(1); + - o.getAge(1); +- } +- } +- 我们通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是HelloConcrete的具体方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很类似,都是方法调用的中转站。 +- 注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不会,因为它是final方法,CGLIB无法代理。 +- 既然是继承就不得不考虑final的问题。我们知道final类型不能有子类,所以CGLIB不能代理final类型。 +- final方法是不能重载的,所以也不能通过CGLIB代理,遇到这种情况不会抛异常,而是会跳过final方法只代理其他方法。 + +#### CGLIB动态代理 原理 +- 1、生成代理类Class的二进制字节码(基于ASM); +- 2、通过 Class.forName加载二进制字节码,生成Class对象; +- 3、通过反射机制获取实例构造,并初始化代理类对象。 + +- 调用委托类的方法是使用invokeSuper + +``` +public Object invokeSuper(Object obj, Object[] args) throws Throwable { + try { + init(); + FastClassInfo fci = fastClassInfo; + return fci.f2.invoke(fci.i2, obj, args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } +} +``` + + + +``` +private static class FastClassInfo +{ + FastClass f1; + FastClass f2; + int i1; + int i2; +} +``` + +- f1指向委托类对象,f2指向代理类对象 +- i1是被代理的方法在对象中的索引位置 +- i2是CGLIB$被代理的方法$0在对象中的索引位置 +##### FastClass实现机制 +- FastClass其实就是对Class对象进行特殊处理,提出下标概念index,通过索引保存方法的引用信息,将原先的反射调用,转化为方法的直接调用,从而体现所谓的fast。 + + + +- 在FastTest中有两个方法, getIndex中对Test类的每个方法根据hash建立索引, invoke根据指定的索引,直接调用目标方法,避免了反射调用。 + + +- + +### 反射 +- Java的动态性体现在:反射机制、动态执行脚本语言、动态操作字节码 +- 反射:在运行时加载、探知、使用编译时未知的类。 + +- Class.forName使用的类加载器是调用者的类加载器 +#### Class +- 表示Java中的类型(class、interface、enum、annotation、primitive type、void)本身。 + + +- 一个类被加载之后,JVM会创建一个对应该类的Class对象,类的整个结构信息会放在相应的Class对象中。 +- 这个Class对象就像一个镜子一样,从中可以看到类的所有信息。 +- 反射的核心就是Class + +- 如果多次执行forName等加载类的方法,类只会被加载一次;一个类只会形成一个Class对象,无论执行多少次加载类的方法,获得的Class都是一样的。 +#### 用途 + +#### 性能 +- 反射带来灵活性的同时,也有降低程序执行效率的弊端 +- setAccessible方法不仅可以标记某些私有的属性方法为可访问的属性方法,并且可以提高程序的执行效率 + +- 实际上是启用和禁用访问安全检查的开关。如果做检查就会降低效率;关闭检查就可以提高效率。 +- 反射调用方法比直接调用要慢大约30倍,如果跳过安全检查的话比直接调用要慢大约7倍 +- 开启和不开启安全检查对于反射而言可能会差4倍的执行效率。 +- 为什么慢? + - 1)验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证 + - 2)产生很多临时对象,造成GC与计算时间消耗 + - 3)由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一) +- 当然,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。 +#### 实现 +- 反射在Java中可以直接调用,不过最终调用的仍是native方法,以下为主流反射操作的实现。 + +##### Class.forName的实现 +- Class.forName可以通过包名寻找Class对象,比如Class.forName("java.lang.String")。 +- 在JDK的源码实现中,可以发现最终调用的是native方法forName0(),它在JVM中调用的实际是FindClassFromCaller(),原理与ClassLoader的流程一样。 + +``` +public static Class forName(String className) + throws ClassNotFoundException { + Class caller = Reflection.getCallerClass(); + return forName0(className, true, ClassLoader.getClassLoader(caller), caller); +} +``` + + +``` +private static native Class forName0(String name, boolean initialize, + ClassLoader loader, + Class caller) + throws ClassNotFoundException; +``` + + + + +``` +Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname, + jboolean initialize, jobject loader, jclass caller) +{ + char *clname; + jclass cls = 0; + char buf[128]; + jsize len; + jsize unicode_len; + + if (classname == NULL) { + JNU_ThrowNullPointerException(env, 0); + return 0; + } + + len = (*env)->GetStringUTFLength(env, classname); + unicode_len = (*env)->GetStringLength(env, classname); + if (len >= (jsize)sizeof(buf)) { + clname = malloc(len + 1); + if (clname == NULL) { + JNU_ThrowOutOfMemoryError(env, NULL); + return NULL; + } + } else { + clname = buf; + } + (*env)->GetStringUTFRegion(env, classname, 0, unicode_len, clname); + + if (VerifyFixClassname(clname) == JNI_TRUE) { + /* slashes present in clname, use name b4 translation for exception */ + (*env)->GetStringUTFRegion(env, classname, 0, unicode_len, clname); + JNU_ThrowClassNotFoundException(env, clname); + goto done; + } + + if (!VerifyClassname(clname, JNI_TRUE)) { /* expects slashed name */ + JNU_ThrowClassNotFoundException(env, clname); + goto done; + } + + cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller); + + done: + if (clname != buf) { + free(clname); + } + return cls; +} +``` + + + +- JVM_ENTRY(jclass, JVM_FindClassFromClass(JNIEnv *env, const char *name, + jboolean init, jclass from)) + JVMWrapper2("JVM_FindClassFromClass %s", name); + if (name == NULL || (int)strlen(name) > Symbol::max_length()) { + // It's impossible to create this class; the name cannot fit + // into the constant pool. + THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name); + } + TempNewSymbol h_name = SymbolTable::new_symbol(name, CHECK_NULL); + oop from_class_oop = JNIHandles::resolve(from); + Klass* from_class = (from_class_oop == NULL) + ? (Klass*)NULL + : java_lang_Class::as_Klass(from_class_oop); + oop class_loader = NULL; + oop protection_domain = NULL; + if (from_class != NULL) { + class_loader = from_class->class_loader(); + protection_domain = from_class->protection_domain(); + } + Handle h_loader(THREAD, class_loader); + Handle h_prot (THREAD, protection_domain); + jclass result = find_class_from_class_loader(env, h_name, init, h_loader, + h_prot, true, thread); + + if (TraceClassResolution && result != NULL) { + // this function is generally only used for class loading during verification. + ResourceMark rm; + oop from_mirror = JNIHandles::resolve_non_null(from); + Klass* from_class = java_lang_Class::as_Klass(from_mirror); + const char * from_name = from_class->external_name(); + + oop mirror = JNIHandles::resolve_non_null(result); + Klass* to_class = java_lang_Class::as_Klass(mirror); + const char * to = to_class->external_name(); + tty->print("RESOLVE %s %s (verification)\n", from_name, to); + } + + return result; +JVM_END + +- + +##### getDeclaredFields的实现 +- 在JDK源码中,可以知道class.getDeclaredFields()方法实际调用的是native方法getDeclaredFields0(),它在JVM主要实现步骤如下: + - 1)根据Class结构体信息,获取field_count与fields[]字段,这个字段早已在load过程中被放入了 + - 2)根据field_count的大小分配内存、创建数组 + - 3)将数组进行forEach循环,通过fields[]中的信息依次创建Object对象 + - 4)返回数组指针 + +- 主要慢在如下方面: +- 创建、计算、分配数组对象 +- 对字段进行循环赋值 + + +``` +public Field[] getDeclaredFields() throws SecurityException { + checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true); + return copyFields(privateGetDeclaredFields(false)); +} +``` + + + +``` +private Field[] privateGetDeclaredFields(boolean publicOnly) { + checkInitted(); + Field[] res; + ReflectionData rd = reflectionData(); + if (rd != null) { + res = publicOnly ? rd.declaredPublicFields : rd.declaredFields; + if (res != null) return res; + } + // No cached value available; request value from VM + res = Reflection.filterFields(this, getDeclaredFields0(publicOnly)); + if (rd != null) { + if (publicOnly) { + rd.declaredPublicFields = res; + } else { + rd.declaredFields = res; + } + } + return res; +} +``` + + + +``` +private static Field[] copyFields(Field[] arg) { + Field[] out = new Field[arg.length]; + ReflectionFactory fact = getReflectionFactory(); + for (int i = 0; i < arg.length; i++) { + out[i] = fact.copyField(arg[i]); + } + return out; +} +``` + + +- + +##### Method.invoke的实现 +- 以下为无同步、无异常的情况下调用的步骤 + + - 1)创建Frame + - 2)如果对象flag为native,交给native_handler进行处理 + - 3)在frame中执行java代码 + - 4)弹出Frame + - 5)返回执行结果的指针 + +- 主要慢在如下方面: + +- 需要完全执行ByteCode而缺少JIT等优化 +- 检查参数非常多,这些本来可以在编译器或者加载时完成 + + +``` +public Object invoke(Object obj, Object... args) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException +{ + if (!override) { + if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { + Class caller = Reflection.getCallerClass(); + checkAccess(caller, clazz, obj, modifiers); + } + } + MethodAccessor ma = methodAccessor; // read volatile + if (ma == null) { + ma = acquireMethodAccessor(); + } + return ma.invoke(obj, args); +} +``` + + +- NativeMethodAccessorImpl#invoke + +``` +public Object invoke(Object obj, Object[] args) + throws IllegalArgumentException, InvocationTargetException +{ + // We can't inflate methods belonging to vm-anonymous classes because + // that kind of class can't be referred to by name, hence can't be + // found from the generated bytecode. + if (++numInvocations > ReflectionFactory.inflationThreshold() + && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { + MethodAccessorImpl acc = (MethodAccessorImpl) + new MethodAccessorGenerator(). + generateMethod(method.getDeclaringClass(), + method.getName(), + method.getParameterTypes(), + method.getReturnType(), + method.getExceptionTypes(), + method.getModifiers()); + parent.setDelegate(acc); + } + + return invoke0(method, obj, args); +} +``` + + + +``` +private static native Object invoke0(Method m, Object obj, Object[] args); +``` + + +- Java_sun_reflect_NativeMethodAccessorImpl_invoke0 +(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args) +{ + return JVM_InvokeMethod(env, m, obj, args); +} + + +- JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0)) + JVMWrapper("JVM_InvokeMethod"); + Handle method_handle; + if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) { + method_handle = Handle(THREAD, JNIHandles::resolve(method)); + Handle receiver(THREAD, JNIHandles::resolve(obj)); + objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0))); + oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL); + jobject res = JNIHandles::make_local(env, result); + if (JvmtiExport::should_post_vm_object_alloc()) { + oop ret_type = java_lang_reflect_Method::return_type(method_handle()); + assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!"); + if (java_lang_Class::is_primitive(ret_type)) { + // Only for primitive type vm allocates memory for java object. + // See box() method. + JvmtiExport::post_vm_object_alloc(JavaThread::current(), result); + } + } + return res; + } else { + THROW_0(vmSymbols::java_lang_StackOverflowError()); + } +JVM_END + +- + +##### class.newInstance的实现 + - 1)检测权限、预分配空间大小等参数 + - 2)创建Object对象,并分配空间 + - 3)通过Method.invoke调用构造函数(()) + - 4)返回Object指针 + +- 主要慢在如下方面: + +- 参数检查不能优化或者遗漏 +- ()的查表 +- Method.invoke本身耗时 + +``` +public T newInstance() + throws InstantiationException, IllegalAccessException +{ + if (System.getSecurityManager() != null) { + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false); + } + + // NOTE: the following code may not be strictly correct under + // the current Java memory model. + + // Constructor lookup + if (cachedConstructor == null) { + if (this == Class.class) { + throw new IllegalAccessException( + "Can not call newInstance() on the Class for java.lang.Class" + ); + } + try { + Class[] empty = {}; + final Constructor c = getConstructor0(empty, Member.DECLARED); + // Disable accessibility checks on the constructor + // since we have to do the security check here anyway + // (the stack depth is wrong for the Constructor's + // security check to work) + java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Void run() { + c.setAccessible(true); + return null; + } + }); + cachedConstructor = c; + } catch (NoSuchMethodException e) { + throw (InstantiationException) + new InstantiationException(getName()).initCause(e); + } + } + Constructor tmpConstructor = cachedConstructor; + // Security check (same as in java.lang.reflect.Constructor) + int modifiers = tmpConstructor.getModifiers(); + if (!Reflection.quickCheckMemberAccess(this, modifiers)) { + Class caller = Reflection.getCallerClass(); + if (newInstanceCallerCache != caller) { + Reflection.ensureMemberAccess(caller, this, null, modifiers); + newInstanceCallerCache = caller; + } + } + // Run constructor + try { + return tmpConstructor.newInstance((Object[])null); + } catch (InvocationTargetException e) { + Unsafe.getUnsafe().throwException(e.getTargetException()); + // Not reached + return null; + } +} +``` + + +- + +### XML +#### DOM +- OM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。由于它是基于信息层次的,因而DOM被认为是基于树或基于对象的。 + +- 优点 +- ①允许应用程序对数据和结构做出更改。 +- ②访问是双向的,可以在任何时候在树中上下导航,获取和操作任意部分的数据。 +- 缺点 +- ①通常需要加载整个XML文档来构造层次结构,消耗资源大。 + +- +#### SAX +- SAX处理的优点非常类似于流媒体的优点。分析能够立即开始,而不是等待所有的数据被处理。而且,由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中。这对于大型文档来说是个巨大的优点。事实上,应用程序甚至不必解析整个文档;它可以在某个条件得到满足时停止解析。一般来说,SAX还比它的替代者DOM快许多。 +- 优点 +- ①不需要等待所有数据都被处理,分析就能立即开始。 +- ②只在读取数据时检查数据,不需要保存在内存中。 +- ③可以在某个条件得到满足时停止解析,不必解析整个文档。 +- ④效率和性能较高,能解析大于系统内存的文档。 + +- 缺点 +- ①需要应用程序自己负责TAG的处理逻辑(例如维护父/子关系等),文档越复杂程序就越复杂。 +- ②单向导航,无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持XPath。 + +- JDOM +- DOM4J + +- + +### Java8 +#### Lambda表达式&函数式接口&方法引用&Stream API +- Java8 stream迭代的优势和区别;lambda表达式?为什么要引入它 + + - 1)流(高级Iterator):对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation),隐式迭代等,代码简洁 + - 2)方便地实现并行(并行流),比如实现MapReduce + - 3)Lamdba:简化匿名内部类的实现,代码更加紧凑 + - 4)方法引用:方法引用是lambda表达式的另一种表达方式 +- 对象::实例方法 +- 类::静态方法 +- 类::实例方法名 + +#### Optional +- Optional仅仅是一个容易:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查。 +#### CompletableFuture + - 1)实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果) + - 2)将批量同步操作转为异步操作(并行流/CompletableFuture) + - 3)多个异步任务合并 +#### 时间日期API +- 新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。 + +#### 接口中的默认方法与静态方法 +- 默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。 +- 默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。 + + +- + +### Java9 +#### 模块化 +- 提供了类似于OSGI框架的功能,模块之间存在相互的依赖关系,可以导出一个公共的API,并且隐藏实现的细节,Java提供该功能的主要的动机在于,减少内存的开销,在JVM启动的时候,至少会有30~60MB的内存加载,主要原因是JVM需要加载rt.jar,不管其中的类是否被classloader加载,第一步整个jar都会被JVM加载到内存当中去,模块化可以根据模块的需要加载程序运行需要的class。 +- 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。使得JDK可以在更小的设备中使用。采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非是整个JDK框架了。 +#### HTTP/2 +- Java 9的版本中引入了一个新的package:java.net.http,里面提供了对Http访问很好的支持,不仅支持Http1.1而且还支持HTTP/2,以及WebSocket,据说性能特别好。 +#### JShell +- java9引入了jshell这个交互性工具,让Java也可以像脚本语言一样来运行,可以从控制台启动 jshell ,在 jshell 中直接输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。 +- 除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。 +#### 不可变集合工厂方法 +- Java 9增加了List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法来创建不可变集合。 +#### 私有接口方法 +- Java 8 为我们提供了接口的默认方法和静态方法,接口也可以包含行为,而不仅仅是方法定义。 +- 默认方法和静态方法可以共享接口中的私有方法,因此避免了代码冗余,这也使代码更加清晰。如果私有方法是静态的,那这个方法就属于这个接口的。并且没有静态的私有方法只能被在接口中的实例调用。 +#### 多版本兼容 JAR + - 当一个新版本的 Java 出现的时候,你的库用户要花费很长时间才会切换到这个新的版本。这就意味着库要去向后兼容你想要支持的最老的 Java 版本 (许多情况下就是 Java 6 或者 7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本。 + +#### 统一 JVM 日志 +- Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等。 +#### 垃圾收集机制 +- Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时把G1设为默认的垃圾回收器实现。替代了之前默认使用的Parallel GC,对于这个改变,evens的评论是酱紫的:这项变更是很重要的,因为相对于Parallel来说,G1会在应用线程上做更多的事情,而Parallel几乎没有在应用线程上做任何事情,它基本上完全依赖GC线程完成所有的内存管理。这意味着切换到G1将会为应用线程带来额外的工作,从而直接影响到应用的性能 + +#### I/O 流新特性 +- java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。 +- readAllBytes:读取 InputStream 中的所有剩余字节。 +- readNBytes: 从 InputStream 中读取指定数量的字节到数组中。 +- transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。 + +- + +## 设计模式 +### 设计原则 +#### 单一职责原则 +- 不要存在多于一个导致类变更的原因。 +- 总结:一个类只负责一项职责。 +#### 里氏替换原则 +- 1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 +- 2.子类中可以增加自己特有的方法。 +- 3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 +- 4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 +- 总结:所有引用父类的地方必须能透明地使用其子类对象 +#### 依赖倒置原则/面向接口编程 +- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 +#### 接口隔离原则 +- 使用多个专门的接口来替代一个统一的接口; +- 一个类对另一个类的依赖应建立在最小的接口上 +#### 迪米特法则 +- 一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部 +#### 开闭原则 +- 对扩展开放,对修改关闭 +- 用抽象构建框架,用实现扩展细节 +#### 合成复用原则/组合优于继承 +- 尽量多使用组合和聚合,尽量少使用甚至不使用继承关系 +- + +### 分类 +- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 +- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 +- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 + +### 创建型设计模式 +#### 工厂方法模式 +##### 介绍 +- 工厂模式分为简单(静态)工厂模式、工厂方法模式和抽象工厂模式 + + - 1) 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。 + - 2) 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。 + - 3) 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。 + +- 简单工厂模式:一个工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, +- 工厂方法模式:一个抽象产品类,可以派生出多个具体产品类。    +- 一个抽象工厂类,可以派生出多个具体工厂类。 +-    每个具体工厂类只能创建一个具体产品类的实例。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)客户不需要知道要使用的对象的创建过程 + - 2)客户使用的对象存在变动的可能,或者根本就不知道使用哪一个具体对象 +- 缺点: +- 类的数量膨胀 +- + +#### 抽象工厂模式 +##### 介绍 +- 抽象工厂模式:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。 +-    一个抽象工厂类,可以派生出多个具体工厂类。 +-    每个具体工厂类可以创建多个具体产品类的实例。 +- 区别: 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。 +-    工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)系统中有多个产品族,而系统一次只能消费其中一族产品 + - 2)同属于同一个产品族的产品一起使用 + +- + +#### 单例模式 +##### 介绍 +- 通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 +##### UML + +##### 适用场景与优缺点 +- 使用场景: + - 1)当类只有一个实例且客户可以从一个众所周知的访问点 访问它 + - 2)当这个唯一实例应该是通过子类化可扩展的,且客户应该无序更改代码就能使用一个扩展的实例 + +- 优点: + - 1)对唯一实例的受控访问 + - 2)缩小命名空间,避免命名污染 + - 3)允许单例有子类 + - 4)允许可变数目的实例 + + +``` +public class Car{ +``` + +- //懒汉式,线程不安全 + +``` + private static Car instance; +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- if(instance == null) { +- instance = new Car(); +- } +- return instance; +- } +- 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。 + +- //懒汉式,线程安全 + +``` + private static Car instance ; +``` + + +``` + private Car() {} +``` + + +``` + public static synchronized Car getInstance(){ +``` + +- if(instance == null) { +- instance = new Car(); +- } +- return instance; +- } +- 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。 + +- //饿汉式 + +``` + private static Car instance = new Car(); +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return instance; +- } +- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。 + +- //饿汉式变种 + +``` + private static Car instance; +``` + +- static { +- instance = new Car(); +- } + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return instance; +- } +- 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。 +- +- //静态内部类(类的加载是线程安全的) + +``` + private static class CarHolder{ +``` + + +``` + private static final Car instance = new Car(); +``` + +- } + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return CarHolder.instance; +- } +- 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。 +- +- // 枚举 + +``` +public enum Car { +``` + +- INSTANCE; +- } +- //双重校验锁 + +``` + private volatile static Car instance; +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- if(instance == null) { +- synchronized(Car.class) { +- if(instance == null) { +- instance = new Car(); +- } +- } +- } +- return instance; +- } +- 这个是第二种方式的升级版,俗称双重检查锁定。 +- 所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步, 而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块, 这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。 +- 双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile +- 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。 +- 说明:由于volatile关键字可能会屏蔽掉虚拟机中的一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用”双重检查加锁“机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。 +- } +- + +#### 建造者模式 +##### 介绍 +- 将一个复杂对象的创建和它的表示分离,使得同样的创建过程可以创建不同的表示。 + +- Builder用于构建组件 +- Director负责装配 +- 客户端通过Director来获得最终产品,Director与Builder打交道,持有一个Builder的引用。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的赚个屁方式时 + - 2)当构造过程必须允许被构造的对象有不同的表示时 + +- 优点: + - 1)可以改变一个对象的内部表示:Builder对象提供给Director一个构造产品的抽象接口,该接口使得Buildewr可以隐藏这个产品的表示和内部结构,同时隐藏了该产品是如何装配的。 + - 2)将构造代码与表示代码分离 + - 3)可以对构造过程进行更精细化的控制 +- + +#### 原型模式 +##### 介绍 + +##### UML + +##### 适用场景与优缺点 +- • 当要实例化的类是在运行时刻指定时,例如,通过动态装载; +- • 为了避免创建一个与产品类层次平行的工厂类层次时; +- • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。 + +- 优点: +- 性能优良。原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。 +- 缺点: +- 逃避构造函数的约束。这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。 + +- + +### 结构型设计模式 +#### 适配器模式 +##### 介绍 +- 将一个类的接口转换成客户所期待的另一种接口 + +- Adapter可以组合+实现(对象适配器方式),也可以继承+实现(类适配器方式)。但是继承不如组合好,因此尽量使用组合+实现。 + +##### UML + + +##### 适用场景与优缺点 +- 适用场景: + - 1)想使用一个已存在的类,而它的接口不符合你的需求 + - 2)想创建一个可以复用的类,该类可以与不相关的类或不可预见的类协同工作 +#### 装饰器模式 +##### 介绍 + + +- + +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)在不影响其他对象的情况下,以动态透明的方式给单个对象添加职责 + - 2)处理那些可以撤销的职责 + - 3)当不能通过生成子类的方法进行扩充时 +- 优点: + - 1)比继承更加灵活,可以用添加和分离的方式,用装饰在运行时 增加和删除职责 + - 2)避免在层次结构高的类有太多特征,用装饰器为其逐渐地添加功能 +- + +#### 代理模式 +##### 介绍 +- 代理可以分为静态代理和动态代理 +- 为其他对象提供一种代理以控制对这个对象的访问。 +为了一个对象提供一个替身或者占位符,以控制对这个对象的访问 + +- * 远程代理能够控制访问远程对象(RMI) +- * 虚拟代理控制访问创建开销大的资源(先创建一个资源消耗较小的对象表示,真实对象只在需要时才会被创建) +- * 保护代理基于权限控制对资源的访问 +##### UML + +##### 适用场景与优缺点 +- 使用场景: +- 按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。 + +- 优点: +- 1、职责清晰。 2、高扩展性。 3、智能化。 +- 缺点: +- 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 +- 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。 +#### 外观模式 +##### 介绍 +- 为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这一接口使得子系统更加容易使用。 + +- 遵循了迪米特法则: + +``` +通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。 +``` + +- 外观模式就是一种较好的封装 +- 是整体和子组件之间的关系,外部类不应该与一个类的子组件过多的接触,应该尽可能与整体打交道。 + +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)为一个复杂子系统提供一个简单接口 + - 2)客户与抽象类的实现部分之间存在着很大的依赖性,引入Facade将子系统与客户解耦,提高了子系统的独立性和可移植性 +- 优点: + - 1)对客户屏蔽了子系统组件,减少客户处理的对象数目,并使得子系统使用起来更加容易 + - 2)实现了子系统与客户之间的松耦合 + - 3)降低了大型软件中的编译依赖性 + - 4)只是提供了一个访问子系统的统一入口,并不影响客户直接使用子系统 +#### 桥接模式 +##### 介绍 +- 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立地扩展,在抽象层建立关联。 +- 一个维度的父类持有另一个维度的接口的引用(使用组合代替了继承) + + +- 希望有一个Bridge类来将类型维度和品牌维度连接起来,这样增加类型和增加品牌不会影响对方。 +- 两种变化以上的情况应该考虑桥接模式 +##### UML + +##### 适用场景与优缺点 +- 适用场景: +- 1.如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。 +- 2.设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。 +- 3.一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。 +- 4.虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。 + + +- + +#### 组合模式(树形结构) +##### 介绍 + +- 无子节点的是叶子,有子节点的是容器 +- 叶子和容器的共同点抽象为Component组件 +- 每个容器持有一个Component的List引用,包含它的所有子节点。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)想表示对象的层次结构 + - 2)希望客户忽略组合对象与单一对象的不同,用户将统一使用组合结构中的所有对象 +- 优点: + - 1)定义了包含基本对象和组合对象的类层次结构 + - 2)简化客户代码,客户可以一致地使用组合结构和单个对象 + - 3)更容易添加新类型的组件 +#### 享元模式 +##### 介绍 + + +- 将相同部分放在一个类中,工厂持有一个Map,可以创建相同部分,如果已持有那么直接返回。 +- 不同部分单独设计一个类,可以作为相同部分类的方法的参数传入 +- 将一个对象拆成两部分(成员变量拆成两部分):相同部分和不同部分。相同部分使用工厂创建,进行共享;不同部分作为参数传入 +##### UML + +##### 适用场景与优缺点 +- 适用场景:池化 内存池 数据库连接池 线程池 +- 优点: + - 1)极大减少内存中对象的数量 + - 2)相同或相似对象内存中只存一份,节省内存 + - 3)外部状态相对独立,不影响内部状态 + +- 缺点: + - 1)模式较复杂,使程序逻辑复杂化 + - 2)读取外部状态使运行时间变长,用时间换取了空间 + +- + +### 行为型设计模式 +#### 策略模式 +##### 介绍 +- 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。 +- 环境类(Context):用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可定义一个接口来让Strategy访问它的数据。 +- 抽象策略类(Strategy):定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy定义的算法。 +- 具体策略类(ConcreteStrategy):以Strategy接口实现某具体算法。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: +- 实现某一个功能由多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能 +- 优点: + - 1)Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。 + - 2)提供了可以替换继承关系的方法 + - 3)消除if-else +#### 模板方法模式 +##### 介绍 + + +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)一次性实现一个算法的不变部分,并将可变部分留给子类来实现 + - 2)个子类中公共的行为提取出来并集中到一个公共父类中以避免重复 + - 3)控制子类扩展,只允许在某些点进行扩展 + +- 优点: + - 1)在一个父类中形式化地定义算法,由它的子类实现细节的处理 + - 2)是一个代码复用的基本技术 + - 3)控制翻转(好莱坞原则),父类调用子类的操作,通过对子类的扩展来增加新的行为,符合开闭原则 +#### 观察者模式 +##### 介绍 +- 也称为发布-订阅模式。 +- 在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。 +- 与Reactor模式非常类似,不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)当一个对象的改变需要通知其他对象,而且它不知道具体有多少个对象有待通知时 + - 2)当一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这二者封装在独立的对象中国以使它们可以独立地改变和服用 +- 优点: + - 1)独立地改变目标和观察者,解耦 + - 2)是吸纳表示层和数据逻辑层分离(表示层是观察者,逻辑层是主题) +#### 迭代器模式 +##### 介绍 +- 找到一种不同容器的统一的遍历方式,定义一个接口,所有可以提供遍历方式的容器都实现这个接口,返回一个迭代器,然后所有的迭代器的接口是一致的。 + +- 所有的容器都可以通过iterator方法返回一个迭代器Iterator,这个迭代器对外暴露的接口是一致的,因此可以保证对所有的容器遍历方法是一致的,仅需得到这个容器的迭代器即可,而各个容器对迭代器的实现是不同的,即遍历方式是不同的。迭代器模式可以将各个容器的遍历方式的调用方式统一起来,隐藏了内部遍历的实现细节。 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)访问一个聚合对象的内容而无需暴露它的内部表示 + - 2)需要为聚合对象提供多种遍历方式 + - 3)为遍历不同的聚合结构提供一个统一的接口 + +- 优点: + - 1)支持以不同的方式遍历一个聚合对象 + - 2)简化聚合接口 + - 3)方便添加新的聚合类和迭代器类 +#### 责任链模式 +##### 介绍 +- 使多个处理器对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些处理器对象连成一条链,并沿这条链传递请求,直到有一个处理器对象处理它为止 +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)有多个处理器对象可以处理一个请求,哪个处理器对象处理该请求在运行时动态确定 + - 2)在不明确指定接收者的情况下,向多个处理器对象中的一个提交请求 + - 3)可以动态指定一组处理器对象处理请求 + +- 优点: + - 1)降低耦合,使得 请求发送者无需知道是哪个处理器对象处理请求 + - 2)简化对象的相互连接 + - 3)增强了给对象指派责任的灵活性 + - 4)方便添加新的请求处理类 +#### 命令模式 +##### 介绍 +- 命令模式把一个请求或者操作封装到一个对象中,把发出命令的责任和执行命令的责任分割开,委派给不同的对象,可降低行为请求者与行为实现者之间耦合度。从使用角度来看就是请求者把接口实现类作为参数传给使用者,使用者直接调用这个接口的方法,而不用关心具体执行的那个命令。 + +- Command模式将操作的执行逻辑封装到一个个Command对象中,解耦了操作发起者和操作执行逻辑之间的耦合关系:操作发起者要进行一个操作,不用关心具体的执行逻辑,只需创建一个相应的Command实例,调用它的执行接口即可。而在swing中,与界面交互的各种操作,比如插入,删除等被称之为Edit,实际上就是Command。 +- 使用undo包很简单,主要操作步骤如下: +- 1、创建CommandManager实例(持有Command的undo栈和redo栈); +- 2、创建各种实现Command的具体操作类; +- 3、调用某种操作时,创建一个具体操作类的实例,加入CommandManager; +- 4、在Undo/Redo时,直接调用CommandManager的undo/redo方法。 +##### UML + +- 黑色箭头表示持有,关联关系 Client持有Invoker +- 菱形箭头也是持有,聚合关系 Invoker持有Command +- 白色箭头是继承,ConcreteCommand继承了Command +##### 适用场景与优缺点 +- 适用场景: + - 1)系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互 + - 2)系统需要在不同的时间制定请求,将请求排序和执行请求 + - 3)系统需要支持undo和redo操作 + - 4)系统需要将一组操作组合在一起 + +- 优点: + - 1)降低系统的耦合度,调用者和接收者解耦 + - 2)Command是头等对象,可以被操纵和扩展 + - 3)组合命令 + - 4)方便实现undo和redo +- + +#### 备忘录模式 +##### 介绍 + +- Originate是实体类,并负责创建和恢复Memento(比如JavaBean) +- Memento负责保存对象的状态 +- CareTaker 负责存储Memento(一个或一系列)(多条历史记录) +- Originate除了对象的属性和setter getter之外,还有创建和恢复Memento的方法 +- Memento也持有对象的所有属性和setter getter,它的构造方法是由Originate对象得到其内部状态。 +- CareTaker持有一个或一组Memento,并提供setter and getter + +##### UML + +##### 适用场景与优缺点 +- 适用场景: +- 1、需要保存/恢复数据的相关状态场景。 +- 2、提供一个可回滚的操作。 + +- 优点: +- 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 +- 2、实现了信息的封装,使得用户不需要关心状态的保存细节。 + +- 缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。 +#### 状态模式 +##### 介绍 + +##### UML + +##### 适用场景与优缺点 +- 适用场景: + - 1)一个对象的行为取决于它的状态 + - 2)代码中包含大量的与对象状态有关的条件语句 + +- 优点: + - 1)将与特定状态相关的行为局部化,并且将不同状态的行为分割开来 + - 2)使得状态转换显式化 + - 3)State对象可被共享 + +- + +#### 访问者模式 +##### 介绍 + +##### UML + +##### 适用场景与优缺点 + +#### 中介者模式 +##### 介绍 +- 解耦多个同事对象之间的交互关系。 +- 每个同事对象都持有中介者对象的引用,只跟中介者打交道。我们通过中介者统一管理这些交互关系。 + +- 每个同事类都持有一个中介者类的引用。 +##### UML + +- 将多对多的关系解耦后转为一对多的关系,每个对象和中介者打交道,不直接和其他对象打交道。 +- 如果关系比较简单,那么没有必要使用中介者模式,反而会复杂化。 +##### 适用场景与优缺点 +- MVC中的C就是中介者 + +- 适用场景: + - 1)系统中对象之间存在着复杂的引用关系 + - 2)一组对象以定义良好但复杂的方式进行通信 + - 3)一个对象引用其他很多对象并直接与这些对象通信,导致难以复用该对象 + +- 优点: + - 1)减少子类生成 + - 2)简化同事类的设计和实现 + - 3)简化对象协议(一对多代替多对多) + - 4)对对象如何协作进行了抽象 + - 5)使控制集中化(将交互复杂性变为中介者的复杂性) +#### 解释器模式 +##### 介绍 + +##### UML + +##### 适用场景与优缺点 + + +### 设计模式的区分 +#### 代理模式和装饰器区别 +- 装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。 +- 因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 + +- 相同点:都是为被代理(被装饰)的类扩充新的功能。 +- 不同点:代理模式具有控制被代理类的访问等性质,而装饰模式紧紧是单纯的扩充被装饰的类。所以区别仅仅在是否对被代理/被装饰的类进行了控制而已。 + +#### 适配器模式和代理模式的区别 +- 适配器模式,一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。 + +- 装饰器模式,原有的不能满足现有的需求,对原有的进行增强。 + +- 代理模式,同一个类而去调用另一个类的方法,不对这个方法进行直接操作,控制访问。 +#### 抽象工厂和工厂方法模式的区别 +- 工厂方法:创建某个具体产品 +- 抽象工厂:创建某个产品族中的系列产品 + +工厂方法模式 抽象工厂模式 +针对的是一个产品等级结构 针对的是面向多个产品等级结构 +一个抽象产品类 多个抽象产品类 +可以派生出多个具体产品类 每个抽象产品类可以派生出多个具体产品类 +一个抽象工厂类,可以派生出多个具体工厂类 一个抽象工厂类,可以派生出多个具体工厂类 +每个具体工厂类只能创建一个具体产品类的实例 每个具体工厂类可以创建多个具体产品类的实例 + +- + +### JDK中的设计模式(17) +#### 创建型 + - 1)工厂方法 +- Collection.iterator() 由具体的聚集类来确定使用哪一个Iterator + - 2)单例模式 +- Runtime.getRuntime() + - 3)建造者模式 +- StringBuilder + - 4)原型模式 +- Java中的Cloneable +#### 结构性 + - 1)适配器模式 +- InputStreamReader +- OutputStreamWriter +- RunnableAdapter + - 2)装饰器模式 +- io包 FileInputStream BufferedInputStream + - 3)代理模式 +- 动态代理;RMI + - 4)外观模式 +- java.util.logging + - 5)桥接模式 +- JDBC + - 6)组合模式 +- dom + - 7)享元模式 +- Integer.valueOf +#### 行为型 + - 1)策略模式 +- 线程池的四种拒绝策略 + - 2)模板方法模式 +- AbstractList、AbstractMap等 +- InputStream、OutputStream +- AQS + - 3)观察者模式 +- Swing中的Listener + - 4)迭代器模式 +- 集合类中的iterator + - 5)责任链模式 +- J2EE中的Filter + - 6)命令模式 +- Runnable、Callable,ThreadPoolExecutor + - 7)备忘录模式 + - 8)状态模式 + - 9)访问者模式 +- 10)中介者模式 + - 11)解释器模式 +- + +### Spring中的设计模式(6) + - 1)抽象工厂模式: +- BeanFactory + - 2)代理模式: +- AOP + - 3)模板方法模式: +- AbstractApplicationContext中定义了一系列的抽象方法,比如refreshBeanFactory、closeBeanFactory、getBeanFactory。 + - 4)单例模式: +- Spring可以管理单例对象,控制对象为单例 + - 5)原型模式: +- Spring可以管理多例对象,控制对象为prototype + - 6)适配器模式: +- Advice与Interceptor的适配 +- Adapter类接口:Target + +``` +public interface AdvisorAdapter { +``` + +-   +- boolean supportsAdvice(Advice advice); +-   +-       MethodInterceptor getInterceptor(Advisor advisor); +-   +- } MethodBeforeAdviceAdapter类,Adapter +- class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { +-   + +``` +      public boolean supportsAdvice(Advice advice) { +``` + +-             return (advice instanceof MethodBeforeAdvice); +-       } +-   + +``` +      public MethodInterceptor getInterceptor(Advisor advisor) { +``` + +-             MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); +-       return new MethodBeforeAdviceInterceptor(advice); +-       } +-   +- } From c5829fa46868bee13c87f2e3c8c081d947ba1ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:46:44 +0800 Subject: [PATCH 49/97] =?UTF-8?q?Update=20Java&=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...76\350\256\241\346\250\241\345\274\217.md" | 1095 +++-------------- 1 file changed, 185 insertions(+), 910 deletions(-) diff --git "a/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" index 077fe85b..84174c7a 100644 --- "a/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,9 +1,9 @@ -## Java +# Java - Oracle JDK有部分源码是闭源的,如果确实需要可以查看OpenJDK的源码,可以在该网站获取。 - http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/ - http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/73d5bcd0585d/src - 上面这个还可以查看native方法。 -### JDK&JRE&JVM +# 1.1 JDK&JRE&JVM - JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具(编译、开发工具)和Java核心类库。 - Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。 - JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。 @@ -11,9 +11,9 @@ - JDK包含JRE和Java编译、开发工具; - JRE包含JVM和Java核心类库; - 运行Java仅需要JRE;而开发Java需要JDK。 -### 跨平台 +# 1.2 跨平台 - 字节码是在虚拟机上运行的,而不是编译器。换而言之,是因为JVM能跨平台安装,所以相应JAVA字节码便可以跟着在任何平台上运行。只要JVM自身的代码能在相应平台上运行,即JVM可行,则JAVA的程序员就可以不用考虑所写的程序要在哪里运行,反正都是在虚拟机上运行,然后变成相应平台的机器语言,而这个转变并不是程序员应该关心的。 -### 基础数据类型 +# 1.3 基础数据类型 - 第一类:整型 byte short int long - 第二类:浮点型 float double - 第三类:逻辑型 boolean(它只有两个值可取true false) @@ -36,33 +36,33 @@ - Java标准库实现的对char与String的序列化规定使用UTF-8作为外码。Java的Class文件中的字符串常量与符号名字也都规定用UTF-8编码。这大概是当时设计者为了平衡运行时的时间效率(采用定长编码的UTF-16)与外部存储的空间效率(采用变长的UTF-8编码)而做的取舍。 -### 引用类型 +# 1.4 引用类型 - 类、接口、数组都是引用类型 -#### 四种引用 +## 四种引用 - 目的:避免对象长期占用内存, -##### 强引用 +### 强引用 - StringReference GC时不回收 - 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 -##### 软引用 +### 软引用 - SoftReference GC时如果JVM内存不足时会回收 - 软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 -##### 弱引用 +### 弱引用 - WeakReference GC时立即回收 - 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 - 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 -##### 虚引用 +### 虚引用 - PhantomReference - 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 - 在Java集合中有一种特殊的Map类型:WeakHashMap, 在这种Map中存放了键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。 -#### 基础数据类型包装类 -##### 为什么需要 +## 基础数据类型包装类 +### 为什么需要 - 由于基本数据类型不是对象,所以java并不是纯面向对象的语言,好处是效率较高(全部包装为对象效率较低)。 - Java是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。 - -##### 有哪些 +### 有哪些  基本类型     包装器类型   boolean Boolean char Character @@ -73,14 +73,14 @@ long Long float Float double Double - Number是所有数字包装类的父类 -##### 自动装箱、自动拆箱(编译器行为) +### 自动装箱、自动拆箱(编译器行为) - 自动装箱:可以将基础数据类型包装成对应的包装类 - Integer i = 10000; // 编译器会改为new Integer(10000) - 自动拆箱:可以将包装类转为对应的基础数据类型 - int i = new Integer(1000);//编译器会修改为 int i = new Integer(1000).intValue(); - 自动拆箱时如果包装类是null,那么会抛出NPE -##### Integer.valueOf +### Integer.valueOf ``` public static Integer valueOf(int i) { @@ -95,15 +95,15 @@ public static Integer valueOf(int i) { - 所以在此访问内的Integer对象使用==和equals结果是一样的。 - 如果Integer的值一致,且在此范围内,因为是同一个对象,所以==返回true;但此访问之外的对象==比较的是内存地址,值相同,也是返回false。 -### Object +# 1.5 Object -#### == 与 equals的区别 +## == 与 equals的区别 - 如果两个引用类型变量使用==运算符,那么比较的是地址,它们分别指向的是否是同一地址的对象。结果一定是false,因为两个对象不可能存放在同一地址处。 - 要求是两个对象都不是能空值,与空值比较返回false。 - ==不能实现比较对象的值是否相同。 - 所有对象都有equals方法,默认是Object类的equals,其结果与==一样。 - 如果希望比较对象的值相同,必须重写equals方法。 -#### hashCode与equals的区别 +## hashCode与equals的区别 - Object中的equals: ``` @@ -139,20 +139,20 @@ public native int hashCode(); ((k = p.key) == key || (key != null && key.equals(k)))) e = p; - 如果equals返回结果相同,则值一定相同,不再存入。 -#### 如果重写equals不重写hashCode会怎样 +## 如果重写equals不重写hashCode会怎样 - 两个值不同的对象的hashCode一定不一样,那么执行equals,结果为true,HashSet或HashMap的键会放入值相同的对象。 -### String&StringBuffer&StringBuilder +# 1.6 String&StringBuffer&StringBuilder - 都是final类,不允许继承; - String长度不可变,StringBuffer、StringBuilder长度可变; -#### String +## String ``` public final class String implements java.io.Serializable, Comparable, CharSequence {} ``` -##### equals&hashCode +### equals&hashCode - String重写了Object的hashCode和equals。 ``` @@ -179,7 +179,7 @@ public boolean equals(Object anObject) { } ``` -##### 添加功能 +### 添加功能 - String是final类,不可被继承,也不可重写一个java.lang.String(类加载机制)。 - 一般是使用StringUtils来增强String的功能。 @@ -191,7 +191,7 @@ public boolean equals(Object anObject) { - 但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap, - 父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。 - 所以,用户自定义的java.lang.String不被加载,也就是不会被使用。 -##### + substring +### + substring - 会创建一个新的字符串; - 编译时会将+转为StringBuilder的append方法。 - 注意新的字符串是在运行时在堆里创建的。 @@ -242,7 +242,7 @@ public String(char value[], int offset, int count) { ``` -##### 常量池 +### 常量池 - String str = new String(“ABC”); - 至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。同时如果这个字符串在字符串常量池里不存在,会在池里创建这个String对象“ABC”。 - String s1= “a”; @@ -258,7 +258,7 @@ public String(char value[], int offset, int count) { - 如果不在,那么会先在常量池中创建一份("abc"),然后在堆中创建一份(new String),共创建两个对象。 - -##### 编译优化 +### 编译优化 - 字面量,final 都会在编译期被优化,并且会被直接运算好。 - @@ -336,14 +336,14 @@ System.out.println(str1 == "SEUCalvin");// false - 在实例三的基础上加了第一行 - str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,你可以去验证它们两个是返回的true。后面的"SEUCalvin"也一样指向str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。 -#### StringBuffer&StringBuilder +## StringBuffer&StringBuilder - StringBuffer是线程安全的,StringBuilder不是线程安全的,但它们两个中的所有方法都是相同的。StringBuffer在StringBuilder的方法之上添加了synchronized,保证线程安全。 - StringBuilder比StringBuffer性能更好。 - -### 面向对象 -#### 抽象类与接口 +# 1.7 面向对象 +## 抽象类与接口 - 区别: - 1)抽象类中方法可以不是抽象的;接口中的方法必须是抽象方法; - 2)抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。 @@ -353,22 +353,22 @@ System.out.println(str1 == "SEUCalvin");// false - 使用场景: - 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。 - 如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。 -#### 三大特性 +## 三大特性 - 面向对象的三个特性:封装;继承;多态 - 封装:将数据与操作数据的方法绑定起来,隐藏实现细节,对外提供接口。 - 继承:代码重用;可扩展性 - 多态:允许不同子类对象对同一消息做出不同响应 - 多态的三个必要条件:继承、方法的重写、父类引用指向子类对象 -#### 重写和重载 +## 重写和重载 - 根据对象对方法进行选择,称为分派 - 编译期的静态多分派:overloading重载 根据调用引用类型和方法参数决定调用哪个方法(编译器) - 运行期的动态单分派:overriding 重写 根据指向对象的类型决定调用哪个方法(JVM) - -### 关键类 -#### ThreadLocal(线程局部变量) +# 1.8 关键类 +## ThreadLocal(线程局部变量) - 在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。 - 例如有一个静态变量 @@ -391,7 +391,7 @@ public static final ThreadLocal sdf = new ThreadLocal> { @@ -463,7 +463,7 @@ static class Entry extends WeakReference> { - 它也是一个类似HashMap的数据结构,但是并没实现Map接口。 - 也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。 - ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。 -###### 构造方法 +#### 构造方法 - ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { - // 表的大小始终为2的幂次 table = new Entry[INITIAL_CAPACITY]; @@ -568,7 +568,7 @@ private int expungeStaleEntry(int staleSlot) { return i; } -###### set(线性探测法解决hash冲突) +#### set(线性探测法解决hash冲突) ``` private void set(ThreadLocal key, Object value) { @@ -643,7 +643,7 @@ private static int nextIndex(int i, int len) { - 这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置 - 可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。 -###### cleanSomeSlots(启发式地清理slot) +#### cleanSomeSlots(启发式地清理slot) - i是当前位置,n是元素个数 - i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空) - n是用于控制控制扫描次数的 @@ -673,7 +673,7 @@ private boolean cleanSomeSlots(int i, int n) { ``` -###### rehash +#### rehash - 先全量清理,如果清理后现有元素个数超过负载,那么扩容 ``` @@ -736,7 +736,7 @@ private void resize() { ``` -###### remove +#### remove ``` private void remove(ThreadLocal key) { @@ -767,7 +767,7 @@ public void clear() { ``` -###### 内存泄露 +#### 内存泄露 - 只有调用TheadLocal的remove或者get、set时才会采取措施去清理被回收的ThreadLocal对应的value(但也未必会清理所有的需要被回收的value)。假如一个局部的ThreadLocal不再需要,如果没有去调用remove方法清除,那么有可能会发生内存泄露。 - 既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。 @@ -781,7 +781,7 @@ JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLoca - -#### Iterator / ListIterator / Iterable +## Iterator / ListIterator / Iterable - 普通for循环时不能删除元素,否则会抛出异常;Iterator可以 @@ -960,7 +960,7 @@ private class ListItr extends Itr implements ListIterator { } ``` -#### for /增强for/ forEach +## for /增强for/ forEach For-each loop Equivalent for loop for (type var : arr) { body-of-loop @@ -993,7 +993,7 @@ for (type var : coll) { - -#### Comparable与Comparator +## Comparable与Comparator - 基本数据类型包装类和String类均已实现了Comparable接口。 - 实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序,默认为升序。 @@ -1002,7 +1002,7 @@ for (type var : coll) { - 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如TreeSet,TreeMap)的顺序。 - -### 继承 +# 1.9 继承 ``` 子类继承父类所有的成员变量(即使是private变量,有所有权,但是没有使用权,不能访问父类的private的成员变量)。 @@ -1021,7 +1021,7 @@ for (type var : coll) { - -### 内部类 +# 1.10 内部类 - 在另一个类的里面定义的类就是内部类 - 内部类是编译器现象,与虚拟机无关。 - 编译器会将内部类编译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。 @@ -1032,7 +1032,7 @@ for (type var : coll) { ``` -#### 优点 +## 优点 - 每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。 - 接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。 @@ -1045,13 +1045,13 @@ for (type var : coll) { - 只有静态内部类可以同时拥有静态成员和非静态成员,其他内部类只有拥有非静态成员。 -#### 成员内部类:就像外部类的一个成员变量 +## 成员内部类:就像外部类的一个成员变量 - 注意内部类的对象总有一个外部类的引用 - 当创建内部类对象时,会自动将外部类的this引用传递给当前的内部类的构造方法。 -#### 静态内部类:就像外部类的一个静态成员变量 +## 静态内部类:就像外部类的一个静态成员变量 ``` @@ -1067,7 +1067,7 @@ public class OuterClass { ``` -#### 局部内部类:定义在一个方法或者一个块作用域里面的类 +## 局部内部类:定义在一个方法或者一个块作用域里面的类 - 想创建一个类来辅助我们的解决方案,又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。 - JDK1.8之前不能访问非final的局部变量! @@ -1093,7 +1093,7 @@ public Destination destination(String str) { ``` -#### 匿名内部类:必须继承一个父类或实现一个接口 +## 匿名内部类:必须继承一个父类或实现一个接口 - 匿名内部类和局部内部类在JDK1.8 之前都不能访问一个非final的局部变量,只能访问final的局部变量,原因是生命周期不同,可能栈中的局部变量已经被销毁,而堆中的对象仍存活,此时会访问一个不存在的内存区域。假如是final的变量,那么编译时会将其拷贝一份,延长其生命周期。 - 拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。 @@ -1118,10 +1118,10 @@ public class AnonymousOuter { ``` -### 关键字 -#### final +# 1.11 关键字 +## final -#### try-finally-return +## try-finally-return - 1、不管有没有出现异常,finally块中代码都会执行; - 2、当try和catch中有return时,finally仍然会执行;无论try里执行了return语句、break语句、还是continue语句,finally语句块还会继续执行;如果执行try和catch时JVM退出(比如System.exit(0)),那么finally不会被执行; - finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的; @@ -1164,33 +1164,33 @@ private static int test2() { ``` -#### static +## static - static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。 -##### 1)修饰成员方法:静态成员方法 +### 1)修饰成员方法:静态成员方法 - 在静态方法中不能访问类的非静态成员变量和非静态成员方法; - 在非静态成员方法中是可以访问静态成员方法/变量的; - 即使没有显式地声明为static,类的构造器实际上也是静态方法 -##### 2)修饰成员变量:静态成员变量 +### 2)修饰成员变量:静态成员变量 - 静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 - 静态成员变量并发下不是线程安全的,并且对象是单例的情况下,非静态成员变量也不是线程安全的。 - 怎么保证变量的线程安全? - 只有一个线程写,其他线程都是读的时候,加volatile;线程既读又写,可以考虑Atomic原子类和线程安全的集合类;或者考虑ThreadLocal -##### 3)修饰代码块:静态代码块 +### 3)修饰代码块:静态代码块 - 用来构造静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 -##### 4)修饰内部类:静态内部类 +### 4)修饰内部类:静态内部类 - 成员内部类和静态内部类的区别: - 1)前者只能拥有非静态成员;后者既可拥有静态成员,又可拥有非静态成员 - 2)前者持有外部类的的引用,可以访问外部类的静态成员和非静态成员;后者不持有外部类的引用,只能访问外部类的静态成员 - 3)前者不能脱离外部类而存在;后者可以 -##### 5)修饰import:静态导包 +### 5)修饰import:静态导包 - -#### switch -##### switch字符串实现原理 +## switch +### switch字符串实现原理 - 对比反编译之后的结果: - 编译后switch还是基于整数,该整数来自于String的hashCode。 @@ -1198,16 +1198,16 @@ private static int test2() { - -##### 字节码实现原理(tableswitch / lookupswitch) +### 字节码实现原理(tableswitch / lookupswitch) - 编译器会使用tableswitch和lookupswitch指令来生成switch语句的编译代码。当switch语句中的case分支的条件值比较稀疏时,tableswitch指令的空间使用率偏低。这种情况下将使用lookupswitch指令来替代。lookupswitch指令的索引表由int类型的键(来源于case语句块后面的数值)与对应的目标语句偏移量所构成。当lookupswitch指令执行时,switch语句的条件值将和索引表中的键进行比较,如果某个键和条件值相符,那么将转移到这个键对应的分支偏移量继续执行,如果没有键值符合,执行将在default分支执行。 -#### abstract +## abstract - 只要含有抽象方法,这个类必须添加abstract关键字,定义为抽象类。 - 只要父类是抽象类,内含抽象方法,那么继承这个类的子类的相对应的方法必须重写。如果不重写,就需要把父类的声明抽象方法再写一遍,留给这个子类的子类去实现。同时将这个子类也定义为抽象类。 - 注意抽象类中可以有抽象方法,也可以有具体实现方法(当然也可以没有)。 - 抽象方法须加abstract关键字,而具体方法不可加 - 只要是抽象类,就不能存在这个类的对象(不可以new一个这个类的对象)。 -#### this & super +## this & super - this - 自身引用;访问成员变量与方法;调用其他构造方法 - 1. 通过this调用另一个构造方法,用法是this(参数列表),这个仅在类的构造方法中可以使用 @@ -1222,10 +1222,10 @@ private static int test2() { - 与this类似,super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员,如果不冲突的话也可以不加super。 - 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,为了区别,调用父类的成员必须要加super - 3.调用父类的构造函数 -#### 访问权限 +## 访问权限 -### 枚举 -#### JDK实现 +# 1.12 枚举 +## JDK实现 - 实例: ``` @@ -1250,7 +1250,7 @@ public enum Labels0 { - 可以清晰地看到枚举被编译后其实就是一个类,该类被声明成 final,说明其不能被继承,同时它继承了 Enum 类。枚举里面的元素被声明成 static final ,另外生成一个静态代码块 static{},最后还会生成 values 和 valueOf 两个方法。下面以最简单的 Labels 为例,一个一个模块来看。 -##### Enum 类 +### Enum 类 - Enum 类是一个抽象类,主要有 name 和 ordinal 两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引,而构造函数传入的两个变量刚好与之对应。 - toString 方法直接返回 name。 @@ -1260,18 +1260,18 @@ public enum Labels0 { - compareTo 方法可以看到就是比较 ordinal 的大小。 - valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素。 -##### 静态代码块的实现 +### 静态代码块的实现 - 在静态代码块中创建对象,对象是单例的! - 可以看到静态代码块主要完成的工作就是先分别创建 Labels 对象,然后将“ENVIRONMENT”、“TRAFFIC”和“PHONE”字符串作为 name ,按照顺序分别分配位置索引0、1、2作为 ordinal,然后将其值设置给创建的三个 Labels 对象的 name 和 ordinal 属性,此外还会创建一个大小为3的 Labels 数组 ENUM$VALUES,将前面创建出来的 Labels 对象分别赋值给数组。 -##### values的实现 +### values的实现 - 可以看到它是一个静态方法,主要是使用了前面静态代码块中的 Labels 数组 ENUM$VALUES,调用 System.arraycopy 对其进行复制,然后返回该数组。所以通过 Labels.values()[2]就能获取到数组中索引为2的元素。 -##### valueOf 方法 +### valueOf 方法 - 该方法同样是个静态方法,可以看到该方法的实现是间接调用了父类 Enum 类的 valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素,比如可以通过 Labels.valueOf("ENVIRONMENT")获取 Labels.ENVIRONMENT。 - 枚举本质其实也是一个类,而且都会继承java.lang.Enum类,同时还会生成一个静态代码块 static{},并且还会生成 values 和 valueOf 两个方法。而上述的工作都需要由编译器来完成,然后我们就可以像使用我们熟悉的类那样去使用枚举了。 - -#### 用enum代替int常量 +## 用enum代替int常量 - 将int枚举常量翻译成可打印的字符串,没有很便利的方法。 - 要遍历一个枚举组中的所有int 枚举常量,甚至获得int枚举组的大小。 @@ -1282,15 +1282,15 @@ public enum Labels0 { - 可以在枚举类型中放入这段代码,可以实现String2Enum。 - 注意Operation是枚举类型名。 -#### 用实例域代替序数 +## 用实例域代替序数 - 这种实现不好,不推荐使用ordinal方法,推荐使用下面这种实现: -#### 用EnumSet代替位域 +## 用EnumSet代替位域 - 位域是将几个常量合并到一个集合中,我们推荐用枚举代替常量,用EnumSet代替集合 - EnumSet.of(enum1,enum2) -> Set<枚举> -#### 用EnumMap代替序数索引 +## 用EnumMap代替序数索引 @@ -1302,14 +1302,14 @@ public enum Labels0 { - -### 序列化 -#### JDK序列化(Serizalizable) +# 1.13 序列化 +## JDK序列化(Serizalizable) - 定义:将实现了Serializable接口(标记型接口)的对象转换成一个字节数组,并可以将该字节数组转为原来的对象。 - ObjectOutputStream 是专门用来输出对象的输出流; - ObjectOutputStream 将 Java 对象写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。 -#### serialVersionUID +## serialVersionUID - Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)。 - 1)如果没有添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),此时会报错。 @@ -1317,11 +1317,11 @@ public enum Labels0 { - 如果设置了serialVersionUID并且一致,那么可能会反序列化部分数据;如果没有设置,那么只要属性不同,那么无法反序列化。 -#### 其他序列化工具 +## 其他序列化工具 - XML/JSON - Thrift/Protobuf -#### 对象深拷贝与浅拷贝 +## 对象深拷贝与浅拷贝 - 当拷贝一个变量时,原始引用和拷贝的引用指向同一个对象,改变一个引用所指向的对象会对另一个引用产生影响。 - 如果需要创建一个对象的浅拷贝,那么需要调用clone方法。 - Object 类本身不实现接口 Cloneable,直接调用clone会抛出异常。 @@ -1398,13 +1398,13 @@ protected Object clone() { - -### 异常 +# 1.14 异常 -#### Error、Exception +## Error、Exception - Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 - Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。 -#### 常见RuntimeException +## 常见RuntimeException - IllegalArgumentException - 方法的参数无效 - NullPointerException - 试图访问一空对象的变量、方法或空数组的元素 - ArrayIndexOutOfBoundsException - 数组越界访问 @@ -1412,12 +1412,12 @@ protected Object clone() { - NumberFormatException 继承IllegalArgumentException,字符串转换为数字时出现。比如int i= Integer.parseInt("ab3"); - -#### RuntimeException与非Runtime Exception +## RuntimeException与非Runtime Exception - RuntimeException是运行时异常,也称为未检查异常; - 非RuntimeException 也称为CheckedException 受检异常 - 前者可以不必进行try-catch,后者必须要进行try-catch或者throw。 -#### 异常包装 +## 异常包装 - 在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型 - try{ - … @@ -1441,14 +1441,14 @@ protected Object clone() { - 早抛出,晚捕获。 - -### 泛型 +# 1.15 泛型 - 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 -#### 泛型接口/类/方法 -#### 泛型继承、实现 +## 泛型接口/类/方法 +## 泛型继承、实现 - 父类使用泛型,子类要么去指定具体类型参数,要么继续使用泛型 -#### 泛型的约束和局限性 +## 泛型的约束和局限性 - 1)只能使用包装器类型,不能使用基本数据类型; - 2)运行时类型查询只适用于原始类型,不适用于带类型参数的类型; @@ -1465,7 +1465,7 @@ public static T[] minmax(T… a){ - T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),个数); - …复制 - } -#### 通配符 +## 通配符 - ? 未知类型 只可以用于声明时,声明类型或方法参数,不能用于定义时(指定类型参数时) - List unknownList; @@ -1478,7 +1478,7 @@ public static T[] minmax(T… a){ - -#### extends 指定类型必为自身或其子类 +## extends 指定类型必为自身或其子类 - List - 这个引用变量如果作为参数,哪些引用可以传入? @@ -1501,11 +1501,11 @@ public static T[] minmax(T… a){ - list.add(new Integer(1)); //error - list.add(new Float(1.2f)); //error -#### super 指定类型必为自身或其父类 +## super 指定类型必为自身或其父类 - 不能同时声明泛型通配符申明上界和下界 -#### PECS(读extends,写super) +## PECS(读extends,写super) - producer-extends, consumer-super. - produce是指参数是producer,consumer是指参数是consumer。 - 要往泛型类写数据时,用extends; @@ -1560,7 +1560,7 @@ public static void copy(List dest, List src) { - -#### 泛型擦除(编译时擦除) +## 泛型擦除(编译时擦除) - 编译器生成的bytecode是不包含泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即泛型擦除。 - 擦除类型变量,并替换为限定类型(无限定的变量用Object)。 @@ -1602,13 +1602,13 @@ public interface Builder { - -### IO -#### Unix IO模型 +# 1.16 IO +## Unix IO模型 - 异步I/O 是指用户程序发起IO请求后,不等待数据,同时操作系统内核负责I/O操作把数据从内核拷贝到用户程序的缓冲区后通知应用程序。数据拷贝是由操作系统内核完成,用户程序从一开始就没有等待数据,发起请求后不参与任何IO操作,等内核通知完成。 - 同步I/O 就是非异步IO的情况,也就是用户程序要参与把数据拷贝到程序缓冲区(例如java的InputStream读字节流过程)。 - 同步IO里的非阻塞 是指用户程序发起IO操作请求后不等待数据,而是调用会立即返回一个标志信息告知条件不满足,数据未准备好,从而用户请求程序继续执行其它任务。执行完其它任务,用户程序会主动轮询查看IO操作条件是否满足,如果满足,则用户程序亲自参与拷贝数据动作。 - Unix IO模型的语境下,同步和异步的区别在于数据拷贝阶段是否需要完全由操作系统处理。阻塞和非阻塞操作是针对发起IO请求操作后是否有立刻返回一个标志信息而不让请求线程等待。 -#### BIO NIO AIO介绍 +## BIO NIO AIO介绍 - BIO:同步阻塞,每个客户端的连接会对应服务器的一个线程 - NIO:同步非阻塞,多路复用器轮询客户端的请求,每个客户端的IO请求会对应服务器的一个线程 @@ -1624,7 +1624,7 @@ public interface Builder { - 异步IO就是指AIO,AIO需要操作系统支持。 - -#### Java BIO 使用 +## Java BIO 使用 - Server @@ -2035,7 +2035,7 @@ public class ChatClient extends Frame { - } -#### Java NIO 使用 +## Java NIO 使用 - 传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。 - NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。 - BIO中的accept是没有客户端连接时阻塞,NIO的accept是没有客户端连接时立即返回。 @@ -2044,14 +2044,14 @@ public class ChatClient extends Frame { - Buffer是用于容纳数据的缓冲区,Channel是与IO设备之间的连接,类似于流。 - 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。 - Selector是Channel的多路复用器。 -##### Buffer(缓冲区) +### Buffer(缓冲区) - - clear 是将position置为0,limit置为capacity; - flip是将limit置为position,position置为0; -###### MappedByteBuffer(对应OS中的内存映射文件) +#### MappedByteBuffer(对应OS中的内存映射文件) - ByteBuffer有两种模式:直接/间接。间接模式就是HeapByteBuffer,即操作堆内存 (byte[])。 - 但是内存毕竟有限,如果我要发送一个1G的文件怎么办?不可能真的去分配1G的内存.这时就必须使用"直接"模式,即 MappedByteBuffer。 @@ -2080,7 +2080,7 @@ public class ChatClient extends Frame { - MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。 - -###### DirectByteBuffer(堆外内存) +#### DirectByteBuffer(堆外内存) - DirectByteBuffer继承自MappedByteBuffer,它们都是使用的堆外内存,不受JVM堆大小的限制,只是前者仅仅是分配内存,后者是将文件映射到内存中。 - 可以通过ByteBuf.allocateDirect方法获取。 @@ -2097,7 +2097,7 @@ public class ChatClient extends Frame { - -###### 堆外内存的释放 +#### 堆外内存的释放 - java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。 - GC方式: @@ -2149,9 +2149,9 @@ public static void main(String[] args) { - -##### Channel(通道) +### Channel(通道) - Channel与IO设备的连接,与Stream是平级的概念。 -###### 流与通道的区别 +#### 流与通道的区别 - 1、流是单向的,通道是双向的,可读可写。 - 2、流读写是阻塞的,通道可以异步读写。 - 3、流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入 @@ -2159,11 +2159,11 @@ public static void main(String[] args) { - 注意,FileChannel 不能设置为非阻塞模式。 -###### 分散与聚集 +#### 分散与聚集 - 分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 - 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。 -###### Pipe +#### Pipe ``` @@ -2233,7 +2233,7 @@ public class PipeTest { - -###### FileChannel与文件锁 +#### FileChannel与文件锁 - 在通道中我们可以对文件或者部分文件进行上锁。上锁和我们了解的线程锁差不多,都是为了保证数据的一致性。在文件通道FileChannel中可以对文件进行上锁,通过FileLock可以对文件进行锁的释放。 - 文件加锁是建立在文件通道(FileChannel)之上的,套接字通道(SockeChannel)不考虑文件加锁,因为它是不共享的。它对文件加锁有两种方式: - ①lock @@ -2259,7 +2259,7 @@ public class PipeTest { - -##### Selector (Channel的多路复用器) +### Selector (Channel的多路复用器) - Selector可以用单线程去管理多个Channel(多个连接)。 - 放在网络编程的环境下:Selector使用单线程,轮询客户端对应的Channel的请求,如果某个Channel需要进行IO,那么分配一个线程去执行IO操作。 @@ -2271,11 +2271,11 @@ public class PipeTest { - 每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。 - SelectionKey是一个复合事件,绑定到某个selector对应的某个channel上,可能是多个事件的复合或单一事件。 -#### Java NIO 实例(文件上传) +## Java NIO 实例(文件上传) - 服务器主线程先创建Socket,并注册到selector,然后轮询selector。 - 1)如果有客户端需要进行连接,那么selector返回ACCEPT事件,主线程建立连接(accept),并将该客户端连接注册到selector,结束,继续轮询selector等待下一个客户端事件; - 2)如果有已连接的客户端需要进行读写,那么selector返回READ/WRITE事件,主线程将该请求交给IO线程池中的某个线程执行操作,结束,继续轮询selector等待下一个客户端事件。 -##### 服务器 +### 服务器 ``` public class NIOTCPServer { @@ -2401,7 +2401,7 @@ public class NIOTCPServer { } ``` -##### 客户端 +### 客户端 ``` public class NIOTCPClient { @@ -2454,7 +2454,7 @@ public class NIOTCPClient { - -#### Java AIO 使用 +## Java AIO 使用 - 对AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。 - AIO的四步: - 1、进程向操作系统请求数据 @@ -2631,9 +2631,9 @@ public class AIOServer { - -#### Java NIO 源码 +## Java NIO 源码 - 关于Selector源码过于难以理解,可以先放过。 -##### Buffer +### Buffer ``` public abstract class Buffer { @@ -2694,9 +2694,9 @@ public static ByteBuffer allocateDirect(int capacity) { ``` -##### HeapByteBuffer(间接模式) +### HeapByteBuffer(间接模式) - 底层基于byte数组。 -###### 初始化 +#### 初始化 ``` HeapByteBuffer(int cap, int lim) { // package-private @@ -2721,7 +2721,7 @@ ByteBuffer(int mark, int pos, int lim, int cap, // package-private final int offset; boolean isReadOnly; // Valid only for heap buffers -###### get +#### get ``` public byte get() { @@ -2744,7 +2744,7 @@ final int nextGetIndex() { // package-private return i + offset; } -###### put +#### put ``` public ByteBuffer put(byte x) { @@ -2766,14 +2766,14 @@ final int nextPutIndex() { // package-private - -##### DirectByteBuffer(直接模式) +### DirectByteBuffer(直接模式) - 底层基于c++的malloc分配的堆外内存,是使用Unsafe类分配的,底层调用了native方法。 - 在创建DirectByteBuffer的同时,创建一个与其对应的cleaner,cleaner是一个虚引用。 - 回收堆外内存的几种情况: - 1)程序员手工释放,需要使用sun的非公开API实现。 - 2)申请新的堆外内存而内存不足时,会进行调用Cleaner(作为一个Reference)的静态方法tryHandlePending(false),它又会调用cleaner的clean方法释放内存。 - 3)当DirectByteBuffer失去强引用,只有虚引用时,当等到某一次System.gc(full gc)(比如堆外内存达到XX:MaxDirectMemorySize)时,当DirectByteBuffer对象从pending状态 -> enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。 -###### 初始化 +#### 初始化 - 重要成员变量: ``` @@ -2963,7 +2963,7 @@ ByteBuffer(int mark, int pos, int lim, int cap) { // package-private return true; } -###### Deallocator +#### Deallocator - 后面是调用unsafe的分配堆外内存的方法,然后初始化了该DirectByteBuffer对应的cleaner。 - 注:在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。 @@ -3000,7 +3000,7 @@ private static class Deallocator ``` -###### Cleaner(回收) +#### Cleaner(回收) ``` public class Cleaner extends PhantomReference { @@ -3091,7 +3091,7 @@ public PhantomReference(T referent, ReferenceQueue q) { ``` -###### get +#### get ``` public byte get() { @@ -3099,7 +3099,7 @@ public byte get() { } ``` -###### put +#### put ``` public ByteBuffer put(byte x) { @@ -3111,10 +3111,10 @@ public ByteBuffer put(byte x) { - -##### FileChannel(阻塞式) +### FileChannel(阻塞式) - FileChannel的read、write和map通过其实现类FileChannelImpl实现。 - FileChannelImpl的Oracle JDK没有提供源码,只能在OpenJDK中查看。 -###### open +#### open ``` public static FileChannel open(Path path, OpenOption... options) @@ -3365,7 +3365,7 @@ private static native long CreateFile0(long lpFileName, - -###### read +#### read ``` public int read(ByteBuffer dst) throws IOException { @@ -3419,7 +3419,7 @@ public int read(ByteBuffer dst) throws IOException { - 3、把bb的数据读取到dst(用户定义的缓存,在jvm中分配内存)。 - read方法导致数据复制了两次。 -###### write +#### write ``` public int write(ByteBuffer src) throws IOException { @@ -3507,9 +3507,9 @@ private static int writeFromNativeBuffer(FileDescriptor fd, - write方法也导致了数据复制了两次。 - -##### ServerSocketChannel +### ServerSocketChannel - 它的实现类是ServerSocketChannelImpl,同样是闭源的。 -###### open +#### open ``` public static ServerSocketChannel open() throws IOException { @@ -3549,7 +3549,7 @@ public ServerSocketChannel openServerSocketChannel() throws IOException { - -###### bind +#### bind ``` public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException { @@ -3577,7 +3577,7 @@ public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOExcep - -###### register +#### register - Selector是通过Selector.open方法获得的。 - 将这个通道channel注册到指定的selector中,返回一个SelectionKey对象实例。 - register这个方法在实现代码上的逻辑有以下四点: @@ -3639,11 +3639,11 @@ private SelectionKey findKey(Selector sel) { - -##### Selector(如何实现Channel多路复用) +### Selector(如何实现Channel多路复用) - SocketChannel、ServerSocketChannel和Selector的实例初始化都通过SelectorProvider类实现,其中Selector是整个NIO Socket的核心实现。 - SelectorProvider在windows和linux下有不同的实现,provider方法会返回对应的实现。 -###### 成员变量 +#### 成员变量 1.- final class WindowsSelectorImpl extends SelectorImpl   2.- {   3. @@ -3749,7 +3749,7 @@ private SelectionKey findKey(Selector sel) { 27.-     }    28.- }   -###### open +#### open ``` public static Selector open() throws IOException { @@ -3817,7 +3817,7 @@ protected final SelectionKey register(AbstractSelectableChannel ch, } } -###### select(返回有事件发生的SelectionKey数量) +#### select(返回有事件发生的SelectionKey数量) - var1是timeout时间,无参数的版本对应的timeout为0. - select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。 - 这个方法并不能提供精确时间的保证,和当执行wait(long timeout)方法时并不能保证会延时timeout道理一样。 @@ -4074,7 +4074,7 @@ Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject - epoll是Linux下的一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄。 - 在Windows下是IOCP -###### WindowsSelectorImpl.wakeup() +#### WindowsSelectorImpl.wakeup() ``` public Selector wakeup() { @@ -4119,10 +4119,10 @@ Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this, - -#### Java AIO 源码 -##### AsynchronousFileChannle(AIO,基于CompletionHandler回调) +## Java AIO 源码 +### AsynchronousFileChannle(AIO,基于CompletionHandler回调) - 在Java 7中,AsynchronousFileChannel被添加到Java NIO。AsynchronousFileChannel使读取数据,并异步地将数据写入文件成为可能。 -###### open +#### open - Path path = Paths.get("data/test.xml"); - AsynchronousFileChannel fileChannel = @@ -4214,19 +4214,19 @@ public static AsynchronousFileChannel open(FileDescriptor fdo, ``` -###### read -###### write +#### read +#### write - - -#### Netty NIO +## Netty NIO - 基于这个语境,Netty目前的版本是没有把IO操作交过操作系统处理的,所以是属于同步的。如果别人说Netty是异步非阻塞,如果要深究,那真要看看Netty新的版本是否把IO操作交过操作系统处理,或者看看有否使用JDK1.7中的AIO API,否则他们说的异步其实是指客户端程序调用Netty的IO操作API“不停顿等待”。 - 很多人所讲的异步其实指的是编程模型上的异步(即回调),而非应用程序的异步。 -#### NIO与Epoll +## NIO与Epoll - Linux2.6之后支持epoll - windows支持select而不支持epoll - 不同系统下nio的实现是不一样的,包括Sunos linux 和windows @@ -4237,7 +4237,7 @@ public static AsynchronousFileChannel open(FileDescriptor fdo, - -### 动态代理 +# 1.17 动态代理 - 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。 - 动态代理:代理类是在运行时生成的。也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。 @@ -4246,7 +4246,7 @@ public static AsynchronousFileChannel open(FileDescriptor fdo, - 前者必须基于接口,后者不需要接口,是基于继承的,但是不能代理final类和final方法; - JDK采用反射机制调用委托类的方法,CGLIB采用类似索引的方式直接调用委托类方法; - 前者效率略低于后者效率,CGLIB效率略高(不是一定的) -#### JDK动态代理 使用 +## JDK动态代理 使用 - Proxy类(代理类)的设计用到代理模式的设计思想,Proxy类对象实现了代理目标的所有接口,并代替目标对象进行实际的操作。代理的目的是在目标对象方法的基础上作增强,这种增强的本质通常就是对目标对象的方法进行拦截。所以,Proxy应该包括一个方法拦截器,来指示当拦截到方法调用时作何种处理。InvocationHandler就是拦截器的接口。 - Proxy (代理) 提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。 @@ -4257,7 +4257,7 @@ public static AsynchronousFileChannel open(FileDescriptor fdo, - 第一步,我们要有一个接口,还要有一个接口的实现类,而这个实现类呢就是我们要代理的对象, 所谓代理呢也就是在调用实现类的方法时,可以在方法执行前后做额外的工作。 - 第二步,我们要自己写一个在代理类的方法要执行时,能够做额外工作的类(拦截器),而这个类必须继承InvocationHandler接口, 为什么要继承它呢?因为代理类的实例在调用实现类的方法的时候,不会调用真正的实现类的这个方法, 而是转而调用这个类的invoke方法(继承时必须实现的方法),在这个方法中你可以调用真正的实现类的这个方法。 -#### JDK动态代理 原理 +## JDK动态代理 原理 - Proxy#newProxyInstance - 会返回一个实现了指定接口的代理对象,对该对象的所有方法调用都会转发给InvocationHandler.invoke()方法。 @@ -4321,7 +4321,7 @@ public static Object newProxyInstance(ClassLoader loader, } } -##### 1)getProxyClass0(生成代理类的class) +### 1)getProxyClass0(生成代理类的class) - 最终生成是通过ProxyGenerator的generateProxyClass方法实现的。 ``` @@ -4789,10 +4789,10 @@ private byte[] generateClassFile() { ``` -##### 2)getConstructor(获取代理类的构造方法) -##### 3)newInstance(初始化代理对象) +### 2)getConstructor(获取代理类的构造方法) +### 3)newInstance(初始化代理对象) -#### CGLIB动态代理 使用 +## CGLIB动态代理 使用 - CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理。 - CGLIB的核心类: - net.sf.cglib.proxy.Enhancer – 主要的增强类 @@ -4853,7 +4853,7 @@ public class Main { - 既然是继承就不得不考虑final的问题。我们知道final类型不能有子类,所以CGLIB不能代理final类型。 - final方法是不能重载的,所以也不能通过CGLIB代理,遇到这种情况不会抛异常,而是会跳过final方法只代理其他方法。 -#### CGLIB动态代理 原理 +## CGLIB动态代理 原理 - 1、生成代理类Class的二进制字节码(基于ASM); - 2、通过 Class.forName加载二进制字节码,生成Class对象; - 3、通过反射机制获取实例构造,并初始化代理类对象。 @@ -4887,7 +4887,7 @@ private static class FastClassInfo - f1指向委托类对象,f2指向代理类对象 - i1是被代理的方法在对象中的索引位置 - i2是CGLIB$被代理的方法$0在对象中的索引位置 -##### FastClass实现机制 +### FastClass实现机制 - FastClass其实就是对Class对象进行特殊处理,提出下标概念index,通过索引保存方法的引用信息,将原先的反射调用,转化为方法的直接调用,从而体现所谓的fast。 @@ -4897,12 +4897,12 @@ private static class FastClassInfo - -### 反射 +# 1.18 反射 - Java的动态性体现在:反射机制、动态执行脚本语言、动态操作字节码 - 反射:在运行时加载、探知、使用编译时未知的类。 - Class.forName使用的类加载器是调用者的类加载器 -#### Class +## Class - 表示Java中的类型(class、interface、enum、annotation、primitive type、void)本身。 @@ -4911,9 +4911,9 @@ private static class FastClassInfo - 反射的核心就是Class - 如果多次执行forName等加载类的方法,类只会被加载一次;一个类只会形成一个Class对象,无论执行多少次加载类的方法,获得的Class都是一样的。 -#### 用途 +## 用途 -#### 性能 +## 性能 - 反射带来灵活性的同时,也有降低程序执行效率的弊端 - setAccessible方法不仅可以标记某些私有的属性方法为可访问的属性方法,并且可以提高程序的执行效率 @@ -4925,10 +4925,10 @@ private static class FastClassInfo - 2)产生很多临时对象,造成GC与计算时间消耗 - 3)由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一) - 当然,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。 -#### 实现 +## 实现 - 反射在Java中可以直接调用,不过最终调用的仍是native方法,以下为主流反射操作的实现。 -##### Class.forName的实现 +### Class.forName的实现 - Class.forName可以通过包名寻找Class对象,比如Class.forName("java.lang.String")。 - 在JDK的源码实现中,可以发现最终调用的是native方法forName0(),它在JVM中调用的实际是FindClassFromCaller(),原理与ClassLoader的流程一样。 @@ -5045,7 +5045,7 @@ JVM_END - -##### getDeclaredFields的实现 +### getDeclaredFields的实现 - 在JDK源码中,可以知道class.getDeclaredFields()方法实际调用的是native方法getDeclaredFields0(),它在JVM主要实现步骤如下: - 1)根据Class结构体信息,获取field_count与fields[]字段,这个字段早已在load过程中被放入了 - 2)根据field_count的大小分配内存、创建数组 @@ -5104,7 +5104,7 @@ private static Field[] copyFields(Field[] arg) { - -##### Method.invoke的实现 +### Method.invoke的实现 - 以下为无同步、无异常的情况下调用的步骤 - 1)创建Frame @@ -5205,7 +5205,7 @@ JVM_END - -##### class.newInstance的实现 +### class.newInstance的实现 - 1)检测权限、预分配空间大小等参数 - 2)创建Object对象,并分配空间 - 3)通过Method.invoke调用构造函数(()) @@ -5279,8 +5279,8 @@ public T newInstance() - -### XML -#### DOM +# 1.19 XML +## DOM - OM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。由于它是基于信息层次的,因而DOM被认为是基于树或基于对象的。 - 优点 @@ -5290,7 +5290,7 @@ public T newInstance() - ①通常需要加载整个XML文档来构造层次结构,消耗资源大。 - -#### SAX +## SAX - SAX处理的优点非常类似于流媒体的优点。分析能够立即开始,而不是等待所有的数据被处理。而且,由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中。这对于大型文档来说是个巨大的优点。事实上,应用程序甚至不必解析整个文档;它可以在某个条件得到满足时停止解析。一般来说,SAX还比它的替代者DOM快许多。 - 优点 - ①不需要等待所有数据都被处理,分析就能立即开始。 @@ -5307,8 +5307,8 @@ public T newInstance() - -### Java8 -#### Lambda表达式&函数式接口&方法引用&Stream API +# 1.20 Java8 +## Lambda表达式&函数式接口&方法引用&Stream API - Java8 stream迭代的优势和区别;lambda表达式?为什么要引入它 - 1)流(高级Iterator):对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation),隐式迭代等,代码简洁 @@ -5319,773 +5319,48 @@ public T newInstance() - 类::静态方法 - 类::实例方法名 -#### Optional +## Optional - Optional仅仅是一个容易:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查。 -#### CompletableFuture +## CompletableFuture - 1)实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果) - 2)将批量同步操作转为异步操作(并行流/CompletableFuture) - 3)多个异步任务合并 -#### 时间日期API +## 时间日期API - 新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。 -#### 接口中的默认方法与静态方法 +## 接口中的默认方法与静态方法 - 默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。 - 默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。 - -### Java9 -#### 模块化 +# 1.21 Java9 +## 模块化 - 提供了类似于OSGI框架的功能,模块之间存在相互的依赖关系,可以导出一个公共的API,并且隐藏实现的细节,Java提供该功能的主要的动机在于,减少内存的开销,在JVM启动的时候,至少会有30~60MB的内存加载,主要原因是JVM需要加载rt.jar,不管其中的类是否被classloader加载,第一步整个jar都会被JVM加载到内存当中去,模块化可以根据模块的需要加载程序运行需要的class。 - 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。使得JDK可以在更小的设备中使用。采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非是整个JDK框架了。 -#### HTTP/2 +## HTTP/2 - Java 9的版本中引入了一个新的package:java.net.http,里面提供了对Http访问很好的支持,不仅支持Http1.1而且还支持HTTP/2,以及WebSocket,据说性能特别好。 -#### JShell +## JShell - java9引入了jshell这个交互性工具,让Java也可以像脚本语言一样来运行,可以从控制台启动 jshell ,在 jshell 中直接输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。 - 除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。 -#### 不可变集合工厂方法 +## 不可变集合工厂方法 - Java 9增加了List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法来创建不可变集合。 -#### 私有接口方法 +## 私有接口方法 - Java 8 为我们提供了接口的默认方法和静态方法,接口也可以包含行为,而不仅仅是方法定义。 - 默认方法和静态方法可以共享接口中的私有方法,因此避免了代码冗余,这也使代码更加清晰。如果私有方法是静态的,那这个方法就属于这个接口的。并且没有静态的私有方法只能被在接口中的实例调用。 -#### 多版本兼容 JAR +## 多版本兼容 JAR - 当一个新版本的 Java 出现的时候,你的库用户要花费很长时间才会切换到这个新的版本。这就意味着库要去向后兼容你想要支持的最老的 Java 版本 (许多情况下就是 Java 6 或者 7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本。 -#### 统一 JVM 日志 +## 统一 JVM 日志 - Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等。 -#### 垃圾收集机制 +## 垃圾收集机制 - Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时把G1设为默认的垃圾回收器实现。替代了之前默认使用的Parallel GC,对于这个改变,evens的评论是酱紫的:这项变更是很重要的,因为相对于Parallel来说,G1会在应用线程上做更多的事情,而Parallel几乎没有在应用线程上做任何事情,它基本上完全依赖GC线程完成所有的内存管理。这意味着切换到G1将会为应用线程带来额外的工作,从而直接影响到应用的性能 -#### I/O 流新特性 +## I/O 流新特性 - java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。 - readAllBytes:读取 InputStream 中的所有剩余字节。 - readNBytes: 从 InputStream 中读取指定数量的字节到数组中。 - transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。 - - -## 设计模式 -### 设计原则 -#### 单一职责原则 -- 不要存在多于一个导致类变更的原因。 -- 总结:一个类只负责一项职责。 -#### 里氏替换原则 -- 1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 -- 2.子类中可以增加自己特有的方法。 -- 3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 -- 4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 -- 总结:所有引用父类的地方必须能透明地使用其子类对象 -#### 依赖倒置原则/面向接口编程 -- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 -#### 接口隔离原则 -- 使用多个专门的接口来替代一个统一的接口; -- 一个类对另一个类的依赖应建立在最小的接口上 -#### 迪米特法则 -- 一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部 -#### 开闭原则 -- 对扩展开放,对修改关闭 -- 用抽象构建框架,用实现扩展细节 -#### 合成复用原则/组合优于继承 -- 尽量多使用组合和聚合,尽量少使用甚至不使用继承关系 -- - -### 分类 -- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 -- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 -- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 - -### 创建型设计模式 -#### 工厂方法模式 -##### 介绍 -- 工厂模式分为简单(静态)工厂模式、工厂方法模式和抽象工厂模式 - - - 1) 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。 - - 2) 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。 - - 3) 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。 - -- 简单工厂模式:一个工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, -- 工厂方法模式:一个抽象产品类,可以派生出多个具体产品类。    -- 一个抽象工厂类,可以派生出多个具体工厂类。 --    每个具体工厂类只能创建一个具体产品类的实例。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)客户不需要知道要使用的对象的创建过程 - - 2)客户使用的对象存在变动的可能,或者根本就不知道使用哪一个具体对象 -- 缺点: -- 类的数量膨胀 -- - -#### 抽象工厂模式 -##### 介绍 -- 抽象工厂模式:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。 --    一个抽象工厂类,可以派生出多个具体工厂类。 --    每个具体工厂类可以创建多个具体产品类的实例。 -- 区别: 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。 --    工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)系统中有多个产品族,而系统一次只能消费其中一族产品 - - 2)同属于同一个产品族的产品一起使用 - -- - -#### 单例模式 -##### 介绍 -- 通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 -##### UML - -##### 适用场景与优缺点 -- 使用场景: - - 1)当类只有一个实例且客户可以从一个众所周知的访问点 访问它 - - 2)当这个唯一实例应该是通过子类化可扩展的,且客户应该无序更改代码就能使用一个扩展的实例 - -- 优点: - - 1)对唯一实例的受控访问 - - 2)缩小命名空间,避免命名污染 - - 3)允许单例有子类 - - 4)允许可变数目的实例 - - -``` -public class Car{ -``` - -- //懒汉式,线程不安全 - -``` - private static Car instance; -``` - - -``` - private Car() {} -``` - - -``` - public static Car getInstance() { -``` - -- if(instance == null) { -- instance = new Car(); -- } -- return instance; -- } -- 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。 - -- //懒汉式,线程安全 - -``` - private static Car instance ; -``` - - -``` - private Car() {} -``` - - -``` - public static synchronized Car getInstance(){ -``` - -- if(instance == null) { -- instance = new Car(); -- } -- return instance; -- } -- 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。 - -- //饿汉式 - -``` - private static Car instance = new Car(); -``` - - -``` - private Car() {} -``` - - -``` - public static Car getInstance() { -``` - -- return instance; -- } -- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。 - -- //饿汉式变种 - -``` - private static Car instance; -``` - -- static { -- instance = new Car(); -- } - -``` - private Car() {} -``` - - -``` - public static Car getInstance() { -``` - -- return instance; -- } -- 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。 -- -- //静态内部类(类的加载是线程安全的) - -``` - private static class CarHolder{ -``` - - -``` - private static final Car instance = new Car(); -``` - -- } - -``` - private Car() {} -``` - - -``` - public static Car getInstance() { -``` - -- return CarHolder.instance; -- } -- 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。 -- -- // 枚举 - -``` -public enum Car { -``` - -- INSTANCE; -- } -- //双重校验锁 - -``` - private volatile static Car instance; -``` - - -``` - private Car() {} -``` - - -``` - public static Car getInstance() { -``` - -- if(instance == null) { -- synchronized(Car.class) { -- if(instance == null) { -- instance = new Car(); -- } -- } -- } -- return instance; -- } -- 这个是第二种方式的升级版,俗称双重检查锁定。 -- 所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步, 而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块, 这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。 -- 双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile -- 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。 -- 说明:由于volatile关键字可能会屏蔽掉虚拟机中的一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用”双重检查加锁“机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。 -- } -- - -#### 建造者模式 -##### 介绍 -- 将一个复杂对象的创建和它的表示分离,使得同样的创建过程可以创建不同的表示。 - -- Builder用于构建组件 -- Director负责装配 -- 客户端通过Director来获得最终产品,Director与Builder打交道,持有一个Builder的引用。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的赚个屁方式时 - - 2)当构造过程必须允许被构造的对象有不同的表示时 - -- 优点: - - 1)可以改变一个对象的内部表示:Builder对象提供给Director一个构造产品的抽象接口,该接口使得Buildewr可以隐藏这个产品的表示和内部结构,同时隐藏了该产品是如何装配的。 - - 2)将构造代码与表示代码分离 - - 3)可以对构造过程进行更精细化的控制 -- - -#### 原型模式 -##### 介绍 - -##### UML - -##### 适用场景与优缺点 -- • 当要实例化的类是在运行时刻指定时,例如,通过动态装载; -- • 为了避免创建一个与产品类层次平行的工厂类层次时; -- • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。 - -- 优点: -- 性能优良。原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。 -- 缺点: -- 逃避构造函数的约束。这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。 - -- - -### 结构型设计模式 -#### 适配器模式 -##### 介绍 -- 将一个类的接口转换成客户所期待的另一种接口 - -- Adapter可以组合+实现(对象适配器方式),也可以继承+实现(类适配器方式)。但是继承不如组合好,因此尽量使用组合+实现。 - -##### UML - - -##### 适用场景与优缺点 -- 适用场景: - - 1)想使用一个已存在的类,而它的接口不符合你的需求 - - 2)想创建一个可以复用的类,该类可以与不相关的类或不可预见的类协同工作 -#### 装饰器模式 -##### 介绍 - - -- - -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)在不影响其他对象的情况下,以动态透明的方式给单个对象添加职责 - - 2)处理那些可以撤销的职责 - - 3)当不能通过生成子类的方法进行扩充时 -- 优点: - - 1)比继承更加灵活,可以用添加和分离的方式,用装饰在运行时 增加和删除职责 - - 2)避免在层次结构高的类有太多特征,用装饰器为其逐渐地添加功能 -- - -#### 代理模式 -##### 介绍 -- 代理可以分为静态代理和动态代理 -- 为其他对象提供一种代理以控制对这个对象的访问。 -为了一个对象提供一个替身或者占位符,以控制对这个对象的访问 - -- * 远程代理能够控制访问远程对象(RMI) -- * 虚拟代理控制访问创建开销大的资源(先创建一个资源消耗较小的对象表示,真实对象只在需要时才会被创建) -- * 保护代理基于权限控制对资源的访问 -##### UML - -##### 适用场景与优缺点 -- 使用场景: -- 按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。 - -- 优点: -- 1、职责清晰。 2、高扩展性。 3、智能化。 -- 缺点: -- 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 -- 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。 -#### 外观模式 -##### 介绍 -- 为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这一接口使得子系统更加容易使用。 - -- 遵循了迪米特法则: - -``` -通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。 -``` - -- 外观模式就是一种较好的封装 -- 是整体和子组件之间的关系,外部类不应该与一个类的子组件过多的接触,应该尽可能与整体打交道。 - -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)为一个复杂子系统提供一个简单接口 - - 2)客户与抽象类的实现部分之间存在着很大的依赖性,引入Facade将子系统与客户解耦,提高了子系统的独立性和可移植性 -- 优点: - - 1)对客户屏蔽了子系统组件,减少客户处理的对象数目,并使得子系统使用起来更加容易 - - 2)实现了子系统与客户之间的松耦合 - - 3)降低了大型软件中的编译依赖性 - - 4)只是提供了一个访问子系统的统一入口,并不影响客户直接使用子系统 -#### 桥接模式 -##### 介绍 -- 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立地扩展,在抽象层建立关联。 -- 一个维度的父类持有另一个维度的接口的引用(使用组合代替了继承) - - -- 希望有一个Bridge类来将类型维度和品牌维度连接起来,这样增加类型和增加品牌不会影响对方。 -- 两种变化以上的情况应该考虑桥接模式 -##### UML - -##### 适用场景与优缺点 -- 适用场景: -- 1.如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。 -- 2.设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。 -- 3.一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。 -- 4.虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。 - - -- - -#### 组合模式(树形结构) -##### 介绍 - -- 无子节点的是叶子,有子节点的是容器 -- 叶子和容器的共同点抽象为Component组件 -- 每个容器持有一个Component的List引用,包含它的所有子节点。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)想表示对象的层次结构 - - 2)希望客户忽略组合对象与单一对象的不同,用户将统一使用组合结构中的所有对象 -- 优点: - - 1)定义了包含基本对象和组合对象的类层次结构 - - 2)简化客户代码,客户可以一致地使用组合结构和单个对象 - - 3)更容易添加新类型的组件 -#### 享元模式 -##### 介绍 - - -- 将相同部分放在一个类中,工厂持有一个Map,可以创建相同部分,如果已持有那么直接返回。 -- 不同部分单独设计一个类,可以作为相同部分类的方法的参数传入 -- 将一个对象拆成两部分(成员变量拆成两部分):相同部分和不同部分。相同部分使用工厂创建,进行共享;不同部分作为参数传入 -##### UML - -##### 适用场景与优缺点 -- 适用场景:池化 内存池 数据库连接池 线程池 -- 优点: - - 1)极大减少内存中对象的数量 - - 2)相同或相似对象内存中只存一份,节省内存 - - 3)外部状态相对独立,不影响内部状态 - -- 缺点: - - 1)模式较复杂,使程序逻辑复杂化 - - 2)读取外部状态使运行时间变长,用时间换取了空间 - -- - -### 行为型设计模式 -#### 策略模式 -##### 介绍 -- 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。 -- 环境类(Context):用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可定义一个接口来让Strategy访问它的数据。 -- 抽象策略类(Strategy):定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy定义的算法。 -- 具体策略类(ConcreteStrategy):以Strategy接口实现某具体算法。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: -- 实现某一个功能由多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能 -- 优点: - - 1)Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。 - - 2)提供了可以替换继承关系的方法 - - 3)消除if-else -#### 模板方法模式 -##### 介绍 - - -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)一次性实现一个算法的不变部分,并将可变部分留给子类来实现 - - 2)个子类中公共的行为提取出来并集中到一个公共父类中以避免重复 - - 3)控制子类扩展,只允许在某些点进行扩展 - -- 优点: - - 1)在一个父类中形式化地定义算法,由它的子类实现细节的处理 - - 2)是一个代码复用的基本技术 - - 3)控制翻转(好莱坞原则),父类调用子类的操作,通过对子类的扩展来增加新的行为,符合开闭原则 -#### 观察者模式 -##### 介绍 -- 也称为发布-订阅模式。 -- 在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。 -- 与Reactor模式非常类似,不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)当一个对象的改变需要通知其他对象,而且它不知道具体有多少个对象有待通知时 - - 2)当一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这二者封装在独立的对象中国以使它们可以独立地改变和服用 -- 优点: - - 1)独立地改变目标和观察者,解耦 - - 2)是吸纳表示层和数据逻辑层分离(表示层是观察者,逻辑层是主题) -#### 迭代器模式 -##### 介绍 -- 找到一种不同容器的统一的遍历方式,定义一个接口,所有可以提供遍历方式的容器都实现这个接口,返回一个迭代器,然后所有的迭代器的接口是一致的。 - -- 所有的容器都可以通过iterator方法返回一个迭代器Iterator,这个迭代器对外暴露的接口是一致的,因此可以保证对所有的容器遍历方法是一致的,仅需得到这个容器的迭代器即可,而各个容器对迭代器的实现是不同的,即遍历方式是不同的。迭代器模式可以将各个容器的遍历方式的调用方式统一起来,隐藏了内部遍历的实现细节。 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)访问一个聚合对象的内容而无需暴露它的内部表示 - - 2)需要为聚合对象提供多种遍历方式 - - 3)为遍历不同的聚合结构提供一个统一的接口 - -- 优点: - - 1)支持以不同的方式遍历一个聚合对象 - - 2)简化聚合接口 - - 3)方便添加新的聚合类和迭代器类 -#### 责任链模式 -##### 介绍 -- 使多个处理器对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些处理器对象连成一条链,并沿这条链传递请求,直到有一个处理器对象处理它为止 -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)有多个处理器对象可以处理一个请求,哪个处理器对象处理该请求在运行时动态确定 - - 2)在不明确指定接收者的情况下,向多个处理器对象中的一个提交请求 - - 3)可以动态指定一组处理器对象处理请求 - -- 优点: - - 1)降低耦合,使得 请求发送者无需知道是哪个处理器对象处理请求 - - 2)简化对象的相互连接 - - 3)增强了给对象指派责任的灵活性 - - 4)方便添加新的请求处理类 -#### 命令模式 -##### 介绍 -- 命令模式把一个请求或者操作封装到一个对象中,把发出命令的责任和执行命令的责任分割开,委派给不同的对象,可降低行为请求者与行为实现者之间耦合度。从使用角度来看就是请求者把接口实现类作为参数传给使用者,使用者直接调用这个接口的方法,而不用关心具体执行的那个命令。 - -- Command模式将操作的执行逻辑封装到一个个Command对象中,解耦了操作发起者和操作执行逻辑之间的耦合关系:操作发起者要进行一个操作,不用关心具体的执行逻辑,只需创建一个相应的Command实例,调用它的执行接口即可。而在swing中,与界面交互的各种操作,比如插入,删除等被称之为Edit,实际上就是Command。 -- 使用undo包很简单,主要操作步骤如下: -- 1、创建CommandManager实例(持有Command的undo栈和redo栈); -- 2、创建各种实现Command的具体操作类; -- 3、调用某种操作时,创建一个具体操作类的实例,加入CommandManager; -- 4、在Undo/Redo时,直接调用CommandManager的undo/redo方法。 -##### UML - -- 黑色箭头表示持有,关联关系 Client持有Invoker -- 菱形箭头也是持有,聚合关系 Invoker持有Command -- 白色箭头是继承,ConcreteCommand继承了Command -##### 适用场景与优缺点 -- 适用场景: - - 1)系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互 - - 2)系统需要在不同的时间制定请求,将请求排序和执行请求 - - 3)系统需要支持undo和redo操作 - - 4)系统需要将一组操作组合在一起 - -- 优点: - - 1)降低系统的耦合度,调用者和接收者解耦 - - 2)Command是头等对象,可以被操纵和扩展 - - 3)组合命令 - - 4)方便实现undo和redo -- - -#### 备忘录模式 -##### 介绍 - -- Originate是实体类,并负责创建和恢复Memento(比如JavaBean) -- Memento负责保存对象的状态 -- CareTaker 负责存储Memento(一个或一系列)(多条历史记录) -- Originate除了对象的属性和setter getter之外,还有创建和恢复Memento的方法 -- Memento也持有对象的所有属性和setter getter,它的构造方法是由Originate对象得到其内部状态。 -- CareTaker持有一个或一组Memento,并提供setter and getter - -##### UML - -##### 适用场景与优缺点 -- 适用场景: -- 1、需要保存/恢复数据的相关状态场景。 -- 2、提供一个可回滚的操作。 - -- 优点: -- 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 -- 2、实现了信息的封装,使得用户不需要关心状态的保存细节。 - -- 缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。 -#### 状态模式 -##### 介绍 - -##### UML - -##### 适用场景与优缺点 -- 适用场景: - - 1)一个对象的行为取决于它的状态 - - 2)代码中包含大量的与对象状态有关的条件语句 - -- 优点: - - 1)将与特定状态相关的行为局部化,并且将不同状态的行为分割开来 - - 2)使得状态转换显式化 - - 3)State对象可被共享 - -- - -#### 访问者模式 -##### 介绍 - -##### UML - -##### 适用场景与优缺点 - -#### 中介者模式 -##### 介绍 -- 解耦多个同事对象之间的交互关系。 -- 每个同事对象都持有中介者对象的引用,只跟中介者打交道。我们通过中介者统一管理这些交互关系。 - -- 每个同事类都持有一个中介者类的引用。 -##### UML - -- 将多对多的关系解耦后转为一对多的关系,每个对象和中介者打交道,不直接和其他对象打交道。 -- 如果关系比较简单,那么没有必要使用中介者模式,反而会复杂化。 -##### 适用场景与优缺点 -- MVC中的C就是中介者 - -- 适用场景: - - 1)系统中对象之间存在着复杂的引用关系 - - 2)一组对象以定义良好但复杂的方式进行通信 - - 3)一个对象引用其他很多对象并直接与这些对象通信,导致难以复用该对象 - -- 优点: - - 1)减少子类生成 - - 2)简化同事类的设计和实现 - - 3)简化对象协议(一对多代替多对多) - - 4)对对象如何协作进行了抽象 - - 5)使控制集中化(将交互复杂性变为中介者的复杂性) -#### 解释器模式 -##### 介绍 - -##### UML - -##### 适用场景与优缺点 - - -### 设计模式的区分 -#### 代理模式和装饰器区别 -- 装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。 -- 因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 - -- 相同点:都是为被代理(被装饰)的类扩充新的功能。 -- 不同点:代理模式具有控制被代理类的访问等性质,而装饰模式紧紧是单纯的扩充被装饰的类。所以区别仅仅在是否对被代理/被装饰的类进行了控制而已。 - -#### 适配器模式和代理模式的区别 -- 适配器模式,一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。 - -- 装饰器模式,原有的不能满足现有的需求,对原有的进行增强。 - -- 代理模式,同一个类而去调用另一个类的方法,不对这个方法进行直接操作,控制访问。 -#### 抽象工厂和工厂方法模式的区别 -- 工厂方法:创建某个具体产品 -- 抽象工厂:创建某个产品族中的系列产品 - -工厂方法模式 抽象工厂模式 -针对的是一个产品等级结构 针对的是面向多个产品等级结构 -一个抽象产品类 多个抽象产品类 -可以派生出多个具体产品类 每个抽象产品类可以派生出多个具体产品类 -一个抽象工厂类,可以派生出多个具体工厂类 一个抽象工厂类,可以派生出多个具体工厂类 -每个具体工厂类只能创建一个具体产品类的实例 每个具体工厂类可以创建多个具体产品类的实例 - -- - -### JDK中的设计模式(17) -#### 创建型 - - 1)工厂方法 -- Collection.iterator() 由具体的聚集类来确定使用哪一个Iterator - - 2)单例模式 -- Runtime.getRuntime() - - 3)建造者模式 -- StringBuilder - - 4)原型模式 -- Java中的Cloneable -#### 结构性 - - 1)适配器模式 -- InputStreamReader -- OutputStreamWriter -- RunnableAdapter - - 2)装饰器模式 -- io包 FileInputStream BufferedInputStream - - 3)代理模式 -- 动态代理;RMI - - 4)外观模式 -- java.util.logging - - 5)桥接模式 -- JDBC - - 6)组合模式 -- dom - - 7)享元模式 -- Integer.valueOf -#### 行为型 - - 1)策略模式 -- 线程池的四种拒绝策略 - - 2)模板方法模式 -- AbstractList、AbstractMap等 -- InputStream、OutputStream -- AQS - - 3)观察者模式 -- Swing中的Listener - - 4)迭代器模式 -- 集合类中的iterator - - 5)责任链模式 -- J2EE中的Filter - - 6)命令模式 -- Runnable、Callable,ThreadPoolExecutor - - 7)备忘录模式 - - 8)状态模式 - - 9)访问者模式 -- 10)中介者模式 - - 11)解释器模式 -- - -### Spring中的设计模式(6) - - 1)抽象工厂模式: -- BeanFactory - - 2)代理模式: -- AOP - - 3)模板方法模式: -- AbstractApplicationContext中定义了一系列的抽象方法,比如refreshBeanFactory、closeBeanFactory、getBeanFactory。 - - 4)单例模式: -- Spring可以管理单例对象,控制对象为单例 - - 5)原型模式: -- Spring可以管理多例对象,控制对象为prototype - - 6)适配器模式: -- Advice与Interceptor的适配 -- Adapter类接口:Target - -``` -public interface AdvisorAdapter { -``` - --   -- boolean supportsAdvice(Advice advice); --   --       MethodInterceptor getInterceptor(Advisor advisor); --   -- } MethodBeforeAdviceAdapter类,Adapter -- class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { --   - -``` -      public boolean supportsAdvice(Advice advice) { -``` - --             return (advice instanceof MethodBeforeAdvice); --       } --   - -``` -      public MethodInterceptor getInterceptor(Advisor advisor) { -``` - --             MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); --       return new MethodBeforeAdviceInterceptor(advice); --       } --   -- } From 4b6cb33aba80de133783872ac637a07a8551e089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:47:13 +0800 Subject: [PATCH 50/97] =?UTF-8?q?Create=20=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...76\350\256\241\346\250\241\345\274\217.md" | 725 ++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 "docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" diff --git "a/docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..a7b0d793 --- /dev/null +++ "b/docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,725 @@ + +# 设计模式 +# 1.22 设计原则 +## 单一职责原则 +- 不要存在多于一个导致类变更的原因。 +- 总结:一个类只负责一项职责。 +## 里氏替换原则 +- 1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 +- 2.子类中可以增加自己特有的方法。 +- 3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 +- 4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 +- 总结:所有引用父类的地方必须能透明地使用其子类对象 +## 依赖倒置原则/面向接口编程 +- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 +## 接口隔离原则 +- 使用多个专门的接口来替代一个统一的接口; +- 一个类对另一个类的依赖应建立在最小的接口上 +## 迪米特法则 +- 一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部 +## 开闭原则 +- 对扩展开放,对修改关闭 +- 用抽象构建框架,用实现扩展细节 +## 合成复用原则/组合优于继承 +- 尽量多使用组合和聚合,尽量少使用甚至不使用继承关系 +- + +# 1.23 分类 +- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 +- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 +- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 + +# 1.24 创建型设计模式 +## 工厂方法模式 +### 介绍 +- 工厂模式分为简单(静态)工厂模式、工厂方法模式和抽象工厂模式 + + - 1) 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。 + - 2) 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。 + - 3) 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。 + +- 简单工厂模式:一个工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, +- 工厂方法模式:一个抽象产品类,可以派生出多个具体产品类。    +- 一个抽象工厂类,可以派生出多个具体工厂类。 +-    每个具体工厂类只能创建一个具体产品类的实例。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)客户不需要知道要使用的对象的创建过程 + - 2)客户使用的对象存在变动的可能,或者根本就不知道使用哪一个具体对象 +- 缺点: +- 类的数量膨胀 +- + +## 抽象工厂模式 +### 介绍 +- 抽象工厂模式:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。 +-    一个抽象工厂类,可以派生出多个具体工厂类。 +-    每个具体工厂类可以创建多个具体产品类的实例。 +- 区别: 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。 +-    工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)系统中有多个产品族,而系统一次只能消费其中一族产品 + - 2)同属于同一个产品族的产品一起使用 + +- + +## 单例模式 +### 介绍 +- 通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例 +### UML + +### 适用场景与优缺点 +- 使用场景: + - 1)当类只有一个实例且客户可以从一个众所周知的访问点 访问它 + - 2)当这个唯一实例应该是通过子类化可扩展的,且客户应该无序更改代码就能使用一个扩展的实例 + +- 优点: + - 1)对唯一实例的受控访问 + - 2)缩小命名空间,避免命名污染 + - 3)允许单例有子类 + - 4)允许可变数目的实例 + + +``` +public class Car{ +``` + +- //懒汉式,线程不安全 + +``` + private static Car instance; +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- if(instance == null) { +- instance = new Car(); +- } +- return instance; +- } +- 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。 + +- //懒汉式,线程安全 + +``` + private static Car instance ; +``` + + +``` + private Car() {} +``` + + +``` + public static synchronized Car getInstance(){ +``` + +- if(instance == null) { +- instance = new Car(); +- } +- return instance; +- } +- 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。 + +- //饿汉式 + +``` + private static Car instance = new Car(); +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return instance; +- } +- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。 + +- //饿汉式变种 + +``` + private static Car instance; +``` + +- static { +- instance = new Car(); +- } + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return instance; +- } +- 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。 +- +- //静态内部类(类的加载是线程安全的) + +``` + private static class CarHolder{ +``` + + +``` + private static final Car instance = new Car(); +``` + +- } + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- return CarHolder.instance; +- } +- 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。 +- +- // 枚举 + +``` +public enum Car { +``` + +- INSTANCE; +- } +- //双重校验锁 + +``` + private volatile static Car instance; +``` + + +``` + private Car() {} +``` + + +``` + public static Car getInstance() { +``` + +- if(instance == null) { +- synchronized(Car.class) { +- if(instance == null) { +- instance = new Car(); +- } +- } +- } +- return instance; +- } +- 这个是第二种方式的升级版,俗称双重检查锁定。 +- 所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步, 而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块, 这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。 +- 双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile +- 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。 +- 说明:由于volatile关键字可能会屏蔽掉虚拟机中的一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用”双重检查加锁“机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。 +- } +- + +## 建造者模式 +### 介绍 +- 将一个复杂对象的创建和它的表示分离,使得同样的创建过程可以创建不同的表示。 + +- Builder用于构建组件 +- Director负责装配 +- 客户端通过Director来获得最终产品,Director与Builder打交道,持有一个Builder的引用。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的赚个屁方式时 + - 2)当构造过程必须允许被构造的对象有不同的表示时 + +- 优点: + - 1)可以改变一个对象的内部表示:Builder对象提供给Director一个构造产品的抽象接口,该接口使得Buildewr可以隐藏这个产品的表示和内部结构,同时隐藏了该产品是如何装配的。 + - 2)将构造代码与表示代码分离 + - 3)可以对构造过程进行更精细化的控制 +- + +## 原型模式 +### 介绍 + +### UML + +### 适用场景与优缺点 +- • 当要实例化的类是在运行时刻指定时,例如,通过动态装载; +- • 为了避免创建一个与产品类层次平行的工厂类层次时; +- • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。 + +- 优点: +- 性能优良。原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。 +- 缺点: +- 逃避构造函数的约束。这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。 + +- + +# 1.25 结构型设计模式 +## 适配器模式 +### 介绍 +- 将一个类的接口转换成客户所期待的另一种接口 + +- Adapter可以组合+实现(对象适配器方式),也可以继承+实现(类适配器方式)。但是继承不如组合好,因此尽量使用组合+实现。 + +### UML + + +### 适用场景与优缺点 +- 适用场景: + - 1)想使用一个已存在的类,而它的接口不符合你的需求 + - 2)想创建一个可以复用的类,该类可以与不相关的类或不可预见的类协同工作 +## 装饰器模式 +### 介绍 + + +- + +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)在不影响其他对象的情况下,以动态透明的方式给单个对象添加职责 + - 2)处理那些可以撤销的职责 + - 3)当不能通过生成子类的方法进行扩充时 +- 优点: + - 1)比继承更加灵活,可以用添加和分离的方式,用装饰在运行时 增加和删除职责 + - 2)避免在层次结构高的类有太多特征,用装饰器为其逐渐地添加功能 +- + +## 代理模式 +### 介绍 +- 代理可以分为静态代理和动态代理 +- 为其他对象提供一种代理以控制对这个对象的访问。 +为了一个对象提供一个替身或者占位符,以控制对这个对象的访问 + +- * 远程代理能够控制访问远程对象(RMI) +- * 虚拟代理控制访问创建开销大的资源(先创建一个资源消耗较小的对象表示,真实对象只在需要时才会被创建) +- * 保护代理基于权限控制对资源的访问 +### UML + +### 适用场景与优缺点 +- 使用场景: +- 按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。 + +- 优点: +- 1、职责清晰。 2、高扩展性。 3、智能化。 +- 缺点: +- 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 +- 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。 +## 外观模式 +### 介绍 +- 为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这一接口使得子系统更加容易使用。 + +- 遵循了迪米特法则: + +``` +通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。 +``` + +- 外观模式就是一种较好的封装 +- 是整体和子组件之间的关系,外部类不应该与一个类的子组件过多的接触,应该尽可能与整体打交道。 + +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)为一个复杂子系统提供一个简单接口 + - 2)客户与抽象类的实现部分之间存在着很大的依赖性,引入Facade将子系统与客户解耦,提高了子系统的独立性和可移植性 +- 优点: + - 1)对客户屏蔽了子系统组件,减少客户处理的对象数目,并使得子系统使用起来更加容易 + - 2)实现了子系统与客户之间的松耦合 + - 3)降低了大型软件中的编译依赖性 + - 4)只是提供了一个访问子系统的统一入口,并不影响客户直接使用子系统 +## 桥接模式 +### 介绍 +- 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立地扩展,在抽象层建立关联。 +- 一个维度的父类持有另一个维度的接口的引用(使用组合代替了继承) + + +- 希望有一个Bridge类来将类型维度和品牌维度连接起来,这样增加类型和增加品牌不会影响对方。 +- 两种变化以上的情况应该考虑桥接模式 +### UML + +### 适用场景与优缺点 +- 适用场景: +- 1.如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。 +- 2.设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。 +- 3.一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。 +- 4.虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。 + + +- + +## 组合模式(树形结构) +### 介绍 + +- 无子节点的是叶子,有子节点的是容器 +- 叶子和容器的共同点抽象为Component组件 +- 每个容器持有一个Component的List引用,包含它的所有子节点。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)想表示对象的层次结构 + - 2)希望客户忽略组合对象与单一对象的不同,用户将统一使用组合结构中的所有对象 +- 优点: + - 1)定义了包含基本对象和组合对象的类层次结构 + - 2)简化客户代码,客户可以一致地使用组合结构和单个对象 + - 3)更容易添加新类型的组件 +## 享元模式 +### 介绍 + + +- 将相同部分放在一个类中,工厂持有一个Map,可以创建相同部分,如果已持有那么直接返回。 +- 不同部分单独设计一个类,可以作为相同部分类的方法的参数传入 +- 将一个对象拆成两部分(成员变量拆成两部分):相同部分和不同部分。相同部分使用工厂创建,进行共享;不同部分作为参数传入 +### UML + +### 适用场景与优缺点 +- 适用场景:池化 内存池 数据库连接池 线程池 +- 优点: + - 1)极大减少内存中对象的数量 + - 2)相同或相似对象内存中只存一份,节省内存 + - 3)外部状态相对独立,不影响内部状态 + +- 缺点: + - 1)模式较复杂,使程序逻辑复杂化 + - 2)读取外部状态使运行时间变长,用时间换取了空间 + +- + +# 1.26 行为型设计模式 +## 策略模式 +### 介绍 +- 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。 +- 环境类(Context):用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可定义一个接口来让Strategy访问它的数据。 +- 抽象策略类(Strategy):定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy定义的算法。 +- 具体策略类(ConcreteStrategy):以Strategy接口实现某具体算法。 +### UML + +### 适用场景与优缺点 +- 适用场景: +- 实现某一个功能由多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能 +- 优点: + - 1)Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。 + - 2)提供了可以替换继承关系的方法 + - 3)消除if-else +## 模板方法模式 +### 介绍 + + +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)一次性实现一个算法的不变部分,并将可变部分留给子类来实现 + - 2)个子类中公共的行为提取出来并集中到一个公共父类中以避免重复 + - 3)控制子类扩展,只允许在某些点进行扩展 + +- 优点: + - 1)在一个父类中形式化地定义算法,由它的子类实现细节的处理 + - 2)是一个代码复用的基本技术 + - 3)控制翻转(好莱坞原则),父类调用子类的操作,通过对子类的扩展来增加新的行为,符合开闭原则 +## 观察者模式 +### 介绍 +- 也称为发布-订阅模式。 +- 在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。 +- 与Reactor模式非常类似,不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)当一个对象的改变需要通知其他对象,而且它不知道具体有多少个对象有待通知时 + - 2)当一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这二者封装在独立的对象中国以使它们可以独立地改变和服用 +- 优点: + - 1)独立地改变目标和观察者,解耦 + - 2)是吸纳表示层和数据逻辑层分离(表示层是观察者,逻辑层是主题) +## 迭代器模式 +### 介绍 +- 找到一种不同容器的统一的遍历方式,定义一个接口,所有可以提供遍历方式的容器都实现这个接口,返回一个迭代器,然后所有的迭代器的接口是一致的。 + +- 所有的容器都可以通过iterator方法返回一个迭代器Iterator,这个迭代器对外暴露的接口是一致的,因此可以保证对所有的容器遍历方法是一致的,仅需得到这个容器的迭代器即可,而各个容器对迭代器的实现是不同的,即遍历方式是不同的。迭代器模式可以将各个容器的遍历方式的调用方式统一起来,隐藏了内部遍历的实现细节。 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)访问一个聚合对象的内容而无需暴露它的内部表示 + - 2)需要为聚合对象提供多种遍历方式 + - 3)为遍历不同的聚合结构提供一个统一的接口 + +- 优点: + - 1)支持以不同的方式遍历一个聚合对象 + - 2)简化聚合接口 + - 3)方便添加新的聚合类和迭代器类 +## 责任链模式 +### 介绍 +- 使多个处理器对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些处理器对象连成一条链,并沿这条链传递请求,直到有一个处理器对象处理它为止 +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)有多个处理器对象可以处理一个请求,哪个处理器对象处理该请求在运行时动态确定 + - 2)在不明确指定接收者的情况下,向多个处理器对象中的一个提交请求 + - 3)可以动态指定一组处理器对象处理请求 + +- 优点: + - 1)降低耦合,使得 请求发送者无需知道是哪个处理器对象处理请求 + - 2)简化对象的相互连接 + - 3)增强了给对象指派责任的灵活性 + - 4)方便添加新的请求处理类 +## 命令模式 +### 介绍 +- 命令模式把一个请求或者操作封装到一个对象中,把发出命令的责任和执行命令的责任分割开,委派给不同的对象,可降低行为请求者与行为实现者之间耦合度。从使用角度来看就是请求者把接口实现类作为参数传给使用者,使用者直接调用这个接口的方法,而不用关心具体执行的那个命令。 + +- Command模式将操作的执行逻辑封装到一个个Command对象中,解耦了操作发起者和操作执行逻辑之间的耦合关系:操作发起者要进行一个操作,不用关心具体的执行逻辑,只需创建一个相应的Command实例,调用它的执行接口即可。而在swing中,与界面交互的各种操作,比如插入,删除等被称之为Edit,实际上就是Command。 +- 使用undo包很简单,主要操作步骤如下: +- 1、创建CommandManager实例(持有Command的undo栈和redo栈); +- 2、创建各种实现Command的具体操作类; +- 3、调用某种操作时,创建一个具体操作类的实例,加入CommandManager; +- 4、在Undo/Redo时,直接调用CommandManager的undo/redo方法。 +### UML + +- 黑色箭头表示持有,关联关系 Client持有Invoker +- 菱形箭头也是持有,聚合关系 Invoker持有Command +- 白色箭头是继承,ConcreteCommand继承了Command +### 适用场景与优缺点 +- 适用场景: + - 1)系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互 + - 2)系统需要在不同的时间制定请求,将请求排序和执行请求 + - 3)系统需要支持undo和redo操作 + - 4)系统需要将一组操作组合在一起 + +- 优点: + - 1)降低系统的耦合度,调用者和接收者解耦 + - 2)Command是头等对象,可以被操纵和扩展 + - 3)组合命令 + - 4)方便实现undo和redo +- + +## 备忘录模式 +### 介绍 + +- Originate是实体类,并负责创建和恢复Memento(比如JavaBean) +- Memento负责保存对象的状态 +- CareTaker 负责存储Memento(一个或一系列)(多条历史记录) +- Originate除了对象的属性和setter getter之外,还有创建和恢复Memento的方法 +- Memento也持有对象的所有属性和setter getter,它的构造方法是由Originate对象得到其内部状态。 +- CareTaker持有一个或一组Memento,并提供setter and getter + +### UML + +### 适用场景与优缺点 +- 适用场景: +- 1、需要保存/恢复数据的相关状态场景。 +- 2、提供一个可回滚的操作。 + +- 优点: +- 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 +- 2、实现了信息的封装,使得用户不需要关心状态的保存细节。 + +- 缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。 +## 状态模式 +### 介绍 + +### UML + +### 适用场景与优缺点 +- 适用场景: + - 1)一个对象的行为取决于它的状态 + - 2)代码中包含大量的与对象状态有关的条件语句 + +- 优点: + - 1)将与特定状态相关的行为局部化,并且将不同状态的行为分割开来 + - 2)使得状态转换显式化 + - 3)State对象可被共享 + +- + +## 访问者模式 +### 介绍 + +### UML + +### 适用场景与优缺点 + +## 中介者模式 +### 介绍 +- 解耦多个同事对象之间的交互关系。 +- 每个同事对象都持有中介者对象的引用,只跟中介者打交道。我们通过中介者统一管理这些交互关系。 + +- 每个同事类都持有一个中介者类的引用。 +### UML + +- 将多对多的关系解耦后转为一对多的关系,每个对象和中介者打交道,不直接和其他对象打交道。 +- 如果关系比较简单,那么没有必要使用中介者模式,反而会复杂化。 +### 适用场景与优缺点 +- MVC中的C就是中介者 + +- 适用场景: + - 1)系统中对象之间存在着复杂的引用关系 + - 2)一组对象以定义良好但复杂的方式进行通信 + - 3)一个对象引用其他很多对象并直接与这些对象通信,导致难以复用该对象 + +- 优点: + - 1)减少子类生成 + - 2)简化同事类的设计和实现 + - 3)简化对象协议(一对多代替多对多) + - 4)对对象如何协作进行了抽象 + - 5)使控制集中化(将交互复杂性变为中介者的复杂性) +## 解释器模式 +### 介绍 + +### UML + +### 适用场景与优缺点 + + +# 1.27 设计模式的区分 +## 代理模式和装饰器区别 +- 装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。 +- 因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 + +- 相同点:都是为被代理(被装饰)的类扩充新的功能。 +- 不同点:代理模式具有控制被代理类的访问等性质,而装饰模式紧紧是单纯的扩充被装饰的类。所以区别仅仅在是否对被代理/被装饰的类进行了控制而已。 + +## 适配器模式和代理模式的区别 +- 适配器模式,一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。 + +- 装饰器模式,原有的不能满足现有的需求,对原有的进行增强。 + +- 代理模式,同一个类而去调用另一个类的方法,不对这个方法进行直接操作,控制访问。 +## 抽象工厂和工厂方法模式的区别 +- 工厂方法:创建某个具体产品 +- 抽象工厂:创建某个产品族中的系列产品 + +工厂方法模式 抽象工厂模式 +针对的是一个产品等级结构 针对的是面向多个产品等级结构 +一个抽象产品类 多个抽象产品类 +可以派生出多个具体产品类 每个抽象产品类可以派生出多个具体产品类 +一个抽象工厂类,可以派生出多个具体工厂类 一个抽象工厂类,可以派生出多个具体工厂类 +每个具体工厂类只能创建一个具体产品类的实例 每个具体工厂类可以创建多个具体产品类的实例 + +- + +# 1.28 JDK中的设计模式(17) +## 创建型 + - 1)工厂方法 +- Collection.iterator() 由具体的聚集类来确定使用哪一个Iterator + - 2)单例模式 +- Runtime.getRuntime() + - 3)建造者模式 +- StringBuilder + - 4)原型模式 +- Java中的Cloneable +## 结构性 + - 1)适配器模式 +- InputStreamReader +- OutputStreamWriter +- RunnableAdapter + - 2)装饰器模式 +- io包 FileInputStream BufferedInputStream + - 3)代理模式 +- 动态代理;RMI + - 4)外观模式 +- java.util.logging + - 5)桥接模式 +- JDBC + - 6)组合模式 +- dom + - 7)享元模式 +- Integer.valueOf +## 行为型 + - 1)策略模式 +- 线程池的四种拒绝策略 + - 2)模板方法模式 +- AbstractList、AbstractMap等 +- InputStream、OutputStream +- AQS + - 3)观察者模式 +- Swing中的Listener + - 4)迭代器模式 +- 集合类中的iterator + - 5)责任链模式 +- J2EE中的Filter + - 6)命令模式 +- Runnable、Callable,ThreadPoolExecutor + - 7)备忘录模式 + - 8)状态模式 + - 9)访问者模式 +- 10)中介者模式 + - 11)解释器模式 +- + +# 1.29 Spring中的设计模式(6) + - 1)抽象工厂模式: +- BeanFactory + - 2)代理模式: +- AOP + - 3)模板方法模式: +- AbstractApplicationContext中定义了一系列的抽象方法,比如refreshBeanFactory、closeBeanFactory、getBeanFactory。 + - 4)单例模式: +- Spring可以管理单例对象,控制对象为单例 + - 5)原型模式: +- Spring可以管理多例对象,控制对象为prototype + - 6)适配器模式: +- Advice与Interceptor的适配 +- Adapter类接口:Target + +``` +public interface AdvisorAdapter { +``` + +-   +- boolean supportsAdvice(Advice advice); +-   +-       MethodInterceptor getInterceptor(Advisor advisor); +-   +- } MethodBeforeAdviceAdapter类,Adapter +- class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { +-   + +``` +      public boolean supportsAdvice(Advice advice) { +``` + +-             return (advice instanceof MethodBeforeAdvice); +-       } +-   + +``` +      public MethodInterceptor getInterceptor(Advisor advisor) { +``` + +-             MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); +-       return new MethodBeforeAdviceInterceptor(advice); +-       } +-   +- } From 96d56dfc399d735837c84b10e8870d7134d16c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:48:06 +0800 Subject: [PATCH 51/97] =?UTF-8?q?Rename=20Java&=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md=20to=201.Java=E5=9F=BA=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.Java\345\237\272\347\241\200.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" => "docs/1.Java\345\237\272\347\241\200.md" (100%) diff --git "a/docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/1.Java\345\237\272\347\241\200.md" similarity index 100% rename from "docs/Java&\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "docs/1.Java\345\237\272\347\241\200.md" From bfe0d9ce181b3f5b6887f66dbd9a5a321e3392ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:48:38 +0800 Subject: [PATCH 52/97] =?UTF-8?q?Rename=20=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md=20to=202.=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2.\350\256\276\350\256\241\346\250\241\345\274\217.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" => "docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" (100%) diff --git "a/docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 100% rename from "docs/\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" From ed9cdb0d2ee034a8bed3e142fd3710db5cbf6036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:54:44 +0800 Subject: [PATCH 53/97] =?UTF-8?q?Update=202.=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\276\350\256\241\346\250\241\345\274\217.md" | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git "a/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" index a7b0d793..513943cb 100644 --- "a/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,6 +1,5 @@ - # 设计模式 -# 1.22 设计原则 +# 2.1 设计原则 ## 单一职责原则 - 不要存在多于一个导致类变更的原因。 - 总结:一个类只负责一项职责。 @@ -24,12 +23,12 @@ - 尽量多使用组合和聚合,尽量少使用甚至不使用继承关系 - -# 1.23 分类 +# 2.2 分类 - 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 - 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 - 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 -# 1.24 创建型设计模式 +# 2.3 创建型设计模式 ## 工厂方法模式 ### 介绍 - 工厂模式分为简单(静态)工厂模式、工厂方法模式和抽象工厂模式 @@ -282,7 +281,7 @@ public enum Car { - -# 1.25 结构型设计模式 +# 2.4 结构型设计模式 ## 适配器模式 ### 介绍 - 将一个类的接口转换成客户所期待的另一种接口 @@ -416,7 +415,7 @@ public enum Car { - -# 1.26 行为型设计模式 +# 2.5 行为型设计模式 ## 策略模式 ### 介绍 - 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。 @@ -602,7 +601,7 @@ public enum Car { ### 适用场景与优缺点 -# 1.27 设计模式的区分 +# 2.6 设计模式的区分 ## 代理模式和装饰器区别 - 装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。 - 因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。 @@ -629,7 +628,7 @@ public enum Car { - -# 1.28 JDK中的设计模式(17) +# 2.7 JDK中的设计模式(17) ## 创建型 - 1)工厂方法 - Collection.iterator() 由具体的聚集类来确定使用哪一个Iterator @@ -678,7 +677,7 @@ public enum Car { - 11)解释器模式 - -# 1.29 Spring中的设计模式(6) +# 2.8 Spring中的设计模式(6) - 1)抽象工厂模式: - BeanFactory - 2)代理模式: From e343ef324b87d7f85857268c43582cb1a25d889b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:56:43 +0800 Subject: [PATCH 54/97] =?UTF-8?q?Rename=201.Java=E5=9F=BA=E7=A1=80.md=20to?= =?UTF-8?q?=20=E4=B8=80.Java=E5=9F=BA=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\270\200.Java\345\237\272\347\241\200.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/1.Java\345\237\272\347\241\200.md" => "docs/\344\270\200.Java\345\237\272\347\241\200.md" (100%) diff --git "a/docs/1.Java\345\237\272\347\241\200.md" "b/docs/\344\270\200.Java\345\237\272\347\241\200.md" similarity index 100% rename from "docs/1.Java\345\237\272\347\241\200.md" rename to "docs/\344\270\200.Java\345\237\272\347\241\200.md" From f36d57dbb55ee440040612c78e29ae643a6e1c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 16:57:25 +0800 Subject: [PATCH 55/97] =?UTF-8?q?Rename=202.=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F.md=20to=20=E4=BA=8C=20=E3=80=81=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...43\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" => "docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" (100%) diff --git "a/docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 100% rename from "docs/2.\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" From 883bdfdfde5215b6dd31b2b7d718a4958ecceb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 17:01:14 +0800 Subject: [PATCH 56/97] =?UTF-8?q?Rename=20=E4=B8=80.Java=E5=9F=BA=E7=A1=80?= =?UTF-8?q?.md=20to=20=E4=B8=80=E3=80=81Java=E5=9F=BA=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\270\200\343\200\201Java\345\237\272\347\241\200.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\270\200.Java\345\237\272\347\241\200.md" => "docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" (100%) diff --git "a/docs/\344\270\200.Java\345\237\272\347\241\200.md" "b/docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" similarity index 100% rename from "docs/\344\270\200.Java\345\237\272\347\241\200.md" rename to "docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" From e3cc165c7048ebec936a77b9ab85759a3b0ef9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:07:27 +0800 Subject: [PATCH 57/97] =?UTF-8?q?Create=20=E4=B8=89=E3=80=81Java=20?= =?UTF-8?q?=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\343\200\201Java \351\233\206\345\220\210" | 7654 +++++++++++++++++ 1 file changed, 7654 insertions(+) create mode 100644 "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" diff --git "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" new file mode 100644 index 00000000..285c32bb --- /dev/null +++ "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" @@ -0,0 +1,7654 @@ +# 集合框架 + +- + +# 3.1 接口 +## 常见接口 +- Map 接口和 Collection 接口是所有集合框架的父接口; +- Collection 接口的子接口包括:Set 接口、List 接口和Queue接口; +- Map 接口的实现类主要有:HashMap、TreeMap、LinkedHashMap、Hashtable、ConcurrentHashMap 以及 Properties 等; +- Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet 等; +- List 接口的实现类主要有:ArrayList、LinkedList、Stack 、Vector以及CopyOnWriteArrayList 等; +- Queue接口的主要实现类有:ArrayDeque、ArrayBlockingQueue、LinkedBlockingQueue、PriorityQueue等; + +## List接口和Set接口的区别 +- List 元素是有序的,可以重复;Set 元素是无序的,不可以重复。 +## 队列、Set、Map 区别 +- List 有序列表 +- Set无序集合 +- Map键值对的集合 +- Queue队列FlFO + +- + +# 3.2 List +- 有顺序,可重复 +## ArrayList +- 基于数组实现,无容量的限制。 +- 在执行插入元素时可能要扩容,在删除元素时并不会减小数组的容量,在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。 +- 是非线程安全的。 +- 注意点: + - (1)ArrayList随机元素时间复杂度O(1),插入删除操作需大量移动元素,效率较低 + - (2)为了节约内存,当新建容器为空时,会共享Object[] EMPTY_ELEMENTDATA = {}和 Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}空数组 + - (3)容器底层采用数组存储,每次扩容为1.5倍 + - (4)ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法,其实Arrays.copyof()内部也是调用System.arraycopy()。System.arraycopy()为Native方法 + - (5)两个ToArray方法 +- Object[] toArray()方法。该方法有可能会抛出java.lang.ClassCastException异常 +- T[] toArray(T[] a)方法。该方法可以直接将ArrayList转换得到的Array进行整体向下转型 + - (6)ArrayList可以存储null值 + - (7)ArrayList每次修改(增加、删除)容器时,都是修改自身的modCount;在生成迭代器时,迭代器会保存该modCount值,迭代器每次获取元素时,会比较自身的modCount与ArrayList的modCount是否相等,来判断容器是否已经被修改,如果被修改了则抛出异常(fast-fail机制)。 + + + +### 成员变量 + +``` +/** + * Default initial capacity. + */ +private static final int DEFAULT_CAPACITY = 10; + +/** + * Shared empty array instance used for empty instances. + */ +private static final Object[] EMPTY_ELEMENTDATA = {}; + +/** + * Shared empty array instance used for default sized empty instances. We + * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when + * first element is added. + */ +private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + +/** + * The array buffer into which the elements of the ArrayList are stored. + * The capacity of the ArrayList is the length of this array buffer. Any + * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA + * will be expanded to DEFAULT_CAPACITY when the first element is added. + */ +transient Object[] elementData; // non-private to simplify nested class access + +/** + * The size of the ArrayList (the number of elements it contains). + * + * @serial + */ +private int size; +``` + + +### 构造方法 + +``` +public ArrayList(int initialCapacity) { + if (initialCapacity > 0) { + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) { + this.elementData = EMPTY_ELEMENTDATA; + } else { + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + } +} +``` + +### 添加 add(e) + +``` +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} +``` + + +- 即使初始化时指定大小 小于10个,添加元素时会调整大小,保证capacity不会少于10个。 + +``` +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); +} +``` + + + +``` +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} +``` + + +### 扩容 + +``` +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + + +- Arrays.copyOf底层是System.arrayCopy + +``` +public static T[] copyOf(U[] original, int newLength, Class newType) { + @SuppressWarnings("unchecked") + T[] copy = ((Object)newType == (Object)Object[].class) + ? (T[]) new Object[newLength] + : (T[]) Array.newInstance(newType.getComponentType(), newLength); + System.arraycopy(original, 0, copy, 0, + Math.min(original.length, newLength)); + return copy; +} +``` + + + +``` +public static native void arraycopy(Object src, int srcPos, + Object dest, int destPos, + int length); +``` + +### 添加 add(index,e) + +``` +public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + System.arraycopy(elementData, index, elementData, index + 1, + size - index); + elementData[index] = element; + size++; +} +``` + + + +``` +private void rangeCheckForAdd(int index) { + if (index > size || index < 0) + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); +} +``` + + +### 删除 remove(o) + +``` +public boolean remove(Object o) { + if (o == null) { + for (int index = 0; index < size; index++) + if (elementData[index] == null) { + fastRemove(index); + return true; + } + } else { + for (int index = 0; index < size; index++) + if (o.equals(elementData[index])) { + fastRemove(index); + return true; + } + } + return false; +} +``` + + + +``` +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` + +### 删除 remove(index) + +``` +public E remove(int index) { + rangeCheck(index); + + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; +} +``` + + +- + +### 获取 + +``` +public E get(int index) { + rangeCheck(index); + + return elementData(index); +} +``` + + + +``` +private void rangeCheck(int index) { + if (index >= size) + throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); +} +``` + + +- E elementData(int index) { + return (E) elementData[index]; +} +### 更新 + +``` +public E set(int index, E element) { + rangeCheck(index); + + E oldValue = elementData(index); + elementData[index] = element; + return oldValue; +} +``` + + +### 遍历 + +``` +public Iterator iterator() { + return new Itr(); +} + +/** + * An optimized version of AbstractList.Itr + */ +private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + if (i >= size) + throw new NoSuchElementException(); + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i + 1; + return (E) elementData[lastRet = i]; + } + + public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + + @Override + @SuppressWarnings("unchecked") + public void forEachRemaining(Consumer consumer) { + Objects.requireNonNull(consumer); + final int size = ArrayList.this.size; + int i = cursor; + if (i >= size) { + return; + } + final Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) { + throw new ConcurrentModificationException(); + } + while (i != size && modCount == expectedModCount) { + consumer.accept((E) elementData[i++]); + } + // update once at end of iteration to reduce heap write traffic + cursor = i; + lastRet = i - 1; + checkForComodification(); + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } +} +``` + +### 包含 + +``` +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +``` + + + +``` +public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} +``` + + +## LinkedList +- 基于双向链表机制 +- 在插入元素时,须创建一个新的Entry对象,并切换相应元素的前后元素的引用;在查找元素时,须遍历链表;在删除元素时,须遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。 +- 是非线程安全的。 +- 注意: + - (1)LinkedList有两个构造参数,一个为无參构造,只是新建一个空对象,第二个为有参构造,新建一个空对象,然后把所有元素添加进去。 + - (2)LinkedList的存储单元为一个名为Node的内部类,包含pre指针,next指针,和item元素,实现为双向链表 + - (3)LinkedList的删除、添加操作时间复杂度为O(1),查找时间复杂度为O(n),查找函数有一定优化,容器会先判断查找的元素是离头部较近,还是尾部较近,来决定从头部开始遍历还是尾部开始遍历 + - (4)LinkedList实现了Deque接口,因此也可以作为栈、队列和双端队列来使用。 + - (5)LinkedList可以存储null值 +### 成员变量 +- transient int size = 0; +transient Node first; +transient Node last; +### 构造方法 + +``` +public LinkedList() { +} +``` + + +### 添加 add(e) + +``` +public boolean add(E e) { + linkLast(e); + return true; +} +``` + +- 把一个元素添加到最后一个位置 +- void linkLast(E e) { + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} + +### 添加 add(index,e) + +``` +public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) + linkLast(element); + else + linkBefore(element, node(index)); +} +``` + + + - Node node(int index) { + // assert isElementIndex(index); + + if (index < (size >> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} + +- void linkBefore(E e, Node succ) { + // assert succ != null; + final Node pred = succ.prev; + final Node newNode = new Node<>(pred, e, succ); + succ.prev = newNode; + if (pred == null) + first = newNode; + else + pred.next = newNode; + size++; + modCount++; +} + +### 删除 remove(o) + +``` +public boolean remove(Object o) { + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) { + unlink(x); + return true; + } + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) { + unlink(x); + return true; + } + } + } + return false; +} +``` + + +- E unlink(Node x) { + // assert x != null; + final E element = x.item; + final Node next = x.next; + final Node prev = x.prev; + + if (prev == null) { + first = next; + } else { + prev.next = next; + x.prev = null; + } + + if (next == null) { + last = prev; + } else { + next.prev = prev; + x.next = null; + } + + x.item = null; + size--; + modCount++; + return element; +} + +### 删除 remove(index) + +``` +public E remove(int index) { + checkElementIndex(index); + return unlink(node(index)); +} +``` + + +### 获取 + +``` +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +``` + +### 更新 + +``` +public E set(int index, E element) { + checkElementIndex(index); + Node x = node(index); + E oldVal = x.item; + x.item = element; + return oldVal; +} +``` + + +### 遍历 + +``` +public ListIterator listIterator(int index) { + checkPositionIndex(index); + return new ListItr(index); +} +``` + + +``` + +private class ListItr implements ListIterator { + private Node lastReturned; + private Node next; + private int nextIndex; + private int expectedModCount = modCount; + + ListItr(int index) { + // assert isPositionIndex(index); + next = (index == size) ? null : node(index); + nextIndex = index; + } + + public boolean hasNext() { + return nextIndex < size; + } + + public E next() { + checkForComodification(); + if (!hasNext()) + throw new NoSuchElementException(); + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } + + public boolean hasPrevious() { + return nextIndex > 0; + } + + public E previous() { + checkForComodification(); + if (!hasPrevious()) + throw new NoSuchElementException(); + + lastReturned = next = (next == null) ? last : next.prev; + nextIndex--; + return lastReturned.item; + } + + public int nextIndex() { + return nextIndex; + } + + public int previousIndex() { + return nextIndex - 1; + } + + public void remove() { + checkForComodification(); + if (lastReturned == null) + throw new IllegalStateException(); + + Node lastNext = lastReturned.next; + unlink(lastReturned); + if (next == lastReturned) + next = lastNext; + else + nextIndex--; + lastReturned = null; + expectedModCount++; + } + + public void set(E e) { + if (lastReturned == null) + throw new IllegalStateException(); + checkForComodification(); + lastReturned.item = e; + } + + public void add(E e) { + checkForComodification(); + lastReturned = null; + if (next == null) + linkLast(e); + else + linkBefore(e, next); + nextIndex++; + expectedModCount++; + } + + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + while (modCount == expectedModCount && nextIndex < size) { + action.accept(next.item); + lastReturned = next; + next = next.next; + nextIndex++; + } + checkForComodification(); + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } +} +``` + + +### 包含 + +``` +public boolean contains(Object o) { + return indexOf(o) != -1; +} +``` + + + +``` +public int indexOf(Object o) { + int index = 0; + if (o == null) { + for (Node x = first; x != null; x = x.next) { + if (x.item == null) + return index; + index++; + } + } else { + for (Node x = first; x != null; x = x.next) { + if (o.equals(x.item)) + return index; + index++; + } + } + return -1; +} +``` + + +## Vector +- 基于synchronized实现的线程安全的ArrayList,但在插入元素时容量扩充的机制和ArrayList稍有不同,并可通过传入capacityIncrement来控制容量的扩充。 +### 成员变量 +- protected Object[] elementData; +protected int elementCount; +protected int capacityIncrement; + +### 构造方法 + +``` +public Vector(int initialCapacity) { + this(initialCapacity, 0); +} +``` + + + +``` +public Vector() { + this(10); +} +``` + + + +``` +public Vector(int initialCapacity, int capacityIncrement) { + super(); + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + this.elementData = new Object[initialCapacity]; + this.capacityIncrement = capacityIncrement; +} +``` + +### 添加 + +``` +public synchronized boolean add(E e) { + modCount++; + ensureCapacityHelper(elementCount + 1); + elementData[elementCount++] = e; + return true; +} +``` + + +### 删除 + +``` +public boolean remove(Object o) { + return removeElement(o); +} +``` + + + +``` +public synchronized boolean removeElement(Object obj) { + modCount++; + int i = indexOf(obj); + if (i >= 0) { + removeElementAt(i); + return true; + } + return false; +} +``` + + +### 扩容 + +``` +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + ((capacityIncrement > 0) ? + capacityIncrement : oldCapacity); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +### 获取 + +``` +public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); +} +``` + + +### 更新 + +``` +public synchronized E set(int index, E element) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + E oldValue = elementData(index); + elementData[index] = element; + return oldValue; +} +``` + + +### 包含 + +``` +public boolean contains(Object o) { + return indexOf(o, 0) >= 0; +} +``` + + + + +``` +public synchronized int indexOf(Object o, int index) { + if (o == null) { + for (int i = index ; i < elementCount ; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = index ; i < elementCount ; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} +``` + + +- + +## Stack +- 基于Vector实现,支持LIFO。 +### 类声明 + +``` +public class Stack extends Vector {} +``` + + +### push + +``` +public E push(E item) { + addElement(item); + return item; +} +``` + + +### pop + +``` +public synchronized E pop() { + E obj; + int len = size(); + obj = peek(); + removeElementAt(len - 1); + return obj; +} +``` + + +### peek + +``` +public synchronized E peek() { + int len = size(); + if (len == 0) + throw new EmptyStackException(); + return elementAt(len - 1); +} +``` + +## CopyOnWriteArrayList +- 是一个线程安全、并且在读操作时无锁的ArrayList。 +- 很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。 + +- 优点 + - 1)采用读写分离方式,读的效率非常高 + - 2)CopyOnWriteArrayList的迭代器是基于创建时的数据快照的,故数组的增删改不会影响到迭代器 + +- 缺点 + - 1)内存占用高,每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC + - 2)只能保证数据的最终一致性,不能保证数据的实时一致性。写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。 + +### 成员变量 + +``` +/** The lock protecting all mutators */ +final transient ReentrantLock lock = new ReentrantLock(); + +/** The array, accessed only via getArray/setArray. */ +private transient volatile Object[] array; +``` + +### 构造方法 + +``` +public CopyOnWriteArrayList() { + setArray(new Object[0]); +} +``` + + +- final void setArray(Object[] a) { + array = a; +} + +### 添加(有锁,锁内重新创建数组) +- final Object[] getArray() { + return array; +} + + +``` +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len + 1); + newElements[len] = e; + setArray(newElements); + return true; + } finally { + lock.unlock(); + } +} +``` + +### 存在则添加(有锁,锁内重新创建数组) +- 先保存一份数组snapshot,如果snapshot中存在,则直接返回。 +- 如果不存在,那么加锁,获取当前数组current,比较snapshot与current,遍历它们共同长度内的元素,如果发现current中某一个元素等于e,那么直接返回(当然current与snapshot相同就不必看了); +- 之后再遍历current单独的部分,如果发现current中某一个元素等于e,那么直接返回; +- 此时可以去创建一个长度+1的新数组,将e加入。 + + +``` +public boolean addIfAbsent(E e) { + Object[] snapshot = getArray(); + return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false : + addIfAbsent(e, snapshot); +} +``` + + + +``` +private boolean addIfAbsent(E e, Object[] snapshot) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] current = getArray(); + int len = current.length; + if (snapshot != current) { + // Optimize for lost race to another addXXX operation + int common = Math.min(snapshot.length, len); + for (int i = 0; i < common; i++) +``` + +- //如果snapshot与current元素不同但current与e相同,那么直接返回(扫描0到common) + if (current[i] != snapshot[i] && eq(e, current[i])) + return false; + - // 如果current中存在e,那么直接返回(扫描commen到len) + if (indexOf(e, current, common, len) >= 0) + return false; + } + Object[] newElements = Arrays.copyOf(current, len + 1); + newElements[len] = e; + setArray(newElements); + return true; + } finally { + lock.unlock(); + } +} + +### 删除(有锁,锁内重新创建数组) + +``` +public E remove(int index) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + int len = elements.length; + E oldValue = get(elements, index); + int numMoved = len - index - 1; + if (numMoved == 0) + setArray(Arrays.copyOf(elements, len - 1)); + else { + Object[] newElements = new Object[len - 1]; + System.arraycopy(elements, 0, newElements, 0, index); + System.arraycopy(elements, index + 1, newElements, index, + numMoved); + setArray(newElements); + } + return oldValue; + } finally { + lock.unlock(); + } +} +``` + + +### 获取(无锁) + +``` +public E get(int index) { + return get(getArray(), index); +} +``` + + + +``` +private E get(Object[] a, int index) { + return (E) a[index]; +} +``` + +### 更新(有锁,锁内重新创建数组) + +``` +public E set(int index, E element) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + E oldValue = get(elements, index); + + if (oldValue != element) { + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len); + newElements[index] = element; + setArray(newElements); + } else { +``` + +- // 为了保持“volatile”的语义,任何一个读操作都应该是一个写操作的结果, +- 也就是读操作看到的数据一定是某个写操作的结果(尽管写操作没有改变数据本身)。 +- 所以这里即使不设置也没有问题,仅仅是为了一个语义上的补充(就如源码中的注释所言) + // Not quite a no-op; ensures volatile write semantics + setArray(elements); + } + return oldValue; + } finally { + lock.unlock(); + } +} + +### 包含(无锁) + +``` +public boolean contains(Object o) { + Object[] elements = getArray(); + return indexOf(o, elements, 0, elements.length) >= 0; +} +``` + + + +``` +private static int indexOf(Object o, Object[] elements, + int index, int fence) { + if (o == null) { + for (int i = index; i < fence; i++) + if (elements[i] == null) + return i; + } else { + for (int i = index; i < fence; i++) + if (o.equals(elements[i])) + return i; + } + return -1; +} +``` + + +### 遍历(遍历的是获取iterator时的数组快照) + +``` +public Iterator iterator() { + return new COWIterator(getArray(), 0); +} +``` + + + +``` +static final class COWIterator implements ListIterator { + /** Snapshot of the array */ + private final Object[] snapshot; + /** Index of element to be returned by subsequent call to next. */ + private int cursor; + + private COWIterator(Object[] elements, int initialCursor) { + cursor = initialCursor; + snapshot = elements; + } + + public boolean hasNext() { + return cursor < snapshot.length; + } + + public boolean hasPrevious() { + return cursor > 0; + } + + @SuppressWarnings("unchecked") + public E next() { + if (! hasNext()) + throw new NoSuchElementException(); + return (E) snapshot[cursor++]; + } + + @SuppressWarnings("unchecked") + public E previous() { + if (! hasPrevious()) + throw new NoSuchElementException(); + return (E) snapshot[--cursor]; + } + + public int nextIndex() { + return cursor; + } + + public int previousIndex() { + return cursor-1; + } + + /** + * Not supported. Always throws UnsupportedOperationException. + * @throws UnsupportedOperationException always; {@code remove} + * is not supported by this iterator. + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Not supported. Always throws UnsupportedOperationException. + * @throws UnsupportedOperationException always; {@code set} + * is not supported by this iterator. + */ + public void set(E e) { + throw new UnsupportedOperationException(); + } + + /** + * Not supported. Always throws UnsupportedOperationException. + * @throws UnsupportedOperationException always; {@code add} + * is not supported by this iterator. + */ + public void add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + Object[] elements = snapshot; + final int size = elements.length; + for (int i = cursor; i < size; i++) { + @SuppressWarnings("unchecked") E e = (E) elements[i]; + action.accept(e); + } + cursor = size; + } +} +``` + + +- + +## List实现类之间的区别 + - (1) 对于需要快速插入,删除元素,应该使用LinkedList。 + - (2) 对于需要快速随机访问元素,应该使用ArrayList。 + - (3) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。 +- 对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector、CopyOnWriteArrayList)。 + + +- + +# 3.3 Set +- 没有顺序,不可重复 +## HashSet(底层是HashMap) +- Set不允许元素重复。 +- 基于HashMap实现,无容量限制。 +- 是非线程安全的。 + +### 成员变量 + +``` +private transient HashMap map; + +// Dummy value to associate with an Object in the backing Map +private static final Object PRESENT = new Object(); +``` + + +### 构造方法 + +``` +/** + * Constructs a new, empty set; the backing HashMap instance has + * default initial capacity (16) and load factor (0.75). + */ +public HashSet() { + map = new HashMap<>(); +} +``` + + + +``` +public HashSet(int initialCapacity) { + map = new HashMap<>(initialCapacity); +} +``` + + + +``` +public HashSet(int initialCapacity, float loadFactor) { + map = new HashMap<>(initialCapacity, loadFactor); +} +``` + + +### 添加 + +``` +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +``` + + +### 删除 + +``` +public boolean remove(Object o) { + return map.remove(o)==PRESENT; +} +``` + + +### 遍历 + +``` +public Iterator iterator() { + return map.keySet().iterator(); +} +``` + + +### 包含 + +``` +public boolean contains(Object o) { + return map.containsKey(o); +} +``` + + +- + +## TreeSet(底层是TreeMap) +- 基于TreeMap实现,支持排序(自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序)。 +- 是非线程安全的。 + +### 成员变量 + +``` +/** + * The backing map. + */ +private transient NavigableMap m; + +// Dummy value to associate with an Object in the backing Map +private static final Object PRESENT = new Object(); +``` + + +### 构造方法 + +``` +public TreeSet() { + this(new TreeMap()); +} +``` + + + +``` +public TreeSet(Comparator comparator) { + this(new TreeMap<>(comparator)); +} +``` + + +### 添加 + +``` +public boolean add(E e) { + return m.put(e, PRESENT)==null; +} +``` + + +### 删除 + +``` +public boolean remove(Object o) { + return m.remove(o)==PRESENT; +} +``` + + +### 遍历 + +``` +public Iterator iterator() { + return m.navigableKeySet().iterator(); +} +``` + + +### 包含 + +``` +public boolean contains(Object o) { + return m.containsKey(o); +} +``` + + +### 获取开头 + +``` +public E first() { + return m.firstKey(); +} +``` + + +### 获取结尾 + +``` +public E last() { + return m.lastKey(); +} +``` + +### 子集 + +``` +public NavigableSet subSet(E fromElement, boolean fromInclusive, + E toElement, boolean toInclusive) { + return new TreeSet<>(m.subMap(fromElement, fromInclusive, + toElement, toInclusive)); +} +``` + +- 默认是含头不含尾 + +``` +public SortedSet subSet(E fromElement, E toElement) { + return subSet(fromElement, true, toElement, false); +} +``` + + +- + +## LinkedHashSet(继承自HashSet,底层是LinkedHashMap) +- LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。 +### 类声明 + +``` +public class LinkedHashSet + extends HashSet + implements Set, Cloneable, java.io.Serializable {} +``` + + +### 构造方法 + +``` +public LinkedHashSet(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor, true); +} + +/** + * Constructs a new, empty linked hash set with the specified initial + * capacity and the default load factor (0.75). + * + * @param initialCapacity the initial capacity of the LinkedHashSet + * @throws IllegalArgumentException if the initial capacity is less + * than zero + */ +public LinkedHashSet(int initialCapacity) { + super(initialCapacity, .75f, true); +} + +/** + * Constructs a new, empty linked hash set with the default initial + * capacity (16) and load factor (0.75). + */ +public LinkedHashSet() { + super(16, .75f, true); +} +``` + + +- super指的是HashSet的default访问级别的构造方法 + +``` +/** + * Constructs a new, empty linked hash set. (This package private + * constructor is only used by LinkedHashSet.) The backing + * HashMap instance is a LinkedHashMap with the specified initial + * capacity and the specified load factor. + * + * @param initialCapacity the initial capacity of the hash map + * @param loadFactor the load factor of the hash map + * @param dummy ignored (distinguishes this + * constructor from other int, float constructor.) + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive + */ +HashSet(int initialCapacity, float loadFactor, boolean dummy) { + map = new LinkedHashMap<>(initialCapacity, loadFactor); +} +``` + + +## BitSet(位集,底层是long数组,用于替代List) +- BitSet是位操作的对象,值只有0或1即false和true,内部维护了一个long数组,初始只有一个long,所以BitSet最小的size是64(8个字节64个位,可以存储64个数字),当随着存储的元素越来越多,BitSet内部会动态扩充,最终内部是由N个long来存储,这些针对操作都是透明的。 +- 默认情况下,BitSet的所有位都是false即0。 +- 不是线程安全的。 +- 用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用的时候既可根据某一个是否为0表示,此数是否出现过。 + +- 一个1GB的空间,有8*1024*1024*1024 = 8.58*10^9bit,也就是1GB的空间可以表示85亿多个数。 +- 常见的应用是那些需要对海量数据进行一些统计工作的时候,比如日志分析、用户数统计等等,如统计40亿个数据中没有出现的数据,将40亿个不同数据进行排序,海量数据去重等等。 + +- JDK选择long数组作为BitSet的内部存储结构是出于性能的考虑,因为BitSet提供and和or这种操作,需要对两个BitSet中的所有bit位做and或者or,实现的时候需要遍历所有的数组元素。使用long能够使得循环的次数降到最低,所以Java选择使用long数组作为BitSet的内部存储结构。 +BitSet() +          创建一个新的位 set。 +BitSet(int nbits) +          创建一个位 set,它的初始大小足以显式表示索引范围在 0 到 nbits-1 的位。 + void and(BitSet set) +          对此目标位 set 和参数位 set 执行逻辑与操作。 + void andNot(BitSet set) +          清除此 BitSet 中所有的位,其相应的位在指定的 BitSet 中已设置。 + int cardinality() +          返回此 BitSet 中设置为 true 的位数。 + void clear() +          将此 BitSet 中的所有位设置为 false。 + void clear(int bitIndex) +          将索引指定处的位设置为 false。 + void clear(int fromIndex, int toIndex) +          将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的位设置为 false。 + Object clone() +          复制此 BitSet,生成一个与之相等的新 BitSet。 + boolean equals(Object obj) +          将此对象与指定的对象进行比较。 + void flip(int bitIndex) +          将指定索引处的位设置为其当前值的补码。 + void flip(int fromIndex, int toIndex) +          将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的每个位设置为其当前值的补码。 + boolean get(int bitIndex) +          返回指定索引处的位值。 + BitSet get(int fromIndex, int toIndex) +          返回一个新的 BitSet,它由此 BitSet 中从 fromIndex(包括)到 toIndex(不包括)范围内的位组成。 + int hashCode() +          返回此位 set 的哈希码值。 + boolean intersects(BitSet set) +          如果指定的 BitSet 中有设置为 true 的位,并且在此 BitSet 中也将其设置为true,则返回 ture。 + boolean isEmpty() +          如果此 BitSet 中没有包含任何设置为 true 的位,则返回 ture。 + int length() +          返回此 BitSet 的“逻辑大小”:BitSet 中最高设置位的索引加 1。 + int nextClearBit(int fromIndex) +          返回第一个设置为 false 的位的索引,这发生在指定的起始索引或之后的索引上。 + int nextSetBit(int fromIndex) +          返回第一个设置为 true 的位的索引,这发生在指定的起始索引或之后的索引上。 + void or(BitSet set) +          对此位 set 和位 set 参数执行逻辑或操作。 + void set(int bitIndex) +          将指定索引处的位设置为 true。 + void set(int bitIndex, boolean value) +          将指定索引处的位设置为指定的值。 + void set(int fromIndex, int toIndex) +          将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的位设置为 true。 + void set(int fromIndex, int toIndex, boolean value) +          将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的位设置为指定的值。 + int size() +          返回此 BitSet 表示位值时实际使用空间的位数。 + String toString() +          返回此位 set 的字符串表示形式。 + void xor(BitSet set) +          对此位 set 和位 set 参数执行逻辑异或操作。 + + +### 去重示例 + +``` +public static void containChars(String str) { + BitSet used = new BitSet(); + for (int i = 0; i < str.length(); i++) + used.set(str.charAt(i)); // set bit for char + StringBuilder sb = new StringBuilder(); + sb.append("["); + int size = used.size(); + for (int i = 0; i < size; i++) { + if (used.get(i)) { + sb.append((char) i); + } + } + sb.append("]"); + System.out.println(sb.toString()); +} + +public static void main(String[] args) { + containChars("abcdfab"); +} +``` + +- [abcdf] + +### 排序示例 + +``` +public static void sortArray(int[] array) { + + BitSet bitSet = new BitSet(2 << 13); + // 虽然可以自动扩容,但尽量在构造时指定估算大小,默认为64 + System.out.println("BitSet size: " + bitSet.size()); + + for (int i = 0; i < array.length; i++) { + bitSet.set(array[i]); + } + //剔除重复数字后的元素个数 + int bitLen = bitSet.cardinality(); + + //进行排序,即把bit为true的元素复制到另一个数组 + int[] orderedArray = new int[bitLen]; + int k = 0; + for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { + orderedArray[k++] = i; + } + + System.out.println("After ordering: "); + for (int i = 0; i < bitLen; i++) { + System.out.print(orderedArray[i] + "\t"); + } +} + +public static void main(String[] args) { + int[] array = new int[]{423, 700, 9999, 2323, 356, 6400, 1, 2, 3, 2, 2, 2, 2}; + sortArray(array); +} +``` + +- BitSet size: 16384 +- After ordering: +- 1 2 3 356 423 700 2323 6400 9999 + +## CopyOnWriteArraySet(底层是CopyOnWriteArrayList) +- 基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法。 +- 在每次add的时候都要进行数组的遍历,因此其性能会略低于CopyOnWriteArrayList。 +### 成员变量 + +``` +private final CopyOnWriteArrayList al; +``` + + +### 构造方法 + +``` +public CopyOnWriteArraySet() { + al = new CopyOnWriteArrayList(); +} +``` + + +### 添加 + +``` +public boolean add(E e) { + return al.addIfAbsent(e); +} +``` + + +### 删除 + +``` +public boolean remove(Object o) { + return al.remove(o); +} +``` + + +### 遍历 + +``` +public Iterator iterator() { + return al.iterator(); +} +``` + + +### 包含 + +``` +public boolean contains(Object o) { + return al.contains(o); +} +``` + +- + +# 3.4 Queue +- 先进先出”(FIFO—first in first out)的线性表 +- LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。 +- Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList)。 + +- + +- Deque既可以作为栈使用,也可以作为队列使用。 +Queue Method Equivalent Deque Method 说明 +add(e) addLast(e) 向队尾插入元素,失败则抛出异常 +remove() removeFirst() 获取并删除队首元素,失败则抛出异常 + +element() getFirst() 获取但不删除队首元素,失败则抛出异常 +offer(e) offerLast(e) 向队尾插入元素,失败则返回false +poll() pollFirst() 获取并删除队首元素,失败则返回null +peek() peekFirst() 获取但不删除队首元素,失败则返回null + + +Stack Method Equivalent Deque Method 说明 +push(e) addFirst(e) 向栈顶插入元素,失败则抛出异常 +无 offerFirst(e) 向栈顶插入元素,失败则返回false +pop() removeFirst() 获取并删除栈顶元素,失败则抛出异常 +无 pollFirst() 获取并删除栈顶元素,失败则返回null +peek() peekFirst() 获取但不删除栈顶元素,失败则抛出异常 +无 peekFirst() 获取但不删除栈顶元素,失败则返回null + +- ArrayDeque和LinkedList是Deque的两个通用实现。 + +- + +## 1)ArrayDeque(底层是循环数组,有界队列) + +- head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。 +### 成员变量 + + +``` +transient Object[] elements; // non-private to simplify nested class access +transient int head; +transient int tail; +private static final int MIN_INITIAL_CAPACITY = 8; +``` + +### 构造方法 + +``` +public ArrayDeque() { + elements = new Object[16]; +} +``` + + + +``` +public ArrayDeque(int numElements) { + allocateElements(numElements); +} +``` + + + +``` +/** + * Allocates empty array to hold the given number of elements. + * + * @param numElements the number of elements to hold + */ +private void allocateElements(int numElements) { + int initialCapacity = MIN_INITIAL_CAPACITY; + // Find the best power of two to hold elements. + // Tests "<=" because arrays aren't kept full. + if (numElements >= initialCapacity) { + initialCapacity = numElements; + initialCapacity |= (initialCapacity >>> 1); + initialCapacity |= (initialCapacity >>> 2); + initialCapacity |= (initialCapacity >>> 4); + initialCapacity |= (initialCapacity >>> 8); + initialCapacity |= (initialCapacity >>> 16); + initialCapacity++; + + if (initialCapacity < 0) // Too many elements, must back off + initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements + } + elements = new Object[initialCapacity]; +} +``` + +- + +### 扩容 + + +``` +/** + * Doubles the capacity of this deque. Call only when full, i.e., + * when head and tail have wrapped around to become equal. + */ +private void doubleCapacity() { + assert head == tail; + int p = head; + int n = elements.length; + int r = n - p; // number of elements to the right of p + int newCapacity = n << 1; + if (newCapacity < 0) + throw new IllegalStateException("Sorry, deque too big"); + Object[] a = new Object[newCapacity]; + System.arraycopy(elements, p, a, 0, r); + System.arraycopy(elements, 0, a, r, p); + elements = a; + head = 0; + tail = n; +} +``` + + +### offer + +``` +public boolean offer(E e) { + return offerLast(e); +} +``` + + + +``` +public boolean offerLast(E e) { + addLast(e); + return true; +} +``` + + + +``` +public void addLast(E e) { + if (e == null) + throw new NullPointerException(); + elements[tail] = e; + if ( (tail = (tail + 1) & (elements.length - 1)) == head) + doubleCapacity(); +} +``` + + +### poll + +``` +public E poll() { + return pollFirst(); +} +``` + + + +``` +public E pollFirst() { + int h = head; + @SuppressWarnings("unchecked") + E result = (E) elements[h]; + // Element is null if deque empty + if (result == null) + return null; + elements[h] = null; // Must null out slot + head = (h + 1) & (elements.length - 1); + return result; +} +``` + + +### peek + +``` +public E peek() { + return peekFirst(); +} +``` + + + +``` +public E peekFirst() { + // elements[head] is null if deque empty + return (E) elements[head]; +} +``` + + +- + +## ConcurrentLinkedQueue(底层是链表,基于CAS的非阻塞队列,无界队列) +- ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(非阻塞)来实现。 + +- 1 . 使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。 +- 2. head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。 +- 3. 以批处理方式来更新head/tail,从整体上减少入队 / 出队操作的开销。 +- 4. ConcurrentLinkedQueue的迭代器是弱一致性的,这在并发容器中是比较普遍的现象,主要是指在一个线程在遍历队列结点而另一个线程尝试对某个队列结点进行修改的话不会抛出ConcurrentModificationException,这也就造成在遍历某个尚未被修改的结点时,在next方法返回时可以看到该结点的修改,但在遍历后再对该结点修改时就看不到这种变化。 + +- 1. 在入队时最后一个结点中的next域为null +- 2. 队列中的所有未删除结点的item域不能为null且从head都可以在O(N)时间内遍历到 +- 3. 对于要删除的结点,不是将其引用直接置为空,而是将其的item域先置为null(迭代器在遍历是会跳过item为null的结点) +- 4. 允许head和tail滞后更新,也就是上文提到的head/tail并非总是指向队列的头 / 尾节点(这主要是为了减少CAS指令执行的次数,但同时会增加volatile读的次数,但是这种消耗较小)。具体而言就是,当在队列中插入一个元素是,会检测tail和最后一个结点之间的距离是否在两个结点及以上(内部称之为hop);而在出队时,对head的检测就是与队列的第一个结点的距离是否达到两个,有则将head指向第一个结点并将head原来指向的结点的next域指向自己,这样就能断开与队列的联系从而帮助GC +- head节点并不是总指向第一个结点,tail也并不是总指向最后一个节点。 + +- 源码过于复杂,可以先跳过。 + +### 成员变量 + +``` +private transient volatile Node head; +private transient volatile Node tail; +``` + + +### 构造方法 + +``` +public ConcurrentLinkedQueue() { + head = tail = new Node(null); +} +``` + +- Node#CAS操作 +- 在obj的offset位置比较object field和期望的值,如果相同则更新。这个方法的操作应该是原子的,因此提供了一种不可中断的方式更新object field。 + + +- 如果node的next值为cmp,则将其更新为val +- boolean casNext(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); +} + +- boolean casItem(E cmp, E val) { + return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); +} + + +``` +private boolean casHead(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val); +} +``` + + +- void lazySetNext(Node val) { + UNSAFE.putOrderedObject(this, nextOffset, val); +} + + +### offer(无锁) + + +``` +/** + * Inserts the specified element at the tail of this queue. + * As the queue is unbounded, this method will never return {@code false}. + * + * @return {@code true} (as specified by {@link Queue#offer}) + * @throws NullPointerException if the specified element is null + */ +public boolean offer(E e) { + checkNotNull(e); + final Node newNode = new Node(e); + + for (Node t = tail, p = t;;) { + Node q = p.next; +``` + +- // q/p.next/tail.next为null,则说明p是尾节点,则插入 + if (q == null) { + // CAS插入 p.next = newNode,多线程环境下只有一个线程可以设置成功 +- // 此时 tail.next = newNode + if (p.casNext(null, newNode)) { +- // CAS成功说明新节点已经放入链表 +- // 如果p不为t,说明当前线程是之前CAS失败后又重试CAS成功的,tail = newNode + if (p != t) // hop two nodes at a time +- casTail(t, newNode); // Failure is OK. + return true; + } + // Lost CAS race to another thread; re-read next + } + else if (p == q) +- //多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要重新找新的head,因为新的head后面的节点才是激活的节点 + // p = head , t = tail +- p = (t != (t = tail)) ? t : head; + else +- // 对上一次CAS失败的线程而言,t.next/p.next/tail.next/q 不是null了 +- // 副作用是p = q,p和q都指向了尾节点,进入第三次循环 + p = (p != t && t != (t = tail)) ? t : q; + } +} + +### poll(无锁) + +``` +public E poll() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { +``` + +- // 保存当前节点的值 + E item = p.item; + // 当前节点有值则CAS置为null, p.item = null + if (item != null && p.casItem(item, null)) { +- // CAS成功代表当前节点已经从链表中移除 +- + if (p != h) // hop two nodes at a time + updateHead(h, ((q = p.next) != null) ? q : p); + return item; + } // 当前队列为空时则返回null + else if ((q = p.next) == null) { + updateHead(h, p); + return null; + } // 自引用了,则重新找新的队列头节点 + else if (p == q) + continue restartFromHead; + else + p = q; + } + } +} + +- final void updateHead(Node h, Node p) { + if (h != p && casHead(h, p)) + h.lazySetNext(h); +} + +### peek(无锁) + +``` +public E peek() { + restartFromHead: + for (;;) { + for (Node h = head, p = h, q;;) { + E item = p.item; + if (item != null || (q = p.next) == null) { + updateHead(h, p); + return item; + } + else if (p == q) + continue restartFromHead; + else + p = q; + } + } +} +``` + +### size(遍历计算大小,效率低) + +``` +public int size() { + int count = 0; + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // Collection.size() spec says to max out + if (++count == Integer.MAX_VALUE) + break; + return count; +} +``` + + +- + +### ConcurrentLinkedDeque(底层是双向链表,基于CAS的非阻塞队列,无界队列) +## 2)PriorityQueue(底层是数组,逻辑上是小顶堆,无界队列) +- PriorityQueue底层实现的数据结构是“堆”,堆具有以下两个性质: +- 任意一个节点的值总是不大于(最大堆)或者不小于(最小堆)其父节点的值;堆是一棵完全二叉树 + - 基于数组实现的二叉堆,对于数组中任意位置的n上元素,其左孩子在[2n+1]位置上,右孩子[2(n+1)]位置,它的父亲则在[(n-1)/2]上,而根的位置则是[0]。 + + - 1)时间复杂度:remove()方法和add()方法时间复杂度为O(logn),remove(Object obj)和contains()方法需要O(n)时间复杂度,取队头则需要O(1)时间 + - 2)在初始化阶段会执行建堆函数,最终建立的是最小堆,每次出队和入队操作不能保证队列元素的有序性,只能保证队头元素和新插入元素的有序性,如果需要有序输出队列中的元素,则只要调用Arrays.sort()方法即可 + - 3)可以使用Iterator的迭代器方法输出队列中元素 + - 4)PriorityQueue是非同步的,要实现同步需要调用java.util.concurrent包下的PriorityBlockingQueue类来实现同步 + - 5)在队列中不允许使用null元素 + - 6)PriorityQueue默认是一个小顶堆,然而可以通过传入自定义的Comparator函数来实现大顶堆 + +- 替代:用TreeMap复杂度太高,有没有更好的方法。hash方法,但是队列不是定长的,如果改变了大小要rehash代价太大,还有什么方法?用堆实现,那每次get put复杂度是多少(lgN) +### 成员变量 + +``` +transient Object[] queue; // non-private to simplify nested class access + +/** + * The number of elements in the priority queue. + */ +private int size = 0; + +/** + * The comparator, or null if priority queue uses elements' + * natural ordering. + */ +private final Comparator comparator; + +/** + * The number of times this priority queue has been + * structurally modified. See AbstractList for gory details. + */ +transient int modCount = 0; // non-private to simplify nested class access +``` + + +### 构造方法 + +``` +public PriorityQueue() { + this(DEFAULT_INITIAL_CAPACITY, null); +} + +/** + * Creates a {@code PriorityQueue} with the specified initial + * capacity that orders its elements according to their + * {@linkplain Comparable natural ordering}. + * + * @param initialCapacity the initial capacity for this priority queue + * @throws IllegalArgumentException if {@code initialCapacity} is less + * than 1 + */ +public PriorityQueue(int initialCapacity) { + this(initialCapacity, null); +} + +/** + * Creates a {@code PriorityQueue} with the default initial capacity and + * whose elements are ordered according to the specified comparator. + * + * @param comparator the comparator that will be used to order this + * priority queue. If {@code null}, the {@linkplain Comparable + * natural ordering} of the elements will be used. + * @since 1.8 + */ +public PriorityQueue(Comparator comparator) { + this(DEFAULT_INITIAL_CAPACITY, comparator); +} + +/** + * Creates a {@code PriorityQueue} with the specified initial capacity + * that orders its elements according to the specified comparator. + * + * @param initialCapacity the initial capacity for this priority queue + * @param comparator the comparator that will be used to order this + * priority queue. If {@code null}, the {@linkplain Comparable + * natural ordering} of the elements will be used. + * @throws IllegalArgumentException if {@code initialCapacity} is + * less than 1 + */ +public PriorityQueue(int initialCapacity, + Comparator comparator) { + // Note: This restriction of at least one is not actually needed, + // but continues for 1.5 compatibility + if (initialCapacity < 1) + throw new IllegalArgumentException(); + this.queue = new Object[initialCapacity]; + this.comparator = comparator; +} +``` + +### 扩容 +- Double size if small; else grow by 50% + +``` +private void grow(int minCapacity) { + int oldCapacity = queue.length; + // Double size if small; else grow by 50% + int newCapacity = oldCapacity + ((oldCapacity < 64) ? + (oldCapacity + 2) : + (oldCapacity >> 1)); + // overflow-conscious code + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + queue = Arrays.copyOf(queue, newCapacity); +} +``` + + + +``` +private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; +} +``` + + +### offer + +``` +public boolean offer(E e) { + if (e == null) + throw new NullPointerException(); + modCount++; + int i = size; + if (i >= queue.length) + grow(i + 1); + size = i + 1; + if (i == 0) + queue[0] = e; + else + siftUp(i, e); + return true; +} +``` + + + +``` +private void siftUp(int k, E x) { + if (comparator != null) + siftUpUsingComparator(k, x); + else + siftUpComparable(k, x); +} +``` + + + +``` +private void siftUpUsingComparator(int k, E x) { + while (k > 0) { + int parent = (k - 1) >>> 1; + Object e = queue[parent]; + if (comparator.compare(x, (E) e) >= 0) + break; + queue[k] = e; + k = parent; + } + queue[k] = x; +} +``` + + + +``` +private void siftUpComparable(int k, E x) { + Comparable key = (Comparable) x; + while (k > 0) { + int parent = (k - 1) >>> 1; + Object e = queue[parent]; + if (key.compareTo((E) e) >= 0) + break; + queue[k] = e; + k = parent; + } + queue[k] = key; +} +``` + + +### poll + +``` +public E poll() { + if (size == 0) + return null; + int s = --size; + modCount++; + E result = (E) queue[0]; + E x = (E) queue[s]; + queue[s] = null; + if (s != 0) + siftDown(0, x); + return result; +} +``` + + + +``` +private void siftDown(int k, E x) { + if (comparator != null) + siftDownUsingComparator(k, x); + else + siftDownComparable(k, x); +} +``` + + + +``` +private void siftDownUsingComparator(int k, E x) { + int half = size >>> 1; + while (k < half) { + int child = (k << 1) + 1; + Object c = queue[child]; + int right = child + 1; + if (right < size && + comparator.compare((E) c, (E) queue[right]) > 0) + c = queue[child = right]; + if (comparator.compare(x, (E) c) <= 0) + break; + queue[k] = c; + k = child; + } + queue[k] = x; +} +``` + + + +``` +private void siftDownComparable(int k, E x) { + Comparable key = (Comparable)x; + int half = size >>> 1; // loop while a non-leaf + while (k < half) { + int child = (k << 1) + 1; // assume left child is least + Object c = queue[child]; + int right = child + 1; + if (right < size && + ((Comparable) c).compareTo((E) queue[right]) > 0) + c = queue[child = right]; + if (key.compareTo((E) c) <= 0) + break; + queue[k] = c; + k = child; + } + queue[k] = key; +} +``` + +### peek + +``` +public E peek() { + return (size == 0) ? null : (E) queue[0]; +} +``` + + +- + +## 3)BlockingQueue +- 对于许多多线程问题,都可以通过使用一个或多个队列以优雅的方式将其形式化 +- 生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。 +- 比如转账 +- 一个线程将转账指令放入队列 +- 一个线程从队列中取出指令执行转账,只有这个线程可以访问银行对象的内部。因此不需要同步 + +- 当试图向队列中添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞 +- 在协调多个线程之间的合作时,阻塞队列是很有用的。 +- 工作者线程可以周期性地将中间结果放入阻塞队列,其他工作者线程取出中间结果并进一步修改。队列会自动平衡负载,大概第一个线程集比第二个运行的慢,那么第二个线程集在等待结果时会阻塞,反之亦然 + + - 1)LinkedBlockingQueue的容量是没有上边界的,是一个双向队列 + - 2)ArrayBlockingQueue在构造时需要指定容量,并且有一个参数来指定是否需要公平策略 + - 3)PriorityBlockingQueue是一个带优先级的队列,元素按照它们的优先级顺序被移走。该队列没有容量上限。 + - 4)DelayQueue包含实现了Delayed接口的对象 + - 5)TransferQueue接口允许生产者线程等待,直到消费者准备就绪可以接收一个元素。如果生产者调用transfer方法,那么这个调用会阻塞,直到插入的元素被消费者取出之后才停止阻塞。 +- LinkedTransferQueue类实现了这个接口 + +- + +## ArrayBlockingQueue(底层是数组,阻塞队列,一把锁两个Condition,有界同步队列) +- 基于数组、先进先出、线程安全的集合类,特点是可实现指定时间的阻塞读写,并且容量是可限制的。 +### 成员变量 + +``` +/** The queued items */ +final Object[] items; + +/** items index for next take, poll, peek or remove */ +int takeIndex; + +/** items index for next put, offer, or add */ +int putIndex; + +/** Number of elements in the queue */ +int count; + +/* + * Concurrency control uses the classic two-condition algorithm + * found in any textbook. + */ + +/** Main lock guarding all access */ +final ReentrantLock lock; + +/** Condition for waiting takes */ +private final Condition notEmpty; + +/** Condition for waiting puts */ +private final Condition notFull; + +/** + * Shared state for currently active iterators, or null if there + * are known not to be any. Allows queue operations to update + * iterator state. + */ +transient Itrs itrs = null; +``` + + +### 构造方法 + +``` +public ArrayBlockingQueue(int capacity) { + this(capacity, false); +} + +/** + * Creates an {@code ArrayBlockingQueue} with the given (fixed) + * capacity and the specified access policy. + * + * @param capacity the capacity of this queue + * @param fair if {@code true} then queue accesses for threads blocked + * on insertion or removal, are processed in FIFO order; + * if {@code false} the access order is unspecified. + * @throws IllegalArgumentException if {@code capacity < 1} + */ +public ArrayBlockingQueue(int capacity, boolean fair) { + if (capacity <= 0) + throw new IllegalArgumentException(); + this.items = new Object[capacity]; + lock = new ReentrantLock(fair); + notEmpty = lock.newCondition(); + notFull = lock.newCondition(); +} +``` + +### put(有锁,队列满则阻塞) + +``` +public void put(E e) throws InterruptedException { + checkNotNull(e); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + while (count == items.length) + notFull.await(); + enqueue(e); + } finally { + lock.unlock(); + } +} +``` + + + +``` +private void enqueue(E x) { + // assert lock.getHoldCount() == 1; + // assert items[putIndex] == null; + final Object[] items = this.items; + items[putIndex] = x; + if (++putIndex == items.length) + putIndex = 0; + count++; + notEmpty.signal(); +} +``` + + +### take(有锁,队列空则阻塞) + +``` +public E take() throws InterruptedException { + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + while (count == 0) + notEmpty.await(); + return dequeue(); + } finally { + lock.unlock(); + } +} +``` + + + +``` +private E dequeue() { + // assert lock.getHoldCount() == 1; + // assert items[takeIndex] != null; + final Object[] items = this.items; + @SuppressWarnings("unchecked") + E x = (E) items[takeIndex]; + items[takeIndex] = null; + if (++takeIndex == items.length) + takeIndex = 0; + count--; + if (itrs != null) + itrs.elementDequeued(); + notFull.signal(); + return x; +} +``` + +### offer(有锁,最多阻塞一段时间) + +``` +public boolean offer(E e, long timeout, TimeUnit unit) + throws InterruptedException { + + checkNotNull(e); + long nanos = unit.toNanos(timeout); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + while (count == items.length) { + if (nanos <= 0) + return false; + nanos = notFull.awaitNanos(nanos); + } + enqueue(e); + return true; + } finally { + lock.unlock(); + } +} +``` + + +### poll(有锁,最多阻塞一段时间) + +``` +public E poll(long timeout, TimeUnit unit) throws InterruptedException { + long nanos = unit.toNanos(timeout); + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + while (count == 0) { + if (nanos <= 0) + return null; + nanos = notEmpty.awaitNanos(nanos); + } + return dequeue(); + } finally { + lock.unlock(); + } +} +``` + + +### peek(有锁) + +``` +public E peek() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return itemAt(takeIndex); // null when queue is empty + } finally { + lock.unlock(); + } +``` + + +- final E itemAt(int i) { + return (E) items[i]; +} + +### 遍历(构造迭代器加锁,遍历迭代器也加锁) + +``` +public Iterator iterator() { + return new Itr(); +} +``` + + + +``` +private class Itr implements Iterator { + /** Index to look for new nextItem; NONE at end */ + private int cursor; + + /** Element to be returned by next call to next(); null if none */ + private E nextItem; + + /** Index of nextItem; NONE if none, REMOVED if removed elsewhere */ + private int nextIndex; + + /** Last element returned; null if none or not detached. */ + private E lastItem; + + /** Index of lastItem, NONE if none, REMOVED if removed elsewhere */ + private int lastRet; + + /** Previous value of takeIndex, or DETACHED when detached */ + private int prevTakeIndex; + + /** Previous value of iters.cycles */ + private int prevCycles; + + /** Special index value indicating "not available" or "undefined" */ + private static final int NONE = -1; + + /** + * Special index value indicating "removed elsewhere", that is, + * removed by some operation other than a call to this.remove(). + */ + private static final int REMOVED = -2; + + /** Special value for prevTakeIndex indicating "detached mode" */ + private static final int DETACHED = -3; + + Itr() { + // assert lock.getHoldCount() == 0; + lastRet = NONE; + final ReentrantLock lock = ArrayBlockingQueue.this.lock; + lock.lock(); + try { + if (count == 0) { + // assert itrs == null; + cursor = NONE; + nextIndex = NONE; + prevTakeIndex = DETACHED; + } else { + final int takeIndex = ArrayBlockingQueue.this.takeIndex; + prevTakeIndex = takeIndex; + nextItem = itemAt(nextIndex = takeIndex); + cursor = incCursor(takeIndex); + if (itrs == null) { + itrs = new Itrs(this); + } else { + itrs.register(this); // in this order + itrs.doSomeSweeping(false); + } + prevCycles = itrs.cycles; + // assert takeIndex >= 0; + // assert prevTakeIndex == takeIndex; + // assert nextIndex >= 0; + // assert nextItem != null; + } + } finally { + lock.unlock(); + } + } +``` + +- } + +- + +## LinkedBlockingQueue(底层是链表,阻塞队列,两把锁,各自对应一个Condition,无界同步队列) +- 另一种BlockingQueue的实现,基于链表,没有容量限制。 +- 由于出队只操作队头,入队只操作队尾,这里巧妙地使用了两把锁,对于put和offer入队操作使用一把锁,对于take和poll出队操作使用一把锁,避免了出队、入队时互相竞争锁的现象,因此LinkedBlockingQueue在高并发读写都多的情况下,性能会较ArrayBlockingQueue好很多,在遍历以及删除的情况下则要两把锁都要锁住。 +- 多CPU情况下可以在同一时刻既消费又生产。 +### 成员变量 + +``` +/** The capacity bound, or Integer.MAX_VALUE if none */ +private final int capacity; + +/** Current number of elements */ +private final AtomicInteger count = new AtomicInteger(); + +/** + * Head of linked list. + * Invariant: head.item == null + */ +transient Node head; + +/** + * Tail of linked list. + * Invariant: last.next == null + */ +private transient Node last; + +/** Lock held by take, poll, etc */ +private final ReentrantLock takeLock = new ReentrantLock(); + +/** Wait queue for waiting takes */ +private final Condition notEmpty = takeLock.newCondition(); + +/** Lock held by put, offer, etc */ +private final ReentrantLock putLock = new ReentrantLock(); + +/** Wait queue for waiting puts */ +private final Condition notFull = putLock.newCondition(); +``` + + +### 构造方法 + +``` +public LinkedBlockingQueue() { + this(Integer.MAX_VALUE); +} + +/** + * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. + * + * @param capacity the capacity of this queue + * @throws IllegalArgumentException if {@code capacity} is not greater + * than zero + */ +public LinkedBlockingQueue(int capacity) { + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + last = head = new Node(null); +} +``` + + +### put(加putLock锁,队列满则阻塞) + +``` + +/** + * Inserts the specified element at the tail of this queue, waiting if + * necessary for space to become available. + * + * @throws InterruptedException {@inheritDoc} + * @throws NullPointerException {@inheritDoc} + */ +public void put(E e) throws InterruptedException { + if (e == null) throw new NullPointerException(); + // Note: convention in all put/take/etc is to preset local var + // holding count negative to indicate failure unless set. + int c = -1; + Node node = new Node(e); + final ReentrantLock putLock = this.putLock; + final AtomicInteger count = this.count; + putLock.lockInterruptibly(); + try { + /* + * Note that count is used in wait guard even though it is + * not protected by lock. This works because count can + * only decrease at this point (all other puts are shut + * out by lock), and we (or some other waiting put) are + * signalled if it ever changes from capacity. Similarly + * for all other uses of count in other wait guards. + */ + while (count.get() == capacity) { +``` + +- // 阻塞,直至有剩余空间 + notFull.await(); + } + enqueue(node); + c = count.getAndIncrement(); + if (c + 1 < capacity) +- // 还有剩余空间时,唤醒其他生产者 + notFull.signal(); + } finally { + putLock.unlock(); + } + if (c == 0) +- // c是放入当前元素之前队列的容量,现在新添加一个元素,那么唤醒消费者进行消费 +- signalNotEmpty(); +} + + +``` +private void enqueue(Node node) { + // assert putLock.isHeldByCurrentThread(); + // assert last.next == null; + last = last.next = node; +} +``` + + + +``` +private void signalNotEmpty() { + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { +``` + +- // 唤醒消费线程 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } +} + +### take(加takeLock锁,队列空则阻塞) + +``` +public E take() throws InterruptedException { + E x; + int c = -1; + final AtomicInteger count = this.count; + final ReentrantLock takeLock = this.takeLock; + takeLock.lockInterruptibly(); + try { + while (count.get() == 0) { +``` + + - // 队列空则阻塞 + notEmpty.await(); + } + x = dequeue(); + c = count.getAndDecrement(); + if (c > 1) +- // 还有元素则唤醒其他消费者 + notEmpty.signal(); + } finally { + takeLock.unlock(); + } + if (c == capacity) +- // c是消费当前元素之前队列的容量,现在的容量是c-1,可以继续放入元素,唤醒生产者进行生产 + signalNotFull(); + return x; +} + + +``` +private E dequeue() { + // assert takeLock.isHeldByCurrentThread(); + // assert head.item == null; + Node h = head; + Node first = h.next; + h.next = h; // help GC + head = first; + E x = first.item; + first.item = null; + return x; +} +``` + + + +``` +private void signalNotFull() { + final ReentrantLock putLock = this.putLock; + putLock.lock(); + try { +``` + +- // 唤醒生产者 + notFull.signal(); + } finally { + putLock.unlock(); + } +} + +### peek(加takeLock锁) + +``` +public E peek() { + if (count.get() == 0) + return null; + final ReentrantLock takeLock = this.takeLock; + takeLock.lock(); + try { + Node first = head.next; + if (first == null) + return null; + else + return first.item; + } finally { + takeLock.unlock(); + } +} +``` + + +### remove(加两把锁) + +``` +/** + * Locks to prevent both puts and takes. + */ +void fullyLock() { + putLock.lock(); + takeLock.lock(); +} + +/** + * Unlocks to allow both puts and takes. + */ +void fullyUnlock() { + takeLock.unlock(); + putLock.unlock(); +} +``` + + + + +``` +public boolean remove(Object o) { + if (o == null) return false; + fullyLock(); + try { + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (o.equals(p.item)) { + unlink(p, trail); + return true; + } + } + return false; + } finally { + fullyUnlock(); + } +} +``` + + +### 遍历(加两把锁) + +``` +public Iterator iterator() { + return new Itr(); +} + +private class Itr implements Iterator { + /* + * Basic weakly-consistent iterator. At all times hold the next + * item to hand out so that if hasNext() reports true, we will + * still have it to return even if lost race with a take etc. + */ + + private Node current; + private Node lastRet; + private E currentElement; + + Itr() { + fullyLock(); + try { + current = head.next; + if (current != null) + currentElement = current.item; + } finally { + fullyUnlock(); + } + } + + public boolean hasNext() { + return current != null; + } + + /** + * Returns the next live successor of p, or null if no such. + * + * Unlike other traversal methods, iterators need to handle both: + * - dequeued nodes (p.next == p) + * - (possibly multiple) interior removed nodes (p.item == null) + */ + private Node nextNode(Node p) { + for (;;) { + Node s = p.next; + if (s == p) + return head.next; + if (s == null || s.item != null) + return s; + p = s; + } + } + + public E next() { + fullyLock(); + try { + if (current == null) + throw new NoSuchElementException(); + E x = currentElement; + lastRet = current; + current = nextNode(current); + currentElement = (current == null) ? null : current.item; + return x; + } finally { + fullyUnlock(); + } + } + + public void remove() { + if (lastRet == null) + throw new IllegalStateException(); + fullyLock(); + try { + Node node = lastRet; + lastRet = null; + for (Node trail = head, p = trail.next; + p != null; + trail = p, p = p.next) { + if (p == node) { + unlink(p, trail); + break; + } + } + } finally { + fullyUnlock(); + } + } +} +``` + + +- + +### LinkedBlockingDeque(底层是双向链表,阻塞队列,一把锁两个Condition,无界同步队列) +- LinkedBlockingDeque是一个基于链表的双端阻塞队列。和LinkedBlockingQueue类似,区别在于该类实现了Deque接口,而LinkedBlockingQueue实现了Queue接口。 +- LinkedBlockingDeque内部只有一把锁以及该锁上关联的两个条件,所以可以推断同一时刻只有一个线程可以在队头或者队尾执行入队或出队操作(类似于ArrayBlockingQueue)。可以发现这点和LinkedBlockingQueue不同,LinkedBlockingQueue可以同时有两个线程在两端执行操作。 + +- LinkedBlockingDeque和LinkedBlockingQueue的相同点在于: +- 1. 基于链表 +- 2. 容量可选,不设置的话,就是Int的最大值 + +- 和LinkedBlockingQueue的不同点在于: +- 1. 双端链表和单链表 +- 2. 不存在哨兵节点 +- 3. 一把锁+两个条件 + +- LinkedBlockingDeque和ArrayBlockingQueue的相同点在于:使用一把锁+两个条件维持队列的同步。 +- + +## PriorityBlockingQueue(底层是数组,出队时队空则阻塞;无界队列,不存在队满情况,一把锁一个Condition) +- 支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。 +### 成员变量 + +``` +private static final int DEFAULT_INITIAL_CAPACITY = 11; + +/** + * The maximum size of array to allocate. + * Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in + * OutOfMemoryError: Requested array size exceeds VM limit + */ +private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + +/** + * Priority queue represented as a balanced binary heap: the two + * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The + * priority queue is ordered by comparator, or by the elements' + * natural ordering, if comparator is null: For each node n in the + * heap and each descendant d of n, n <= d. The element with the + * lowest value is in queue[0], assuming the queue is nonempty. + */ +private transient Object[] queue; + +/** + * The number of elements in the priority queue. + */ +private transient int size; + +/** + * The comparator, or null if priority queue uses elements' + * natural ordering. + */ +private transient Comparator comparator; + +/** + * Lock used for all public operations + */ +private final ReentrantLock lock; + +/** + * Condition for blocking when empty + */ +private final Condition notEmpty; + +/** + * Spinlock for allocation, acquired via CAS. + */ +private transient volatile int allocationSpinLock; + +/** + * A plain PriorityQueue used only for serialization, + * to maintain compatibility with previous versions + * of this class. Non-null only during serialization/deserialization. + */ +private PriorityQueue q; +``` + + +### 构造方法 + +``` +public PriorityBlockingQueue() { + this(DEFAULT_INITIAL_CAPACITY, null); +} + +/** + * Creates a {@code PriorityBlockingQueue} with the specified + * initial capacity that orders its elements according to their + * {@linkplain Comparable natural ordering}. + * + * @param initialCapacity the initial capacity for this priority queue + * @throws IllegalArgumentException if {@code initialCapacity} is less + * than 1 + */ +public PriorityBlockingQueue(int initialCapacity) { + this(initialCapacity, null); +} + +/** + * Creates a {@code PriorityBlockingQueue} with the specified initial + * capacity that orders its elements according to the specified + * comparator. + * + * @param initialCapacity the initial capacity for this priority queue + * @param comparator the comparator that will be used to order this + * priority queue. If {@code null}, the {@linkplain Comparable + * natural ordering} of the elements will be used. + * @throws IllegalArgumentException if {@code initialCapacity} is less + * than 1 + */ +public PriorityBlockingQueue(int initialCapacity, + Comparator comparator) { + if (initialCapacity < 1) + throw new IllegalArgumentException(); + this.lock = new ReentrantLock(); + this.notEmpty = lock.newCondition(); + this.comparator = comparator; + this.queue = new Object[initialCapacity]; +} +``` + +### 扩容(基于CAS+Lock,CAS控制创建新的数组原子执行,Lock控制数组替换原子执行) + +``` +private void tryGrow(Object[] array, int oldCap) { + lock.unlock(); // must release and then re-acquire main lock + Object[] newArray = null; + if (allocationSpinLock == 0 && + UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, + 0, 1)) { + try { + int newCap = oldCap + ((oldCap < 64) ? + (oldCap + 2) : // grow faster if small + (oldCap >> 1)); + if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow + int minCap = oldCap + 1; + if (minCap < 0 || minCap > MAX_ARRAY_SIZE) + throw new OutOfMemoryError(); + newCap = MAX_ARRAY_SIZE; + } + if (newCap > oldCap && queue == array) + newArray = new Object[newCap]; + } finally { + allocationSpinLock = 0; + } + } + if (newArray == null) // back off if another thread is allocating + Thread.yield(); + lock.lock(); + if (newArray != null && queue == array) { + queue = newArray; + System.arraycopy(array, 0, newArray, 0, oldCap); + } +} +``` + + +### put(有锁) + +``` +public void put(E e) { + offer(e); // never need to block +} +``` + + + +``` +public boolean offer(E e) { + if (e == null) + throw new NullPointerException(); + final ReentrantLock lock = this.lock; + lock.lock(); + int n, cap; + Object[] array; + while ((n = size) >= (cap = (array = queue).length)) + tryGrow(array, cap); + try { + Comparator cmp = comparator; + if (cmp == null) + siftUpComparable(n, e, array); + else + siftUpUsingComparator(n, e, array, cmp); + size = n + 1; + notEmpty.signal(); + } finally { + lock.unlock(); + } + return true; +} +``` + + +### take(有锁) + +``` +public E take() throws InterruptedException { + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + E result; + try { + while ( (result = dequeue()) == null) + notEmpty.await(); + } finally { + lock.unlock(); + } + return result; +} +``` + + + +``` +private E dequeue() { + int n = size - 1; + if (n < 0) + return null; + else { + Object[] array = queue; + E result = (E) array[0]; + E x = (E) array[n]; + array[n] = null; + Comparator cmp = comparator; + if (cmp == null) + siftDownComparable(0, x, array, n); + else + siftDownUsingComparator(0, x, array, n, cmp); + size = n; + return result; + } +} +``` + + +### peek(有锁) + +``` +public E peek() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return (size == 0) ? null : (E) queue[0]; + } finally { + lock.unlock(); + } +} +``` + + + +- + +## DelayQueue(底层是PriorityQueue,无界阻塞队列,过期元素方可移除,基于Lock) + +``` +public class DelayQueue extends AbstractQueue + implements BlockingQueue { + + private final transient ReentrantLock lock = new ReentrantLock(); + private final PriorityQueue q = new PriorityQueue(); +``` + +- DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。 +- 每个元素都必须实现Delayed接口 + +``` +public interface Delayed extends Comparable { + + /** + * Returns the remaining delay associated with this object, in the + * given time unit. + * + * @param unit the time unit + * @return the remaining delay; zero or negative values indicate + * that the delay has already elapsed + */ + long getDelay(TimeUnit unit); +} +``` + +- getDelay方法返回对象的残留延迟,负值表示延迟结束 +- 元素只有在延迟用完的时候才能从DelayQueue移出。还必须实现Comparable接口。 + +- 一个典型场景是重试机制的实现,比如当调用接口失败后,把当前调用信息放入delay=10s的元素,然后把元素放入队列,那么这个队列就是一个重试队列,一个线程通过take方法获取需要重试的接口,take返回则接口进行重试,失败则再次放入队列,同时也可以在元素加上重试次数。 +### 成员变量 + +``` +private final transient ReentrantLock lock = new ReentrantLock(); +private final PriorityQueue q = new PriorityQueue(); + +private Thread leader = null; + +private final Condition available = lock.newCondition(); +``` + + +### 构造方法 + +``` +public DelayQueue() {} +``` + + +### put + +``` +public void put(E e) { + offer(e); +} +``` + + + +``` +public boolean offer(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + q.offer(e); + if (q.peek() == e) { + leader = null; +``` + +- // 通知最先等待的线程 + available.signal(); + } + return true; + } finally { + lock.unlock(); + } +} + +### take +- 获取并移除队列首元素,如果队列没有过期元素则等待。 + - 第一次调用take时候由于队列空,所以调用(2)把当前线程放入available的条件队列等待,当执行offer并且添加的元素就是队首元素时候就会通知最先等待的线程激活,循环重新获取队首元素,这时候first假如不空,则调用getdelay方法看该元素海剩下多少时间就过期了,如果delay<=0则说明已经过期,则直接出队返回。否则看leader是否为null,不为null则说明是其他线程也在执行take则把该线程放入条件队列,否则是当前线程执行的take方法,则调用(5) await直到剩余过期时间到(这期间该线程会释放锁,所以其他线程可以offer添加元素,也可以take阻塞自己),剩余过期时间到后,该线程会重新竞争得到锁,重新进入循环。 + - (6)说明当前take返回了元素,如果当前队列还有元素则调用singal激活条件队列里面可能有的等待线程。leader那么为null,那么是第一次调用take获取过期元素的线程,第一次调用的线程调用设置等待时间的await方法等待数据过期,后面调用take的线程则调用await直到signal。 + +``` +public E take() throws InterruptedException { + final ReentrantLock lock = this.lock; + lock.lockInterruptibly(); + try { + for (;;) { +``` + + - // 1)获取但不移除队首元素 + E first = q.peek(); + if (first == null) + - // 2)无元素,则阻塞 + available.await(); + else { + long delay = first.getDelay(NANOSECONDS); + - // 3)有元素,且已经过期,则移除 + if (delay <= 0) + return q.poll(); + first = null; // don't retain ref while waiting + - // 4) + if (leader != null) + available.await(); + else { + Thread thisThread = Thread.currentThread(); + - // 5) + leader = thisThread; + try { +- // 继续阻塞延迟的时间 + available.awaitNanos(delay); + } finally { + if (leader == thisThread) + leader = null; + } + } + } + } + } finally { + if (leader == null && q.peek() != null) + available.signal(); + lock.unlock(); + } +} + +### peek +## SynchronousQueue(只存储一个元素,阻塞队列,基于CAS) +- 实现了BlockingQueue,是一个阻塞队列。 +- 一个只存储一个元素的的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态,吞吐量高于LinkedBlockingQueue。 +- SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。 + +- // 如果为 true,则等待线程以 FIFO 的顺序竞争访问;否则顺序是未指定的。 +- // SynchronousQueue sc =new SynchronousQueue<>(true);//fair - +- SynchronousQueue sc = new SynchronousQueue<>(); // 默认不指定的话是false,不公平的 + +## 4)TransferQueue(特殊的BlockingQueue) +- 生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事) +- 当我们不想生产者过度生产消息时,TransferQueue可能非常有用,可避免发生OutOfMemory错误。在这样的设计中,消费者的消费能力将决定生产者产生消息的速度。 + +``` +public interface TransferQueue extends BlockingQueue { +``` + + +``` + /** +``` + +- * 立即转交一个元素给消费者,如果此时队列没有消费者,那就false +- */ +- boolean tryTransfer(E e); + + +``` + /** +``` + +- * 转交一个元素给消费者,如果此时队列没有消费者,那就阻塞 +- */ +- void transfer(E e) throws InterruptedException; + + +``` + /** +``` + +- * 带超时的tryTransfer +- */ +- boolean tryTransfer(E e, long timeout, TimeUnit unit) +- throws InterruptedException; + + +``` + /** +``` + +- * 是否有消费者等待接收数据,瞬时状态,不一定准 +- */ +- boolean hasWaitingConsumer(); + + +``` + /** +``` + +- * 返回还有多少个等待的消费者,跟上面那个一样,都是一种瞬时状态,不一定准 +- */ +- int getWaitingConsumerCount(); +- } +- + +### LinkedTransferQueue(底层是链表,阻塞队列,无界同步队列) +- LinkedTransferQueue实现了TransferQueue接口,这个接口继承了BlockingQueue。之前BlockingQueue是队列满时再入队会阻塞,而这个接口实现的功能是队列不满时也可以阻塞,实现一种有阻塞的入队功能。 +- LinkedTransferQueue实际上是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集。而且LinkedTransferQueue更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。 +- + +## 5)Queue实现类之间的区别 +- 非线程安全的:ArrayDeque、LinkedList、PriorityQueue +- 线程安全的:ConcurrentLinkedQueue、ConcurrentLinkedDeque、ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue +- 线程安全的又分为阻塞队列和非阻塞队列,阻塞队列提供了put、take等会阻塞当前线程的方法,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,也有offer、poll等阻塞一段时间候返回的方法; +- 非阻塞队列是使用CAS机制保证offer、poll等可以线程安全地入队出队,并且不需要加锁,不会阻塞当前线程,比如ConcurrentLinkedQueue、ConcurrentLinkedDeque。 + +### ArrayBlockingQueue和LinkedBlockingQueue 区别 +- 1. 队列中锁的实现不同 +- ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁; +- LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock +- 2. 底层实现不同 +- 前者基于数组,后者基于链表 +- 3. 队列边界不同 +- ArrayBlockingQueue实现的队列中必须指定队列的大小,是有界队列 +- LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE,是无界队列 + +- + +# 3.5 Map +## HashMap(底层是数组+链表/红黑树,无序键值对集合,非线程安全) + +- 基于哈希表实现,链地址法。 +- loadFactor默认为0.75,threshold(阈)为12,并创建一个大小为16的Entry数组。 +- 在遍历时是无序的,如需有序,建议使用TreeMap。 +- 采用数组方式存储key、value构成的Entry对象,无容量限制。 +- 基于key hash寻找Entry对象存放在数组中的位置,对于hash冲突采用链表/红黑树的方式来解决。 +- HashMap在插入元素时可能会扩大数组的容量,在扩大容量时需要重新计算hash,并复制对象到新的数组中。 +- 是非线程安全的。 + +- // 1. 哈希冲突时采用链表法的类,一个哈希桶多于8个元素改为TreeNode +- static class Node implements Map.Entry +- // 2. 哈希冲突时采用红黑树存储的类,一个哈希桶少于6个元素改为Node +- static final class TreeNode extends LinkedHashMap.Entry + +- 某个桶对应的链表过长的话搜索效率低,改为红黑树效率会提高。 + +- 为何按位与而不是取摸 hashmap的iterator读取时是否会读到另一个线程put的数据 红黑树;hashmap报ConcurrentModificationException的情况 + +- Hash冲突中链表结构的数量大于8个,则调用树化转为红黑树结构,红黑树查找稍微快些;红黑树结构的数量小于6个时,则转为链表结构 +- 如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。 + - 一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。 + - 哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。 +- Map#Entry(接口) + +``` +interface Entry { + K getKey(); + + V getValue(); + + V setValue(V value); + + boolean equals(Object o); + int hashCode(); + public static , V> Comparator> comparingByKey() { + return (Comparator> & Serializable) + (c1, c2) -> c1.getKey().compareTo(c2.getKey()); + } + + public static > Comparator> comparingByValue() { + return (Comparator> & Serializable) + (c1, c2) -> c1.getValue().compareTo(c2.getValue()); + } + + public static Comparator> comparingByKey(Comparator cmp) { + Objects.requireNonNull(cmp); + return (Comparator> & Serializable) + (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey()); + } + + public static Comparator> comparingByValue(Comparator cmp) { + Objects.requireNonNull(cmp); + return (Comparator> & Serializable) + (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue()); + } +} +``` + + +- HashMap#Node(Map.Entry的实现,链表的基本元素) + +``` +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } +} +``` + + +- HashMap#TreeNode(Map.Entry的实现,红黑树的基本元素) +- static final class TreeNode extends LinkedHashMap.Entry { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; // needed to unlink next upon deletion + boolean red; + TreeNode(int hash, K key, V val, Node next) { + super(hash, key, val, next); + } +- //... +- } + +- LinkedHashMap#Entry +- static class Entry extends HashMap.Node { + Entry before, after; + Entry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } +} + +- + +### 成员变量 + +``` +/** + * The default initial capacity - MUST be a power of two. + */ +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 + +/** + * The maximum capacity, used if a higher value is implicitly specified + * by either of the constructors with arguments. + * MUST be a power of two <= 1<<30. + */ +static final int MAXIMUM_CAPACITY = 1 << 30; + +/** + * The load factor used when none specified in constructor. + */ +static final float DEFAULT_LOAD_FACTOR = 0.75f; + +/** + * The bin count threshold for using a tree rather than list for a + * bin. Bins are converted to trees when adding an element to a + * bin with at least this many nodes. The value must be greater + * than 2 and should be at least 8 to mesh with assumptions in + * tree removal about conversion back to plain bins upon + * shrinkage. + */ +static final int TREEIFY_THRESHOLD = 8; + +/** + * The bin count threshold for untreeifying a (split) bin during a + * resize operation. Should be less than TREEIFY_THRESHOLD, and at + * most 6 to mesh with shrinkage detection under removal. + */ +static final int UNTREEIFY_THRESHOLD = 6; + +/** + * The smallest table capacity for which bins may be treeified. + * (Otherwise the table is resized if too many nodes in a bin.) + * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts + * between resizing and treeification thresholds. + */ +static final int MIN_TREEIFY_CAPACITY = 64; +``` + + +``` +/** + * The table, initialized on first use, and resized as + * necessary. When allocated, length is always a power of two. + * (We also tolerate length zero in some operations to allow + * bootstrapping mechanics that are currently not needed.) + */ +transient Node[] table; + +/** + * Holds cached entrySet(). Note that AbstractMap fields are used + * for keySet() and values(). + */ +transient Set> entrySet; + +/** + * The number of key-value mappings contained in this map. + */ +transient int size; + +/** + * The number of times this HashMap has been structurally modified + * Structural modifications are those that change the number of mappings in + * the HashMap or otherwise modify its internal structure (e.g., + * rehash). This field is used to make iterators on Collection-views of + * the HashMap fail-fast. (See ConcurrentModificationException). + */ +transient int modCount; + +/** + * The next size value at which to resize (capacity * load factor). + * + * @serial + */ +// (The javadoc description is true upon serialization. +// Additionally, if the table array has not been allocated, this +// field holds the initial array capacity, or zero signifying +// DEFAULT_INITIAL_CAPACITY.) +``` + + +``` +// HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*装载因子) +int threshold; + +/** + * The load factor for the hash table. + * + * @serial + */ +final float loadFactor; +``` + + +- AbstractMap +- transient Set keySet; +transient Collection values; + +### 构造方法 +- 注意哪怕是指定了初始容量,也不会直接初始化table,而是在第一次put时调用resize来初始化table,resize里会将threshold视为初始容量。 + +``` +public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; +``` + + +``` +// 阈值为不小于容量的2的幂次 + this.threshold = tableSizeFor(initialCapacity); +} + +public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); +} + +/** + * Constructs an empty HashMap with the default initial capacity + * (16) and the default load factor (0.75). + */ +public HashMap() { + this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted +} +``` + +- + +### tableSizeFor(找到大于等于initialCapacity的最小的2的幂次以及原因) + +``` +/** + * Returns a power of two size for the given target capacity. + */ +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; +} +``` + +- +### hash(hash算法,算法比较高效、均匀) + - static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} + +- key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。) +- 保证了对象的hashCode的高16位的变化能反应到低16位中, + +### hash to index +- 如何根据hash值计算index?(put和get中的代码) +- n = table.length; + - index = (n-1)& hash; + + - 当n总是2的n次方时,hash & (n-1)运算等价于h%n,但是&比%具有更高的效率。 + +### put + +``` +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + + +- // onlyIfAbsent如果为true,只有在hashmap没有该key的时候才添加 +- // evict如果为false,hashmap为创建模式;只有在使用Map集合作为构造器创建LinkedHashMap或HashMap时才会为false。 +- // 这两个参数均为实现java8的新接口而设置 +- Node newNode(int hash, K key, V value, Node next) { + return new Node<>(hash, key, value, next); +} + +- final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; // table +- Node p; // node pointer +- int n, i; // n 为length, i 为 node index + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + - // index处没有元素,则直接放入新节点 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { +- // index处有元素 + Node e; +- K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) +- // 假如key是相同的,那么替换value即可 + e = p; + else if (p instanceof TreeNode) +- // key不同,但如果p是红黑树根节点,那么将新节点放入红黑树 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { +- // key不同,但如果p是链表头节点,那么判断链表中是否有该节点,如没有,则将新节点插入到链表尾部 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + - // 插入后如果发现已经链表长度已经适合转为红黑树了,则转换 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + // 链表中某元素key和key相同,则替换value即可 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; +- + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} + +- + +### 扩容 resize +- // 扩容函数,如果hash桶为空,初始化默认大小,否则双倍扩容 +- // 注意!!因为扩容为2的倍数,根据hash桶的计算方法,元素哈希值不变 +- // 所以元素在新的hash桶的下标,要不跟旧的hash桶下标一致,要不增加1倍。 +- cap:capacity +- thr:threshold + - final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) { + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; +- + +- if (oldTab != null) { + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { +- // j位置原本元素存在 + oldTab[j] = null; + if (e.next == null) +- // 如果该位置没有形成链表,则再次计算index,放入新table +- // 假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定 +- 那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位 +- 因此,table中的元素只有两种情况: +- 元素hash值第N+1位为0:不需要进行位置调整 + - 元素hash值第N+1位为1:调整至原索引的两倍位置 + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) +- // 如果该位置形成了红黑树,则split + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order +- // 如果该位置形成了链表,则分成两个链表,分别放在0~oldCap,oldCap~oldCap*2位置处 + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; +- // 用于确定元素hash值第N+1位是否为0: +- 若为0,则使用loHead与loTail,将元素移至新table的原索引处 +- 若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} +### get(O(logn)) + +``` +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} +``` + + + - final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { +- // table不为空,且hash对应index元素不为空 +- // 如果index位置就是我们要找的key,则直接返回 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; +- // 如果不是,则从链表或红黑树的角度继续找 + if ((e = first.next) != null) { + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +### remove + +``` +public V remove(Object key) { + Node e; + return (e = removeNode(hash(key), key, null, false, true)) == null ? + null : e.value; +} +``` + + +- value=null,matchValue=false,movable=true + - final Node removeNode(int hash, Object key, Object value, + boolean matchValue, boolean movable) { + Node[] tab; Node p; int n, index; + if ((tab = table) != null && (n = tab.length) > 0 && + (p = tab[index = (n - 1) & hash]) != null) { + Node node = null, e; K k; V v; + - // 1) 如果hash 对应index即为我们要找的key,则找到 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + node = p; + - // 2) 从链表或红黑树的角度继续找 + else if ((e = p.next) != null) { + if (p instanceof TreeNode) + node = ((TreeNode)p).getTreeNode(hash, key); + else { + do { + if (e.hash == hash && + ((k = e.key) == key || + (key != null && key.equals(k)))) { + node = e; + break; + } + p = e; + } while ((e = e.next) != null); + } + } +- // 找到后,根据找到的位置不同 相应地进行删除 + if (node != null && (!matchValue || (v = node.value) == value || + (value != null && value.equals(v)))) { + if (node instanceof TreeNode) + ((TreeNode)node).removeTreeNode(this, tab, movable); + else if (node == p) + tab[index] = node.next; + else + p.next = node.next; + ++modCount; + --size; + afterNodeRemoval(node); + return node; + } + } + return null; +} + +### containsKey + +``` +public boolean containsKey(Object key) { + return getNode(hash(key), key) != null; +} +``` + + +### containsValue + +``` +public boolean containsValue(Object value) { + Node[] tab; V v; + if ((tab = table) != null && size > 0) { + for (int i = 0; i < tab.length; ++i) { + for (Node e = tab[i]; e != null; e = e.next) { + if ((v = e.value) == value || + (value != null && value.equals(v))) + return true; + } + } + } + return false; +} +``` + + +- + +### a)链表转红黑树 treeifyBin + +``` +/** + * Replaces all linked nodes in bin at index for given hash unless + * table is too small, in which case resizes instead. + */ +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + else if ((e = tab[index = (n - 1) & hash]) != null) { + TreeNode hd = null, tl = null; + do { + TreeNode p = replacementTreeNode(e, null); + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +- b)红黑树转链表 TreeNode#untreeify +- final Node untreeify(HashMap map) { + Node hd = null, tl = null; + for (Node q = this; q != null; q = q.next) { + Node p = map.replacementNode(q, null); + if (tl == null) + hd = p; + else + tl.next = p; + tl = p; + } + return hd; +} + +### c)红黑树 查找 +- final TreeNode getTreeNode(int h, Object k) { + return ((parent != null) ? root() : this).find(h, k, null); +} + + +``` +/** + * Finds the node starting at root p with the given hash and key. + * The kc argument caches comparableClassFor(key) upon first use + * comparing keys. + */ +final TreeNode find(int h, Object k, Class kc) { + TreeNode p = this; + do { + int ph, dir; K pk; + TreeNode pl = p.left, pr = p.right, q; + if ((ph = p.hash) > h) + p = pl; + else if (ph < h) + p = pr; + else if ((pk = p.key) == k || (k != null && k.equals(pk))) + return p; + else if (pl == null) + p = pr; + else if (pr == null) + p = pl; + else if ((kc != null || + (kc = comparableClassFor(k)) != null) && + (dir = compareComparables(kc, k, pk)) != 0) + p = (dir < 0) ? pl : pr; + else if ((q = pr.find(h, k, kc)) != null) + return q; + else + p = pl; + } while (p != null); + return null; +} +``` + + +### d)红黑树 添加 +- final TreeNode putTreeVal(HashMap map, Node[] tab, + int h, K k, V v) { + Class kc = null; + boolean searched = false; + TreeNode root = (parent != null) ? root() : this; + for (TreeNode p = root;;) { + int dir, ph; K pk; + if ((ph = p.hash) > h) + dir = -1; + else if (ph < h) + dir = 1; + else if ((pk = p.key) == k || (k != null && k.equals(pk))) + return p; + else if ((kc == null && + (kc = comparableClassFor(k)) == null) || + (dir = compareComparables(kc, k, pk)) == 0) { + if (!searched) { + TreeNode q, ch; + searched = true; + if (((ch = p.left) != null && + (q = ch.find(h, k, kc)) != null) || + ((ch = p.right) != null && + (q = ch.find(h, k, kc)) != null)) + return q; + } + dir = tieBreakOrder(k, pk); + } + + TreeNode xp = p; + if ((p = (dir <= 0) ? p.left : p.right) == null) { + Node xpn = xp.next; + TreeNode x = map.newTreeNode(h, k, v, xpn); + if (dir <= 0) + xp.left = x; + else + xp.right = x; + xp.next = x; + x.parent = x.prev = xp; + if (xpn != null) + ((TreeNode)xpn).prev = x; + moveRootToFront(tab, balanceInsertion(root, x)); + return null; + } + } +} + +### e)红黑树 删除 + +``` +/** + * Removes the given node, that must be present before this call. + * This is messier than typical red-black deletion code because we + * cannot swap the contents of an interior node with a leaf + * successor that is pinned by "next" pointers that are accessible + * independently during traversal. So instead we swap the tree + * linkages. If the current tree appears to have too few nodes, + * the bin is converted back to a plain bin. (The test triggers + * somewhere between 2 and 6 nodes, depending on tree structure). + */ +final void removeTreeNode(HashMap map, Node[] tab, + boolean movable) { + int n; + if (tab == null || (n = tab.length) == 0) + return; + int index = (n - 1) & hash; + TreeNode first = (TreeNode)tab[index], root = first, rl; + TreeNode succ = (TreeNode)next, pred = prev; + if (pred == null) + tab[index] = first = succ; + else + pred.next = succ; + if (succ != null) + succ.prev = pred; + if (first == null) + return; + if (root.parent != null) + root = root.root(); + if (root == null || root.right == null || + (rl = root.left) == null || rl.left == null) { + tab[index] = first.untreeify(map); // too small + return; + } + TreeNode p = this, pl = left, pr = right, replacement; + if (pl != null && pr != null) { + TreeNode s = pr, sl; + while ((sl = s.left) != null) // find successor + s = sl; + boolean c = s.red; s.red = p.red; p.red = c; // swap colors + TreeNode sr = s.right; + TreeNode pp = p.parent; + if (s == pr) { // p was s's direct parent + p.parent = s; + s.right = p; + } + else { + TreeNode sp = s.parent; + if ((p.parent = sp) != null) { + if (s == sp.left) + sp.left = p; + else + sp.right = p; + } + if ((s.right = pr) != null) + pr.parent = s; + } + p.left = null; + if ((p.right = sr) != null) + sr.parent = p; + if ((s.left = pl) != null) + pl.parent = s; + if ((s.parent = pp) == null) + root = s; + else if (p == pp.left) + pp.left = s; + else + pp.right = s; + if (sr != null) + replacement = sr; + else + replacement = p; + } + else if (pl != null) + replacement = pl; + else if (pr != null) + replacement = pr; + else + replacement = p; + if (replacement != p) { + TreeNode pp = replacement.parent = p.parent; + if (pp == null) + root = replacement; + else if (p == pp.left) + pp.left = replacement; + else + pp.right = replacement; + p.left = p.right = p.parent = null; + } + + TreeNode r = p.red ? root : balanceDeletion(root, replacement); + + if (replacement == p) { // detach + TreeNode pp = p.parent; + p.parent = null; + if (pp != null) { + if (p == pp.left) + pp.left = null; + else if (p == pp.right) + pp.right = null; + } + } + if (movable) + moveRootToFront(tab, r); +} +``` + + +### f)红黑树 遍历 +- 使用next指针,类似链表方式,便可遍历红黑树。 + +### 遍历(先迭代table,再迭代bucket->链表/红黑树) +#### keySet +- keySet().iterator() + +``` +public Set keySet() { + Set ks = keySet; + if (ks == null) { + ks = new KeySet(); + keySet = ks; + } + return ks; +} +``` + + + +``` +final class KeySet extends AbstractSet { + public final Iterator iterator() { return new KeyIterator(); } +} +``` + + +- KeyIterator实现了Iterator接口,并继承了HashIterator。前者仅适用于KeySet的迭代,后者适合所有基于HashMap的迭代。 +- HashMap#HashIterator + +``` +abstract class HashIterator { + Node next; // next entry to return + Node current; // current entry + int expectedModCount; // for fast-fail + int index; // current slot + + HashIterator() { + expectedModCount = modCount; + Node[] t = table; + current = next = null; + index = 0; + if (t != null && size > 0) { // advance to first entry + do {} while (index < t.length && (next = t[index++]) == null); + } + } + + public final boolean hasNext() { + return next != null; + } + + final Node nextNode() { + Node[] t; + Node e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); +``` + + +``` +// next的next为空的话,则继续遍历table,否则就返回next的next(链表或红黑树的下一个节点) + if ((next = (current = e).next) == null && (t = table) != null) { + do {} while (index < t.length && (next = t[index++]) == null); + } + return e; + } + + public final void remove() { + Node p = current; + if (p == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + current = null; + K key = p.key; + removeNode(hash(key), key, null, false, false); + expectedModCount = modCount; + } +} +``` + + +- HashMap#KeyIterator + +``` +final class KeyIterator extends HashIterator + implements Iterator { + public final K next() { return nextNode().key; } +} +``` + + +#### entrySet + +``` +public Set> entrySet() { + Set> es; + return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; +} +``` + +- 使用的是该迭代器: + +``` +final class EntryIterator extends HashIterator + implements Iterator> { + public final Map.Entry next() { return nextNode(); } +} +``` + + + +### 多线程环境下的问题 +- 1.8中hashmap的确不会因为多线程put导致死循环(1.7代码中会这样子),但是依然有其他的弊端,比如数据丢失等等。因此多线程情况下还是建议使用ConcurrentHashMap。 + +- 数据丢失:当多线程put的时候,当index相同而又同时达到链表的末尾时,另一个线程put的数据会把之前线程put的数据覆盖掉,就会产生数据丢失。 +- if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); +- } + +## Hashtable +- Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阈值)时,同样会自动增长。 +- Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。 +- Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。 +- Hashtable#Entry + +``` +private static class Entry implements Map.Entry { + final int hash; + final K key; + V value; + Entry next; + + protected Entry(int hash, K key, V value, Entry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + @SuppressWarnings("unchecked") + protected Object clone() { + return new Entry<>(hash, key, value, + (next==null ? null : (Entry) next.clone())); + } + + // Map.Entry Ops + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + if (value == null) + throw new NullPointerException(); + + V oldValue = this.value; + this.value = value; + return oldValue; + } + + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry)o; + + return (key==null ? e.getKey()==null : key.equals(e.getKey())) && + (value==null ? e.getValue()==null : value.equals(e.getValue())); + } + + public int hashCode() { + return hash ^ Objects.hashCode(value); + } + + public String toString() { + return key.toString()+"="+value.toString(); + } +} +``` + + +### 成员变量 + +``` +/** + * The hash table data. + */ +private transient Entry[] table; + +/** + * The total number of entries in the hash table. + */ +private transient int count; + +/** + * The table is rehashed when its size exceeds this threshold. (The + * value of this field is (int)(capacity * loadFactor).) + * + * @serial + */ +private int threshold; + +/** + * The load factor for the hashtable. + * + * @serial + */ +private float loadFactor; + +/** + * The number of times this Hashtable has been structurally modified + * Structural modifications are those that change the number of entries in + * the Hashtable or otherwise modify its internal structure (e.g., + * rehash). This field is used to make iterators on Collection-views of + * the Hashtable fail-fast. (See ConcurrentModificationException). + */ +private transient int modCount = 0; +``` + + +### 构造方法 + +``` +public Hashtable(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal Load: "+loadFactor); + + if (initialCapacity==0) + initialCapacity = 1; + this.loadFactor = loadFactor; + table = new Entry[initialCapacity]; + threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); +} + +/** + * Constructs a new, empty hashtable with the specified initial capacity + * and default load factor (0.75). + * + * @param initialCapacity the initial capacity of the hashtable. + * @exception IllegalArgumentException if the initial capacity is less + * than zero. + */ +public Hashtable(int initialCapacity) { + this(initialCapacity, 0.75f); +} + +/** + * Constructs a new, empty hashtable with a default initial capacity (11) + * and load factor (0.75). + */ +public Hashtable() { + this(11, 0.75f); +} +``` + + +- 11? +- Hashtable 的容量增加逻辑是乘2+1,保证奇数。 + - 在应用数据分布在等差数据集合(如偶数)上时,如果公差与桶容量有公约数n,则至少有(n-1)/n数量的桶是利用不到的。 +### hash to index +- int hash = key.hashCode(); +int index = (hash & 0x7FFFFFFF) % tab.length; +- 取与之后一定是一个非负数 +- 0x7FFFFFFF is 0111 1111 1111 1111 1111 1111 1111 1111 : all 1 except the sign bit. +- (hash & 0x7FFFFFFF) will result in a positive integer. +- (hash & 0x7FFFFFFF) % tab.length will be in the range of the tab length. +### put(有锁) + +``` +public synchronized V put(K key, V value) { + // Make sure the value is not null + if (value == null) { + throw new NullPointerException(); + } + + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key.hashCode(); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry entry = (Entry)tab[index]; + for(; entry != null ; entry = entry.next) { + if ((entry.hash == hash) && entry.key.equals(key)) { + V old = entry.value; + entry.value = value; + return old; + } + } + + addEntry(hash, key, value, index); + return null; +} +``` + + + +``` +private void addEntry(int hash, K key, V value, int index) { + modCount++; + + Entry tab[] = table; + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + hash = key.hashCode(); + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + @SuppressWarnings("unchecked") + Entry e = (Entry) tab[index]; + tab[index] = new Entry<>(hash, key, value, e); + count++; +} +``` + + +### 扩容 rehash + - protected void rehash() { + int oldCapacity = table.length; + Entry[] oldMap = table; + + // overflow-conscious code + int newCapacity = (oldCapacity << 1) + 1; + if (newCapacity - MAX_ARRAY_SIZE > 0) { + if (oldCapacity == MAX_ARRAY_SIZE) + // Keep running with MAX_ARRAY_SIZE buckets + return; + newCapacity = MAX_ARRAY_SIZE; + } + Entry[] newMap = new Entry[newCapacity]; + + modCount++; + threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); + table = newMap; + + for (int i = oldCapacity ; i-- > 0 ;) { + for (Entry old = (Entry)oldMap[i] ; old != null ; ) { + Entry e = old; + old = old.next; + // 所有元素重新散列 + int index = (e.hash & 0x7FFFFFFF) % newCapacity; + e.next = (Entry)newMap[index]; + newMap[index] = e; + } + } +} + +### get(有锁) + +``` +public synchronized V get(Object key) { + Entry tab[] = table; + int hash = key.hashCode(); + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index] ; e != null ; e = e.next) { + if ((e.hash == hash) && e.key.equals(key)) { + return (V)e.value; + } + } + return null; +} +``` + + +- + +### remove(有锁) + +``` +public synchronized V remove(Object key) { + Entry tab[] = table; + int hash = key.hashCode(); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry e = (Entry)tab[index]; + for(Entry prev = null ; e != null ; prev = e, e = e.next) { + if ((e.hash == hash) && e.key.equals(key)) { + modCount++; + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + V oldValue = e.value; + e.value = null; + return oldValue; + } + } + return null; +} +``` + + +- + +## LinkedHashMap(底层是(数组+链表/红黑树)+环形双向链表,继承自HashMap) +- LinkedHashMap是key键有序的HashMap的一种实现。它除了使用哈希表这个数据结构,使用环形双向链表来保证key的顺序。 +- HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初放置到HashMap的顺序。HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个子类 —— LinkedHashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap 和 保持访问顺序(LRU,get后调整链表序,最新获取的放在最后)的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。 + +- 特点: +- 一般来说,如果需要使用的Map中的key无序,选择HashMap;如果要求key有序,则选择TreeMap。 + - 但是选择TreeMap就会有性能问题,因为TreeMap的get操作的时间复杂度是O(log(n))的,相比于HashMap的O(1)还是差不少的,LinkedHashMap的出现就是为了平衡这些因素,使得能够以O(1)时间复杂度增加查找元素,又能够保证key的有序性 + +- 实现原理: +- 将所有Entry节点链入一个双向链表的HashMap。在LinkedHashMap中,所有put进来的Entry都保存在哈希表中,但由于它又额外定义了一个以head为头结点的双向链表,因此对于每次put进来Entry,除了将其保存到哈希表上外,还会将其插入到双向链表的尾部。 + +- LinkedHashMap#Entry + +- static class Entry extends HashMap.Node { + Entry before, after; + Entry(int hash, K key, V value, Node next) { + super(hash, key, value, next); + } +} + +### 成员变量 + +``` +/** + * The head (eldest) of the doubly linked list. + */ +transient LinkedHashMap.Entry head; + +/** + * The tail (youngest) of the doubly linked list. + */ +transient LinkedHashMap.Entry tail; + +/** + * The iteration ordering method for this linked hash map: true + * for access-order, false for insertion-order. + * + * @serial + */ +final boolean accessOrder; +``` + + +### 构造方法 + +``` +public LinkedHashMap(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + accessOrder = false; +} + +/** + * Constructs an empty insertion-ordered LinkedHashMap instance + * with the specified initial capacity and a default load factor (0.75). + * + * @param initialCapacity the initial capacity + * @throws IllegalArgumentException if the initial capacity is negative + */ +public LinkedHashMap(int initialCapacity) { + super(initialCapacity); + accessOrder = false; +} + +/** + * Constructs an empty insertion-ordered LinkedHashMap instance + * with the default initial capacity (16) and load factor (0.75). + */ +public LinkedHashMap() { + super(); + accessOrder = false; +} +``` + + + +``` +/** + * Constructs an empty LinkedHashMap instance with the + * specified initial capacity, load factor and ordering mode. + * + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + * @param accessOrder the ordering mode - true for + * access-order, false for insertion-order + * @throws IllegalArgumentException if the initial capacity is negative + * or the load factor is nonpositive + */ +public LinkedHashMap(int initialCapacity, + float loadFactor, + boolean accessOrder) { + super(initialCapacity, loadFactor); + this.accessOrder = accessOrder; +} +``` + + +### put +- 同HashMap,但重写了afterNodeInsertion。 +- void afterNodeInsertion(boolean evict) { // possibly remove eldest + LinkedHashMap.Entry first; + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true); + } +} +- //可以自行重写该方法 +- protected boolean removeEldestEntry(Map.Entry eldest) { + return false; +} + + +``` +public class LRUHashMap extends LinkedHashMap{ +``` + + + + +``` + private final int MAX_CACHE_SIZE; +``` + + + +``` + public BaseLRUCache(int cacheSize) { +``` + +- super(cacheSize, 0.75f, true); +- MAX_CACHE_SIZE = cacheSize; +- } + +- @Override +- protected boolean removeEldestEntry(Map.Entry eldest) { +- return size() > MAX_CACHE_SIZE; +- } + + +- } +### get + +``` +public V get(Object key) { + Node e; + if ((e = getNode(hash(key), key)) == null) + return null; + if (accessOrder) + afterNodeAccess(e); + return e.value; +} +``` + + +- void afterNodeAccess(Node e) { // move node to last + LinkedHashMap.Entry last; + if (accessOrder && (last = tail) != e) { + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.after = null; + if (b == null) + head = a; + else + b.after = a; + if (a != null) + a.before = b; + else + last = b; + if (last == null) + head = p; + else { + p.before = last; + last.after = p; + } + tail = p; + ++modCount; + } +} +### remove +- 同HashMap,但重写了afterNodeRemoval。 +- void afterNodeRemoval(Node e) { // unlink + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.before = p.after = null; + if (b == null) + head = a; + else + b.after = a; + if (a == null) + tail = b; + else + a.before = b; +} + +### 遍历(迭代环形双向链表) +#### entrySet + +``` +public Set> entrySet() { + Set> es; + return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es; +} +``` + +- 它使用的是该迭代器: + +``` +abstract class LinkedHashIterator { + LinkedHashMap.Entry next; + LinkedHashMap.Entry current; + int expectedModCount; + + LinkedHashIterator() { + next = head; + expectedModCount = modCount; + current = null; + } + + public final boolean hasNext() { + return next != null; + } + + final LinkedHashMap.Entry nextNode() { + LinkedHashMap.Entry e = next; + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + if (e == null) + throw new NoSuchElementException(); + current = e; + next = e.after; + return e; + } + + public final void remove() { + Node p = current; + if (p == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + current = null; + K key = p.key; + removeNode(hash(key), key, null, false, false); + expectedModCount = modCount; + } +} +``` + + + +``` +final class LinkedEntryIterator extends LinkedHashIterator + implements Iterator> { + public final Map.Entry next() { return nextNode(); } +} +``` + + +## TreeMap(底层是红黑树) +- 支持排序的Map实现。 +- 基于红黑树实现,无容量限制。 +- 是非线程安全的。 + +- TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key +- TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap。 +- TreeMap的key不能为null,而HashMap的key可以为null。 +- TreeMap#Entry + +``` +static final class Entry implements Map.Entry { + K key; + V value; + Entry left; + Entry right; + Entry parent; + boolean color = BLACK; + + /** + * Make a new cell with given key, value, and parent, and with + * {@code null} child links, and BLACK color. + */ + Entry(K key, V value, Entry parent) { + this.key = key; + this.value = value; + this.parent = parent; + } + + /** + * Returns the key. + * + * @return the key + */ + public K getKey() { + return key; + } + + /** + * Returns the value associated with the key. + * + * @return the value associated with the key + */ + public V getValue() { + return value; + } + + /** + * Replaces the value currently associated with the key with the given + * value. + * + * @return the value associated with the key before this method was + * called + */ + public V setValue(V value) { + V oldValue = this.value; + this.value = value; + return oldValue; + } + + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry)o; + + return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); + } + + public int hashCode() { + int keyHash = (key==null ? 0 : key.hashCode()); + int valueHash = (value==null ? 0 : value.hashCode()); + return keyHash ^ valueHash; + } + + public String toString() { + return key + "=" + value; + } +} +``` + +### 成员变量 + +``` +private final Comparator comparator; + +private transient Entry root; + +/** + * The number of entries in the tree + */ +private transient int size = 0; + +/** + * The number of structural modifications to the tree. + */ +private transient int modCount = 0; +``` + + +### 构造方法 + +``` +public TreeMap() { + comparator = null; +} +``` + + +``` + +public TreeMap(Comparator comparator) { + this.comparator = comparator; +} +``` + + +### put + +``` +public V put(K key, V value) { + Entry t = root; + if (t == null) { + compare(key, key); // type (and possibly null) check + + root = new Entry<>(key, value, null); + size = 1; + modCount++; + return null; + } + int cmp; + Entry parent; + // split comparator and comparable paths + Comparator cpr = comparator; + if (cpr != null) { + do { + parent = t; + cmp = cpr.compare(key, t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else + return t.setValue(value); + } while (t != null); + } + else { + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + do { + parent = t; + cmp = k.compareTo(t.key); + if (cmp < 0) + t = t.left; + else if (cmp > 0) + t = t.right; + else + return t.setValue(value); + } while (t != null); + } + Entry e = new Entry<>(key, value, parent); + if (cmp < 0) + parent.left = e; + else + parent.right = e; + fixAfterInsertion(e); + size++; + modCount++; + return null; +} +``` + + +### get + +``` +public V get(Object key) { + Entry p = getEntry(key); + return (p==null ? null : p.value); +} +``` + + +- final Entry getEntry(Object key) { + // Offload comparator-based version for sake of performance + if (comparator != null) + return getEntryUsingComparator(key); + if (key == null) + throw new NullPointerException(); + @SuppressWarnings("unchecked") + Comparable k = (Comparable) key; + Entry p = root; + while (p != null) { + int cmp = k.compareTo(p.key); + if (cmp < 0) + p = p.left; + else if (cmp > 0) + p = p.right; + else + return p; + } + return null; +} + +- final Entry getEntryUsingComparator(Object key) { + @SuppressWarnings("unchecked") + K k = (K) key; + Comparator cpr = comparator; + if (cpr != null) { + Entry p = root; + while (p != null) { + int cmp = cpr.compare(k, p.key); + if (cmp < 0) + p = p.left; + else if (cmp > 0) + p = p.right; + else + return p; + } + } + return null; +} + +### remove + +``` +public V remove(Object key) { + Entry p = getEntry(key); + if (p == null) + return null; + + V oldValue = p.value; + deleteEntry(p); + return oldValue; +} +``` + + + +``` +private void deleteEntry(Entry p) { + modCount++; + size--; + + // If strictly internal, copy successor's element to p and then make p + // point to successor. + if (p.left != null && p.right != null) { + Entry s = successor(p); + p.key = s.key; + p.value = s.value; + p = s; + } // p has 2 children + + // Start fixup at replacement node, if it exists. + Entry replacement = (p.left != null ? p.left : p.right); + + if (replacement != null) { + // Link replacement to parent + replacement.parent = p.parent; + if (p.parent == null) + root = replacement; + else if (p == p.parent.left) + p.parent.left = replacement; + else + p.parent.right = replacement; + + // Null out links so they are OK to use by fixAfterDeletion. + p.left = p.right = p.parent = null; + + // Fix replacement + if (p.color == BLACK) + fixAfterDeletion(replacement); + } else if (p.parent == null) { // return if we are the only node. + root = null; + } else { // No children. Use self as phantom replacement and unlink. + if (p.color == BLACK) + fixAfterDeletion(p); + + if (p.parent != null) { + if (p == p.parent.left) + p.parent.left = null; + else if (p == p.parent.right) + p.parent.right = null; + p.parent = null; + } + } +} +``` + + +### containsKey + +``` +public boolean containsKey(Object key) { + return getEntry(key) != null; +} +``` + + +### containsValue + +``` +public boolean containsValue(Object value) { + for (Entry e = getFirstEntry(); e != null; e = successor(e)) + if (valEquals(value, e.value)) + return true; + return false; +} +``` + + +- final Entry getFirstEntry() { + Entry p = root; + if (p != null) + while (p.left != null) + p = p.left; + return p; +} + +- static TreeMap.Entry successor(Entry t) { + if (t == null) + return null; + else if (t.right != null) { + Entry p = t.right; + while (p.left != null) + p = p.left; + return p; + } else { + Entry p = t.parent; + Entry ch = t; + while (p != null && ch == p.right) { + ch = p; + p = p.parent; + } + return p; + } +} + + - static final boolean valEquals(Object o1, Object o2) { + return (o1==null ? o2==null : o1.equals(o2)); +} + +### 遍历 + +``` +public Set> entrySet() { + EntrySet es = entrySet; + return (es != null) ? es : (entrySet = new EntrySet()); +} +``` + + + +``` +class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new EntryIterator(getFirstEntry()); + } +``` + +- } + + +``` +abstract class PrivateEntryIterator implements Iterator { + Entry next; + Entry lastReturned; + int expectedModCount; + + PrivateEntryIterator(Entry first) { + expectedModCount = modCount; + lastReturned = null; + next = first; + } + + public final boolean hasNext() { + return next != null; + } + + final Entry nextEntry() { + Entry e = next; + if (e == null) + throw new NoSuchElementException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + next = successor(e); + lastReturned = e; + return e; + } + + final Entry prevEntry() { + Entry e = next; + if (e == null) + throw new NoSuchElementException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + next = predecessor(e); + lastReturned = e; + return e; + } + + public void remove() { + if (lastReturned == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + // deleted entries are replaced by their successors + if (lastReturned.left != null && lastReturned.right != null) + next = lastReturned; + deleteEntry(lastReturned); + expectedModCount = modCount; + lastReturned = null; + } +} +``` + + + + +``` +final class EntryIterator extends PrivateEntryIterator> { + EntryIterator(Entry first) { + super(first); + } + public Map.Entry next() { + return nextEntry(); + } +} +``` + + +- + +## ConcurrentHashMap(底层是数组+链表/红黑树,基于CAS+synchronized) +- JDK1.7前:分段锁 +- 基于currentLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可以允许16个线程并发无阻塞地操作集合对象,尽可能地减少并发时的阻塞现象。 +- put、remove会加锁。get和containsKey不会加锁。 +- 计算size:在不加锁的情况下遍历所有的段,读取其count以及modCount,这两个属性都是volatile类型的,并进行统计,再遍历一次所有的段,比较modCount是否有改变。如有改变,则再尝试两次机上动作。 +- 如执行了三次上述动作,仍然有问题,则遍历所有段,分别进行加锁,然后进行计算,计算完毕后释放所有锁,从而完成计算动作。 + +- JDK1.8后:CAS+synchronized +- bin是桶 bucket的意思 + +- ConcurrentHashMap是延迟初始化的,只有在插入数据时,整个HashMap才被初始化为2的次方大小个桶(bin),每个bin包含哈希值相同的一系列Node(一般含有0或1个Node)。每个bin的第一个Node作为这个bin的锁,Hash值为零或者负的将被忽略; +- 每个bin的第一个Node插入用到CAS原理,这是在ConcurrentHashMap中最常发生的操作,其余的插入、删除、替换操作对bin中的第一个Node加锁,进行操作 +- ConcurrentHashMap的size()函数一般比较少用,同时为了提高增删查改的效率,容器并未在内部保存一个size值,而且采用每次调用size()函数时累加各个bin中Node的个数计算得到,而且这一过程不加锁,即得到的size值不一定是最新的。 +- + +- ConcurrentHashMap#Node +- Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别:它对value和next属性设置了volatile属性;’它不允许调用setValue方法直接改变Node的value域;它增加了find方法辅助map.get()方法。 + +``` +static class Node implements Map.Entry { + final int hash; + final K key; + volatile V val; // value和next是volatile的 + volatile Node next; + + Node(int hash, K key, V val, Node next) { + this.hash = hash; + this.key = key; + this.val = val; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return val; } + public final int hashCode() { return key.hashCode() ^ val.hashCode(); } + public final String toString(){ return key + "=" + val; } + public final V setValue(V value) { + throw new UnsupportedOperationException(); + } + + public final boolean equals(Object o) { + Object k, v, u; Map.Entry e; + return ((o instanceof Map.Entry) && + (k = (e = (Map.Entry)o).getKey()) != null && + (v = e.getValue()) != null && + (k == key || k.equals(key)) && + (v == (u = val) || v.equals(u))); + } + + /** + * Virtualized support for map.get(); overridden in subclasses. + */ + Node find(int h, Object k) { + Node e = this; + if (k != null) { + do { + K ek; + if (e.hash == h && + ((ek = e.key) == k || (ek != null && k.equals(ek)))) + return e; + } while ((e = e.next) != null); + } + return null; + } +} +``` + + +- ConcurrentHashMap#TreeNode +- 当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap继承自Node类,而并非HashMap中的继承自LinkedHashMap.Entry类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。 + +``` +static final class TreeNode extends Node { + TreeNode parent; // red-black tree links + TreeNode left; + TreeNode right; + TreeNode prev; // needed to unlink next upon deletion + boolean red; + + TreeNode(int hash, K key, V val, Node next, + TreeNode parent) { + super(hash, key, val, next); + this.parent = parent; + } + + Node find(int h, Object k) { + return findTreeNode(h, k, null); + } + + /** + * Returns the TreeNode (or null if not found) for the given key + * starting at given root. + */ + final TreeNode findTreeNode(int h, Object k, Class kc) { + if (k != null) { + TreeNode p = this; + do { + int ph, dir; K pk; TreeNode q; + TreeNode pl = p.left, pr = p.right; + if ((ph = p.hash) > h) + p = pl; + else if (ph < h) + p = pr; + else if ((pk = p.key) == k || (pk != null && k.equals(pk))) + return p; + else if (pl == null) + p = pr; + else if (pr == null) + p = pl; + else if ((kc != null || + (kc = comparableClassFor(k)) != null) && + (dir = compareComparables(kc, k, pk)) != 0) + p = (dir < 0) ? pl : pr; + else if ((q = pr.findTreeNode(h, k, kc)) != null) + return q; + else + p = pl; + } while (p != null); + } + return null; + } +} +``` + +- ConcurrentHashMap#TreeBin +- 这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。 + +- 可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识位;同时也看到我们熟悉的红黑树构造方法。 + +``` +/** + * TreeNodes used at the heads of bins. TreeBins do not hold user + * keys or values, but instead point to list of TreeNodes and + * their root. They also maintain a parasitic read-write lock + * forcing writers (who hold bin lock) to wait for readers (who do + * not) to complete before tree restructuring operations. + */ +static final class TreeBin extends Node { + TreeNode root; + volatile TreeNode first; + volatile Thread waiter; + volatile int lockState; + // values for lockState + static final int WRITER = 1; // set while holding write lock + static final int WAITER = 2; // set when waiting for write lock + static final int READER = 4; // increment value for setting read lock + + /** + * Tie-breaking utility for ordering insertions when equal + * hashCodes and non-comparable. We don't require a total + * order, just a consistent insertion rule to maintain + * equivalence across rebalancings. Tie-breaking further than + * necessary simplifies testing a bit. + */ + static int tieBreakOrder(Object a, Object b) { + int d; + if (a == null || b == null || + (d = a.getClass().getName(). + compareTo(b.getClass().getName())) == 0) + d = (System.identityHashCode(a) <= System.identityHashCode(b) ? + -1 : 1); + return d; + } + + /** + * Creates bin with initial set of nodes headed by b. + */ + TreeBin(TreeNode b) { + super(TREEBIN, null, null, null); + this.first = b; + TreeNode r = null; + for (TreeNode x = b, next; x != null; x = next) { + next = (TreeNode)x.next; + x.left = x.right = null; + if (r == null) { + x.parent = null; + x.red = false; + r = x; + } + else { + K k = x.key; + int h = x.hash; + Class kc = null; + for (TreeNode p = r;;) { + int dir, ph; + K pk = p.key; + if ((ph = p.hash) > h) + dir = -1; + else if (ph < h) + dir = 1; + else if ((kc == null && + (kc = comparableClassFor(k)) == null) || + (dir = compareComparables(kc, k, pk)) == 0) + dir = tieBreakOrder(k, pk); + TreeNode xp = p; + if ((p = (dir <= 0) ? p.left : p.right) == null) { + x.parent = xp; + if (dir <= 0) + xp.left = x; + else + xp.right = x; + r = balanceInsertion(r, x); + break; + } + } + } + } + this.root = r; + assert checkInvariants(root); + } +``` + +- } +- + +- ConcurrentHashMap#ForwardingNode + +``` +/** + * A node inserted at head of bins during transfer operations. + */ +static final class ForwardingNode extends Node { + final Node[] nextTable; + ForwardingNode(Node[] tab) { + super(MOVED, null, null, null); + this.nextTable = tab; + } + + Node find(int h, Object k) { + // loop to avoid arbitrarily deep recursion on forwarding nodes + outer: for (Node[] tab = nextTable;;) { + Node e; int n; + if (k == null || tab == null || (n = tab.length) == 0 || + (e = tabAt(tab, (n - 1) & h)) == null) + return null; + for (;;) { + int eh; K ek; + if ((eh = e.hash) == h && + ((ek = e.key) == k || (ek != null && k.equals(ek)))) + return e; + if (eh < 0) { + if (e instanceof ForwardingNode) { + tab = ((ForwardingNode)e).nextTable; + continue outer; + } + else + return e.find(h, k); + } + if ((e = e.next) == null) + return null; + } + } + } +} +``` + +- + +- ConcurrentHashMap#ReservationNode + +``` +/** + * A place-holder node used in computeIfAbsent and compute + */ +static final class ReservationNode extends Node { + ReservationNode() { + super(RESERVED, null, null, null); + } + + Node find(int h, Object k) { + return null; + } +} +``` + + +- + +### 节点类型 +- hash值大于等于0,则是链表节点,Node +- hash值为-1 MOVED,则是forwarding nodes,存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。 +- hash值为-2 TREEBIN,则是红黑树根,TreeBin类型 +- hash值为-3 RESERVED,则是reservation nodes, + +- static final int MOVED = -1; // hash for forwarding nodes +static final int TREEBIN = -2; // hash for roots of trees +static final int RESERVED = -3; // hash for transient reservations + +- + +### 成员变量 + +``` +/** + * The largest possible table capacity. This value must be + * exactly 1<<30 to stay within Java array allocation and indexing + * bounds for power of two table sizes, and is further required + * because the top two bits of 32bit hash fields are used for + * control purposes. + */ +private static final int MAXIMUM_CAPACITY = 1 << 30; + +/** + * The default initial table capacity. Must be a power of 2 + * (i.e., at least 1) and at most MAXIMUM_CAPACITY. + */ +private static final int DEFAULT_CAPACITY = 16; + +/** + * The largest possible (non-power of two) array size. + * Needed by toArray and related methods. + */ +static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + +/** + * The default concurrency level for this table. Unused but + * defined for compatibility with previous versions of this class. + */ +private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + +/** + * The load factor for this table. Overrides of this value in + * constructors affect only the initial table capacity. The + * actual floating point value isn't normally used -- it is + * simpler to use expressions such as {@code n - (n >>> 2)} for + * the associated resizing threshold. + */ +private static final float LOAD_FACTOR = 0.75f; + +/** + * The bin count threshold for using a tree rather than list for a + * bin. Bins are converted to trees when adding an element to a + * bin with at least this many nodes. The value must be greater + * than 2, and should be at least 8 to mesh with assumptions in + * tree removal about conversion back to plain bins upon + * shrinkage. + */ +static final int TREEIFY_THRESHOLD = 8; + +/** + * The bin count threshold for untreeifying a (split) bin during a + * resize operation. Should be less than TREEIFY_THRESHOLD, and at + * most 6 to mesh with shrinkage detection under removal. + */ +static final int UNTREEIFY_THRESHOLD = 6; + +/** + * The smallest table capacity for which bins may be treeified. + * (Otherwise the table is resized if too many nodes in a bin.) + * The value should be at least 4 * TREEIFY_THRESHOLD to avoid + * conflicts between resizing and treeification thresholds. + */ +static final int MIN_TREEIFY_CAPACITY = 64; + +/** + * Minimum number of rebinnings per transfer step. Ranges are + * subdivided to allow multiple resizer threads. This value + * serves as a lower bound to avoid resizers encountering + * excessive memory contention. The value should be at least + * DEFAULT_CAPACITY. + */ +private static final int MIN_TRANSFER_STRIDE = 16; + +/** + * The number of bits used for generation stamp in sizeCtl. + * Must be at least 6 for 32bit arrays. + */ +private static int RESIZE_STAMP_BITS = 16; + +/** + * The maximum number of threads that can help resize. + * Must fit in 32 - RESIZE_STAMP_BITS bits. + */ +private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; + +/** + * The bit shift for recording size stamp in sizeCtl. + */ +private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; +/* + * Encodings for Node hash fields. See above for explanation. + */ +static final int MOVED = -1; // hash for forwarding nodes +static final int TREEBIN = -2; // hash for roots of trees +static final int RESERVED = -3; // hash for transient reservations +static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash + +/** Number of CPUS, to place bounds on some sizings */ +static final int NCPU = Runtime.getRuntime().availableProcessors(); +``` + + + +``` +/** + * The array of bins. Lazily initialized upon first insertion. + * Size is always a power of two. Accessed directly by iterators. + */ +transient volatile Node[] table; + +/** + * The next table to use; non-null only while resizing. + */ +private transient volatile Node[] nextTable; + +/** + * Base counter value, used mainly when there is no contention, + * but also as a fallback during table initialization + * races. Updated via CAS. + */ +private transient volatile long baseCount; + +/** + * Table initialization and resizing control. When negative, the + * table is being initialized or resized: -1 for initialization, + * else -(1 + the number of active resizing threads). Otherwise, + * when table is null, holds the initial table size to use upon + * creation, or 0 for default. After initialization, holds the + * next element count value upon which to resize the table. +``` + +- 负数代表正在进行初始化或扩容操作 +- -1代表正在初始化 +- -N 表示有N-1个线程正在进行扩容操作 + +``` +正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。 + */ +private transient volatile int sizeCtl; + +/** + * The next table index (plus one) to split while resizing. + */ +private transient volatile int transferIndex; + +/** + * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. + */ +private transient volatile int cellsBusy; + +/** + * Table of counter cells. When non-null, size is a power of 2. + */ +private transient volatile CounterCell[] counterCells; + +// views +private transient KeySetView keySet; +private transient ValuesView values; +private transient EntrySetView entrySet; +``` + + +### 构造方法 + +``` +public ConcurrentHashMap() { +} + +/** + * Creates a new, empty map with an initial table size + * accommodating the specified number of elements without the need + * to dynamically resize. + * + * @param initialCapacity The implementation performs internal + * sizing to accommodate this many elements. + * @throws IllegalArgumentException if the initial capacity of + * elements is negative + */ +public ConcurrentHashMap(int initialCapacity) { + if (initialCapacity < 0) + throw new IllegalArgumentException(); + int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? + MAXIMUM_CAPACITY : + tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); + this.sizeCtl = cap; +} + +/** + * Creates a new map with the same mappings as the given map. + * + * @param m the map + */ +public ConcurrentHashMap(Map m) { + this.sizeCtl = DEFAULT_CAPACITY; + putAll(m); +} + +/** + * Creates a new, empty map with an initial table size based on + * the given number of elements ({@code initialCapacity}) and + * initial table density ({@code loadFactor}). + * + * @param initialCapacity the initial capacity. The implementation + * performs internal sizing to accommodate this many elements, + * given the specified load factor. + * @param loadFactor the load factor (table density) for + * establishing the initial table size + * @throws IllegalArgumentException if the initial capacity of + * elements is negative or the load factor is nonpositive + * + * @since 1.6 + */ +public ConcurrentHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, 1); +} + +/** + * Creates a new, empty map with an initial table size based on + * the given number of elements ({@code initialCapacity}), table + * density ({@code loadFactor}), and number of concurrently + * updating threads ({@code concurrencyLevel}). + * + * @param initialCapacity the initial capacity. The implementation + * performs internal sizing to accommodate this many elements, + * given the specified load factor. + * @param loadFactor the load factor (table density) for + * establishing the initial table size + * @param concurrencyLevel the estimated number of concurrently + * updating threads. The implementation may use this value as + * a sizing hint. + * @throws IllegalArgumentException if the initial capacity is + * negative or the load factor or concurrencyLevel are + * nonpositive + */ +public ConcurrentHashMap(int initialCapacity, + float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) + throw new IllegalArgumentException(); + if (initialCapacity < concurrencyLevel) // Use at least as many bins + initialCapacity = concurrencyLevel; // as estimated threads + long size = (long)(1.0 + (long)initialCapacity / loadFactor); + int cap = (size >= (long)MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : tableSizeFor((int)size); + this.sizeCtl = cap; +} +``` + +### CAS + +``` +private static final sun.misc.Unsafe U; +``` + +- Unsafe类的几个CAS方法,可以原子性地修改对象的某个属性值 + + +``` +/** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ +public final native boolean compareAndSwapObject(Object o, long offset, + Object expected, + Object x); + +/** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ +public final native boolean compareAndSwapInt(Object o, long offset, + int expected, + int x); + +/** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ +public final native boolean compareAndSwapLong(Object o, long offset, + long expected, + long x); +``` + + +``` +/** + * Fetches a reference value from a given Java variable, with volatile + * load semantics. Otherwise identical to {@link #getObject(Object, long)} + */ +public native Object getObjectVolatile(Object o, long offset); + +/** + * Stores a reference value into a given Java variable, with + * volatile store semantics. Otherwise identical to {@link #putObject(Object, long, Object)} + */ +public native void putObjectVolatile(Object o, long offset, Object x); +``` + +- Unsafe.getObjectVolatile可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。 +### 三个核心方法 +- ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。 +- static final Node tabAt(Node[] tab, int i) { + return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); +} + +static final boolean casTabAt(Node[] tab, int i, + Node c, Node v) { + return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); +} + +static final void setTabAt(Node[] tab, int i, Node v) { + U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); +} +- + +### 初始化 +- 对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。 + +- 初始化方法主要应用了关键属性sizeCtl 如果这个值<0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。 + + +``` +private final Node[] initTable() { + Node[] tab; int sc; + while ((tab = table) == null || tab.length == 0) { + if ((sc = sizeCtl) < 0) + Thread.yield(); // lost initialization race; just spin +``` + + - // 利用CAS方法把sizectl的值置为-1 表示本线程正在进行初始化 + else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if ((tab = table) == null || tab.length == 0) { + int n = (sc > 0) ? sc : DEFAULT_CAPACITY; + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n]; + table = tab = nt; + - // 相当于0.75*n 设置一个扩容的阈值 + sc = n - (n >>> 2); + } + } finally { + sizeCtl = sc; + } + break; + } + } + return tab; +} +- + +- + +### spread(hash) +- h是某个对象的hashCode返回值 + - static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; +} + +- static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash + +- 类似于Hashtable+HashMap的hash实现,Hashtable中也是和一个魔法值取与,保证结果一定为正数;HashMap中也是将hashCode与其移动低n位的结果再取异或,保证了对象的hashCode的高16位的变化能反应到低16位中, +### size相关 +#### 成员变量 +- @sun.misc.Contended static final class CounterCell { + volatile long value; + CounterCell(long x) { value = x; } +} + + +``` +/** + * Base counter value, used mainly when there is no contention, + * but also as a fallback during table initialization + * races. Updated via CAS. + */ +private transient volatile long baseCount; +``` + + +``` +/** + * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. + */ +private transient volatile int cellsBusy; + +/** + * Table of counter cells. When non-null, size is a power of 2. + */ +private transient volatile CounterCell[] counterCells; +``` + +- 每个CounterCell都对应一个bucket,CounterCell中的long值就是对应bucket的binCount。 +- 计算总大小就是将所有bucket的binCount求和,而每个binCount都存储在CounterCell#value中,每当put或者remove时都会更新节点所在bucket对应的CounterCell#value。 + +#### size() +- 没有直接返回baseCount 而是统计一次这个值,而这个值其实也是一个大概的数值,因此可能在统计的时候有其他线程正在执行插入或删除操作。 + +``` +public int size() { + long n = sumCount(); + return ((n < 0L) ? 0 : + (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int)n); +} +``` + +- 在baseCount基础上再加上所有counterCell的值求和。 + +- 而在addCount时,会先尝试CAS更新baseCount,如果有冲突,则再尝试CAS更新随机的一个counterCell中的value,这样求和就是正确的size了。 +- final long sumCount() { + CounterCell[] as = counterCells; +- CounterCell a; + long sum = baseCount; + if (as != null) { + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) +- // 所有counter的值求和 + sum += a.value; + } + } + return sum; +} +- + +### put(若bucket第一个结点插入则使用CAS,否则加锁) + +``` +public V put(K key, V value) { + return putVal(key, value, false); +} +``` + + +- 整体流程就是首先定义不允许key或value为null的情况放入 。对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。 + - 1)如果这个位置是空的,那么直接放入,而且不需要加锁操作。 + - 2)如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。 +- a)如果是MOVED节点,则表示正在扩容,帮助进行扩容 +- b)如果是链表节点(hash >=0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。 如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。 +- c)如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。 + - 3)addCount 增加计数值 + + +``` +/** Implementation for put and putIfAbsent */ +final V putVal(K key, V value, boolean onlyIfAbsent) { + if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; +``` + +- // 死循环,只有插入成功时才会跳出 + for (Node[] tab = table;;) { + Node f; int n, i, fh; + if (tab == null || (n = tab.length) == 0) + - // table为空则初始化(延迟初始化) + tab = initTable(); + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { +- // hash to index后正好为空,则CAS放入;如果失败那么进入下次循环继续尝试 + if (casTabAt(tab, i, null, + new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } +- // 如果index处非空,且hash为MOVED(表示该节点是ForwardingNode),则表示有其它线程正在扩容,则一起进行扩容操作。 +- else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); +- // 如果index处非空,且为链表节点或树节点 + else { + V oldVal = null; +- // 对某个bucket上执行添加操作仅需要锁住第一个Node即可(可以保证不会多线程同时对某个bucket进行写入) + synchronized (f) { + if (tabAt(tab, i) == f) { + - // 1) 如果是链表节点,那么插入到链表中 + if (fh >= 0) { +- // binCount是该bucket中元素个数 + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, + value, null); + break; + } + } + } + - // 2)如果是红黑树树根,那么插入到红黑树中 + else if (f instanceof TreeBin) { + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, + value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } +- // 插入节点/释放锁之后,如果大小合适调整为红黑树,那么将链表转为红黑树 + if (binCount != 0) { + if (binCount >= TREEIFY_THRESHOLD) + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } +- // 将当前ConcurrentHashMap的元素数量+1 ,如果超过阈值,那么进行扩容 + addCount(1L, binCount); + return null; +} +### treeifyBin(有锁,数组较小则扩容,较大则转为红黑树) + +``` +private final void treeifyBin(Node[] tab, int index) { + Node b; int n, sc; + if (tab != null) { +``` + + - // 如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置 + if ((n = tab.length) < MIN_TREEIFY_CAPACITY) + tryPresize(n << 1); + else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { + synchronized (b) { + if (tabAt(tab, index) == b) { + TreeNode hd = null, tl = null; + for (Node e = b; e != null; e = e.next) { + TreeNode p = + new TreeNode(e.hash, e.key, e.val, + null, null); + if ((p.prev = tl) == null) + hd = p; + else + tl.next = p; + tl = p; + } + setTabAt(tab, index, new TreeBin(hd)); + } + } + } + } +} +- tryPreSize + +``` +/** + * Tries to presize table to accommodate the given number of elements. + * + * @param size number of elements (doesn't need to be perfectly accurate) + */ +private final void tryPresize(int size) { + int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : + tableSizeFor(size + (size >>> 1) + 1); + int sc; + while ((sc = sizeCtl) >= 0) { + Node[] tab = table; int n; + if (tab == null || (n = tab.length) == 0) { + n = (sc > c) ? sc : c; + if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if (table == tab) { + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n]; + table = nt; + sc = n - (n >>> 2); + } + } finally { + sizeCtl = sc; + } + } + } + else if (c <= sc || n >= MAXIMUM_CAPACITY) + break; + else if (tab == table) { + int rs = resizeStamp(n); + if (sc < 0) { + Node[] nt; + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + transfer(tab, nt); + } + else if (U.compareAndSwapInt(this, SIZECTL, sc, + (rs << RESIZE_STAMP_SHIFT) + 2)) + transfer(tab, null); + } + } +} +``` + + +- + +### 扩容 +#### tryPresize +- tryPresize在putAll以及treeifyBin中调用 + +``` +private final void tryPresize(int size) { +``` + + - // c是扩容之后预计表的大小 + int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : + tableSizeFor(size + (size >>> 1) + 1); + int sc; +- // 没有正在初始化或扩容 + while ((sc = sizeCtl) >= 0) { + Node[] tab = table; int n; + if (tab == null || (n = tab.length) == 0) { + n = (sc > c) ? sc : c; + - // 期间没有其他线程对表操作,则CAS将SIZECTL状态置为-1,表示正在进行初始化 + if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if (table == tab) { + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n]; + table = nt; + - // 即0.75*n + sc = n - (n >>> 2); + } + } finally { + sizeCtl = sc; + } + } + } +- // 若欲扩容值不大于原阈值,或现有容量>=最值,则do nothing + else if (c <= sc || n >= MAXIMUM_CAPACITY) + break; + - // table不为空,且在此期间其他线程未修改table + else if (tab == table) { + int rs = resizeStamp(n); + if (sc < 0) { + Node[] nt; + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + - // 协助扩容 + transfer(tab, nt); + } + else if (U.compareAndSwapInt(this, SIZECTL, sc, + (rs << RESIZE_STAMP_SHIFT) + 2)) +- // 发起扩容 + transfer(tab, null); + } + } +} + +#### addCount +- x=1,check=bucketCount + +``` +private final void addCount(long x, int check) { +``` + +- // 计数值加x +- // 利用CAS方法更新baseCount的值 + CounterCell[] as; long b, s; + if ((as = counterCells) != null || + !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { +- // 如果CAS更新baseCount失败或者counterCells不为空,那么尝试CAS更新当前线程的hashCode对应的bucket的value + CounterCell a; long v; int m; + boolean uncontended = true; + - if (as == null || (m = as.length - 1) < 0 || + (a = as[ThreadLocalRandom.getProbe() & m]) == null || + !(uncontended = + U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { + - // 如果两次CAS都失败了,那么调用fullAddCount方法 + fullAddCount(x, uncontended); + return; + } + if (check <= 1) + return; + s = sumCount(); + } +- // 以上与扩容无关,如果check值大于等于0 则需要检查是否需要进行扩容操作 + if (check >= 0) { + Node[] tab, nt; int n, sc; +- while (s >= (long)(sc = sizeCtl) && (tab = table) != null && + (n = tab.length) < MAXIMUM_CAPACITY) { + int rs = resizeStamp(n); +- // 如果sizeCtl是小于0的,说明有其他线程正在执行扩容操作,nextTable一定不为空 + if (sc < 0) { + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || (nt = nextTable) == null || + transferIndex <= 0) + break; +- // 协助扩容 + - if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) + transfer(tab, nt); + } + - // 当前线程是唯一的或是第一个发起扩容的线程 此时nextTable=null + else if (U.compareAndSwapInt(this, SIZECTL, sc, + (rs << RESIZE_STAMP_SHIFT) + 2)) +- // 发起扩容 + transfer(tab, null); + s = sumCount(); + } + } +} + + +``` +private final void fullAddCount(long x, boolean wasUncontended) { + int h; + if ((h = ThreadLocalRandom.getProbe()) == 0) { + ThreadLocalRandom.localInit(); // force initialization + h = ThreadLocalRandom.getProbe(); + wasUncontended = true; + } + boolean collide = false; // True if last slot nonempty + for (;;) { + CounterCell[] as; CounterCell a; int n; long v; + if ((as = counterCells) != null && (n = as.length) > 0) { + if ((a = as[(n - 1) & h]) == null) { + if (cellsBusy == 0) { // Try to attach new Cell + CounterCell r = new CounterCell(x); // Optimistic create + if (cellsBusy == 0 && + U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { + boolean created = false; + try { // Recheck under lock + CounterCell[] rs; int m, j; + if ((rs = counterCells) != null && + (m = rs.length) > 0 && + rs[j = (m - 1) & h] == null) { + rs[j] = r; + created = true; + } + } finally { + cellsBusy = 0; + } + if (created) + break; + continue; // Slot is now non-empty + } + } + collide = false; + } + else if (!wasUncontended) // CAS already known to fail + wasUncontended = true; // Continue after rehash + else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) + break; + else if (counterCells != as || n >= NCPU) + collide = false; // At max size or stale + else if (!collide) + collide = true; + else if (cellsBusy == 0 && + U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { + try { + if (counterCells == as) {// Expand table unless stale + CounterCell[] rs = new CounterCell[n << 1]; + for (int i = 0; i < n; ++i) + rs[i] = as[i]; + counterCells = rs; + } + } finally { + cellsBusy = 0; + } + collide = false; + continue; // Retry with expanded table + } + h = ThreadLocalRandom.advanceProbe(h); + } + else if (cellsBusy == 0 && counterCells == as && + U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { + boolean init = false; + try { // Initialize table + if (counterCells == as) { + CounterCell[] rs = new CounterCell[2]; + rs[h & 1] = new CounterCell(x); + counterCells = rs; + init = true; + } + } finally { + cellsBusy = 0; + } + if (init) + break; + } + else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) + break; // Fall back on using base + } +} +``` + +- + +#### transfer +- 当table容量不足的时候,即table的元素数量达到容量阈值sizeCtl,需要对table进行扩容。 整个扩容分为两部分: + - 1)构建一个nextTable,大小为table的两倍。 + - 2)把table的数据复制到nextTable中。 + +- 这两个过程在单线程下实现很简单,但是ConcurrentHashMap是支持并发插入的,扩容操作自然也会有并发的出现,这种情况下,第二步可以支持节点的并发复制,这样性能自然提升不少,但实现的复杂度也上升了一个台阶。 +- 先看第一步,构建nextTable,毫无疑问,这个过程只能有单个线程进行nextTable的初始化。 +- 通过Unsafe.compareAndSwapInt修改sizeCtl值,保证只有一个线程能够初始化nextTable,扩容后的数组长度为原来的两倍。 +- 节点从table移动到nextTable,大体思想是遍历、复制的过程。 + - 1)首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个ForwardingNode实例fwd。 + - 2)如果f==null,则在table中的i位置放入fwd,这个过程是采用 +- Unsafe.compareAndSwapObjectf方法实现的,很巧妙的实现了节点的并发移动。 + - 3)如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。 + - 4)如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。 + - 5)遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。 + +- 在多线程环境下,ConcurrentHashMap用两点来保证正确性:ForwardingNode和synchronized。当一个线程遍历到的节点如果是ForwardingNode,则继续往后遍历,如果不是,则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点,以便要其他线程可以看到该节点已经处理过了,如此交叉进行,高效而又安全。 + + +``` +/** + * Moves and/or copies the nodes in each bin to new table. See + * above for explanation. + */ +private final void transfer(Node[] tab, Node[] nextTab) { + int n = tab.length, stride; + if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) + stride = MIN_TRANSFER_STRIDE; // subdivide range +``` + +- // 扩容第一步,创建两倍长的数组nextTable,单线程执行 +- // initiating只能有一个线程进行构造nextTable,如果别的线程进入发现不为空就不用构造nextTable了 + if (nextTab == null) { // initiating + try { + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n << 1]; + nextTab = nt; + } catch (Throwable ex) { // try to cope with OOME + sizeCtl = Integer.MAX_VALUE; + return; + } + nextTable = nextTab; + // 原先扩容大小 + transferIndex = n; + } +- // 扩容第二步,把table的数据复制到nextTable中,多线程可以同时进行 + int nextn = nextTab.length; +- // 构造一个ForwardingNode用于多线程之间的共同扩容情况 + ForwardingNode fwd = new ForwardingNode(nextTab); + boolean advance = true; + boolean finishing = false; // to ensure sweep before committing nextTab + // 遍历每个节点 +- for (int i = 0, bound = 0;;) { + Node f; int fh; + while (advance) { + int nextIndex, nextBound; + if (--i >= bound || finishing) + advance = false; + else if ((nextIndex = transferIndex) <= 0) { + i = -1; + advance = false; + } + else if (U.compareAndSwapInt + (this, TRANSFERINDEX, nextIndex, + nextBound = (nextIndex > stride ? + nextIndex - stride : 0))) { + bound = nextBound; + i = nextIndex - 1; + advance = false; + } + } + +``` +// 得到一个i,i指向table中某一个尚未拷贝的bucket,下面的代码是对i对应的bucket进行拷贝,拷贝完后将bucket赋值为fwd(ForwadingNode) + //**************************************************************// +``` + +- if (i < 0 || i >= n || i + n >= nextn) { + int sc; +- // 如果原table已经复制结束 + if (finishing) { + nextTable = null; + table = nextTab; + - // 修改扩容后的阀值,应该是现在容量的0.75倍 + sizeCtl = (n << 1) - (n >>> 1); + return; + } + // 采用CAS算法更新SizeCtl,减一,表示有一个新的线程参与到扩容操作 + - if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { + if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) + return; + finishing = advance = true; + i = n; // recheck before commit + } + } + // CAS算法获取某一个数组的节点,为空就设为ForwordingNode + else if ((f = tabAt(tab, i)) == null) + advance = casTabAt(tab, i, null, fwd); +- // 如果这个节点的hash值是MOVED,就表示这个节点是ForwordingNode节点,就表示这个节点已经被处理过了,直接跳过 +- else if ((fh = f.hash) == MOVED) + advance = true; // already processed + else { + synchronized (f) { + if (tabAt(tab, i) == f) { + Node ln, hn; + if (fh >= 0) { +- //如果这个节点的确是链表节点,则拆分为两个子链表,存储到nextTable相应的两个位置 + int runBit = fh & n; + Node lastRun = f; + for (Node p = f.next; p != null; p = p.next) { + int b = p.hash & n; + if (b != runBit) { + runBit = b; + lastRun = p; + } + } + if (runBit == 0) { + ln = lastRun; + hn = null; + } + else { + hn = lastRun; + ln = null; + } +- // + for (Node p = f; p != lastRun; p = p.next) { + int ph = p.hash; K pk = p.key; V pv = p.val; + if ((ph & n) == 0) + ln = new Node(ph, pk, pv, ln); + else + hn = new Node(ph, pk, pv, hn); + } +- // CAS存储在nextTable的i位置上 + setTabAt(nextTab, i, ln); +- // CAS存储在nextTable的i+n位置上 + setTabAt(nextTab, i + n, hn); +- // CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕 + setTabAt(tab, i, fwd); + advance = true; + } +- // 如果这个节点是红黑树,则拆分为两颗子树,保存到nextTable相应的两个位置 + else if (f instanceof TreeBin) { + TreeBin t = (TreeBin)f; + TreeNode lo = null, loTail = null; + TreeNode hi = null, hiTail = null; + int lc = 0, hc = 0; + for (Node e = t.first; e != null; e = e.next) { + int h = e.hash; + TreeNode p = new TreeNode + (h, e.key, e.val, null, null); + if ((h & n) == 0) { + if ((p.prev = loTail) == null) + lo = p; + else + loTail.next = p; + loTail = p; + ++lc; + } + else { + if ((p.prev = hiTail) == null) + hi = p; + else + hiTail.next = p; + hiTail = p; + ++hc; + } + } +- // 如果拆分后的树的节点数量已经少于6个就需要重新转化为链表 + ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : + (hc != 0) ? new TreeBin(lo) : t; + hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : + (lc != 0) ? new TreeBin(hi) : t; +- // CAS存储在nextTable的i位置上 + setTabAt(nextTab, i, ln); +- // CAS存储在nextTable的i+n位置上 + setTabAt(nextTab, i + n, hn); +- // CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕 +- setTabAt(tab, i, fwd); + advance = true; + } + } + } + } + } +} + +#### helpTransfer + - final Node[] helpTransfer(Node[] tab, Node f) { + Node[] nextTab; int sc; + if (tab != null && (f instanceof ForwardingNode) && + (nextTab = ((ForwardingNode)f).nextTable) != null) { + int rs = resizeStamp(tab.length); + while (nextTab == nextTable && table == tab && + (sc = sizeCtl) < 0) { + if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || + sc == rs + MAX_RESIZERS || transferIndex <= 0) + break; + if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { +- // 调用扩容方法 + transfer(tab, nextTab); + break; + } + } + return nextTab; + } + return table; +} +### putIfAbsent + + +``` +public V putIfAbsent(K key, V value) { + return putVal(key, value, true); +} +``` + +### get(无锁) + +``` +public V get(Object key) { + Node[] tab; Node e, p; int n, eh; K ek; + int h = spread(key.hashCode()); + if ((tab = table) != null && (n = tab.length) > 0 && + (e = tabAt(tab, (n - 1) & h)) != null) { + if ((eh = e.hash) == h) { +``` + +- // bucket中第一个结点就是我们要找的,直接返回 + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + return e.val; + } + else if (eh < 0) +- // bucket中第一个结点是红黑树根,则调用find方法去找 + return (p = e.find(h, key)) != null ? p.val : null; +- // bucket中第一个结点是链表,则遍历链表查找 + while ((e = e.next) != null) { + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; +} +- + +### untreeify(无锁) +- static Node untreeify(Node b) { + Node hd = null, tl = null; + for (Node q = b; q != null; q = q.next) { + Node p = new Node(q.hash, q.key, q.val, null); + if (tl == null) + hd = p; + else + tl.next = p; + tl = p; + } + return hd; +} + +### remove(有锁) + +``` +public boolean remove(Object key, Object value) { + if (key == null) + throw new NullPointerException(); + return value != null && replaceNode(key, null, value) != null; +} +``` + + + +``` +/** + * Implementation for the four public remove/replace methods: + * Replaces node value with v, conditional upon match of cv if + * non-null. If resulting value is null, delete. + */ +final V replaceNode(Object key, V value, Object cv) { + int hash = spread(key.hashCode()); + for (Node[] tab = table;;) { + Node f; int n, i, fh; + if (tab == null || (n = tab.length) == 0 || + (f = tabAt(tab, i = (n - 1) & hash)) == null) + break; + else if ((fh = f.hash) == MOVED) +``` + +- // 如果已经被移动,那么就帮助进行扩容 + tab = helpTransfer(tab, f); + else { + V oldVal = null; + boolean validated = false; + synchronized (f) { + if (tabAt(tab, i) == f) { +- // 如果是链表,则删除链表中的节点 + if (fh >= 0) { + validated = true; + for (Node e = f, pred = null;;) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + V ev = e.val; + if (cv == null || cv == ev || + (ev != null && cv.equals(ev))) { + oldVal = ev; + if (value != null) + e.val = value; + else if (pred != null) + pred.next = e.next; + else + setTabAt(tab, i, e.next); + } + break; + } + pred = e; + if ((e = e.next) == null) + break; + } + } + - // 如果是红黑树,则从红黑树中删除结点 + else if (f instanceof TreeBin) { + validated = true; + TreeBin t = (TreeBin)f; + TreeNode r, p; + if ((r = t.root) != null && + (p = r.findTreeNode(hash, key, null)) != null) { + V pv = p.val; + if (cv == null || cv == pv || + (pv != null && cv.equals(pv))) { + oldVal = pv; + if (value != null) + p.val = value; + else if (t.removeTreeNode(p)) + setTabAt(tab, i, untreeify(t.first)); + } + } + } + } + } + if (validated) { + if (oldVal != null) { + if (value == null) + addCount(-1L, -1); + return oldVal; + } + break; + } + } + } + return null; +} + +### containsKey + +``` +public boolean containsKey(Object key) { + return get(key) != null; +} +``` + + +### containsValue + +``` +public boolean containsValue(Object value) { + if (value == null) + throw new NullPointerException(); + Node[] t; + if ((t = table) != null) { + Traverser it = new Traverser(t, t.length, 0, t.length); + for (Node p; (p = it.advance()) != null; ) { + V v; + if ((v = p.val) == value || (v != null && value.equals(v))) + return true; + } + } + return false; +} +``` + + + +``` +static class Traverser { + Node[] tab; // current table; updated if resized + Node next; // the next entry to use + TableStack stack, spare; // to save/restore on ForwardingNodes + int index; // index of bin to use next + int baseIndex; // current index of initial table + int baseLimit; // index bound for initial table + final int baseSize; // initial table size + + Traverser(Node[] tab, int size, int index, int limit) { + this.tab = tab; + this.baseSize = size; + this.baseIndex = this.index = index; + this.baseLimit = limit; + this.next = null; + } + + /** + * Advances if possible, returning next valid node, or null if none. + */ + final Node advance() { + Node e; + if ((e = next) != null) + e = e.next; + for (;;) { + Node[] t; int i, n; // must use locals in checks + if (e != null) + return next = e; + if (baseIndex >= baseLimit || (t = tab) == null || + (n = t.length) <= (i = index) || i < 0) + return next = null; + if ((e = tabAt(t, i)) != null && e.hash < 0) { + if (e instanceof ForwardingNode) { + tab = ((ForwardingNode)e).nextTable; + e = null; + pushState(t, i, n); + continue; + } + else if (e instanceof TreeBin) + e = ((TreeBin)e).first; + else + e = null; + } + if (stack != null) + recoverState(n); + else if ((index = i + baseSize) >= n) + index = ++baseIndex; // visit upper slots if present + } + } + + /** + * Saves traversal state upon encountering a forwarding node. + */ + private void pushState(Node[] t, int i, int n) { + TableStack s = spare; // reuse if possible + if (s != null) + spare = s.next; + else + s = new TableStack(); + s.tab = t; + s.length = n; + s.index = i; + s.next = stack; + stack = s; + } + + /** + * Possibly pops traversal state. + * + * @param n length of current table + */ + private void recoverState(int n) { + TableStack s; int len; + while ((s = stack) != null && (index += (len = s.length)) >= n) { + n = len; + index = s.index; + tab = s.tab; + s.tab = null; + TableStack next = s.next; + s.next = spare; // save for reuse + stack = next; + spare = s; + } + if (s == null && (index += baseSize) >= n) + index = ++baseIndex; + } +} +``` + + +### 遍历 + +``` +public Set> entrySet() { + EntrySetView es; + return (es = entrySet) != null ? es : (entrySet = new EntrySetView(this)); +} +``` + + + +``` +public Iterator> iterator() { + ConcurrentHashMap m = map; + Node[] t; + int f = (t = m.table) == null ? 0 : t.length; + return new EntryIterator(t, f, 0, f, m); +} +``` + + + + +``` +static class BaseIterator extends Traverser { + final ConcurrentHashMap map; + Node lastReturned; + BaseIterator(Node[] tab, int size, int index, int limit, + ConcurrentHashMap map) { + super(tab, size, index, limit); + this.map = map; + advance(); + } + + public final boolean hasNext() { return next != null; } + public final boolean hasMoreElements() { return next != null; } + + public final void remove() { + Node p; + if ((p = lastReturned) == null) + throw new IllegalStateException(); + lastReturned = null; + map.replaceNode(p.key, null, null); + } +} +``` + + + + +``` +static final class EntryIterator extends BaseIterator + implements Iterator> { + EntryIterator(Node[] tab, int index, int size, int limit, + ConcurrentHashMap map) { + super(tab, index, size, limit, map); + } + + public final Map.Entry next() { + Node p; + if ((p = next) == null) + throw new NoSuchElementException(); + K k = p.key; + V v = p.val; + lastReturned = p; + advance(); + return new MapEntry(k, v, map); + } +} +``` + + +- + +### 1.7 分段锁实现 +- 采用 Segment + HashEntry的方式进行实现 + +#### put +- 当执行 put方法插入数据时,根据key的hash值,在 Segment数组中找到相应的位置,如果相应位置的 Segment还未初始化,则通过CAS进行赋值,接着执行 Segment对象的 put方法通过加锁机制插入数据,实现如下: + +- 场景:线程A和线程B同时执行相同 Segment对象的 put方法 + +- 1、线程A执行 tryLock()方法成功获取锁,则把 HashEntry对象插入到相应的位置; + +- 2、线程B获取锁失败,则执行 scanAndLockForPut()方法,在 scanAndLockForPut方法中,会通过重复执行 tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行 tryLock()方法的次数超过上限时,则执行 lock()方法挂起线程B; + +- 3、当线程A执行完插入操作时,会通过 unlock()方法释放锁,接着唤醒线程B继续执行; +#### size +- 因为 ConcurrentHashMap是可以并发插入数据的,所以在准确计算元素时存在一定的难度,一般的思路是统计每个 Segment对象中的元素个数,然后进行累加,但是这种方式计算出来的结果并不一样的准确的,因为在计算后面几个 Segment的元素个数时,已经计算过的 Segment同时可能有数据的插入或则删除。 +- 先采用不加锁的方式,连续计算元素的个数,最多计算3次: 1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的; 2、如果前后两次计算结果都不同,则给每个 Segment进行加锁,再计算一次元素的个数; + +- + +### ConcurrentSkipListMap +- ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点: +-   1、ConcurrentSkipListMap 的key是有序的。 +- 2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。 + +- SkipList 跳表: +- 跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。 +- 下面来研究一下跳表的核心思想: +- 先从链表开始,如果是一个简单的链表,那么我们知道在链表中查找一个元素I的话,需要将整个链表遍历一次。 +-   +-  如果是说链表是排序的,并且节点中还存储了指向前面第二个节点的指针的话,那么在查找一个节点时,仅仅需要遍历N/2个节点即可。 + +- 这基本上就是跳表的核心思想,其实也是一种通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。 + +- + +## Map实现类之间的区别 +### HashMap与ConcurrentHashMap区别 + - 1)前者允许key或value为null,后者不允许 + - 2)前者不是线程安全的,后者是 +### HashMap、TreeMap与LinkedHashMap区别 + - 1)HashMap遍历时,取得数据的顺序是完全随机的; +- TreeMap可以按照自然顺序或Comparator排序; + - LinkedHashMap可以按照插入顺序或访问顺序排序,且get的效率(O(1))比TreeMap(O(logn))更高。 + - 2)HashMap底层基于哈希表,数组+链表/红黑树; +- TreeMap底层基于红黑树 +- LinkedHashMap底层基于HashMap与环形双向链表 + - 3)就get和put效率而言,HashMap是最高的,LinkedHashMap次之,TreeMap最次。 +### HashMap与Hashtable区别 + - 1. 扩容策略:Hashtable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂(*2+1),而HashMap则要求一定为2的整数次幂(*2)。 +- 2. 允许null:Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。 +- 3. 线程安全:前者不是线程安全的,后者是; +### ConcurrentHashMap、Collections.synchronizedMap与Hashtable的异同 +- 它们都是同步Map,但三者实现同步的机制不同;后两者都是简单地在方法上加synchronized实现的,锁的粒度较大;前者是基于CAS和synchronized实现的,锁的粒度较小,大部分都是lock-free无锁实现同步的。 +- ConcurrentHashMap还提供了putIfAbsent同步方法。 + +# 3.6 Collections +## 同步集合包装 + + +``` +public static Map synchronizedMap(Map m) { + return new SynchronizedMap<>(m); +} +``` + + + +``` +private static class SynchronizedMap + implements Map, Serializable { + private static final long serialVersionUID = 1978198479659022715L; + + private final Map m; // Backing Map + final Object mutex; // Object on which to synchronize + + SynchronizedMap(Map m) { + this.m = Objects.requireNonNull(m); + mutex = this; + } + + SynchronizedMap(Map m, Object mutex) { + this.m = m; + this.mutex = mutex; + } + + public int size() { + synchronized (mutex) {return m.size();} + } + public boolean isEmpty() { + synchronized (mutex) {return m.isEmpty();} + } + public boolean containsKey(Object key) { + synchronized (mutex) {return m.containsKey(key);} + } + public boolean containsValue(Object value) { + synchronized (mutex) {return m.containsValue(value);} + } + public V get(Object key) { + synchronized (mutex) {return m.get(key);} + } + + public V put(K key, V value) { + synchronized (mutex) {return m.put(key, value);} + } + public V remove(Object key) { + synchronized (mutex) {return m.remove(key);} + } + public void putAll(Map map) { + synchronized (mutex) {m.putAll(map);} + } + public void clear() { + synchronized (mutex) {m.clear();} + } + + private transient Set keySet; + private transient Set> entrySet; + private transient Collection values; + + public Set keySet() { + synchronized (mutex) { + if (keySet==null) + keySet = new SynchronizedSet<>(m.keySet(), mutex); + return keySet; + } + } + + public Set> entrySet() { + synchronized (mutex) { + if (entrySet==null) + entrySet = new SynchronizedSet<>(m.entrySet(), mutex); + return entrySet; + } + } + + public Collection values() { + synchronized (mutex) { + if (values==null) + values = new SynchronizedCollection<>(m.values(), mutex); + return values; + } + } + + public boolean equals(Object o) { + if (this == o) + return true; + synchronized (mutex) {return m.equals(o);} + } + public int hashCode() { + synchronized (mutex) {return m.hashCode();} + } + public String toString() { + synchronized (mutex) {return m.toString();} + } + + // Override default methods in Map + @Override + public V getOrDefault(Object k, V defaultValue) { + synchronized (mutex) {return m.getOrDefault(k, defaultValue);} + } + @Override + public void forEach(BiConsumer action) { + synchronized (mutex) {m.forEach(action);} + } + @Override + public void replaceAll(BiFunction function) { + synchronized (mutex) {m.replaceAll(function);} + } + @Override + public V putIfAbsent(K key, V value) { + synchronized (mutex) {return m.putIfAbsent(key, value);} + } + @Override + public boolean remove(Object key, Object value) { + synchronized (mutex) {return m.remove(key, value);} + } + @Override + public boolean replace(K key, V oldValue, V newValue) { + synchronized (mutex) {return m.replace(key, oldValue, newValue);} + } + @Override + public V replace(K key, V value) { + synchronized (mutex) {return m.replace(key, value);} + } + @Override + public V computeIfAbsent(K key, + Function mappingFunction) { + synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);} + } + @Override + public V computeIfPresent(K key, + BiFunction remappingFunction) { + synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);} + } + @Override + public V compute(K key, + BiFunction remappingFunction) { + synchronized (mutex) {return m.compute(key, remappingFunction);} + } + @Override + public V merge(K key, V value, + BiFunction remappingFunction) { + synchronized (mutex) {return m.merge(key, value, remappingFunction);} + } + + private void writeObject(ObjectOutputStream s) throws IOException { + synchronized (mutex) {s.defaultWriteObject();} + } +} +``` + + +## 不可变集合包装 + +``` +public static Map unmodifiableMap(Map m) { + return new UnmodifiableMap<>(m); +} +``` + + + +``` +private static class UnmodifiableMap implements Map, Serializable { + private static final long serialVersionUID = -1034234728574286014L; + + private final Map m; + + UnmodifiableMap(Map m) { + if (m==null) + throw new NullPointerException(); + this.m = m; + } + + public int size() {return m.size();} + public boolean isEmpty() {return m.isEmpty();} + public boolean containsKey(Object key) {return m.containsKey(key);} + public boolean containsValue(Object val) {return m.containsValue(val);} + public V get(Object key) {return m.get(key);} + + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + public void clear() { + throw new UnsupportedOperationException(); + } + + private transient Set keySet; + private transient Set> entrySet; + private transient Collection values; + + public Set keySet() { + if (keySet==null) + keySet = unmodifiableSet(m.keySet()); + return keySet; + } + + public Set> entrySet() { + if (entrySet==null) + entrySet = new UnmodifiableEntrySet<>(m.entrySet()); + return entrySet; + } + + public Collection values() { + if (values==null) + values = unmodifiableCollection(m.values()); + return values; + } + + public boolean equals(Object o) {return o == this || m.equals(o);} + public int hashCode() {return m.hashCode();} + public String toString() {return m.toString();} + + // Override default methods in Map + @Override + @SuppressWarnings("unchecked") + public V getOrDefault(Object k, V defaultValue) { + // Safe cast as we don't change the value + return ((Map)m).getOrDefault(k, defaultValue); + } + + @Override + public void forEach(BiConsumer action) { + m.forEach(action); + } + + @Override + public void replaceAll(BiFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfPresent(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V compute(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V merge(K key, V value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } +``` + + +## 空集合包装 + +``` +public static final Map emptyMap() { + return (Map) EMPTY_MAP; +} +``` + + + +``` +public static final Map EMPTY_MAP = new EmptyMap<>(); +``` + + + +``` +private static class EmptyMap + extends AbstractMap + implements Serializable +{ + private static final long serialVersionUID = 6428348081105594320L; + + public int size() {return 0;} + public boolean isEmpty() {return true;} + public boolean containsKey(Object key) {return false;} + public boolean containsValue(Object value) {return false;} + public V get(Object key) {return null;} + public Set keySet() {return emptySet();} + public Collection values() {return emptySet();} + public Set> entrySet() {return emptySet();} + + public boolean equals(Object o) { + return (o instanceof Map) && ((Map)o).isEmpty(); + } + + public int hashCode() {return 0;} + + // Override default methods in Map + @Override + @SuppressWarnings("unchecked") + public V getOrDefault(Object k, V defaultValue) { + return defaultValue; + } + + @Override + public void forEach(BiConsumer action) { + Objects.requireNonNull(action); + } + + @Override + public void replaceAll(BiFunction function) { + Objects.requireNonNull(function); + } + + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfAbsent(K key, + Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V computeIfPresent(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V compute(K key, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public V merge(K key, V value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + // Preserves singleton property + private Object readResolve() { + return EMPTY_MAP; + } +} +``` + + +## Collections.sort + +``` +public static > void sort(List list) { + list.sort(null); +} +``` + +- List#sort +- default void sort(Comparator c) { + Object[] a = this.toArray(); + Arrays.sort(a, (Comparator) c); + ListIterator i = this.listIterator(); + for (Object e : a) { + i.next(); + i.set((E) e); + } +} + + +``` +public static void sort(T[] a, Comparator c) { + if (c == null) { + sort(a); + } else { + if (LegacyMergeSort.userRequested) + legacyMergeSort(a, c); + else + TimSort.sort(a, 0, a.length, c, null, 0, 0); + } +} +``` + + + +``` +public static void sort(Object[] a) { + if (LegacyMergeSort.userRequested) + legacyMergeSort(a); + else + ComparableTimSort.sort(a, 0, a.length, null, 0, 0); +} +``` + +### 1.7(TimSort) + +- 结合了归并排序和插入排序的混合算法,它基于一个简单的事实,实际中大部分数据都是部分有序(升序或降序)的。 +- TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。 +- 综上述过程,Timsort算法的过程包括 +- (0)如果数组长度小于某个值,直接用二分插入排序算法 + - (1)找到各个run,并入栈 + - (2)按规则合并run + + +``` +/** + * Sorts the given range, using the given workspace array slice + * for temp storage when possible. This method is designed to be + * invoked from public methods (in class Arrays) after performing + * any necessary array bounds checks and expanding parameters into + * the required forms. + * + * @param a the array to be sorted + * @param lo the index of the first element, inclusive, to be sorted + * @param hi the index of the last element, exclusive, to be sorted + * @param work a workspace array (slice) + * @param workBase origin of usable space in work array + * @param workLen usable size of work array + * @since 1.8 + */ +static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) { + assert a != null && lo >= 0 && lo <= hi && hi <= a.length; + + int nRemaining = hi - lo; + if (nRemaining < 2) + return; // Arrays of size 0 and 1 are always sorted + + // If array is small, do a "mini-TimSort" with no merges + if (nRemaining < MIN_MERGE) { + int initRunLen = countRunAndMakeAscending(a, lo, hi); + binarySort(a, lo, hi, lo + initRunLen); + return; + } + + /** + * March over the array once, left to right, finding natural runs, + * extending short natural runs to minRun elements, and merging runs + * to maintain stack invariant. + */ + ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen); + int minRun = minRunLength(nRemaining); + do { + // Identify next run + int runLen = countRunAndMakeAscending(a, lo, hi); + + // If run is short, extend to min(minRun, nRemaining) + if (runLen < minRun) { + int force = nRemaining <= minRun ? nRemaining : minRun; + binarySort(a, lo, lo + force, lo + runLen); + runLen = force; + } + + // Push run onto pending-run stack, and maybe merge + ts.pushRun(lo, runLen); + ts.mergeCollapse(); + + // Advance to find next run + lo += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + // Merge all remaining runs to complete sort + assert lo == hi; + ts.mergeForceCollapse(); + assert ts.stackSize == 1; +} +``` + + + +``` +/** + * Creates a TimSort instance to maintain the state of an ongoing sort. + * + * @param a the array to be sorted + * @param work a workspace array (slice) + * @param workBase origin of usable space in work array + * @param workLen usable size of work array + */ +private ComparableTimSort(Object[] a, Object[] work, int workBase, int workLen) { + this.a = a; + + // Allocate temp storage (which may be increased later if necessary) + int len = a.length; + int tlen = (len < 2 * INITIAL_TMP_STORAGE_LENGTH) ? + len >>> 1 : INITIAL_TMP_STORAGE_LENGTH; + if (work == null || workLen < tlen || workBase + tlen > work.length) { + tmp = new Object[tlen]; + tmpBase = 0; + tmpLen = tlen; + } + else { + tmp = work; + tmpBase = workBase; + tmpLen = workLen; + } + + /* + * Allocate runs-to-be-merged stack (which cannot be expanded). The + * stack length requirements are described in listsort.txt. The C + * version always uses the same stack length (85), but this was + * measured to be too expensive when sorting "mid-sized" arrays (e.g., + * 100 elements) in Java. Therefore, we use smaller (but sufficiently + * large) stack lengths for smaller arrays. The "magic numbers" in the + * computation below must be changed if MIN_MERGE is decreased. See + * the MIN_MERGE declaration above for more information. + * The maximum value of 49 allows for an array up to length + * Integer.MAX_VALUE-4, if array is filled by the worst case stack size + * increasing scenario. More explanations are given in section 4 of: + * http://envisage-project.eu/wp-content/uploads/2015/02/sorting.pdf + */ + int stackLen = (len < 120 ? 5 : + len < 1542 ? 10 : + len < 119151 ? 24 : 49); + runBase = new int[stackLen]; + runLen = new int[stackLen]; +} +``` + + +### 1.6 (MergeSort) + +``` +private static void legacyMergeSort(Object[] a) { + Object[] aux = a.clone(); + mergeSort(aux, a, 0, a.length, 0); +} +``` + + + +``` +private static void mergeSort(Object[] src, + Object[] dest, + int low, + int high, + int off) { + int length = high - low; + // 7 + // Insertion sort on smallest arrays + if (length < INSERTIONSORT_THRESHOLD) { + for (int i=low; ilow && + ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--) + swap(dest, j, j-1); + return; + } + + // Recursively sort halves of dest into src + int destLow = low; + int destHigh = high; + low += off; + high += off; + int mid = (low + high) >>> 1; + mergeSort(dest, src, low, mid, -off); + mergeSort(dest, src, mid, high, -off); + + // If list is already sorted, just copy from src to dest. This is an + // optimization that results in faster sorts for nearly ordered lists. + if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) { + System.arraycopy(src, low, dest, destLow, length); + return; + } + + // Merge sorted halves (now in src) into dest + for(int i = destLow, p = low, q = mid; i < destHigh; i++) { + if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0) + dest[i] = src[p++]; + else + dest[i] = src[q++]; + } +} +``` + + + +# 3.7 Fail-Fast +- 在ArrayList,LinkedList,HashMap等等的内部实现增,删,改中我们总能看到modCount的身影,modCount字面意思就是修改次数,但为什么要记录modCount的修改次数呢? +- 所有使用modCount属性集合的都是线程不安全的。 +- 在一个迭代器初始的时候会赋予它调用这个迭代器的对象的modCount,在迭代器遍历的过程中,一旦发现这个对象的modCount和迭代器中存储的modCount不一样那就抛异常。 + +- 它是 java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast。 + + - 例如 :假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。 + +- 原因: 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。 + +- 每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。 + +- 解决办法:使用线程安全的集合 +- + +- + +- + +- + From 624ba0a14212c7d857c4f0a7d0edc120919735a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:07:49 +0800 Subject: [PATCH 58/97] =?UTF-8?q?Rename=20=E4=BA=8C=20=E3=80=81=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=A8=A1=E5=BC=8F.md=20to=20=E4=BA=8C=E3=80=81?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...43\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" => "docs/\344\272\214\343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" (100%) diff --git "a/docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/\344\272\214\343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 100% rename from "docs/\344\272\214 \343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "docs/\344\272\214\343\200\201\350\256\276\350\256\241\346\250\241\345\274\217.md" From bd01b39128c8bf14a88a7069375b140b57abe08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:08:14 +0800 Subject: [PATCH 59/97] =?UTF-8?q?Update=20Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/Java\345\271\266\345\217\221.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/Java\345\271\266\345\217\221.md" index 6772c6b3..96f9b4bc 100644 --- "a/docs/Java\345\271\266\345\217\221.md" +++ "b/docs/Java\345\271\266\345\217\221.md" @@ -1,5 +1,5 @@ -# 一. 并发框架 +# 四. 并发框架 ## Doug Lea 如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。 说他是这个世界上对Java影响力最大的个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。 From 8a368e6b512a20f578b79fe2deeea70c5c98ffc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:08:32 +0800 Subject: [PATCH 60/97] =?UTF-8?q?Rename=20Java=E5=B9=B6=E5=8F=91.md=20to?= =?UTF-8?q?=20=E5=9B=9B=E3=80=81Java=E5=B9=B6=E5=8F=91.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\345\233\233\343\200\201Java\345\271\266\345\217\221.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/Java\345\271\266\345\217\221.md" => "docs/\345\233\233\343\200\201Java\345\271\266\345\217\221.md" (100%) diff --git "a/docs/Java\345\271\266\345\217\221.md" "b/docs/\345\233\233\343\200\201Java\345\271\266\345\217\221.md" similarity index 100% rename from "docs/Java\345\271\266\345\217\221.md" rename to "docs/\345\233\233\343\200\201Java\345\271\266\345\217\221.md" From 5d12ed89ce9da0796db56eadbbbf81e106055705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:39:10 +0800 Subject: [PATCH 61/97] =?UTF-8?q?Create=20=E4=BA=94=E3=80=81JVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/\344\272\224\343\200\201JVM" | 1204 ++++++++++++++++++++++++++++ 1 file changed, 1204 insertions(+) create mode 100644 "docs/\344\272\224\343\200\201JVM" diff --git "a/docs/\344\272\224\343\200\201JVM" "b/docs/\344\272\224\343\200\201JVM" new file mode 100644 index 00000000..7c3a057a --- /dev/null +++ "b/docs/\344\272\224\343\200\201JVM" @@ -0,0 +1,1204 @@ +# JVM +# 运行时数据区 +- Java运行时数据区有 +- 堆 ,本地方法栈,虚拟机栈,程序计数器,方法区(运行时常量池,属性和方法数据,代码区) + + +- Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存将会包括以下集合运行时数据区域: + +# 5.1 程序计数器(线程独享) +- 程序计数器(Program Counter Register)是一块很小的内存区域,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 +- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。 +- 如果线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法, 这个计数器的值为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 +# 5.2 虚拟机栈(线程独享) +- Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 +# 5.3 本地方法栈(线程独享) +- 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。 +# 5.4 Java堆 +- 对大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。 +- Java堆是垃圾收集器管理的主要区域。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。 +- Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。 +# 5.5 方法区 +- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。 +- “PermGen space”是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”。 +- HotSpot虚拟机将GC分代收集拓展至方法区,或者说使用永久代来实现方法区。这样的HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。如果实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但是用永久代实现方法区,并不是一个好主意,因为这样容易遇到内存溢出问题。 +- 垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区就永久存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。 +- 在Java8中,永久代被删除,方法区的HotSpot的实现为Metaspace元数据区,不放在虚拟机中而放在本地内存中,存储类的元信息; +- 而将类的静态变量(放在Class对象中)和运行时常量池放在堆中。 + +- 为什么? + - 1)移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。 + + - 2)现实使用中易出问题 +- 由于永久代内存经常不够用或发生内存泄露,出现异常java.lang.OutOfMemoryError: PermGen +## 运行时常量池 +- 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有关的描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池。 +- 运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如String类的intern方法。 +# 5.6 直接内存 +- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁使用。JDK的NIO类,引入了一种基于通道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合显著提高性能,避免了在Java堆和Native堆来回复制数据。 +- 直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存的限制。 +# 对象 +# 5.7 对象的创建 +- 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那就执行类加载过程。 +- 在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一块与对象大小相等的距离,这种分配方式称为指针碰撞。如果Java堆中的内存不是规整的,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 +- 除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两个方案,一种是对分配内存空间的动作进行同步处理,另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲TLAB。哪个线程分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。 +- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何找到类的元数据等信息。这些信息存放在对象的对象头之中。上述工作完成后,从虚拟机的视角来看,一个新的对象已经产生,但从Java程序的视角来看,构造方法还没有执行,字段都还为0。所以执行new指令之后会接着执行构造方法等,这样一个对象才算真正产生出来。 +# 5.8 对象的内存布局 +- 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3个区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 +- 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 +- 实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承的,还是在子类中定义的,都需要记录下来。相同宽度的字段总是被分配到一起,在这个前提下,在父类中定义的变量会出现在子类之前。 +- 对齐填充并不是必然存在的,它仅仅起着占位符的作用,HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍,而对象头正好是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要对齐填充来补全。 +# 5.9 对象的访问定位 +- Java程序需要通过栈上的Reference数据来操作堆上的具体对象。由于Reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式来定位、访问堆中对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目标主流的方式有使用句柄和直接指针两种。 +- 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,Reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。 + +- 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而Reference中存储的直接就是对象地址。 + +- 使用句柄来访问的最大好处就是Reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而Reference本身不需要修改。 +- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。 + +# 内存溢出与内存泄露 +- OOM ;方法区OOM时的异常;查看dump 文件,怎么查看,具体命令记得吗,答jstack 具体怎么用的 +# 5.10 堆溢出 +- Java堆用于存储对象实例,只要不断增加对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生OOM异常。 + +``` +public class HeapOOM { + static class OOMObject{ + + } + public static void main(String[] args) { + List list = new ArrayList<>(); + while(true){ + list.add(new OOMObject()); + } + } +} +``` + +- VM Options: +- -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError + +- java.lang.OutOfMemoryError: Java heap space +- Dumping heap to java_pid15080.hprof ... +- Heap dump file created [28193498 bytes in 0.125 secs] +- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space +- at java.util.Arrays.copyOf(Arrays.java:3210) + - at java.util.Arrays.copyOf(Arrays.java:3181) + - at java.util.ArrayList.grow(ArrayList.java:261) + - at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) + - at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) + - at java.util.ArrayList.add(ArrayList.java:458) + - at cn.sinjinsong.se.review.oom.HeapOOM.main(HeapOOM.java:17) + +- 要解决这个区域的异常,一般的手段是通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要判断是出现来内存泄露还是内存溢出。前者的话要进一步通过工具查看泄露对象到GC Roots的引用链;后者的话可以调大虚拟机的堆参数(-Xms和-Xmx),或者从代码上检查某些对象生命周期过长等。 + +- + +# 5.11 栈溢出(虚拟机栈和本地方法栈) +- 对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在JVM规范中描述了两种异常: + - 1) 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 + - 2)如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。 + + +``` +public class StackSOF { + private int stackLength = -1; + public void stackLeak() { + stackLength++; + stackLeak(); + } + + public static void main(String[] args) { + StackSOF sof = new StackSOF(); + try { + sof.stackLeak(); + } catch (Throwable e) { + System.out.println("stack length:" + sof.stackLength); + throw e; + } + } +} +``` + + +- -Xss128k(设置栈容量) + +- stack length:998 +- Exception in thread "main" java.lang.StackOverflowError +- at cn.sinjinsong.se.review.oom.StackSOF.stackLeak(StackSOF.java:10) + - at cn.sinjinsong.se.review.oom.StackSOF.stackLeak(StackSOF.java:11) +- ... + +- 操作系统分配给每个进程的内存是有限制的,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。 +- 如果线程过多导致SOF,可以通过减少最大堆和减少栈容量来换取更多的线程。 + +``` +public class StackSOFByThread { + public void stackLeakByThread() { + while(true) { + new Thread(() -> { + while (true){} + }).start(); + } + } + + public static void main(String[] args) { + new StackSOFByThread().stackLeakByThread(); + } +} +``` + + +- + +# 5.12 方法区溢出 +- 注意Java8下运行时常量池在堆中,所以运行时常量池过大会体现为OOM:heap; +- 而在此以前是放在永久代中,体现为OOM:PermGen space。 + +``` +public class RuntimeConstantPoolOOM { + public static void main(String[] args) { + List list = new ArrayList<>(); + int i = 0; + while (true) { + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +- VM Options: -Xms20m -Xmx20m +- +Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded + - at java.lang.Integer.toString(Integer.java:401) + - at java.lang.String.valueOf(String.java:3099) + - at cn.sinjinsong.se.review.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:15) + +- 方法区还存放Class的相关信息,运行时产生大量的类也会导致方法区(Java8中放在直接内存中)溢出。 + +``` +public class MetaspaceOOM { + public static void main(String[] args) { + while(true){ + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(HeapOOM.OOMObject.class); + enhancer.setUseCache(false); + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return proxy.invokeSuper(obj,args); + } + }); + enhancer.create(); + } + } +} +``` + +- VM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m +- Caused by: java.lang.OutOfMemoryError: Metaspace +- at java.lang.ClassLoader.defineClass1(Native Method) + - at java.lang.ClassLoader.defineClass(ClassLoader.java:763) +- ... 11 more + +- 方法区溢出也是一种常见的内存溢出异常,一个类被GC,判定条件是比较苛刻的。在经常生成大量Class的应用中,需要特别注意类的回收情况。这类场景除了动态代理生成类和动态语言外,还有:大量使用JSP、基于OSGi的应用。 + +# 5.13 直接内存溢出 +- 直接内存可以使用-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值相同。 +- 虽然使用DirectByteBuffer分配内存也会抛出OOM异常,但它抛出异常时并没有真正向OS申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常。 +- 真正申请内存的方法是unsafe.allocateMemory()。 + +``` +public class DirectMemoryOOM { + private static final int _1MB = 1024 * 1024; + + public static void main(String[] args) throws IllegalAccessException { + Field unsafeField = Unsafe.class.getDeclaredFields()[0]; + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + while(true) { + unsafe.allocateMemory(_1MB); + } + } +} +``` + +- VM Options: -XX:MaxDirectMemorySize=10m + +- Exception in thread "main" java.lang.OutOfMemoryError +- at sun.misc.Unsafe.allocateMemory(Native Method) + - at cn.sinjinsong.se.review.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:19) +- + +# 5.14 内存泄露 + - 1)非静态内部类 + - 2)连接未关闭:比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。 + +- + +# GC +# 5.15 对象是否存活 +## 引用计数算法 +- 很多教科书判断对象是否存活的算法是这样的:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计算器为0的对象就是不可能再被使用的。 +- 主流的Java虚拟机中没有选用计数算法来管理内存,最主要的原因是它很难就解决对象之间相互循环引用的问题。 + +## 可达性分析算法 +- 主流的商用程序语言的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达的。下图章,对象object5、object6、object7虽然互相有关联,但是它们到GC Roots时不可达的,所以它们将会被判定为可回收的对象。 + +- 在Java中,可作为GC Roots的对象包括: +- 虚拟机栈中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中常量引用的对象 +- 本地方法栈中JNI(一般说的Native方法)引用的对象 +## finalize +- 即使在可达性分析中不可达的对象,也并非是非死不可。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()已经被虚拟机调用过,虚拟机将这两种情况视为没有必要执行。 +- 如果这个对象被判为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,\如果对象要在finalize()中拯救自己,只要重新与引用链上的任何一个对象建立联系即可,比如把自己this复制给某个类变量或对象的成员变量,那在第二次标记时它将被移出即将回收的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。任何一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。 +## 回收方法区 +- 在方法区(永久代)中进行垃圾收集的性价比较低:在堆中,尤其在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。 +- 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量和回收Java堆中的对象类似。以常量池中字面量的回收为例,没有任何String对象引用常量池中的某个字符串常量,这个常量就会被系统清理出常量池。常量池中的其他类、方法、字段的符号引用也与此类似。 +- 判定一个类是否是无用的类的条件比较苛刻,需要同时满足以下三个条件: + - 1)该类的所有实例都已经被回收 + - 2)加载该类的类加载器已经被回收 + - 3)该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 +- 虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是可以,而不是和对象一样,不适用了就必然会被回收。是否对类回收,HotSpot虚拟机提供了参数进行控制。 +- 在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。 + +- + +# 5.16 GC算法 +## 标记-清除算法 +- 最基础的收集算法是标记-清除算法(Mark-Sweep),算法分为标记和清除两个阶段。首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。他的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 +## 复制算法 +- 为了解决效率问题,出现了复制算法。它将可用内存按容量划分为大小相等的两块,每次只是用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存空间,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。 + +- 现在的商业虚拟机都采用这种收集算法来回收新生代。将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次都使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。 +## 标记-整理算法 +- 复制收集算法在对象存活率较高时,效率就会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,所以老年代一般不能直接选用这种算法。 +- 根据老年代的特点,有人提出一种标记-整理算法(Mark-Compact),标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 + +## 分代收集算法 +- 当前商业虚拟机的垃圾收集都采用分代收集算法(Generational Collection),这种算法是根据对象存活周期的不同将内存划分为适当的几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法来进行回收。 + +- + +# 5.17 Minor Full GC + +## Minor GC +- 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 +- 非常频繁,回收速度较快。 +- 各种Young GC的触发原因都是eden区满了。 +## Full GC +- 收集整个堆,包括年轻代、老年代、元数据区等所有部分。 +- 速度较慢。 +- 触发原因不确定,因具体垃圾收集器而异。 +- 比如老年代内存不足,ygc出现promotion failure,System.gc()等。 +- CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发收集时的使用。 + +# 5.18 HotSpot的垃圾收集器 +- Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能有很大差别,并且一般都会提供参数供用户根据自己得到应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于HotSpot虚拟机,这个虚拟机包含的所有收集器如图所示。 + +- + +## (1).Serial垃圾收集器 +- Serial是最基本、历史最悠久的垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。 +- Serial是一个单线程的收集器,它不仅仅只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 +- Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以 获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。 +## (2).ParNew垃圾收集器 +- ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。 +- ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。 +- ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。 +## (3).Parallel Scavenge收集器 +- Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量 (Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量 可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 +- Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量: +- a.-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于0的毫秒数。 + - b.-XX:GCTimeRation:直接设置吞吐量大小,是一个大于0小于100的整数,也就是程序运行时间占总时间的比率,默认值是99,即垃圾收集运行最大1%(1/(1+99))的垃圾收集时间。 +- Parallel Scavenge是吞吐量优先的垃圾收集器,它还提供一个参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,打开之后就不需 要手动指定新生代大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、新生代晋升年老代对象年龄 (-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以达到最大吞吐 量,这种方式称为GC自适应调节策略,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。 +## (4).Serial Old收集器 +- Serial Old是Serial垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。 +- 在Server模式下,主要有两个用途: +- a.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。 +- b.作为年老代中使用CMS收集器的后备垃圾收集方案。 +- 新生代Serial与年老代Serial Old搭配垃圾收集过程图: + +- 新生代Parallel Scavenge收集器与ParNew收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。 +- 新生代Parallel Scavenge/ParNew与年老代Serial Old搭配垃圾收集过程图: + +## (5).Parallel Old收集器 +- Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。 +- 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。 +- 新生代Parallel Scavenge和年老代Parallel Old收集器搭配运行过程图: + +## (6).CMS收集器(重点) +- Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 +- 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,CMS收集器是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作。 +- CMS工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段: +- a.初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 +- b.并发标记:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。 +- c.重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。 +- d.并发清除:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。 + +- 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。 +- CMS收集器工作过程: + +- CMS收集器有以下三个不足: + - a.CMS收集器对CPU资源非常敏感,其默认启动的收集线程数=(CPU数量+3)/4,在用户程序本来CPU负荷已经比较高的情况下,如果还要分出CPU资源用来运行垃圾收集器线程,会使得CPU负载加重。 +- b.CMS无法处理浮动垃圾(Floating Garbage),可能会导致Concurrent ModeFailure失败而导致另一次Full GC。由于CMS收集器和用户线程并发运行,因此在收集过程中不断有新的垃圾产生,这些垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好 等待下一次GC时再将其清理掉,这些垃圾就称为浮动垃圾。 +- CMS垃圾收集器不能像其他垃圾收集器那样等待年老代机会完全被填满之后再进行收集,需要预留一部分空间供并发收集时的使用,可以通过参数 +- -XX:CMSInitiatingOccupancyFraction来设置年老代空间达到多少的百分比时触发CMS进行垃圾收集,默认是68%。 +- 如果在CMS运行期间,预留的内存无法满足程序需要,就会出现一次ConcurrentMode Failure失败,此时虚拟机将启动预备方案,使用Serial Old收集器重新进行年老代垃圾回收。 +- c.CMS收集器是基于标记-清除算法,因此不可避免会产生大量不连续的内存碎片,如果无法找到一块足够大的连续内存存放对象时,将会触发因此 Full GC。CMS提供一个开关参数-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数-XX:CMSFullGCsBeforeCompaction, 用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。 + +- promotion failure 发生在 young gc 阶段,即 cms 的 ParNewGC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的; +- concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的 +## (7).G1收集器(重点) +- Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器,G1收集器两个最突出的改进是: +- a.基于标记-整理算法,不产生内存碎片。 +- b.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 +- G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。 +- 区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。 + +- + +# 5.19 内存分配原则 +- 对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,分配的规则不是固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。 +- 下面会讲解几条最普遍的内存分配原则。 +## 对象优先在Eden分配 +- 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。如果GC期间虚拟机发现已有的对象全部无法放入Survivor空间,会通过分配担保机制提前转移至老年代中。 + +## 大对象直接进入老年代 +- 所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来安置它们。 +## 长期存活的对象将进入老年代 +- 虚拟机为每个对象定义一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代。 +## 动态对象年龄判定 +- 虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。 +## 空间分配担保 +- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那此时也要改为进行一次Full GC。 +- 冒险是指当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor区无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共会有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之间每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。 +- 取平均值进行比较其实仍然是一种动态概率的手段,依然存在担保失败的情况。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。 + +- + +# 5.20 GC相关API +## System.gc +- 建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。 + +``` +public static void gc() { +``` + +-    Runtime.getRuntime().gc(); +- } +- Runtime.gc的底层实现位于Runtime.c文件中 +- JNIEXPORT void JNICALL +- Java_java_lang_Runtime_gc(JNIEnv *env, jobject this) +- { +-    JVM_GC(); +- } +- JVM_ENTRY_NO_ENV(void, JVM_GC(void)) +-  JVMWrapper("JVM_GC"); +-  if (!DisableExplicitGC) { +-    Universe::heap()->collect(GCCause::_java_lang_system_gc); +-  } +- JVM_END +- 这里有一个DisableExplicitGC参数,默认是false,如果启动JVM时添加了参数-XX:+DisableExplicitGC,那么JVM_GC相当于一个空函数,并不会进行GC。 + +- 其中Universe::heap()返回当前堆对象,由collect方法开始执行GC,并设置当前触发GC的条件为_java_lang_system_gc,内部会根据GC条件执行不同逻辑。 +- JVM的具体堆实现,在Universe.cpp文件中的initialize_heap()由启动参数所设置的垃圾回收算法决定。 +- 堆实现和回收算法对应关系: +- 1、UseParallelGC:ParallelScavengeHeap +- 2、UseG1GC:G1CollectedHeap +- 3、默认或者CMS:GenCollectedHeap + +- + +# 类文件结构 + +- Class类文件的结构 +- Class文件并不一定定义在文件里,也可以通过类加载器直接生成。 +- Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。 +- Class文件结构采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。 +- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值。 +- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据。 + +- 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。 +- 魔数与Class文件的版本 +- 每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。很多文件存储格式都使用魔数来进行身份识别。魔数的值为0xCAFEBABE。 +- 紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是此版本号,第7和第8个字节是主版本号。 +- 简单的一段Java代码,后面的内容将以此为例进行讲解: + +``` +public class TestClass { +``` + + +``` + private int m; +``` + + +``` + public int inc(){ +``` + +- return m+1; +- } +- } +- 常量池 +- 紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件中的资源仓库,它是Class文件结构中和其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。 +- 由于常量池中常量的数量是不固定的,所以在常量池入口需要放置一项u2类型的数据,代表常量池容量计数器(从1开始)。对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。 +- 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如字符串、final常量值。而符号引用则属于编译原理方面的概念,包括了下面三类常量: +- 类和接口的全限定名 +- 字段的名称和描述符 +- 方法的名称和描述符 +- Java代码在javac编译的时候,并没有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。 +- 常量池中每一项常量都是一个表,在JDK1.7之前有11种不同结构的表结构数据。1.7增加了3种。它们的共同特点是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。 + +- 访问标志 + +``` +在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。 +``` + + +- 访问标志中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位一律为0。 +- 类索引、父类索引与接口索引集合 +- 类索引和父类索引都是一个u2类型数据,而接口索引集合是一组u2类型数据的集合,Class文件中由这3项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements 语句后的接口顺序从左到右排列在接口索引集合中。 +- 类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。 + +- 对于接口类型集合,入口的第一项---u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。 + +- 字段表集合 +- 字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。可以包括的信息有:作用域(访问权限)、static修饰符、final修饰符、并发可见性、序列化修饰符等。 + +- 跟随access_flags标志的是两项索引值:name_index and desciptor_index。它们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。 +- 全限定名和简单名称:org/fenixsoft/clazz/TestClass是这个类的全限定名。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类的inc()方法和m字段的简单名称分别是inc和m。 +- 相对于全限定名和简单名称而言,方法和字段的描述符要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。基本数据类型(byte、char、double…)以及代表无返回值的void类型都用一个大写字母表示,而对象类型则用字符L加对象的全限定名来表示。 + +- 对于数组来说,每一维度将使用一个前置的【字符来描述,如定义一个java.lang.String[][]类型的二维数组,将被记录为[[Ljava/lang/String,一个整数数组 int[] 将被记录为[I。 +- 用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号之内。比如方法void inc()的描述符为()V,方法java.lang.String.toString()的描述符为()Ljava/lang/String。 +- 字段表集合首先是一个容量计数器,说明该类的字段表数据个数,然后是access_flags标志,然后是其他标志、 +- 在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息。 +- 字段表集合不会列出从超类或者父接口继承而来的字段,但有可能列出原本Java代码中不存在的字段,比如内部类会自动添加指向外部类实例的字段。 +- 方法表集合 +- 方法表的结构依次包括了访问标志、名称索引、描述符索引、属性表集合。 + +- 方法里面的Java代码,经过编译器编译为字节码指令后,存放在方法属性表中一个名为Code的属性里面,属性表是Class文件格式中最具拓展性的一种数据项目。 +- 与字段表集合对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器和实例构造器。 +- 要重载一个方法,除了要有和原方法相同的简单名称外,还必须有一个不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的结婚,也就是返回值不会包含在特征签名中。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存在同一个Class文件中的。 +- 属性表集合 +- 与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制宽松一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息。为了能正确解析Class文件,Java虚拟机规范预定义了9项虚拟机实现应当能识别的属性。(现已增至21项) + +- 以上列出其中的5种。 +- 对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性表的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足下图所定义的结构: + +- 1、Code属性 +- Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这种属性。 + +- max_stack代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。 +- max_locals代表了局部变量表所需的存储空间,单位是slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char等长度不超过32位的数据类型,每个局部变量占1个Slot,而long和double占2个Slot。方法参数(包括this)、显式异常处理器参数(catch所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。并不是方法中用到了多少个局部变量,就把这些变量所占Slot之和作为max_locals的值,因为局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用。 +- Code属性表是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。 +- 在任何实例方法中,都可以通过this关键字访问到此方法所属的对象。它的实现就是通过javac编译器变异的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用。 +- 在字节码指令之后的是这个方法的显式异常处理表集合,异常表对Code属性来说并不是必须存在的。 +- 异常表包含4个字段,这些字段的含义是:如果当字节码在第start_pc行到第end_pc之间(不含end_pc)出现了类型为catch_type或者其子类的异常,则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。 + +- 异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。 +- 2、Exceptions属性 +- Exceptions属性的作用是列举出方法中可能抛出的受检查异常,也就是方法描述时在throws关键字后面列举的异常。它的结构: + +- 3、LineNumberTable属性 +- LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系,是可选的属性。如果选择不生成LineNumberTable属性,对程序运行的最主要的影响就是当跳出异常时,堆栈中将不会显示出错的行号,并且在调试的时候,也无法按照源码行来设置断点。 +- 4、LocalVariableTable属性 +- LocalVariableTable属性用于描述栈帧中局部变量表中的变量和Java源码中定义的变量之间的关系,它也是可选的属性。 + +- 5、SourceFile属性 +- SourceFile属性用于记录生成这个Class文件的源码文件名称,也是可选的。 +- 6、ConstantValue属性 +- ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static修饰的变量才可以使用这项属性。对于非static类型的变量的赋值是在实例构造方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果是常量(static final),并且这个常量的数据类型是基本类型或者String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型或字符串,则将会选择在类构造器中进行初始化。 + +- 字节码指令简介 +- Java虚拟机指令是由一个字节长度的、代表着某种特定操作含义的数字(操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。 +- 1个字节意味着指令集的操作码总数不能超过256条;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构。放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号。 +- Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解: + +- 字节码与数据类型 +- 在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。iload指令用于从局部变量表中记载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立的操作码。 +- 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务,i代表对int类型的数据操作,l代表long等。 +- 大部分的指令都没有支持整数类型byte、char和short,编译器会在编译时或运行时将byte和short类型的数据带符号拓展为相应的int类型的数据。大多数对于boolean、byte、short和char类型的数据的操作,实际上都是使用相应的int类型作为运算类型。 +- 加载和存储指令 +- 加载和存储指令用于将数据在栈帧中的局部变量表和操作数之间来回传输。 + +- 尖括号结尾的指令实际上是代表了一组指令,这几组指令都是某个带有一个操作数的通用指令的特殊形式,对于这若干组的特殊指令来说,它们省略掉了显式地操作数,不需要进行取操作数的动作,实际上操作数就隐含在指令中。 +- 运算指令 + + +- 类型转换指令 + +- 尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。 +- 对象创建与访问指令 + +- 操作数栈管理指令 + +- 控制转换指令 + +- 方法调用和返回指令 + +- 异常处理指令 +- 在Java程序中显式抛出的操作都由athrow指令来实现。处理异常(catch)不是由字节码指令来实现的,而是采用异常表来完成的。 +- athrow指令与异常表: + +``` +public void catchException() { + +``` + +- try { + +- throw new Exception(); + + - } catch (Exception var2) { + +- ; + +- } + +- } + +- 字节码: + +``` +public void catchException(); +``` + +- Code: +- Stack=2, Locals=2, Args_size=1 +- 0: new #58; //class java/lang/Exception +- 3: dup +- 4: invokespecial #60; //Method java/lang/Exception."":()V +- 7: athrow +- 8: astore_1 +- 9: return +- Exception table: +- from to target type +- 0 8 8 Class java/lang/Exception + +- 偏移为7的athrow指令,这个指令运作过程大致是首先检查操作栈顶,这时栈顶必须存在一个reference类型的值,并且是java.lang.Throwable的子类(虚拟机规范中要求如果遇到null则当作NPE异常使用),然后暂时先把这个引用出栈,接着搜索本方法的异常表,找一下本方法中是否有能处理这个异常的handler,如果能找到合适的handler就会重新初始化PC寄存器指针指向此异常handler的第一个指令的偏移地址。接着把当前栈帧的操作栈清空,再把刚刚出栈的引用重新入栈。如果在当前方法中很悲剧的找不到handler,那只好把当前方法的栈帧出栈(这个栈是VM栈,不要和前面的操作栈搞混了,栈帧出栈就意味着当前方法退出),这个方法的调用者的栈帧就自然在这条线程VM栈的栈顶了,然后再对这个新的当前方法再做一次刚才做过的异常handler搜索,如果还是找不到,继续把这个栈帧踢掉,这样一直到找,要么找到一个能使用的handler,转到这个handler的第一条指令开始继续执行,要么把VM栈的栈帧抛光了都没有找到期望的handler,这样的话这条线程就只好被迫终止、退出了。 + - 上面的异常表只有一个handler记录,它指明了从偏移地址0开始(包含0),到偏移地址8结束(不包含8),如果出现了java.lang.Exception类型的异常,那么就把PC寄存器指针转到8开始继续执行。顺便说一下,对于Java语言中的关键字catch和finally,虚拟机中并没有特殊的字节码指令去支持它们,都是通过编译器生成字节码片段以及不同的异常处理器来实现。 +- + +- 同步指令(重点) +- Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。 +- 方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。 +- 同步一段指令集序列通常是由synchronized语句块来表示的,Java虚拟机的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javc编译器和虚拟机两者共同协作支持。 + +- 方法中调用过的每一条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是否正常结束。 +- 为了保证在方法异常完成时monitorenter和monitorexit指令异常可以正常配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有的异常,它的目的就是用来执行monitorexit指令。 +# 类加载机制 +- 概述 +- 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 +- 与那些编译时需要进行连接的语言不同,Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时增加一些性能开销,但是是为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。 +- 类加载的时机 +- 类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。 + +- 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始,而解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。 +- 什么时候开始类加载过程的第一个阶段:加载? +- Java虚拟机规范规定有且只有5种情况必须立即对类进行初始化: + - 1)遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有过初始化,则需要先初始化。生成这4条指令的最常见的Java代码场景是:使用new实例化对象时、读取或设置一个类的静态字段时、调用一个类的静态方法时 + - 2)反射 + - 3)如果一个类的父类尚未初始化,那么先触发其父类的初始化 + - 4)main方法所在类 + - 5)java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、 REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 +- 只有直接定义一个静态字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确确定,这点取决于虚拟机的具体实现。 +- 当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口(如引用接口中定义的常量)的时候才会初始化。 +- 类加载的过程 + - 1) 加载 +- 加载是类加载过程的一个阶段。 +- 在加载阶段,虚拟机需要完成以下3件事情: + - 1)通过一个类的全限定名获得此类的二进制字节流 + - 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 + - 3)在内存中生成一个代表这个类的Class对象(HotSpot中放在堆里),作为方法区这个类的各种数据的访问入口。 +- 加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。 +- 加载阶段和连接阶段的部分内容是交叉进行的 +- java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用,InstanceKlass放在方法区(Java8HotSpot中放在元数据区) + - 2) 验证 +- 验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 +- 验证阶段是否严谨直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。 +- 验证阶段大致会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。 +- 1、文件格式验证 +- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。 +- 2、元数据验证 +- 第二阶段是对字节码描述的数据进行语义分析,以保证其描述的信息符合Java语言规范的要求。 +- 3、字节码验证 +- 第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的事件。 +- 如果一个方法体通过了字节码校验,也不能说明其一定就是安全的,这里涉及一个停机问题,通过程序去校验程序逻辑是无法做到绝对准确的-----不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。 +- 4、符号引用验证 +- 最后一个阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段—解析阶段中发生。符号引用可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 + - 3) 准备 +- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量;这里所说的初始值,是指0值。 +- 如果是static final 常量,那么会被初始化为ConstantValue属性所指定的值。 + - 4) 解析 +- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 +- 符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定都已经加载到内存中。 +- 直接引用:直接引用是可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必须已经在内存中存在。 +- 对同一个符号引用进行多次解析请求是很常见的事情,除了invokedynamic指令外,虚拟机实现可以对第一次解析的结果进行缓存,从而避免解析动作重复进行。动态(invokedynamic)的含义是必须等到程序实际运行到这条指令的时候,解析动作才能进行。 + - 5) 初始化 +- 类初始化是类加载阶段的最后一步。到了初始化阶段,才真正开始执行类中定义的Java程序代码。 +- 初始化阶段是执行类构造器方法的过程。 +- 类构造器是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块只能访问到定义在静态代码块之间的变量,定义在它之后的变量,在前面的静态代码块可以赋值,但是不能访问。 +- 类构造器与类的构造方法不同,它不需要显式调用父类构造器,虚拟机会保证在子类的类构造器执行之前,父类的类构造器已经执行完毕。 +- 类构造器对于类或接口不是必需的,如果一个类中没有静态代码块,也没有对变量的赋值操作,那么编译器可以不为这个类生成类构造器。 +- 接口中不能使用静态代码块,但是仍然有变量初始化的赋值操作,因此接口和类一样都会生成类构造器。但接口与类不同的是,执行接口的类构造器不需要先执行父接口的类构造器。只有当父接口中定义的变量使用时,父接口才会初始化,另外,接口的实现类在初始化时也一样不会执行接口的类构造器。 +- 虚拟机会保证一个类的类构造器在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直至活动线程执行类构造器完毕。 + +- 类的主动引用和被动引用 +- 主动引用(一定会发生类的初始化) +- new对象 +- 引用静态变量(static非final)和静态方法 +- 反射 +- main方法所在类 +- 当前类的所有父类 + +- 被动引用(不会发生类的初始化) +- 访问一个类的静态变量,只有真正声明这个静态变量的类才会被初始化 +- 通过数组定义类引用 +- 引用常量(存在方法区的运行时常量池) +- 类加载器 +- 类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器。 +- 类与类加载器 +- 类加载器虽然只用于实现类的加载动作,但它在Java程序中的作用不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。换句话说,比较两个类是否相等,只有在这两个类是由同一个类加载器(实例)加载的前提下才有意义,否则,即使这两个来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。 +- 这里所指的相等,包括类的Class对象的equals方法等的返回结果,也包括instance of的返回结果。 +- 双亲委派模型 +- 从Java虚拟机角度来讲,只存在两种不同的类加载器:一种是启动类加载器(bootstrap classloader),这个类加载器由C++语言实现(HotSpot),是虚拟机自身的一部分;另一种就是所有的其他类加载器,都由Java语言实现,独立于虚拟机外部。并且全继承自java.lang.ClassLoader。 +- 从Java开发人员的角度看,Java程序使用到以下3种系统提供的类加载器: + - 1)启动类加载器:负责将存放在\lib目录中的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。 + - 2)扩展类加载器(Extension ClassLoader):这个加载器负责加载\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器。 + - 3)应用程序类加载器(Application ClassLoader):或称系统类加载器,负责加载用户classpath下所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 + +- 类加载器之间的层次关系成为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层了启动类加载器,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合的方式来复用父加载器的代码。 +- 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己去加载。 +- 使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随它的类加载器一起具备了一种带有优先级的层次关系。 +- 破坏双亲委派模型 +- 双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。比如OSGi环境下,类加载不再是双亲委派模型中的树形结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求,OSGi将按照下面的顺序进行类搜索: + + +## 热部署 +- 如果我们希望将java类卸载,并且替换更新版本的java类,该怎么做呢?’ +- 1、销毁该自定义ClassLoader +- 2、更新class文件 +- 3、创建新的ClassLoader去加载更新后的class文件。 +# 5.21 对象初始化的先后顺序 +- 单个类: + - 1)类的静态变量清0 + - 2)静态变量赋值,静态代码块(按照编写顺序调用) + - 3)成员变量清0 + - 4)成员变量赋值,非静态代码块(按照编写顺序调用) + - 5)构造方法 + +- 1、2统称为类的初始化 +- 4、5统称为对象初始化 + +- 带有继承时: + - 1)父类类初始化 + - 2)子类类初始化 + - 3)成员变量清0,包括父类和子类 + - 3)父类对象初始化 + - 4)子类对象初始化 + + +- 实例一: + +``` +public class StaticTest { + public static void main(String[] args) { + staticFunction(); + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + + /** + main 方法属于静态方法,主动引用,开始执行类的初始化:按照编写顺序进行静态变量赋值与静态代码块执行 + 1)先初始化StaticTest,对象实例化时,因为类已经被加载,所以执行对象初始化,先对成员变量进行初始化(a赋值为0), + 然后按照编写顺序进行非静态变量赋值与非静态代码块执行(打印2,a赋值为110), + 再调用构造方法(打印3,打印a=110,b=0) + 2)再执行静态代码块,打印1 + 3)再赋值b为112, + 4)至此类加载完毕,执行main方法,打印4 + + + 2 + 3 + a=110,b=0 + 1 + 4 + */ +} +``` + + + +- 实例二: + +``` +public class InitializeDemo { + private static int k = 1; + private static InitializeDemo t1 = new InitializeDemo("t1"); + private static InitializeDemo t2 = new InitializeDemo("t2"); + private static int i = print("i"); + private static int n = 99; + + static { + print("静态块"); + } + + private int j = print("j"); + + { + print("构造块"); + } + + public InitializeDemo(String str) { + System.out.println((k++) + ":" + str + " i=" + i + " n=" + n); + ++i; + ++n; + } + + public static int print(String str) { + System.out.println((k++) + ":" + str + " i=" + i + " n=" + n); + ++n; + return ++i; + } + + public static void main(String args[]) { + new InitializeDemo("init"); + } +} +``` + + +- + +- 1:j i=0 n=0 +- 2:构造块 i=1 n=1 +- 3:t1 i=2 n=2 +- 4:j i=3 n=3 +- 5:构造块 i=4 n=4 +- 6:t2 i=5 n=5 +- 7:i i=6 n=6 +- 8:静态块 i=7 n=99 +- 9:j i=8 n=100 +- 10:构造块 i=9 n=101 +- 11:init i=10 n=102 + + +- 实例三: + +``` +class Glyph { + void draw() { + System.out.println("Glyph.draw()"); + } + + Glyph() { + System.out.println("Glyph() before draw()"); + draw(); + System.out.println("Glyph() after draw()"); + } +} + +class RoundGlyph extends Glyph { + private int radius = 1; + + RoundGlyph(int r) { + radius = r; + System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius); + } + + void draw() { + System.out.println("RoundGlyph.draw(), radius = " + radius); + } +} + +public class PolyConstructors { + public static void main(String[] args) { + new RoundGlyph(5); + /** + * + Glyph() before draw() + RoundGlyph.draw(), radius = 0 + Glyph() after draw() + RoundGlyph.RoundGlyph(), radius = 5 + */ + } +} +``` + + +- + +- + +# 字节码执行引擎 +- 概述 +- 虚拟机的执行引擎不是直接建立在处理器、硬件、指令集和操作系统层面的,而是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并能够执行哪些不被硬件直接支持的指令集格式。 +- 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行和编译执行(通过即时编译器产生本地代码)两种选择,也可能两者兼备。但从外观上看起来,所有的Java虚拟机都是一样的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行过程。 +- 运行时栈帧结构 +- 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、返回地址等信息。每一个方法从调用开始至执行完成过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 +- 在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。 +- 一个线程中的方法调用链可能会很长,很多方法同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。 + +- 局部变量表 +- 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。 +- 局部变量表的容量以变量槽(Slot)为最小单位,每个Slot都应该能存放一个boolean、byte、char、short、Reference等类型的数据,允许Slot的长度可以随着处理器、操作系统或虚拟机的实现不同而发生变化。 +- 一个Slot可以存放一个对象实例的引用,虚拟机能够通过这个引用做到两点:一是从此引用中直接或间接地查找对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。 +- 局部变量表是线程私有的数据,无论读写两个连续的Slot(long、double)是否为原子操作,都不会引起线程安全问题。 +- 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。 +- 虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的Slot数量。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个。 +- 在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字this来访问到这个隐含的参数。其他参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。 +- 局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那整个变量对应的Slot就可以交给其他变量使用。Slot的复用会直接影响到系统的垃圾收集行为。 +- 操作数栈 +- 操作数栈(Operand Stack)是一个后进先出栈。操作数栈的最大深度也是在编译的时候就写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深入都不会超过max_stacks。 +- 在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。 +- Java虚拟机的解释执行引擎称为基于栈的执行引擎,其中的栈就是操作数栈。 + +- 动态连接 +- 每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。 +- 方法返回地址 +- 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。 +- 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。 +- 无论何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是通过异常处理器确定的,栈帧中一般不会保存这部分信息。 +- 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出可能的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。 +# 5.22 方法调用 +- 方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部的具体执行过程。Class文件的编译过程不包含传统编译中的连接步骤,一切方法调用在Class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址。这个特性使得Java方法调用过程变得复杂,需要在类加载器件,甚至到运行期间才能确定目标方法的直接引用。 +- 解析 +- 符号引用能转为直接引用成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。 +- 在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法,前者和类型直接关联,后者在外部不可被访问,这两种方法的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。 + +- 只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这个方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final)。 +- 非虚方法也包含被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法调用者进行多态选择,又或者说多态选择的结果肯定是位移的。 +- 解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转为可确定的直接引用,不会延迟到运行期再去完成。而分派调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的组合就构成了静态单分派、静态多分派、动态单分派、动态多分派这4中分派组合情况。 +- 分派 +- 分派调用过程将会揭示多态性的一些最基本体现,如重载和重写。 +- 1、静态分派 + +- 上面代码中的Human称为变量的静态类型(Static Type),或者叫做外观类型,后面的Man则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会发生改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。 +- 代码中刻意定义了两个静态类型相同但是实际类型不同的变量,但编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。 +- 所有依赖静态类型来定位方法执行版本的分派动作被称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一的,往往只能确定一个更加合适的版本。产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式地静态类型,它的静态类型只能通过语言上的规则去理解和推断。 +- 2、动态分派 +- 重写 + + +- 导致整个现象的原因很明显,是这两个变量的实际类型不同。 +- 以下为字节码 + + + + +- 由于invokevitual指令执行的第一步就是 在运行期确定接受者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质,我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。 +- 3、单分派与多分派 +- 方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。 + +- 今天的Java语言是一门静态多分派、动态单分派的语言。 +- 4、虚拟机动态分派的实现 +- 由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如何频繁的搜索。最常用的稳定优化的方法就是为类在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。 + +- 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。 +- 为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引编号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。 +- 方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。 +- 虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存和基于类型继承关系分析技术的守护内联两种非稳定的激进优化手段来获得更高的性能。 +- 基于栈的字节码解释执行引擎 +- 许多Java虚拟机的执行引擎在执行Java代码的时候都有解释执行和编译执行两种选择。 +- 解释执行 +- Java语言经常被人们定位为解释执行的语言,但当主流的虚拟机都包含了即时编译器后,Class文件中的代码到底会被解释执行还是编译执行,就成了只有虚拟机自己才能准确判断的事情。只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。 +- 基于栈的指令集和基于寄存器的指令集 +- Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之相对的另外一套常用的指令集架构是基于寄存器的指令集。 +- 计算1+1: +- 前者: + +- 后者: + +- 基于栈的指令集主要的优点是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。栈架构的指令集还有一些其他的优点,如代码相对更加紧凑、编译器实现更加简单(不需要考虑空间分配,都在栈上操作)等。 +- 栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。 +- 虽然栈架构指令集的代码非常紧凑,但是完成相同功能所需的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。尽管虚拟机可以采取栈顶缓存的手段,把最常用的操作映射到寄存器中避免直接内存访问,但这也只能是优化措施而不是解决本质问题的方法。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度会相对较慢。 +- + +# 程序编译与代码优化 +# 5.23 字节码的编译过程(前端编译器) +- Java语言的编译期是一段不确定的操作过程。 + - 1)编译器前端/前端编译器:把java文件转为class文件,比如Sun的Javac + - 2)编译器后端/后端运行时编译器(JIT Just In Time 编译器):把字节码转为机器码,比如HotSpot VM的C1、C2编译器 + - 3)静态提前编译器(AOT Ahead Of Time 编译器):直接把java文件编译为本地机器代码,比如GNU Compiler for the Java(GCJ)。 + +- 通常意义上的编译器就是前端编译器,这类编译器对代码的运行效率几乎没有任何优化,把对性能的优化集中到了后端编译器,这样可以使其他语言的class文件也同样能享受到编译器优化所带来的好处。 +- 但是Javac做了很多针对Java语言编码过程中的优化措施来改善程序员的编码风格和提高编码效率,相当多的新的语法特性都是靠前端编译器的语法糖实现的,而非依赖虚拟机的底层改进来实现。 + +- Javac的编译过程大致可以分为三个阶段: + - 1)解析和填充符号表 + - 2)插入式注解处理器的注解处理 + - 3)语义分析与字节码生成 + +## 解析与填充符号表 + - 1)解析包括了词法分析和语法分析两个过程。 +- 词法分析是将源代码的字符流变为Token序列; +- 语法分析是根据Token序列构造抽象语法树AST的过程 + - 2)填充符号表 +- 符号表是由一组符号地址和符号信息构成的表格。 +- 符号表中所登记的信息在编译的不同阶段都要用到。 + +## 插入式注解处理器的注解处理 +- 插入式注解处理器可以视为一组编译器的插件,可以读取、修改、添加AST中的任意元素。如果在处理注解期间对AST进行了修改,那么编译器将回到解析与填充符号表的过程重新处理,每一次循环称为一个Record。 + +## 语义分析与字节码生成 +- 语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查。 +- 语义分析的过程分为Token检查和数据及控制流分析两个阶段。 + - 1)Token检查的内容包括变量使用前是否声明、变量和赋值之间的数据类型能否匹配,还有常量折叠等。 + - 2)数据及控制流分析是对程序上下文逻辑进行更进一步的验证,它可以检查出如程序员局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检异常都被正确处理等。 + - 3)解语法糖:比如泛型、变长参数、自动装箱/拆箱等 + - 4)字节码生成:不仅仅是把签个各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作,比如添加实例构造器()和类构造器()。 +# 5.24 后端编译器的优化(JIT) +- 解决以下几个问题: + - 1)为何HotSpot虚拟机要使用解释器和编译器并存的架构 + - 2)为何HotSpot虚拟机要实现两个不同的JIT + - 3)程序何时使用解释器执行,何时使用编译器执行 + - 4)哪些程序代码会被编译为本地代码,如何编译为本地代码 + - 5)如何从外部观察JIT的编译过程和编译结果 + +## 编译器与解释器 +- 解释器与编译器各有优势,前者节省内存,后者提高效率。 +- 在整个虚拟机执行架构中,解释器与编译器经常配合工作。 + +- HotSpot虚拟机中内置了两个JIT,分别称为Client Compiler和Server Compiler。在虚拟机中习惯将Client Compiler称为C1,将Server Complier 称为C2。目前主流的HotSpot虚拟机中,默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与机器硬件性能自动选择运行模式,用户也可以使用-client或者-server参数去强制指定虚拟机运行的模式。 +- 无论采用哪一种编译器,解释器与编译器搭配使用的方式在虚拟机中称为混合模式,用户可以使用参数-Xint强制虚拟机运行于解释模式,这时编译器完全不介入工作;也可以使用参数-Xcomp强制虚拟机运行于编译模式,这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。 + +- 为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机会逐渐启用分层编译的策略。 +- 第0层:程序解释执行,解释器不开启性能监控功能,可触发第1层编译 +- 第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必须将加入性能监控的逻辑 +- 第2层(或2层以上):也称为C2编译,也是将字节码编译为本地代码,但是会启动一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。 + +- 实时分层编译后,Client Compiler和Server Compiler将会同时工作,很多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compile获取更好的编译质量,在解释执行的时候也无需再承担收集性能监控信息的任务。 + +## 编译对象与触发条件 +- 在运行过程中被JIT编译的热点代码有两类: + - 1)被多次调用的方法 + - 2)被多次执行的循环体 +### 热点探测 +- 编译器都会以整个方法作为编译对象。这种编译方式因为编译发生在方法执行过程之中,因此形象地成为栈上替换(On Stack Replacement OSR)。 +- 判断一段代码是不是热点代码,是不是需要触发JIT,这样的行为称为热点探测。目前主流的热点探测判定方式有两种: + - 1)基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这些方法就是热点方法。好处是简单高效,还可以很容易得获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。 + - 2)基于计数器的热点探测:采用这种方法的虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。好处是更加精确演进,缺点是实现较为麻烦。 + +- HotSpot采用的第二种方法,因为它为每个方法准备了两类计数器:方法调用计数器和回边计数器。这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。 +### 方法调用计数器 +- 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器加一,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。如果已超过阈值,那么会向JIT提交一个该方法的代码编译请求。 +- 如果不做任何设置,方法调用计数器统计的并不是方法被调用的总次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给JIT编译,则这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的衰减,而这段时间就称为此方法统计的半衰周期。 + +### 回边计数器 +- 回边计数器的作用是统计一个方法中循环体的代码执行次数,在字节码中遇到控制流向后调换的指令称为回边,回边计数器统计的目的就是为了触发OSR编译。 +- 当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有这已经编译好的版本,如果有,它将会优先执行已编译的代码,否则就把回边计数器的值加一,然后判断方法调用计数器与回边计数器之和是否超过回边计数器的阈值。当超过阈值时,将会提交一个OSR编译请求,并且把回边计数器的值降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果。 +- + +## Client Compiler(编译速度快) +- 是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。 +- 三段式: + - 1)第一个阶段,一个平台独立的前端会将字节码构造成一种高级中间代码表示(HIR High-Level Intermediate Representation)。HIR使用静态单分配的形式来表示代码值,这使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。 +- 在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等。 + - 2)第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(LIR Low-Level Intermediate Representation),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等。 + - 3)第三个阶段,一个平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。 + + +## Server Compiler(编译质量高) +- 是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的编译器。它会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除。另外还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联、分值预测检测等。 +## 编译优化 +- 语言无关的经典优化技术之一:公共子表达式消除 +- 语言相关的经典优化技术之一:数组范围检查消除 +- 最重要的优化技术之一:方法内联 +- 最前沿的优化技术之一:逃逸分析 +### 语言相关的优化技术——逃逸分析 +- 分析指针动态范围的方法称之为逃逸分析(通俗点讲,当一个对象的指针被多个方法或线程引用时们称这个指针发生了逃逸)。 +- 逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸。 +- 1、方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中; +- 2、线程逃逸:如类变量或实例变量,可能被其它线程访问到; + +- 如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配。 + +- 同步消除 +- 线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,则可以消除对该对象的同步锁,通过-XX:+EliminateLocks可以开启同步消除。 + +- 标量替换 +- 1、标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量; +- 2、如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换; +- 3、如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量; +- 通过-XX:+EliminateAllocations可以开启标量替换, -XX:+PrintEliminateAllocations查看标量替换情况。 + +- 栈上分配 +- 故名思议就是在栈上分配对象,其实目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换。 + +# 性能监控与故障处理工具 +- 如果一个接口调用很慢,原因是,如何定位,没有日志的话:假设一下,复现问题,dump查看内存,查看监控日志 +- 如何把java内存的数据全部dump出来 +- 在生产线Dump堆分析程序是否有内存及性能问题 +- jstack jmap、jconsole 等工具 可视化工具使用;如何线上排查JVM的相关问题? +- JVM线程死锁,你该如何判断是因为什么?如果用VisualVM,dump线程信息出来,会有哪些信息? +- 查看jvm虚拟机里面堆、线程的信息,你用过什么命令? +- 内存泄露如何定位 + +# 5.25 JPS:显示所有虚拟机进程 +# 5.26 JConsole:图形化工具,查询JVM中的内存变化情况。 +# 5.27 JVisualVM:图形化工具,分析GC趋势、内存消耗情况 +- 可以分析堆dump文件 + +# 5.28 JMap:命令行工具,查看JVM中各个代的内存状况、JVM中对象的内存的占用状况,以及dump整个JVM中的内存信息。 + +- jmap –heap [pid] 整个JVM中内存的状况 +- jmap –histo [pid] JVM堆中对象的详细占用情况 +- jmap –dump:format=b,file=文件名 [pid] 将整个JVM内存拷贝到文件中 + +# 5.29 JHat 用于分析JVM堆的dump文件:jhat –J-Xmx1024M [file] +- 可以通过浏览器访问,端口号是7000. + +# 5.30 JStack:看到JVM中线程的运行状况,包括锁的等待、线程是否在运行等。 +- jstack [pid] + +- jstack [option] pid +- jstack [option] executable core +- jstack [option] [server-id@]remote-hostname-or-ip +- 命令行参数选项说明如下: +- -l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况 +- -m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法) +- jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多。下面我们来一个实例找出某个Java进程中最耗费CPU的Java线程并定位堆栈信息,用到的命令有ps、top、printf、jstack、grep。 + +- 第一步先找出Java进程ID,服务器上的Java应用名称为mrf-center: + +- root@ubuntu:/# ps -ef | grep mrf-center | grep -v grep (或者直接JPS查看进程PID) +- root 21711 1 1 14:47 pts/3 00:02:10 java -jar mrf-center.jar +- 第二步 top -H -p pid +- 用第三个,输出如下: +- PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +- 21936 root 20 0 1747m 21m 9404 S 0.0 0.6 0:00.00 java +- 21937 root 20 0 1747m 21m 9404 S 0.0 0.6 0:00.14 java +- 21938 root 20 0 1747m 21m 9404 S 0.0 0.6 0:00.00 java +- 21939 root 20 0 1747m 21m 9404 S 0.0 0.6 0:00.00 java +- 21940 root 20 0 1747m 21m 9404 S 0.0 0.6 0:00.00 java + +- TIME列就是各个Java线程耗费的CPU时间,CPU时间最长的是线程ID为21742的线程,用 + +- printf "%x\n" 21742 +- 得到21742的十六进制值为54ee,下面会用到。 + +- OK,下一步终于轮到jstack上场了,它用来输出进程21711的堆栈信息,然后根据线程ID的十六进制值grep,如下: + +- root@ubuntu:/# jstack 21711 | grep 54ee +- "PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() +- 可以看到CPU消耗在PollIntervalRetrySchedulerThread这个类的Object.wait(),我找了下我的代码,定位到下面的代码: + +- // Idle wait +- getLog().info("Thread [" + getName() + "] is idle waiting..."); +- schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting; +- long now = System.currentTimeMillis(); +- long waitTime = now + getIdleWaitTime(); +- long timeUntilContinue = waitTime - now; +- synchronized(sigLock) { +- try { +- if(!halted.get()) { +- sigLock.wait(timeUntilContinue); +- } +- } +- catch (InterruptedException ignore) { +- } +- } +- 它是轮询任务的空闲等待代码,上面的sigLock.wait(timeUntilContinue)就对应了前面的Object.wait()。 +# 5.31 JStat:JVM统计监测工具 +- jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ] +- vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID。interval是采样时间间隔。count是采样数目。 + +- 比如下面输出的是GC信息,采样时间间隔为250ms,采样数为4: +- root@ubuntu:/# jstat -gc 21711 250 4 +- S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT +- 192.0 192.0 64.0 0.0 6144.0 1854.9 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 +- 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 +- 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 +- 192.0 192.0 64.0 0.0 6144.0 2109.7 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 + +- S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used) +- EC、EU:Eden区容量和使用量 +- OC、OU:年老代容量和使用量 +- PC、PU:永久代容量和使用量 +- YGC、YGT:年轻代GC次数和GC耗时 +- FGC、FGCT:Full GC次数和Full GC耗时 +- GCT:GC总耗时 +# 5.32 MAT 可视化分析dump文件 +- Memory Analyzer Tool + +- + +# 性能调优 +# 5.33 参数 +## 堆设置 +- -Xms:初始堆大小 +- -Xmx:最大堆大小 +- -Xmn年轻代大小 +- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 +- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 +## 栈设置 +- -Xss 设置每个线程的栈大小 +## 元数据区设置 +- -XX:MetaspaceSize -XX:MaxMetaspaceSize 元数据区的初始大小和最大大小 + +## 异常设置 +- -XX:+HeapDumpOnOutOfMemoryError 使得JVM在产生内存溢出时自动生成堆内存快照(日后再进行分析,写监控脚本,如果发现应用崩溃则重启,并提醒开发人员去查看dump信息) +- -XX:HeapDumpPath 改变默认的堆内存快照生成路径,可以是相对或者绝对路径 +- -XX:OnOutOfMemoryError 当内存发生溢出时 执行一串指令 + +## 收集器设置 +- -XX:+UseSerialGC:设置串行收集器 +- -XX:+UseParallelGC:设置并行收集器 +- -XX:+UseParalledlOldGC:设置并行年老代收集器 +- -XX:+UseConcMarkSweepGC:设置并发收集器 +## 垃圾回收统计信息 +- -XX:+PrintGC +- -XX:+PrintGCDetails +- -XX:+PrintGCTimeStamps +- -Xloggc:filename +## 并行收集器设置 +- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 +- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 +- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) +## 并发收集器设置 +- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 +- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 +# 5.34 调优原则 +- JVM的内存参数;xmx,xms,xmn,xss参数你有调优过吗,设置大小和原则你能介绍一下吗?;Xss默认大小,在实际项目中你一般会设置多大 + +- 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理。 + +## 代大小的调优 +- 1. 避免新生代大小设置过小,过小的话一是minor GC更加频繁,二是有可能导致minor GC对象直接进入老年代,此时如进入老年代的对象占据了老年代剩余空间,则触发Full GC。 +- 2. 避免新生代大小设置过大,过大的话一是老年代变小了,有可能导致Full GC频繁执行,二是minor GC的耗时大幅度增加。 +- 3. 避免Survivor区过小或过大 +- 4. 合理设置新生代存活周期,存活周期决定了新生代的对象经过多少次Minor GC后进入老年代,对应的JVM参数是-XX;MaxTenuringThreshold。 + + +## GC策略的调优 +- 串行GC性能太差,在实际场景中使用的主要为并行和并发GC。 +- 由于CMS GC多数动作是和应用并发进行的,确实可以减小GC给应用带来的暂停。 + +- From 6f33e9e45cb31187d85f55696084440f24bd54f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:41:48 +0800 Subject: [PATCH 62/97] =?UTF-8?q?Rename=20Java=E5=89=91=E6=8C=87offer.md?= =?UTF-8?q?=20to=20=E5=85=AD=E3=80=81Java=E5=89=91=E6=8C=87offer.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\345\205\255\343\200\201Java\345\211\221\346\214\207offer.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/Java\345\211\221\346\214\207offer.md" => "docs/\345\205\255\343\200\201Java\345\211\221\346\214\207offer.md" (100%) diff --git "a/docs/Java\345\211\221\346\214\207offer.md" "b/docs/\345\205\255\343\200\201Java\345\211\221\346\214\207offer.md" similarity index 100% rename from "docs/Java\345\211\221\346\214\207offer.md" rename to "docs/\345\205\255\343\200\201Java\345\211\221\346\214\207offer.md" From be5b50a4ec872eef3608d72dcb9d6d3d3f64962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:46:44 +0800 Subject: [PATCH 63/97] =?UTF-8?q?Update=20and=20rename=201.md=20to=20?= =?UTF-8?q?=E5=BF=85=E8=AF=BB=EF=BC=81=EF=BC=81=EF=BC=81.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/1.md | 1 - ...7\205\350\257\273\357\274\201\357\274\201\357\274\201.md" | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 docs/1.md create mode 100644 "docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" diff --git a/docs/1.md b/docs/1.md deleted file mode 100644 index 8b137891..00000000 --- a/docs/1.md +++ /dev/null @@ -1 +0,0 @@ - diff --git "a/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" "b/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" new file mode 100644 index 00000000..132ec655 --- /dev/null +++ "b/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" @@ -0,0 +1,5 @@ + +- 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,右侧有导航栏,方面大家阅读。 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) From 6d2a037a5bc767adbc89de4552dd94a419de7587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 18:47:04 +0800 Subject: [PATCH 64/97] =?UTF-8?q?Update=20=E5=BF=85=E8=AF=BB=EF=BC=81?= =?UTF-8?q?=EF=BC=81=EF=BC=81.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" "b/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" index 132ec655..f77ef675 100644 --- "a/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" +++ "b/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" @@ -1,5 +1,5 @@ - 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 -- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,右侧有导航栏,方面大家阅读。 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 ![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) From c0d9b7901602f9c0bdc8f52f9a8ac07f4073dd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:21:43 +0800 Subject: [PATCH 65/97] =?UTF-8?q?Rename=20Java=E9=9B=86=E5=90=88.md=20to?= =?UTF-8?q?=20Java=E9=9B=86=E5=90=88=E9=9D=A2=E8=AF=95=E9=A2=98=E5=8F=8A?= =?UTF-8?q?=E7=AD=94=E6=A1=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\257\225\351\242\230\345\217\212\347\255\224\346\241\210.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/Java\351\233\206\345\220\210.md" => "docs/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\345\217\212\347\255\224\346\241\210.md" (100%) diff --git "a/docs/Java\351\233\206\345\220\210.md" "b/docs/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\345\217\212\347\255\224\346\241\210.md" similarity index 100% rename from "docs/Java\351\233\206\345\220\210.md" rename to "docs/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230\345\217\212\347\255\224\346\241\210.md" From 58f73b24df60694ddc91b12f8b44553d2f1a8be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:23:41 +0800 Subject: [PATCH 66/97] =?UTF-8?q?Update=20=E4=B8=80=E3=80=81Java=E5=9F=BA?= =?UTF-8?q?=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\270\200\343\200\201Java\345\237\272\347\241\200.md" | 5 +++++ 1 file changed, 5 insertions(+) diff --git "a/docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" "b/docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" index 84174c7a..bd0e0eac 100644 --- "a/docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" +++ "b/docs/\344\270\200\343\200\201Java\345\237\272\347\241\200.md" @@ -1,3 +1,8 @@ +- 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) + # Java - Oracle JDK有部分源码是闭源的,如果确实需要可以查看OpenJDK的源码,可以在该网站获取。 - http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/ From 262de4168f5c11111f6e53f89bd98e7d80cda8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:24:21 +0800 Subject: [PATCH 67/97] =?UTF-8?q?Update=20=E4=B8=89=E3=80=81Java=20?= =?UTF-8?q?=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" | 2 ++ 1 file changed, 2 insertions(+) diff --git "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" index 285c32bb..4abb84b7 100644 --- "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" +++ "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" @@ -1,3 +1,5 @@ + + # 集合框架 - From 21295020b756e65689101ab5bd1621334a5362f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:24:44 +0800 Subject: [PATCH 68/97] =?UTF-8?q?Update=20=E4=B8=89=E3=80=81Java=20?= =?UTF-8?q?=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" | 4 ++++ 1 file changed, 4 insertions(+) diff --git "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" index 4abb84b7..7c596a8c 100644 --- "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" +++ "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" @@ -1,3 +1,7 @@ +- 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) # 集合框架 From 90c0396f6bd8b550d6f25473c6b76477393bc7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:28:03 +0800 Subject: [PATCH 69/97] =?UTF-8?q?Create=20=E4=B8=83=E3=80=81JavaWeb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "docs/\344\270\203\343\200\201JavaWeb" | 4028 ++++++++++++++++++++++++ 1 file changed, 4028 insertions(+) create mode 100644 "docs/\344\270\203\343\200\201JavaWeb" diff --git "a/docs/\344\270\203\343\200\201JavaWeb" "b/docs/\344\270\203\343\200\201JavaWeb" new file mode 100644 index 00000000..e17dbbe8 --- /dev/null +++ "b/docs/\344\270\203\343\200\201JavaWeb" @@ -0,0 +1,4028 @@ +# JavaWeb +# 三层模型 MVC +- 1、MVC设计模式 + +- MVC设计模式 + +- MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。 +- MVC模式最早为Trygve Reenskaug提出,为施乐帕罗奥多研究中心(Xerox PARC)的Smalltalk语言发明的一种软件设计模式。 +- MVC可对程序的后期维护和扩展提供了方便,并且使程序某些部分的重用提供了方便。而且MVC也使程序简化,更加直观。 +- 控制器Controller:对请求进行处理,负责请求转发; +- 视图View:界面设计人员进行图形界面设计; +- 模型Model:程序编写程序应用的功能(实现算法等等)、数据库管理; + +- 注意,MVC不是Java的东西,几乎现在所有B/S结构的软件都采用了MVC设计模式。但是要注意,MVC在B/S结构软件并没有完全实现,例如在我们今后的B/S软件中并不会有事件驱动! + +- 2、JavaWeb与MVC +-   JavaWeb的经历了JSP Model1、JSP Model1二代、JSP Model2三个时期。 +- 2.1 JSP Model1第一代 +- JSP Model1是JavaWeb早期的模型,它适合小型Web项目,开发成本低!Model1第一代时期,服务器端只有JSP页面,所有的操作都在JSP页面中,连访问数据库的API也在JSP页面中完成。也就是说,所有的东西都耦合在一起,对后期的维护和扩展极为不利。 + + +- 2.2 JSP Model1第二代 +-   JSP Model1第二代有所改进,把业务逻辑的内容放到了JavaBean中,而JSP页面负责显示以及请求调度的工作。虽然第二代比第一代好了些,但还让JSP做了过多的工作,JSP中把视图工作和请求调度(控制器)的工作耦合在一起了。 + + + +- 2.3 JSP Model2 +- JSP Model2模式已经可以清晰的看到MVC完整的结构了。 +- JSP:视图层,用来与用户打交道。负责接收用来的数据,以及显示数据给用户; +- Servlet:控制层,负责找到合适的模型对象来处理业务逻辑,转发到合适的视图; +- JavaBean:模型层,完成具体的业务工作,例如:开启、转账等。 + + +- JSP Model2适合多人合作开发大型的Web项目,各司其职,互不干涉,有利于开发中的分工,有利于组件的重用。但是,Web项目的开发难度加大,同时对开发人员的技术要求也提高了。 + +# 5.1 JavaWeb经典三层框架 +- 我们常说的三层框架是由JavaWeb提出的,也就是说这是JavaWeb独有的! +- 所谓三层是表述层(WEB层)、业务逻辑层(Business Logic),以及数据访问层(Data Access)。 +- WEB层:包含JSP和Servlet等与WEB相关的内容; +- 业务层:业务层中不包含JavaWeb API,它只关心业务逻辑; +- 数据层:封装了对数据库的访问细节; + +-   注意,在业务层中不能出现JavaWeb API,例如request、response等。也就是说,业务层代码是可重用的,甚至可以应用到非Web环境中。业务层的每个方法可以理解成一个万能,例如转账业务方法。业务层依赖数据层,而Web层依赖业务层! + + + +# Web服务器 +- Web服务器的作用是接收客户端的请求,给客户端作出响应。 +- 对于JavaWeb程序而已,还需要有Servlet容器,Servlet容器的基本功能是把动态资源转换成静态资源。 +- 我们需要使用的是Web服务器和Servlet容器,通常这两者会集于一身。下面是对JavaWeb服务器: +- Tomcat(Apache):当前应用最广的JavaWeb服务器; +- JBoss(Redhat红帽):支持JavaEE,应用比较广;EJB容器 +- GlassFish(Orcale):Oracle开发JavaWeb服务器,应用不是很广; +- Resin(Caucho):支持JavaEE,应用越来越广; +- Weblogic(Oracle):收费;支持JavaEE,适合大型项目; +- Websphere(IBM):收费;支持JavaEE,适合大型项目; + +- 支持JavaEE是EJB容器 + +# Servlet +# 5.2 定义 +- Servlet是JavaWeb的三大组件之一,它属于动态资源。Servlet的作用是处理请求,服务器会把接收到的请求交给Servlet来处理,在Servlet中通常需要: +- 接收请求数据; +- 处理请求; +- 完成响应。 +-   例如客户端发出登录请求,或者输出注册请求,这些请求都应该由Servlet来完成处理!Servlet需要我们自己来编写,每个Servlet必须实现javax.servlet.Servlet接口。 +# 5.3 生命周期 +- 生命周期方法: +- void init(ServletConfig):出生之后(1次); +- void service(ServletRequest request, ServletResponse response):每次处理请求时都会被调用; +- void destroy():临死之前(1次); + +- 特性: +- 单例,一个类只有一个对象;当然可能存在多个Servlet类! +- 多线程的,所以它的效率是高的!(不是线程安全的) + +- Servlet类由我们来写,但对象由服务器来创建,并且由服务器来调用相应的方法。 +## Servlet的出生 +- 服务器会在Servlet第一次被访问时创建Servlet,或者是在服务器启动时创建Servlet。如果服务器启动时就创建Servlet,那么还需要在web.xml文件中配置。也就是说默认情况下,Servlet是在第一次被访问时由服务器创建的。 +- 而且一个Servlet类型,服务器只创建一个实例对象,例如在我们首次访问http://localhost:8080/helloservlet/helloworld时,服务器通过“/helloworld”找到了绑定的Servlet名称为cn.itcast.servlet.HelloServlet,然后服务器查看这个类型的Servlet是否已经创建过,如果没有创建过,那么服务器才会通过反射来创建HelloServlet的实例。当我们再次访问http://localhost:8080/helloservlet/helloworld时,服务器就不会再次创建HelloServlet实例了,而是直接使用上次创建的实例。 +- 在Servlet被创建后,服务器会马上调用Servlet的void init(ServletConfig)方法。请记住, Servlet出生后马上就会调用init()方法,而且一个Servlet的一生。这个方法只会被调用一次。 +- 我们可以把一些对Servlet的初始化工作放到init方法中! +## Servlet服务 +-   当服务器每次接收到请求时,都会去调用Servlet的service()方法来处理请求。服务器接收到一次请求,就会调用service() 方法一次,所以service()方法是会被调用多次的。正因为如此,所以我们才需要把处理请求的代码写到service()方法中! +## Servlet的销毁 +-   Servlet是不会轻易离去的,通常都是在服务器关闭时Servlet才会离去!在服务器被关闭时,服务器会去销毁Servlet,在销毁Servlet之前服务器会先去调用Servlet的destroy()方法,我们可以把Servlet的临终遗言放到destroy()方法中,例如对某些资源的释放等代码放到destroy()方法中。 +- Servlet接口相关类型 +- 在Servlet接口中还存在三个我们不熟悉的类型: +- ServletRequest:service() 方法的参数,它表示请求对象,它封装了所有与请求相关的数据,它是由服务器创建的; +- ServletResponse:service()方法的参数,它表示响应对象,在service()方法中完成对客户端的响应需要使用这个对象; +- ServletConfig:init()方法的参数,它表示Servlet配置对象,它对应Servlet的配置 +- ServletRequest和ServletResponse +- ServletRequest和ServletResponse是Servlet#service() 方法的两个参数,一个是请求对象,一个是响应对象,可以从ServletRequest对象中获取请求数据,可以使用ServletResponse对象完成响应。 +- ServletRequest和ServletResponse的实例由服务器创建,然后传递给service()方法。如果在service() 方法中希望使用HTTP相关的功能,那么可以把ServletRequest和ServletResponse强转成HttpServletRequest和HttpServletResponse。这也说明我们经常需要在service()方法中对ServletRequest和ServletResponse进行强转,这是很心烦的事情。不过后面会有一个类来帮我们解决这一问题的。 +- HttpServletRequest方法: +- String getParameter(String paramName):获取指定请求参数的值; +- String getMethod():获取请求方法,例如GET或POST; +- String getHeader(String name):获取指定请求头的值; +- void setCharacterEncoding(String encoding):设置请求体的编码! +- 因为GET请求没有请求体,所以这个方法只对POST请求有效。当调用 +- request.setCharacterEncoding(“utf-8”)之后,再通过getParameter()方法获取参数值时,那么参数值都已经通过了转码,即转换成了UTF-8编码。所以,这个方法必须在调用getParameter()方法之前调用! +- HttpServletResponse方法: +- PrintWriter getWriter():获取字符响应流,使用该流可以向客户端输出响应信息。例如response.getWriter().print(“

Hello JavaWeb!

”); +- ServletOutputStream getOutputStream():获取字节响应流,当需要向客户端响应字节数据时,需要使用这个流,例如要向客户端响应图片; +- void setCharacterEncoding(String encoding):用来设置字符响应流的编码,例如在调用setCharacterEncoding(“utf-8”);之后,再response.getWriter()获取字符响应流对象,这时的响应流的编码为utf-8,使用response.getWriter()输出的中文都会转换成utf-8编码后发送给客户端; +- void setHeader(String name, String value):向客户端添加响应头信息, +- 例如setHeader(“Refresh”, “3;url=http://www.itcast.cn”),表示3秒后自动刷新到http://www.itcast.cn; +- void setContentType(String contentType):该方法是setHeader(“content-type”, “xxx”)的简便方法,即用来添加名为content-type响应头的方法。content-type响应头用来设置响应数据的MIME类型,例如要向客户端响应jpg的图片,那么可以setContentType(“image/jepg”),如果响应数据为文本类型,那么还要同时设置编码,例如setContentType(“text/html;chartset=utf-8”)表示响应数据类型为文本类型中的html类型,并且该方法会调用setCharacterEncoding(“utf-8”)方法; +- void sendError(int code, String errorMsg):向客户端发送状态码,以及错误消息。例如给客户端发送404:response(404, “您要查找的资源不存在!”)。 +- + +- ServletConfig +- Servlet的配置信息,即web.xml文件中的元素。 +- 一个ServletConfig对象对应着一个servlet元素的配置信息(servlet-name,servlet-class) + + +- getServletName获取的是的功能 +- getServletContext获取的是Servlet上下文对象 + +- ServletConfig对象对应web.xml文件中的元素。例如你想获取当前Servlet在web.xml文件中的配置名,那么可以使用servletConfig.getServletName()方法获取! + +- ServletConfig对象是由服务器创建的,然后传递给Servlet的init()方法,你可以在init()方法中使用它! +- String getServletName():获取Servlet在web.xml文件中的配置名称,即指定的名称; +- ServletContext getServletContext():用来获取ServletContext对象; +- String getInitParameter(String name):用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值; +- Enumeration getInitParameterNames():用来获取在web.xml中配置的所有初始化参数名称; +- 在元素中还可以配置初始化参数: + + One + cn.itcast.servlet.OneServlet + + paramName1 + paramValue1 + + + paramName2 + paramValue2 + + + +- 在OneServlet中,可以使用ServletConfig对象的getInitParameter()方法来获取初始化参数,例如: +- String value1 = servletConfig.getInitParameter(“paramName1”);//获取到paramValue1 +# 5.4 实现Servlet的方式 +- 实现Servlet有三种方式: +- 实现javax.servlet.Servlet接口; +- 继承javax.servlet.GenericServlet类; +- 继承javax.servlet.http.HttpServlet类; +- 通常我们会去继承HttpServlet类来完成我们的Servlet +- GenericServlet +- GenericServlet概述 + +- GenericServlet是Servlet接口的实现类,我们可以通过继承GenericServlet来编写自己的Servlet。下面是GenericServlet类的源代码: +- GenericServlet.java +public abstract class GenericServlet implements Servlet, ServletConfig, + java.io.Serializable { + private static final long serialVersionUID = 1L; + private transient ServletConfig config; + public GenericServlet() {} + @Override + public void destroy() {} + @Override + public String getInitParameter(String name) { + return getServletConfig().getInitParameter(name); + } + @Override + public Enumeration getInitParameterNames() { + return getServletConfig().getInitParameterNames(); + } + @Override + public ServletConfig getServletConfig() { + return config; + } + @Override + public ServletContext getServletContext() { + return getServletConfig().getServletContext(); + } + @Override + public String getServletInfo() { + return ""; + } + @Override + public void init(ServletConfig config) throws ServletException { + this.config = config; + this.init(); + } +public void init() throws ServletException {} +这个init方法是为了拓展init而设置的,子类可以重写这个无参数的init方法 + public void log(String msg) { + getServletContext().log(getServletName() + ": " + msg); + } + public void log(String message, Throwable t) { + getServletContext().log(getServletName() + ": " + message, t); + } + @Override + public abstract void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException; + @Override + public String getServletName() { + return config.getServletName(); + } +} + +- GenericServlet的init()方法 +- 在GenericServlet中,定义了一个ServletConfig实例变量,并在init(ServletConfig)方法中把参数ServletConfig赋给了实例变量。然后在该类的很多方法中使用了实例变量config。 +- 如果子类覆盖了GenericServlet的init(StringConfig)方法,那么this.config=config这一条语句就会被覆盖了,也就是说GenericServlet的实例变量config的值为null,那么所有依赖config的方法都不能使用了。如果真的希望完成一些初始化操作,那么去覆盖GenericServlet提供的init()方法,它是没有参数的init()方法,它会在init(ServletConfig)方法中被调用。 +- 实现了ServletConfig接口 +-   GenericServlet还实现了ServletConfig接口,所以可以直接调用getInitParameter()、getServletContext()等ServletConfig的方法。 + +- HttpServlet +- HttpServlet概述 +- HttpServlet类是GenericServlet的子类,它提供了对HTTP请求的特殊支持,所以通常我们都会通过继承HttpServlet来完成自定义的Servlet。 +- HttpServlet覆盖了service()方法 +- HttpServlet类中提供了service(HttpServletRequest,HttpServletResponse)方法,这个方法是HttpServlet自己的方法,不是从Servlet继承来的。在HttpServlet的service(ServletRequest,ServletResponse)方法中会把ServletRequest和ServletResponse强转成HttpServletRequest和HttpServletResponse,然后调用 +- service(HttpServletRequest,HttpServletResponse)方法,这说明子类可以去覆盖service(HttpServletRequest,HttpServletResponse)方法即可,这就不用自己去强转请求和响应对象了。 +- 其实子类也不用去覆盖service(HttpServletRequest,HttpServletResponse)方法,因为HttpServlet还要做另一步简化操作,下面会介绍。 + +- HttpServlet.java +public abstract class HttpServlet extends GenericServlet { + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + …… +} + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + + HttpServletRequest request; + HttpServletResponse response; + + try { + request = (HttpServletRequest) req; + response = (HttpServletResponse) res; + } catch (ClassCastException e) { + throw new ServletException("non-HTTP request or response"); + } + service(request, response); +} +…… +} +- doGet()和doPost() +- 在HttpServlet的service(HttpServletRequest,HttpServletResponse)方法会去判断当前请求是GET还是POST,如果是GET请求,那么会去调用本类的doGet()方法,如果是POST请求会去调用doPost()方法,这说明我们在子类中去覆盖doGet()或doPost()方法即可。 +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("hello doGet()..."); + } +} +public class BServlet extends HttpServlet { + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("hello doPost()..."); + } +} + +- Servlet细节 +- 不要在Servlet中创建成员!创建局部变量即可! +- 可以创建无状态成员! +- 可以创建有状态的成员,但状态必须为只读的!(只有get,没有set) +- Servlet与线程安全 +- 因为一个类型的Servlet只有一个实例对象,那么就有可能会现时出一个Servlet同时处理多个请求,那么Servlet是否为线程安全的呢?答案是:“不是线程安全的”。这说明Servlet的工作效率很高,但也存在线程安全问题! +- 所以我们不应该在Servlet中创建成员变量,因为可能会存在一个线程对这个成员变量进行写操作,另一个线程对这个成员变量进行读操作。 + +- 让服务器在启动时就创建Servlet(很少这么做) +- 默认情况下,服务器会在某个Servlet第一次收到请求时创建它。也可以在web.xml中对Servlet进行配置,使服务器启动时就创建Servlet。 + + hello1 + cn.itcast.servlet.Hello1Servlet + 0 + + + hello1 + /hello1 + + + hello2 + cn.itcast.servlet.Hello2Servlet + 1 + + + hello2 + /hello2 + + + hello3 + cn.itcast.servlet.Hello3Servlet + 2 + + + hello3 + /hello3 + + +- 在元素中配置元素可以让服务器在启动时就创建该Servlet,其中元素的值必须是大于等于的整数,它的使用是服务器启动时创建Servlet的顺序。上例中,根据的值可以得知服务器创建Servlet的顺序为Hello1Servlet、Hello2Servlet、Hello3Servlet。 +- +- 的子元素,用来指定Servlet的访问路径,即URL。 +- 它必须是以“/”开头! +1)- 可以在中给出多个,例如: + + AServlet + /AServlet + /BServlet + + +- 那么这说明一个Servlet绑定了两个URL,无论访问/AServlet还是/BServlet,访问的都是AServlet。 + +2)- 还可以在中使用通配符,所谓通配符就是星号“*”,星号可以匹配任何URL前缀或后缀,使用通配符可以命名一个Servlet绑定一组URL,例如: + +``` +/servlet/*:/servlet/a、/servlet/b,都匹配/servlet/*; +``` + +- *.do:/abc/def/ghi.do、/a.do,都匹配*.do; + +``` +/*:匹配所有URL; +``` + + + +``` +请注意,通配符要么为前缀,要么为后缀,不能出现在URL中间位置,也不能只有通配符。例如:/*.do就是错误的,因为星号出现在URL的中间位置上了。*.*也是不对的,因为一个URL中最多只能出现一个通配符。 +``` + +- 注意,通配符是一种模糊匹配URL的方式,如果存在更具体的,那么访问路径会去匹配具体的。例如: + + hello1 + cn.itcast.servlet.Hello1Servlet + + + hello1 + /servlet/hello1 + + + hello2 + cn.itcast.servlet.Hello2Servlet + + + hello2 + /servlet/* + + +-   当访问路径为http://localhost:8080/hello/servlet/hello1时,因为访问路径即匹配hello1的,又匹配hello2的,但因为hello1的中没有通配符,所以优先匹配,即设置hello1。 +- web.xml文件的继承 +-   在${CATALINA_HOME}\conf\web.xml中的内容,相当于写到了每个项目的web.xml中,它是所有web.xml的父文件。 +- 每个完整的JavaWeb应用中都需要有web.xml,但我们不知道所有的web.xml文件都有一个共同的父文件,它在Tomcat的conf/web.xml路径。 +- conf/web.xml + + + + + default + org.apache.catalina.servlets.DefaultServlet + + debug + 0 + + + listings + false + + 1 + + + + jsp + org.apache.jasper.servlet.JspServlet + + fork + false + + + xpoweredBy + false + + 3 + + + + default + / + + + + jsp + *.jsp + *.jspx + + + + 30 + + + + + bmp + image/bmp + + + htm + text/html + + + + index.html + index.htm + index.jsp + + + +- ServletContext(存取数据,获取资源) +- 一个项目只有一个ServletContext对象! +- 我们可以在N多个Servlet中来获取这个唯一的对象,使用它可以给多个Servlet传递数据! +- 这个对象在Tomcat启动时就创建,在Tomcat关闭时才会死去! +- ServletContext概述 +- 服务器会为每个应用创建一个ServletContext对象: +- ServletContext对象的创建是在服务器启动时完成的; +- ServletContext对象的销毁是在服务器关闭时完成的。 + +-    ServletContext对象的作用是在整个Web应用的动态资源之间共享数据!例如在AServlet中向ServletContext对象中保存一个值,然后在BServlet中就可以获取这个值,这就是共享数据了。 + +- 获取ServletContext +- ServletConfig#getServletContext(); +- GenericServlet#getServletContext(); +- HttpSession#getServletContext() +- ServletContextEvent#getServletContext() + + +- 在Servlet中获取ServletContext对象: +- 在void init(ServletConfig config)中:ServletContext context = config.getServletContext();,ServletConfig类的getServletContext()方法可以用来获取ServletContext对象; +- 在GenericeServlet或HttpServlet中获取ServletContext对象: +- GenericServlet类有getServletContext()方法,所以可以直接使用 +- this.getServletContext()来获取; +-    +public class MyServlet implements Servlet { +public void init(ServletConfig config) { + ServletContext context = config.getServletContext(); +} +… +} +public class MyServlet extends HttpServlet { +public void doGet(HttpServletRequest request, HttpServletResponse response) { + ServletContext context = this.getServletContext(); +} +} +- 域对象的功能 +- ServletContext是JavaWeb四大域对象之一: +- PageContext; +- ServletRequest; +- HttpSession; +- ServletContext; + +- 所有域对象都有存取数据的功能,因为域对象内部有一个Map,用来存储数据,下面是ServletContext对象用来操作数据的方法: +- void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:servletContext.setAttribute(“xxx”, “XXX”),在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同; +- Object getAttribute(String name):用来获取ServletContext中的数据,当前在获取之前需要先去存储才行,例如:String value = (String)servletContext.getAttribute(“xxx”);,获取名为xxx的域属性; +- void removeAttribute(String name):用来移除ServletContext中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做; +- Enumeration getAttributeNames():获取所有域属性的名称; +- 获取应用初始化参数 +- Servlet也可以获取初始化参数,但它是局部的参数;也就是说,一个Servlet只能获取自己的初始化参数,不能获取别人的,即初始化参数只为一个Servlet准备! +- 可以配置公共的初始化参数,为所有Servlet而用!这需要使用ServletContext才能使用! +- 还可以使用ServletContext来获取在web.xml文件中配置的应用初始化参数!注意,应用初始化参数与Servlet初始化参数不同: +- web.xml + + ... + (为ServletContext设置的公共初始化参数) + paramName1 + paramValue1 + + + paramName2 + paramValue2 + + + ServletContext context = this.getServletContext(); + String value1 = context.getInitParameter("paramName1"); + String value2 = context.getInitParameter("paramName2"); + System.out.println(value1 + ", " + value2); + + Enumeration names = context.getInitParameterNames(); + while(names.hasMoreElements()) { + System.out.println(names.nextElement()); + } + +# 5.5 请求&响应 + +- 服务器每次收到请求时,都会为这个请求开辟一个新的线程。 +- Response +- 1、概述 +- response是Servlet#service方法的一个参数,类型为 +- javax.servlet.http.HttpServletResponse。在客户端发出每个请求时,服务器都会创建一个response对象,并传入给Servlet.service()方法。response对象是用来对客户端进行响应的,这说明在service()方法中使用response对象可以完成对客户端的响应工作。 +- response对象的功能分为以下四种: +- 设置响应头信息; +- 发送状态码; +- 设置响应正文; +- 重定向。 +- 2、响应正文 +- response是响应对象,向客户端输出响应正文(响应体)可以使用response的响应流,repsonse一共提供了两个响应流对象: +- PrintWriter out = response.getWriter():获取字符流; +- ServletOutputStream out = response.getOutputStream():获取字节流; +- 当然,如果响应正文内容为字符(html),那么使用response.getWriter(),如果响应内容是字节(图片等),例如下载时,那么可以使用response.getOutputStream() +- (ServletOutputStream)。 +- 注意,在一个请求中,不能同时使用这两个流!也就是说,要么你使用repsonse.getWriter(),要么使用response.getOutputStream(),但不能同时使用这两个流。不然会抛出IllegalStateException异常。 +- 字符响应流 +- 字符编码 +- 在使用response.getWriter()时需要注意默认字符编码为ISO-8859-1,如果希望设置字符流的字符编码为utf-8,可以使用response.setCharaceterEncoding(“utf-8”)来设置。这样可以保证输出给客户端的字符都是使用UTF-8编码的! +- 但客户端浏览器并不知道响应数据是什么编码的!如果希望通知客户端使用UTF-8来解读响应数据,那么还是使用response.setContentType("text/html;charset=utf-8")方法比较好,因为这个方法不只会调用response.setCharaceterEncoding(“utf-8”),还会设置content-type响应头,客户端浏览器会使用content-type头来解读响应数据。 +- 缓冲区 +- response.getWriter()是PrintWriter类型,所以它有缓冲区,缓冲区的默认大小为8KB。也就是说,在响应数据没有输出8KB之前,数据都是存放在缓冲区中,而不会立刻发送到客户端。当Servlet执行结束后,服务器才会去刷新流,使缓冲区中的数据发送到客户端。 +- 如果希望响应数据马上发送给客户端: +- 向流中写入大于8KB的数据; +- 调用response.flushBuffer()方法来手动刷新缓冲区; +- 字节响应流 +- 将一张图片转为字节流写入到response中 + +- 读取图片,使用commons-io包的方法。 +- byte[] image = IOUtils.toByteArray(new FileInputStream(this.getServletContext().getRealPath("/images/cat.jpeg"))); +- response.getOutputStream().write(image); + +- 3、设置响应头信息 +- 响应头是键值对 +-   可以使用response对象的setHeader()方法来设置响应头!使用该方法设置的响应头最终会发送给客户端浏览器! +- response.setHeader(“content-type”, “text/html;charset=utf-8”):设置content-type响应头,该头的作用是告诉浏览器响应内容为html类型,编码为utf-8。而且同时会设置response的字符流编码为utf-8,即response.setCharaceterEncoding(“utf-8”); +- response.setHeader("Refresh","5; URL=http://www.itcast.cn"):5秒后自动跳转到传智主页。 + +- 4、设置状态码及其他方法 +- response.setContentType("text/html;charset=utf-8"):等同\于调用 +- response.setHeader(“content-type”, “text/html;charset=utf-8”); +- response.setCharacterEncoding(“utf-8”):设置字符响应流的字符编码为utf-8; +- response.setStatus(200):设置状态码; +- response.sendError(404, “您要查找的资源不存在”):当发送错误状态码时,Tomcat会跳转到固定的错误页面去,但可以显示错误信息。 +- response.sendError(状态码) +- 5、重定向 + - 1)什么是重定向 +- 当你访问http://www.sun.com时,你会发现浏览器地址栏中的URL会变成http://www.oracle.com/us/sun/index.htm,这就是重定向了。 +- 重定向是服务器通知浏览器去访问另一个地址,即再发出另一个请求。 + + - 2)完成重定向 +- 响应码为200表示响应成功,而响应码为302表示重定向。所以完成重定向的第一步就是设置响应码为302。 +- 因为重定向是通知浏览器发出第二个请求,所以浏览器需要知道第二个请求的URL,所以完成重定向的第二步是设置Location头,指定第二个请求的URL地址。 + +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setStatus(302); + response.setHeader("Location", "http://www.itcast.cn"); + } +} + +-   上面代码的作用是:当访问AServlet后,会通知浏览器重定向到传智主页。客户端浏览器解析到响应码为302后,就知道服务器让它重定向,所以它会马上获取响应头Location,然发出第二个请求。 + + - 3)便捷的重定向方式sendRedirect +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.sendRedirect("http://www.itcast.cn"); + } +} +- response.sendRedirect()方法会设置响应头为302,以设置Location响应头。 +- 如果要重定向的URL是在同一个服务器内,那么可以使用相对路径,例如: +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.sendRedirect("/hello/BServlet"); + 注意是/项目名/路径(请求URI) + } +} +- 重定向的URL地址为:http://localhost:8080/hello/BServlet + - 4)重定向小结 +- 重定向是两次请求; +- 重定向的URL可以是其他应用,不局限于当前应用; +- 重定向的响应头为302,并且必须要有Location响应头; +- 重定向就不要再使用response.getWriter()或response.getOutputStream()输出数据,不然可能会出现异常; +- + +- Request +- 1、概述 +- request是Servlet.service()方法的一个参数,类型为javax.servlet.http.HttpServletRequest。在客户端发出每个请求时,服务器都会创建一个request对象,并把请求数据封装到request中,然后在调用Servlet.service()方法时传递给service()方法,这说明在service()方法中可以通过request对象来获取请求数据。 + +- request的功能可以分为以下几种: +- 封装了请求头数据; +- 封装了请求正文数据,如果是GET请求,那么就没有正文; +- request是一个域对象,可以把它当成Map来添加获取数据; +- request提供了请求转发和请求包含功能。 +- 2、域方法 +- request是域对象!在JavaWeb中一共四个域对象,其中ServletContext就是域对象,它在整个应用中只创建一个ServletContext对象。request其中一个,request可以在一个请求中共享数据。 +- 一个请求会创建一个request对象,如果在一个请求中经历了多个Servlet,那么多个Servlet就可以使用request来共享数据。 +- 下面是request的域方法: +- void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:servletContext.setAttribute(“xxx”, “XXX”),在request中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同; +- Object getAttribute(String name):用来获取request中的数据,当前在获取之前需要先去存储才行,例如:String value = (String)request.getAttribute(“xxx”);,获取名为xxx的域属性; +- void removeAttribute(String name):用来移除request中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做; +- Enumeration getAttributeNames():获取所有域属性的名称; + +- 3、获取请求头数据 +- request与请求头相关的方法有: +- String getHeader(String name):获取指定名称的请求头; +- Enumeration getHeaderNames():获取多值头; +- int getIntHeader(String name):获取值为int类型的请求头。 +- long getDateHeader(String name):获取值为long类型的请求头 + +- 4、获取请求相关的其它方法 +- request中还提供了与请求相关的其他方法,有些方法是为了我们更加便捷的方法请求头数据而设计,有些是与请求URL相关的方法。 +- int getContentLength():获取请求体的字节数,GET请求没有请求体,没有请求体返回-1; +- String getContentType():获取请求类型,如果请求是GET,那么这个方法返回null;如果是POST请求,那么默认为application/x-www-form-urlencoded,表示请求体内容使用了URL编码; +- String getMethod():返回请求方法,例如:GET/POST +- Locale getLocale():返回当前客户端浏览器的Locale。java.util.Locale表示国家和言语,这个东西在国际化中很有用; +- String getCharacterEncoding():获取请求编码,如果没有setCharacterEncoding(),那么返回null,表示使用ISO-8859-1编码; +- void setCharacterEncoding(String code):设置请求编码,只对请求体有效!注意,对于GET而言,没有请求体!!!所以此方法只能对POST请求中的参数有效! +- String getContextPath():返回上下文路径(/项目名),例如:/hello +- String getQueryString():返回请求URL中的参数,例如:name=zhangSan +- String getRequestURI():返回请求URI路径,例如:/hello/oneServlet +- StringBuffer getRequestURL():返回请求URL路径,例如: +- http://localhost/hello/oneServlet,即返回除了参数以外的路径信息; +- String getServletPath():返回Servlet路径,例如:/oneServlet +- String getRemoteAddr():返回当前客户端的IP地址; +- String getRemoteHost():返回当前客户端的主机名,但这个方法的实现还是获取IP地址; +- String getScheme():返回请求协议,例如:http; +- String getServerName():返回主机名,例如:localhost +- int getServerPort():返回服务器端口号,例如:8080 + +- System.out.println("IP:"+request.getRemoteAddr()); +- System.out.println("请求方式:"+request.getMethod()); +- System.out.println("User-Agent请求头:"+request.getHeader("User-Agent")); +- System.out.println("协议名:"+request.getScheme()); +- System.out.println("主机名:"+request.getServerName()); +- System.out.println("端口号:"+request.getServerPort()); +- System.out.println("项目:"+request.getContextPath()); +- System.out.println("Servlet路径:"+request.getServletPath()); +- System.out.println("请求参数:"+request.getQueryString()); +- System.out.println("URI:"+request.getRequestURI()); +- System.out.println("URL:"+request.getRequestURL()); +- ========================================================================= +- http://localhost:8080/Request&Response/RequestHeaderServlet?username=sxj&password=sxj +- ========================================================================== +- IP:127.0.0.1 +- 请求方式:GET + - User-Agent请求头:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 +- 协议名:http +- 主机名:localhost +- 端口号:8080 +- 项目:/Request&Response +- Servlet路径:/RequestHeaderServlet +- 请求参数: username=sxj&password=sxj +- URI:/Request&Response/RequestHeaderServlet(RequestURI) +- URL:http://localhost:8080/Request&Response/RequestHeaderServlet +- 整个访问的路径就等于URL+QueryString +- URL=协议名+主机名+端口号+URI +- 案例:request.getRemoteAddr():封IP +- 可以使用request.getRemoteAddr()方法获取客户端的IP地址,然后判断IP是否为禁用IP。 + String ip = request.getRemoteAddr(); + System.out.println(ip); + if(ip.equals("127.0.0.1")) { + response.getWriter().print("您的IP已被禁止!"); + } else { + response.getWriter().print("Hello!"); + } +- 案例:防盗链 +- 可以使用request.getAttribute(“Referer”)如果不是当前页面,那么属于盗链,则跳转到当前页面 +- 如果是从地址栏直接输入URL,那么Referee返回的是null +- String referer = request.getHeader("Referer"); +- System.out.println(referer); +- if(referer == null || !referer.contains("localhost")){ +- response.sendRedirect("http://www.baidu.com"); +- }else{ +- response.getWriter().print("hello");//如果是从指定页面跳转来的 +- } + +- 5、获取请求参数 +- 最为常见的客户端传递参数方式有两种: +- 浏览器地址栏直接输入:一定是GET请求; +- 超链接:一定是GET请求; +- 表单:可以是GET,也可以是POST,这取决与
的method属性值; + +- GET请求和POST请求的区别: +- GET请求: +- 请求参数会在浏览器的地址栏中显示,所以不安全; +- 请求参数长度限制长度在1K之内; +- GET请求没有请求体,无法通过request.setCharacterEncoding()来设置参数的编码; +- POST请求: +- 请求参数不会显示浏览器的地址栏,相对安全; +- 请求参数长度没有限制; +- 无论是GET|POST请求,都可以使用相同的API来获取请求参数。 +- 请求参数有一个key一个value的,也有一个key多个value的。 +- + + 超链接 +
+ + 参数1:
+ 参数2:
+ +
+ + +- 下面是使用request获取请求参数的API: +- String getParameter(String name):通过指定名称获取参数值; + + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String v1 = request.getParameter("p1"); + String v2 = request.getParameter("p2"); + System.out.println("p1=" + v1); + System.out.println("p2=" + v2); + } + + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String v1 = request.getParameter("p1"); + String v2 = request.getParameter("p2"); + System.out.println("p1=" + v1); + System.out.println("p2=" + v2); + } + +- String[] getParameterValues(String name):当多个参数名称相同时,可以使用方法来获取; +超链接 + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String[] names = request.getParameterValues("name"); + System.out.println(Arrays.toString(names)); + } +- Enumeration getParameterNames():获取所有参数的名字; +
+ 参数1:
+ 参数2:
+ +
+ public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + Enumeration names = request.getParameterNames(); + while(names.hasMoreElements()) { + System.out.println(names.nextElement()); + } + } + +- Map getParameterMap():获取所有参数封装到Map中,其中key为参数名,value为参数值,因为一个参数名称可能有多个值,所以参数值是String[],而不是String。 +超链接 + Map paramMap = request.getParameterMap(); + for(String name : paramMap.keySet()) { + String[] values = paramMap.get(name); + System.out.println(name + ": " + Arrays.toString(values)); + } +p2: [v2, vv2] +p1: [v1, vv1] + +- 示例: + +- +- 点击这里,测试GET请求
+-
+- 用户名:
+- 密 码:
+- 爱 好:吃饭
+- 睡觉
+- 编程
+- +-
+- + +- //遍历Map + +``` +public class RequestParameterServlet extends HttpServlet { +``` + + +``` + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +``` + +- Enumeration names = request.getParameterNames(); +- while(names.hasMoreElements()){ +- String name = names.nextElement(); +- System.out.println(name+"="+request.getParameter(name)); +- } +- } + +- + +``` + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +``` + +- Map map = request.getParameterMap(); +- for(Entry entry:map.entrySet()){ +- System.out.println(entry.getKey()+"="+Arrays.toString(entry.getValue())); +- } +- +- } +- } + +- username=sxj +- password=sxj +- username=[23] +- password=[123] +- hobby=[eat, sleep] +- String username = request.getParameter("username"); +- String password = request.getParameter("password"); +- String [] hobbies = request.getParameterValues("hobby"); +- System.out.println("username:"+username); +- System.out.println("password:"+password); +- System.out.println("hobbies:"+Arrays.toString(hobbies)); +- 6、请求转发和请求包含 +- 无论是请求转发还是请求包含,都表示由多个Servlet共同来处理一个请求。例如Servlet1来处理请求,然后Servlet1又转发给Servlet2来继续处理这个请求。 + + - 1)请求转发(常用) +- 在AServlet中,把请求转发到BServlet: +- 参数是Servlet路径(servlet-mapping中的url-pattern) +- 相当于/项目名 +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("AServlet"); + RequestDispatcher rd = request.getRequestDispatcher("/BServlet"); + rd.forward(request, response); + } +} +public class BServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("BServlet"); + } +} +Aservlet +BServlet +- 如果转发了,那么在原本请求的Servlet中设置的响应体是失效的。 +- 如果转发前,在原本请求的Servlet中设置的响应体超过24K,那么响应体仍有效(缓冲区溢出)。 + - 2)请求包含(不常用) +- 在AServlet中,把请求包含到BServlet: +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("AServlet"); + RequestDispatcher rd = request.getRequestDispatcher("/BServlet"); + rd.include(request, response); + } +} +public class BServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("BServlet"); + } +} +Aservlet +BServlet + + - 3)请求转发与请求包含比较 +- 如果在AServlet中请求转发到BServlet,那么在AServlet中就不允许再输出响应体,即不能再使用response.getWriter()和response.getOutputStream()向客户端输出,这一工作应该由BServlet来完成;如果是使用请求包含,那么没有这个限制; +- 请求转发虽然不能输出响应体,但还是可以设置响应头的,例如:response.setContentType(”text/html;charset=utf-8”); +- 请求包含大多是应用在JSP页面中,完成多页面的合并; +- 请求转发大多是应用在Servlet中,转发目标大多是JSP页面; + + - 4)请求转发与重定向比较 + - 1)请求转发是一个请求,而重定向是两个请求; +- 请求转发后浏览器地址栏不会有变化,而重定向会有变化,因为重定向是两个请求; + - 2)请求转发的目标只能是本应用中的资源(给出Servlet路径),重定向的目标可以是其他应用(请求URI或者其他路径); + - 3)请求转发对AServlet和BServlet的请求方法是相同的,即要么都是GET,要么都是POST,因为请求转发是一个请求; +- 重定向的第二个请求一定是GET; + - 4)请求转发效率更高;当需要地址栏发生变化时,需要使用重定向;需要在下一个Servlet中获取之前Servlet设置的域,需要使用转发 +- + +# Filter +- 什么是过滤器 +- 过滤器JavaWeb三大组件之一,它与Servlet很相似!不过过滤器是用来拦截请求的,而不是处理请求的。 +- 当用户请求某个Servlet时,会先执行部署在这个请求上的Filter,如果Filter“放行”,那么会继承执行用户请求的Servlet;如果Filter不“放行”,那么就不会执行用户请求的Servlet。 +- 其实可以这样理解,当用户请求某个Servlet时,Tomcat会去执行注册在这个请求上的Filter,然后是否“放行”由Filter来决定。可以理解为,Filter来决定是否调用Servlet!当执行完成Servlet的代码后,还会执行Filter后面的代码。 + +- 过滤器之hello world +-   其实过滤器与Servlet很相似,我们回忆一下如果写的第一个Servlet应用!写一个类,实现Servlet接口!没错,写过滤器就是写一个类,实现Filter接口。 +public class HelloFilter implements Filter { + public void init(FilterConfig filterConfig) throws ServletException {} + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + System.out.println("Hello Filter"); + } + public void destroy() {} +} + +- 第二步也与Servlet一样,在web.xml文件中部署Filter: + + helloFilter + cn.itcast.filter.HelloFilter + + + helloFilter + /*(/*表示拦截所有的访问请求) + + +- 当用户访问index.jsp页面时,会执行HelloFilter的doFilter()方法!在我们的示例中,index.jsp页面是不会被执行的,如果想执行index.jsp页面,那么我们需要放行! + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + System.out.println("filter start..."); + chain.doFilter(request, response); + System.out.println("filter end..."); + } + + +-   有很多同学总是错误的认为,一个请求在给客户端输出之后就算是结束了,这是不对的!其实很多事情都需要在给客户端响应之后才能完成! + +- + +- 过滤器的生命周期 +- init(FilterConfig):在服务器启动时会创建Filter实例,并且每个类型的Filter只创建一个实例,从此不再创建!在创建完Filter实例后,会马上调用init()方法完成初始化工作,这个方法只会被执行一次; +- doFilter(ServletRequest req,ServletResponse res,FilterChain chain):这个方法会在用户每次访问“目标资源(pattern>index.jsp
)”时执行,如果需要“放行”,那么需要调用FilterChain的doFilter(ServletRequest,ServletResponse)方法,如果不调用FilterChain的doFilter()方法,那么目标资源将无法执行; +- destroy():服务器会在创建Filter对象之后,把Filter放到缓存中一直使用,通常不会销毁它。一般会在服务器关闭时销毁Filter对象,在销毁Filter对象之前,服务器会调用Filter对象的destory()方法。 +- 单例,同Servlet一致 +- FilterConfig +- Filter接口中的init()方法的参数类型为FilterConfig类型。它的功能与ServletConfig相似,与web.xml文件中的配置信息对应。下面是FilterConfig的功能介绍: +- ServletContext getServletContext():获取ServletContext的方法; +- String getFilterName():获取Filter的配置名称;与元素对应; +- String getInitParameter(String name):获取Filter的初始化配置,与元素对应; +- Enumeration getInitParameterNames():获取所有初始化参数的名称。 + + +- FilterChain +- doFilter()方法的参数中有一个类型为FilterChain的参数,它只有一个方法:doFilter(ServletRequest,ServletResponse)。 +- 前面我们说doFilter()方法的放行,让请求流访问目标资源!但这么说不严谨,其实调用该方法的意思是,“我(当前Filter)”放行了,但不代表其他人(其他过滤器)也放行。 +- 如果当前过滤器是最后一个过滤器,那么调用chain.doFilter()方法表示执行目标资源,而不是最后一个过滤器,那么chain.doFilter()表示执行下一个过滤器的doFilter()方法。 +- 多个过滤器执行顺序 +- 一个目标资源可以指定多个过滤器,过滤器的执行顺序是在web.xml文件中的部署顺序: + + myFilter1 + cn.itcast.filter.MyFilter1 + + + myFilter1 + /index.jsp + + + myFilter2 + cn.itcast.filter.MyFilter2 + + + myFilter2 + /index.jsp + +public class MyFilter1 extends HttpFilter { + public void doFilter(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws IOException, ServletException { + System.out.println("filter1 start..."); + chain.doFilter(request, response);//放行,执行MyFilter2的doFilter()方法 + System.out.println("filter1 end..."); + } +} +public class MyFilter2 extends HttpFilter { + public void doFilter(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws IOException, ServletException { + System.out.println("filter2 start..."); + chain.doFilter(request, response);//放行,执行目标资源 + System.out.println("filter2 end..."); + } +} + + This is my JSP page.
+

index.jsp

+ <%System.out.println("index.jsp"); %> + + +- 当有用户访问index.jsp页面时,输出结果如下: +filter1 start... +filter2 start... +index.jsp +filter2 end... +filter1 end... +- AFilter B Filter CFilter +- 有点像是递归,调用filterchain的doFilter方法会放行给下一个过滤器,这个doFilter前面的代码的执行顺序是A-B-C,而后面的代码的执行顺序是C-B-A + +- 四种拦截方式 +- 我们来做个测试,写一个过滤器,指定过滤的资源为b.jsp,然后我们在浏览器中直接访问b.jsp,你会发现过滤器执行了! +- 但是,当我们在a.jsp中request.getRequestDispathcer(“/b.jsp”).forward(request,response)时,就不会再执行过滤器了!也就是说,默认情况下,只能直接访问目标资源才会执行过滤器,而forward执行目标资源,不会执行过滤器! +public class MyFilter extends HttpFilter { + public void doFilter(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + System.out.println("myfilter..."); + chain.doFilter(request, response); + } +} + + myfilter + cn.itcast.filter.MyFilter + + + myfilter + /b.jsp + + +

b.jsp

+ +

a.jsp

+ <% + request.getRequestDispatcher("/b.jsp").forward(request, response); + %> + + +- http://localhost:8080/filtertest/b.jsp -->直接访问b.jsp时,会执行过滤器内容; +- http://localhost:8080/filtertest/a.jsp --> 访问a.jsp,但a.jsp会forward到b.jsp,这时就不会执行过滤器! + +- 其实过滤器有四种拦截方式!分别是:REQUEST、FORWARD、INCLUDE、ERROR。 +- REQUEST:直接访问目标资源时执行过滤器。包括:在地址栏中直接访问、表单提交、超链接、重定向,只要在地址栏中可以看到目标资源的路径,就是REQUEST; +- FORWARD:转发访问执行过滤器。包括RequestDispatcher#forward()方法、标签都是转发访问; +- INCLUDE:包含访问执行过滤器。包括RequestDispatcher#include()方法、标签都是包含访问; +- ERROR:当目标资源在web.xml中配置为中时,并且真的出现了异常,转发到目标资源时,会执行过滤器。 + +- 可以在中添加0~n个子元素,来说明当前访问的拦截方式。 +- 如果没有写dispatcher,默认为REQUEST + + myfilter + /b.jsp + REQUEST + FORWARD + + + myfilter + /b.jsp + + + myfilter + /b.jsp + FORWARD + + +- 其实最为常用的就是REQUEST和FORWARD两种拦截方式,而INCLUDE和ERROR都比较少用!下面给出ERROR拦截方式的例子: + + myfilter + /b.jsp + ERROR + + + 500 + /b.jsp + + +

a.jsp

+ <% + if(true) + throw new RuntimeException("嘻嘻~"); + %> + + +- 过滤器的应用场景 +- 过滤器的应用场景: +- 执行目标资源之前做预处理工作,例如设置编码,这种通常都会放行,只是在目标资源执行之前做一些准备工作; +- 几乎是所有的Sevlet中都需要写request.setCharacterEndoing() 可以把它放入到一个Filter中 +- 通过条件判断是否放行,例如校验当前用户是否已经登录,或者用户IP是否已经被禁用; +- 在目标资源执行后,做一些后续的特殊处理工作,例如把目标资源输出的数据进行处理; +- 设置目标资源 +- 在web.xml文件中部署Filter时,可以通过“*”来执行目标资源: + + myfilter + /* + + + +``` +这一特性与Servlet完全相同!通过这一特性,我们可以在用户访问敏感资源时,执行过滤器,例如:/admin/*,可以把所有管理员才能访问的资源放到/admin路径下,这时可以通过过滤器来校验用户身份。 +``` + +- 还可以为指定目标资源为某个Servlet,例如: + + myservlet + cn.itcast.servlet.MyServlet + + + myservlet + /abc + + + myfilter + cn.itcast.filter.MyFilter + + + myfilter + myservlet(可以有多个) + + +-   当用户访问http://localhost:8080/filtertest/abc时,会执行名字为myservlet的Servlet,这时会执行过滤器。 +- Filter小结 +- Filter的三个方法: +- void init(FilterConfig):在Tomcat启动时被调用; +- void destroy():在Tomcat关闭时被调用; +- void doFilter(ServletRequest,ServletResponse,FilterChain):每次有请求时都调用该方法; + +- FilterConfig类:与ServletConfig相似,用来获取Filter的初始化参数 +- ServletContext getServletContext():获取ServletContext的方法; +- String getFilterName():获取Filter的配置名称; +- String getInitParameter(String name):获取Filter的初始化配置,与元素对应; +- Enumeration getInitParameterNames():获取所有初始化参数的名称。 + +- FilterChain类: +- void doFilter(ServletRequest,ServletResponse):放行!表示执行下一个过滤器,或者执行目标资源。可以在调用FilterChain的doFilter()方法的前后添加语句,在FilterChain的doFilter()方法之前的语句会在目标资源执行之前执行,在FilterChain的doFilter()方法之后的语句会在目标资源执行之后执行。 + +- 四种拦截方式:REQUEST、FORWARD、INCLUDE、ERROR,默认是REQUEST方式。 +- REQUEST:拦截直接请求方式; +- FORWARD:拦截请求转发方式; +- INCLUDE:拦截请求包含方式; +- ERROR:拦截错误转发方式。 + +- + +# Listener +# 5.6 1、监听器概述 +- 在JavaWeb被监听的事件源为:ServletContext、HttpSession、ServletRequest,即三大域对象。 +- 监听域对象“创建”与“销毁”的监听器; +- 监听域对象“操作域属性”的监听器; +- 监听HttpSession的监听器。 +- 2、创建与销毁监听器 +- 创建与销毁监听器一共有三个: +- ServletContextListener:Tomcat启动和关闭时调用下面两个方法 + +``` +public void contextInitialized(ServletContextEvent evt):ServletContext对象被创建后调用; +``` + + +``` +public void contextDestroyed(ServletContextEvent evt):ServletContext对象被销毁前调用; +``` + +- HttpSessionListener:开始会话和结束会话时调用下面两个方法 + +``` +public void sessionCreated(HttpSessionEvent evt):HttpSession对象被创建后调用; +``` + + +``` +public void sessionDestroyed(HttpSessionEvent evt):HttpSession对象被销毁前调用; +``` + +- ServletRequestListener:开始请求和结束请求时调用下面两个方法 + +``` +public void requestInitiallized(ServletRequestEvent evt):ServletRequest对象被创建后调用; +``` + + +``` +public void requestDestroyed(ServletRequestEvent evt):ServletRequest对象被销毁前调用。 +``` + + +- 事件对象 +- ServletContextEvent:ServletContext getServletContext(); +- HttpSessionEvent:HttpSession getSession(); +- ServletRequestEvent: +- ServletRequest getServletRequest() +- ServletContext getServletContext() + +- 编写测试例子: +- 编写MyServletContextListener类,实现ServletContextListener接口; +- 在web.xml文件中部署监听器; +- 为了看到session销毁的效果,在web.xml文件中设置session失效时间为1分钟; + +/* + * ServletContextListener实现类 + * contextDestroyed() -- 在ServletContext对象被销毁前调用 + * contextInitialized() -- -- 在ServletContext对象被创建后调用 + * ServletContextEvent -- 事件类对象 + * 该类有getServletContext(),用来获取ServletContext对象,即获取事件源对象 + */ +public class MyServletContextListener implements ServletContextListener { + public void contextDestroyed(ServletContextEvent evt) { + System.out.println("销毁ServletContext对象"); + } + + public void contextInitialized(ServletContextEvent evt) { + System.out.println("创建ServletContext对象"); + } +} +/* + * HttpSessionListener实现类 + * sessionCreated() -- 在HttpSession对象被创建后被调用 + * sessionDestroyed() -- -- 在HttpSession对象被销毁前调用 + * HttpSessionEvent -- 事件类对象 + * 该类有getSession(),用来获取当前HttpSession对象,即获取事件源对象 + */ +public class MyHttpSessionListener implements HttpSessionListener { + public void sessionCreated(HttpSessionEvent evt) { + System.out.println("创建session对象"); + } + + public void sessionDestroyed(HttpSessionEvent evt) { + System.out.println("销毁session对象"); + } +} + +/* + * ServletRequestListener实现类 + * requestDestroyed() -- 在ServletRequest对象被销毁前调用 + * requestInitialized() -- 在ServletRequest对象被创建后调用 + * ServletRequestEvent -- 事件类对象 + * 该类有getServletContext(),用来获取ServletContext对象 + * 该类有getServletRequest(),用来获取当前ServletRequest对象,即事件源对象 + */ +public class MyServletRequestListener implements ServletRequestListener { + public void requestDestroyed(ServletRequestEvent evt) { + System.out.println("销毁request对象"); + } + + public void requestInitialized(ServletRequestEvent evt) { + System.out.println("创建request对象"); + } +} + + +cn.itcast.listener.MyServletContextListener + + +cn.itcast.listener.MyHttpSessionListener + + +cn.itcast.listener.MyServletRequestListener + + + 1 + + +- 3、操作域属性的监听器 +- 当对域属性进行增、删、改时,执行的监听器一共有三个: +- ServletContextAttributeListener:在ServletContext域进行增、删、改属性时调用下面方法。 + +``` +public void attributeAdded(ServletContextAttributeEvent evt) +``` + + +``` +public void attributeRemoved(ServletContextAttributeEvent evt) +``` + + +``` +public void attributeReplaced(ServletContextAttributeEvent evt) +``` + +- HttpSessionAttributeListener:在HttpSession域进行增、删、改属性时调用下面方法 + +``` +public void attributeAdded(HttpSessionBindingEvent evt) +``` + + +``` +public void attributeRemoved (HttpSessionBindingEvent evt) +``` + + +``` +public void attributeReplaced (HttpSessionBindingEvent evt) +``` + +- ServletRequestAttributeListener:在ServletRequest域进行增、删、改属性时调用下面方法 + +``` +public void attributeAdded(ServletRequestAttributeEvent evt) +``` + + +``` +public void attributeRemoved (ServletRequestAttributeEvent evt) +``` + + +``` +public void attributeReplaced (ServletRequestAttributeEvent evt) +``` + + +- 下面对这三个监听器的事件对象功能进行介绍: +- ServletContextAttributeEvent +- String getName():获取当前操作的属性名; +- Object getValue():获取当前操作的属性值; +- ServletContext getServletContext():获取ServletContext对象。 +- HttpSessionBindingEvent +- String getName():获取当前操作的属性名; +- Object getValue():获取当前操作的属性值; +- HttpSession getSession():获取当前操作的session对象。 +- ServletRequestAttributeEvent +- String getName():获取当前操作的属性名; +- Object getValue():获取当前操作的属性值; +- ServletContext getServletContext():获取ServletContext对象; +- ServletRequest getServletRequest():获取当前操作的ServletRequest对象。 + +- 如果是替换,那么getValue返回的是原值;如果想拿到新值,那么去对应的域对象中取。 +- 4、HttpSession的监听器 +- 还有两个与HttpSession相关的特殊的监听器,这两个监听器的特点如下: +- 不用在web.xml文件中部署; +- 这两个监听器不是给session添加,而是给Bean添加。即让Bean类实现监听器接口,然后再把Bean对象添加到session域中。 + +- 下面对这两个监听器介绍一下: +- HttpSessionBindingListener:当某个类实现了该接口后,可以感知本类对象添加到session中,以及感知从session中移除。例如让Person类实现HttpSessionBindingListener接口,那么当把Person对象添加到session中,或者把Person对象从session中移除时会调用下面两个方法: + +``` +public void valueBound(HttpSessionBindingEvent event):当把监听器对象添加到session中会调用监听器对象的本方法; +``` + + +``` +public void valueUnbound(HttpSessionBindingEvent event):当把监听器对象从session中移除时会调用监听器对象的本方法; +``` + +- 这里要注意,HttpSessionBindingListener监听器的使用与前面介绍的都不相同,当该监听器对象添加到session中,或把该监听器对象从session移除时会调用监听器中的方法。并且无需在web.xml文件中部署这个监听器。 +- 示例步骤: +- 编写Person类,让其实现HttpSessionBindingListener监听器接口; +- 编写Servlet类,一个方法向session中添加Person对象,另一个从session中移除Person对象; +- 在index.jsp中给出两个超链接,分别访问Servlet中的两个方法。 +- Pseron.java +public class Person implements HttpSessionBindingListener { + private String name; + private int age; + private String sex; + + public Person(String name, int age, String sex) { + super(); + this.name = name; + this.age = age; + this.sex = sex; + } + + public Person() { + super(); + } + + public String toString() { + return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]"; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } + + public void valueBound(HttpSessionBindingEvent evt) { + System.out.println("把Person对象存放到session中:" + evt.getValue()); + } + + public void valueUnbound(HttpSessionBindingEvent evt) { + System.out.println("从session中移除Pseron对象:" + evt.getValue()); + } +} + +- ListenerServlet.java +public class ListenerServlet extends BaseServlet { + public String addPerson(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + Person p = new Person("zhangSan", 23, "male"); + request.getSession().setAttribute("person", p); + return "/index.jsp"; + } + + public String removePerson(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + request.getSession().removeAttribute("person"); + return "/index.jsp"; + } + +- index.jsp + + addPerson +
+ removePerson +
+ + +- Session有一种重生的性质。 +- 在服务器开启时保存了session,然后当服务器被关闭时,会在硬盘上保存一个session属性的序列化文件。 +- 启动服务器时会重新将该序列化文件读入内存,并删除该文件。 +- 在conf/context.xml中进行一些配置可以取消保存session序列化文件的操作。 +- HttpSessionActivationListener:Tomcat会在session不被使用时钝化session对象,所谓钝化session,就是把session通过序列化的方式保存到硬盘文件中。当用户再使用session时,Tomcat还会把钝化的对象再活化session,所谓活化就是把硬盘文件中的session在反序列化回内存。当session被Tomcat钝化时,session中存储的对象也被钝化,当session被活化时,也会把session中存储的对象活化。如果某个类实现了HttpSessionActiveationListener接口后,当对象随着session被钝化和活化时,下面两个方法就会被调用: + +``` +public void sessionWillPassivate(HttpSessionEvent se):当对象感知被活化时调用本方法; +``` + + +``` +public void sessionDidActivate(HttpSessionEvent se):当对象感知被钝化时调用本方法; +``` + +- HttpSessionActivationListener监听器与HttpSessionBindingListener监听器相似,都是感知型的监听器,例如让Person类实现了HttpSessionActivationListener监听器接口,并把Person对象添加到了session中后,当Tomcat钝化session时,同时也会钝化session中的Person对象,这时Person对象就会感知到自己被钝化了,其实就是调用Person对象的sessionWillPassivate()方法。当用户再次使用session时,Tomcat会活化session,这时Person会感知到自己被活化,其实就是调用Person对象的sessionDidActivate()方法。 +- 注意JavaBean同时需要实现Serializable接口。 +- 注意,因为钝化和活化session,其实就是使用序列化和反序列化技术把session从内存保存到硬盘,和把session从硬盘加载到内存。这说明如果Person类没有实现Serializable接口,那么当session钝化时就不会钝化Person,而是把Person从session中移除再钝化!这也说明session活化后,session中就不再有Person对象了。 +- + +# Cookie(服务器创建,客户端保存) +- Cookie如果我不想js或http读,应该设置什么属性?里面有个属性httpOnly,这个属性你了解吗? +# 5.7 1、Cookie概述 +- 1.1 什么叫Cookie +- Cookie翻译成中文是小甜点,小饼干的意思。在HTTP中它表示服务器送给客户端浏览器的小甜点。由HTTP协议制定。其实Cookie就是一个键和一个值构成的,随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。 + +-   Cookie是由服务器创建,然后通过响应发送给客户端的一个键值对。客户端会保存Cookie,并会标注出Cookie的来源(哪个服务器的Cookie)。当客户端向服务器发出请求时会把所有这个服务器Cookie包含在请求中发送给服务器,这样服务器就可以识别客户端了! +- 1.2 Cookie规范 +- Cookie大小上限为4KB; +- 一个服务器最多在客户端浏览器上保存20个Cookie; +- 一个浏览器最多保存300个Cookie; +- 上面的数据只是HTTP的Cookie规范,但在浏览器大战的今天,一些浏览器为了打败对手,为了展现自己的能力起见,可能对Cookie规范“扩展”了一些,例如每个Cookie的大小为8KB,最多可保存500个Cookie等!但也不会出现把你硬盘占满的可能! +- 注意,不同浏览器之间是不共享Cookie的。也就是说在你使用IE访问服务器时,服务器会把Cookie发给IE,然后由IE保存起来,当你在使用FireFox访问服务器时,不可能把IE保存的Cookie发送给服务器。 +- 1.3 Cookie与HTTP头 +- Cookie是通过HTTP请求头和响应头在客户端和服务器端传递的: +- Cookie:请求头,客户端发送给服务器端; +- 格式:Cookie: a=A; b=B; c=C。即多个Cookie用分号分开; +- 一个Cookie对应多个键值对 +- Set-Cookie:响应头,服务器端发送给客户端; +- 一个Cookie对象一个Set-Cookie,一个键值对 +- Set-Cookie: a=A +- Set-Cookie: b=B +- Set-Cookie: c=C +- 比如可以用response.addHeader(“Set-Cookie”,”a=A”);发送一个Cookie +- 便捷的使用Cookie方式: + + +- HttpServletRequest: + +- 没有返回null +- HttpServletResponse: + +- 1.4 Cookie的覆盖 +-   如果服务器端发送重复的Cookie那么会覆盖原有的Cookie,例如客户端的第一个请求服务器端发送的Cookie是:Set-Cookie: a=A;第二请求服务器端发送的是:Set-Cookie: a=AA,那么客户端只留下一个Cookie,即:a=AA。 +- 2、Cookie的生命周期 +-   Cookie不只是有name和value,Cookie还是生命。所谓生命就是Cookie在客户端的有效时间,可以通过setMaxAge(int)来设置Cookie的有效时间。 + - cookie.setMaxAge(-1):cookie的maxAge属性的默认值就是-1,表示只在浏览器内存中存活。一旦关闭浏览器窗口,那么cookie就会消失。 +- cookie.setMaxAge(60*60):表示cookie对象可存活1小时。当生命大于0时,浏览器会把Cookie保存到硬盘上,就算关闭浏览器,就算重启客户端电脑,cookie也会存活1小时; +- cookie.setMaxAge(0):cookie生命等于0是一个特殊的值,它表示cookie被作废!也就是说,如果原来浏览器已经保存了这个Cookie,那么可以通过Cookie的setMaxAge(0)来删除这个Cookie。无论是在浏览器内存中,还是在客户端硬盘上都会删除这个Cookie。 +- 3、Cookie的path(服务器的路径) +- 3.1 什么是Cookie的路径 +- 现在有WEB应用A,向客户端发送了10个Cookie,这就说明客户端无论访问应用A的哪个Servlet都会把这10个Cookie包含在请求中!但是也许只有AServlet需要读取请求中的Cookie,而其他Servlet根本就不会获取请求中的Cookie。这说明客户端浏览器有时发送这些Cookie是多余的! +- 可以通过设置Cookie的path来指定浏览器,在访问什么样的路径时,包含什么样的Cookie。 +- 3.2 Cookie路径与请求路径的关系 +- 下面我们来看看Cookie路径的作用: +- 下面是客户端浏览器保存的3个Cookie的路径: +- a: /cookietest; +- b: /cookietest/servlet; +- c: /cookietest/jsp; +- 下面是浏览器请求的URL: +- A: http://localhost:8080/cookietest/AServlet; +- B: http://localhost:8080/cookietest/servlet/BServlet; +- C: http://localhost:8080/cookietest/jsp/a.jsp; + +- 请求A时,会在请求中包含a; +- 请求B时,会在请求中包含a、b; +- 请求C时,会在请求中包含a、c; + +- 请求路径如果包含了Cookie路径,那么会在请求中包含这个Cookie。 +- A请求的URL包含了“/cookietest”,所以会在请求中包含路径为“/cookietest”的Cookie; +- B请求的URL包含了“/cookietest”,以及“/cookietest/servlet”,所以请求中包含路径为“/cookietest”和“/cookietest/servlet”两个Cookie; +- C请求的URL包含了“/cookietest”,以及“/cookietest/jsp”,所以请求中包含路径为“/cookietest”和“/cookietest/jsp”两个Cookie; +- 3.3 设置Cookie的路径 +- 设置Cookie的路径需要使用setPath()方法,例如: +- cookie.setPath(“/cookietest/servlet”); +- 默认路径是访问资源的上一级路径 +- 如果没有设置Cookie的路径,那么Cookie路径的默认值当前访问资源所在路径(上一级),例如: +- 访问http://localhost:8080/cookietest/AServlet时添加的Cookie默认路径为/cookietest; +- 访问http://localhost:8080/cookietest/servlet/BServlet时添加的Cookie默认路径为/cookietest/servlet; +- 访问http://localhost:8080/cookietest/jsp/BServlet时添加的Cookie默认路径为/cookietest/jsp; + +- 4、Cookie的domain +- Cookie的domain属性可以让网站中二级域共享Cookie,次要! +- 百度你是了解的对吧! +- http://www.baidu.com +- http://zhidao.baidu.com +- http://news.baidu.com +- http://tieba.baidu.com +- 现在我希望在这些主机之间共享Cookie(例如在www.baidu.com中响应的cookie,可以在news.baidu.com请求中包含)。很明显,现在不是路径的问题了,而是主机的问题,即域名的问题。处理这一问题其实很简单,只需要下面两步: +- 设置Cookie的path为“/”:c.setPath(“/”); +- 设置Cookie的domain为“.baidu.com”:c.setDomain(“.baidu.com”)。 +- 5、Cookie中保存中文 +- Cookie的name和value都不能使用中文,如果希望在Cookie中使用中文,那么需要先对中文进行URL编码,然后把编码后的字符串放到Cookie中。 +-  向客户端响应中添加Cookie + String name = URLEncoder.encode("姓名", "UTF-8"); + String value = URLEncoder.encode("张三", "UTF-8"); + Cookie c = new Cookie(name, value); + c.setMaxAge(3600); + response.addCookie(c); + +- 从客户端请求中获取Cookie + response.setContentType("text/html;charset=utf-8"); + Cookie[] cs = request.getCookies(); + if(cs != null) { + for(Cookie c : cs) { + String name = URLDecoder.decode(c.getName(), "UTF-8"); + String value = URLDecoder.decode(c.getValue(), "UTF-8"); + String s = name + ": " + value + "
"; + response.getWriter().print(s); + } + } +# 5.8 6、Cookie中的属性 +## HttpOnly +- 如果Cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。 +- Servlet3.0支持setHttpOnly(boolean httpOnly)。 +- 非servlet3.0的JAVAEE项目也可以通过设置Header进行设置: +- response.setHeader("Set-Cookie", "cookiename=value; Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly"); +## max-age +- expires/Max-Age 字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。 +## secure +- 设置是否只能通过https来传递此条cookie +- + +- Session(服务器创建,服务器保存) +- session默认过期时间,过长会怎么样:30min,过长占用内存 +- 如何防止session被别人伪造cookie得到:Cookie的值后面跟上一段防篡改的验证串。防篡改的验证串可以由DES(cookie的内容+盐值),也可以用 MD5(cookie内容+密钥),。也可以是SHA1(cookie内容+密钥),这里的密钥只有站点本身知道,如果这个都泄漏了那就真完蛋了。这个值在服务器接收到Cookie以后,就可以用Cookie的内容+密钥重新计算一次验证串,和提交上来的做比对,如果是一致的,我们就认为cookie没有被篡改,反之,cookie肯定被篡改过,我们就不要相信这一次提交。如果所有的Cookie都经过防篡改验证,那么也就不用担心SessionID被冒名顶替的事情发生了。 +- 如果服务器的负载量很高内存负荷不住要怎么办:尽量规避使用,优先以Cookie(包括加密Cookie)来解决;Session要自己实现分布式储存,比如使用 redis 等做储存。 +- 1、HttpSession概述(属于JavaWeb) +- 1.1 什么是HttpSesssion +- javax.servlet.http.HttpSession接口表示一个会话,我们可以把一个会话内需要共享的数据保存到HttSession对象中! +- 会话:会话范围是某个用户从首次访问服务器开始,到该用户关闭浏览器结束。 +- 1.2 获取HttpSession对象 +- HttpSession request.getSesssion():如果当前会话已经有了session对象那么直接返回,如果当前会话还不存在会话,那么创建session并返回; +- HttpSession request.getSession(boolean):当参数为true时,与requeset.getSession()相同。如果参数为false,那么如果当前会话中存在session则返回,不存在返回null; +- JSP中session是一个内置对象,可以直接使用 +- 1.3 HttpSession是域对象 +- 我们已经学习过HttpServletRequest、ServletContext,它们都是域对象,现在我们又学习了一个HttpSession,它也是域对象。它们三个是Servlet中可以使用的域对象, +- 而JSP中可以多使用一个域对象PageContext。 +- HttpServletRequest:一个请求创建一个request对象,所以在同一个请求中可以共享request,例如一个请求从AServlet转发到BServlet,那么AServlet和BServlet可以共享request域中的数据; +- ServletContext:一个应用只创建一个ServletContext对象,所以在ServletContext中的数据可以在整个应用中共享,只要不关闭服务器,那么ServletContext中的数据就可以共享; +- HttpSession:一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据; + +- 下面是session的域方法: +- void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:session.setAttribute(“xxx”, “XXX”),在session中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同; +- Object getAttribute(String name):用来获取session中的数据,当前在获取之前需要先去存储才行,例如:String value = (String) session.getAttribute(“xxx”);,获取名为xxx的域属性; +- void removeAttribute(String name):用来移除HttpSession中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做; +- Enumeration getAttributeNames():获取所有域属性的名称; + +- 2、登录案例 +- 需要的页面: +- login.jsp:登录页面,提供登录表单; +- succ1.jsp:主页,显示当前用户名称,如果没有登录,显示您还没登录; +- succ2.jsp:同上 + +- Servlet: +- LoginServlet:在login.jsp页面提交表单时,请求本Servlet。在本Servlet中获取用户名、密码进行校验,如果用户名、密码错误,显示“用户名或密码错误”,如果正确保存用户名session中,然后重定向到succ1.jsp; +- 保存用户名到session的目的是保存登录状态 +-   当用户没有登录时访问succ1.jsp或succ2.jsp,保存错误信息“您还没有登录”,并转发给login.jsp。如果用户在login.jsp登录成功后到达succ1.jsp页面会显示当前用户名,而且不用再次登录去访问succ2.jsp也会显示用户名。因为多次请求在一个会话范围,succ1.jsp和succ2.jsp都会到session中获取用户名,session对象在一个会话中是相同的,所以都可以获取到用户名! + +- 用户名的自动填充:如果曾经登录过(session中保存有username),则将该username保存到Cookie中,发送给客户端。如果再次访问login.jsp,则将username自动填充。 +- 注意要设置Cookie的生命周期 + +- 代码: +- login.jsp + +- +- <% +- String username = ""; +- Cookie[] cookies = request.getCookies(); +- if (cookies != null) { +- for (Cookie c : cookies) { +- if (c.getName().equals("username")) { +- username = c.getValue(); +- } +- } +- } +- %> +-
+- username:
+- password:
+-
+- <% +- String status = (String) request.getAttribute("status"); +- if (status != null) { +- out.print(status); +- } +- %> +- +- LoginServlet + +``` +public class LoginServlet extends HttpServlet { +``` + + + +``` + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +``` + +- request.setCharacterEncoding("utf-8"); +- String username = request.getParameter("username"); +- String password = request.getParameter("password"); +- if (username.equals("admin") && password.equals("admin")) { +- // 使用session保存信息,可以使用重定向,因为处于同一会话 +- // 避免Servlet的路径出现在地址栏中 +- +- //保存一个session的域 +- request.getSession().setAttribute("username", username); +- //保存一个cookie +- Cookie cookie = new Cookie("username", username); + - cookie.setMaxAge(60*60*24);//一天 +- response.addCookie(cookie); +- response.sendRedirect("/Session/jsp/succ1.jsp"); +- } else { +- // 使用request保存信息,只能使用转发,因为处于同一请求 +- request.setAttribute("status", "用户名或密码错误"); +- request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); +- } +- } +- } +- succ1.jsp +- +- <% +- String username = (String)session.getAttribute("username"); +- if(username != null){ +- out.print("欢迎您,"+username+"!"); +- }else{ +- request.setAttribute("status", "您尚未登录"); +- request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); +- } +- %> +- + +- + +- 3、session的实现原理 +- session底层是依赖Cookie的!我们来理解一下session的原理吧! +- 当首次使用session时,服务器端要创建session,session是保存在服务器端,而给客户端的session的id(一个cookie中保存了sessionId)。客户端带走的是sessionId,而数据是保存在session中。 +- 当客户端再次访问服务器时,在请求中会带上sessionId,而服务器会通过sessionId找到对应的session,而无需再创建新的session。 +- 如果Servlet中没有创建session,那么响应头中就不会有sessionid的cookie。但是所有的JSP页面中,session可以直接使用,也就是说每个JSP页面都会自动获取session,无论是否使用,访问JSP页面一定会带回来一个sessionid。 + +- + +- 4、session与浏览器 +- session保存在服务器,而sessionId通过Cookie发送给客户端,但这个Cookie的生命不是-1,即只在浏览器内存中存在,也就是说如果用户关闭了浏览器,那么这个Cookie就丢失了。 +- 当用户再次打开浏览器访问服务器时,就不会有sessionId发送给服务器,那么服务器会认为你没有session,所以服务器会创建一个session,并在响应中把sessionId中到Cookie中发送给客户端。      +- 你可能会说,那原来的session对象会怎样?当一个session长时间没人使用的话,服务器会把session删除了!这个时长在Tomcat中配置是30分钟,可以在${CATALANA}/conf/web.xml找到这个配置,当然你也可以在自己的web.xml中覆盖这个配置! +- web.xml + + 30 + + +- session失效时间也说明一个问题!如果你打开网站的一个页面开始长时间不动,超出了30分钟后,再去点击链接或提交表单时你会发现,你的session已经丢失了! +- 5、session其他常用API +- String getId():获取sessionId;32位长,16进制字符串,使用UUID生成随机字符串; +- int getMaxInactiveInterval():获取session可以的最大不活动时间(秒),默认为30分钟。当session在30分钟内没有使用,那么Tomcat会在session池中移除这个session; +- void setMaxInactiveInterval(int interval):设置session允许的最大不活动时间(秒),如果设置为1秒,那么只要session在1秒内不被使用,那么session就会被移除; +- long getCreationTime():返回session的创建时间,返回值为当前时间的毫秒值; +- long getLastAccessedTime():返回session的最后活动时间,返回值为当前时间的毫秒值; +- void invalidate():让session失效!调用这个方法会被session失效,当session失效后,客户端再次请求,服务器会给客户端创建一个新的session,并在响应中给客户端新session的sessionId;(比如退出按钮,退出时让session失效) +- boolean isNew():查看session是否为新。当客户端第一次请求时,服务器为客户端创建session,但这时服务器还没有响应客户端,也就是还没有把sessionId响应给客户端时,这时session的状态为新。请求中没有sessionid的Cookie +- request.getSession().isNew()可以判断这个Session是新的还是旧的。 +- 6、URL重写 +- 我们知道session依赖Cookie,那么session为什么依赖Cookie呢?因为服务器需要在每次请求中获取sessionId,然后找到客户端的session对象。那么如果客户端浏览器关闭了Cookie呢?那么session是不是就会不存在了呢? +- 其实还有一种方法让服务器收到的每个请求中都带有sessioinId,那就是URL重写!在每个页面中的每个链接和表单中都添加名为jSessionId的参数,值为当前sessionid。当用户点击链接或提交表单时也服务器可以通过获取jSessionId这个参数来得到客户端的sessionId,找到sessoin对象。 +- index.jsp + +

URL重写

+主页 + +
+ +
+ + +- 也可以使用response.encodeURL()对每个请求的URL处理,这个方法会自动追加jsessionid参数,与上面我们手动添加是一样的效果。 +主页 + +
+ +
+ +-   使用response.encodeURL()更加“智能”,它会判断客户端浏览器是否禁用了Cookie,如果禁用了,那么这个方法在URL后面追加jsessionid,否则不会追加。 +- + +# AsyncServlet&Reactor +# 5.9 API +- Servlet异步处理就是让Servlet在处理费时的请求时不要阻塞,而是一部分一部分的显示。 +- 也就是说,在使用Servlet异步处理之后,页面可以一部分一部分的显示数据,而不是一直卡,等到请求响应结束后一起显示。 +- 在使用异步处理之前,一定要在@WebServlet注解中给出asyncSupported=true,不然默认Servlet是不支持异步处理的。如果存在过滤器,也要设置@WebFilter的asyncSupportedt=true。 +@WebServlet(urlPatterns = {"/MyServlet"}, asyncSupported=true) +public class MyServlet extends HttpServlet {…} + +-   注意,响应类型必须是text/html,所以:response.setContentType(“text/html;charset=utf-8”); + +- 使用异步处理大致可以分为两步: +- Servlet正常响应数据; +- Servlet异常响应数据。 + +- 在Servlet正常响应数据时,没什么可说的,可通知response.getWriter().print()来向客户端输出,但输出后要使用response.getWriter().flush()刷新,不然数据只是在缓冲区中,不能向客户端发送数据的。 +- 异步响应数据需要使用request.startAsync()方法获取AsyncContext对象。然后调用AsyncContext对象的start()方法启动异步响应,start()方法需要一个Runnable类型的参数。在Runnable的run()方法中给出异步响应的代码。 +AsyncContext ac = request.startAsyncContext(request, response); +ac.start(new Runnable() {…}); + +- 注意在异步处理线程中使用response做响应后,要使用response.getWriter().flush()来刷新流,不然数据是不能响应到客户端浏览器的。 + + asyncContext.start(new Runnable() { + public void run() { + for(char i = 'a'; i <= 'z'; i++) { + try { + Thread.sleep(100); + asyncContext.getResponse().getWriter().print(i + " "); + asyncContext.getResponse().getWriter().flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + asyncContext.complete(); + } + }); + +- Tomcat需要知道异步响应是否结束,如果响应不结束,虽然客户端浏览器会看到响应的数据,但是鼠标上只是有个圈圈的不行的转啊转的,表示还没有结束响应。Tomcat会等待到超时为止,这个超时的时间可以通过AsyncContext类的getTimeout()方法获取,Tomcat默认为20000毫秒。当然也可以通过setTimeOut()方法设置,以毫秒为单位。ac.setTimeout(1000*10)。 +- 如果异步线程已经结束了响应,那么可以在异步线程中调用AsyncContext.complete()方法,这样Tomcat就知道异步线程已经完成了工作了。 +@WebServlet(urlPatterns = {"/AServlet"}, asyncSupported=true) +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/html;charset=utf-8"); + PrintWriter out = response.getWriter(); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + out.println("Servlet begin
"); + + out.flush(); + final AsyncContext asyncContext = request.startAsync(request, response); + asyncContext.setTimeout(1000 * 20); + asyncContext.start(new Runnable() { + public void run() { + try { + Thread.sleep(1000); + asyncContext.getResponse().getWriter().print("马上开始" + "
"); + asyncContext.getResponse().getWriter().flush(); + Thread.sleep(2000); + } catch (Exception e1) { + } + for(char i = 'a'; i <= 'z'; i++) { + try { + Thread.sleep(100); + asyncContext.getResponse().getWriter().print(i + " "); + asyncContext.getResponse().getWriter().flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + asyncContext.complete(); + } + }); + // asyncContext.start(businessHandleThread); + // 也可以用这种方法启动异步线程 + out.println("Servlet end
"); + } +} + +- + +- 模型 +- 在引入Servlet3之前我们的线程模型是如下样子的: + + +- 整个请求解析、业务处理、生成响应都是由Tomcat线程池进行处理,而且都是在一个线程中处理;不能分离线程处理;比如接收到请求后交给其他线程处理,这样不能灵活定义业务处理模型。 + +- 引入Servlet3之后,我们的线程模型可以改造为如下样子: + +- 此处可以看到请求解析使用Tomcat单线程;而解析完成后会扔到业务队列中,由业务线程池进行处理;这种处理方式可以得到如下好处: +- 1、根据业务重要性对业务进行分级,然后根据分级定义线程池; +- 2、可以拿到业务线程池,可以进行很多的操作,比如监控、降级等。 + +- 分级线程池 + + +- 好处 +- 更高的并发能力; +- 请求解析和业务处理线程池分离; +- 根据业务重要性对业务分级,并分级线程池; +- 对业务线程池进行监控、运维、降级等处理。 + +- 总结 +- 通过异步化我们不会获得更快的响应时间,但是我们获得了整体吞吐量和我们需要的灵活性:请求解析和业务处理线程池分离;根据业务重要性对业务分级,并分级线程池;对业务线程池进行监控、运维、降级等处理。 +# 5.10 与SpringWebFlux +- Servlet3.1 规范其中一个新特性是异步处理支持。 +- 异步处理支持:Servlet 线程不需一直阻塞,即不需要到业务处理完毕再输出响应,然后结束 Servlet线程。异步处理的作用是在接收到请求之后,Servlet 线程可以将耗时的操作委派给另一个线程来完成,在不生成响应的情况下返回至容器。主要应用场景是针对业务处理较耗时的情况,可以减少服务器资源的占用,并且提高并发处理速度。 +- 所以 WebFlux 支持的容器有 Tomcat、Jetty(Non-Blocking IO API) ,也可以像 Netty 和 Undertow 的本身就支持异步容器。在容器中 Spring WebFlux 会将输入流适配成 Mono 或者 Flux 格式进行统一处理。 +# 5.11 Reactor +- Reactive Streams(以下简称为RS)是“一种规范,它为基于非阻塞回压的异步流处理提供了标准”。它是一组包含了TCK工具套件和四个简单接口(Publisher、Subscriber、Subscription和Processor)的规范,这些接口将被集成到Java 9. +- Reactor不同于其它框架的最关键一点就是RS。Flux和Mono这两者都是RS的Publisher实现,它们都具备了响应式回压的特点。 +- Reactive Streams的主流实现有RxJava和Reactor,Spring WebFlux默认集成的是Reactor。 +## Flux 和 Mono +- Flux 和 Mono 是 Reactor 中的两个基本概念。Flux 表示的是包含 0 到 N 个元素的异步序列。在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 onNext(), onComplete()和 onError()会被调用。Mono 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。Flux 和 Mono 之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。 + +- + +# JDBC +- JDBC的桥接设计模式 jdbc的详细链接过程;底层源码;在Java中调用存储过程的方法 + +- 为什么要使用JDBC + +- JDBC可以跨数据库平台 +- 系统可以 小规模时使用MySQL 规模变大时使用Oracle +- 如果不需要改动API函数,就需要分层。 + + +- ODBC统一与数据库的接口,ADO是.NET统一与数据库的接口 +- JDBC是java与数据库统一的接口 +- 但是注意JDBC还是要求将SQL语句插入到代码中,而SQL语句各数据库有所不同,所以不能完全实现移植。如果要实现完全的移植,需要使用Hibernate技术 +- Hibernate将不同数据库微小的区别也屏蔽掉了 +- EJB也实现了屏蔽数据库间微小的区别 + +- +- JDBC对于java的接口是一致,但对于不同数据库JDBC所用的类库是不同的,对于程序猿而言是透明的。 + +- JDBC编程步骤 + +- 先需要找JDBC的类库 java.sql +- Driver 驱动,用来提供给JDBC连接到数据库 +- java并不知道使用的是哪种数据库,而java有一个管理数据库的管家DriverManager,它来管理使用哪种数据库的连接。 +- 连接到某个数据库,需要先向DriverManager注册。然后通过DriverManager连接到数据库。 + +- 代码示例: +- import java.sql.*; + +``` +public class TestJDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection conn = null; +- Statement stmt = null; +- ResultSet rs = null; +- try { +- Class.forName("com.mysql.jdbc.Driver"); +- conn = DriverManager.getConnection("jdbc:mysql://localhost/mydata?user=root&password=130119"); +- stmt = conn.createStatement(); +- rs = stmt.executeQuery("select * from emp"); +- while(rs.next()){ +- System.out.println(rs.getString("deptno")); +- } +- } catch (SQLException | InstantiationException | IllegalAccessException | ClassNotFoundException e) { +- e.printStackTrace(); +- }finally{ +- try{ +- if(rs != null){ +- rs.close(); +- rs = null; +- } +- if(stmt != null ){ +- stmt.close(); +- stmt= null; +- } +- if(conn != null){ +- conn.close(); +- conn = null; +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- } +- } +- +- } +- } +- + +- 总结:JDBC连接流程 +1.- 注册:驱动实例化 Class forName() +2.- 连接:获取连接 Connection DriverManager getConnection() +3.- 执行:通过连接执行SQL语句 Statement createStatement() executeQuery() +4.- 接收并输出:循环遍历取出数据 ResultSet next() getString() +5.- 关闭:close 先打开的后关闭 close() + +- 除此之外,还需要抓Exception,并将关闭的代码放到finally里面 +- 关闭前先判断是否为null,如果为null那么关闭时会出错 +- 在close中也会抛Exception,也要写try catch + +- + +# JDBCUtils +- import java.io.IOException; +- import java.util.Properties; +- import java.sql.*; + + +``` +public final class JDBCUtils { +``` + + +``` + private static String driver; +``` + + +``` + private static String url; +``` + + +``` + private static String username; +``` + + +``` + private static String password; +``` + + + +``` + private JDBCUtils() { +``` + +- } + +- static { + +- Properties props = new Properties(); +- try { +- props.load(JDBCUtils.class.getClassLoader().getResourceAsStream( +- "dbinfo.properties")); +- driver = props.getProperty("driver"); +- url = props.getProperty("url"); +- username = props.getProperty("username"); +- password = props.getProperty("password"); +- } catch (IOException e) { +- throw new ExceptionInInitializerError(e); +- } + +- try { +- Class.forName(driver); +- } catch (ClassNotFoundException e) { +- throw new ExceptionInInitializerError(e); +- } +- } + + +``` + public static Connection getConn() { +``` + +- Connection conn = null; +- try { +- conn = DriverManager.getConnection(url, username, password); +- } catch (SQLException e) { +- e.printStackTrace(); +- } +- return conn; +- } + + +``` + public static void free(ResultSet rs, Statement stmt, Connection conn) { +``` + +- try { +- if (rs != null) { +- rs.close(); +- rs = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally { +- try { +- if (stmt != null) { +- stmt.close(); +- stmt = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally { +- try { +- if (conn != null) { +- conn.close(); +- conn = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } +- } +- } +- } +- } + +- + +- 实际调用的代码: +- import java.sql.*; + + +``` +public class TestTemplate { +``` + + +``` + public static void main(String[] args) { +``` + +- read(); +- } + + +``` + public static void read(){ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- rs = stmt.executeQuery("select * from dept"); +- while(rs.next()){ +- System.out.println(rs.getString("deptno")); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, stmt, c); +- } +- } +- } + +- 以下可以忽略 +- 工具类可以采用单例模式 + +``` +类中有一个静态私有的实例,有一个公开的静态的获取该实例的方法,其他方法是public但不是静态的 +``` + +- 当其他的类要调用该工具类的方法时,先获取该类的实例,再调用其方法 + +- 还可以采用延迟加载,当第一次使用该类时new出实例;一开始是没有实例的成员变量的 +- 以后使用该类就不会再new第二个实例了。 +- 构造实例的成本很高时多采用该方法 + + + +- 还可以再完善,并发(多线程)控制时将new实例的部分加锁,避免出现两个实例 +- 永远保持单个实例 +- 这是双重检查 + +- + +- DML增删改 +- 注意sql语句不建议用*,并且get获取值时不应该写1,2 ;写为字段名可读性更高 +- 最好将sql语句中列出所需要的字段,全部取出效率较低 + +- 这是为了降低维护成本 +- executeUpdate 返回值是int类型,是执行成功的影响行数 + +- 注意关闭执行DML语句的资源也可以使用封装的free方法,因为会判断是否为空,空即不关闭 + +- 注意与之前步骤不同,不需要ResultSet这个类,因为不需要接收结果集 +- 示例模板(使用了JDBCUtils工具类): + +- import java.sql.*; + + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + +- add(); +- } + +``` + private static void add() { +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- String sql = "insert into dept2 values(14,'lala','china')"; +- int result = stmt.executeUpdate(sql); +- System.out.println(result); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, stmt, c); +- } +- } +- } +# JDBC进阶 + + + +- SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。 +- select * from emp where ename = '' or 1 = 1; +- PreparedStatement类 + +- 是Statement接口的子接口 +- 表示预编译的 SQL 语句的对象。 +- SQL 语句(增改删DML)被预编译并存储在 PreparedStatement 对象中。然后可以使用此对象多次高效地执行该SQL语句。 +- 与Statement不同的是通过preparedStatement方法来获得PreparedStatement对象。 + +- 参数: +- sql - 可能包含一个或多个 '?' IN 参数占位符的 SQL 语句 +- 返回: +- 包含预编译 SQL 语句的新的默认 PreparedStatement 对象 + +- 不同于createStatement不需要参数,这个方法需要一个参数,即预编译的SQL语句,其插入的数据用?占位符表示 +- PreparedStatement pstmt = conn.prepareStatement(“insert into dept values(?,?,?)”); +- 之后再使用pstmt来为这三个?赋值 +- 使用的方法有: + +- 这些方法的第一个参数都是表示第几个占位符 +- 1表示第一个? +- 2表示第二个? + +- pstmt.setInt(1,deptno); +- pstmt.setString(2,dname); +- pstmt.setString(3,loc); +- 另一个区别是executeUpdate方法不再需要参数传入。 +- PreparedStatement类的好处是不必去考虑怎么才能拼凑出格式正确的SQL语句,而是调用方法就可以设置相应的值,十分方便;也易于修改占位符值。尽量使用;可以解决SQL注入问题(过滤掉特殊字符)(最大的优点);同时效率同Statement相比较高(避免频繁SQL语句,是预编译) +- 可以执行查询和DML操作。 +- 示例: +- import java.sql.*; + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + + - if(args.length != 3){ +- System.out.println("Input Error!"); + - System.exit(-1); +- } +- int deptno =0 ; +- try{ +- deptno = Integer.parseInt(args[0]); +- }catch(NumberFormatException e){ +- e.printStackTrace(); +- } +- String dname = args[1]; +- String loc = args[2]; +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement("insert into dept2 values(?,?,?)"); +- pstmt.setInt(1, deptno); +- pstmt.setString(2,dname); +- pstmt.setString(3,loc); +- pstmt.executeUpdate(); +- +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, pstmt, c);//这里仍可以执行,因为PreparedStatement是Statement的子类,而非超类 +- } +- } +- } +- 在java中SQL语句测定时间的一种方法,在执行之前和执行之后的时间打印出来 + + +- JDBC中最耗时间的是建立连接;使用的是Socket连接,三次握手机制 +- 发送用户名密码 +- 比发送执行SQL语句的时间要长得多 + +- 注意虽然PreparedStatement是Statement是子类,但是不能调用父类的executeQuery(sql); +- 会出错 +- 因为该方法会直接将参数原始的sql语句传到数据库,而不管之前的填充sql语句的步骤 + +- 示例: +- import java.sql.*; + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + +- read("SMITH"); +- } + + +``` + private static void read(String name) { +``` + +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- String sql = "select * from emp where ename = ?"; +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement(sql); +- pstmt.setString(1,name); +- rs = pstmt.executeQuery();//如果这里加上sql,那么会报错 +- +- while(rs.next()){ + - System.out.println(rs.getString(1)); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, pstmt, c); +- } +- } +- } +- 如何选择? 一般带有参数的sql都使用PreparedStatement +- 如果没有条件或条件固定的可以使用Statement。 建议全部使用PreparedStatement +- CallableStatement(存储过程) + +- 是PreparedStatement的子接口,是Statement的孙子接口。 +- 用于执行 SQL 存储过程的接口。JDBC API 提供了一个存储过程 SQL 转义语法,该语法允许对所有 RDBMS 使用标准方式调用存储过程。此转义语法有一个包含结果参数的形式和一个不包含结果参数的形式。如果使用结果参数,则必须将其注册为 OUT 参数。其他参数可用于输入、输出或同时用于二者。参数是根据编号按顺序引用的,第一个参数的编号是 1。 + +- 继承自PreparedStatement + +- Connection + +- 可以创建一个CallableStatement对象 +- 参数是sql语句 +- sql语句是这样写的 +- CollableStatement cs = null; +- cs = conn.prepareCall(“{call pro1(?,?)}”); //参数以?表示,之后赋值 +- cs.setString(1,”SMITH”); +- cs.setIntFloat(2,456.7f);//f是表示float类型 + +- 第一个参数是1,2,3等 表示第一个、第二个、第三个? +- 第二个参数是所要设为的值 + +- 设置完后调用execute方法 + + +- 注意如果在sql 中没有commit提交事务,那么java中无法访问数据库,处于阻塞状态 + +- 代码: + +``` +public static void main(String []args){ +``` + +- Connection c = null; +- CallableStatement cs = null; +- try{ +- +- c =JDBCUtils.getConn(); +- cs = c.prepareCall("{call pro3(?,?)}"); +- cs.setString(1, "SMITH"); +- cs.setFloat(2, 7800f); //这个也可以是setString +- //注意oracle提供一种自动转换机制,如果该字段是数字类型,那么会自动将字符串转为数字 +- cs.execute(); + +- 可以在SQL工具类中封装一个可以调用存储过程的方法 +- 工具类中的成员变量均为静态变量,方法都是静态方法 +- SQLHelper: + + +``` +public static void callProcedure(String sql,String []parameters){ +``` + +- Connection c= null; +- CallableStatement cs= null; +- ResultSet rs = null; +- //所有含有参数的sql语句都使用PreparedStatement,传入时还包括一个字符串数据 +- 然后通过一个循环将参数使用setString将sql语句补充完毕 +- 然后调用 +- try{ +- c =JDBCUtils.getConn(); +- cs = c.prepareCall(sql); +- if(parameters != null && "".equals(parameters)){ +- for(int i = 0; i< parameters.length ;i++){ +- cs.setString(i+1,parameters[i]); +- } +- } +- cs.execute(); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,cs,c); +- } +- } + +- 调用此方法的代码: + + +- ------------------------------------------------------------------------------------------------------------------------------- +- 带返回值参数的存储过程 +- 分页查询 +- 存储过程代码: +- create or replace procedure paged_query +- (v_in_table in varchar2,v_in_record_per_page in number, v_in_now_page in number,v_in_col varchar2, +- v_out_records out number,v_out_pages out number, v_out_result out p1.my_cursor) is +- v_start number; +- v_end number; +- v_sql varchar2(2000); +- v_sql_get_record varchar2(300); + +- begin + - v_start := 1+v_in_record_per_page*( v_in_now_page-1) ; +- v_end := 1+ v_in_record_per_page * v_in_now_page; +- v_sql_get_record := 'select count(*) from '||v_in_table; +- execute immediate v_sql_get_record into v_out_records; +- if mod(v_out_records,v_in_record_per_page) = 0 then +- v_out_pages := v_out_records / v_in_record_per_page; +- else +- v_out_pages := v_out_records / v_in_record_per_page + 1; +- end if; +- v_sql := 'select t2.* from (select t1.* ,rownum rn from +- (select * from '||v_in_table||' order by '||v_in_col||') t1 +- where rownum <= '||v_end||' +- ) t2 +- where rn >= '||v_start; +- open v_out_result for v_sql; +- end; +- / + +- + +- 使用CallableStatement来接收Connection的prepareCall方法,得到statement +- 传入的是含有等同于参数个数的sql语句 +- String sql = “{call pro1(?,?)}”; +- 然后set方法设置每个?所对应的值;注意数值类型也可以用setString +- oracle会自动转为相应的数值类型 + +- 与不含输出参数的过程的第一个不同是还需要调用这个方法registerOutParameter + +- 注册输出参数的类型 +- 注意不同数据库,参数类型也是不同的 +- 第一个参数是给第n个?赋值;第二个参数是对应的返回值的类型 +- 对oracle而言是oracle.jdbc.OracleTypes.某个类型 +- 最后execute +- --------------------------------------------------------------------------------------------------------------------------------- +- 第二个不同是还需要再取出返回值 +- get…数据类型(n) +- 比如getString(第几个?的返回值); + +- 如果返回的是结果集(游标),那么需要使用结果集来接收游标变量 +- oracle.jdbc.OracleTypes.CURSOR +- 取出返回值的集合 +- 方法是getObject 将游标视为一个对象 +- 接收的是ResltSet结果集(需要将Object类型强制转为ResultSet) +- ResultSet实际上就是游标 +- 之后就可以使用next和get…来获取值了 + +- + +- java调用代码: +- try { +- c = JDBCUtils.getConn(); +- cs = c.prepareCall("{call paged_query(?,?,?,?,?,?,?)}"); +- int page = 2; +- cs.setString(1, "emp"); +- cs.setString(2,"5"); +- cs.setString(3,""+page); +- cs.setString(4, "sal"); +- cs.registerOutParameter(5, oracle.jdbc.OracleTypes.NUMBER); +- //每个返回的变量都需要注册 +- cs.registerOutParameter(6, oracle.jdbc.OracleTypes.NUMBER); +- cs.registerOutParameter(7, oracle.jdbc.OracleTypes.CURSOR); +- cs.execute(); + - String recordCount = cs.getString(5); + - String pageCount = cs.getString(6); +- System.out.println("共有"+recordCount+"条记录"); +- //执行完后需要逐个取出返回值 +- System.out.println("共有"+pageCount+"页"); +- System.out.println("第"+page+"页:"); + - rs = (ResultSet)cs.getObject(7); +- while(rs.next()){ + - System.out.println(rs.getString(1)+","+rs.getString(2)); +- }catch ……略 + + +- getGeneratedKeys产生主键 +- 用于拿出插入的记录的主键 + + +- + +- getGeneratedKeys(获取主键) +- API介绍: +- Connection: + +- prepareStatement +- PreparedStatement prepareStatement(String sql, +- int autoGeneratedKeys) +- throws SQLException +- 创建一个默认 PreparedStatement 对象,该对象能获取自动生成的键。给定常量告知驱动程序是否可以获取自动生成的键。如果 SQL 语句不是一条 INSERT 语句,或者 SQL 语句能够返回自动生成的键(这类语句的列表是特定于供应商的),则忽略此参数。 +- 该SQL语句是插入语句,可以立刻获得插入记录的主键 +- 第二个参数是Statement中的常量 + + +- PreparedStatement: +- ResultSet getGeneratedKeys() throws SQLException +- 获取由于执行此 Statement 对象而创建的所有自动生成的键。如果此 Statement 对象没有生成任何键,则返回空的 ResultSet 对象。 +- 注:如果未指定表示自动生成键的列,则 JDBC 驱动程序实现将确定最能表示自动生成键的列。 +- 返回:包含通过执行此 Statement 对象自动生成的键的 ResultSet 对象 +- 执行的是insert语句,返回的是ResultSet +- ResultSet 可能是多个主键(组合主键),所以需要ResultSet + +- 代码: + +``` +public static void add() { +``` + +- Connection c = null; +- PreparedStatement ps = null; +- ResultSet rs = null; +- String sql = "insert into UserTable(user_name,user_password) values(?,?)"; +- try { +- c = JDBCUtils.getConn(); +- ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); +- ps.setString(1, "wulitaotao"); +- ps.setString(2, "666"); +- ps.executeUpdate(); +- rs = ps.getGeneratedKeys(); +- if(rs.next()){ + - System.out.println(“刚插入记录的id为”+rs.getInt(1)); +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally{ +- JDBCUtils.free(rs,ps,c); +- } +- } + +- 实际作用是如果数据库的表是自动主键,那么插入之后是不知道id的。只能从数据库中根据其他的唯一键来找到这条记录再取出主键 +- 而这种方式可以在插入之后立刻得到主键,然后取出赋给对象,ok + +- 优化UserDAOJDBCImpl中的addUser方法 + +- try { +- c = JDBCUtils.getConn(); +- String sql = "insert into UserTable(user_name,user_password,user_birthday) +- values(?,?,?)"; +- pstmt = c.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS); +- pstmt.setString(1, user.getUsername()); +- pstmt.setString(2, user.getPassword()); +- pstmt.setDate(3, new java.sql.Date(user.getBirthday().getTime())); +- pstmt.executeUpdate(); +- rs = pstmt.getGeneratedKeys(); +- if(rs.next()){ + - user.setId(rs.getInt(1)); +- } +- } catch (SQLException e) { +- throw new DAOException(e.getMessage(),e); +- } finally { +- JDBCUtils.free(rs, pstmt, c); +- } + +- + +- 批处理Batch +- 可以一次执行多条SQL语句,调用Statement接口的addBatch方法 +- 不必建立多次的连接(建立连接成本很高),只建立一次连接就可以执行多条语句 + +- 将给定的 SQL 命令添加到此 Statement 对象的当前命令列表中。通过调用方法 executeBatch 可以批量执行此列表中的命令。 +- 参数: +- sql - 通常此参数为 SQL INSERT 或 UPDATE 语句 + +- 执行语句时调用 + +- 将一批命令提交给数据库来执行 +- 如果全部命令执行成功,则返回更新计数组成的数组。返回数组的 int 元素的排序对应于批中的命令,批中的命令根据被添加到批中的顺序排序。 +- 返回: +- 包含批中每个命令的一个元素的更新计数所组成的数组。数组的元素根据将命令添加到批中的顺序排序。 + +- 批处理适用于Statement,当然适用于其子类PreparedStatement + +- 示例1 Statement: +- import java.sql.*; + +``` +public class JDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- stmt.addBatch("insert into dept2 values(88,'aaa','aaa')"); +- stmt.addBatch("insert into dept2 values(89,'aaa','aaa')"); +- stmt.addBatch("insert into dept2 values(90,'aaa','aaa')"); +- stmt.executeBatch(); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } +- } +- ------------------------------------------------------------------------------------------------------ +- 示例2 PreparedStatement: +- import java.sql.*; + +``` +public class JDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement("insert into +- UserTable(user_name,user_password,user_birthday) values(?,?,?)"); +- for(int i = 0 ; i<100;i++){//一次性插入100条记录 + - pstmt.setString(1, "user"+i+1); +- pstmt.setString(2, "111"); +- pstmt.setDate(3, new Date(System.currentTimeMillis())); +- pstmt.addBatch(); +- //设置完一条记录添加一次,然后重新设置下一条记录 字段的值 +- } +- int []id = pstmt.executeBatch();//返回值全部是1 +- for(int i:id){ +- System.out.println(i); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,pstmt,c); +- } +- } +- } +- addBatch(sql) 也是存在的,可以将sql语句传入 + +- 如果记录太多可能会内存溢出 + +- + +- 事务处理 +- 在java程序中将多个DML语句视为一个事务,统一提交和回滚 + +- 注意java中事务是自动提交的,也就是执行一条DML语句就commit一下 +- 需要我们去设置不自动提交事务,由自己来设定事务 +- Connection有一个方法setAutoCommit +- 参数为true or false +- 当想提交时,执行Connection的commit方法 +- 在执行commit方法之前的DML语句都还未提交,当执行commit方法时将之前的DML语句视为一个事务,整体地执行 +- 当事务执行过程中出现异常,可以在catch到exception后回滚 +- 可以Connection的rollback方法,有重载的方法,可以无参数,对应sql中的rollback; +- rollback方法可以有参数,是保存点,回滚到这个保存点 + + + +- + + +``` +public static void testSavepoint() throws Exception{ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- String sql1 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql2 = "update emp2 set sal = sal - 800 where empno = 7369"; + +- try { +- c = JDBCUtils.getConn(); +- c.setAutoCommit(false); +- stmt = c.createStatement(); + - stmt.executeUpdate(sql1); +- int i = 8/0; //故意制造出一些错误 + - stmt.executeUpdate(sql2); +- c.commit(); +- c.setAutoCommit(true);//恢复现场 +- }catch (Exception e) { //捕捉到任何异常,立刻返回到初始状态 +- //不能只写SQLException,应该是所有异常,否则ArithmeticException就捕捉不到了,捕捉不到也就无法执行catch块中的代码了 +- if(c != null) //如果没有建立连接那么没有必要去rollback +- c.rollback(); +- c.setAutoCommit(true);//无论如何都应该恢复现场 +- throw e;//交给上一级去处理 +- } finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } +- } +- 设置保存点: +- 假如在执行了第一条sql之后设置保存点,如果在执行第二条sql时出错,回滚到第一条sql之后 +- Connection: + + +- + + +``` +public static void testSavepoint() throws SQLException { +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- Savepoint sp = null; +- String sql1 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql2 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql3 = "select sal from emp2 where empno = 7369"; +- try { +- c = JDBCUtils.getConn(); +- c.setAutoCommit(false); +- stmt = c.createStatement(); + - stmt.executeUpdate(sql1); +- sp = c.setSavepoint(); + - rs = stmt.executeQuery(sql3); +- int sal = 0; +- if(rs.next()) +- sal = rs.getInt("sal"); +- if(sal < 1000) +- throw new RuntimeException("工资过低!"); +- //自己new一个异常,以便于与SQLException区分开来 + - stmt.executeUpdate(sql2); +- c.commit(); +- c.setAutoCommit(true);//恢复现场 +- }catch (RuntimeException e) { +- if(c != null && sp != null){ +- c.rollback(sp); +- //如果工资减去800之后小于1000,那么不再减少,保留第一次的结果 +- } +- c.setAutoCommit(true); +- throw e; +- } catch (SQLException e) { +- if(c != null){ +- c.rollback(); +- c.setAutoCommit(true); +- } +- throw e; +- } finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } + +- JTA类似指挥官,第一阶段给所有数据库发送一个准备提交的请求,如果有数据库提出要回滚,那么JTA会通知其他数据库,一起回滚 +- 如果没有数据库没有提出要回滚,那么第二阶段JTA发送提交的命令 +- + +# JDBC桥接模式源码分析(以MySQL为例) +# 5.12 Class.forName +- Class.forName("com.mysql.jdbc.Driver"); +- Driver#static{} +- 注册MySQL的Driver + +``` +public class Driver extends NonRegisteringDriver implements java.sql.Driver { + // ~ Static fields/initializers + // --------------------------------------------- + + // + // Register ourselves with the DriverManager + // + static { + try { + java.sql.DriverManager.registerDriver(new Driver()); + } catch (SQLException E) { + throw new RuntimeException("Can't register driver!"); + } + } +``` + +- } + +- DriverManager#registerDriver + +``` +public static synchronized void registerDriver(java.sql.Driver driver) + throws SQLException { + + registerDriver(driver, null); +} +``` + + + +``` +public static synchronized void registerDriver(java.sql.Driver driver, + DriverAction da) + throws SQLException { + + /* Register the driver if it has not already been added to our list */ + if(driver != null) { + registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); + } else { + // This is for compatibility with the original DriverManager + throw new NullPointerException(); + } + + println("registerDriver: " + driver); + +} +``` + + +- DriverManager#getConnection + +``` +public static Connection getConnection(String url) + throws SQLException { + + java.util.Properties info = new java.util.Properties(); + return (getConnection(url, info, Reflection.getCallerClass())); +} +``` + + + + +``` +private static Connection getConnection( + String url, java.util.Properties info, Class caller) throws SQLException { + /* + * When callerCl is null, we should check the application's + * (which is invoking this class indirectly) + * classloader, so that the JDBC driver class outside rt.jar + * can be loaded from here. + */ + ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; + synchronized(DriverManager.class) { + // synchronize loading of the correct classloader. + if (callerCL == null) { + callerCL = Thread.currentThread().getContextClassLoader(); + } + } + + if(url == null) { + throw new SQLException("The url cannot be null", "08001"); + } + + println("DriverManager.getConnection(\"" + url + "\")"); + + // Walk through the loaded registeredDrivers attempting to make a connection. + // Remember the first exception that gets raised so we can reraise it. + SQLException reason = null; + + for(DriverInfo aDriver : registeredDrivers) { + // If the caller does not have permission to load the driver then + // skip it. + if(isDriverAllowed(aDriver.driver, callerCL)) { + try { + println(" trying " + aDriver.driver.getClass().getName()); + Connection con = aDriver.driver.connect(url, info); + if (con != null) { + // Success! + println("getConnection returning " + aDriver.driver.getClass().getName()); + return (con); + } + } catch (SQLException ex) { + if (reason == null) { + reason = ex; + } + } + + } else { + println(" skipping: " + aDriver.getClass().getName()); + } + + } + + // if we got here nobody could connect. + if (reason != null) { + println("getConnection failed: " + reason); + throw reason; + } + + println("getConnection: no suitable driver found for "+ url); + throw new SQLException("No suitable driver found for "+ url, "08001"); +} +``` + + +- Driver#connect + +``` +public java.sql.Connection connect(String url, Properties info) + throws SQLException { + if (url != null) { + if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { + return connectLoadBalanced(url, info); + } else if (StringUtils.startsWithIgnoreCase(url, + REPLICATION_URL_PREFIX)) { + return connectReplicationConnection(url, info); + } + } + + Properties props = null; + + if ((props = parseURL(url, info)) == null) { + return null; + } + + if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) { + return connectFailover(url, info); + } + + try { + Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance( + host(props), port(props), props, database(props), url); + + return newConn; + } catch (SQLException sqlEx) { + // Don't wrap SQLExceptions, throw + // them un-changed. + throw sqlEx; + } catch (Exception ex) { + SQLException sqlEx = SQLError.createSQLException(Messages + .getString("NonRegisteringDriver.17") //$NON-NLS-1$ + + ex.toString() + + Messages.getString("NonRegisteringDriver.18"), //$NON-NLS-1$ + SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null); + + sqlEx.initCause(ex); + + throw sqlEx; + } +} +``` + +# 5.17 总结 +- 有了抽象部分——JDBC的API,有了具体实现部分——驱动程序,那么它们如何连接起来呢?就是如何桥接呢? +- 就是前面提到的DriverManager来把它们桥接起来,从某个侧面来看,DriverManager在这里起到了类似于简单工厂的功能,基于JDBC的应用程序需要使用JDBC的API,如何得到呢?就通过DriverManager来获取相应的对象。 +- JDBC的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。而且,JDBC的这种架构,还合理的划分了应用程序开发人员和驱动程序开发人员的边界。 + +- Class.forName是用MySql还是Oracle,这个Driver一定会实现接口java.sql.Driver,然后通过DriverManager.registerDriver(new Driver());使DriverManager类持有一个Driver,是否可以把DriverManager当成桥,当成桥连接中的抽象类?然后持有一个接口Driver,至于是MySql还是Oracle,不关心,坐等传参。因为DriverManager持有的是一个Driver接口,你传过来什么,我就得到什么的实例化,然后我再通过getConnection用你的实例,去调用你自己的方法connect,去获得Connection的一个实例。 +- + +# 安全 +# XSS攻击 +# 5.18 原理 +- 跨站脚本攻击(Cross-Site Scripting, XSS):主要是指在用户浏览器内运行了JavaScript 脚本。比如富文本编辑器,如果不过滤用户输入的数据直接显示用户输入的HTML内容的话,就会有可能运行恶意的 JavaScript 脚本,导致页面结构错乱,Cookies 信息被窃取等问题。 + +- XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。 +- 常见的XSS攻击类型有两种,一种是反射型,一种是持久型。 +## 反射型 +- 攻击者诱使用户点击一个嵌入恶意脚本的链接,达到工具的目的。 +- 比如新浪微博中,攻击者发布的微博中含有一个恶意脚本的URL(URL中包含脚本的链接),用户点击该URL,脚本会自动关注攻击者的新浪微博ID,发布含有恶意脚本URL的微博,攻击就被扩散了。 +- 现实中,攻击者可以采用XSS攻击,偷取用户Cookie、密码等重要数据,进而伪造交易、盗窃用户财产、窃取情报。 + +## 持久型 +- 黑客提交含有恶意脚本的请求,保存在被攻击的Web站点的数据库中,用户浏览网页时,恶意脚本被包含在正常页面中,达到攻击的目的。此种攻击经常使用在论坛、博客等Web应用中。 + + +# 5.19 预防 +- Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。 +- 后端在入库前应该选择不相信任何前端数据,将所有的字段统一进行转义处理。 +- 后端在输出给前端数据统一进行转义处理。 +- 前端在渲染页面 DOM 的时候应该选择不相信任何后端数据,任何字段都需要做转义处理。 +## 消毒 +- XSS攻击者一般都是在请求中嵌入恶意脚本达到攻击的目的,这些脚本是一般在用户输入中不常用的,如果进行过滤和消毒处理,即对某些HTML危险字符转义,如”>”转义为”>”,就可以防止大部分的攻击。为了避免对不必要的内容错误转义,如”3<5”中的”<”需要进行文本匹配后再转义,如” + 关键字: + + + + +- 打开HttpWatch,输入hello后点击提交,查看请求内容如下: +POST /hello/index.jsp HTTP/1.1 +Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, */* +Referer: http://localhost:8080/hello/index.jsp +Accept-Language: zh-cn,en-US;q=0.5 +User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) +Content-Type: application/x-www-form-urlencoded +Accept-Encoding: gzip, deflate +Host: localhost:8080 +Content-Length: 13 +Connection: Keep-Alive +Cache-Control: no-cache +Cookie: JSESSIONID=E365D980343B9307023A1D271CC48E7D + +keyword=hello + +- POST请求是可以有体的,而GET请求不能有请求体。 +- Referer: http://localhost:8080/hello/index.jsp:请求来自哪个页面,例如你在百度上点击链接到了这里,那么Referer:http://www.baidu.com;如果你是在浏览器的地址栏中直接输入的地址,那么就没有Referer这个请求头了; +- Content-Type: application/x-www-form-urlencoded:表单的数据类型,说明会使用url格式编码数据;url编码的数据都是以“%”为前缀,后面跟随两位的16进制,例如“传智”这两个字使用UTF-8的url编码用为“%E4%BC%A0%E6%99%BA”; +- Content-Length:13:请求体的长度,这里表示13个字节。 +- keyword=hello:请求体内容!hello是在表单中输入的数据,keyword是表单字段的名字。 + +- Referer请求头是比较有用的一个请求头,它可以用来做统计工作,也可以用来做防盗链。 +- 统计工作:我公司网站在百度上做了广告,但不知道在百度上做广告对我们网站的访问量是否有影响,那么可以对每个请求中的Referer进行分析,如果Referer为百度的很多,那么说明用户都是通过百度找到我们公司网站的。 +- 防盗链:我公司网站上有一个下载链接,而其他网站盗链了这个地址,例如在我网站上的index.html页面中有一个链接,点击即可下载JDK7.0,但有某个人的微博中盗链了这个资源,它也有一个链接指向我们网站的JDK7.0,也就是说登录它的微博,点击链接就可以从我网站上下载JDK7.0,这导致我们网站的广告没有看,但下载的却是我网站的资源。这时可以使用Referer进行防盗链,在资源被下载之前,我们对Referer进行判断,如果请求来自本网站,那么允许下载,如果非本网站,先跳转到本网站看广告,然后再允许下载。 +# 5.32 GET和POST请求的区别 +- 幂等意味着对同一URL的多个请求应该返回同样的结果。 + - 1)前者将请求参数放在URL中,文本格式;后者将请求参数放在请求体中,可以是文本、二进制等格式 + - 2)前者语义上是从服务器获取资源,安全(无副作用)、幂等、可缓存;后者语义上是向服务器提交资源,不安全(有副作用)、不幂等、不可缓存 + - 3)前者的URL是明文传输,会保存在浏览器历史记录中,安全性不足,可能会受到CSRF攻击;后者较为安全(但是如果没有加密的话,都是可以明文获取的) +# 5.33 其他请求方法 +- GET(SELECT):从服务器取出资源(一项或多项)。 +- POST(CREATE):在服务器新建一个资源。 +- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源,数据输入必须与由 GET 接收的数据表示保持一致)。 +- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 +- DELETE(DELETE):从服务器删除资源。 +- HEAD:获取资源的元数据。 +- OPTIONS:1、获取服务器支持的HTTP请求方法 +- 2、用来检查服务器的性能 +# 5.34 响应内容 +- 响应协议的格式如下: +响应首行;由HTTP协议版本号, 状态码, 状态消息 三部分组成。 +响应头信息; +空行; +响应体。 + +- 响应内容是由服务器发送给浏览器的内容,浏览器会根据响应内容来显示。 +HTTP/1.1 200 OK +Server: Apache-Coyote/1.1 +Content-Type: text/html;charset=UTF-8 +Content-Length: 724 +Set-Cookie: JSESSIONID=C97E2B4C55553EAB46079A4F263435A4; Path=/hello +Date: Wed, 25 Sep 2012 04:15:03 GMT + + + + + + + My JSP 'index.jsp' starting page + + + + + + + + + +
+ 关键字: + +
+ + + +- HTTP/1.1 200 OK:响应协议为HTTP1.1,状态码为200,表示请求成功,OK是对状态码的解释; +- Server: Apache-Coyote/1.1:服务器的版本信息; +- Content-Type: text/html;charset=UTF-8:响应体使用的编码为UTF-8; +- Content-Length: 724:响应体为724字节; +- Set-Cookie: JSESSIONID=C97E2B4C55553EAB46079A4F263435A4; Path=/hello:响应给客户端的Cookie; +- Date: Wed, 25 Sep 2012 04:15:03 GMT:响应的时间,这可能会有8小时的时区差; + +# 5.35 响应码 +- 响应头对浏览器来说很重要,它说明了响应的真正含义。例如200表示响应成功了,302表示重定向,这说明浏览器需要再发一个新的请求。 +- 2xx表示成功,3xx表示重定向,4xx表示客户端出错,5xx表示服务器出错。 +- 200:请求成功,浏览器会把响应体内容(通常是html)显示在浏览器中; +- 404:请求的资源没有找到,说明客户端错误的请求了不存在的资源; +- 500:请求资源找到了,但服务器内部出现了错误; +- 302:重定向,当响应码为302时,表示服务器要求浏览器重新再发一个请求,服务器会发送一个响应头Location,它指定了新请求的URL地址; +- 304:(缓存) +## 301&302 +- 301 Move Permanently +- 302 Found +- 301是永久性重定向。当网站需要改版时,多域名指向同一个页面时,为了不让网站被降低和分散权重,就需要使用301重定向来实现,同时在搜索引擎索引库中彻底废弃掉原先的老地址。 +- 302是临时性重定向,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的,不会传递权重。 +# 5.36 其他响应头 +- 告诉浏览器不要缓存的响应头: +- Expires: -1;(过期时间,-1表示马上过期) +- Cache-Control: no-cache;(不缓存) +- Pragma: no-cache;(不缓存) + +- 自动刷新响应头,浏览器会在3秒之后请求http://www.baidu.com: +- Refresh: 3;url=http://www.baidu.com + +# 5.37 HTML中指定响应头 +- 在HTMl页面中可以使用来指定响应头,例如在index.html页面中给出,表示浏览器只会显示index.html页面3秒,然后自动跳转到http://www.itcast.cn。 +- + +# 缓存 +# 5.38 强缓存与协商缓存 +- 浏览器HTTP缓存可以分为强缓存和协商缓存。强缓存和协商缓存最大也是最根本的区别是:强缓存命中的话不会发请求到服务器(比如chrome中的200 from memory cache),协商缓存一定会发请求到服务器,通过资源的请求首部字段验证资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的实体,而是通知客户端可以从缓存中加载这个资源(304 not modified)。 + +# 5.39 控制强缓存的字段按优先级介绍 +1.- Pragma        Pragma是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。虽然它是一个通用首部,但是它在响应报文中时的行为没有规范,依赖于浏览器的实现。RFC中该字段只有no-cache一个可选值,会通知浏览器不直接使用缓存,要求向服务器发请求校验新鲜度。因为它优先级最高,当存在时一定不会命中强缓存。 +2.- Cache-Control        Cache-Control是一个通用首部字段,也是HTTP/1.1控制浏览器缓存的主流字段。和浏览器缓存相关的是如下几个响应指令: +指令 参数 说明 +private 无 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它) +public 可省略 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存 +no-cache 可省略 缓存前必需确认其有效性 +no-store 无 不缓存请求或响应的任何内容 +max-age=[s] 必需 响应的最大值 +- max-age(单位为s)设置缓存的存在时间,相对于发送请求的时间。只有响应报文首部设置Cache-Control为非0的max-age或者设置了大于请求日期的Expires(下文会讲)才有可能命中强缓存。当满足这个条件,同时响应报文首部中Cache-Control不存在no-cache、no-store且请求报文首部不存在Pragma字段,才会真正命中强缓存。 +- no-cache 表示请求必须先与服务器确认缓存的有效性,如果有效才能使用缓存(协商缓存),无论是响应报文首部还是请求报文首部出现这个字段均一定不会命中强缓存。Chrome硬性重新加载(Command+shift+R)会在请求的首部加上Pragma:no-cache和Cache-Control:no-cache。 +- no-store 表示禁止浏览器以及所有中间缓存存储任何版本的返回响应,一定不会出现强缓存和协商缓存,适合个人隐私数据或者经济类数据。 + +``` +public 表明响应可以被浏览器、CDN等等缓存。 +``` + + +``` +private 响应只作为私有的缓存,不能被CDN等缓存。如果要求HTTP认证,响应会自动设置为private。 +``` + +3.- Expires        Expires是一个响应首部字段,它指定了一个日期/时间,在这个时间/日期之前,HTTP缓存被认为是有效的。无效的日期比如0,表示这个资源已经过期了。如果同时设置了Cache-Control响应首部字段的max-age,则Expires会被忽略。它也是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。 +# 5.40 控制协商缓存的字段 +1.- Last-Modified/If-Modified-Since        If-Modified-Since是一个请求首部字段,并且只能用在GET或者HEAD请求中。Last-Modified是一个响应首部字段,包含服务器认定的资源作出修改的日期及时间。当带着If-Modified-Since头访问服务器请求资源时,服务器会检查Last-Modified,如果Last-Modified的时间早于或等于If-Modified-Since则会返回一个不带主体的304响应,否则将重新返回资源。 +- If-Modified-Since: , :: GMT Last-Modified: , :: GMT +2.- ETag/If-None-Match        ETag是一个响应首部字段,它是根据实体内容生成的一段hash字符串,标识资源的状态,由服务端产生。If-None-Match是一个条件式的请求首部。如果请求资源时在请求首部加上这个字段,值为之前服务器端返回的资源上的ETag,则当且仅当服务器上没有任何资源的ETag属性值与这个首部中列出的时候,服务器才会返回带有所请求资源实体的200响应,否则服务器会返回不带实体的304响应。ETag优先级比Last-Modified高,同时存在时会以ETag为准。 +- 因为ETag的特性,所以相较于Last-Modified有一些优势: +- 1. 某些情况下服务器无法获取资源的最后修改时间 +- 2. 资源的最后修改时间变了但是内容没变,使用ETag可以正确缓存 +- 3. 如果资源修改非常频繁,在秒以下的时间进行修改,Last-Modified只能精确到秒 + +# 5.41 协商缓存细节 +- 当用户第一次请求index.html时,服务器会添加一个名为Last-Modified响应头,这个头说明了index.html的最后修改时间,浏览器会把index.html内容,以及最后响应时间缓存下来。当用户第二次请求index.html时,在请求中包含一个名为If-Modified-Since请求头,它的值就是第一次请求时服务器通过Last-Modified响应头发送给浏览器的值,即index.html最后的修改时间,If-Modified-Since请求头就是在告诉服务器,我这里浏览器缓存的index.html最后修改时间是这个,您看看现在的index.html最后修改时间是不是这个,如果还是,那么您就不用再响应这个index.html内容了,我会把缓存的内容直接显示出来。而服务器端会获取If-Modified-Since值,与index.html的当前最后修改时间比对,如果相同,服务器会发响应码304,表示index.html与浏览器上次缓存的相同,无需再次发送,浏览器可以显示自己的缓存页面,如果比对不同,那么说明index.html已经做了修改,服务器会响应200。(只有html等静态资源可以做缓存,动态资源不做缓存) + +- 响应头: +- Last-Modified:最后的修改时间; +- 请求头: +- If-Modified-Since:把上次请求的index.html的最后修改时间还给服务器; +- 状态码:304,比较If-Modified-Since的时间与文件真实的时间一样时,服务器会响应304,而且不会有响应正文,表示浏览器缓存的就是最新版本! + + +# 幂等性(并非是HTTP的问题,而是服务器API设计问题) +- 幂等性是http层面的问题吗,还是服务器要处理和解决的内容? + +- 对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。这里所讨论的HTTP幂等性主要针对RESTful风格的,但幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。 + +- HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。 +- 比如:GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。 + +- HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 +- 比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。 + +- 比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。而PUT所对应的URI是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。 + +# 无状态 +- 客户端和服务器在某次会话中产生的数据,从而【无状态】就意味着,这些数据不会被保留;协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。但是通过增加cookie和session机制,现在的网络请求其实是有状态的。在没有状态的http协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话。 +- 与之相对的是TCP,TCP是有状态的,因为每一条消息的seq和ack(还有一堆滑动窗口,拥塞的控制参数,等)都和前面消息相关。 +- HTTP并不会在内存里保留前次请求相关的任何状态,仅仅以协议逻辑(打包解包)存在,所以是它无状态的。 + +- 无状态的设计会加强透明度(visibility),稳定度(reliability)和伸缩度(scalability)。提高透明度是因为系统无需通过请求内容以外的信息判断请求的完整内容;提高稳定度是指在部分失败的情况下,减轻了恢复的难度;提高伸缩度的原因是无需储存请求间的状态使服务器端可以很快释放资源并简化实现。 +- 优点在于解放了服务器,每一次请求“点到为止”不会造成不必要连接占用,缺点在于每次请求会传输大量重复的内容信息。 +# 跨域 CORS 跨域资源共享 +- 之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 +- 同源策略具体限制些什么呢? +- 1. 不能向工作在不同源的的服务请求数据(client to server)这里有个问题之前也困扰了我很久,就是为什么home.com加载的cdn.home.com/index.js可以向home.com发请求而不会跨域呢?其实home.com加载的JS是工作在home.com的,它的源不是提供JS的cdn,所以这个时候是没有跨域的问题的,并且script标签能够加载非同源的资源,不受同源策略的影响。 +- 2. 无法获取不同源的document/cookie等BOM和DOM,可以说任何有关另外一个源的信息都无法得到 (client to client)。 + +- 跨域最常用的方法,应当属CORS,如下图所示: + +- 只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。 +- CORS把请求分为两种,一种是简单请求,另一种是需要触发预检请求,这两者是相对的,怎样才算“不简单”?只要属于下面的其中一种就不是简单请求: + - (1)使用了除GET/POST/HEAD之外的请求方式,如PUT/DELETE + - (2)使用了除Content-Type/Accept等几个常用的http头这个时候就认为需要先发个预检请求 +# 5.42 简单请求 +- 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 + +- 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。 + +- GET /cors HTTP/1.1 +- Origin: http://api.bob.com +- Host: api.alice.com +- Accept-Language: en-US +- Connection: keep-alive +- User-Agent: Mozilla/5.0... +- 上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。 + +- 如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。 + +- 如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。 + + +- Access-Control-Allow-Origin: http://api.bob.com +- Access-Control-Allow-Credentials: true +- Access-Control-Expose-Headers: FooBar +- Content-Type: text/html; charset=utf-8 +- 上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。 + - (1)Access-Control-Allow-Origin +- 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 + - (2)Access-Control-Allow-Credentials +- 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 + - (3)Access-Control-Expose-Headers +- 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。 +# 5.43 非简单请求 +- 简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。 + +- 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 + +- 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式XMLHttpRequest请求,否则就报错。 +- "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。 +- 除了Origin字段,"预检"请求的头信息包括两个特殊字段。 + - (1)Access-Control-Request-Method +- 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法 + - (2)Access-Control-Request-Headers +- 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段 +- 服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。 + +- 如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。 + +- 一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。 + +- CORS与JSONP的使用目的相同,但是比JSONP更强大。 + +- JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。 +# 长轮询与短轮询 +- 短轮询相信大家都不难理解,比如你现在要做一个电商中商品详情的页面,这个详情界面中有一个字段是库存量(相信这个大家都不陌生,随便打开淘宝或者京东都能找到这种页面)。而这个库存量需要实时的变化,保持和服务器里实际的库存一致。 + +- 这个时候,你会怎么做? + +- 最简单的一种方式,就是你用JS写个死循环,不停的去请求服务器中的库存量是多少,然后刷新到这个页面当中,这其实就是所谓的短轮询。 + +- 这种方式有明显的坏处,那就是你很浪费服务器和客户端的资源。客户端还好点,现在PC机配置高了,你不停的请求还不至于把用户的电脑整死,但是服务器就很蛋疼了。如果有1000个人停留在某个商品详情页面,那就是说会有1000个客户端不停的去请求服务器获取库存量,这显然是不合理的。 + +- 那怎么办呢? + +- 长轮询这个时候就出现了,其实长轮询和短轮询最大的区别是,短轮询去服务端查询的时候,不管库存量有没有变化,服务器就立即返回结果了。而长轮询则不是,在长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒,Object.wait)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回(Object.notify),否则就一直等到超时为止。 + +- 而对于客户端来说,不管是长轮询还是短轮询,客户端的动作都是一样的,就是不停的去请求,不同的是服务端,短轮询情况下服务端每次请求不管有没有变化都会立即返回结果,而长轮询情况下,如果有变化才会立即返回结果,而没有变化的话,则不会再立即给客户端返回结果,直到超时为止。 +- 这样一来,客户端的请求次数将会大量减少(这也就意味着节省了网络流量,毕竟每次发请求,都会占用客户端的上传流量和服务端的下载流量),而且也解决了服务端一直疲于接受请求的窘境。 + +- 但是长轮询也是有坏处的,因为把请求挂起同样会导致资源的浪费,假设还是1000个人停留在某个商品详情页面,那就很有可能服务器这边挂着1000个线程,在不停检测库存量,这依然是有问题的。 + +- 因此,从这里可以看出,不管是长轮询还是短轮询,都不太适用于客户端数量太多的情况,因为每个服务器所能承载的TCP连接数是有上限的,这种轮询很容易把连接数顶满。 + +- + +# 长连接与短连接 +- HTTP的短连接和长连接;长连接与短连接的区别(LVS是通过长连接作负载均衡) +- HTTP的长连接和短连接本质上是TCP长连接和短连接。 +- 在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 + +- 但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive。 +- 在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。 + +- 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损。 + +- 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。 +# URL +- url有最大长度限制,就问长度有限制是get的原因还是url的原因,为什么长度会有限制,是http数据包的头的字段原因还是内容字段的原因 +- 是GET的原因,长度受到服务器和客户端的限制。 + +- URL编解码 +- Url的编码格式采用的是ASCII码,而不是Unicode,这也就是说你不能在Url中包含任何非ASCII字符,例如中文。 + - Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符; +- RFC3986中指定了以下字符为保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ] + +- Url编码通常也被称为百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的十六进制字符。 +# URI&URL +- URL(Uniform ResourceLocator)统一资源定位符,是专门为标识网络上的资源位置而设计的一种编址方式。URL一般由3个部分组成: +- 应用层协议 +- 主机IP地址或域名 +- 资源所在路径/文件名 + +- 统一资源标识符(Uniform Resource Identifier,或URI)是一个用于标识某一互联网资源名称的字符串。 +- URI :Uniform Resource Identifier,统一资源标识符; +- URL:Uniform Resource Locator,统一资源定位符; +- URN:Uniform ResourceName,统一资源名称。 +- 其中,URL,URN是URI的子集。 +- URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位。 +# HTTPS +# 5.44 HTTP缺点 +- 明文传输,内容可能会被窃听 +- 不验证通信方的身份,因此有可能遭遇伪装 +- 无法证明报文的完整性,所以有可能已遭篡改 +- HTTP+加密+认证+完整性保护=HTTPS +- + +- 用SSL将通信的报文主体内容进行加密,使用SSL建立http的安全通信线路,SSL处于http与TCP通信之间,这样的SSL与HTTP组合被称为HTTPS。 +- HTTPS 采用对称加密和非对称加密两者并用的混合加密机制 + +- HTTPS 公钥能用公钥解吗?在客户端抓包,看到的是加密的还是没加密 是没加密的 +- https ssl tcp三者关系,其中哪些用到了对称加密,哪些用到了非对称加密,非对称加密密钥是如何实现的 +- 加密的私钥和公钥各自如何分配(客户端拿公钥,服务器拿私钥) +- 客户端是如何认证服务器的真实身份,详细说明一下过程,包括公钥如何申请,哪一层加密哪一层解密 +- 怎么攻击https +- TLS改进,如果session ticket被偷听到会怎样,如何防止中间人攻击 + +# 5.45 SSL/TLS +- SSL(Secure Socket Layer安全套接字层) +- TLS(Transport Layer Security) + +- SSL发展到3.0版本后改成了TLS。 +- TLS主要提供三个基本服务 +- 加密 +- 身份验证 +- 消息完整性校验 + +- 通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信了。用 SSL 建立安全通信线路之后,就可以在这条线路上进行 HTTP 通信了。 +- SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP 和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全技术。 +- 虽然使用 HTTP 协议无法确定通信方,但如果使用 SSL 则可以。SSL 不仅提供加密处理,而且还使用了一种被称为证书的手段,可用于确定双方身份。 +- 证书由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的。另外,伪造证书从技术角度来说是异常困难的一件事。所以只要能够确认通信方(服务器或客户端)持有的证书,即可判断通信方的真实意图。 +# 5.46 中间人攻击 +- mim 就是man in the middle,中间人攻击正常情况下浏览器与服务器在TLS连接下内容是加密的,第三方即使可以嗅探到所有的数据,也不能解密。中间人可以与你建立连接,然后中间人再与服务器建立连接,转发你们之间的内容。这时候中间人就获得了明文的信息。 +- 有什么危害?你与服务器的通信被第三方解密、查看、修改。如何防范?如果确定是否被攻击?在访问https连接的时候,查看一下服务器提供的证书是不是正确的。除非入侵并取得服务器的证书私钥,否则中间人是不能完全伪装成服务器的样子的。 +- 数字证书可以保证服务器发来的公钥是真的来自服务器的 +# 5.47 服务器保证其提供的公钥的正确性——数字证书 +- 公钥是由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。 +- 数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的立场上。服务器会将这份由数字证书认证机构颁发的公钥证书发送给客户端,以进行公开密钥加密方式通信。公钥证书也可叫做数字证书或直接称为证书。 +- 接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书上的数字签名进行验证,一旦验证通过,客户端便可明确两件事: +- 认证服务器的公开密钥的是真实有效的数字证书认证机构 +- 服务器的公开密钥是值得信赖的 + +- 此处认证机关的公开密钥必须安全地转交给客户端。使用通信方式时,如何安全转交是一件很困难的事,因此,多数浏览器开发商发布版本时,会事先在内部植入常用认证机关的公开密钥。 + + +# 5.48 过程 +-  客户端发起HTTPS请求 这个没什么好说的,就是用户在浏览器里输入一个HTTPS网址,然后连接到服务端的443端口。 +-  服务端的配置 采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。 +-  传送证书 这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。 +-  客户端解析证书 这部分工作是由客户端的SSL/TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警示框,提示证书存在的问题。如果证书没有问题,那么就生成一个随机值。然后用证书(也就是公钥)对这个随机值进行加密。 +-  传送加密信息 这部分传送的是用证书加密后的随机值,目的是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。 +-  服务端解密信息 服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该随机值(密钥)进行对称加密,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够复杂,私钥够复杂,数据就够安全。 +-  传输加密后的信息 这部分信息就是服务端用私钥加密后的信息,可以在客户端用随机值解密还原。 +-  客户端解密信息 客户端用之前生产的私钥解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。 + +- 客户端获得服务器的公钥的过程是基于非对称加密实现的(数字证书) +- 而之后客户端和服务器之间的数据交换是基于对称加密实现的。 +# 5.49 更具体的过程 +-  客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等) +-  服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收 到的客户端加密组件内筛选出来的。 +-  之后服务器发送 Certificate 报文。报文中包含公开密钥证书。 +-  最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的 SSL 握手协商部分结束。 +-  SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该 报文已用步骤 3 中的公开密钥进行加密。 +-  接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。 +-  客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。 +-  服务器同样发送 Change Cipher Spec 报文。 +-  服务器同样发送 Finished 报文。 +-  服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发 送 HTTP 请求。 +-  应用层协议通信,即发送 HTTP 响应。 +-  最后由客户端断开连接。断开连接时,发送 close_notify 报文。 + +- + +# WebSocket +- web浏览器和web服务器之间全双工通信标准。 +- 优点是,直接发送数据,不用等待客户端请求,一直保持连接状态,且首部信息量少,通信量减少。 + +- + +# HTTP 2.0 +# 5.50 二进制分帧 +- 在应用层(HTTP2.0)和传输层(TCP or UDP)之间增加一个二进制分帧层。 +- 在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。Frame 由 Frame Header 和 Frame Payload 两部分组成。不论是原来的 HTTP Header 还是 HTTP Body,在 HTTP/2 中,都将这些数据存储到 Frame Payload,组成一个个 Frame,再发送响应/请求。通过 Frame Header 中的 Type 区分这个 Frame 的类型。由此可见语义并没有太大变化,而是数据的格式变成二进制的 Frame。 +- HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。 + +# 5.51 首部压缩 +- HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。 + +- 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。 +- HTTP/2 使用了专门为首部压缩而设计的 HPACK 算法。 + +# 5.52 服务器推送 +- HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,服务器除了对最初请求的响应外,还可以额外向客户端推送资源,而无需客户端明确地请求。 + +- 当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。我们常用的内嵌图片也可以理解为一种强制的服务器推送:我请求html,却内嵌了张图。 + +- 有了HTTP2.0的服务器推送,HTTP1.x时代的内嵌资源的优化手段也变得没有意义了。而且使用服务器推送的资源的方式更加高效,因为客户端还可以缓存起来,甚至可以由不同的页面共享(依旧遵循同源策略)。当然,浏览器是可以决绝服务器推送的资源的。 +# 5.53 多路复用 +- 多路复用允许同时通过单一的HTTP/2连接发起多重的请求-响应信息。 + +- 每个 Frame Header 都有一个 Stream ID 就是被用于实现该特性。每次请求/响应使用不同的 Stream ID。就像同一个 TCP 链接上的数据包通过 IP:PORT来区分出数据包去往哪里一样。通过 Stream ID 标识,所有的请求和响应都可以欢快的同时跑在一条 TCP 链接上了。 + +# SOA +- Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术。是通过SOAP在Web上提供的软件服务,使用WSDL文件进行说明,并通过UDDI进行注册。 +- SOA是一种架构风格,包括两个方面的内容: + - 1)抽象出服务,这些服务满足离散、松耦合、可复用、自治、无状态等特征; + - 2)服务可以灵活地组装和编排,满足流程整合和业务变化的需要 + +- WebService是SOA的一种实现技术,跨语言,跨平台,提供了标准的服务定义、服务注册、服务接入和访问的方式。使用了XML、SOAP、WSDL、UDDI等技术。 + +# SOA三角操作模型 + - 1)三种角色 +- 服务提供者:发布自己的服务,并且对服务请求进行响应 +- 服务请求者:利用服务注册中心查找所需要的服务,然后使用该服务 +- 服务注册中心:注册已经发布的服务,对其进行分类,并提供搜索服务 + - 2)三个操作: +- 发布:为了使服务可访问,需要发布服务描述以使服务使用者可以发现它 +- 查找:服务请求者查询服务注册中心来找到满足其要求的服务 +- 绑定:检索到服务描述后,服务请求者继续根据服务描述中的信息调用服务 +# XML +- XML:(Extensible Markup Language)扩展型可标记语言。面向短期的临时数据处理、面向万维网络,是Soap的基础。 +# SOAP +- SOAP:(Simple Object Access Protocol)简单对象传输协议。是XML Web Service 的通信协议。当用户通过UDDI找到你的WSDL描述文档后,他通过可以SOAP调用你建立的Web服务中的一个或多个操作。SOAP是XML文档形式的调用方法的规范,它可以支持不同的底层接口,像HTTP(S)或者SMTP。 + +- SOAP=RPC+HTTP+XML:采用HTTP作为底层通讯协议;RPC作为一致性的调用途径,XML作为数据传送的格式,允许服务提供者和服务客户经过防火墙在INTERNET进行通讯交互。 + +- 简单对象传输协议,是轻量级的、简单的、基于XML的用于交换数据的协议。 +- SOAP本质上是一个 XML文档,包含以下元素: + - 1)Envelope元素:必需元素,根元素,标识此XML文档为一条SOAP消息 +- 可以包含命名空间和声明额外的属性 + - 2)Header元素:可选元素,有关SOAP消息的应用程序专用消息 + - 3)Body元素:必需元素,包含所有的请求和响应信息 + - 4)Fault元素:可选元素,提供有关在处理此消息所发生错误的信息 + +- SOAP处理模型: + - 1)用XML打包请求 + - 2)将请求发送给服务器 + - 3)服务器接收到请求,解码XML,处理请求,以XML格式返回响应 + +- SOAP并不假定传输数据的下层协议,因此必须设计为能在各种协议上运行。即使绝大多数SOAP是运行在HTTP上,使用URI标识服务,SOAP也仅仅使用POST方法发送请求,用一个唯一的URI标识服务的入口。 + +- 使用 HTTP 协议的 SOAP,由于其设计原则上并不像 REST 那样强调与 Web 的工作方式相一致,所以,基于 SOAP 应用很难充分发挥 HTTP 本身的缓存能力。 +- HTTP是其通信协议/传输协议,SOAP是其应用协议 + +# WSDL +- WSDL:(Web Services Description Language) WSDL 文件是一个 XML 文档,用于说明一组 SOAP 消息以及如何交换这些消息。大多数情况下由软件自动生成和使用。 +- 网络服务描述语言,是基于XML的,用于描述网络服务、服务定位和服务提供的操作的协议。 + +# UDDI +- UDDI (Universal Description, Discovery, and Integration) 是一个主要针对Web服务供应商和使用者的新项目。在用户能够调用Web服务之前,必须确定这个服务内包含哪些商务方法,找到被调用的接口定义,还要在服务端来编制软件,UDDI是一种根据描述文档来引导系统查找相应服务的机制。UDDI利用SOAP消息机制(标准的XML/HTTP)来发布,编辑,浏览以及查找注册信息。它采用XML格式来封装各种不同类型的数据,并且发送到注册中心或者由注册中心来返回需要的数据。 + +- 统一描述、发现、集成协议,提供基于网络服务的注册和发现机制 + +# REST +- SOAP协议属于复杂的、重量级的协议,当前随着Web2.0的兴起,表述性状态转移(Representational State Transfer,REST)逐步成为一个流行的架构风格。REST是一种轻量级的Web Service架构风格,其实现和操作比SOAP和XML-RPC更为简洁,可以完全通过HTTP协议实现,还可以利用缓存Cache来提高响应速度,性能、效率和易用性上都优于SOAP协议。REST架构对资源的操作包括获取、创建、修改和删除资源的操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法,这种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。REST架构尤其适用于完全无状态的CRUD(Create、Read、Update、Delete,创建、读取、更新、删除)操作。 +- REST简单而直观,把HTTP协议利用到了极限,在这种思想指导下,它甚至用HTTP请求的头信息来指明资源的表示形式(如果一个资源有多种形式的话,例如人类友善的页面还是机器可读的数据?),用HTTP的错误机制来返回访问资源的错误。由此带来的直接好处是构建的成本减少了,例如用URI定位每一个资源可以利用通用成熟的技术,而不用再在服务器端开发一套资源访问机制。又如只需简单配置服务器就能规定资源的访问权限,例如通过禁止非GET访问把资源设成只读。 + +- 1.面向资源的接口设计 +- 所有的接口设计都是针对资源来设计的,也就很类似于我们的面向对象和面向过程的设计区别,只不过现在将网络上的操作实体都作为资源来看待,同时URI的设计也是体现了对于资源的定位设计。后面会提到有一些网站的API设计说是REST设计,其实是RPC-REST的混合体,并非是REST的思想。 + +- 2.抽象操作为基础的CRUD +- 这点很简单,Http中的get,put,post,delete分别对应了read,update,create,delete四种操作,如果仅仅是作为对于资源的操作,抽象成为这四种已经足够了,但是对于现在的一些复杂的业务服务接口设计,可能这样的抽象未必能够满足。其实这也在后面的几个网站的API设计中暴露了这样的问题,如果要完全按照REST的思想来设计,那么适用的环境将会有限制,而非放之四海皆准的。 +- 3.Http是应用协议而非传输协议 +- 这点在后面各大网站的API分析中有很明显的体现,其实有些网站已经走到了SOAP的老路上,说是REST的理念设计,其实是作了一套私有的SOAP协议,因此称之为REST风格的自定义SOAP协议。 + +- 4.无状态,自包含 +- 这点其实不仅仅是对于REST来说的,作为接口设计都需要能够做到这点,也是作为可扩展和高效性的最基本的保证,就算是使用SOAP的WebService也是一样。 + +# Git +# git init +- 在本地新建一个repo,进入一个项目目录,执行git init,会初始化一个repo,并在当前文件夹下创建一个.git文件夹. +# git clone +- 获取一个url对应的远程Git repo, 创建一个local copy. +- 一般的格式是git clone [url]. +- clone下来的repo会以url最后一个斜线后面的名称命名,创建一个文件夹,如果想要指定特定的名称,可以git clone [url] newname指定. +# git status +- 查询repo的状态. +- git status -s: -s表示short, -s的输出标记会有两列,第一列是对staging区域而言,第二列是对working目录而言. +- +# git log +- show commit history of a branch. +- git log --oneline --number: 每条log只显示一行,显示number条. +- git log --oneline --graph:可以图形化地表示出分支合并历史. +- git log branchname可以显示特定分支的log. +- git log --oneline branch1 ^branch2,可以查看在分支1,却不在分支2中的提交.^表示排除这个分支(Window下可能要给^branch2加上引号). +- git log --decorate会显示出tag信息. +- git log --author=[author name] 可以指定作者的提交历史. +- git log --since --before --until --after 根据提交时间筛选log. +- --no-merges可以将merge的commits排除在外. +- git log --grep 根据commit信息过滤log: git log --grep=keywords +- 默认情况下, git log --grep --author是OR的关系,即满足一条即被返回,如果你想让它们是AND的关系,可以加上--all-match的option. +- git log -S: filter by introduced diff. +- 比如: git log -SmethodName (注意S和后面的词之间没有等号分隔). +- git log -p: show patch introduced at each commit. +- 每一个提交都是一个快照(snapshot),Git会把每次提交的diff计算出来,作为一个patch显示给你看. +- 另一种方法是git show [SHA]. +- git log --stat: show diffstat of changes introduced at each commit. +- 同样是用来看改动的相对信息的,--stat比-p的输出更简单一些. +- +# git add +- 在提交之前,Git有一个暂存区(staging area),可以放入新添加的文件或者加入新的改动. commit时提交的改动是上一次加入到staging area中的改动,而不是我们disk上的改动. +- git add . +- 会递归地添加当前工作目录中的所有文件. +- +# git diff +- 不加参数的git diff: +- show diff of unstaged changes. +- 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容. +- +- 若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用: +- git diff --cached 命令. +- show diff of staged changes. +- (Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的). +- +- git diff HEAD +- show diff of all staged or unstated changes. +- 也即比较woking directory和上次提交之间所有的改动. +- +- 如果想看自从某个版本之后都改动了什么,可以用: +- git diff [version tag] +- 跟log命令一样,diff也可以加上--stat参数来简化输出. +- +- git diff [branchA] [branchB]可以用来比较两个分支. +- 它实际上会返回一个由A到B的patch,不是我们想要的结果. +- 一般我们想要的结果是两个分支分开以后各自的改动都是什么,是由命令: +- git diff [branchA]…[branchB]给出的. +- 实际上它是:git diff $(git merge-base [branchA] [branchB]) [branchB]的结果. +- +- +# git commit +- 提交已经被add进来的改动. +- git commit -m “the commit message" +- git commit -a 会先把所有已经track的文件的改动add进来,然后提交(有点像svn的一次提交,不用先暂存). 对于没有track的文件,还是需要git add一下. +- git commit --amend 增补提交. 会使用与当前提交节点相同的父节点进行一次新的提交,旧的提交将会被取消. +- +# git reset +- undo changes and commits. +- 这里的HEAD关键字指的是当前分支最末梢最新的一个提交.也就是版本库中该分支上的最新版本. +- git reset HEAD: unstage files from index and reset pointer to HEAD +- 这个命令用来把不小心add进去的文件从staged状态取出来,可以单独针对某一个文件操作: git reset HEAD - - filename, 这个- - 也可以不加. +- git reset --soft +- move HEAD to specific commit reference, index and staging are untouched. +- git reset --hard +- unstage files AND undo any changes in the working directory since last commit. +- 使用git reset —hard HEAD进行reset,即上次提交之后,所有staged的改动和工作目录的改动都会消失,还原到上次提交的状态. +- 这里的HEAD可以被写成任何一次提交的SHA-1. +- 不带soft和hard参数的git reset,实际上带的是默认参数mixed. +- +- 总结: +- git reset --mixed id,是将git的HEAD变了(也就是提交记录变了),但文件并没有改变,(也就是working tree并没有改变). 取消了commit和add的内容. +- git reset --soft id. 实际上,是git reset –mixed id 后,又做了一次git add.即取消了commit的内容. +- git reset --hard id.是将git的HEAD变了,文件也变了. +- 按改动范围排序如下: +- soft (commit) < mixed (commit + add) < hard (commit + add + local working) +- +# git revert +- 反转撤销提交.只要把出错的提交(commit)的名字(reference)作为参数传给命令就可以了. +- git revert HEAD: 撤销最近的一个提交. +- git revert会创建一个反向的新提交,可以通过参数-n来告诉Git先不要提交. +- +# git rm +- git rm file: 从staging区移除文件,同时也移除出工作目录. +- git rm --cached: 从staging区移除文件,但留在工作目录中. +- git rm --cached从功能上等同于git reset HEAD,清除了缓存区,但不动工作目录树. +- +# git clean +- git clean是从工作目录中移除没有track的文件. +- 通常的参数是git clean -df: +- -d表示同时移除目录,-f表示force,因为在git的配置文件中, clean.requireForce=true,如果不加-f,clean将会拒绝执行. +- +# git mv +- git rm - - cached orig; mv orig new; git add new +- +# git stash +- 把当前的改动压入一个栈. +- git stash将会把当前目录和index中的所有改动(但不包括未track的文件)压入一个栈,然后留给你一个clean的工作状态,即处于上一次最新提交处. +- git stash list会显示这个栈的list. +- git stash apply:取出stash中的上一个项目(stash@{0}),并且应用于当前的工作目录. +- 也可以指定别的项目,比如git stash apply stash@{1}. +- 如果你在应用stash中项目的同时想要删除它,可以用git stash pop +- +- 删除stash中的项目: +- git stash drop: 删除上一个,也可指定参数删除指定的一个项目. +- git stash clear: 删除所有项目. +- +# git branch +- git branch可以用来列出分支,创建分支和删除分支. +- git branch -v可以看见每一个分支的最后一次提交. +- git branch: 列出本地所有分支,当前分支会被星号标示出. +- git branch (branchname): 创建一个新的分支(当你用这种方式创建分支的时候,分支是基于你的上一次提交建立的). +- git branch -d (branchname): 删除一个分支. +- 删除remote的分支: +- git push (remote-name) :(branch-name): delete a remote branch. +- 这个是因为完整的命令形式是: +- git push remote-name local-branch:remote-branch +- 而这里local-branch的部分为空,就意味着删除了remote-branch +- +# git checkout +-   git checkout (branchname) +- 切换到一个分支. +- git checkout -b (branchname): 创建并切换到新的分支. +- 这个命令是将git branch newbranch和git checkout newbranch合在一起的结果. +- checkout还有另一个作用:替换本地改动: +- git checkout -- +- 此命令会使用HEAD中的最新内容替换掉你的工作目录中的文件.已添加到暂存区的改动以及新文件都不会受到影响. +- 注意:git checkout filename会删除该文件中所有没有暂存和提交的改动,这个操作是不可逆的. +- +# git merge +- 把一个分支merge进当前的分支. +- git merge [alias]/[branch] +- 把远程分支merge到当前分支. +- +- 如果出现冲突,需要手动修改,可以用git mergetool. +- 解决冲突的时候可以用到git diff,解决完之后用git add添加,即表示冲突已经被resolved. +- +# git tag +- tag a point in history as import. +- 会在一个提交上建立永久性的书签,通常是发布一个release版本或者ship了什么东西之后加tag. +- 比如: git tag v1.0 +- git tag -a v1.0, -a参数会允许你添加一些信息,即make an annotated tag. +- 当你运行git tag -a命令的时候,Git会打开一个编辑器让你输入tag信息. +- +- 我们可以利用commit SHA来给一个过去的提交打tag: +- git tag -a v0.9 XXXX +- +- push的时候是不包含tag的,如果想包含,可以在push时加上--tags参数. +- fetch的时候,branch HEAD可以reach的tags是自动被fetch下来的, tags that aren’t reachable from branch heads will be skipped.如果想确保所有的tags都被包含进来,需要加上--tags选项. +- +# git remote +- list, add and delete remote repository aliases. +- 因为不需要每次都用完整的url,所以Git为每一个remote repo的url都建立一个别名,然后用git remote来管理这个list. +- git remote: 列出remote aliases. +- 如果你clone一个project,Git会自动将原来的url添加进来,别名就叫做:origin. +- git remote -v:可以看见每一个别名对应的实际url. +- git remote add [alias] [url]: 添加一个新的remote repo. +- git remote rm [alias]: 删除一个存在的remote alias. +- git remote rename [old-alias] [new-alias]: 重命名. +- git remote set-url [alias] [url]:更新url. 可以加上—push和fetch参数,为同一个别名set不同的存取地址. +- +# git fetch +- download new branches and data from a remote repository. +- 可以git fetch [alias]取某一个远程repo,也可以git fetch --all取到全部repo +- fetch将会取到所有你本地没有的数据,所有取下来的分支可以被叫做remote branches,它们和本地分支一样(可以看diff,log等,也可以merge到其他分支),但是Git不允许你checkout到它们. +- +# git pull +- fetch from a remote repo and try to merge into the current branch. +- pull == fetch + merge FETCH_HEAD +- git pull会首先执行git fetch,然后执行git merge,把取来的分支的head merge到当前分支.这个merge操作会产生一个新的commit. +- 如果使用--rebase参数,它会执行git rebase来取代原来的git merge. +- +- +# git rebase +- --rebase不会产生合并的提交,它会将本地的所有提交临时保存为补丁(patch),放在”.git/rebase”目录中,然后将当前分支更新到最新的分支,最后把保存的补丁应用到分支上。本地的所有提交记录会被丢弃。 +- rebase的过程中,也许会出现冲突,Git会停止rebase并让你解决冲突,在解决完冲突之后,用git add去更新这些内容,然后无需执行commit,只需要: +- git rebase --continue就会继续打余下的补丁. +- git rebase --abort将会终止rebase,当前分支将会回到rebase之前的状态. +- +# git push +- push your new branches and data to a remote repository. +- git push [alias] [branch] +- 将会把当前分支merge到alias上的[branch]分支.如果分支已经存在,将会更新,如果不存在,将会添加这个分支. +- 如果有多个人向同一个remote repo push代码, Git会首先在你试图push的分支上运行git log,检查它的历史中是否能看到server上的branch现在的tip,如果本地历史中不能看到server的tip,说明本地的代码不是最新的,Git会拒绝你的push,让你先fetch,merge,之后再push,这样就保证了所有人的改动都会被考虑进来. +- +# git reflog +- git reflog是对reflog进行管理的命令,reflog是git用来记录引用变化的一种机制,比如记录分支的变化或者是HEAD引用的变化. +- 当git reflog不指定引用的时候,默认列出HEAD的reflog. +- HEAD@{0}代表HEAD当前的值,HEAD@{3}代表HEAD在3次变化之前的值. +- git会将变化记录到HEAD对应的reflog文件中,其路径为.git/logs/HEAD, 分支的reflog文件都放在.git/logs/refs目录下的子目录中. + +- + +# AJAX +# 1、AJAX概述 +# 5.54 1.1 什么是AJAX +- AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML)。 +- AJAX还有一个最大的特点就是,当服务器响应时,不用刷新整个浏览器页面,而是可以局部刷新。这一特点给用户的感受是在不知不觉中完成请求和响应过程。 +- 与服务器异步交互; +- 浏览器页面局部刷新; +# 5.55 1.2. 同步交互与异步交互 +- 同步交互:客户端发出一个请求后,需要等待服务器响应结束后,才能发出第二个请求; +- 异步交互:客户端发出一个请求后,无需等待服务器响应结束,就可以发出第二个请求。 + +# 5.56 1.3. AJAX常见应用情景 + + +- 当我们在百度中输入一个“传”字后,会马上出现一个下拉列表!列表中显示的是包含“传”字的10个关键字。 +- 其实这里就使用了AJAX技术!当文件框发生了输入变化时,浏览器会使用AJAX技术向服务器发送一个请求,查询包含“传”字的前10个关键字,然后服务器会把查询到的结果响应给浏览器,最后浏览器把这10个关键字显示在下拉列表中。 +- 整个过程中页面没有刷新,只是刷新页面中的局部位置而已! +- 当请求发出后,浏览器还可以进行其他操作,无需等待服务器的响应! + + + +- 当输入用户名后,把光标移动到其他表单项上时,浏览器会使用AJAX技术向服务器发出请求,服务器会查询名为zhangSan的用户是否存在,最终服务器返回true表示名为zhangSan的用户已经存在了,浏览器在得到结果后显示“用户名已被注册!”。 +- 整个过程中页面没有刷新,只是局部刷新了; +- 在请求发出后,浏览器不用等待服务器响应结果就可以进行其他操作; +# 5.57 1.4 AJAX的优缺点 +- 优点: +- AJAX使用Javascript技术向服务器发送异步请求; +- AJAX无须刷新整个页面; +- 因为服务器响应内容不再是整个页面,而是页面中的局部,所以AJAX性能高; +- 缺点: +- AJAX并不适合所有场景,很多时候还是要使用同步交互; +- AJAX虽然提高了用户体验,但无形中向服务器发送的请求次数增多了,导致服务器压力增大; +- 因为AJAX是在浏览器中使用Javascript技术完成的,所以还需要处理浏览器兼容性问题; + +# 2、AJAX技术 +# 5.58 2.1 AJAX第一例(发送GET请求) +## 2.1.1 准备工作 +- 因为AJAX也需要请求服务器,异步请求也是请求服务器,所以我们需要先写好服务器端代码,即编写一个Servlet! +- 这里,Servlet很简单,只需要输出“Hello AJAX!”。 +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("Hello AJAX!"); + response.getWriter().print("Hello AJAX!"); + } +} + +## 2.1.2 AJAX核心(XMLHttpRequest) +- 其实AJAX就是在Javascript中多添加了一个对象:XMLHttpRequest对象。所有的异步交互都是使用XMLHttpRequest对象完成的。也就是说,我们只需要学习一个Javascript的新对象即可。 +- 注意,各个浏览器对XMLHttpRequest的支持也是不同的!大多数浏览器都支持DOM2规范,都可以使用:var xmlHttp = new XMLHttpRequest()来创建对象 +- 为了处理浏览器兼容问题,给出下面方法来创建XMLHttpRequest对象: + function createXMLHttpRequest() { + var xmlHttp; + // 适用于大多数浏览器,以及IE7和IE更高版本 + try{ + xmlHttp = new XMLHttpRequest(); + } catch (e) { + // 适用于IE6 + try { + xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + // 适用于IE5.5,以及IE更早版本 + try{ + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e){} + } + } + return xmlHttp; + } + +## 2.1.3 打开与服务器的连接(open方法) +- 当得到XMLHttpRequest对象后,就可以调用该对象的open()方法打开与服务器的连接了。open()方法的参数如下: +- open(method, url, async): +- method:请求方式,通常为GET或POST; +- url:请求的服务器地址,例如:/ajaxdemo1/AServlet,若为GET请求,还可以在URL后追加参数; +- async:这个参数可以不给,默认值为true,表示异步请求; + + var xmlHttp = createXMLHttpRequest(); + xmlHttp.open("GET", "/ajaxdemo1/AServlet", true); + +## 2.1.4 发送请求 +- 当使用open打开连接后,就可以调用XMLHttpRequest对象的send()方法发送请求了。send()方法的参数为POST请求参数,即对应HTTP协议的请求体内容,若是GET请求,需要在URL后连接参数。 +- 注意:若没有参数,需要给出null为参数!若不给出null为参数,可能会导致FireFox浏览器不能正常发送请求! + xmlHttp.send(null); + +## 2.1.5 接收服务器响应 +- 当请求发送出去后,服务器端Servlet就开始执行了,但服务器端的响应还没有接收到。接下来我们来接收服务器的响应。 +- XMLHttpRequest对象有一个onreadystatechange事件,它会在XMLHttpRequest对象的状态发生变化时被调用。下面介绍一下XMLHttpRequest对象的5种状态: +- 0:初始化未完成状态,只是创建了XMLHttpRequest对象,还未调用open()方法; +- 1:请求已开始,open()方法已调用,但还没调用send()方法; +- 2:请求发送完成状态,send()方法已调用; +- 3:开始读取服务器响应; +- 4:读取服务器响应结束。 + +- onreadystatechange事件会在状态为1、2、3、4时引发。 +-   下面代码会被执行四次!对应XMLHttpRequest的四种状态! + xmlHttp.onreadystatechange = function() { + alert('hello'); + }; + +- 但通常我们只关心最后一种状态,即读取服务器响应结束时,客户端才会做出改变。我们可以通过XMLHttpRequest对象的readyState属性来得到XMLHttpRequest对象的状态。 + xmlHttp.onreadystatechange = function() { + if(xmlHttp.readyState == 4) { + alert('hello'); + } + }; + +- 其实我们还要关心服务器响应的状态码是否为200,其服务器响应为404,或500,那么就表示请求失败了。我们可以通过XMLHttpRequest对象的status属性得到服务器的状态码。 +- 最后,我们还需要获取到服务器响应的内容,可以通过XMLHttpRequest对象的responseText得到服务器响应内容。 +- responsXML是xml格式的文本,是document对象 + xmlHttp.onreadystatechange = function() { + if(xmlHttp.readyState == 4 && xmlHttp.status == 200) { + alert(xmlHttp.responseText); + } + }; + +## 2.1.6 AJAX第一例小结 +- 创建XMLHttpRequest对象; +- 调用open()方法打开与服务器的连接; +- 调用send()方法发送请求; +- 为XMLHttpRequest对象指定onreadystatechange事件函数,这个函数会在XMLHttpRequest的1、2、3、4,四种状态时被调用; +- XMLHttpRequest对象的5种状态: +- 0:初始化未完成状态,只是创建了XMLHttpRequest对象,还未调用open()方法; +- 1:请求已开始,open()方法已调用,但还没调用send()方法; +- 2:请求发送完成状态,send()方法已调用; +- 3:开始读取服务器响应; +- 4:读取服务器响应结束。 +- 通常我们只关心4状态。 +- XMLHttpRequest对象的status属性表示服务器状态码,它只有在readyState为4时才能获取到。 +- XMLHttpRequest对象的responseText属性表示服务器响应内容,它只有在readyState为4时才能获取到! +- +- +-

+- + + +``` +public class AServlet extends HttpServlet { +``` + + +``` + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +``` + +- response.setContentType("text/html;charset=utf-8"); +- response.getWriter().print("hehe"); +- } +- } + + +# 5.59 2.2 AJAX第二例(发送POST请求) +## 2.2.1 发送POST请求注意事项 +- POST请求必须设置ContentType请求头的值为application/x-www.form-encoded。表单的enctype默认值就是为application/x-www.form-encoded!因为默认值就是这个,所以大家可能会忽略这个值!当设置了
的enctype=” application/x-www.form-encoded”时,等同与设置了Cotnent-Type请求头。 +- 但在使用AJAX发送请求时,就没有默认值了,这需要我们自己来设置请求头: +xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + +- 当没有设置Content-Type请求头为application/x-www-form-urlencoded时,Web容器会忽略请求体的内容。所以,在使用AJAX发送POST请求时,需要设置这一请求头,然后使用send()方法来设置请求体内容。 +xmlHttp.send("b=B"); + +-   这时Servlet就可以获取到这个参数!!! + +- AServlet + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println(request.getParameter("b")); + System.out.println("Hello AJAX!"); + response.getWriter().print("Hello AJAX!"); + } + +- ajax2.jsp + +

AJAX2

+ +
+ +# 5.60 2.3 AJAX第三例(用户名是否已被注册) +## 2.3.1 功能介绍 +- 在注册表单中,当用户填写了用户名后,把光标移开后,会自动向服务器发送异步请求。服务器返回true或false,返回true表示这个用户名已经被注册过,返回false表示没有注册过。 +- 客户端得到服务器返回的结果后,确定是否在用户名文本框后显示“用户名已被注册”的错误信息! + +## 2.3.2 案例分析 +- regist.jsp页面中给出注册表单; +- 在username表单字段中添加onblur事件,调用send()方法; +- send()方法获取username表单字段的内容,向服务器发送异步请求,参数为username; +- RegistServlet:获取username参数,判断是否为“itcast”,如果是响应true,否则响应false; + +## 2.3.3 代码 +- regist.jsp + + + 用户名:
+ +
+ +- RegistServlet.java + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + System.out.println(username); + if(username.equals("admin")){ + response.getWriter().print(false); + }else{ + response.getWriter().print(true); + } + } + +# 前端 +- HTML的DOM对象说几个,Document的对象和方法 +document.body 返回元素 1 +document.cookie 返回或设置与当前文档相关的cookie 1 +document.domain 返回当前文档的服务器域名 1 +document.referrer 返回连接至当前文档的文档连接 1 +document.title 返回当前文档的元素 1 +document.URL 返回当前文档的完整URL 1 + + From d2a4dcfd26810b68105e5dbbdd1684edd723e457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:40:00 +0800 Subject: [PATCH 70/97] Create 8.Mysql --- docs/8.Mysql | 3743 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3743 insertions(+) create mode 100644 docs/8.Mysql diff --git a/docs/8.Mysql b/docs/8.Mysql new file mode 100644 index 00000000..739f82c5 --- /dev/null +++ b/docs/8.Mysql @@ -0,0 +1,3743 @@ +# 数据库理论 +# OLTP与OLAP +# 8.1 OLTP(关系型数据库) +- OLTP即联机事务处理,就是我们经常说的关系数据库,意即记录即时的增、删、改、查,就是我们经常应用的东西,这是数据库的基础 +- 对于各种数据库系统环境中大家最常见的OLTP系统,其特点是并发量大,整体数据量比较多,但每次访问的数据比较少,且访问的数据比较离散,活跃数据占总体数据的比例不是太大。对于这类系统的数据库实际上是最难维护,最难以优化的,对主机整体性能要求也是最高的。因为不仅访问量很高,数据量也不小。 +- +- 针对上面的这些特点和分析,我们可以对OLTP的得出一个大致的方向。 +- 虽然系统总体数据量较大,但是系统活跃数据在数据总量中所占的比例不大,那么我们可以通过扩大内存容量来尽可能多的将活跃数据cache到内存中; +- 虽然IO访问非常频繁,但是每次访问的数据量较少且很离散,那么我们对磁盘存储的要求是IOPS(Input/Output Operations Per Second,即每秒进行读写操作的次数)表现要很好,吞吐量是次要因素; +- 并发量很高,CPU每秒所要处理的请求自然也就很多,所以CPU处理能力需要比较强劲; +- 虽然与客户端的每次交互的数据量并不是特别大,但是网络交互非常频繁,所以主机与客户端交互的网络设备对流量能力也要求不能太弱。 +- + +# 8.2 OLAP(数据分析挖掘) +- 用于数据分析的OLAP系统的主要特点就是数据量非常大,并发访问不多,但每次访问所需要检索的数据量都比较多,而且数据访问相对较为集中,没有太明显的活跃数据概念。 +- +- OLAP即联机分析处理,是数据仓库的核心部心,所谓数据仓库是对于大量已经由OLTP形成的数据的一种分析型的数据库,用于处理商业智能、决策支持等重要的决策信息;数据仓库是在数据库应用到一定程序之后而对历史数据的加工与分析 +- 基于OLAP系统的各种特点和相应的分析,针对OLAP系统硬件优化的大致策略如下: +- 数据量非常大,所以磁盘存储系统的单位容量需要尽量大一些; +- 单次访问数据量较大,而且访问数据比较集中,那么对IO系统的性能要求是需要有尽可能大的每秒IO吞吐量,所以应该选用每秒吞吐量尽可能大的磁盘; +- 虽然IO性能要求也比较高,但是并发请求较少,所以CPU处理能力较难成为性能瓶颈,所以CPU处理能力没有太苛刻的要求; +- +- 虽然每次请求的访问量很大,但是执行过程中的数据大都不会返回给客户端,最终返回给客户端的数据量都较小,所以和客户端交互的网络设备要求并不是太高; +- +- 此外,由于OLAP系统由于其每次运算过程较长,可以很好的并行化,所以一般的OLAP系统都是由多台主机构成的一个集群,而集群中主机与主机之间的数据交互量一般来说都是非常大的,所以在集群中主机之间的网络设备要求很高。 +- + +# 数据库完整性 +# 8.3 实体完整性 +- primary key (列级约束和表级约束) +- 定义主码之后,每当用户程序对基本表插入一条记录或对主码列进行更新操作时,DBMS将会检查 + - 1)检查主码值是否唯一:一种方法是全表扫描,耗时长;DBMS一般在主码上自动建立一个索引,通过索引查找基本表中是否已经存在新的主码值将大大提供效率。 + - 2)检查主码值是否为空 + +# 8.4 参照完整性 +- foreign key references ... + + +- 当上述的不一致发生时,系统可以采用以下策略加以处理: + - 1)拒绝执行(No action) + - 2)级联操作(Cascade):当删除或修改被参照表时的一个元组导致与参照表不同时,删除或修改参照表中的所有导致不一致的元组。 + - 3)设置为空值:当删除或修改被参照表时的一个元组导致与参照表不同时,则将参照表中 +- 的所有造成不一致的元组的对应属性设置为空值。 + +- + +- 关系查询处理和查询优化 +- 关系数据库系统的查询处理 +- 查询处理是RDBMS执行查询语句的过程,其任务是把用户提交给RDBMS的查询语句转换为高效的查询执行计划。 +- 查询处理步骤 +- 查询处理分为4个阶段:查询分析、查询检查、查询优化和查询执行。 +- 1、查询分析(语法) +- 对查询语句进行扫描、词法分析、语法分析 +- 2、查询检查(语义) +- 对合法的查询语句进行语义检查,即根据数据字典中有关的模式定义检查语句中的数据库对象,如关系名、属性名等是否存在和有效。然后进行安全性、完整性检查。检查通过后把SQL语句转换成等价的关系代数表达式。RDBMS一般采用查询树(语法树)来表示拓展的关系代数表达式。 +- 3、查询优化 +- 查询优化就是选择一个高效执行的查询处理策略。 +- 分为代数优化和物理优化 + - 1)代数优化是指关系代数表达式的优化,即按照一定的规则,通过对关系代数表达式进行等价变换,改变代数表达式中操作的次序和组合,使查询执行更高效 + - 2)物理优化是指通过存取路径和底层操作算法的选择进行的优化 +- 选择的依据可以是基于规则的、基于代价的、基于语义的。 +- 4、查询执行 +- 依据优化器得到的执行策略生成查询执行计划,由代码生成器生成执行这个查询计划的代码,然后加以执行,回送查询结果。 +- 实现查询操作的算法示例 +### 1、选择操作的实现 + - (1) 全表扫描方法 (Table Scan) +- 对查询的基本表顺序扫描,逐一检查每个元组是否满足选择条件,把满足条件的元组作为结果输出 +- 适合小表,不适合大表 + - (2)索引扫描方法 (Index Scan) +- 适合于选择条件中的属性上有索引(例如B+树索引或Hash索引) +- 通过索引先找到满足条件的元组主码或元组指针,再通过元组指针直接在查询的基本表中找到元组 + + - 1)全表扫描算法 +- 假设可以使用的内存为M块,全表扫描算法思想: +①- 按照物理次序读Student的M块到内存 +②- 检查内存的每个元组t,如果满足选择条件,则输出t +③- 如果student还有其他块未被处理,重复①和② +1)- 索引扫描算法 +- [例9.1-C2] SELECT * FROM Student WHERE Sno='201215121' +- 假设Sno上有索引(或Sno是散列码) +- 算法: +- 使用索引(或散列)得到Sno为‘201215121’ 元组的指针 +- 通过元组指针在Student表中检索到该学生 +- [例9.1-C3] SELECT * FROM Student WHERE Sage>20 +- 假设Sage 上有B+树索引 +- 算法: +- 使用B+树索引找到Sage=20的索引项,以此为入口点在B+树的顺序集上得到Sage>20的所有元组指针 +- 通过这些元组指针到student表中检索到所有年龄大于20的学生。 +- [例9.1-C4] SELECT * FROM Student WHERE Sdept='CS' AND Sage>20; +- 假设Sdept和Sage上都有索引 +- 算法一:分别用索引扫描找到Sdept=’CS’的一组元组指针和Sage>20的另一组元组指针 +- 求这两组指针的交集 +- 到Student表中检索 +- 得到计算机系年龄大于20的学生 +- 算法二:找到Sdept=’CS’的一组元组指针, +- 通过这些元组指针到Student表中检索 +- 并对得到的元组检查另一些选择条件(如Sage>20)是否满足 +- 把满足条件的元组作为结果输出。 +- 当选择率较低时,基于索引的选择算法要优于全表扫描算法。但在某些情况下,例如选择率较高,或者要查找的元组均匀地分布在查找的表中,这是基于索引的选择算法性能不如全表扫描算法。因此除了对表的扫描操作,还要加上对B+树索引的扫描操作,对每一个检索码,从B+树根节点到叶子结点路径上的每个结点都要进行一次IO操作。 + +### 2、连接操作的实现 +- 连接操作是查询处理中最耗时的操作之一 +- 本节只讨论等值连接(或自然连接)最常用的实现算法 + +- [例9.2] SELECT * FROM Student, SC WHERE Student.Sno=SC.Sno; +#### 1)嵌套循环算法(nested loop join) +- 对外层循环(Student表)的每一个元组(s),检索内层循环(SC表)中的每一个元组(sc) +- 检查这两个元组在连接属性(Sno)上是否相等 +- 如果满足连接条件,则串接后作为结果输出,直到外层循环表中的元组处理完为止。 +#### 2)排序-合并算法(sort-merge join 或merge join) +- 如果连接的表没有排好序,先对Student表和SC表按连接属性Sno排序 +- 取Student表中第一个Sno,依次扫描SC表中具有相同Sno的元组 +- 当扫描到Sno不相同的第一个SC元组时,返回Student表扫描它的下一个元组,再扫描SC表中具有相同Sno的元组,把它们连接起来 +- 重复上述步骤直到Student 表扫描完 + +- Student表和SC表都只要扫描一遍 +- 如果两个表原来无序,执行时间要加上对两个表的排序时间 +- 对于大表,先排序后使用排序-合并连接算法执行连接,总的时间一般仍会减少 +#### 3)索引连接(index join)算法 +- 步骤: +- ① 在SC表上已经建立属性Sno的索引。 +- ② 对Student中每一个元组,由Sno值通过SC的索引查找相应的SC元组。 +- ③ 把这些SC元组和Student元组连接起来 +- 循环执行②③,直到Student表中的元组处理完为止 +- 只有表2需要索引 +#### 4)Hash Join算法 +- 把连接属性作为hash码,用同一个hash函数把Student表和SC表中的元组散列到hash表中。 +- 划分阶段(Build) +- 对包含较少元组的表(如Student表)进行一遍处理 +- 把它的元组按hash函数分散到hash表的桶中 +- 试探阶段(Probe) +- 对另一个表(SC表)进行一遍处理 +- 把SC表的元组也按同一个hash函数(hash码是连接属性)进行散列 +- 把SC元组与桶中来自Student表并与之相匹配的元组连接起来 + +- 将小表转为哈希表,用表1的匹配字段用哈希函数映射到哈希表 + +- 上面hash join算法前提:假设两个表中较小的表在第一阶段后可以完全放入内存的hash桶中 +- 关系数据库系统的查询优化 +- 查询优化在关系数据库系统中有着非常重要的地位 +- 关系查询优化是影响关系数据库管理系统性能的关键因素 +- 由于关系表达式的语义级别很高,使关系系统可以从关系表达式中分析查询语义,提供了执行查询优化的可能性 + +- 查询优化概述 +- 关系系统的查询优化 +- 是关系数据库管理系统实现的关键技术又是关系系统的优点所在 +- 减轻了用户选择存取路径的负担 +- 非关系系统 +- 用户使用过程化的语言表达查询要求,执行何种记录级的操作,以及操作的序列是由用户来决定的 +- 用户必须了解存取路径,系统要提供用户选择存取路径的手段,查询效率由用户的存取策略决定 +- 如果用户做了不当的选择,系统是无法对此加以改进的 + +- 查询优化的优点 +- 1、用户不必考虑如何最好地表达查询以获得较好的效率 +- 2、系统可以比用户程序的“优化”做得更好 + - (1) 优化器可以从数据字典中获取许多统计信息,而用户程序则难以获得这些信息。 + - (2)如果数据库的物理统计信息改变了,系统可以自动对查询重新优化以选择相适应的执行计划。在非关系系统中必须重写程序,而重写程序在实际应用中往往是不太可能的。 + - (3)优化器可以考虑数百种不同的执行计划,程序员一般只能考虑有限的几种可能性。 + - (4)优化器中包括了很多复杂的优化技术,这些优化技术往往只有最好的程序员才能掌握。系统的自动优化相当于使得所有人都拥有这些优化技术。 + +- 关系数据库管理系统通过某种代价模型计算出各种查询执行策略的执行代价,然后选取代价最小的执行方案 + +- 查询优化的总目标 +- 选择有效的策略 +- 求得给定关系表达式的值 +- 使得查询代价最小(实际上是较小) + +- 一个实例 +- 一个关系查询可以对应不同的执行方案,其效率可能相差非常大。 +- [例9.3] 求选修了2号课程的学生姓名。 +- SELECT Student.Sname FROM Student, SC +- WHERE Student.Sno=SC.Sno AND SC.Cno=’2’ +- 假定学生-课程数据库中有1000个学生记录,10000个选课记录 +- 选修2号课程的选课记录为50个 +- 第一种情况: +- 1、计算笛卡尔积 +- 算法: + - 1)在内存中尽可能多地装入某个表(如Student表)的若干块,留出一块存放另一个表(如SC表)的元组。 + - 2)把SC中的每个元组和Student中每个元组连接,连接后的元组装满一块后就写到中间文件上 + - 3)从SC中读入一块和内存中的Student元组连接,直到SC表处理完。 + - 4)再读入若干块Student元组,读入一块SC元组 + - 5)重复上述处理过程,直到把Student表处理完 + +- 2、作选择操作 +- 依次读入连接后的元组,按照选择条件选取满足要求的记录 +- 假定内存处理时间忽略。读取中间文件花费的时间(同写中间文件一样)需读入106块。 +- 若满足条件的元组假设仅50个,均可放在内存。 +- 3、作投影操作 +- 把第2步的结果在Sname上作投影输出,得到最终结果 +- 第一种情况下执行查询的总读写数据块 +- 第二种情况: +- +- 1、计算自然连接 + - 1)执行自然连接,读取Student和SC表的策略不变,总的读取块数仍为2100块 + - 2)自然连接的结果比第一种情况大大减少,为104个元组 + - 3)写出数据块= 103 块 +- 2、读取中间文件块,执行选择运算,读取的数据块= 103 块 +- 3、把第2步结果投影输出。 +- 第二种情况下执行查询的总读写数据块=2100+ 103 +103 +- 其执行代价大约是第一种情况的488分之一 +- 第三种情况: +- +- 1、先对SC表作选择运算,只需读一遍SC表,存取100块,因为满足条件的元组仅50个,不必使用中间文件。 +- 2、读取Student表,把读入的Student元组和内存中的SC元组作连接。也只需读一遍Student表共100块。 +- 3、把连接结果投影输出 +- 第三种情况总的读写数据块=100+100 +- 其执行代价大约是第一种情况的万分之一,是第二种情况的20分之一 +- 假如SC表的Cno字段上有索引 +- 第一步就不必读取所有的SC元组而只需读取Cno=‘2’的那些元组(50个) +- 存取的索引块和SC中满足条件的数据块大约总共3~4块 +- 若Student表在Sno上也有索引 +- 不必读取所有的Student元组 +- 因为满足条件的SC记录仅50个,涉及最多50个Student记录 +- 读取Student表的块数也可大大减少 + + +- 有选择和连接操作时,先做选择操作,这样参加连接的元组就可以大大减少,这是代数优化 + +- 在Q3中SC表的选择操作算法有全表扫描或索引扫描,经过初步估算,索引扫描方法较优。 +- 对于Student和SC表的连接,利用Student表上的索引,采用索引连接代价也较小,这就是物理优化。 +- 代数优化 +- 关系代数表达式等价变换规则 +- 代数优化策略:通过对关系代数表达式的等价变换来提高查询效率 +- 关系代数表达式的等价:指用相同的关系代替两个表达式中相应的关系所得到的结果是相同的 +- 两个关系表达式E1和E2是等价的,可记为E1≡E2 +- 常用的代数变换规则: +- 1.连接、笛卡尔积交换律 + +- 2. 连接、笛卡尔积的结合律 + +- 3.投影的串接定律 + +- 减少IO次数 +- 4. 选择的串接定律 + +- 合并条件 +- 5. 选择与投影操作的交换律 + +- 先选择后投影效率更高 +- 6. 选择与笛卡尔积的交换律 + +- 减少IO次数 +- 7. 选择与并的分配律 + +- 8. 选择与差运算的分配律 + +- 9. 选择对自然连接的分配律 + +- 10. 投影与笛卡尔积的分配律 + +- 11. 投影与并的分配律 + +- 查询树的启发式优化 +- 典型的启发式规则 + - (1)选择运算应尽可能先做 +- 在优化策略中这是最重要、最基本的一条。 + - (2)把投影运算和选择运算同时进行 +- 如有若干投影和选择运算,并且它们都对同一个关系操作,则可以在扫描此关系的同时完成所有的这些运算以避免重复扫描关系。 + - (3) 把投影同其前或其后的双目运算结合起来,没有必要为了去掉某些字段而扫描一遍关系。 + - (4) 把某些选择同在它前面要执行的笛卡尔积结合起来成为一个连接运算,连接特别是等值连接运算要比同样关系上的笛卡尔积省很多时间。 + - (5) 找出公共子表达式 +- 如果这种重复出现的子表达式的结果不是很大的关系,并且从外存中读入这个关系比计算该子表达式的时间少得多 +- 则先计算一次公共子表达式并把结果写入中间文件是合算的。 +- 当查询的是视图时,定义视图的表达式就是公共子表达式的情况 + +- 遵循这些启发式规则,应用9.3.1的等价变换公式来优化关系表达式的算法。 +- 算法:关系表达式的优化 +- 输入:一个关系表达式的查询树 +- 输出:优化的查询树 +- 方法: + - (1)利用等价变换规则4把形如σF1∧F2∧…∧Fn(E)变换为 +- σF1(σF2(…(σFn(E))…))。(分开选择条件) +- + - (2)对每一个选择,利用等价变换规则4~9尽可能把它移到树的叶端。 +- (选择先做) + + - (3)对每一个投影利用等价变换规则3,5,10,11中的一般形式尽可能把它移向树的叶端。 +- 注意: +- 等价变换规则3使一些投影消失或使一些投影出现 +- 规则5把一个投影分裂为两个,其中一个有可能被移向树的叶端 + - (4)利用等价变换规则3~5,把选择和投影的串接合并成单个选择、单个投影或一个选择后跟一个投影,使多个选择或投影能同时执行,或在一次扫描中全部完成 +- (选择投影一起做) + - (5)把上述得到的语法树的内节点分组。 +- 每一双目运算()和它所有的直接祖先为一组(这些直接祖先是(σ,π运算)。 +- 如果其后代直到叶子全是单目运算,则也将它们并入该组 +- 但当双目运算是笛卡尔积(×),而且后面不是与它组成等值连接的选择时,则不能把选择与这个双目运算组成同一组 +- 示例: + - 1)把SQL语句转换成查询树 + +- 为了使用关系代数表达式的优化法,假设内部表示是关 +- 系代数语法树,则上面的查询树如图 + + - 2)对查询树进行优化 +- 利用规则4、6把选择σSC.Cno=‘2’移到叶端,图9.4查询树便转换成下图优化的查询树。这就是9.2.2节中Q3的查询树表示。 + +- 物理优化 +- 代数优化改变查询语句中操作的次序和组合,不涉及底层的存取路径 +- 对于一个查询语句有许多存取方案,它们的执行效率不同, 仅仅进行代数优化是不够的 +- 物理优化就是要选择高效合理的操作算法或存取路径,求得优化的查询计划 + +- 物理优化方法 + - 1)基于规则的启发式优化 +- 启发式规则是指那些在大多数情况下都适用,但不是在每种情况下都是最好的规则。 + - 2)基于代价估算的优化 +- 优化器估算不同执行策略的代价,并选出具有最小代价的执行计划。 + - 3)两者结合的优化方法: +- 常常先使用启发式规则,选取若干较优的候选方案,减少代价估算的工作量 +- 然后分别计算这些候选方案的执行代价,较快地选出最终的优化方案 +- 基于启发式规则的存取路径选择优化(定性) +- 1.选择操作的启发式规则 +- 对于小关系,使用全表顺序扫描,即使选择列上有索引 +- 对于大关系,启发式规则有: + - 1)对于选择条件是“主码=值”的查询 +- 查询结果最多是一个元组,可以选择主码索引 +- 一般的RDBMS会自动建立主码索引 + - 2)对于选择条件是“非主属性=值”的查询,并且选择列上有索引 +- 要估算查询结果的元组数目 +- 如果比例较小(<10%)可以使用索引扫描方法,否则还是使用全表顺序扫描 + - 3)对于选择条件是属性上的非等值查询或者范围查询,并且选择列上有索引 +- 要估算查询结果的元组数目 +- 如果比例较小(<10%)可以使用索引扫描方法,否则还是使用全表顺序扫描 + - 4)对于用AND连接的合取选择条件 +- 如果有涉及这些属性的组合索引,优先采用组合索引扫描方法 +- 如果某些属性上有一般的索引,可以用索引扫描方法 +- 通过分别查找满足每个条件的指针,求指针的交集 +- 通过索引查找满足部分条件的元组,然后在扫描这些元组时判断是否满足剩余条件 +- 其他情况:使用全表顺序扫描 + - 5)对于用OR连接的析取选择条件,一般使用全表顺序扫描 +- 2.连接操作的启发式规则 + - 1)如果2个表都已经按照连接属性排序:选用排序-合并算法 + - 2)如果一个表在连接属性上有索引,选用索引连接算法 + - 3)如果上面2个规则都不适用,其中一个表较小,选用Hash join算法 + - 4)可以选用嵌套循环方法,并选择其中较小的表,确切地讲是占用的块数(b)较少的表,作为外表(外循环的表) +- 理由: +- 设连接表R与S分别占用的块数为Br与Bs +- 连接操作使用的内存缓冲区块数为K +- 分配K-1块给外表 + - 如果R为外表,则嵌套循环法存取的块数为Br+BrBs/(K-1) +- 显然应该选块数小的表作为外表 + +- +- 基于代价估算的优化(定量) +- 启发式规则优化是定性的选择,适合解释执行的系统。因为解释执行的系统,优化开销包含在查询总开销之中。 +- 编译执行的系统中查询优化和查询执行是分开的,因此可以采用精细复杂一些的基于代价的优化方法 + +- 1.统计信息 +- 基于代价的优化方法要计算查询的各种不同执行方案的执行代价,它与数据库的状态密切相关 +- 优化器需要的统计信息 + - (1)对每个基本表 +①- 该表的元组总数(N) +②- 元组长度(l) +③- 占用的块数(B) +④- 占用的溢出块数(BO) + - (2)对基表的每个列 +①- 该列不同值的个数(m) +②- 列最大值 +③- 最小值 +④- 列上是否已经建立了索引 +⑤- 哪种索引(B+树索引、Hash索引、聚集索引) +⑥- 可以计算选择率(f) +- 如果不同值的分布是均匀的,f=1/m +- 如果不同值的分布不均匀,则要计算每个值的选择率, +- f=具有该值的元组数/N + - (3)对索引 +①- 索引的层数(L) +②- 不同索引值的个数 +③- 索引的选择基数S(有S个元组具有某个索引值) +④- 索引的叶结点数(Y) +- 2.代价估算示例 + - 1)全表扫描算法的代价估算公式 +- 如果基本表大小为B块,全表扫描算法的代价 cost=B +- 如果选择条件是“码=值”,那么平均搜索代价 cost=B/2 (可能是第一块也可能是最后一块) + - 2)索引扫描算法的代价估算公式 +- 如果选择条件是“码=值”,则采用该表的主索引 +- 若为B+树,层数为L,需要存取B+树中从根结点到叶结点L块,再加上基本表中该元组所在的那一块,所以cost=L+1 +- 如果选择条件涉及非码属性 +- 若为B+树索引,选择条件是相等比较,S是索引的选择基数(有S个元组满足条件) +- 满足条件的元组可能会保存在不同的块上,所以(最坏的情况)cost=L+S +- 如果比较条件是>,>=,<,<=操作 +- 假设有一半的元组满足条件,就要存取一半的叶结点 +- 通过索引访问一半的表存储块 +- cost=L+Y/2+B/2 +- 如果可以获得更准确的选择基数,可以进一步修正Y/2与B/2 + - 3)嵌套循环连接算法的代价估算公式 + - 嵌套循环连接算法的代价 cost=Br+BrBs/(K-1) + - 如果需要把连接结果写回磁盘 cost=Br+Br Bs/(K-1)+(Frs*Nr*Ns)/Mrs +- 其中Frs为连接选择性(join selectivity),表示连接结果元组数的比例 +- Mrs是存放连接结果的块因子,表示每块中可以存放的结果元组数目 + - 4)排序-合并连接算法的代价估算公式 +- 如果连接表已经按照连接属性排好序,则cost=Br+Bs+(Frs*Nr*Ns)/Mrs +- 如果必须对文件排序还需要在代价函数中加上排序的代价 +- 对于包含B个块的文件排序的代价大约是 (2*B)+(2*B*log2B) +- 小结: +- 查询处理是关系数据库管理系统的核心,查询优化技术是查询处理的关键技术 +- 本章主要内容 +- 查询处理过程 +- 查询优化 +- 代数优化 +- 物理优化 +- 查询执行 + +- 比较复杂的查询,尤其是涉及连接和嵌套的查询 +- 不要把优化的任务全部放在RDBMS上 +- 应该找出RDBMS的优化规律,以写出适合RDBMS自动优化的SQL语句 +- 对于RDBMS不能优化的查询需要重写查询语句,进行手工调整以优化性能 +- + +# 事务与数据库恢复技术 +- 事务处理技术包括数据库恢复技术和并发控制技术。 +- 数据库恢复机制和并发控制机制是DBMS的重要组成部分。 +# 8.5 事务的基本概念 +- 1.事务:是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。 +- 事务和程序比较 +- 在关系数据库中,一个事务可以是一条或多条SQL语句,也可以包含一个或多个程序。 +- 一个程序通常包含多个事务 +- 显式定义方式: +- begin transaction +- .... +- commit/rollback +- 示例: +- begin transaction +- select * from teacher; +- update teacher +- set title=null +- where tno=‘101’; +- select * from teacher; +- rollback; +- select * from teacher; +- 隐式方式 +- 当用户没有显式地定义事务时,DBMS按缺省规定自动划分事务 +- AutoCommit事务是SQL Server默认事务方式, +- 2.事务的特性(ACID特性) +1.- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做; +2.- 一致性(consistency):事务必须使数据库从一个一致性状态变成另一个一致性状态; +3.- 隔离性(isolation):一个事务的执行不能被其他事务干扰; +4.- 持续性(durability):也称永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的. + +# 8.6 数据库恢复概述 +- 故障是不可避免的 +- 系统故障:计算机软、硬件故障 +- 人为故障:操作员的失误、恶意的破坏等。 +- 数据库的恢复 +- 把数据库从错误状态恢复到某一已知的正确状态(亦称为一致状态或完整状态) +# 8.7 故障的种类 +- 1、事务内部的故障: + - 1)有的是可以通过事务程序本身发现的 + - 2)有的是非预期的,不能由应用程序处理(如运算溢出、死锁等) +- 以后,事务故障仅指非预期的故障 +- 事务故障的恢复:UNDO撤销 +- 2、系统故障 +- 造成系统停止运转的任何事件,使得系统要重新启动。(硬件错误、断电等) +- 影响正在运行的所有事务,但不破坏数据库。此时内存,尤其是数据库缓冲区中的内容全部丢失,所有运行事务非正常终止。 +- 恢复: + - 1)未提交的事务:UNDO撤销所有未完成的事务 + - 2)已提交的事务,但缓冲区内容未完全写入磁盘:REDO重做所有已提交的事务 +- 3、介质故障 +- 系统故障称为软故障,介质故障称为硬故障(外存故障,如磁盘损坏) +- 恢复:装入数据库发生介质故障前某个时刻的数据副本 +- REDO自此时开始的所有成功事务,将这些事务已提交的结果重新写入数据库 +- 4、计算机病毒 +- 计算机病毒是一种人为的故障或破坏,是一些恶作剧者研制的一种计算机程序。 +- 可以繁殖和传播,并造成对计算机系统包括数据库的危害。 + +- 总之:各类故障,对数据库的影响有两种可能性 +- 一是数据库本身被破坏 +- 二是数据库没有被破坏,但数据可能不正确,这是由于事务的运行被非正常终止造成的。 +- + +# 8.8 恢复的实现技术 +- 恢复操作的基本原理:冗余 +- 恢复机制涉及的两个关键问题 +1)- 如何建立冗余数据 +•- 数据转储(backup) +•- 登录日志文件(logging) +2)- 如何利用这些冗余数据实施数据库恢复 +## 数据转储 +- 数据转储定义: +- 转储是指DBA将整个数据库复制到其他存储介质上保存起来的过程,备用的数据称为后备副本或后援副本 +- 如何使用 +1)- 数据库遭到破坏后可以将后备副本重新装入 +2)- 重装后备副本只能将数据库恢复到转储时的状态 +- 转储方法 +1)- 静态转储与动态转储 +2)- 海量转储与增量转储 +- 静态转储: + - 1)定义:在系统中无事务运行时进行的转储操作。转储开始的时刻数据库处于一 致性状态,而转储不允许对数据库的任何存取、修改活动。静态转储得到的一定是一个数据一致性的副本。 + - 2)优点:实现简单 + - 3)缺点:降低了数据库的可用性 +- 转储必须等待正运行的用户事务结束才能进行;新的事务必须等待转储结束才能执行 +- 动态转储: + - 1)定义:转储期间允许对数据库进行存取或修改。转储和用户事务可以并发执行。 + - 2)优点:不用等待正在运行的用户事务结束;不会影响新事务的运行。 + - 3)实现:必须把转储期间各事务对数据库的修改活动登记下来,建立日志文件 后备副本加上日志文件就能把数据库恢复到某一时刻的正确状态。 +- 海量转储: + - 1)定义:每次转储全部数据库 + - 2)特点:从恢复角度,使用海量转储得到的后备副本进行恢复更方便一些。 +- 增量转储: + - 1)定义:每次只转储上一次转储后更新过的数据 + - 2)特点:如果数据库很大,事务处理又十分频繁,则增量转储方式更实用更有效。 + +## 日志文件 +- 1、什么是日志文件 +- 日志文件(log)是用来记录事务对数据库的更新操作的文件 +- 2、日志文件的格式 + - 1)以记录为单位: +- 日志文件中需要登记的内容包括: +①- 各个事务的开始标记(BEGIN TRANSACTION) +②- 各个事务的结束标记(COMMIT或ROLLBACK) +③- 各个事务的所有更新操作 +- 以上均作为日志文件中的一个日志记录 +- 每个日志记录的内容: +①- 事务标识(标明是哪个事务) +②- 操作类型(插入、删除或修改) +③- 操作对象(记录内部标识) +④- 更新前数据的旧值(对插入操作而言,此项为空值) +⑤- 更新后数据的新值(对删除操作而言, 此项为空值) + + - 2)以数据块为单位 +- 日志记录内容包括: +- 事务标识(标明是哪个事务) +- 被更新的数据块 +- 3、日志文件的作用: +- 进行事务故障恢复 +- 进行系统故障恢复 +- 协助后备副本进行介质故障恢复 + - 1)事务故障恢复和系统故障恢复必须用日志文件 + - 2)在动态转储方式中必须建立日志文件,后备副本和日志文件结合起来才能有效地恢复数据库 + - 3)静态转储方式中也可以建立日志文件(重新装入后备副本,然后利用日志文件把已完成的事务进行重做,对未完成事务进行撤销) + +- 4、登记日志文件: +- 基本原则 +- 登记的次序严格按并行事务执行的时间次序 +- 必须先写日志文件,后写数据库 +- 为什么要先写日志文件? + - 1)写数据库和写日志文件是两个不同的操作,在这两个操作之间可能发生故障 + - 2)如果先写了数据库修改,而在日志文件中没有登记下这个修改,则以后就无法恢复这个修改了 + - 3)如果先写日志,但没有修改数据库,按日志文件恢复时只不过是多执行一次不必要的UNDO操作,并不会影响数据库的正确性 +# 8.9 恢复策略 +## 事务故障的恢复 +- 事务故障:事务在运行至正常终止点前被终止 +- 恢复方法 +- 由恢复子系统应利用日志文件撤消(UNDO)此事务已对数据库进行的修改 +- 事务故障的恢复由系统自动完成,对用户是透明的,不需要用户干预 +- 事务故障的恢复步骤 +- 1. 反向扫描文件日志,查找该事务的更新操作。 +- 2. 对该事务的更新操作执行逆操作。即将日志记录中“更新前的值” 写入数据库。 +- 插入操作, “更新前的值”为空,则相当于做删除操作 +- 删除操作,“更新后的值”为空,则相当于做插入操作 +- 若是修改操作,则相当于用修改前值代替修改后值 +- 3. 继续反向扫描日志文件,查找该事务的其他更新操作,并做同样处理。 +- 4. 如此处理下去,直至读到此事务的开始标记,事务故障恢复就完成了。 +## 系统故障的恢复 +- 系统故障造成数据库不一致状态的原因 +- 未完成事务对数据库的更新已写入数据库 +- 已提交事务对数据库的更新还留在缓冲区没来得及写入数据库 +- 恢复方法 +- 1. Undo 故障发生时未完成的事务 +- 2. Redo 已完成的事务 +- 系统故障的恢复由系统在重新启动时自动完成,不需要用户干预 +- 系统故障的恢复步骤 +- 1. 正向扫描日志文件 +- 重做(REDO) 队列: 在故障发生前已经提交的事务 +- 这些事务既有BEGIN TRANSACTION记录,也有COMMIT记录 +- 撤销 (Undo)队列: 故障发生时尚未完成的事务 +- 这些事务只有BEGIN TRANSACTION记录,无相应的COMMIT记录 +- 2. 对撤销(Undo)队列事务进行撤销(UNDO)处理 +- 反向扫描日志文件,对每个UNDO事务的更新操作执行逆操作 +- 3. 对重做(Redo)队列事务进行重做(REDO)处理 +- 正向扫描日志文件,对每个REDO事务重新执行登记的操作 +## 介质故障的恢复 +- 恢复步骤 +- 1.重装数据库 +•- 装入最新的后备副本,使数据库恢复到最近一次转储时的一致性状态。 +–- 对于静态转储的数据库副本,装入后数据库即处于一致性状态 +–- 对于动态转储的数据库副本,还须同时装入转储时刻的日志文件副本,利用恢复系统故障的方法(即REDO+UNDO),才能将数据库恢复到一致性状态。 +- 2. 装入有关的日志文件副本,重做已完成的事务。 +•- 首先扫描日志文件,找出故障发生时已提交的事务的标识,将其记入重做队列。 +•- 然后正向扫描日志文件,对重做队列中的所有事务进行重做处理。 +- 介质故障的恢复需要DBA介入 +- DBA的工作 +- 重装最近转储的数据库副本和有关的各日志文件副本 +- 执行系统提供的恢复命令,具体的恢复操作仍由DBMS完成 +# 8.10 具有检查点的数据恢复 +- 利用日志技术进行数据库恢复存在两个问题 +- 搜索整个日志将耗费大量的时间 +- REDO处理:事务实际上已经执行,又重新执行,浪费了大量时间 +- 具有检查点(checkpoint)的恢复技术 +- 在日志文件中增加检查点记录(checkpoint) +- 增加重新开始文件,并让恢复子系统在登录日志文件期间动态地维护日志 +- 检查点记录的内容 +- 建立检查点时刻所有正在执行的事务清单 +- 这些事务最近一个日志记录的地址 +- 重新开始文件的内容 +- 记录各个检查点记录在日志文件中的地址 + +- 动态维护日志文件的方法 +- 周期性地执行如下操作:建立检查点,保存数据库状态。 +- 具体步骤是: +- 1.将当前日志缓冲区中的所有日志记录写入磁盘的日志文件上 +- 2.在日志文件中写入一个检查点记录 +- 3.将当前数据缓冲区的所有数据记录写入磁盘的数据库中 +- 4.把检查点记录在日志文件中的地址写入一个重新开始文件 + +- 使用检查点方法可以改善恢复效率 +- 当事务T在一个检查点之前提交 +- T对数据库所做的修改一定都已写入数据库 +- 写入时间是在这个检查点建立之前或在这个检查点建立之时 +- 在进行恢复处理时,没有必要对事务T执行REDO操作 +- 使用检查点的恢复步骤 +- 1.从重新开始文件中找到最后一个检查点记录在日志文件中的地址,由该地址在日志文件中找到最后一个检查点记录 +- 2.由该检查点记录得到检查点建立时刻所有正在执行的事务清单ACTIVE-LIST +•- 建立两个事务队列 +–- UNDO-LIST +–- REDO-LIST +•- 把ACTIVE-LIST暂时放入UNDO-LIST队列,REDO队列暂为空 +- 3.从检查点开始正向扫描日志文件,直到日志文件结束 +•- 如有新开始的事务Ti,把Ti暂时放入UNDO-LIST队列 +•- 如有提交的事务Tj,把Tj从UNDO-LIST队列移到REDO-LIST队列 +- 4.对UNDO-LIST中的每个事务执行UNDO操作 +- 对REDO-LIST中的每个事务执行REDO操作 + +# 8.11 数据库镜像 +- 为避免硬盘介质出现故障影响数据库的可用性,许多DBMS提供了数据库映像(mirror)功能用于数据库恢复。 +- 将整个数据库或其中的关键数据复制到另一个磁盘上,每当主数据库更新时,DBMS自动把更新后的数据复制过去,由DBMS自动保证镜像数据与主数据库的一致性。一旦出现介质故障,可由镜像磁盘继续提供使用,同时DBMS自动利用磁盘数据进行数据库的恢复,不需要关闭系统和重装数据库副本。 +- 在没有出现故障时,数据库镜像还可以用于并发操作,即当一个用户对数据库加排它锁修改数据时,其他用户可以读镜像数据库上的数据,而不必等待该用户释放锁。 + +- 由于数据库镜像是通过复制数据实现的,频繁地赋值数据自然会降低系统运行效率。因此在实际应用中用户往往只选择对关键数据和日志文件进行镜像。 + + + +- 小结: +- 如果数据库只包含成功事务提交的结果,就说数据库处于一致性状态。保证数据一致性是对数据库的最基本的要求。 +- 事务是数据库的逻辑工作单位 +- DBMS保证系统中一切事务的原子性、一致性、隔离性和持续性 +- DBMS必须对事务故障、系统故障和介质故障进行恢复 +- 恢复中最经常使用的技术:数据库转储和登记日志文件 +- 恢复的基本原理:利用存储在后备副本、日志文件和数据库镜像中的冗余数据来重建数据库 +- 常用恢复技术 +- 事务故障的恢复 +- UNDO +- 系统故障的恢复 +- UNDO + REDO +- 介质故障的恢复 +- 重装备份并恢复到一致性状态 + REDO +- 提高恢复效率的技术 +- 检查点技术 +- 可以提高系统故障的恢复效率 +- 可以在一定程度上提高利用动态转储备份进行介质故障恢复的效率 +- 镜像技术 +- 镜像技术可以改善介质故障的恢复效率 +- + +# 并发控制 +- 多用户数据库:允许多个用户同时使用的数据库(订票系统) +- 不同的多事务执行方式: +- 1.串行执行:每个时刻只有一个事务运行,其他事务必须等到这个事务结束后方能运行。 +- 2.交叉并发方式: +- 单处理机系统中,事务的并发执行实际上是这些并行事务的并行操作轮流交叉运行(不是真正的并发,但是提高了系统效率) +- 3.同时并发方式: +- 多处理机系统中,每个处理机可以运行一个事务,多个处理机可以同时运行多个事务,实现多个事务真正的并行运行 +- 并发执行带来的问题: +- 多个事务同时存取同一数据(共享资源) +- 存取不正确的数据,破坏事务一致性和数据库一致性 +# 8.12 并发控制概述 +- 并发操作带来的数据不一致性包括 + - 1)丢失修改(lost update) + - 2)不可重复读(non-repeatable read) + - 3)读脏数据(dirty read) +- 记号:W(x)写数据x R(x)读数据x + +- 并发控制机制的任务: + - 1)对并发操作进行正确的调度 + - 2)保证事务的隔离性 + - 3)保证数据库的一致性 +- 并发控制的主要技术 + - 1)封锁(locking)(主要使用的) + - 2)时间戳(timestamp) + - 3)乐观控制法(optimistic scheduler) + - 4)多版本并发控制(multi-version concurrency control ,MVCC) + +# 8.13 封锁 +- 封锁:封锁就是事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象 + +- 确切的控制由封锁的类型决定 +- 基本的封锁类型有两种:排它锁(X锁,exclusive locks)、共享锁(S 锁,share locks) +- 排它锁又称写锁,对A加了排它锁之后,其他事务不能对A加 任何类型的锁(排斥读和写) +- 共享锁又称读锁,对A加了共享锁之后,其他事务只能对A加S锁,不能加X锁(只排斥写) +- + +# 8.14 封锁协议 +- 在运用X锁和S锁对数据对象加锁时,需要约定一些规则:封锁协议(Locking Protocol) +- 何时申请X锁或S锁 +- 持锁时间、何时释放 + +- 对封锁方式制定不同的规则,就形成了各种不同的封锁协议。 +- 常用的封锁协议:三级封锁协议 +- 三级封锁协议在不同程度上解决了并发问题,为并发操作的正确调度提供一定的保证。 + +- 1、一级封锁协议 +- 事务T在修改数据R之前,必须先对其加X锁,直到事务结束(commit/rollback)才释放。 +- 一级封锁协议可以防止丢失修改 +- 如果是读数据,不需要加锁的,所以它不能保证可重复读和不读“脏”数据。 + + + +- 2、 二级封锁协议 +- 在一级封锁协议的基础(写要加X锁,事务结束释放)上,增加事务T在读入数据R之前必须先对其加S锁,读完后即可释放S锁。(读要加S锁,读完即释放) +- 二级封锁协议除了可以防止丢失修改,还可以防止读脏数据 +- 由于读完数据即释放S锁,不能保证不可重复读 + + + +- 3、三级封锁协议: +- 在一级封锁协议基础上增加事务T在读取数据R之前必须先对其加S锁,直到事务结束后释放。 +- 三级封锁协议除了可以防止丢失修改和读脏数据外,还防止了不可重复读 + +- 三级封锁协议的主要区别是什么操作需要申请锁,何时释放锁。封锁协议越高,一致性程度越高。 + + +# 8.15 饥饿和死锁 +## 饥饿 +- 饥饿:事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁之后,系统首先批准了T3的请求,T2仍然等待。 T4又请求封锁R,当T3释放了R上的封锁之后系统又批准了T4的请求……T2有可能永远等待,这就是饥饿的情形 + +- 避免饥饿的方法:先来先服务 +- 当多个事务请求封锁同一数据对象时,按请求封锁的先后次序对这些事务排队 +- 该数据对象上的锁一旦释放,首先批准申请队列中第一个事务获得锁。 + +## 死锁 +- 死锁:事务T1封锁了数据R1, T2封锁了数据R2。 T1又请求封锁R2,因T2已封锁了R2,于是T1等待T2释放R2上的锁。 接着T2又申请封锁R1,因T1已封锁了R1,T2也只能 +- 等待T1释放R1上的锁。 这样T1在等待T2,而T2又在等待T1,T1和T2两个事务永远不能结束,形成死锁。 + +- 解决死锁的方法:预防、诊断和解除 +- 1、死锁的预防 +- 产生死锁的原因是两个或多个事务都已经封锁了一些数据对象,然后又都请求对已被其他事务封锁的数据对象加锁,从而出现死等待。 +- 预防死锁发生就是破坏产生死锁的条件 +- 方法 + - 1)一次封锁法: +- 要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行。 +- 存在的问题:降低系统的并发度;难以实现精确确定封锁对象 +- + - 2)顺序封锁法: +- 预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实施封锁。 +- 存在的问题: +- 维护成本:数据库系统中的封锁对象极多,并且在不断地变化 +- 难以实现:很难实现确定每一个事务要封锁哪些对象 +- DBMS普通采用的诊断并解除死锁的方法 + +- 2、死锁的诊断和解除 +- 方法:超时法和事务等待图法 + - 1)超时法:如果一个事务的等待时间超过了规定的时限,就认为发生了死锁 +- 优点:实现简单 +- 缺点:误判死锁;时限若设置太长,死锁发生后不能及时发现。 + - 2)事务等待图法:用事务等待图动态反映所有事务的等待情况事务 +- 等待图是一个有向图G=(T,U),T为结点的集合,每个结点表示正运行的事务, U为边的集合,每条边表示事务等待的情况。若T1等待T2,则T1、T2之间划一条有向边,从T +- 1指向T2。 + +- 并发控制子系统周期性地(比如每隔数秒)生成事务等待图,检测事务。如果发现图中存在回路,则表示系统中出现了死锁。 +- +- 解除死锁:并发控制子系统选择一个处理死锁代价最小的事务,将其撤销。 +- 释放该事务持有的所有的锁,使其他事务能够继续运行下去。 +- + +# 8.16 并发调度的可串行性 +- 什么样的调度是正确的?串行调度是正确的。 +- (执行结果等价于串行调度的调度也是正确的,这样的调度称为可串行化调度。) +## 可串行化调度 +- 定义:多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同,称这种调度策略为可串行化调度(serializable)。 +- 可串行性是并发事务正确调度的准则。按这个准则规定,一个给定的并发调度,当且仅当它是可串行化的,才认为是正确调度。 + + + +## 冲突可串行化调度 +- 判断可串行化调度的充分条件 + +- 冲突操作:不同的事务对同一个数据的读写和写写操作。 + +- 不同事务的冲突操作和同一事务的两个操作是不能交换的。 +- Ri(x)和Wj(x)不可交换,Wi(x)和Wj(x)不可交换 + +- 冲突可串行化调度: +- 一个调度Sc在保证冲突操作的次序不变的情况下,通过交换两个事务不冲突操作的次序得到另一个调度Sc’,如果Sc’是串行的,称调度Sc为冲突可串行化的调度。 + +- 若一个调度是冲突可串行化,则一定是可串行化的。冲突可串行化调度是可串行化调度的充分条件而非必要条件,同样存在不满足冲突可串行化调度的可串行化调度。 +- + +# 8.17 两段锁协议 +- DBMS的并发控制机制必须提供一定的手段来保证调度是可串行化的。目前DBMS普遍采用两段锁协议(TwoPhase Locking,简称2PL)的方法来显示并发调度的可串行性。 + +- 两段锁协议是指所有事务必须分两个阶段对数据对象进行加锁和解锁。 + - 1)在对任何数据进行读写操作以前,首先要申请并获得对该数据的锁。 + - 2)在释放一个锁之后,事务不再申请和获得其他任何的锁。 +- “两段”锁的含义:事务分为两个阶段 +- 第一阶段是获得封锁,也称为扩展阶段 +- 事务可以申请获得任何数据对象上的任何类型的锁,但是不能释放任何锁 +- 第二阶段是释放封锁,也称为收缩阶段 +- 事务可以释放任何数据对象上的任何类型的锁,但是不能再申请任何锁 + + +- 符合两段锁协议的可串行化调度示例: + +- 事务遵守两段锁协议是可串行化调度的充分条件,而不是必要条件。 +- 若并发事务都遵守两段锁协议,则对这些事务的任何并发调度策略都是可串行化的 +- 若并发事务的一个调度是可串行化的,不一定所有事务都符合两段锁协议 + +- 两段锁协议与防止死锁的一次封锁法 +- 一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行,因此一次封锁法遵守两段锁协议 +- 但是两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁 + +# 8.18 封锁的粒度 +- 封锁对象的大小称为封锁粒度(granularity)。 +- 封锁的对象可以是逻辑单元(属性值、属性值集合、元组、关系、索引项、数据库),也可以是物理单元(页、物理记录)。 + +- 选择封锁粒度原则: +- 封锁粒度和系统的并发度和并发控制的开销密切相关 +- 封锁的粒度越大,数据库所能够封锁的数据单元就越少,并发度就越低,系统开销也 +- 越小; +- 封锁的粒度越小,并发度较高,但系统开销也就越大 + + +## 多粒度封锁 +- 如果在一个系统中同时支持多种封锁粒度供不同的事务选择,这种封锁方法称为多粒度封锁。(multiple granularity locking) +- 选择封锁粒度应该同时考虑封锁开销和并发度两个因素,适当选择封锁粒度以求得最优的效果。 +- 需要处理多个关系的大量元组的用户事务:以数据库为封锁单位 +- 需要处理大量元组的用户事务:以关系为封锁单元 +- 只处理少量元组的用户事务:以元组为封锁单位 + +- 多粒度树 +- 以树形结构来表示多级封锁粒度。根结点是整个数据库,表示最大的数据粒度,叶结点表示最小的数据粒度 + +- 多粒度封锁协议:允许多粒度树中的每个节点被独立地加锁,对一个节点加锁意味着这个节点的所有子节点也被加以同样类型的锁。因此,在多粒度封锁中一个数据对象可能以显式封锁和隐式封锁两种方式封锁。 +- • 显式封锁:直接加到数据对象上的封锁 +- • 隐式封锁:该数据对象没有独立加锁,是由于其上级结点加锁而使该数据对象加上了锁 +- • 显式封锁和隐式封锁的效果是一样的 +- 系统检查封锁冲突时要检查显式封锁,还要检查隐式封锁 + +- 例如事务T要对关系R1加X锁,系统必须搜索其上级结点数据库、关系R1,还要搜索R1的下级结点,即R1中的每一个元组。如果其中某一个数据对象已经加了不相容锁,则T必 +- 须等待。 + +- 对某个数据对象加锁,系统要检查该数据对象上有无显式封锁与之冲突;再检查其所有上级节点,看本事务的显式封锁是否与该数据对象上的隐式封锁(由于上级节点已加的封锁造成的)冲突;还要检查其所有下级节点,看它们的显式封锁是否与本事务的隐式封锁(将加到下级节点的封锁)冲突。 +- 这种检查方法效率较低,引入一种新的锁,意向锁。有了意向锁,DBMS就无须逐个检查下一级节点的显式封锁。 + +## 意向锁 +- 意向锁:如果对一个节点加意向锁,则可说明该节点的下层节点正在被加锁;对任一节点加锁时,必须先对它的上层节点加意向锁。 +- 例如,对任一元组加锁时,必须先对它所在的数据库和关系加意向锁。 +- 三种常用的意向锁:意向共享锁(Intent Share Lock,IS锁);意向排它锁(Intent Exclusive Lock,IX锁);共享意向排它锁(Share Intent Exclusive Lock,SIX锁)。 + +- 1、IS锁 +- 如果对一个数据对象加IS锁,表示它的子节点拟加S锁。 +- 例如:事务T1要对R1中某个元组加S锁,则要首先对关系R1和数据库加IS锁 + +- 2、IX锁 +- 如果对一个数据对象加IX锁,表示它的子节点拟加X锁。 +- 例如:事务T1要对R1中某个元组加X锁,则要首先对关系R1和数据库加IX锁 + +- 3、SIX锁 +- 如果对一个数据对象加SIX锁,表示对它加S锁,再加IX锁,即SIX = S + IX。 + +- 例如:对某个表加SIX锁,则表示该事务要读整个表(所以要对该表加S锁),同 +- 时会更新个别元组(所以要对该表加IX锁) + + +- 意向锁的强度: 锁的强度是指它对其他锁的排斥程度。一个事务在申请封锁时以强锁代替弱锁是安全的,反之则不然。 + +- 具有意向锁的多粒度封锁方法 +- 申请封锁时应该按自上而下的次序进行 +- 释放封锁时则应该按自下而上的次序进行 + +- 优点: + - 1)提高了系统并发度 + - 2)减少了加锁和解锁的开销 +- 在实际的DBMS产品中得到广泛应用。 + +# 8.19 其他并发控制机制 +- 并发控制的方法除了封锁技术外,还有时间戳方法、乐观控制法和多版本并发控制。 +- 时间戳方法:给每一个事务盖上一个时标,即事务开始的时间。每个事务具有唯一的时间戳,并按照这个时间戳来解决事务的冲突操作。如果发生冲突操作,就回滚到具有较早时间戳的事务,以保证其他事务的正常执行,被回滚的事务被赋予新的时间戳被从头开始执行。 +- 乐观控制法认为事务执行时很少发生冲突,所以不对事务进行特殊的管制,而是让它自由执行,事务提交前再进行正确性检查。如果检查后发现该事务执行中出现过冲突并影响了可串行性,则拒绝提交并回滚该事务。又称为验证方法 +- 多版本控制是指在数据库中通过维护数据对象的多个版本信息来实现高效并发的一种策略。 +# 范式(避免数据冗余和操作异常) +# 8.20 函数依赖 +- A->B A和B是两个属性集,来自同一关系模式,对于同样的A属性值,B属性值也相同 +# 8.21 平凡的函数依赖 +- X->Y,如果Y是X的子集 +# 8.22 非平凡的函数依赖 +- X->Y,如果Y不是X的子集 +# 8.23 部分函数依赖 +- X->Y,如果存在W->Y,且W⊂X +# 8.24 传递函数依赖 +- 在R(U)中,如果X→Y(非平凡函数依赖,完全函数依赖),Y→Z, 则称Z对X传递函数依赖。 +- 记为:XZ +# 8.25 super key&candidate key&primary key&主属性&非主属性 +- super key:在关系中能唯一标识元素的属性集 +- candidate key或key:不含有多余属性的super key +- primary key:在candidate key 中任选一个 + +- candidate key中X决定所有属性的函数依赖是完全函数依赖 +- 包含在任何一个candidate key中的属性 ,称为主属性 +- 不包含在candidate key中的属性称为非主属性 +# 8.26 1NF 列不可分 +- 列不可分 +# 8.27 2NF 消除了非主属性对键的部分函数依赖 +- 在关系T上有函数依赖集F,F+是F的闭包。 +- F满足2NF,当且仅当 每个非平凡的函数依赖X->A(F+),A是单个非主属性,要求X不是任何key的真子集(有可能是super key,也有可能是非key)。 + +# 8.28 3NF 消除了非主属性对键的传递函数依赖 +- F满足3NF,当且仅当 每个非平凡的函数依赖X->A(F+),A是单个非主属性,要求X是T的super key。 + +# 8.29 BCNF 消除了主属性对键的部分函数依赖和传递函数依赖 +- F满足BCNF,当且仅当 每个非平凡的函数依赖X->A(F+),A是单个属性,要求X是T的super key。 +- 对于F+中 的任意一个X->A,如果A是单个属性,且A不在X中,那么X一定是T的super key。 +- + +# 反范式(减少连接,提高查询效率) +# 8.30 Pattern1:合并1对1关系 +- 例:学院给老师配车,车少人多,车完全参与,人部分参与 +- car +car_id car_name +1 c1 +2 c2 +3 c3 + +- teacher +teacher_id teacher_name +1 t1 +2 t2 +3 t3 +4 t4 +- 合并后 +- car_and_teacher +car_id car_name teacher_id teacher_name +1 c1 1 t1 +2 c2 2 t2 +3 c3 3 t3 +NULL NULL 4 t4 + +- 问题:会产生大量空值,若两边都部分参与则不能合并; +- 部分参与为大部分参与时比较适合Pattern1 +# 8.31 Pattern2:1对N关系中复制非键属性以减少连接 +- 两表连接时复制非键属性以减少连接 +- 例:查询学生以及所在学院名,可以在学生表中不仅存储学院id,并且存储学院名 +- faculty +fid fname +1 f1 + +- student +sid sname fid fname +1 s1 1 f1 + +- 维护时: + - 1)如果在UI中,只允许用户进行选择,不能自行输入,保证输入一致性 + - 2)如果是程序员,对于类似学院名这种一般不变的代码表,在修改时直接对两张表都进行修改;如果经常变化,则可以加一个触发器。 +# 8.32 Pattern3:1对N关系中复制外键以减少连接 +- 把另一张表的主键复制变成外键 + +- 应用后: + +# 8.33 Pattern4:N对N关系中复制属性,把两张表中经常需要的内容复制到中间关系表中以减少连接 +- 例: + +# 8.34 Pattern5:引入重复值 +- 通常对于一个多值属性,值不太多,且不会经常变,可以在表中建立多个有关此属性的列 +- address1 | address2 | address3 | address4 +# 8.35 Pattern6:建立提取表 +- 为了解决查询和更新之间不可调和的矛盾,可以将更新和查询放在两张表中,从工作表中提取查询表,专门用于查询。只适用于查询实时性不高的情况。 +# 8.36 Pattern7:分表 +- 水平拆分 +- 垂直拆分 +- + +# MySQL使用 +# MySQL特点 +- MySQL是一个关系型数据库管理系统,开发者为瑞典MySQL AB公司。目前MySQL被广泛地应用在互联网行业。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多互联网公司选择了MySQL作为后端数据库。2008年MySQL被Sun公司收购,2010年甲骨文成功收购Sun公司。 +- MySQL数据库的优点: +- 1、多语言支持:MySQL为C、C++、Python、Java、Perl、PHP、Ruby等多种编程语言提供了API,访问和使用方便。 +- 2、可以移植性好:MySQL是跨平台的。 +- 3、免费开源。 +- 4、高效:MySQL的核心程序采用完全的多线程编程。 +- 5、支持大量数据查询和存储:MySQL可以承受大量的并发访问 +# 数据类型 +# 8.37 数值类型 +- 整数类型: + +- 实数类型: + +- 定点数:DECIMAL和NUMERIC类型在MySQL中视为相同的类型。它们用于保存必须为确切精度的值。 +- DECIMAL(M,D),其中M表示十进制数字总的个数,D表示小数点后面数字的位数。 + - 如果存储时,整数部分超出了范围(如上面的例子中,添加数值为1000.01),MySql就会报错,不允许存这样的值。 +- 如果存储时,小数点部分若超出范围,就分以下情况: +- 若四舍五入后,整数部分没有超出范围,则只警告,但能成功操作并四舍五入删除多余的小数位后保存。如999.994实际被保存为999.99。 +- 若四舍五入后,整数部分超出范围,则MySql报错,并拒绝处理。如999.995和-999.995都会报错。 +- M的默认取值为10,D默认取值为0。如果创建表时,某字段定义为decimal类型不带任何参数,等同于decimal(10,0)。带一个参数时,D取默认值。 + +- M的取值范围为1~65,取0时会被设为默认值,超出范围会报错。 +- D的取值范围为0~30,而且必须<=M,超出范围会报错。 +- 所以,很显然,当M=65,D=0时,可以取得最大和最小值。 + +- 浮点数类型:float,double和real。他们定义方式为:FLOAT(M,D) 、 REAL(M,D) 、 DOUBLE PRECISION(M,D)。 “(M,D)”表示该值一共显示M位整数,其中D位位于小数点后面 +- FLOAT和DOUBLE中的M和D的取值默认都为0,即除了最大最小值,不限制位数。 +- M取值范围为0~255。FLOAT只保证6位有效数字的准确性,所以FLOAT(M,D)中,M<=6时,数字通常是准确的。如果M和D都有明确定义,其超出范围后的处理同decimal。 +- D取值范围为0~30,同时必须<=M。double只保证16位有效数字的准确性,所以DOUBLE(M,D)中,M<=16时,数字通常是准确的。如果M和D都有明确定义,其超出范围后的处理同decimal。 +- 内存中,FLOAT占4-byte(1位符号位 8位表示指数 23位表示尾数),DOUBLE占8-byte(1位符号位 11位表示指数 52位表示尾数)。 + +- 浮点数比定点数类型存储空间少,计算速度快,但是不够精确。 + +- 因为需要计算额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时 才使用DECIMAL。但在数据量比较大的情况下,可以考虑使用BIGINT代替DECIMAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。 + +- BIT数据类型可用来保存位字段值。BIT(M)类型允许存储M位值。M范围为1~64,默认为1。 +- BIT其实就是存入二进制的值,类似010110。 +- 如果存入一个BIT类型的值,位数少于M值,则左补0. +- 如果存入一个BIT类型的值,位数多于M值,MySQL的操作取决于此时有效的SQL模式: +- 如果模式未设置,MySQL将值裁剪到范围的相应端点,并保存裁减好的值。 +- 如果模式设置为traditional(“严格模式”),超出范围的值将被拒绝并提示错误,并且根据SQL标准插入会失败。 +- MySQL把BIT当做字符串类型,而非数字类型。 + + + +- +# 8.38 字符串类型 +- 字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。 +## CHAR & VARCHAR +- CHAR和VARCHAR类型声明的长度表示你想要保存的最大字符数。例如,CHAR(30)可以占用30个字符。默认长度都为255。 +- CHAR列的长度固定为创建表时声明的长度。长度可以为从0到255的任何值。当保存CHAR值时,在它们的右边填充空格以达到指定的长度。当检索到CHAR值时,尾部的空格被删除掉,所以,我们在存储时字符串右边不能有空格,即使有,查询出来后也会被删除。在存储或检索过程中不进行大小写转换。 + +- 所以当char类型的字段为唯一值时,添加的值是否已经存在以不包含末尾空格(可能有多个空格)的值确定,比较时会在末尾补满空格后与现已存在的值比较。 + + - VARCHAR列中的值为可变长字符串。长度可以指定为0到65,535之间的值(实际可指定的最大长度与编码和其他字段有关,比如,MySql使用utf-8编码格式,大小为标准格式大小的2倍,仅有一个varchar字段时实测最大值仅21844,如果添加一个char(3),则最大取值减少3。整体最大长度是65,532字节)。 + +- 同CHAR对比,VARCHAR值保存时只保存需要的字符数,另加一个字节来记录长度(如果列声明的长度超过255,则使用两个字节)。 + +- VARCHAR值保存时不进行填充。当值保存和检索时尾部的空格仍保留,符合标准SQL。 + +- 如果分配给CHAR或VARCHAR列的值超过列的最大长度,则对值进行裁剪以使其适合。如果被裁掉的字符不是空格,则会产生一条警告。如果裁剪非空格字符,则会造成错误(而不是警告)并通过使用严格SQL模式禁用值的插入。 + +## BINARY & VARBINARY +- BINARY和VARBINARY类型类似于CHAR和VARCHAR类型,但是不同的是,它们存储的不是字符串,而是二进制串。所以它们没有编码格式,并且排序和比较基于列值字节的数值值。 + +- 当保存BINARY值时,在它们右边填充0x00(零字节)值以达到指定长度。取值时不删除尾部的字节。比较时所有字节很重要(因为空格和0x00是不同的,0x00<空格),包括ORDER BY和DISTINCT操作。比如插入'a '会变成'a \0'。 + +- 对于VARBINARY,插入时不填充字符,选择时不裁剪字节。比较时所有字节很重要。 + +## BLOB & TEXT +- BLOB是一个二进制大对象,可以容纳可变数量的数据。有4种BLOB类型:TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB。它们只是可容纳值的最大长度不同。 + +- 有4种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT。这些对应4种BLOB类型,有相同的最大长度和存储需求。 + +- BLOB列被视为二进制字符串。TEXT列被视为字符字符串,类似BINARY和CHAR。 + +- 在TEXT或BLOB列的存储或检索过程中,不存在大小写转换。 + +- 未运行在严格模式时,如果你为BLOB或TEXT列分配一个超过该列类型的最大长度的值,值被截取以保证适合。如果截掉的字符不是空格,将会产生一条警告。使用严格SQL模式,会产生错误,并且值将被拒绝而不是截取并给出警告。 + +- 在大多数方面,可以将BLOB列视为能够足够大的VARBINARY列。同样,可以将TEXT列视为VARCHAR列。 + +- BLOB和TEXT在以下几个方面不同于VARBINARY和VARCHAR: +- 当保存或检索BLOB和TEXT列的值时不删除尾部空格。(这与VARBINARY和VARCHAR列相同)。 +- 比较时将用空格对TEXT进行扩充以适合比较的对象,正如CHAR和VARCHAR。 +- 对于BLOB和TEXT列的索引,必须指定索引前缀的长度。对于CHAR和VARCHAR,前缀长度是可选的。 +- BLOB和TEXT列不能有默认值。 +- BLOB或TEXT对象的最大大小由其类型确定,但在客户端和服务器之间实际可以传递的最大值由可用内存数量和通信缓存区大小确定。你可以通过更改max_allowed_packet变量的值更改消息缓存区的大小,但必须同时修改服务器和客户端程序。 + +- 每个BLOB或TEXT值分别由内部分配的对象表示。 +- 它们(TEXT和BLOB同)的长度: + - Tiny:最大长度255个字符(2^8-1) + - BLOB或TEXT:最大长度65535个字符(2^16-1) + - Medium:最大长度16777215个字符(2^24-1) + - LongText 最大长度4294967295个字符(2^32-1) +- 实际长度与编码有关,比如utf-8的会减半。 + +- 当BLOB和TEXT值太大时,InnoDB会使用专门的外部存储区域来进行存储,此时单个值在行内需要1~4个字节存储一个指针,然后在外部存储区域存储实际的值。 +- MySQL会BLOB和TEXT进行排序与其他类型是不同的:它只对每个类的最前max_sort_length字节而不是整个字符串进行排序。 +- MySQL不能将BLOB和TEXT列全部长度的字符串进行索引,也不能使用这些索引消除排序。 + +- ENUM 使用枚举代替字符串类型 +- MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一个或两个字节中。MySQL在内部将每个值在列表中的位置保存为整数,并且在表的.frm文件中保存“数组——字符串”映射关系的查找表。 +- 枚举字段是按照内部存储的整数而不是定义的字符串进行排序的; +- 由于MySQL把每个枚举值都保存为整数,并且必须通过查找才能转换为字符串,所以枚举列有一定开销。在特定情况下,把CHAR/VARCHAR列与枚举列进行JOIN可能会比直接关联CHAR/VARCHAR更慢。 + +# 8.39 时间和日期类型 + +- DATE, DATETIME, 和TIMESTAMP类型 这三者其实是关联的,都用来表示日期或时间。 + +- 当你需要同时包含日期和时间信息的值时则使用DATETIME类型。MySQL以'YYYY-MM-DD HH:MM:SS'格式检索和显示DATETIME值。支持的范围为'1000-01-01 00:00:00'到'9999-12-31 23:59:59'。 + +- 当你只需要日期值而不需要时间部分时应使用DATE类型。MySQL用'YYYY-MM-DD'格式检索和显示DATE值。支持的范围是'1000-01-01'到 '9999-12-31'。 + +- TIMESTAMP类型同样包含日期和时间,范围从'1970-01-01 00:00:01' UTC 到'2038-01-19 03:14:07' UTC。 +- TIME值的范围可以从'-838:59:59'到'838:59:59'。小时部分会因此大的原因是TIME类型不仅可以用于表示一天的时间(必须小于24小时),还可能为某个事件过去的时间或两个事件之间的时间间隔(可以大于24小时,或者甚至为负) + +- 两者的存储方式不一样 +- 对于TIMESTAMP,它把客户端插入的时间从当前时区转化为UTC(世界标准时间)进行存储。查询时,将其又转化为客户端当前时区进行返回。 +- 而对于DATETIME,不做任何改变,基本上是原样输入和输出。 + +- YEAR类型是一个单字节类型用于表示年。 + +- MySQL以YYYY格式检索和显示YEAR值。范围是1901到2155。 + + + + +- + +# 逻辑架构 +- MySQL的特点体现在其存储引擎的架构上。 +- 插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取相分离,这种架构可以让用户根据业务需求和实际需要选择合适的存储引擎。 + + +# 8.40 连接层(管理客户端的连接,维护线程池) +- 最上层是一些客户端和连接服务,引入了线程池的概念;实现基于SSL的安全连接 +- 每个客户端都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行。 +- 当客户端连接到MySQL服务器时,服务器需要对其进行认证。如果使用了SSL安全套接字的方式连接,还会使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限。 + +# 8.41 服务器(与具体存储引擎解耦,服务器通过API与存储引擎进行通信) +- SQL接口 +- SQL分析与优化 +- 存储过程 +- 触发器 +- 视图 +- MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。 +- 优化器并不关心表使用的是什么存储引擎,但存储引擎对优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。 + +- 对于SELECT语句,在解析查询前,服务器会先检查查询缓存,如果能够在其中找到对应的查询,服务器就不再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。 + +# 8.42 存储引擎层(负责数据的存储和存取) +- 存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取。 +- 存储引擎API包含了几十个底层函数,用于执行诸如“开启一个事务”或者“根据主键提取一行记录”等操作。但存储引擎不会去解析SQL(InnoDB是一个例外,它会解析外键定义,因为服务器没有实现该功能),不同存储引擎之间也不会相互通信,而只是简单地响应上层服务器的请求。 + +# 8.43 存储层(将数据存储到文件系统上) +- 数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。 + +- + +# 存储引擎 + +- MyISAM崩溃后无法安全恢复(由于不支持事务) +# 约束 +- 主键约束 :不允许重复记录,避免数据冗余 +- 外键约束:保证本事务所关联的其他事务是存在的(主键表中的这个字段) +- check约束:限制某一个值在某一个范围之内 +- check() ()内是关系表达式和逻辑表达式的嵌套 +- 注意逻辑运算符是not and or +- default约束:确定默认值(可以更改) 保证事务的某个属性一定会有一个值 +- 有默认值的话如果不想对其更改,可以用insert对其他字段进行赋值,跳过有默认值的字段 +- 但是不能在整体insert的时候跳过这个字段 + +- unique约束 唯一键:唯一的值不可重复,但允许为空 +- 就是该记录的这个值不会有重复的值 unique和not null 可以组合使用,顺序任意 +- 注意空值可以写为null,注意空值的这个值也不能重复,只能有一条记录的这个字段可以是空值 (而oracle中可以允许多个有唯一键的记录为空值) + +- not null约束 要求用户必须为该字段赋一个值,否则出错 +- 如果非空的话必须赋值,不能采用部分insert的办法来跳过对这个字段的赋值 +- 不写not null/null 的话默认就是允许有空值,如果没赋值的话字段的值默认是null +- null和default 关系:都允许不对某字段进行赋值,但是结果不同,一个是空值,另一个是默认值 + +- create table student2 +- ( +- stu_id int primary key, +- stu_name nvarchar(20) unique not null, +- stu_sal int check(stu_sal >= 1000 and stu_sal <2000) not null, + - stu_sex nchar(1) default '男' +- ) +- insert into student values(1,'啦啦',1800,'男') ok +- insert into student values(2,'啦啦',1800,'男') error +- insert into student values(2,'嘿嘿',2400,null) error +- insert into student values(null,'嘿嘿',1200,null) ok + + +- 主键和唯一键的关系: +- 不要用业务逻辑字段当做主键,应添加一个没有任何实际意义的字段(代理主键)当做主键 +- 一般是主键(或者唯一键)作为其他表的外键。 + +- 如果业务逻辑字段的信息修改,则会影响其他表 +- 查询效率低(数字、编号效率高) +- 这个业务逻辑字段修改时,因为这个主键同时充当多个其他表的外键,所以也要一并修改,十分麻烦 + +- 将有实际业务含义的、不能重复的、不是主键的一个字段作为唯一键 + + +- + +# MySQL常用函数 +# 8.44 文本处理函数 +- Left(x,len) – 返回串左边的字符(长度为len) +- Right(x,len) +- Length(x) – 返回串的长度 +- Locate(x,sub_x) – 找出串的一个子串 +- SubString(x, from, to) – 返回字串的字符 +- Lower(x) +- Upper(x) +- LTrim(x) +- RTrim(x) +- Soundex(x) – 读音(用于发音匹配) + +- SELECT cust_name, cust_contact FROM customers WHERE Soundex(cust_contact) = Soundex(‘Y Lie’); + +# 8.45 日期和时间处理函数 +- 日期和时间采用相应的数据类型和特殊的格式存储,以便可以快速和有效的排序或过滤,节省物理存储空间. +- 一般,应用程序不使用用来存储日期和时间的格式,因此日期和时间函数总是被用来读取、统计和处理这些函数. + +- 常用日期和时间处理函数: +- AddDate() – 增加一个日期(天,周等) +- AddTime() – 增加一个时间(时,分等) +- CurDate() – 返回当前日期 +- CurTime() – 返回当前时间 +- Date() – 返回日期时间的日期部分 +- DateDiff() – 计算两个日期之差 +- Date_Add() – 日期运算函数 +- Date_Format() – 返回一个格式化的日期或时间串 +- Day() – 返回一个日期的天数部分 +- DayOfWeek() – 返回日期对应的星期几 +- Hour() – 返回一个时间的小时部分 +- Minute() – 返回一个时间的分钟部分 +- Second() – 返回一个时间的秒部分 +- Month() – 返回一个日期的月部分 +- Now() – 返回当前日期和时间 +- Time() – 返回一个日期时间的时间部分 +- Year() – 返回一个日期的年份部分 + +- 日期首选格式: yyyy-mm-dd; 如2005-09-01 + +- 检索某日期下的数据: +- SELECT cust_id, order_num FROM orders WHERE Date(order_date) = ‘2005-09-01’; + +- 检索某月或日期范围内的数据: +- SELECT cust_id, order_num FROM orders WHERE Year(order_date) = 2005 AND Month(order_date) = 9; +- – or +- SELECT cust_id, order_num FROM orders WHERE date(order_date) BETWEEN ‘2005-09-01’ AND ‘2005-09-30’; + +# 8.46 数值处理函数 +- 代数、三角函数、几何运算等 + +- 常用数值处理函数: +- abs(); cos(); exp(); mod()(取余); Pi(); Rand(); Sin(); Sqrt(); Tan(); +- + +# 视图 +- 视图是虚拟的表,与包含数据的表不同,视图只包含使用时动态检索数据的查询,主要是用于查询。 +# 8.47 为什么使用视图 +- 重用sql语句 +- 简化复杂的sql操作,在编写查询后,可以方便地重用它而不必知道他的基本查询细节。 +- 使用表的组成部分而不是整个表。 +- 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。 +- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。 + +- 注意: +- 在视图创建之后,可以用与表基本相同的方式利用它们。可以对视图执行select操作,过滤和排序数据,将视图联结到其他视图或表,甚至能添加和更新数据。 + +- 重要的是知道视图仅仅是用来查看存储在别处的数据的一种设施。视图本身不包含数据,因此它们返回的数据时从其他表中检索出来的。在添加和更改这些表中的数据时,视图将返回改变过的数据。 + +- 因为视图不包含数据,所以每次使用视图时,都必须处理查询执行时所需的任一检索。如果你使用多个联结和过滤创建了复杂的视图或者嵌套了视图,可能会发现性能下降得很厉害。因此,在部署使用了大量视图的应用前,应该进行测试。 +# 8.48 视图的规则和限制 +- 与表一样,视图必须唯一命名; +- 可以创建任意多的视图; +- 为了创建视图,必须具有足够的访问权限。这些限制通常由数据库管理人员授予。 +- 视图可以嵌套,可以利用从其他视图中检索数据的查询来构造一个视图。 +- Order by 可以在视图中使用,但如果从该视图检索数据select中也是含有order by,那么该视图的order by 将被覆盖。 +- 视图不能索引,也不能有关联的触发器或默认值 +- 视图可以和表一起使用 +# 8.49 视图的创建 +- 利用create view 语句来进行创建视图 +- 使用show create view viewname;来查看创建视图的语句 +- 用drop view viewname 来删除视图 +- 更新视图可以先drop在create,也可以使用create or replace view。 +# 8.50 视图的更新 +- 视图是否可以更新,要视情况而定。 +- 通常情况下视图是可以更新的,可以对他们进行insert,update和delete。更新视图就是更新其基表(视图本身没有数据)。如果你对视图进行增加或者删除行,实际上就是对基表进行增加或者删除行。 + - 但是,如果MySQL不能正确的确定更新的基表数据,则不允许更新(包括插入和删除),这就意味着视图中如果存在以下操作则不能对视图进行更新:(1)分组(使用group by 和 having );(2)联结;(3)子查询;(4)并;(5)聚集函数;(6)dictinct;(7)导出(计算)列。【注意:基于5.0版本的规则,不排除后续变化】 +# 存储过程 +- 存储过程就是为了以后的使用而保存的一条或者多条MySQL语句的集合。可将视为批文件,虽然他们的作用不仅限于批处理。 +# 8.51 为什么使用储存过程? +- 1.通过把处理封装在容易使用的单元中,简化复杂的操作; + +- 2.由于不要求反复建立一系列处理步骤,保证了数据的完整性。如果所有开发人员和应用程序都使用同一(实验和测试)存储过程,则所使用的代码都是相同的。这一点的延伸就是防止错误。需要执行的步骤越多,出错的可能性就越大,防止错误保证了数据的一致性。 + +- 3.简化对变动的管理,如果表名。列名或者业务逻辑等有变化,只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化。这一点延伸就是安全性,通过存储过程限制对基数据的访问减少了数据讹误的机会。 + +- 4.提高性能。因为使用存储过程比使用单独的sql语句更快。 + +- 5.存在一些只能用在单个请求的MySQL元素和特性,存储过程可以使用他们来编写功能更强更灵活的代码 + +- 综上: +- 三个主要的好处:简单、安全、高性能。 +- 两个缺陷: +- 1、存储过程的编写更为复杂,需要更高的技能更丰富的经验。 +- 2、可能没有创建存储过程的安全访问权限。许多数据库管理员限制存储过程的 创建权限,允许使用,不允许创建。 +# 8.52 执行存储过程 +- Call关键字:Call接受存储过程的名字以及需要传递给他的任意参数。存储过程可以显示结果,也可以不显示结果。 +- CREATE PROCEDURE productpricing() +- BEGIN +- SELECT AVG( prod_price) as priceaverage FROM products; +- END; +- 创建名为productpricing的储存过程。如果存储过程中需要传递参数,则将他们在括号中列举出来即可。括号必须有。BEGIN和END关键字用来限制存储过程体。上述存储过程体本身是一个简单的select语句。注意这里只是创建存储过程并没有进行调用。 + +- 储存过程的使用: + +- Call productpring(); + +# 8.53 使用参数的存储过程 +- 一般存储过程并不显示结果,而是把结果返回给你指定的变量上。 +- 变量:内存中一个特定的位置,用来临时存储数据。 +- MySQL> CREATE PROCEDURE prod( + - out pl decimal(8,2), + - out ph decimal(8,2), + - out pa decimal(8,2) +- ) +- begin +- select Min(prod_price) into pl from products; +- select MAx(prod_price) into ph from products; +- select avg(prod_price) into pa from products; +- end; + +- call PROCEDURE(@pricelow,@pricehigh,@priceaverage); + +- select @pricelow; +- select @pricehigh; +- select @pricelow,@pricehigh,@priceaverage; +- 解释: +- 此存储过程接受3个参数,pl存储产品最低价,ph存储产品最高价,pa存储产品平均价。每个参数必须指定类型,使用的为十进制,关键字OUT 指出相应的参数用来从存储过程传出一个值(返回给调用者)。 + +- MySQL支持in(传递给存储过程)、out(从存储过程传出,这里所用)和inout(对存储过程传入和传出)类型的参数。存储过程的代码位于begin和end语句内。他们是一系列select语句,用来检索值。然后保存到相对应的变量(通过INTO关键字)。 +- 存储过程的参数允许的数据类型与表中使用的类型相同。注意记录集是不被允许的类型,因此,不能通过一个参数返回多个行和列,这也是上面为什么要使用3个参数和3条select语句的原因。 + +- 调用:为调用此存储过程,必须指定3个变量名。如上所示。3个参数是存储过程保存结果的3个变量的名字。调用时,语句并不显示任何数据,它返回以后可以显示的变量(或在其他处理中使用)。 + +- 注意:所有的MySQL变量都是以@开头。 +- CREATE PROCEDURE ordertotal( +- IN innumber int, + - OUT outtotal decimal(8,2) +- ) +- BEGIN +- SELECT Sum(item_price * quantity) FROM orderitems WHERE order_num = innumber INTO outtotal; +- end // + +- CALL ordertotal(20005,@total); +- select @total; // 得到20005订单的合计 + +- CALL ordertotal(20009,@total); +- select @total; //得到20009订单的合计 + +# 8.54 带有控制语句的存储过程 +- CREATE PROCEDURE ordertotal( +- IN onumber INT, +- IN taxable BOOLEAN, + - OUT ototal DECIMAL(8,2) +- )COMMENT 'Obtain order total, optionally adding tax' +- BEGIN +- -- declear variable for total + - DECLARE total DECIMAL(8,2); +- -- declear tax percentage +- DECLARE taxrate INT DEFAULT 6; +- -- get the order total +- SELECT Sum(item_price * quantity) FROM orderitems WHERE order_num = onumber INTO total; + +- -- IS this taxable? + +- IF taxable THEN +- -- yes ,so add taxrate to the total +- SELECT total+(total/100*taxrate)INTO total; +- END IF; +- -- finally ,save to out variable +- SELECT total INTO ototal; +- END; +- 在存储过程中我们使用了DECLARE语句,他们表示定义两个局部变量,DECLARE要求指定变量名和数据类型。它也支持可选的默认值(taxrate默认6%),因为后期我们还要判断要不要增加税,所以,我们把SELECT查询的结果存储到局部变量total中,然后在IF 和THEN的配合下,检查taxable是否为真,然后在真的情况下,我们利用另一条SELECT语句增加营业税到局部变量total中,然后我们再利用SELECT语句将total(增加税或者不增加税的结果)保存到总的ototal中。 +- COMMENT关键字 上面的COMMENT是可以给出或者不给出,如果给出,将在SHOW PROCEDURE STATUS的结果中显示。 + +# 触发器 +- 在某个表发生更改时自动处理某些语句,这就是触发器。 + +- 触发器是MySQL响应delete 、update 、insert 、位于begin 和end语句之间的一组语句而自动执行的一条MySQL语句。其他的语句不支持触发器。 +# 8.55 创建触发器 +- 在创建触发器时,需要给出4条语句(规则): +- 1. 唯一的触发器名; +- 2. 触发器关联的表; +- 3. 触发器应该响应的活动; +- 4. 触发器何时执行(处理之前或者之后) + +- Create trigger 语句创建 触发器 +- CREATE TRIGGER newproduct AFTER INSERT ON products FOR EACH ROW SELECT 'Product added' INTO @info; +- CREATE TRIGGER用来创建名为newproduct的新触发器。触发器可以在一个操作发生前或者发生后执行,这里AFTER INSERT 是指此触发器在INSERT语句成功执行后执行。这个触发器还指定FOR EACH ROW , 因此代码对每个插入行都会执行。文本Product added 将对每个插入的行显示一次。 + +- 注意: +- 1、触发器只有表才支持,视图,临时表都不支持触发器。 +- 2、触发器是按照每个表每个事件每次地定义,每个表每个事件每次只允许一个触发器,因此,每个表最多支持六个触发器(insert,update,delete的before 和after)。 +- 3、单一触发器不能与多个事件或多个表关联,所以,你需要一个对insert和update 操作执行的触发器,则应该定义两个触发器。 +- 4、触发器失败:如果before 触发器失败,则MySQL将不执行请求的操作,此外,如果before触发器或者语句本身失败,MySQL则将不执行after触发器。 +# 8.56 触发器类别 +## INSERT触发器 +- 是在insert语句执行之前或者执行之后被执行的触发器。 +- 1、在insert触发器代码中,可引入一个名为new的虚拟表,访问被插入的行; +- 2、在before insert触发器中,new中的值也可以被更新(允许更改被插入的值); +- 3、对于auto_increment列,new在insert执行之前包含0,在insert执行之后包含新的自动生成值 +- CREATE TRIGGER neworder AFTER INSERT ON orders FOR EACH ROW SELECT NEW.order_num; +- 创建一个名为neworder的触发器,按照AFTER INSERT ON orders 执行。在插入一个新订单到orders表时,MySQL生成一个新的订单号并保存到order_num中。触发器从NEW.order_num取得这个值并返回它。此触发器必须按照AFTER INSERT执行,因为在BEFORE INSERT语句执行之前,新order_num还没有生成。对于orders的每次插入使用这个触发器总是返回新的订单号。 +## DELETE触发器 +- Delete触发器在delete语句执行之前或者之后执行。 +- 1、在delete触发器的代码内,可以引用一个名为OLD的虚拟表,用来访问被删除的行。 +- 2、OLD中的值全为只读,不能更新。 +- CREATE TRIGGER deleteorder BEFORE DELETE ON orders FOR EACH ROW +- BEGIN +- INSERT INTO archive_orders(order_num,order_date,cust_id) values (OLD.order_num,OLD.order_date,OLD.cust_id); +- END; + +- ---------------------------------------------------------------- + +- CREATE TABLE archive_orders( + - order_num int(11) NOT NULL AUTO_INCREMENT, +- order_date datetime NOT NULL, + - cust_id int(11) NOT NULL, +- PRIMARY KEY (order_num), +- KEY fk_orders1_customers1 (cust_id), +- CONSTRAINT fk_orders1_customers1 FOREIGN KEY (cust_id) REFERENCES customers +- (cust_id) +- ) ENGINE=InnoDB AUTO_INCREMENT=20011 DEFAULT CHARSET=utf8 +- 在任意订单被删除前将执行此触发器,它使用一条INSERT 语句将OLD中的值(要被删除的订单) 保存到一个名为archive_orders的存档表中(为实际使用这个例子,我们需要用与orders相同的列创建一个名为archive_orders的表) + +- 使用BEFORE DELETE触发器的优点(相对于AFTER DELETE触发器来说)为,如果由于某种原因,订单不能存档,delete本身将被放弃。 + +- 我们在这个触发器使用了BEGIN和END语句标记触发器体。这在此例子中并不是必须的,只是为了说明使用BEGIN END 块的好处是触发器能够容纳多条SQL 语句(在BEGIN END块中一条挨着一条)。 +## UPDATE触发器 +- 在update语句执行之前或者之后执行 +- 1、在update触发器的代码内,可以引用一个名为OLD的虚拟表,用来访问以前(UPDATE语句之前)的值,引用一个名为NEW的虚拟表访问新更新的值。 +- 2、在BEFORE UPDATE触发器中,NEW中的值可能也被用于更新(允许更改将要用于UPDATE语句中的值) +- 3、OLD中的值全为只读,不能更新。 +- CREATE TRIGGER updatevendor BEFORE UPDATE ON vendors FOR EACH ROW SET NEW.vend_state = Upper(NEW.vemd_state); +- 保证州名缩写总是大写(不管UPFATE语句中是否给出了大写),每次更新一行时,NEW.vend_state中的值(将用来更新表行的值)都用Upper(NEW.vend_state)替换。 +# 8.57 总结 +- 1、通常before用于数据的验证和净化(为了保证插入表中的数据确实是需要的数据) 也适用于update触发器。 +- 2、与其他DBMS相比,MySQL 5中支持的触发器相当初级,未来的MySQL版本中估计会存在一些改进和增强触发器的支持。 +- 3、创建触发器可能需要特殊的安全访问权限,但是触发器的执行时自动的,如果insert,update,或者delete语句能够执行,则相关的触发器也能执行。 +- 4、用触发器来保证数据的一致性(大小写,格式等)。在触发器中执行这种类型的处理的优点就是它总是进行这种处理,而且透明的进行,与客户机应用无关。 +- 5、触发器的一种非常有意义的使用就是创建审计跟踪。使用触发器,把更改(如果需要,甚至还有之前和之后的状态)记录到另外一个表是非常容易的。 +- 6、MySQL触发器不支持call语句,无法从触发器内调用存储过程。 + +# MySQL索引 +- b 树和 hash 索引应用场合 区别 +- 主键索引和普通索引的区别 +- 聚簇索引在底层怎么实现的,数据和关键字是怎么存的 +- 复合索引 复合索引要把那个字段放最前,为什么 + + +- 为啥MySQL索引要用B+树而MongoDB用B树? +# 8.58 索引使用的基本原则 +- 最经常查询的列上建立聚簇索引以提高查询效率 +- 一个基本表最多只建立一个聚簇索引 +- 经常更新的列不宜建立聚簇索引 +- 主键和唯一键会自动创建索引 +# 8.59 索引分类——从数据结构角度 +## B-树,B+树,B*树 +- B/B+树是一种多级索引组织方法,是适合于组织存放在外存的大型磁盘文件的一种树状索引结构。其中用得比较多的是B+树。 +### 多路查找树 +- m叉查找树 +- 内结点:非叶节点;外结点:叶节点 +- 定义: + - 1)每个内结点至多有m个孩子和m-1个键值 + - 2)具有n个键值的结点有n+1个孩子 + - 3)有p个键值的结点:C0 K1 C1 K2 ,,, Kp Cp Ci是指针域 Ki是数据域 + - 4)键值有序(从左到右 由小到大) + - 5)满足查找树的要求 C0所在子树的所有键值 < K1 < C1所在子树的所有键值 < ... < Cp所在子树的所有键值 + +- 高度与结点关系 +- m叉查找树的高度为h,则其 h <= 第h行的结点数 <= mh-1 + +### B-树 +- 平衡的m叉查找树 +- 定义:B树首先是一棵多路查找树 + - 1)根节点至少有两个孩子 + - 2)所有非叶结点(除根节点)至少有 ceil(m/2) 个孩子 + - 3)所有叶结点都在同一层,叶结点总数 = 键值总数 +1 + - 因此一个结点的孩子数在 [ceil(m/2),m] 之间 + +- 随机查找的磁盘访问次数最多为树的高度 +### B+树 +- 定义: + - 1)树中每个非叶结点最多有m个孩子 + - 2)根节点至少有2个孩子 + - 3)除根节点外,每个非叶结点至少有ceil(m/2)个孩子 + - 4)有n个孩子的结点有n-1个键值 + - 5)所有叶节点在同一层,包含了所有键值和指向相应数据对象的指针,键值升序 + - 6)每个叶节点中的孩子数允许大于m。假设叶节点可容纳的最多键值数为m1,则指向数据对象的指针数为m1,孩子数n应满足 ceil(m1/2) < n < m1 + +- 通常在B+树上有两个头指针,一个指向根结点(进行随机搜索),一个指向关键字最小的叶结点(进行顺序搜索)。 +- 随机查找key时每次所需要的磁盘I/O次数等于B+树的高度 + +### B+树与B树的比较 +#### 组织方式不一样 +- B+树:所有有效的索引关键字值都必须存储在叶结点中,其内部结点中的键值只用于索引项的查找定位。 +- B树:有效的索引关键字值可以出现在B树的任意一个结点中。 +- 因此: +- B+树:所有关键字的查找速度基本一致 +- B树:依赖于查找关键字所在结点的层次 +#### 叶结点不同 +- B+树中叶节点间增加链表指针,提供对索引关键字的顺序扫描功能;叶节点的个数未必符合m叉查找树的要求,它依赖于键值字节数和指针字节数,为m1阶。 +### 为什么B+比B树更适合实际应用中操作系统的文件索引和数据库索引 + - 1) B+的磁盘读写代价更低 +- B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。 + + - 2) B+树的查询效率更加稳定 +- 由于非叶结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 + + - 3)树的遍历效率较高 +- 数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低) +### B*树 +- 与B+树的区别: + - 1)定义了非叶子结点键值个数至少为(2/3)*m,即块的最低使用率为2/3 + - (代替B+树的1/2); + - 2)为非叶结点也增加链表指针 +- B*树分配新结点的概率比B+树要低,空间使用率更高 + +## MySQL中的B+树适用场景 +- InnoDB存储引擎使用的是B+树。 +- B+树为对如下类型的查询有效: + - 1)全值匹配:和索引中的所有列进行匹配 + - 2)匹配最左前缀:只使用索引的第一列或前几列 + - 3)匹配列前缀:只匹配某一列的值的开头部分 + - 4)匹配范围值 + - 5)精确匹配某一列并范围匹配另外一列 + - 6)覆盖索引/只访问索引的查询 + +- 一般来说,如果B+树可以按照某种方式查找到值,那么也可以按照这种方式用于排序。如果ORDER BY子句满足前面列出的几种查询类型,则这个索引也可以满足对应的排序需求。 + +- 下面是一些关于B+树索引的限制: + - 1)如果不是按照索引的最左列开始查找,则无法使用索引 + - 2)不能跳过索引中的列 + - 3)如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找 +- + +## Hash索引 +- 哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。 + +- 只有精确匹配索引所有列的查询才有效! +- 在MySQL中,只有Memory引擎显式支持Hash索引。 + +- 限制: + - 1)哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行(无法使用覆盖索引)。不过,访问内存中的行的速度很快。 + - 2)哈希索引数据并不是按照索引值顺序存储的,所以无法进行排序 + - 3)哈希索引不支持部分索引列匹配查找。比如建立复合哈希索引(A,B),无法仅使用A使用哈希索引去查询 + - 4)不支持范围查询,仅支持等值查询 + - 5)哈希冲突严重时,索引维护的代码很高。 +## B树索引与Hash索引比较 + - 1)如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据; + +- 如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索; + + - 2)哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询); + + - 3)哈希索引也不支持多列联合索引的最左匹配规则; + + - 4)B+树索引的关键字检索效率比较平均,在有大量重复键值情况下,哈希索引的效率是极低的,因为存在所谓的哈希碰撞问题。 +- + +# 8.60 索引分类——从物理存储角度 + +## 聚簇索引 +- InnoDB的聚簇索引实际上在同一个结构中保存了B+树索引和数据行。 +- 当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。聚簇表示数据行和相邻的键值紧紧地存储在一起。因为无法同时把数据行存储在两个不同的地方,所以一个表只能有一个聚簇索引。 +- InnoDB通过主键聚簇数据。 +- 每张表都会有一个聚簇索引。聚簇索引是一级索引。 +- 聚簇索引一般是主键;没有主键,就是第一个唯一键;没有唯一键,就是隐藏ID。 +- 聚簇索引以外的所有索引都称为二级索引。在InnoDB中,二级索引中的每条记录都包含该行的主键列,以及为二级索引指定的列。 InnoDB使用这个主键值来搜索聚簇索引中的行。 +- 聚簇索引的优点: + - 1)可以将相关数据保存在一起,只需一次IO就可以取出相邻的数据 + - 2)数据访问更快,因为索引和数据保存在同一个B+树中 + - 3)使用覆盖索引扫描的查询可以直接使用叶节点中的主键值 + +- 缺点: + - 1)插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表 + - 2)更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置 + - 3)插入新行或者更新主键导致需要移动行的时候,可能面临页分裂的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。 + - 4)可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候 + - 5)二级索引(非聚簇索引)可能会更大, 因为在二级索引的叶子节点包含了引用行的主键值。这样的策略减少了当出现行移动或者页分裂时二级索引的维护工作。 + - 6)二级索引访问需要两次B树索引查找,而不是一次。因为二级索引中叶子节点保存的是行的主键值,要找到数据行,还需要拿主键值到聚簇索引中进行一次查找。 +- 对于InnoDB,自适应哈希索引能够减少这样的重复工作。 + +## 非聚簇索引 + +# 8.61 索引分类——从逻辑角度 +## 主键索引 +- 索引列的值必须唯一,并且不允许有空值 +## 唯一索引 +- 与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值(注意和主键不同)。如果是组合索引,则列值的组合必须唯一 +## 普通索引 +- 最基本的索引,它没有任何限制 +## 复合索引 +## 全文索引 +- 在相同的列上同时创建全文索引和基于值的B树索引不会有冲突,全文索引适用于MATCH AGAINST操作,而不是普通的WHERE条件操作。 +- FULLTEXT索引仅可用于 MyISAM 表;他们可以从CHAR、VARCHAR或TEXT列中作为CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE 或CREATE INDEX被添加 +## 空间索引(R-Tree) +- 空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON +- MyISAM表支持空间索引,可以用作地理数据存储。和B树索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。MySQL中的GIS支持并不完善,做的比较好的关系数据库是PostgreSQL的PostGIS。 + +- + +# 8.62 索引的特殊应用 +## InnoDB AUTO_INCREMENT +- 如果正在使用InnoDB表并且没有什么数据需要聚集,那么可以定义一个代理键作为主键,这种主键的数据应该与应用无关,最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入的,对于根据主键做关联操作的性能也会更好。 +- 最好避免随机的聚簇索引,特别是对于IO密集型应用,比如UUID,它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。 +- 如果主键的值是顺序的,那么InnoDB会把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时,下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。 + +- 使用UUID作为主键的缺点: + - 1)写入的目标页可能已经刷到磁盘上并从缓存中移除,或者还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机iO + - 2)因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少修改三个页而不是一个页。 + - 3)由于频繁的页分裂,页会变得稀疏并被不规则填充,所以最终数据会有碎片。 + +- 在把这些随机值载入到聚簇索引后,也许需要做一次OPTIMIZE TABLE来重建表并优化页的填充。 + +- 使用InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能地使用单调增加的聚簇值来插入新行。 + +- 顺序主键的缺点是什么? +- 对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。主键的上界会成为热点。因为所有的插入都在这里,所以并发插入可能导致锁竞争。另一个热点可能是AUTO_INCREMENT锁机制,可能需要重新设计表或应用。 +- AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁 +- 在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务必须等待对该表执行自己的插入操作,以便第一个事务插入的行的值是连续的。 +- innodb_autoinc_lock_mode配置选项控制用于自动增量锁定的算法。 它允许您选择如何在可预测的自动递增值序列和插入操作的最大并发性之间进行权衡。 +- innodb会在内存里保存一个计数器用来记录auto_increment的值,当插入一个新行数据时,就会用一个表锁来锁住这个计数器,直到插入结束。如果一行一行的插入数据则没有什么问题,但是如果大量的并发插入就废了,表锁会引起SQL堵塞,不但影响效率,而且可能会瞬间达到max_connections而崩溃。 +- InnoDB提供了一个可配置的锁定机制,可以显著提高使用AUTO_INCREMENT列向表中添加行的SQL语句的可伸缩性和性能。 要对InnoDB表使用AUTO_INCREMENT机制,必须将AUTO_INCREMENT列定义为索引的一部分,以便可以对表执行相当于索引的SELECT MAX(ai_col)查找以获取最大列值。 通常,这是通过使列成为某些表索引的第一列来实现的。 +- 下面介绍AUTO_INCREMENT锁定模式的行为,对不同AUTO_INCREMENT锁定模式设置的使用含义,以及InnoDB如何初始化AUTO_INCREMENT计数器。 +### 插入类型 + - 1)simple inserts + +- simple inserts指的是那种能够事先确定插入行数的语句,比如INSERT/REPLACE INTO 等插入单行或者多行的语句,语句中不包括嵌套子查询。此外,INSERT INTO … ON DUPLICATE KEY UPDATE这类语句也要除外。 + + - 2)bulk inserts + +- bulk inserts指的是事先无法确定插入行数的语句,比如INSERT/REPLACE INTO … SELECT, LOAD DATA等。 + + - 3)mixed-mode inserts + +- 指的是simple inserts类型中有些行指定了auto_increment列的值,有些行没有指定,比如: + - INSERT INTO t1 (c1,c2) VALUES (1,’a’), (NULL,’b’), (5,’c’), (NULL,’d’); +- 另外一种mixed-mode inserts是 INSERT … ON DUPLICATE KEY UPDATE这种语句,可能导致分配的auto_increment值没有被使用。 + +### innodb_autoinc_lock_mode 配置 +#### innodb_autoinc_lock_mode=0(traditional lock mode) +- 传统的auto_increment机制。这种模式下所有针对auto_increment列的插入操作都会加AUTO-INC锁,分配的值也是一个个分配,是连续的,正常情况下也不会有间隙(当然如果事务rollback了这个auto_increment值就会浪费掉,从而造成间隙)。 +#### innodb_autoinc_lock_mode=1(consecutive lock mode) +- 这种情况下,针对bulk inserts才会采用AUTO-INC锁这种方式,而针对simple inserts,则直接通过分析语句,获得要插入的数量,然后一次性分配足够的auto_increment id,只会将整个分配的过程锁住。。当然,如果其他事务已经持有了AUTO-INC锁,则simple inserts需要等待. +- 针对Mixed-mode inserts:直接分析语句,获得最坏情况下需要插入的数量,然后一次性分配足够的auto_increment id,只会将整个分配的过程锁住。 +- 保证同一条insert语句中新插入的auto_increment id都是连续的,语句之间是可能出现auto_increment值的空隙的。比如mixed-mode inserts以及bulk inserts中都有可能导致一些分配的auto_increment值被浪费掉从而导致间隙。 +#### innodb_autoinc_lock_mode=2(interleaved lock mode) +- 这种模式下任何类型的inserts都不会采用AUTO-INC锁,性能最好。这种模式是来一个分配一个,而不会锁表,只会锁住分配id的过程,和innodb_autoinc_lock_mode = 1的区别在于,不会预分配多个。但是在replication中当binlog_format为statement-based时(简称SBR statement-based replication)存在问题,因为是来一个分配一个,这样当并发执行时,“Bulk inserts”在分配时会同时向其他的INSERT分配,会出现主从不一致(从库执行结果和主库执行结果不一样),因为binlog只会记录开始的insert id。 +- 可能会在同一条语句内部产生auto_increment值间隙。 +### 不同模式下间隙情况 +#### simple inserts +- 针对innodb_autoinc_lock_mode=0,1,2,只有在一个有auto_increment列操作的事务出现回滚时,分配的auto_increment的值会丢弃不再使用,从而造成间隙。 +#### bulk inserts +- innodb_autoinc_lock_mode=0,由于一直会持有AUTO-INC锁直到语句结束,生成的值都是连续的,不会产生间隙。 +- innodb_autoinc_lock_mode=1,这时候一条语句内不会产生间隙,但是语句之间可能会产生间隙。 +- innodb_autoinc_lock_mode=2,如果有并发的insert操作,那么同一条语句内都可能产生间隙。 +#### mixed-mode inserts +- 这种模式下针对innodb_autoinc_lock_mode的值配置不同,结果也会不同,当然innodb_autoinc_lock_mode=0时时不会产生间隙的,而innodb_autoinc_lock_mode=1以及innodb_autoinc_lock_mode=2是会产生间隙的。 +- 另外注意的一点是,在master-slave这种架构中,复制如果采用statement-based replication这种方式,则innodb_autoinc_lock_mode=0或1才是安全的。而如果是采用row-based replication或者mixed-based replication,则innodb_autoinc_lock_mode=0,1,2都是安全的。 +## 覆盖索引 +- 如果一个索引包含了所有需要查询字段的值,就称为覆盖索引。 +- 覆盖索引的优点: + - 1)索引条目远少于数据行大小,如果只需要读取索引,则MySQL就会极大地减少数据访问了,这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对IO密集型应用也有帮助,因为索引比数据更小,更容易全部放入内存中。 + - 2)因为索引是按照列值顺序存储的,对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO次数会少得多。 + - 3)InnoDB的二级索引在叶节点中保存了行的主键值,如果二级索引是覆盖索引,则可以避免对主键聚簇索引的二次查询。 + +- 不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引都不存储索引列的值,所以MySQL只能使用B树索引做覆盖索引。 +- 当发起一个索引覆盖查询时,在EXPLAIN的Extra列可以看到Using index的信息。 + +- InnoDB的二级索引的叶子节点都包含了主键的值,这意味着InnoDB的二级索引可以有效利用这些额外的主键列来覆盖查询。 +## 使用索引进行排序 +- MySQL有两种可以生成有序的结果:通过排序操作;按索引顺序扫描。如果EXPLAIN出来的type列的值为index,则说明MySQL使用了索引顺序扫描来做排序。 + +- 扫描索引本身是很快的,但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引顺序读取数据的速度通常要比顺序地全表扫描要慢,尤其是在IO密集型的工作负载时。 + +- 只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(降序或升序,索引默认是升序)都一样时,MySQL才可以使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一张表时,才能使用索引做排序。 +- ORDER BY子句和查找型索引的限制是一样的,都需要满足索引的最左前缀的要求。 + +- 有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。 + + +- + +## 前缀压缩索引 +- MyISAM通过前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中。默认只压缩字符串,但通过参数调整也能对整数进行压缩。 +- MyISAM压缩每个索引块的方法时,先完全保存索引块的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。 +- 压缩块使用更少的情况,代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。 +## 冗余和重复索引 +- 冗余索引:MySQL允许在相同列上创建多个索引。MySQL需要单独维护重复的索引,并且优化器在优化查询时也需要逐个地进行考虑,这会影响性能。 +- 重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应该避免这样创建重复索引,发现以后也应该立即移除。 + +- 冗余索引和重复索引有一些不同。如果创建了索引(A,B),又创建了索引(A)就是冗余索引,索引(A,B)也可以当做索引(A)来使用。但是如果再创建索引(B,A),就不是冗余索引。另外,其他不同类型的索引也不会是B树索引的冗余索引。 + +- 冗余索引通常发生在为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是扩展已有的索引(A),还有一种情况是将一个索引扩展为(A,PK),对于InnoDB而言PK已经包含在二级索引中了,所以这也是冗余的。 + +- 大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。但也有时候出于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。 +- 例如,现在在整数列上有一个索引,需要额外增加一个很长的VARCHAR列来扩展该索引,那性能可能会急剧下降。 + +- 可以使用一些工具来找出冗余和重复的索引。 +## 索引重用 +- 现有索引(A,B,C),如果要使用索引,那么where中必须写为A=a and B = b and C = c。如果没有对B的筛选,还想使用索引,怎么绕过最左前缀匹配呢? + - 假设B是一个选择性很低的列,只有b1和b2两种取值,那么查询可以写为A = a and B in(b1,b2) and C = c。 +## 避免多个范围条件 +- 对于范围条件查询,MySQL无法再使用范围列后面的其他索引列了,但是对于多个等值条件查询(in ...)则没有这个限制。 +- 假设有索引(A,B),查询条件为 A > a and B < b,那么此时无法同时使用A和B的复合索引,只能用到A的索引。一定要用的话可以考虑将A转为in(a1,a2...)。 +## 优化limit +- 延迟关联,使用覆盖索引 +- +- + +# 8.63 适合建索引的情况 +- 主键 +- 连接中频繁使用的列 +- 在某一范围内频繁搜索的列和按排列顺序频繁搜索的列 +# 8.64 不适合建索引的情况 +- 很少或从来不在查询中引用的列 +- 只有两个或很少几个值的列 +- 以bit text image 数据类型定义的列 +- 数据行数很少的小表 +# 8.65 索引优点 + - 1)大大减少了服务器需要扫描的数据量 + - 2)帮助服务器减少排序和临时表(group by和order by都可以使用索引,因为索引有序) + - 3)可以将随机IO变为顺序IO(覆盖索引) +# 8.66 索引缺点 +- 创建索引要花费时间,占用存储空间 +- 减慢数据修改速度 +# 8.67 索引失效 + + +- CREATE TABLE staffs ( +- id INT PRIMARY KEY AUTO_INCREMENT, + - NAME VARCHAR (24) NOT NULL DEFAULT '' COMMENT '姓名', +- age INT NOT NULL DEFAULT 0 COMMENT '年龄', +- pos VARCHAR (20) NOT NULL DEFAULT '' COMMENT '职位', +- add_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间' +- ) CHARSET utf8 COMMENT '员工记录表' ; +- ALTER TABLE staffs ADD INDEX idx_staffs_nameAgePos(name, age, pos); + + + +- + +## 1、全值匹配 +- EXPLAIN SELECT * FROM staffs WHERE NAME = 'July'; +- EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25; +- EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25 AND pos = 'dev'; + + +## 2、最佳左前缀法则 +- 如果索引了多列,要遵守最佳左前缀法则,指的是从索引的最左边的列开始并且不跳过索引中的列。 +- 查询时就按照建索引的顺序进行筛选 + +- EXPLAIN SELECT * FROM staffs WHERE age = 25 AND pos = 'dev'; +- EXPLAIN SELECT * FROM staffs WHERE pos = 'dev'; + +## 3、在索引上使用表达式 + + - 索引列上使用了表达式,如where substr(a, 1, 3) = 'hhh',where a = a + 1,表达式是一大忌讳,再简单MySQL也不认。 +- 有时数据量不是大到严重影响速度时,一般可以先查出来,比如先查所有有订单记录的数据,再在程序中去筛选 +- 哪怕是该字段没有建立索引,但不能保证以后不在这个字段上建立索引,所以可以这么说:不要在任何字段上进行操作。 + +## 4、range 类型查询字段后面的索引无效 + +- 最后一次只用到了两个索引 +- 此时可以建一个只含前两个字段的索引 +## 5、尽量使用覆盖索引 + +## 6、使用不等于时索引失效 + +## 7、is (not) null 时索引失效 + +- 如果没有值,可以使其等于一个默认值,这样就可以利用到索引了。 +## 8、like 以通配符开头会导致全表扫描 + +## 9、varchar 类型不加单引号索引失效 +- 不加单引号会出现类型转换,此时索引失效 + +## 10、使用or时索引失效 +- 所以要少用or + +- + +# 8.68 总结 +- 假设index(a,b,c) +Where语句 索引是否被使用 +where a = 3 Y,使用到a +where a = 3 and b = 5 Y,使用到a,b +where a = 3 and b = 5 and c = 4 Y,使用到a,b,c +where b = 3 | where b = 3 and c = 4 | where c = 4 N +where a = 3 and c = 5 使用到a, 但是C不可以,中间断了 +where a = 3 and b > 4 and c = 7 使用到a和b, c在范围之后,断了 +where a = 3 and b like 'kk%' and c = 4 同上 + +- + +# MySQL查询分析工具 +# 8.69 慢查询日志 +- MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10S以上的语句。默认情况下,Mysql数据库并不启动慢查询日志,需要我们手动来设置这个参数,当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件,也支持将日志记录写入数据库表。 + +- slow_query_log :是否开启慢查询日志,1表示开启,0表示关闭。 +- slow-query-log-file:新版(5.6及以上版本)MySQL数据库慢查询日志存储路径。可以不设置该参数,系统则会默认给一个缺省的文件host_name-slow.log +- long_query_time :慢查询阈值,当查询时间多于设定的阈值时,记录日志。 +- log_queries_not_using_indexes:未使用索引的查询也被记录到慢查询日志中(可选项)。 +- log_output:日志存储方式。log_output='FILE'表示将日志存入文件,默认值是'FILE'。log_output='TABLE'表示将日志存入数据库,这样日志信息就会被写入到mysql.slow_log表中。MySQL数据库支持同时两种日志存储方式,配置的时候以逗号隔开即可,如:log_output='FILE,TABLE'。日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件。 + +- 在实际生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow。 +- s, 是表示按照何种方式排序 +- c: 访问计数 +- +- l: 锁定时间 +- +- r: 返回记录 +- +- t: 查询时间 +- +- al:平均锁定时间 +- +- ar:平均返回记录数 +- +- at:平均查询时间 +- +- -t, 是top n的意思,即为返回前面多少条的数据; +- -g, 后边可以写一个正则匹配模式,大小写不敏感的; +- +- 比如: +- 得到返回记录集最多的10个SQL。 +- mysqldumpslow -s r -t 10 /database/mysql/mysql06_slow.log +- +- 得到访问次数最多的10个SQL +- mysqldumpslow -s c -t 10 /database/mysql/mysql06_slow.log +- +- 得到按照时间排序的前10条里面含有左连接的查询语句。 +- mysqldumpslow -s t -t 10 -g “left join” /database/mysql/mysql06_slow.log +- +- 另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现刷屏的情况。 +- mysqldumpslow -s r -t 20 /mysqldata/mysql/mysql06-slow.log | more +# 8.70 explain +- explain SQL分析 每个列代表什么含义(关于优化级别 ref 和 all,什么时候应该用到index却没用到,关于extra列出现了usetempory 和 filesort分别的原因和如何着手优化等) + + + + + +- 各字段解释: +## id + + + - 1)id相同,表示执行顺序从上到下 + +- where 条件从右往左读取 + - 2)id不同,如果是子查询,id的序号会递增,id越大优先级越高,越先被执行 + +- primary 是主查询 +- subquery是子查询 + + - 3)id有相同的,也有不同的,同时存在 +- id相同的可以被认为是一组,从上往下顺序执行 +- 在所有组中,id值越大,优先级越高,越先执行。 + +- Derived:衍生的 + +## select_type + + +## table +- 显示这一行的数据是来自哪一张表 +## type(重要) + +- type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是: +- system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL , +- 一般来说,得保证查询至少达到range级别,最好能达到ref。 + + +## possible_keys +- 显示可能应用在这张表中的索引,一个或多个。 +- 查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用 +- 理论上可以被用上的 +## key +- 实际使用到的索引 + + +- + +## key_len + + + +## ref + + +- 由key_len可知t1表的idx_col1_col2被充分使用,col1匹配t2表的col1,col2匹配了一个常量,即 'ac' + +- 用到了多少个字段上的索引,ref就会有几个(大部分情况) +- 或者可以根据key_len的倍数来判断使用了几个字段上的索引 + +- + +## rows + + +- rows越少越好 + +## extra + + +- + +- 前两个最重要: +### 1、Using fileSort + + + + + - 建立索引的作用1)查询2)排序 +- 如果排序字段没有索引,那么可能会产生filesort文件排序,降低效率。 + +### 2、临时表Using temporary + + + +- 如果数据量很大,使用临时表效率会很低。 +### 3、Using Index + + +- 覆盖索引(Covering Index),一说为索引覆盖。 +- 理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。 +- +- 理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引。 + +- 注意: +- 如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select *, +- 因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。 + + + + +### 4、Impossible where + + +- + +## explain实例1 + + + - 第一行(执行顺序4):id列为1,表示是union里的第一个select,select_type列的primary表 示该查询为外层查询,table列被标记为<derived3>,表示查询结果来自一个衍生表,其中derived3中3代表该查询衍生自第三个select查询,即id为3的select。【select d1.name......】 + - 第二行(执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【select id,name from t1 where other_column=''】 + - 第三行(执行顺序3):select列表中的子查询select_type为subquery,为整个查询中的第二个select。【select id from t3】 + - 第四行(执行顺序1):select_type为union,说明第四个select是union里的第二个select,最先执行【select name,id from t2】 + - 第五行(执行顺序5):代表从union的临时表中读取行的阶段,table列的<union1,4>表示用第一个和第四个select的结果进行union操作。【两个结果union操作】 + +## explain实例2(单表) + +- CREATE TABLE IF NOT EXISTS `article` ( +- `id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, +- `author_id` INT(10) UNSIGNED NOT NULL, +- `category_id` INT(10) UNSIGNED NOT NULL, +- `views` INT(10) UNSIGNED NOT NULL, +- `comments` INT(10) UNSIGNED NOT NULL, + - `title` VARBINARY(255) NOT NULL, +- `content` TEXT NOT NULL +- ); + + +- #查询 category_id 为 1 且 comments 大于 1 的情况下,views 最多的 article_id。 +- EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1; +- +- #结论:很显然,type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。 +- #开始优化: +- # 1.1 新建索引+删除索引 +- #ALTER TABLE `article` ADD INDEX idx_article_ccv ( `category_id` , `comments`, `views` ); +- create index idx_article_ccv on article(category_id,comments,views); +- DROP INDEX idx_article_ccv ON article + +- # 1.2 第2次EXPLAIN +- EXPLAIN SELECT id,author_id FROM `article` WHERE category_id = 1 AND comments >1 ORDER BY views DESC LIMIT 1; +- EXPLAIN SELECT id,author_id FROM `article` WHERE category_id = 1 AND comments =3 ORDER BY views DESC LIMIT 1 +- + + +- #结论: +- #type 变成了 range,这是可以忍受的。但是 extra 里使用 Using filesort 仍是无法接受的。 +- #但是我们已经建立了索引,为啥没用呢? +- #这是因为按照 BTree 索引的工作原理, +- # 先排序 category_id, +- # 如果遇到相同的 category_id 则再排序 comments,如果遇到相同的 comments 则再排序 views。 +- #当 comments 字段在联合索引里处于中间位置时, +- #因comments > 1 条件是一个范围值(所谓 range), + +- #MySQL 无法利用索引再对后面的 views 部分进行检索,即 range 类型查询字段后面的索引无效。 + + +- # 1.3 删除第一次建立的索引 +- DROP INDEX idx_article_ccv ON article; +- +- # 1.4 第2次新建索引 +- #ALTER TABLE `article` ADD INDEX idx_article_cv ( `category_id` , `views` ) ; +- create index idx_article_cv on article(category_id,views); + +- # 1.5 第3次EXPLAIN +- EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1; + +- #结论:可以看到,type 变为了 ref,Extra 中的 Using filesort 也消失了,结果非常理想。 +- DROP INDEX idx_article_cv ON article; +## explain实例3(两表) +- CREATE TABLE IF NOT EXISTS `class` ( +- `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, +- `card` INT(10) UNSIGNED NOT NULL, +- PRIMARY KEY (`id`) +- ); +- CREATE TABLE IF NOT EXISTS `book` ( +- `bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, +- `card` INT(10) UNSIGNED NOT NULL, +- PRIMARY KEY (`bookid`) +- ); +- class: book: +- + +- # 下面开始explain分析 +- EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card; +- #结论:type 有All +- +- # 添加索引优化 +- ALTER TABLE `book` ADD INDEX Y ( `card`); +- +- # 第2次explain +- EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card; + +- #可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。 +- #这是由左连接特性决定的。LEFT JOIN 条件用于确定如何从右表搜索行,左边一定都有, +- #所以右边是我们的关键点,一定需要建立索引。 +- #左外连接索引建右表 +- # 删除旧索引 + 新建 + 第3次explain +- DROP INDEX Y ON book; +- ALTER TABLE class ADD INDEX X (card); +- EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card; + + +- # 然后来看一个右连接查询: +- #优化较明显。这是因为 RIGHT JOIN 条件用于确定如何从左表搜索行,右边一定都有,所以左边是我们的关键点,一定需要建立索引。 +- EXPLAIN SELECT * FROM class RIGHT JOIN book ON class.card = book.card; +- DROP INDEX X ON class; +- ALTER TABLE book ADD INDEX Y (card); +- # 右连接,基本无变化 +- EXPLAIN SELECT * FROM class RIGHT JOIN book ON class.card = book.card; +## explain实例3(三表) +- phone: + + +- +- ALTER TABLE `phone` ADD INDEX z ( `card`); +- ALTER TABLE `book` ADD INDEX Y ( `card`);#上一个case建过一个同样的 +- EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card=book.card LEFT JOIN phone ON book.card = phone.card; + +- # 后 2 行的 type 都是 ref 且总 rows 优化很好,效果不错。因此索引最好设置在需要经常查询的字段中。 +- ================================================================================== +- 【结论】 +- Join语句的优化 +- +- 尽可能减少Join语句中的NestedLoop的循环总次数;“永远用小结果集驱动大的结果集”。 +- 优先优化NestedLoop的内层循环; +- 保证Join语句中被驱动表上Join条件字段已经被索引; +- 当无法保证被驱动表的Join条件字段被索引且内存资源充足的前提下,不要太吝惜JoinBuffer的设置; + + +- + +# 8.71 show profile + +## 是否支持 +- show variables like 'profiling'; + +## 开启功能 + +- + +## 查看结果 +- show profiles; + +## 诊断SQL + + + +- + +- 一般性建议 + + +- + +# 8.72 习题 + + +- 【建表语句】 +- create table test03( +- id int primary key not null auto_increment, +- c1 char(10), +- c2 char(10), +- c3 char(10), +- c4 char(10), +- c5 char(10) +- ); + +- 【建索引】 + - create index idx_test03_c1234 on test03(c1,c2,c3,c4); +- show index from test03; +- +- + +- 问题:我们创建了复合索引idx_test03_c1234 ,根据以下SQL分析下索引使用情况? +- explain select * from test03 where c1='a1'; +- explain select * from test03 where c1='a1' and c2='a2'; +- explain select * from test03 where c1='a1' and c2='a2' and c3='a3'; +- explain select * from test03 where c1='a1' and c2='a2' and c3='a3' and c4='a4'; + + - 1) +- explain select * from test03 where c1='a1' and c2='a2' and c3='a3' and c4='a4'; + +- 4 + - 2) +- explain select * from test03 where c1='a1' and c2='a2' and c4='a4' and c3='a3'; + +- 4 +- 原因是MySQL的optimizer会进行优化,将查询语句调整为索引的顺序 + + - 3) +- explain select * from test03 where c1='a1' and c2='a2' and c3>'a3' and c4='a4'; + +- 3 + - 4) +- explain select * from test03 where c1='a1' and c2='a2' and c4>'a4' and c3='a3'; + +- 4 +- SQL的优化器也调整为1,2,3,4的顺序 + + - 5) +- explain select * from test03 where c1='a1' and c2='a2' and c4='a4' order by c3; +- c3作用在排序而不是查找 + +- 因为有c3的索引,所以没有出现using filesort +- 2 无filesort + - 6) +- explain select * from test03 where c1='a1' and c2='a2' order by c3; + +- 因为有c3的索引,所以没有出现using filesort +- 2 无filesort + - 7) +- explain select * from test03 where c1='a1' and c2='a2' order by c4; + +- 出现了filesort +- 2 有filesort + - 8) +- 8.1 explain select * from test03 where c1='a1' and c5='a5' order by c2,c3; + +- 只用c1一个字段索引,但是c2、c3用于排序,无filesort +- 1 无filesort +- 8.2 explain select * from test03 where c1='a1' and c5='a5' order by c3,c2; +- 出现了filesort,我们建的索引是1234,它没有按照顺序来,3 2 颠倒了 +- 1 有filesort + + - 9) +- explain select * from test03 where c1='a1' and c2='a2' order by c2,c3; + +- 10) +- 10.1 explain select * from test03 where c1='a1' and c2='a2' and c5='a5' order by c2,c3; 用c1、c2两个字段索引,但是c2、c3用于排序,无filesort + +- 10.2 explain select * from test03 where c1='a1' and c2='a2' and c5='a5' order by c3,c2; +- 本例有常量c2的情况,和8.2对比 + +- where中添加了了c2,此时c2对应的是常量,所以order by c3,c2 真正起作用的只有c3 +- 10.3 explain select * from test03 where c1='a1' and c5='a5' order by c3,c2; +- +- 1 有filesort + - 11) +- explain select * from test03 where c1='a1' and c4='a4' group by c2,c3; + +- 1 无filesort +- group by 也会默认进行排序 + - 12) +- explain select * from test03 where c1='a1' and c4='a4' group by c3,c2; + +- Using where; Using temporary; Using filesort +- 1 有filesort +- 因为group by 顺序与索引顺序不同,所以会产生临时表,并排序 +- 分组之前会先排序 +- + +# MySQL性能优化 +- 性能优化可以理解为在一定工作负载下尽可能地降低响应时间。 +- 性能优化 不等于 提升QPS,这其实仅仅是吞吐量的优化。吞吐量的提升可以看做性能优化的副产品。因为每条查询执行时间变短,因此可以让服务器每秒执行更多的查询。 + +- 如果目标是降低响应时间,那么就需要测量时间花在什么地方。没有测量就没有调优。 +- 一旦掌握并实践面向响应时间的优化方法,就会发现需要不断地对系统进行性能剖析(profiling)。性能剖析分为两个步骤:测量任务所花费的时间;对结果进行统计和排序,把重要的任务排在前面。 +- MySQL的profile将最重要的任务展示在前面,但有时候没显示出来的信息也很重要。比如: +- 值得优化的查询:一些只占总响应时间比重很小的查询是不值得优化的。 +- 异常情况:某些任务即使没有出现在profile输出的前面也需要优化。比如非常影响用户体验的某些任务,即使执行次数较少。 + +- + +# MySQL查询优化 +- 从效果上第一条影响最大,后面越来越小。 +- ① SQL语句及索引的优化 +- ② 数据库表结构的优化 +- ③ 系统配置的优化 +- ④ 硬件的优化 +# 8.73 慢查询基础:优化数据访问 +- 查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不常见。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现提供下面两个步骤来分析总是很有效。 +- 1、确认应用程序是否在检索大量超过需要的数据,通常是访问了太多的行,但有时候也可能是访问了太多的列。 +- 2、确认MySQL服务器层是否在分析大量超过需要的数据行。 +## 是否向数据库请求了不需要的数据 + - 1)查询不需要的记录:尽量使用LIMIT来获取所需的数据,而非取出全部数据然后在Application中获取某些行。 + - 2)多表关联时返回全部列 + - 3)总是取出全部列 + - 4)重复查询相同的数据:使用缓存 +## MySQL是否在扫描额外的记录 +- 对于MySQL,最简单的衡量查询开销的三个指标如下: + - 1)响应时间 + - 2)扫描的行数 + - 3)返回的行数 + +### 响应时间 +- 响应时间是两个部分的和:服务时间和排队时间。服务时间是指数据库处理这个查询真正花了多少时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——等待IO或等待锁。 +- 在不同类型的应用压力下,响应时间并没有什么一致的规律或者公式。响应时间既可能是一个问题的结果也可能是一个问题的原因。 +- 当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。可以采用快速上限估计法来估算查询的响应时间:了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机IO,再用其乘以在具体硬件条件下一次IO的消耗时间,最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。 +### 扫描的行数和返回的行数 +- 分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。 +- 对于找出那些糟糕的查询,这个指标可能还不够完美,因为并不是所有的行的访问代价都是相同的。较短的行的访问速度更快,内存中的行也比磁盘中的行的访问速度要快得多。 +- 理想情况下扫描的行数和返回的行数应该是相同的。一般扫描的行数对返回的行数的比率很小,一般在1:1和1:10之间。 +### 扫描的行数和访问类型 +- 在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无需扫描就能返回结果。 +- 在EXPLAIN语句中的type列反映了访问类型。访问类型有全表扫描、索引扫描、范围扫描、唯一索引、常数引用等,速度由慢到快。 +- 如果查询没有办法找到合适的访问类型,那么解决的最好方法通常就是增加一个合适的索引。 +- 一般MytSQL能够使用如下三种方式应用WHERE条件,从好到坏依次为: + - 1)在索引中使用WHERE条件来过滤不匹配的记录,这是在存储引擎层完成的 + - 2)使用覆盖索引(Extra中Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果,这是在MySQL服务器层完成的,但无需再回表查询记录 + - 3)从数据表中返回数据,然后过滤不满足条件的记录(Extra中Using Where),这是在MySQL服务器层完成的,MySQL需要先从数据表读出记然后过滤。 + +- MySQL不会告诉我们生成结果实际上需要扫描多少行数据(例如关联查询结果返回的一条记录通过是由多条记录组成),而只会告诉我们生成结果时一共扫描了多少行数据。扫描的行数中大部分都很可能是被WHERE条件过滤掉的,对最终的结果集没有贡献。 + +- 如果发现查询需要扫描大量的数据但只返回少数的行,那么通过可以尝试下面的技巧去优化它: + - 1)使用覆盖索引 + - 2)改变表结构,例如使用单独的汇总表 + - 3)重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询 +# 8.74 重构查询的方式 +## 一个复杂查询还是多个简单查询 +- 传统实现中总是强调数据库层完成尽可能多的工作,因为以前认为网络通信、查询解析和优化是一件代价很高的事情。但是这样的想法对MySQL并不适用,MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。 +- MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同时,使用尽可能少的查询当然是很好的。但是有时候,将一个大查询分解为多个小查询是很有必要的。 +## 切分查询 +- 有时候对于一个大查询我们需要分而治之,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一部分查询结果。 +- 比如删除旧的数据,分批删除效率会高很多。 + +## 分解关联查询 +- 很多高性能的应用都会对关联查询进行分解。可以对每一张表进行一次单表查询,然后将结果在应用程序中进行关联。 +- 这样做的好处有: + - 1)让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。对MySQL的查询缓存来说,如果关联中的某张表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某张表很少改变,那么基于该表的查询就可以重复利用缓存了。 + - 2)将查询分解后,执行单个查询可以减少锁的竞争 + - 3)在应用层做关联,可以更容易对数据库进行拆分 + - 4)查询本身效率也可能会有所提升。比如使用IN来代替JOIN,可以让MySQL按照ID顺序进行查询。 + - 5)可以减少冗余记录的查询。在数据库中做关联查询可能需要重复地访问一部分数据。 + - 6)相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。 +# 8.75 优化特定类型的查询 +## JOIN 优化 + - 1)确保ON或者Using子句上的列有索引,在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联时,如果优化器的关联顺序是B、A,那么就不需要在B表的对应列上建索引。一般情况下只需要在关联顺序的第二个表的相应列上创建索引。 + - 2)确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程 + + +## 小表驱动大表 +- 两张表连接,类似于二重循环 +- 外层的表应该是小表,内层的应该是大表 +- 虽然总的遍历次数是一样的,但是频繁切换数据表是影响效率的(IO次数),应该尽可能减少切换表的次数。 + +- A in B: +- for b in B: +- for a in A: +- if a == b: +- putIntoResultSet() + +- A exists B: +- for a in A: +- for b in B: +- if a == b: +- putIntoResultSet() + +- 所以,如果A是小表,B是大表时 +- 如果用in,那么是B in A +- 如果用exists,那么是A exists B + + +## order by优化 +### 尽量使用index方式排序,遵照索引的最佳左前缀 + +- CREATE TABLE tblA( +- #id int primary key not null auto_increment, +- age INT, +- birth TIMESTAMP NOT NULL +- ); +- CREATE INDEX idx_A_ageBirth ON tblA(age,birth); + +- 排序时使用的字段的顺序最好与index建立的顺序相同 + + +- 如果字段顺序不同,那么也会出现filesort + +- MySQL支持二种方式的排序,FileSort和Index,Index效率高. +- 它指MySQL扫描索引本身完成排序。FileSort方式效率较低。 + +- order a,b +- where a = xxx order by b +- + +### 非索引列的filesort算法 + + +- 问题: +- 在sort_buffer中单路排序比双路排序要多占用很多空间,因为单路排序是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排……从而多次I/O。 +- 本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。 + +### 优化策略 + +- 分组时也是需要order by +#### 1. Order by时select * 是一个大忌 +- 只取出需要的字段, 这点非常重要。在这里的影响是: +- 1.1 当Query的字段大小总和小于max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。 +- 1.2 两种算法的数据都有可能超出sort_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size。 +#### 2. 尝试提高 sort_buffer_size +- 不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的 +- +#### 3. 尝试提高 max_length_for_sort_data +- 提高这个参数, 会增加用改进算法的概率。但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率. +### 总结 + +## group by 优化 + +- 当无法使用索引时,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。 +- 当不遵照最佳左前缀,order by会出现filesort,而group by会出现临时表和filesort +## limit 优化 +- 当偏移量非常大的时候,比如limit 1000,20 这样的查询,这时MySQL需要查询10020条记录然后只返回最后20条,这样的代价非常高。要优化这种查询,要么在页面中限制分页数量,要么优化大偏移的性能。 +- 一个简单的办法是使用覆盖索引(延迟关联) +- +- 如果使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描。 +- 假设主键递增: + +- 通过判断id的范围来分页 +- select id,my_sn from big_data where id>5000000 limit 10; +- 也得到了分页的数据,但是我们发现如果id不是顺序的,也就是如果有数据删除过的话,那么这样分页数据就会不正确,这个是有缺陷的。 +## UNION优化 +- MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION查询中都没法很好使用。经常需要手工将WHERE、LIMIT、ORDER BY等子句下推到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化。 +- + +# MySQL实现层次模型 +# 8.76 邻接模型 +- 属性:id,id_parent,other fields +- 兄弟结点无序 +- 特点: + - 1)DML节点效率高,查询性能最高 + - 2)只支持单父节点 + - 3)递归实现 + - 4)删除子树较难 +# 8.77 物化路径模型 +- 属性:materialized_path,other fields + - PathID(1,1.1,1.2,1.1.1) +- 使用层次式的路径,明确地标识出来,一般用字符串存储路径,允许兄弟节点有序 +- 特点: + - 1)查询编写容易 + - 2)计算由路径导出的层次不方便 + - 3)会产生重复记录问题 + - 4)查询性能中等 +# 8.78 嵌套集合模型 +- 属性:left_num,right_num,other fields +- 每一个节点都有一个left_num和一个right_num。某节点的后代的left_num和right_num都会在该节点的left_num和right_num范围内。 +- 特点: + - 1)查找子节点容易,但是无法实现缩排 + - 2)适合DFS + - 3)DML开销大,查询性能最低 + +- 分区分库分表 + + +- + +- 分区(针对表) +- 简介 +- 数据表的物理存储拆分为多个文件 +- 分区表是一个独立的逻辑表,其底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的句柄对象(Handler Object)的封装。对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的。 +- MySQL实现分区表的方式——对底层表的封装——意味着索引也是按照分区的子表定义的,而没有全局索引。 +- MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样查询就无须扫描所有分区——只需要查找包含需要数据的分区即可。 +- 分区的一个目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。 +- 分区非常适合在以下场景: + - 1)表非常大以至于无法全部放在内存中,或者只在表的最后部分有热点数据,其他均为历史数据 + - 2)分区表的数据更容易维护。(批量删除数据->清除整个分区) + - 3)分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备 + - 4)可以使用分区表来避免某些特殊的瓶颈。、比如InnoDB的单个索引的互斥访问,ext3文件系统的inode锁竞争。 + - 5)还可以备份和恢复独立的分区 + +- 分区表也有一些限制: + - 1)一个表最多只能有1024个分区 + - 2)如果分区字段有主键或者唯一索引,那么所有主键列和唯一索引列都必须包含进来 + - 3)分区表中无法使用外键索引 + +## 原理 +- 存储引擎管理分区的各个底层表和管理普通表一样,所有的底层表都必须使用相同的存储引擎,分区表的索引只是在各个底层表上各自加一个完全相同的索引。从存储引擎的角度,底层表和一个普通表没有任何不同。 +### select +- 分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。 +### insert +- 当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应底层表。 +### delete +- 当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。 +### update +- 当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。 + +- 虽然每个操作都会先打开并锁住所有的底层表,但这并不是分区表在处理过程中是锁住全表的,如果存储引擎能够自己实现行级锁,则会在分区层释放对应表锁,比如InnoDB,这个加锁和解锁的过程与普通InnoDB上的查询类似 +- 分区类型 + + +### Range分区 +#### 原理 +- MySQL将会根据指定的拆分策略,把数据放在不同的表文件上。相当于在文件上被拆成了小块.但是,对外给客户的感觉还是一张表,是透明的。 +#### 案例 +- CREATE TABLE tbl_new( +- id INT NOT NULL PRIMARY KEY, +- title VARCHAR(20) NOT NULL DEFAULT '' +- )ENGINE MYISAM CHARSET utf8 +- PARTITION BY RANGE(id)( +- PARTITION t0 VALUES LESS THAN(10), +- PARTITION t1 VALUES LESS THAN(20), +- PARTITION t2 VALUES LESS THAN(MAXVALUE) +- ); + +- 0~10放在t0 +- 10~20放在t1 +- >20放在t2 + +- 如果要查询id在20以上的,那么会直接去t2分区查找 +- 如果插入的记录的id在20以上,那么会插入到t2分区 +- 物理文件: + +- 可以看出,普通的InnoDB引擎的表是一个frm和一个ibd文件 +- 分区之后的MyIasm引擎的表有一个frm和par文件,此外每个分区还有一个myi和myd文件。 +- frm:表的结构信息 +- par:表的分区信息 +- myi:表的索引信息 +- myd:表的数据信息 + + +- range的字段未必一定是id + +### List分区 +#### 原理 +- MySQL中的LIST分区在很多方面类似于RANGE分区。和按照RANGE分区一样,每个分区必须明确定义。它们的主要区别在于, +- LIST分区中每个分区的定义和选择是基于某列的值从属于一个值列表集中的一个值, +- 而RANGE分区是从属于一个连续区间值的集合。 + +#### 案例 +- create table area( +- id INT NOT NULL PRIMARY KEY, +- region varchar(20) +- )engine myisam charset utf8; + +- insert into area values(1,'bj'); +- insert into area values(2,'sh'); +- insert into area values(3,'gz'); +- insert into area values(4,'sz'); +- 这个area的值是确定的 +- create table user ( +- uid int not null, +- userName varchar(20), +- area_id int +- )engine myisam charset utf8 +- partition by list(area_id) ( + - partition bj values in (1), + - partition sh values in (2), + - partition gz values in (3), + - partition sz values in (4) +- ); +- User: + + + + + +### 其他分区 +#### Hash分区 +#### Key分区 +#### 子分区 +## 分区表如何应用于大数据量 +- 数据量超大时,肯定不能去全表扫描,并且B树索引也无法起作用,除非是覆盖索引。这正是分区要做的事情。理解分区时可以将其当做索引的最初形态,以代价非常小的方式定位到需要的数据在哪一片区域。在这篇区域中,可以做顺序扫描,可以建索引,还可以将数据都缓存到内存。 +- 为了保证大数据量的可扩展性,一般有下面两个策略: + - 1)全量扫描数据,不要加任何索引 +- 可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。只要能够使用WHERE条件,将需要的数据限制在少数分区中,则效率是很高的。 + - 2)索引数据,并分离热点 +- 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。这样查询就可以只访问一个很小的分区表,能够使用索引,也能够有效地使用缓存。 +- 分区表的陷阱 +### NULL值会使分区过滤无效 +- 分区的表达式的值可以是NULL;第一个分区是一个特殊分区,如果表达式的值为NULL或非法制,记录都会被存放到第一个分区。WHERE查询时即使看起来可以过滤到只有一个分区,但实际会检查两个分区,即第一个分区。最好是设置分区的列为NOT NULL。 +### 分区列和索引列不匹配 +- 如果定义的索引列和分区列不匹配,会导致索引无法进行分区过滤。 +- 假设在列a上定义了索引,而在列b上进行分区。因为每个分区都有其独立的索引,所以扫描b上的索引就需要扫描每一个分区内对应的索引。 + +### 选择分区的成本可能很高 +- 尤其是范围分区,对于回答“这一行属于哪个分区”、“这些符合查询条件的行在哪些分区”这样的问题的成本可能会非常高。其他的分区类型,比如键分区和哈希分区,就没有这样的问题。 +- 在批量插入时问题尤其严重。 +### 其他限制 + - 1)每个分区都必须使用同样的存储引擎 + - 2)分区函数中可以使用的函数和表达式也有一些限制 + - 3)某些存储引擎不支持分区 + - 4)对应MyISAM表,使用分区表时需要打开更多的文件描述符。有可能出现茶瓯go文件描述符限制的问题。 +## 查询优化 +- 对于访问分区表来说,很重要的一点是要在WHERE条件中加入分区列,有时候即使看似多余的也要带上,这样就可以让优化器能够过滤无须访问的分区。 +- 使用EXPLAIN PARTITION可以观察优化器是否执行而来分区过滤。 +- + +- 分库(针对库) +- 简介 +- 一个库里表太多了,导致了海量数据,系统性能下降,把原本存储于一个库的表拆分存储到多个库上,通常是将表按照功能模块、关系密切程度划分出来,部署到不同库上。 +- 将一个数据库里的表拆分到多个数据库(主机)中,形成数据库集群 +- 比如分为一个静态信息库(基本没有写入)和一个业务相关的库(频繁写入) + +- 为什么要分库 +- 数据库集群环境后都是多台slave,基本满足了读取操作; +- 但是写入或者说大数据、频繁的写入操作对master性能影响就比较大, +- 这个时候,单库并不能解决大规模并发写入的问题。 +- +- 优点 +- 减少增量数据写入时的锁对查询的影响。 +- 由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘IO,时延变短。 +- 但是它无法解决单表数据量太大的问题。 + + +- + +- 分表(针对表) +- 简介 +- 水平拆分(行) +- 类似于Range分区 +- 一张表有很多数据时,将数据分到多张表中 + +- MySQL单表的容量不超过500W(300W就需要拆分),否则建议水平拆分 +- 垂直拆分(列) +- 比如有些表会有大量的属性 +- 将一些相关的属性拆分到一张单独的表 + +- 垂直分表, +- 通常是按照业务功能的使用频次,把主要的、热门的字段放在一起做为主要表; +- 然后把不常用的,按照各自的业务属性进行聚集,拆分到不同的次要表中;主要表和次要表的关系一般都是一对一的。 +- 冷数据放到主要表中,热数据放到次要表中 +- 使用 + +- 切分策略:需要DBA参与研究 +- 导航路由:查询时,怎么导航到哪一张表 + +- 开源方案 + +- MySQL Fabric(官方) + +- 网址:http://www.MySQL.com/products/enterprise/fabric.html +- +- MySQL Fabric 是一个用于管理 MySQL 服务器群的可扩展框架。该框架实现了两个特性 — 高可用性 (HA) 以及使用数据分片的横向扩展 +- 官方推荐,但是2014年左右才推出,是真正的分表,不是代理的(不同于MySQL-proxy)。 +- 未来很有前景,目前属于测试阶段还没大规模运用于生产,期待它的升级。 + +- Atlas(奇虎360) +- Atlas是由 Qihoo 360, Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。 +- 它在MySQL官方推出的MySQL-Proxy 0.8.2版本的基础上,修改了大量bug,添加了很多功能特性。目前该项目在360公司内部得到了广泛应用,很多MySQL业务已经接入了Atlas平台,每天承载的读写请求数达几十亿条。 +- +- 主要功能: +- * 读写分离 +- * 从库负载均衡 +- * IP过滤 +- * SQL语句黑白名单 +- * 自动分表,只支持单库多表,不支持分布式分表,同理,该功能我们可以用分库来代替,多库多表搞不定。 +- +- 网址: https://github.com/Qihoo360/Atlas + +- TDDL(阿里) +- 江湖外号:头都大了 +- 淘宝根据自己的业务特点开发了TDDL(Taobao Distributed Data Layer )框架,主要解决了分库分表对应用的透明化以及异构数据库之间的数据复制,它是一个基于集中式配置的 jdbc datasource实现,具有主备,读写分离,动态数据库配置等功能。 + +- TDDL所处的位置(tddl通用数据访问层,部署在客户端的jar包,用于将用户的SQL路由到指定的数据库中): +- 淘宝很早就对数据进行过分库的处理, 上层系统连接多个数据库,中间有一个叫做DBRoute的路由来对数据进行统一访问。DBRoute对数据进行多库的操作、数据的整合,让上层系统像操作 一个数据库一样操作多个库。但是随着数据量的增长,对于库表的分法有了更高的要求,例如,你的商品数据到了百亿级别的时候,任何一个库都无法存放了,于是 分成2个、4个、8个、16个、32个……直到1024个、2048个。好,分成这么多,数据能够存放了,那怎么查询它?这时候,数据查询的中间件就要能够承担这个重任了,它对上层来说,必须像查询一个数据库一样来查询数据,还要像查询一个数据库一样快(每条查询在几毫秒内完成),TDDL就承担了这样一 个工作。在外面有些系统也用DAL(数据访问层) 这个概念来命名这个中间件。 + + +- 系出名门,淘宝诞生。功能强大,阿里开源(部分) +- 主要优点: +- 1.数据库主备和动态切换 +- 2.带权重的读写分离 +- 3.单线程读重试 +- 4.集中式数据源信息管理和动态变更 +- 5.剥离的稳定jboss数据源 +- 6.支持MySQL和oracle数据库 +- 7.基于jdbc规范,很容易扩展支持实现jdbc规范的数据源 +- 8.无server,client-jar形式存在,应用直连数据库 +- 9.读写次数,并发度流程控制,动态变更 +- 10.可分析的日志打印,日志流控,动态变更 +- TDDL必须要依赖diamond配置中心(diamond是淘宝内部使用的一个管理持久配置的系统,目前淘宝内部绝大多数系统的配置,由diamond来进行统一管理,同时diamond也已开源)。 +- TDDL动态数据源使用示例说明:http://rdc.taobao.com/team/jm/archives/1645 +- diamond简介和快速使用:http://jm.taobao.org/tag/diamond%E4%B8%93%E9%A2%98/ +- TDDL源码:https://github.com/alibaba/tb_tddl +- TDDL复杂度相对较高。当前公布的文档较少,只开源动态数据源,分表分库部分还未开源,还需要依赖diamond,不推荐使用。 + + +- MySQL Proxy(官方) +- 官网提供的,小巧精干型的,但是能力有限,对于大数据量的分库分表无能为力,适合中小型的互联网应用,基本上MySQL-proxy - master/slave就可以构成一个简单版的读写分离和负载均衡 + +- + +- 总结 +- 分库分表演变过程 +- 单库多表--->读写分离主从复制--->垂直分库,每个库又可以带着slave--->继续垂直分库,极端情况单库单表--->分区(变相的水平拆分表,只不过是单库的)--->水平分表后再放入多个数据库里,进行分布式部署 + +- 单库多表 +- 读写分离主从复制 +- 垂直分库(每个库又可以带salve) +- 继续垂直分库,理论上可以到极端情况,单库单表 +- 分区(partition是变相的水平拆分,只不过是单库内进行) +- 终于到水平分表,后续放入多个数据库里,进行分布式部署,终极method。 +- 但是理论上OK,实际上中间的各种通信、调度、维护和编码要求,更加高。 + +- 分库分表后的难题 + +- 分布式事务的问题,数据的完整性和一致性问题。 +- 数据操作维度问题:用户、交易、订单各个不同的维度,用户查询维度、产品数据分析维度的不同对比分析角度。 +- 跨库联合查询的问题,可能需要两次查询 +- 跨节点的count、order by、group by以及聚合函数问题,可能需要分别在各个节点上得到结果后在应用程序端进行合并 +- 额外的数据管理负担,如:访问数据表的导航定位 +- 额外的数据运算压力,如:需要在多个节点执行,然后再合并计算 +- 程序编码开发难度提升,没有太好的框架解决,更多依赖业务看如何分,如何合,是个难题。 +- 不到最后一步不要轻易水平分表 + +- + +# 主从复制 +# 8.79 复制概述 +- 复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。 +- MySQL支持两种复制方式:基于行的复制和基于语句的复制。这两种方式都是通过在主库上记录binlog,在备库重放日志的方式来实现异步的数据复制。这意味着,在同一时间点备库上的数据可能与主库存在不一致,并且保证主备之间的延迟。 +- 复制通常不会增加主库的开销,主要是启用binlog带来的开销,但出于备份或及时从崩溃中恢复的目的,这点开销也是必要的。除此之外,每个备库也会对主库增加一些负载(网络IO),尤其当备库请求从主库读取旧的binlog时,可能会造成更高的IO开销。 +- 通过复制可以将读操作指向备库来获得更好的读扩展,但对于写操作,除非设计得当,否则并不适合通过复制来扩展写操作。 +## 复制解决的问题 +### 数据分布 +- 可以在不同地址位置来分布数据备份,例如不同的数据中心,即使在不稳定的网络环境下,远程复制也可以工作。 +### 负载均衡 +- 可以将读操作分布到多个服务器上,实现对读密集型应用的优化,并且实现方便。 +### 备份 +- 对于备份来说,复制是一项很有意义的技术补充,但复制既不是备份,也不能取代备份。 +### 高可用和故障切换 +- 复制能够帮助应用程序避免MySQL单点失败,一个包含复制的设计良好的故障切换系统能够显著地缩短宕机实现。 +## 复制如何工作 +- 1、在主库上把数据更改记录在binlog中(这些记录称为二进制日志事件) +- 2、备库将主库上的日志复制到自己的中继日志中 +- 3、备库读取中继日志中的事件,将其重放到备库数据之上 + +# 8.80 复制原理 +## 基于语句的复制 +- 主库会记录那些造成数据更改的查询,当备库读取并重放这些事件时,实际上只是把主库上执行过的SQL再执行一遍。 + - 优点:1)实现简单2)binlog中的事件更加紧凑 +- 问题: + - 1)同一条SQL在主库和备库上执行的时间可能稍微或很不相同,因此在传输的binlog中,除了SQL,还有一些元数据,比如时间戳 + - 2)一些无法被正确复制的SQL,存储过程、触发器 + - 3)更新必须是串行的,这需要更多的锁 +## 基于行的复制 +- 会将实际数据记录在binlog中。 + - 好处:1)可以正确地复制每一行,一些语句可以被更加有效地复制 + - 2)复制更加高效(但也视情况而定) +## 比较 +- 理论上基于行的复制方式整体上更有效,并且在实际应用中也适用于大多数场景。 +# 8.81 复制拓扑 +- 可以在任意个主库和备库之间建立复制,只有一个限制:每一个备库只能有一个主库。 +- 每个备库必须有一个唯一的服务器ID;一个主库可以有多个备库;如果打开了log_slave_updates选项,一个备库可以把其主库上的数据变化传播到其他备库。 + - 1)一主库多备库 + + - 2)主动-主动模式下的主主复制 / 双主复制 + +- 每一个都被配置成对方的主库和备库。 +- 这种配置最大的问题是如何解决冲突,比如两台服务器同时修改一行记录,或同时往两台服务器上向一个包含AUTO_INCREMENT列的表里插入数据。 +- 使用起来非常麻烦 + - 3)主动-被动模式下的主主复制 + +- 主要区别在于其中的一台服务器是只读的被动服务器。 +- 这种方式使得反复切换主动和被动服务器非常方便,因为服务器的配置是对称的,这使得故障转移和故障恢复很容易。 +- 设置主动-被动的主主复制在某种意义上类似于创建一个热备份,但是可以使用这个备份来提高性能。比如用它来执行读操作、备份、离线维护以及升级。真正的热备份是做不了这些事情的,然而,你不会获得比单台服务器更好的写性能。 + - 4)拥有备库的主主结构 + +- 为每个主库都增加一个备库,增加了冗余,可以将读查询分配到备库上。 + - 5)环形复制:环形结构可以有三个或更多的主库,每个服务器都是它之前的服务器的备库,是在它之后的服务器的主库。 + +- 环形结构没有双主结构的一些优点,比如对称配置和简单的故障转移,并完全依赖于环上的每一个可用结点,这大大增加了整个系统失效的几率。 +- 可以为每个节点增加备库的方式来减少环形复制的风险。 + +# 8.82 复制和容量规划 +## 为什么复制无法扩展写操作 +- 糟糕的服务容量比例的根本原因是不能像分发读操作那样把写操作等同地分发到更多服务器上。 +- 分区是唯一可以扩展写入的方法(分库分表?) + +## 备库什么时候开始延迟 +- 为了预测在将来的某个时间点会发生什么,可以人为地之制造延迟,然后看多久备库能赶上主库。 +## 规划冗余容量 +- 在构建一个大型应用时,有意让服务器不被充分使用,这应该是一种聪明并且划算的方式,尤其是在使用复制的时候,有多余容量的服务器可以更好地处理负载尖峰,也有更多能力处理慢速查询和维护工作(OPTIMIZE TABLE),并且能更好跟上复制。 +# 8.83 复制管理和维护 +## 监控复制 +- 在主库上,可以使用SHOW MASTER STATUS 来查看当前主库的binlog位置和配置,还可以查看主库当前有哪些binlog是在磁盘上的。 +## 测量备库延迟 +- 虽然SHOW SLAVE STATUS输出的Seconds_behind_master列理论上显示了备库的延时,但由于各种各样的原因,并不总是准确的: + - 1)Seconds_behind_master是通过将备库服务器当前时间戳与binlog中事件的时间戳相对比得到的,所以只有在执行事件时才能报告延迟 + - 2)如果备库复制线程没有运行,就会报延迟为NULL + - 3)一些错误(网络不稳定)可能中断复制/停止复制线程,但Seconds_behind_master将显示为0而不是显示错误 + - 4)即使备库线程正在运行,备库有时候无法计算延时 + - 5)一个大事务可能会导致延迟波动。 + +- 可以使用一些其他方法来监控备库延迟,比如heartbeat record,这是一个在主库上每秒更新一次的时间戳。为了计算延时,可以直接用备库当前的时间戳减去心跳记录的值。 +## 确定主备是否一致 +- 主备经常会因为MySQL的bug、网络中断、服务器崩溃导致不一致。 +- 应该经常性地检查主备是否一致。可以使用一些工具去计算表和数据的checksum。 +## 从主库重新同步备库 +- 传统方法是关闭备库,然后重新从主库复制一份数据。但当数据量很大时,如果能够找出并修复不一致的数据,要比从其他服务器上重新复制数据要有效的多。 +- 最简单的办法是使用mysqldump转储受影响的数据并重新导入。如果数据没有发生变化,这种方法会很好,可以在主库上将表锁住然后转储,再等待备库赶上主库,然后将数据导入到备库中。缺点是在一个繁忙的服务器上可能行不通,另外在备库上通过非复制的方式改变数据可能不够安全。 +## 改变主库 +- 只需在备库中简单地使用CHANGE MASTER TO命令,并指定合适的值。整个过程最难的是获取新主库上合适的binlog的位置,这样备库才可以从和老主库相同的逻辑位置开始复制。 +# 8.84 复制的问题和解决方案 +## 数据损坏或丢失 +- 意外关闭服务器时可能会遇到的情况: + - 1)主库意外关闭:在崩溃前没有将最后几个binlog事件刷新到磁盘中,备库IO线程因此一直处于读不到尚未写入磁盘的事件的状态中。当主库重新启动时,备库将重连到主库逼格再次尝试去读该事件,但主库会告诉备库没有这个binlog偏移量。 +- 解决方法是指定备库从下一个binlog的开头读日志,但一些日志事件将永久地丢失。可以通过在主库开启sync_binlog来避免事件丢失。 + - 2)备库意外关闭:如果使用的都是InnoDB表,可以在重启后观察MySQL错误日志。InnoDB在恢复过程中会打印出它的恢复点的binlog坐标。可以使用这个值来决定备库指向胡库的偏移量。 +## 使用非事务型表 +- 基于语句的复制通过能够很好地处理非事务表。但是当对非事务型表的更新发生错误时,就可能导致主库和备库的数据不一致。如果使用的是MyIASM表,在关闭MySQL之前需要确保已经运行了STOP SLAVE,否则服务器在关闭时会kill所有正在运行的查询。事务型存储引擎则没有这个问题,如果使用的是事务型表,失败的更新会在主库上回滚并且不会记录到binlog中。 +## 混合事务型和非事务型表 +- 如果使用的是事务型存储引擎,只有在事务提交时才会查询记录到binlog中。因此如果事务回滚,MySQL就不会记录这条查询,也就不会在备库重放。 +- 但是如果混合使用事务型和非事务型表,并且发生了一次回滚,MySQL能够回滚事务型表的更新,但非事务型表就会被永久地更新了。 +- 防止该问题的唯一办法是避免混合使用事务型和非事务型表。如果遇到这个问题,唯一的解决办法是忽略错误,并重新同步相关的表。 +- 基于行的复制不会受这个问题影响。 +## 不确定语句 +- 当使用基于语句的复制模式时,如果通过不确定的方式更改数据可能会导致主备不一致。 +- 基于行的复制则没有上述限制。 +## 主库和备库使用不同的存储引擎 +- 当使用基于语句的复制方式时,如果备库使用了不同的存储引擎,则可能造成一条查询在主库和备库上的执行结果不同。 +## 备库发生数据改变 +- 基于语句的复制方式前提是确保备库上有和主库相同的数据,因此不应该允许对备库数据的任何更改。 +- 唯一的解决办法是重新从主库同步数据。 + +## 不唯一的服务器ID + +## 未定义的服务器ID + +## 对未复制数据的依赖性 + +## 丢失的临时表 + + +## 不复制所有的更新 + +## InnoDB加锁读引起的锁争用 + +## 过大的复制延迟 + +## 来自主库过大的包 + +## 受限制的复制带宽 + + +## 磁盘空间不足 + +## 复制的局限性 + +- + +# 高可用解决方案 +- scale-up 向上扩展/垂直扩展:购买更多性能更强的硬件。容易达到性能瓶颈。 +- scale-out 向外扩展:复制、拆分、数据分片 +- 比如按业务分库;单表分区 +# 8.85 脑裂问题 +- 在心跳失效的时候,就发生了脑裂(split-brain)。 +- ( 一种常见的脑裂情况可以描述如下)比如正常情况下,(集群中的)NodeA 和 NodeB 会通过心跳检测以确认对方存在,在通过心跳检测确认不到对方存在时,就接管对应的(共享) resource 。如果突然间,NodeA 和 NodeB 之间的心跳不存在了(如网络断开),而 NodeA 和 NodeB 事实上却都处于 Active 状态,此时 NodeA 要接管 NodeB 的 resource ,同时 NodeB 要接管 NodeA 的 resource ,这时就是脑裂(split-brain)。 + +- 脑裂(split-brain)会 引起数据的不完整性 ,并且可能会对服务造成严重影响 。 +- 起数据的不完整性主要是指,集群中节点(在脑裂期间)同时访问同一共享资源,而此时并没有锁机制来控制针对该数据访问,那么就存在数据的不完整性的可能。 + +- 对付 HA 系统“裂脑”的对策 ,目前我所了解的大概有以下几条: + - 1)添加冗余的心跳线。例如双“心跳线”。尽量减少“脑裂”发生机会。 + - 2)启用磁盘锁。正在服务一方锁住共享磁盘,“脑裂”发生时,让对方完全“抢不走”共享磁盘资源。但使用锁磁盘也会有一个不小的问题,如果占用共享盘的一方不主动解锁,另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃,就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在 HA 系统中设计了“智能”锁。即正在服务的一方只在发现“心跳线”全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了。 + - 3)设置仲裁机制。例如设置参考 IP(如网关IP)Monitor,当心跳线完全断开时,2 个节点都各自 ping 一下 参考 IP ,不通则表明断点就出在本端,不仅“心跳线”断了、对外提供“服务”的本端网络链路也路断了,即使启动(或继续)应用服务也没有用了,那就主动放弃竞争,让能够 ping 通参考 IP 的一端去起服务。更保险一些,ping 不通参考 IP 的一方干脆就自我重启,以彻底释放有可能还占用着的那些共享资源。 +# 8.86 解决方案 +- LVS+Keepalived+MySQL(有脑裂问题?但似乎很多人推荐这个) +- DRBD+Heartbeat+MySQL(有一台机器空余?Heartbeat切换时间较长?有脑裂问题?)MySQL Proxy(不够成熟与稳定?使用了Lua?是不是用了他做分表则可以不用更改客户端逻辑?) +- MySQL Cluster (社区版不支持INNODB引擎?商用案例不足?) +- MySQL + MHA (如果配上异步复制,似乎是不错的选择,又和问题?) +- MySQL + MMM (似乎反映有很多问题,未实践过,谁能给个说法) +# 8.87 MHA +- MHA(Master High Availability)目前在MySQL高可用方面是一个相对成熟的解决方案,它由日本DeNA公司youshimaton(现就职于Facebook公司)开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在0~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。 + +- 该软件由两部分组成: MHA Manager(管理节点)和MHA Node(数据节点)。MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台slave节点上。MHA Node运行在每台MySQL服务器上,MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master。整个故障转移过程对应用程序完全透明。 + +- 在MHA自动故障切换过程中,MHA试图从宕机的主服务器上保存二进制日志,最大程度的保证数据的不丢失,但这并不总是可行的。例如,如果主服务器硬件故障或无法通过ssh访问,MHA没法保存二进制日志,只进行故障转移而丢失了最新的数据。使用MySQL 5.5的半同步复制,可以大大降低数据丢失的风险。MHA可以与半同步复制结合起来。如果只有一个slave已经收到了最新的二进制日志,MHA可以将最新的二进制日志应用于其他所有的slave服务器上,因此可以保证所有节点的数据一致性。 + +- 目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须最少有三台数据库服务器,一主二从,即一台充当master,一台充当备用master,另外一台充当从库,因为至少需要三台服务器, + + + - (1)从宕机崩溃的master保存二进制日志事件(binlog events); + - (2)识别含有最新更新的slave; + - (3)应用差异的中继日志(relay log)到其他的slave; + - (4)应用从master保存的二进制日志事件(binlog events); + - (5)提升一个slave为新的master; + - (6)使其他的slave连接新的master进行复制; + +# 8.88 MMM + +- MMM(Master-Master replication managerfor Mysql,Mysql主主复制管理器)是一套灵活的脚本程序,基于perl实现,用来对mysql replication进行监控和故障迁移,并能管理mysql Master-Master复制的配置(同一时间只有一个节点是可写的)。 + +- mmm_mond:监控进程,负责所有的监控工作,决定和处理所有节点角色活动。此脚本需要在监管机上运行。 +- mmm_agentd:运行在每个mysql服务器上的代理进程,完成监控的探针工作和执行简单的远端服务设置。此脚本需要在被监管机上运行。 +- mmm_control:一个简单的脚本,提供管理mmm_mond进程的命令。 +- mysql-mmm的监管端会提供多个虚拟IP(VIP),包括一个可写VIP,多个可读VIP,通过监管的管理,这些IP会绑定在可用mysql之上,当某一台mysql宕机时,监管会将VIP迁移至其他mysql。 + +- 在整个监管过程中,需要在mysql中添加相关授权用户,以便让mysql可以支持监理机的维护。授权的用户包括一个mmm_monitor用户和一个mmm_agent用户,如果想使用mmm的备份工具则还要添加一个mmm_tools用户。 + +- 优点:高可用性,扩展性好,出现故障自动切换,对于主主同步,在同一时间只提供一台数据库写操作,保证的数据的一致性。 +- 缺点:Monitor节点是单点,可以结合Keepalived实现高可用。 + +- + +# 压力测试 +- sysbench是一个模块化的、跨平台、多线程基准测试工具,主要用于评估测试各种不同系统参数下的数据库负载情况。关于这个项目的详细介绍请看:http://sysbench.sourceforge.net。 +- 它主要包括以下几种方式的测试: +- 1、cpu性能 +- 2、磁盘io性能 +- 3、调度程序性能 +- 4、内存分配及传输速度 +- 5、POSIX线程性能 +- 6、数据库性能(OLTP基准测试) +- 目前sysbench主要支持 MySQL,pgsql,oracle 这3种数据库。 + +- 一、安装 +- 首先,在 http://sourceforge.net/projects/sysbench 下载源码包。 +- 接下来,按照以下步骤安装: + +- tar zxf sysbench-0.4.8.tar.gz +- cd sysbench-0.4.8 +- ./configure && make && make install +- strip /usr/local/bin/sysbench +- 以上方法适用于 MySQL 安装在标准默认目录下的情况,如果 MySQL 并不是安装在标准目录下的话,那么就需要自己指定 MySQL 的路径了。比如我的 MySQL 喜欢自己安装在 /usr/local/mysql 下,则按照以下方法编译: + +- ./configure --with-mysql-includes=/usr/local/mysql/include --with-mysql-libs=/usr/local/mysql/lib && make && make install +- 当然了,用上面的参数编译的话,就要确保你的 MySQL lib目录下有对应的 so 文件,如果没有,可以自己下载 devel 或者 share 包来安装。 +- 另外,如果想要让 sysbench 支持 pgsql/oracle 的话,就需要在编译的时候加上参数 +- --with-pgsql +- 或者 +- --with-oracle +- 这2个参数默认是关闭的,只有 MySQL 是默认支持的。 + +- 二、开始测试 +- 编译成功之后,就要开始测试各种性能了,测试的方法官网网站上也提到一些,但涉及到 OLTP 测试的部分却不够准确。在这里我大致提一下: +- 1、cpu性能测试 + +- sysbench --test=cpu --cpu-max-prime=20000 run +- cpu测试主要是进行素数的加法运算,在上面的例子中,指定了最大的素数为 20000,自己可以根据机器cpu的性能来适当调整数值。 + +- 2、线程测试 + +- sysbench --test=threads --num-threads=64 --thread-yields=100 --thread-locks=2 run +- 3、磁盘IO性能测试 + +- sysbench --test=fileio --num-threads=16 --file-total-size=3G --file-test-mode=rndrw prepare +- sysbench --test=fileio --num-threads=16 --file-total-size=3G --file-test-mode=rndrw run +- sysbench --test=fileio --num-threads=16 --file-total-size=3G --file-test-mode=rndrw cleanup +- 上述参数指定了最大创建16个线程,创建的文件总大小为3G,文件读写模式为随机读。 + +- 4、内存测试 + +- sysbench --test=memory --memory-block-size=8k --memory-total-size=4G run +- 上述参数指定了本次测试整个过程是在内存中传输 4G 的数据量,每个 block 大小为 8K。 + +- 5、OLTP测试 + +- sysbench --test=oltp --mysql-table-engine=myisam --oltp-table-size=1000000 \ +- --mysql-socket=/tmp/mysql.sock --mysql-user=test --mysql-host=localhost \ +- --mysql-password=test prepare +- 上述参数指定了本次测试的表存储引擎类型为 myisam,这里需要注意的是,官方网站上的参数有一处有误,即 --mysql-table-engine,官方网站上写的是 --mysql-table-type,这个应该是没有及时更新导致的。另外,指定了表最大记录数为 1000000,其他参数就很好理解了,主要是指定登录方式。测试 OLTP 时,可以自己先创建数据库 sbtest,或者自己用参数 --mysql-db 来指定其他数据库。--mysql-table-engine 还可以指定为 innodb 等 MySQL 支持的表存储引擎类型。 +- + +# 容灾备份 +# 8.89 为什么要备份 + - 1)灾难恢复 + - 2)人们改变刑法 + - 3)审计 + - 4)测试 +# 8.90 设计备份方案 +- 建议: + - 1)在生产实践中,对于大数据量来说,物理备份还必需的:逻辑备份太慢并受到资源限制,从逻辑备份中恢复需要很长时间。基于快照的备份是最好的选择。对于较小的数据库,逻辑备份可以很好地胜任。 + - 2)保留多个备份集 + - 3)定期从逻辑备份/物理备份中抽取数据进行恢复测试 + - 4)保存binlog以用于基于故障时间点的回复 + - 5)完全不借助备份工具本身来监控本分和备份的过程,需要另外验证备份是否正常 + - 6)通过演练整个恢复过程来测试备份和恢复,测算恢复所需要的资源 +## 在线备份还是离线备份 +- 如果可能,关闭MySQL做备份是最简单安全的,也是所有获取一致性副本中最好的,而且损坏或不一致的风险最小。 +- 由于一致性的需要,对服务器进行在线备份仍然会有明显的服务中断。 +- 最大的权衡是备份时间和备份负载,可以牺牲其一以增强另外一个。 + +## 逻辑备份还是物理备份 +- 有两种主要的方法来备份MySQL数据:逻辑备份(导出)和直接复制原始文件的物理备份。 +- 逻辑备份有如下优点: + - 1)是可以用编辑器或grep、sed查看和操作的普通文件,当需要回复数据或者只查看数据时都非常有帮助 + - 2)恢复非常简单 + - 3)可以通过网络来备份和恢复 + - 4)可以在不能访问底层文件系统的系统中使用 + - 5)非常灵活,因为mysqldump可以接受很多选项。 + - 6)与存储引擎无关。 + - 7)有助于避免数据损坏。 +- 缺点: + - 1)必须由数据库服务器完成生成逻辑备份的工作,要是要更多的CPU周期 + - 2)在某些场景下比数据库文件本身更大 + - 3)无法保证导出后再还原出来的一定是同样的数据 + - 4)效率较低 +- 最大的缺点是从MySQL中导出数据和通过SQL语句将其加载回去的开销。如果使用逻辑备份,测试恢复需要的时间将非常重要。 + +- 物理备份有如下好处: + - 1)只需要将需要的文件复制到其他地方即可完成备份,不需要额外的工作 + - 2)恢复更加简单,并且更快 + +- 缺点: + - 1)原始文件通常比相应的逻辑备份要大得多 + - 2)物理备份不总是可以跨平台、操作系统和MySQL版本。 + + +- 简单地说,逻辑备份不易出错,物理备份更加简单高效 + +## 备份什么 +- 非显著数据,比如binlog和redo log +- 代码 +- 复制配置 +- 服务器配置 +- 选定的操作系统文件 +## 存储引擎和一致性 +- 数据一致性:InnoDB的MVCC可以帮到我们。开始一个事务,转储一组相关的表,然后提交事务。只要在服务器上使用RR事务隔离级别,并且没有任何DDL,就一定会有完美的一致性,以及基于时间点的数据快照,且在备份过程中不会阻塞任何后续的工作。也可以用mysqldump来获得InnoDB表的一致性逻辑备份。可能会导致一个非常长的事务,在某些负载下会导致开销大到不可接受。 + +- 文件一致性:InnoDB如果检测到数据不一致或损坏,会记录错误日志乃至让服务器崩溃。 + +# 8.91 管理和备份binlog +- 服务器的binlog是备份的最重要因素之一,它们对于基于时间点的恢复是必需的,并且通常比数据要小,所以更容易进行频繁的备份。如果有某个时间点的数据备份和所有从那时以后的binlog,就可以重放自从上次全备以来的binlog并前滚所有的变更。 +- +# 8.92 备份数据 +## 生成逻辑备份 +### SQL导出 +- SQL导出是很多人所熟悉的,因为它们是mysqldump默认的方式。 +- 缺点: + - 1)Schema和数据存储在一起 + - 2)巨大的SQL语句 + - 3)单个巨大的文件 +### 符号分隔文件备份 +- 可以使用SQL命令SELECT INTO OUTFILE以符号分隔文件格式创建数据的逻辑备份(mysqldumo的—tab选项)。符号分隔文件包含以ASCII展示的原始数据,没有SQL、注释和列名。 +- 比起SQL导出文件,符号分隔文件要更紧凑且更易于用命令行工具操作,最大的优点是备份和还原速度更快。 +- LOAD DATA INFILE方法可以加载数据到表中。 +## 文件系统快照 +- 文件系统快照是一种非常好的在线备份方法,支持快照的文件系统能够瞬间创建用来备份的内容一致的进行。支持快照的文件系统和设备包括FreeBSD的文件系统、ZFS文件系统、LVM、以及许多的SAN系统和文件存储解决方案。 +### LVM快照 + +- +# 8.93 从备份中恢复 + +- + +# SQL +- 分页 +- 带日期 +- 要加上nextkey锁,语句该怎么写 +- 各种join +- like%..%为什么会扫描全表?遵循什么原则? +- sql语句各种条件的执行顺序,如select, where, order by, group by +# 8.94 执行顺序 +- for human: + +- for machine:SQL解析器执行顺序 + + +- from on join where group by having select distinct order by limit +# 8.95 连接 + +- 7种: +- 内连接 +- 左外连接 +- 右外连接 +- 全外连接(内连接+左外连接+右外连接) +- 左外连接 – 内连接 +- 右外连接 – 内连接 +- 全外连接 – 内连接 + +- 实际的SQL: + +- emp:8 +- dept:5 +## 笛卡尔积/交叉连接 + +- 40 +## 内连接 + +- 7 +## 左外连接 + +- 8 +## 右外连接 + +- + + +- 8 +## 全外连接 + +- MySQL不支持,一种替代做法是: + +## 左外连接 – 内连接 + +- 1 +## 右外连接 – 内连接 + +- 1 +## 全外连接 – 内连接 + +- 2 +- + +# MySQL底层实现 +- 查询处理与查询优化过程 +- MySQL的查询流程大致是: +- MySQL客户端通过协议与MySQL服务器建立连接,发送查询语句,先检查查询缓存,如果命中,直接返回结果,否则进行语句解析 +- +- 有一系列预处理,比如检查语句是否写正确了,然后是查询优化(比如是否使用索引扫描,如果是一个不可能的条件,则提前终止),生成查询计划,然后查询引擎启动,开始执行查询,从底层存储引擎调用API获取数据,最后返回给客户端。怎么存数据、怎么取数据,都与存储引擎有关。 +- MySQL客户端-->MySQL服务器-->缓存-->查询检查-->查询优化-->执行查询 + +# 8.96 查询执行的基础 +- 1、客户端发送一条查询给服务器 +- 2、服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段 +- 3、服务器进行SQL解析、预处理,再由优化器生成对应的执行计划 +- 4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询 +- 5、将结果返回给客户端 + +## MySQL C/S通信协议 +- MySQL客户端和服务器之间的通信协议是半双工的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。所以,我们无法也无需将一个消息切成小块独立来发送。 +- 这种协议让MySQL通信简单快速,但是也从很多地方限制了MySQL,一个明显的限制是,这意味着没法进行流量控制。一旦一端开始发送消息,另一端要接收完整个消息才能响应它。 +- 客户端用一个单独的数据包将查询传给服务器,这也是为什么当查询的语句很长的时候,参宿max_allowed_packet就特别重要的。一旦客户端发送了请求,它能做的事情就只是等待结果了。 +- 相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单地只取前面几条结果,然后让服务器停止发送数据。这种情况下,客户端若接受完整的记过,然后取前面几条需要的记过,或者接收完几条结果后就粗暴地断开连接,都不是好主意,这也是在必要的时候一定要在查询中加上LIMIT限制的原因。 +- 多数连接MySQL的库函数都可以获得全部结果集并缓存到内存里,还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接收全部结果并缓存通过可以减少服务器的压力,让查询能够早点结束,早点释放相应的资源。 + +## 查询缓存 +- 在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节不同,那也不会匹配查询结果,这种情况下查询就会进入下一阶段的处理。 +- 如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情况下,查询不会被解析,不用生成执行计划,不会被执行。 +## 查询优化处理 +### 语法解析器和预处理 +- 首先,MySQL通过对关键字将SQL语句进行解析,并生成一棵对应的解析树。MySQL解析器将使用MySQL语法规则验证和解析查询。 +- 预处理器则根据一些MySQL规则进一步检查解析树是否合法,下一步预处理器会验证权限。 +### 查询优化器 +- 现在语法数被认为是合法的了,将由优化器将其转化为执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。 +- MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。 +- 成本的最小单位是随机读取一个4K数据页的成本。 +- 可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本。 + +- 成本是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索引和数据行的长度、索引分布情况。优化器在评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘IO。 + +- 与很多种原因会导致MySQL优化器选择错误的执行计划: + - 1)统计信息不准确 + - 2)执行计划中的成本估算不等于实际执行的成本 + - 3)MySQL并不按照执行时间最短来选择的,而是基于成本 + - 4)不考虑其他并发执行的查询 + - 5)并不是任何时候都是基于成本的优化,有时也会基于一些固定的规则 + - 6)不会考虑不受其控制的操作的成本(存储过程、用户自定义函数的成本) + +- MySQL的查询优化器是使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。 +- 静态优化可以直接对解析树进行分析,并完成优化。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发送变化,可以认为这是一种编译时优化。 +- 相反,动态优化则和查询的上下文有关,需要在每此查询的时候都重新评估,可以认为这是运行时优化。 + +- MySQL能够处理的优化类型: + - 1)重新定义关联表的顺序 + - 2)将外连接转为内连接 + - 3)使用等价变换规则 + - 4)优化count、min、max + - 5)预估并转化为常数表达式 + - 6)覆盖索引扫描 + - 7)子查询优化 + - 8)提前终止查询(比如limit) + - 9)等值传播 +- 10)列表IN()的比较:MySQL会将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(logn)复杂度的操作,等价地转为OR查询的复杂度为O(n)。 + + +### 数据和索引的统计信息 +- 在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息。 +- 因为服务器层没有任何统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。 +### MySQL如何执行关联查询 +- MySQL认为任何一个查询都是一次关联,并不仅仅是一个查询需要用到两个表匹配才叫关联。 +- 当前MySQL关联执行的策略很简单,MySQL对任何关联都执行嵌套循环关联操作。 +- 不过不是所有的查询都可以转换成上面的形式,比如全外连接,这大概也是MySQL并不支持全外连接的原因。 +### 执行计划 +- 和很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询。MySQL生成查询的一棵指令树,然后通过存储引擎执行完成这颗指令树并返回结果。 +- MySQL总是对一张表开始嵌套循环、回溯完成所有表关联。 +- 比如四表关联: + +### 关联查询优化器 +- MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通过多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。关联查询优化器通过评估不同顺序时的成本来选择一个代价最小的关联顺序。 +- 比如嵌套循环关联时将小表(或者说读取的数据页较小的表)放在最外层。 +### 排序优化 +- 当不能通过索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,MySQL将这个过程统一称为filesort文件排序。 +- 如果需要排序的数据量小于排序缓冲区,MySQL使用内存进行快速排序操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用快速排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并,最后返回排序结果。 +- MySQL有两种排序算法: + - 1)两次传输排序:读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取锁需要的数据行。缺点是会产生大量随机IO,数据传输成本高。 + - 2)单次传输排序:先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。缺点是会占用大量的空间。 +- 当查询需要所有列的总长度不超过max_length_for_sort_data时,MySQL使用单次传输排序。 + +- 在关联查询时如果需要排序,MySQL会分两种情况来处理这样的文件排序。如果ORDER BY子句中的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就会进行文件排序。如果是这样,那么EXPLAIN时会显示Extra字段有“Using filesort”。除此之外的其他情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。此时EXPLAIN会显示Extra字段有“Using temporary;Using filesort”。如果有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。 +## 查询执行引擎 +- MySQL的查询执行引擎会根据执行计划来完成整个查询,执行计划是一个数据结构。 +- 相对于查询优化阶段,查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成,这些接口就是我们成为“handler API”的皆苦,实际上,MySQL在优化阶段就为每个表都创建了一个handler实例,优化器根据这些实例的接口可以获取表的相关信息。 +- 存储引擎接口有着非常丰富的功能,但底层接口却只有几十个,这些接口像搭积木一样能够完成查询的大部分操作。 +## 返回结果给客户端 +- 查询执行的最后一个阶段是将结果返回给客户端,即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数, +- 如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放在查询缓存中。 +- MySQL将结果集返回客户端是一个增量、逐步返回的过程。这样处理有两个好处:服务器无需存储太多的结果,也不会因为要返回太多结果而消耗太多内存。另外这样的处理也让MySQL客户端第一时间获得返回的结果。 +- 结果集中的每一行都会以一个满足MySQL C/S通信协议的封包发送,再通过TCP协议进行传输,在传输过程中,可能对MySQL的封包进行缓存然后批量传输。 + +# 8.97 MySQL查询优化器的局限性 +## 关联子查询 +- MySQL的子查询实现的非常糟糕,最糟糕的一类查询是WHERE中包含IN()的子查询。 + +- 并不是所有关联子查询性能都很差。建议通过一些测试来判断使用哪种写法速度会更快。 +## UNION的限制 +- 有时候MySQL无法将限制条件从外层下推到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。 + +## 索引合并优化 +- 当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。 + +## 并行执行 +- MySQL无法利用多核特性来并行执行查询。 + +## 松散索引扫描 +- MySQL不支持松散索引扫描,也就无法不连续的方式扫描一个索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中很少数的几个,MySQL仍需要扫描这段索引中的每一个条目。 +## 最大值和最小值优化 +- +## 在同一个表上查询和更新 + + + +- + +# 存储实现 +- 每个数据库对应一个子目录,每张表对应子目录下的一个与表同名的.frm文件,它保存了表的定义。 +- 如果是MyIASM引擎,那么表数据存放在.myd文件,表索引存放在.myi文件。 +- 如果是InnoDB引擎,那么表数据和索引文件都放在.ibd文件。 +# InnoDB 简介 +- 从MySQL5.5.8开始,InnoDB存储引擎是默认的存储引擎。InnoDB存储引擎将数据放在一个逻辑的表空间中,这个表空间就像黑盒一样由InnoDB存储引擎自身进行管理。 +- InnoDB通过MVCC来获得高并发性,并且实现了SQL标准的四种隔离级别,默认为可重复读。同时,使用一种被称为next-key lock的策略来避免幻读。除此之外,InnoDB存储引擎还提供了插入缓冲、两次写、自适应哈希索引、预读等高性能和高可用的概念。 +- 对于表中数据的存储,InnoDB存储引擎采用了聚集的方式,因此每张表的存储都是按主键的顺序进行存放。如果没有显式地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此作为主键。 +- + +# InnoDB 体系结构 + +- innoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责: + - 1)维护所有线程需要访问的多个内部数据结构 + - 2)缓存磁盘上的数据,方便快速读取,同时在对磁盘文件的数据修改之前在这里缓存 + - 3)redo log缓冲。 + +- 后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。 +# 8.98 组件 +## 后台线程 +- 1、Master Thread:负责将缓冲池只能怪的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收、 +- 2、IO Thread +- InnoDB大量使用AIO来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作是负责这些IO 请求的回调处理。 +- 3、Purge(清除) Thread +- 事务被提交后,其所使用的undo log可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。 +## 内存 +- 1、缓冲池:InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。缓冲池简单来说就是一块内存取与,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。 +- 在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓存池中,这个过程称为将页“FIX”在缓冲池中,下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。 +- 对于磁盘中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。回写是通过一种称为Checkpoint的机制实现的。 + +- 在InnoDB存储引擎中,缓冲池中页的大小默认为16KB,使用LRU算法对缓冲池进行管理,并且有一定优化,新读取到的页,不是进入首部,而是放入到LRU列表的midpoint位置。 + +- 2、redo log缓冲 +- InnoDB存储引擎首先将redo log信息先放入到这个缓冲区,然后按一定频率将其刷新到redo log文件。默认为8MB大小。 +- 在下列三种情况下会将redo log buffer中的内存刷新到redo log文件中: + - 1)Master Thread每一秒将刷新一次 + - 2)每个事务提交时刷新 + - 3)redo log buffer剩余空间小于一半时,刷新 + +## Checkpoint + +# 8.99 事务日志 redo log(保证事务持久性 物理日志) +- 事务日志即redo log。 +- InnoDB使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无须在每个事务提交时把缓冲区中的脏块刷新到磁盘中。事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机IO。 +- InnoDB用日志把随机IO变成顺序IO,一旦日志安全写到磁盘,事务就持久化了,即使变更还没写到数据文件。如果一些糟糕的事情发生,InnoDB可以重放日志并且恢复已经提交的事务。 +- 当然,InnoDB最后还是要把变更写到数据文件,因为日志有固定大小。InnoDB的日志是环形方式写的,当写到此值的尾部,会重新跳转到开头继续写,但不会覆盖还没应用到数据文件的日志记录。 +- InnoDB使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。实际上,事务日志把数据文件的随机IO转换成几乎顺序的日志文件和数据文件IO,把刷新操作转移到后台使查询可以更快完成,并且缓和查询高峰时IO系统的压力。 +- InnoDB变更任何数据,会写一条变更记录到内存日志缓冲区。在缓冲满的时候、事务提交的时候、或者每一秒钟,InnoDB都会刷新缓冲区的内容到磁盘日志文件——无论上述三个条件哪个先达到。 +- 把日志缓冲写到日志文件和把日志刷新到持久化存储是有区别的,在大部分操作系统中,把缓冲写到日志只是简单地把数据从InnoDB的内存缓冲区转移到了操作系统的缓冲区,并没有真正的持久化。 + + +- 当数据库对数据做修改的时候,需要把数据页从磁盘读到buffer pool中,然后在buffer pool中进行修改,那么这个时候buffer pool中的数据页就与磁盘上的数据页内容不一致,称buffer pool的数据页为dirty page 脏数据,如果这个时候发生非正常的DB服务重启,那么这些数据还在内存,并没有同步到磁盘文件中(注意,同步到磁盘文件是个随机IO,较慢),也就是会发生数据丢失,如果这个时候,能够在有一个文件,当buffer pool 中的data page变更结束后,把相应修改记录记录到这个文件(注意,记录日志是顺序IO),那么当DB服务发生crash的情况,恢复DB的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。 +- Innodb将所有对页面的修改操作写入一个专门的文件(顺序IO,很快)。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。 + + +# 8.100 逻辑存储结构 +- InnoDB把数据保存在表空间内,本质上由一个或多个磁盘文件组成的虚拟文件系统。InnoDB的表空间不只是存储表和索引,它还保存了undo log、插入缓冲、双写缓冲,以及其他内部数据结构。 +- 表空间又由段、区、页组成。页有时也称为块。 +## 表空间 +- 表空间可以看做InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。在默认情况下InnoDB存储引擎中有一个共享表空间ibdata1,即所有数据都存放在这个表空间里。如果用户启用了参数innodb_file_per_table,则每张表内的数据可以单独放在一个表空间中,存放的只是数据、索引和插入缓冲bitmap页,其他数据,如回滚信息、插入缓冲索引页、系统事务信息、二次写缓冲等还是存放在原来的共享表空间中。 + +### 独立表空间 | 共享表空间 +- 独立表空间:每个表都会生成以独立的文件方式来存储,每个表都一个.frm的描述文件,还有一个.ibd文件。其中这个文件包括了单独一个表的数据及索引内容,默认情况下它的存储在mysql指定的目录下。 + +- 独立表空间优缺点: +- 优点: +- 每个表都有自己独立的表空间;每个表的数据和索引都会存储在各个独立的表空间中;可以实现 单表 在不同的数据进行迁移;表空间可以回收(除了drop table操作,表空不能自己回收);drop table 操作自动回收表空间,如果对统计分析或是日值表,删除大量数据后可以通过 :alter table tablename engin=innodb进行回缩不用的空间;对于使用inodb-plugin的innodb使用truncate table会使用空间收缩;对于使用独立表空间,不管怎么删除 ,表空间的碎片都不会太严重。 + +- 缺点: +- 单表增加过大,如超过100G。对于单表增长过大的问题,如果使用共享表空间可以把文件分开,但有同样有一个问题,如果访问的范围过大同样会访问多个文件,一样会比较慢。对于独立表空间也有一个解决办法是:使用分区表,也可以把那个大的表空间移动到别的空间上然后做一个连接。其实从性能上出发,当一个表超过100个G有可能响应也是较慢了,对于独立表空间还容易发现问题早做处理。 + +- 共享表空间:某一个数据库所有的表数据、索引保存在一个单独的表空间中,而这个表空间可以由很多个文件组成,一张表可以跨多个文件存在。默认这个共享表空间的文件路径在data目录下,默认的文件名为 bata1,初始化为10M。 + +- 共享表空间优缺点 + +- 优点:可以将表空间分成多个文件存放在各个磁盘上(表空间文件大小不受表大小的限制,如一个表可以分布在不同的文件上),数据和文件放在一起方便管理。 + +- 缺点:所有的数据和索引存放到一个文件中,将来会是一个很大的文件,虽然可以把一个大文件分成多个小文件,但是多个表及索引在表空间中混合存储,这样对一个表做了大量删除操作后表空间将有大量的空隙,特别是对统计分析、日志系统这类应用最不适合用共享表空间。 +- 共享表空间分配后不能回缩:当出现临时建索引或是创建一个临时表的操作表空间扩大后,就是删除相关的表也没办法回缩那部分空间了 + +- 如何开启独立表空间? + +- 查看是否开启独立表空间: + +- mysql> show variables like '%per_table'; +- +-----------------------+-------+ +- | Variable_name | Value | +- +-----------------------+-------+ +- | innodb_file_per_table | OFF | +- +-----------------------+-------+ + +- 设置开启: +- 在my.cnf文件中[mysqld] 节点下添加innodb_file_per_table=1 +## 段 +- 表空间是由各个段组成的,比如数据段、索引段、回滚段等。 +- 数据段即为B+树的叶子节点,索引段即为B+树的非索引节点。 +## 区 +- 区是由连续页组成的空间,每个区大小为1MB。默认情况下一页大小为16KB,一个区中有64个连续的页。 + +## 页 +- 在InnoDB存储引擎中,默认每个页的大小为16KB,也可以进行重新设置。 +- 在InnoDB存储引擎中,常见的页类型有: + - 1)数据页 + - 2)undo页 + - 3)系统页 + - 4)事务数据页 + - 5)插入缓冲bitmap页 + - 6)插入缓冲空闲列表页 +- 等等 +## 行 +- 每个页允许存放16KB/2 – 200 行的记录,即7992行记录。 +# 8.101 InnoDB 特性 +## 两次写 Double Write +- InnoDB使用双写缓冲来避免页没写完整所导致的数据损坏。当一个磁盘写操作不能完整地完成时,不完整的页写入就可能发生。 +- 双写缓冲是表空间的一个特殊的保留区域,在一些连续的块中足够保存100个页。本质上是一个最近写回的页面的备份拷贝。当InnoDB从缓冲池刷新页面到磁盘时,首先把他们刷新到双写缓冲中,然后再把它们写到其所属的数据区域中,这样可以保证每个页面的写入都是原子并且持久的。 +- 如果有一个不完整的页写到了双写缓冲,原始的页依然会在磁盘上它的真实位置。当InnoDB恢复时,它将用原始页面替换掉双写缓冲中的损坏页面。然而,如果双写缓冲成功写入,但写到页的真实位置失败了,InnoDB在恢复时将使用双写缓冲中的拷贝来替换。InnoDB知道什么时候页面损坏了,因为每个页面在尾部都有校验值。校验值是最后写到页面的冬休,所以如果页面的内容跟校验值不匹配,说明这个页面是损坏的。因此,在恢复的时候,InnoDB只需要读取双写缓冲中每个页面并且验证校验值,如果一个页面的校验值不对,就从它的原始位置读取这个页面。 + +## 插入缓冲 Insert Buffer +## 自适应哈希索引 +## 异步IO +## 刷新邻接页 + + +- + +# InnoDB 数据组织方式与索引分类 +- InnoDB存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过主键索引,就可以获取记录所有的列。 +- 当在表上定义PRIMARY KEY时,InnoDB将它用作聚簇索引。尽量为每个表定义一个主键。如果没有逻辑的唯一且非空的列或一组列,则添加一个新的AUTO_INCREMENT列,其值将自动填入。 + +- 如果没有为表定义一个PRIMARY KEY,那么MySQL将定位第一个UNIQUE索引,其中所有的键列都是非NULL,InnoDB将它用作聚簇索引。 + +- 如果该表没有PRIMARY KEY或合适的UNIQUE索引,则InnoDB会在包含行ID值的合成列内部生成一个名为GEN_CLUST_INDEX的隐藏聚簇索引。这些行按照InnoDB分配给这个表中的行的ID进行排序。行ID是一个6字节的字段,随着新行的插入而单调递增。因此,由行ID排序的行在物理上处于插入顺序。 + +- 因此,每张表都会有一个聚簇索引。聚簇索引是一级索引。 +- 聚簇索引以外的所有索引都称为二级索引。在InnoDB中,二级索引中的每条记录(叶子)都包含该行的主键列,以及为二级索引指定的列。 InnoDB使用这个主键值来搜索聚簇索引中的行。 + +- 聚簇索引一般是主键;没有主键,就是第一个唯一键;没有唯一键,就是隐藏ID。 +- + +# 锁与事务实现原理 +- 概述 +- 所有的存储引擎都以自己的方式实现了锁机制,服务器层完全不了解存储引擎中的锁实现。但服务器层也会使用各种有效的表锁来实现不同的目的。 +- 对于MySQL而言,事务机制更多是靠底层的存储引擎实现的,在服务器层面只有表锁。支持事务的InnoDB存储引擎实现了行锁、gap锁、next-key锁。 +- 分类 +- 按操作类型分 +- 读锁和写锁 +- 按数据操作粒度分 +- 表锁和行锁 + +- MySQL对锁提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是一个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时也会失去对另外一些应用场景的良好支持。好在MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案。 +- + +- MyISAM表锁 +## 特点 +- 偏向MyISAM存储引擎,开销小,加锁快,无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低 +- 在特定的场景中,表锁也可能有良好的性能。比如,READ LOCAL表锁支持某些类型的并发写操作;另外,写锁也比读锁有更高的优先级,因此一个写锁请求可以会被插入到读锁队列的前面。 +- 尽管存储引擎可以管理自己的锁,服务器还是会使用各种有效的表锁来实现不同的目的。比如服务器在ALTER TABLE时使用表锁,而忽略存储引擎的锁机制。 +- MyISAM在读表前自动对表加读锁,在写表前自动对表加写锁。 + +- 案例 +- mylock: + +- 手动增加表锁: +- lock table table1 read/write , table2 read/write ,... +- 显示加过锁的表: +- show open tables; +- 释放表锁: +- unlock tables; + +- In_use为1表示已经被加锁。 +- 加读锁 + +- 左边是用户1,先给mylock加了读锁;右边是用户2,尝试给mylock加写锁,无法获得,处于阻塞状态。 + +- 左边是用户1,给mylock加了读锁,由于读锁是共享锁,所以用户1和用户2都可以查询。 + +session_1 session_2 +获得表mylock的READ锁定 + 连接终端 +当前session可以查询该表记录 + + + 其他session也可以查询该表的记录 + +当前session不能查询其它没有锁定的表 + + 其他session可以查询或者更新未锁定的表 + +当前session中插入或者更新读锁锁定的表都会提示错误: + 其他session插入或者更新锁定表会一直等待获得锁: + +释放锁 + Session2获得锁,插入操作完成: + + +- 用户A给表A加了读锁之后,只能读表A,不能写表A(报错),也不能读写其他表(报错)。 +- 此时用户B可以读表A,可以读写其他表,但是写表A时会出现阻塞(未报错),直至用户A释放表A的锁之后才解除阻塞,执行命令。 + +- 加写锁 + + + +session_1 session_2 +获得表mylock的WRITE锁定 + 连接终端 +当前session对锁定表的查询+更新+插入操作都可以执行: + + 其他session对锁定表的查询被阻塞,需要等待锁被释放: + +释放锁 + Session2获得锁,查询返回: + + + +- 总结 +- MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。 +- MySQL的表级锁有两种模式: +锁类型 可否兼容 读锁 写锁 +读锁 是 是 否 +写锁 是 否 否 + +- 结论: +- 结合上表,所以对MyISAM表进行操作,会有以下情况: +- 1、对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。 +- 2、对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。 +- 简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。 + +- 分析 +- 【看看哪些表被加锁了】 +- MySQL>show open tables; +- 【如何分析表锁定】 +- 可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定: +- SQL:show status like 'table%'; + +- 这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量说明如下: +- Table_locks_immediate:产生表级锁的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1 ; + - Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况; +- 此外,Myisam的读写锁调度是写优先,这也是myisam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。 +- + +- InnoDB行锁 +- 特点 +- 锁粒度小,并发度高;开销大,加锁慢,会出现死锁 +- 支持事务 +- 分析 +- 【如何分析行锁定】 +- 通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况 +- MySQL>show status like 'innodb_row_lock%'; + + +- 对各个状态量的说明如下: +- +- Innodb_row_lock_current_waits:当前正在等待锁定的数量; +- Innodb_row_lock_time:从系统启动到现在锁定总时间长度; +- Innodb_row_lock_time_avg:每次等待所花平均时间; +- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间; +- Innodb_row_lock_waits:系统启动后到现在总共等待的次数; +- 对于这5个状态变量,比较重要的主要是 +- Innodb_row_lock_time_avg(等待平均时长), +- Innodb_row_lock_waits(等待总次数) +- Innodb_row_lock_time(等待总时长)这三项。 +- 尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。 +- 优化建议 + + + +- + +# 8.102 事务隔离级别 +- 锁出现的问题: +- 脏读(读-DB结果不一致):在一个事务中,读取其他事务未提交的数据,其他事务回滚后,导致读到的数据与数据库中的数据不一致; +- 不可重复读(读-读结果不一致):一个事务中多次读取相同记录结果不一致(另一事务对该记录进行增改删); +- 幻读(读-写,用写来验证读,结果不一致):一个事务中读取某个范围内的记录,另一个事务在该范围内插入新的记录,虽然直接查询读取不到,但在插入同PK(同另一个事务插入记录的PK)时会冲突,并且更新范围记录时会同时更新另一个事务新插入的记录。插入同PK和更新范围记录虽然是写,但是在写之前也是要读的,所以也算在读到不同的记录里面了。 + +- 事务隔离级别: +- 读未提交(都不能避免) 事务中的数据即使没有提交,也会对其他事务可见; +- 读已提交(可避免脏读,提交读,可以立即读到其他事务提交的数据):一个事务从开始直接提交之前,所做的任何修改对其他事务都是不可见的; +- 可重复读(可避免脏读、不可重复读,快照读,一致性读):一个事务中多次读取相同的记录,结果是一致的; +- 如果使用select ... for update、lock in share mode才会避免幻读,在第二次读的时候便可读到其他事务更新的数据(相当于破坏了可重复读,但是不会出现幻影)。 +- InnoDB使用MVCC来实现可重复读(也可实现读已提交),但没有解决幻读问题; +- 另外,InnoDB提供了这样的机制:在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据。这个加锁读使用到的机制就是next-key locks。 +- 串行化(都可避免): + + + +## 幻读示例1——插入同PK +Session_1 Session_2 +开启事务(隔离级别为可重复读) 开启事务(隔离级别为可重复读) +范围查询表 插入PK为n的记录,并提交 +再次范围查询表,同上次查询结果一致,没有看到PK为n的记录(体现了可重复读) +插入PK为n的记录,报错:Duplicate key for key ‘PRIMARY’(出现了幻读,因为根据上次查询的结果,本不应该存在PK为n的记录的) +提交 + +## 幻读示例2——范围更新 +Session_1 Session_2 +开启事务(隔离级别为可重复读) 开启事务(隔离级别为可重复读) +范围查询表,共n条记录 在Session_1的范围内插入记录,并提交 +再次范围查询表,同上次查询结果一致,没有看到Session_2插入的新的记录(体现了可重复读) +该范围内记录全部更新,最终更新了n+1条记录,包括Session_2插入的新的记录(出现了幻读,因为根据之前查询的结果只有n条记录的) +提交 + +## 解决幻读示例3——排他锁 +Session_1 Session_2 +开启事务(隔离级别为可重复读) 开启事务(隔离级别为可重复读) +范围查询表,共n条记录,并使用select...for update 在Session_1的范围内插入记录,阻塞。 +再次范围查询表,同上次查询结果一致,没有看到Session_2插入的新的记录(体现了可重复读) +整体更新该范围内的记录,最终更新了n条记录(没有幻读) +提交 解除阻塞 + +- + +# 8.103 事务隔离级别的实现 +## 读未提交 +- 无锁 +## 读已提交 +- MVCC +## 可重复读 +- MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。 +- MVCC最大的作用是: 实现了非阻塞的读操作,写操作也只锁定了必要的行. +## 可序列化 +- 读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论。 + +- + +# 8.104 MVCC +- MVCC 在MySQL 中的实现依赖的是 undo log 与 read view。 +## undo log(保证事务原子性->事务回滚) +- undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘; +- 与redo log不同的是,磁盘上不存在单独的undo log文件。 Undo记录默认记录在系统表空间(ibdata)中,从MySQL 5.6开始,Undo使用的表空间可以分离为独立的Undo log文件。 +- 在Innodb当中,INSERT操作在事务提交前只对当前事务可见,Undo log在事务提交后即会被删除,因为新插入的数据没有历史版本,所以无需维护Undo log。而对于UPDATE、DELETE,则需要维护多版本信息。 +- 在InnoDB当中,UPDATE和DELETE操作产生的Undo log都属于同一类型:update_undo。(update可以视为insert新数据到原位置,delete旧数据,undo log暂时保留旧数据) + +- UNDO内部由多个回滚段组成,即 Rollback segment,一共有128个,保存在ibdata系统表空间中,分别从resg slot0 - resg slot127,每一个resg slot,也就是每一个回滚段,内部由1024个undo segment 组成。 + +- 回滚段(rollback segment)分配如下: +- slot 0 ,预留给系统表空间; +- slot 1- 32,预留给临时表空间,每次数据库重启的时候,都会重建临时表空间; +- slot33-127,如果有独立表空间,则预留给UNDO独立表空间;如果没有,则预留给系统表空间; +- 回滚段中除去32个提供给临时表事务使用,剩下的 128-32=96个回滚段,可执行 96*1024 个并发事务操作,每个事务占用一个 undo segment slot,注意,如果事务中有临时表事务,还会在临时表空间中的 undo segment slot 再占用一个 undo segment slot,即占用2个undo segment slot。如果错误日志中有:Cannot find a free slot for an undo log。则说明并发的事务太多了,需要考虑下是否要分流业务。 +- 回滚段(rollback segment )采用 轮询调度的方式来分配使用,如果设置了独立表空间,那么就不会使用系统表空间回滚段中undo segment,而是使用独立表空间的,同时,如果回顾段正在 Truncate操作,则不分配。 +## rollback segment(为了提高并发度) +- 在Innodb中,undo log被划分为多个段,具体某行的undo log就保存在某个段中,称为回滚段。可以认为undo log和回滚段是同一意思。 +## row +- 最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT。 +- 6字节的DATA_TRX_ID 标记了最新更新这行记录的transaction id,每处理一个事务,其值自动+1 +- 7字节的DATA_ROLL_PTR 指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针 +- 6字节的DB_ROW_ID,当由innodb自动产生聚簇索引时,聚簇索引包括这个DB_ROW_ID的值,否则聚簇索引中不包括这个值.,这个用于索引当中 +- DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候。 + + +- 更新一行的过程: +- begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行 +## read view +- 在innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号(trx_id)与该read view进行比较。 +- 简单来说,Read View记录读开始时,所有的活动事务,这些事务所做的修改对于Read View是不可见的。除此之外,所有其他的小于创建Read View的事务号的所有记录均可见 +- 新建事务(当前事务)与正在commit 的事务不在活跃事务列表中。 +- 函数:read_view_sees_trx_id。 +- read_view中保存了当前全局的事务的范围: +- 【low_limit_id 最迟的事务id, up_limit_id 最早的事务id】 +- 1. 当行记录的事务ID小于当前系统的最早活动id,就是可见的。 +-   if (trx_id < view->up_limit_id) { +-     return(TRUE); +-   } +- 2. 当行记录的事务ID大于等于当前系统的最迟活动id,就是不可见的。 +-   if (trx_id >= view->low_limit_id) { +-     return(FALSE); +-   } +- 3. 当行记录的事务ID在活动范围之中时,判断是否在活动列表中,如果在就不可见,如果不在就是可见的。 +-   for (i = 0; i < n_ids; i++) { +-     trx_id_t view_trx_id + -       = read_view_get_nth_trx_id(view, n_ids - i - 1); +-     if (trx_id <= view_trx_id) { +-     return(trx_id != view_trx_id); +-     } +-   } + +## 不同隔离级别下read_view的生成规则 +### 读已提交 +- 在每次语句执行的过程中,都关闭read_view, 重新在row_search_for_MySQL函数中创建当前的一份read_view。 +- 这样就可以根据当前的全局事务链表创建read_view的事务区间,实现read committed隔离级别。 +### 可重复读 +- 在repeatable read的隔离级别下,创建事务trx结构的时候,就生成了当前的global read view。 +- 使用trx_assign_read_view函数创建,一直维持到事务结束,这样就实现了repeatable read隔离级别。 + +## update + + - 1)事务1更改该行的各字段的值 + +- 当事务1更改该行的值时,会进行如下操作: +- 用排他锁锁定该行 +- 记录redo log +- 把该行修改前的值Copy到undo log,即上图中下面的行 +- 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行 + + - 2)事务2修改该行的值 + +- 与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。 +- 因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。 + +- 当事务正常提交时InnoDB只需要更改事务状态为COMMIT即可,不需做其他额外的工作,而Rollback则稍微复杂点,需要根据当前回滚指针从undo log中找出事务修改前的版本,并恢复。如果事务影响的行非常多,回滚则可能会变的效率不高,根据经验每事务行数在1000~10000之间,Innodb效率还是非常高的。很显然,Innodb是一个COMMIT效率比Rollback高的存储引擎。 +## select +- 查询时会将行的事务id与read_view中的活动事务列表进行匹配。 +- 记录可见,且Deleted bit = 0;当前记录是可见的有效记录。 +- 记录可见,且Deleted bit = 1;当前记录是可见的删除记录。此记录在本事务开始之前,已经删除。 +## InnoDB MVCC 与 理想 MVCC的区别 +- 一般我们认为MVCC有下面几个特点: +- 每行数据都存在一个版本,每次数据更新时都更新该版本 +- 修改时Copy出当前版本随意修改,各个事务之间无干扰 +- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback) +- 就是每行都有版本号,保存时根据版本号决定是否成功,有乐观锁的味道 + +- 而Innodb的实现方式是: +- 事务以排他锁的形式修改原始数据 +- 把修改前的数据存放于undo log,通过回滚指针与主数据关联 +- 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback) +- 二者最本质的区别是,当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC? + +- Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC就无能为力了。 + +- 比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID。 +- +- 理想MVCC难以实现的根本原因在于企图通过乐观锁代替两阶段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而两阶段提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。 + +- + +# 8.105 InnoDB锁分类 +## record lock + - 1)InnoDB里的行锁(record lock)是索引记录的锁 + - 2)record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚簇索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。 + - 3)若多个物理记录对应同一个索引,若同时访问,也会出现锁冲突 + - 4)当表有多个索引时,不同事务可以使用不同的索引锁住不同的行。 +- 如果走的是聚簇索引,那么会锁住聚簇索引; +- 如果走的是二级索引,那么会同时锁住二级索引和聚簇索引 + - 5)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。 +## gap lock +- 锁定一个范围的记录,但不包括记录本身。锁加在未使用的空闲空间上,可能是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间. +- 示例: +- create table test(id int,v1 int,v2 int,primary key(id),key `idx_v1`(`v1`))Engine=InnoDB DEFAULT CHARSET=UTF8; +- 该表的记录如下: +- +----+------+------+ + +- | id | v1 | v2 | + +- +----+------+------+ + +- | 1 | 1 | 0 | + +- | 2 | 3 | 1 | + +- | 3 | 4 | 2 | + +- | 5 | 5 | 3 | + +- | 7 | 7 | 4 | + +- | 10 | 9 | 5 | + +- 间隙锁(Gap Lock)一般是针对非唯一索引而言的,test表中的v1(普通索引,非唯一索引)字段值可以划分的区间为: + - (-∞,1) + - (1,3) + - (3,4) + - (4,5) + - (5,7) + - (7,9) +- (9, +∞) + - 假如要更新v1=7的数据行,那么此时会在索引idx_v1对应的值,也就是v1的值上加间隙锁,锁定的区间是(5,7)和(7,9)。同时找到v1=7的数据行的主键索引和非唯一索引,对key加上锁。 +## next-key lock +- 行锁与间隙锁组合起来用就叫做Next-Key Lock。锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。 +- InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。 +- 例如一个索引有10,11,13,和20这四个值,那么该索引表可能被next-key locking的区间: +- (负无穷,10) + - 【10,11) + - 【11,13) +- 【13,20) +- 【20,正无穷) +- 如果包含唯一索引,那么会对其进行优化,降级为Record Lock。 +- 但是如果唯一索引是复合索引,而查询仅是最左前缀,则仍会使用next-key lock。 +示例: + +## 意向锁 +- innodb的意向锁主要用户多粒度的锁并存的情况。比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,可以在表级上引入新的锁类型来表示其所属行的加锁情况,这就引出了“意向锁”的概念。举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就行了。 +- 说白了意向锁的主要作用是处理行锁和表锁之间的矛盾,能够显示“某个事务正在某一行上持有了锁,或者准备去持有锁” + +## gap lock的危害 +- 【什么是间隙锁】 +- 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”, + +- 【危害】 +- 因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。 +- 间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害 +Session_1 Session_2 + 阻塞产生,暂时不能插入 + +commit; 阻塞解除,完成插入 + + +- + +# 8.106 InnoDB加锁分析 +## 一致性非锁定读(快照读,无锁,读不会阻塞,也不会阻塞其他事务读写) +- 一致性非锁定读基于MVCC,实现了非阻塞读,读不加锁。 + - 1)在read committed隔离级别下: +- 一致性非锁定读总是读取被锁定行的最新一份快照数据. 产生了不可重复读的问题. + - 2)在repeatable read 事务隔离级别下: +- 一致性非锁定读总是读取事务开始时的行数据版本. 解决不可重复读的问题 + +## 一致性锁定读(有锁,读可能阻塞,会阻塞其他事务写) +- 一致性锁定读就是select ... for update 和 select ... in shard mode,读可能会被阻塞,因为是加了锁的。 +- SELECT ... LOCK IN SHARE MODE(其他事务可读,可加共享锁,不可加排他锁,不可写) +- 在扫描到的任何索引记录上加共享的next-key lock,还有聚簇索引加排它锁 +- 保证读到的是最新的数据(参见幻读),并且保证其他事务无法修改正在读的数据,事务完毕后解锁。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据加了共享锁。 + +- SELECT ... FOR UPDATE(其他事务可读,不可加共享锁&排他锁,不可写) +- 在扫描到的任何索引记录上加排它的next-key lock,还有聚簇索引加排它锁 +- 保证读到的是最新的数据(参见幻读),并且保证同一个事务的读-改-写是一个原子性的操作,事务完毕后解锁。 +## 当前读 +- select * from table where ? lock in share mode; +- select * from table where ? for update; +- insert into table values (…); +- update table set ? where ?; +- delete from table where ?; +- 所有以上的语句,都属于当前读,读取记录的最新版本。并且读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。 +- InnoDB 中的加锁,不仅要对where中走的索引加锁,还会对主键聚簇索引加锁。 +- 具体加锁情况要根据事务隔离级别和where中走的索引情况具体分析。 + +- 为什么将 插入/更新/删除 操作,都归为当前读? +- 更新步骤: + +- 一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。 +- 注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。 +## 两段锁协议 + +- 在事务执行过程中,随时都可以锁定,锁只有在执行commit或rollback时才会释放,并且所有的锁都是在同一时刻被释放。 +- 除了DML时隐式加排他锁外,读的时候也可以显式加锁,比如select ... in shard mode 和 select ... for update。 +- MySQL也支持lock tables和unlock tables语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能代替事务处理,如果应用需要用到事务,还是应该选择事务型存储引擎。 +- 并且显式使用lock tables会与事务中使用的锁产生冲突,因此建议不要显式地使用lock tables。 + +## 锁与事务隔离级别中的当前读 +- 前面讲事务隔离级别的实现注重于快照读,这里主要讲的是当前读,也就是一致性非锁定读+DML的锁实现。 + +- 读已提交:当前读时,对读到的记录加行锁 +- 可重复读:当前读时,对读到的记录加行锁,同时对读取的范围加间隙锁。 +- 可序列化:不区分当前读与快照读,所有的读都是当前读,读加读锁,写加写锁。 +- + +## 案例 DML+select...for update +### RC+where走聚簇索引 +- 聚簇索引加行级排他锁 +### RC+where走二级索引(包括唯一索引和非唯一索引) +- 满足条件的记录的二级索引加排他锁,聚簇索引加排他锁 +### RC+where无索引 +- 在聚簇索引上全表加排他锁。为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。 +### RR+where走聚簇索引 +- 聚簇索引加排他锁 +### RR+where走唯一索引 +- 满足条件的记录的唯一索引加排他锁,聚簇索引加排他锁 +### RR+where走非唯一索引 + +- GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。 +- 三个GAP锁:[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f] +- Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻读的情况。 + +- 对于每条满足条件的记录,会先加非唯一索引上的排他锁,加GAP上的GAP锁,然后加聚簇索引上的排他锁。 + +### RR+where无索引 + +- 在聚簇索引上全表加排他锁,同时会锁上聚簇索引上的所有GAP。 +- MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了 innodb_locks_unsafe_for_binlog 参数。 +### Serializable +- MVCC并发控制降级为Lock-Based CC,读也加读锁。 +- + +# 8.107 死锁 +- 死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。 +- +- +# 8.108 只读事务 +- Innodb将所有的事务对象维护在链表上,通过trx_sys来管理,在5.6中,最明显的变化就是事务链表被拆分成了两个链表: +- 一个是只读事务链表:ro_trx_list,其他非标记为只读的事务对象放在链表rw_trx_list上; +- 这种分离,使得读写事务链表足够小,创建readview 的MVCC快照的速度更快; + +- + +# binlog +- binlog是MySQL Server层记录的日志, redo log是InnoDB存储引擎层的日志。 两者都是记录了某些操作的日志(不是所有)自然有些重复(但两者记录的格式不同)。 +- 选择binlog日志作为replication主要原因是MySQL的特点就是支持多存储引擎,为了兼容绝大部分引擎来支持复制这个特性。 +# 8.109 格式 +- binlog有三种格式:Statement、Row以及Mixed。从安全性来看,ROW(最安全)、MIXED(不推荐)、STATEMENT(不推荐)。 + +- –基于SQL语句的复制(statement-based replication,SBR), +- –基于行的复制(row-based replication,RBR), +- –混合模式复制(mixed-based replication,MBR)。 +## Statement +- 每一条会修改数据的sql都会记录在binlog中。在5.6.24中默认格式。 + +- 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。 + +- 缺点:由于记录的只是执行语句,为了这些语句能在slave上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在slave得到和在master端执行时候相同 的结果。另外MySQL 的复制,像一些特定函数功能,slave可与master上要保持一致会有很多相关问题。 + +- ps:相比row能节约多少性能与日志量,这个取决于应用的SQL情况,正常同一条记录修改或者插入row格式所产生的日志量还小于Statement产生的日志量,但是考虑到如果带条件的update操作,以及整表删除,alter表等操作,ROW格式会产生大量日志,因此在考虑是否使用ROW格式日志时应该跟据应用的实际情况,其所产生的日志量会增加多少,以及带来的IO性能问题。 +## Row +- 5.1.5版本的MySQL才开始支持row level的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。 +- 优点: binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。 + +- 缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。 + +- ps:新版本的MySQL中对row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录,如果sql语句确实就是update或者delete等修改数据的语句,那么还是会记录所有行的变更。 +## Mixed +- 从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。 +- 在Mixed模式下,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。 +# 8.110 binlog与redo log的区别 + - 1)首先,binlog会记录所有与MySQL数据库有关的日志记录,包括InnoDB、MyISAM、Heap等其他存储引擎的日志。而InnoDB存储引擎的redo log日志只记录有关该引擎本身的事务日志。 + + - 2)其次,记录的内容不同。无论用户将二进制日志文件记录的格式设为STATEMENT还是ROW,又或是MIXED,其记录的都是关于一个事务的具体操作内容,即该日志是逻辑日志。而InnoDB存储引擎的重做日志是关于每个页(Page)的更改的物理情况。 + + - 3)此外,写入的时间也不同。二进制日志文件仅在事务提交后进行写入,即只写磁盘一次,不论这时该事务多大。而在事务进行的过程中,却不断有redo 条目(redo entry)被写入到重做日志文件中。 + From f5ccd89245f5de6c037eb5bf576aaf19a72d2523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 19:41:03 +0800 Subject: [PATCH 71/97] =?UTF-8?q?Rename=208.Mysql=20to=20=E5=85=AB?= =?UTF-8?q?=E3=80=81Mysql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/8.Mysql => "docs/\345\205\253\343\200\201Mysql" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/8.Mysql => "docs/\345\205\253\343\200\201Mysql" (100%) diff --git a/docs/8.Mysql "b/docs/\345\205\253\343\200\201Mysql" similarity index 100% rename from docs/8.Mysql rename to "docs/\345\205\253\343\200\201Mysql" From 70c0a5dfd23b4b3fb4b118dbb1ccf9a8d23433b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:08:38 +0800 Subject: [PATCH 72/97] =?UTF-8?q?Create=20=E4=B9=9D=E3=80=81=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...3\215\344\275\234\347\263\273\347\273\237" | 480 ++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 "docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" diff --git "a/docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" "b/docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" new file mode 100644 index 00000000..a40aeb1f --- /dev/null +++ "b/docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" @@ -0,0 +1,480 @@ +第 6 章 操作系统 + +第 6.1 节 计算机操作系统 + +概述 +基本特征 +1.并发 +并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。 并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。 +操作系统通过引入进程和线程,使得程序能够并发运行。 +2.共享 +共享是指系统中的资源可以被多个并发进程共同使用。有两种共享方式:互斥共享和同时共享。 +互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。 +3.虚拟 +虚拟技术把一个物理实体转换为多个逻辑实体。 +主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。 +多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片 并快速切换。 + +虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射 到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法, 将该页置换到内存中。 +4.异步 +异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 +基本功能 +1.进程管理 +进程控制、进程同步、进程通信、死锁处理、处理机调度等。 +2.内存管理 +内存分配、地址映射、内存保护与共享、虚拟内存等。 +3.文件管理 +文件存储空间的管理、目录管理、文件读写管理和保护等。 +4.设备管理 +完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。主要包括缓冲管理、设备分配、设备处理、虛拟设备等。 +系统调用 +如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。 + + +Linux 的系统调用主要有以下这些: + +Task Commands +进程控制 fork(); exit(); wait(); +进程通信 pipe(); shmget(); mmap(); +文件操作 open(); read(); write(); +设备操作 ioctl(); read(); write(); +信息维护 getpid(); alarm(); sleep(); +安全 chmod(); umask(); chown(); + +大内核和微内核 +1.大内核 +大内核是将操作系统功能作为一个紧密结合的整体放到内核。由于各模块共享信息,因此有很高的性能。 +2.微内核 +由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则 划分成若干服务,相互独立。 +在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在 用户态。 +因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。 + +中断分类 +1.外中断 + +由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。 +2.异常 +由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 +3.陷入 +在用户程序中使用系统调用。 +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +进程管理 +进程与线程 +1.进程 +进程是资源分配的基本单位。 +进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 +PCB 的操作。 +下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。 + + + + +2.线程 +线程是独立调度的基本单位。 +一个进程中可以有多个线程,它们共享进程资源。 +QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 + + +3.区别 +Ⅰ 拥有资源 +进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 Ⅱ 调度 +线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程 中的线程时,会引起进程切换。 +Ⅲ 系统开销 + +由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 +Ⅳ 通信方面 +线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。 +进程状态的切换 + + +就绪状态(ready):等待被调度运行状态(running) +阻塞状态(waiting):等待资源 +应该注意以下内容: +只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间, 转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 +进程调度算法 +不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。 +1.批处理系统 +批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 +1.1先来先服务 first-come first-serverd(FCFS) 非抢占式的调度算法,按照请求的顺序进行调度。 +有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很 长时间,造成了短作业等待时间过长。 + +1.2短作业优先 shortest job first(SJF) +非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 +长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调 度。 +1.3最短剩余时间优先 shortest remaining time next(SRTN) +最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 +2.交互式系统 +交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。 +2.1时间片轮转 +将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时 继续把 CPU 时间分配给队首的进程。 +时间片轮转算法的效率和时间片的大小有很大关系: +因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进 程切换上就会花过多时间。 +而如果时间片过长,那么实时性就不能得到保证。 + + +2.2优先级调度 +为每个进程分配一个优先级,按优先级进行调度。 +为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 +2.3多级反馈队列 +一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。 +多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 +1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 +每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。 + + + + +3.实时系统 +实时系统要求一个请求在一个确定时间内得到响应。 +分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 +进程同步 +1.临界区 +对临界资源进行访问的那段代码称为临界区。 +为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。 + + +2.同步与互斥 +同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。 互斥:多个进程在同一时刻只有一个进程能进入临界区。 +3.信号量 +信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 +down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 +down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 +如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。 + + + +使用信号量实现生产者-消费者问题 +问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才 可以拿走物品。 +因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。 +为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两 个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用, 当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 +注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。 + + + + +4.管程 +使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出 错,也使得客户端代码调用更容易。 +c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() +方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。 + + +管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其 它进程永远不能使用管程。 +管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 +使用管程实现生产者-消费者问题 + + + + +经典同步问题 +生产者和消费者问题前面已经讨论过了。 +1.读者-写者问题 +允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。 +一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。 + + + + +以下内容由 @Bandi Yugandhar 提供。 +The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer). + + + + +We can observe that every reader is forced to acquire ReadLock. On the otherhand, writers doesn’t need to lock individually. Once the first writer locks the ReadLock, it will be released only when there is no writer left in the queue. +From the both cases we observed that either reader or writer has to starve. Below solutionadds the constraint that no thread shall be allowed to starve; that is, the operation of obtaining a lock on the shared data will always terminate in a bounded amount of time. + + + + +2.哲学家进餐问题 + + +五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家 吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 +下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。 + + + +为了防止死锁的发生,可以设置两个条件: +必须同时拿起左右两根筷子; +只有在两个邻居都没有进餐的情况下才允许进餐。 + + + + +进程通信 +进程同步与进程通信很容易混淆,它们的区别在于: 进程同步:控制多个进程按一定顺序执行; +进程通信:进程间传输信息。 +进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传 输一些进程同步所需要的信息。 +1.管道 +管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。 + + +它具有以下限制: +只支持半双工通信(单向交替传输); 只能在父子进程或者兄弟进程中使用。 + + +2.FIFO +也称为命名管道,去除了管道只能在父子进程中使用的限制。 + + + +FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。 + + + + +3.消息队列 +相比于 FIFO,消息队列具有以下优点: +消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难; 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法; +读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。 +4.信号量 +它是一个计数器,用于为多个进程提供对共享数据对象的访问。 +5.共享存储 +允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。需要使用信号量用来同步对共享存储的访问。 +多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。 +6.套接字 + +与其它通信机制不同的是,它可用于不同机器间的进程通信。 +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +死锁 +必要条件 + + + +互斥:每个资源要么已经分配给了一个进程,要么就是可用的。占有和等待:已经得到了某个资源的进程可以再请求新的资源。 +不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 +环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资 源。 +处理方法 +主要有以下四种方法: 鸵鸟策略 + +死锁检测与死锁恢复死锁预防 +死锁避免 +鸵鸟策略 +把头埋在沙子里,假装根本没发生问题。 +因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。 当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 +大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。 +死锁检测与死锁恢复 +不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 +1.每种类型一个资源的死锁检测 + + +上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源 表示进程请求获取该资源。 +图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。 +每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问 过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 +2.每种类型多个资源的死锁检测 + + + + +上图中,有三个进程四个资源,每个数据代表的含义如下: +E 向量:资源总量 +A 向量:资源剩余量 +C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 +R 矩阵:每个进程请求的资源数量 +进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。 +算法总结如下: +每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。 +1.寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。 +2.如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 +3.如果没有这样一个进程,算法终止。 +3.死锁恢复 +利用抢占恢复利用回滚恢复 +通过杀死进程恢复 +死锁预防 +在程序运行之前预防发生死锁。 +1.破坏互斥条件 +例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 +2.破坏占有和等待条件 +一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 +3.破坏不可抢占条件 +4.破坏环路等待 +给资源统一编号,进程只能按编号顺序来请求资源。 + +死锁避免 +在程序运行时避免发生死锁。 +1.安全状态 + + +图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 +定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个 进程运行完毕,则称该状态是安全的。 +安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类 似,可以结合着做参考对比。 +2.单个资源的银行家算法 +一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全 状态,如果是,就拒绝请求;否则予以分配。 + + +上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 +3.多个资源的银行家算法 + + + + +上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。 +检查一个状态是否安全的算法如下: +查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 +假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。重复以上两步,直到所有进程都标记为终止,则状态时安全的。 +如果一个状态不是安全的,需要拒绝进入这个状态。 +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +内存管理 +虚拟内存 + +虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个 块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内 存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的 指令。 +从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需 要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 + + +分页系统地址映射 +内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间) 和页框(物理内存空间)的映射表。 +一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。 +下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1),页表项最后一位表示是否存在于内存中,1 表示存在。后 12 位存储偏移量。这个页对应的页框的地址为 (110 000000000100)。 + + + +页面置换算法 +在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲 空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 +页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到 达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。 +页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 +1.最佳 +OPT, Optimal replacement algorithm +所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。 是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 +举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列: + + +开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 +7 再次被访问的时间最长。 +2.最近最久未使用 +LRU, Least Recently Used +虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。 +为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 +因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。 + + + + +3.最近未使用 +NRU, Not Recently Used +每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类: +R=0,M=0 R=0,M=1 R=1,M=0 R=1,M=1 +当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。 +NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。 +4.先进先出 +FIFO, First In First Out +选择换出的页面是最先进入的页面。 +该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。 +5.第二次机会算法 +FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改: + +当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。 + + +6.时钟 +Clock +第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向 最老的页面。 + + +分段 +虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。 +下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 + + + +分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 + + +段页式 +程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统 的共享和保护,又拥有分页系统的虚拟内存功能。 +分页与分段的比较 +对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。地址空间的维度:分页是一维地址空间,分段是二维的。 +大小是否可以改变:页的大小不可变,段的大小可以动态改变。 +出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划 分为逻辑上独立的地址空间并且有助于共享和保护。 + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +设备管理 +磁盘结构 +盘面(Platter):一个磁盘有多个盘面; +磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; +扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有512 bytes 与 4 K 两种大小; +磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场 +(写); +制动手臂(Actuator arm):用于在磁道之间移动磁头; 主轴(Spindle):使整个盘面转动。 + + + +磁盘调度算法 +读写一个磁盘块的时间的影响因素有: +旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上) 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上) 实际的数据传输时间 +其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。 +1.先来先服务 +FCFS, First Come First Served +按照磁盘请求的顺序进行调度。 +优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 +2.最短寻道时间优先 +SSTF, Shortest Seek Time First +优先调度与当前磁头所在磁道距离最近的磁道。 +虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁 道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。 + + + +3.电梯算法 +SCAN +电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。 +电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请 求,然后改变方向。 +因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。 + + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + + + +链接 +编译系统 +以下是一个 hello.c 程序: + + +在 Unix 系统上,由编译器把源文件转换为目标文件。 + + +这个过程大致如下: + + +预处理阶段:处理以 # 开头的预处理命令; 编译阶段:翻译成汇编文件; +汇编阶段:将汇编文件翻译成可重定位目标文件; +链接阶段:将可重定位目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。 +静态链接 +静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两 个任务: + +符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一 个符号定义关联起来。 +重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指 向这个内存位置。 + +目标文件 +可执行目标文件:可以直接在内存中执行; +可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件; 共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接; +动态链接 +静态库有以下两个问题: +当静态库更新时那么整个程序都要重新进行链接; +对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。 +共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点: +在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到 引用它的可执行文件中; +在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。 + + + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +参考资料 +Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. + +汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. +史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014. +Operating System Notes Operating-System Structures Processes +Inter Process Communication Presentation[1] +Decoding UCS Invicta – Part 1 From 112fdcaeecfb89664f79461b684df6b62938e5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:10:47 +0800 Subject: [PATCH 73/97] =?UTF-8?q?Create=20=E5=8D=81=E3=80=81=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E6=9C=BA=E7=BD=91=E7=BB=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6\227\346\234\272\347\275\221\347\273\234" | 1017 +++++++++++++++++ 1 file changed, 1017 insertions(+) create mode 100644 "docs/\345\215\201\343\200\201\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234" diff --git "a/docs/\345\215\201\343\200\201\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234" "b/docs/\345\215\201\343\200\201\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234" new file mode 100644 index 00000000..42148888 --- /dev/null +++ "b/docs/\345\215\201\343\200\201\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234" @@ -0,0 +1,1017 @@ +第 5.1 节 计算机网络 + +概述 +网络的网络 +网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。 + + +ISP +互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。 + + +目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。 + + + +主机之间的通信方式 +客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。 + + +对等(P2P):不区分客户和服务器。 + +电路交换与分组交换 +1.电路交换 +电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该 链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。 +2.分组交换 +每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影 响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。 +在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地, 这个过程就是存储转发过程,分组交换也使用了存储转发过程。 +时延 +总时延 = 排队时延 + 处理时延 + 传输时延 + 传播时延 + + +1.排队时延 +分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。 +2.处理时延 +主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据、进行差错检验或查找适当的路 由等。 +3.传输时延 +主机或路由器传输数据帧所需要的时间。 + + +其中 l 表示数据帧的长度,v 表示传输速率。 +4.传播时延 +电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。 + + +其中 l 表示信道长度,v 表示电磁波在信道上的传播速度。 +计算机网络体系结构 + + +1.五层协议 +应用层 :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。 +传输层 :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 +网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。 +数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。 +物理层 :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 +2.OSI +其中表示层和会话层用途如下: +表示层 :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。会话层 :建立及管理会话。 +五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 +3.TCP/IP +它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 + +TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 + + +4.数据在各层之间的传递过程 +在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 +路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输 层和应用层。 +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +物理层 +通信方式 +根据信息在传输线上的传送方向,分为以下三种通信方式: 单工通信:单向传输 +半双工通信:双向交替传输 +全双工通信:双向同时传输 + +带通调制 +模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。 + + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +链路层 +基本问题 +1.封装成帧 + +将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。 + + +2.透明传输 +透明表示一个实际存在的事物看起来好像不存在一样。 +帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的 判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符 前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉 不到转义字符的存在。 + + +3.差错检测 +目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。 +信道分类 +1.广播信道 +一对多通信,一个节点发送的数据能够被广播信道上所有的节点接收到。 +所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰 撞)。 +主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。 +2.点对点信道一对一通信。 +因为不会发生碰撞,因此也比较简单,使用 PPP 协议进行控制。 +信道复用技术 +1.频分复用 +频分复用的所有主机在相同的时间占用不同的频率带宽资源。 + + + +2.时分复用 +时分复用的所有主机在不同的时间占用相同的频率带宽资源。 + + +使用频分复用和时分复用进行通信,在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性 质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。 +3.统计时分复用 +是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后 发送。 + + + +4.波分复用 +光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。 +5.码分复用 +为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 和 有 + + +为了讨论方便,取 m=8,设码片 +为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到 + + +其中 为 的反码。 +利用上面的式子我们知道,当接收端使用码片 +对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 +的是用户发送的比特 0。 +码分复用需要发送的数据量为原先的 m 倍。 + + + +CSMA/CD 协议 +CSMA/CD 表示载波监听多点接入 / 碰撞检测。 +多点接入 :说明这是总线型网络,许多主机以多点的方式连接到总线上。 +载波监听 :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。 +碰撞检测 :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。 +记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 争用期 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。 +当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 截断二进制指数退避算法 来确定。从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。 + + +PPP 协议 +互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。 + + + +PPP 的帧格式: +F 字段为帧的定界符 +A 和 C 字段暂时没有意义 +FCS 字段是使用 CRC 的检验序列信息部分的长度不超过 1500 + + +MAC 地址 +MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。 +一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。 +局域网 +局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限。 主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。 +可以按照网络拓扑结构对局域网进行分类: + + + +以太网 +以太网是一种星型拓扑结构局域网。 +早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线 器同时收到两个不同接口的帧,那么就发生了碰撞。 +目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 +以太网帧格式: +类型 :标记上层使用的协议; +数据 :长度在 46-1500 之间,如果太小则需要填充; +FCS :帧检验序列,使用的是 CRC 检验方法; + + +交换机 +交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。 +正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。 +下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧, 主机 B 回应该帧向主机 A 发送数据包时,交换机查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 2 的映射。 + + + +虚拟局域网 +虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。 例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 +使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。 + + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +网络层 +概述 +因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大 努力交互的数据报服务。 +使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。 + + +与 IP 协议配套使用的还有三个协议: +地址解析协议 ARP(Address Resolution Protocol) +网际控制报文协议 ICMP(Internet Control Message Protocol) 网际组管理协议 IGMP(Internet Group Management Protocol) +IP 数据报格式 + + + +版本 : 有 4(IPv4)和 6(IPv6)两个值; +首部长度 : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为固定部分长 +度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。区分服务 : 用来获得更好的服务,一般情况下不使用。 +总长度 : 包括首部长度和数据部分长度。 +生存时间 :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL +为 0 时就丢弃数据报。 +协议 :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。 +首部检验和 :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。 +标识 : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。片偏移 : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。 + + +IP 地址编址方式 +IP 地址的编址方式经历了三个历史阶段: 分类 +子网划分 +无分类 +1.分类 +由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。 +IP 地址 ::= {< 网络号 >, < 主机号 >} + +2.子网划分 +通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。 +IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} +要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特, 那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。 +注意,外部网络看不到子网的存在。 +3.无分类 +无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 +IP 地址 ::= {< 网络前缀号 >, < 主机号 >} +CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。 +CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。 + +一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由 聚合,也称为 构成超网 。 +在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹 配来确定应该匹配哪一个。 +地址解析协议 ARP +网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 + + +ARP 实现由 IP 地址得到 MAC 地址。 + + + +每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。 +如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 + + + +网际控制报文协议 ICMP +ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 + + +ICMP 报文分为差错报告报文和询问报文。 + +1.Ping +Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 +Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 +2.Traceroute +Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 +Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。 +源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; +源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 +虚拟专用网 VPN +由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。 +有三个专用地址块: +10.0.0.0 ~ 10.255.255.255 +172.16.0.0 ~ 172.31.255.255 +192.168.0.0 ~ 192.168.255.255 +VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信; 虚拟指好像是,而实际上并不是,它有经过公用的互联网。 + +下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。 + + +网络地址转换 NAT +专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。 +在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。 + + +路由器的结构 +路由器从功能上可以划分为:路由选择和分组转发。 +分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。 + + +路由器分组转发流程 +从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; +若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; +若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; 报告转发分组出错。 + + +路由选择协议 +路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。 + +互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。可以把路由选择协议划分为两大类: +自治系统内部的路由选择:RIP 和 OSPF +自治系统间的路由选择:BGP +1.内部网关协议 RIP +RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。 +RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。 +距离向量算法: +对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1; +对修改后的 RIP 报文中的每一个项目,进行以下步骤: +若原来的路由表中没有目的网络 N,则把该项目添加到路由表中; +否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。 +若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。 +RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。 +2.内部网关协议 OSPF +开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。 +开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 +SPF。 +OSPF 具有以下特点: +向本自治系统中的所有路由器发送信息,这种方法是洪泛法。 +发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距 离、时延、带宽等来表示。 +只有当链路状态发生变化时,路由器才会发送信息。 +所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。 +3.外部网关协议 BGP +BGP(Border Gateway Protocol,边界网关协议) AS 之间的路由选择很困难,主要是由于: +互联网规模很大; +各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量; +AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 +BGP 只能寻找一条比较好的路由,而不是最佳路由。 + +每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。 + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +传输层 +网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信, 传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑 通信信道。 +UDP 和 TCP 的特点 +用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文 +(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。 + +传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据 块),每一条 TCP 连接只能是点对点的(一对一)。 +UDP 首部格式 + + +首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 +TCP 首部格式 + + + +序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 +字节,那么下一个报文段的序号应为 401。 +确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。 +数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。 +确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 +ACK 置 1。 +同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 +终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 +TCP 的三次握手 + + + +假设 A 为客户端,B 为服务器端。 +首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 +A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。 +B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 +A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。B 收到 A 的确认后,连接建立。 +三次握手的原因 +第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 +客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待 一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握 手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确 认,不进行第三次握手,因此就不会再次打开连接。 +TCP 的四次挥手 + + + +以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 +A 发送连接释放报文,FIN=1。 +B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。当 B 不再需要连接时,发送连接释放报文,FIN=1。 +A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。B 收到 A 的确认后释放连接。 +四次挥手的原因 +客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。 +TIME_WAIT +客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: +确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文, +A 等待一段时间就是为了处理这种情况的发生。 +等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧 的连接请求报文。 +TCP 可靠传输 +TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 +一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: + + + +其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: + +其中 RTTd 为偏差的加权平均值。 +TCP 滑动窗口 +窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 +发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确 认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类 似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 +接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} +按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 + + +TCP 流量控制 +流量控制是为了控制发送方发送速率,保证接收方来得及接收。 +接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 +0,则发送方不能发送数据。 +TCP 拥塞控制 +如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当 控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制 是为了降低整个网络的拥塞程度。 + + + +TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 +发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状 态变量,实际决定发送方能发送多少数据的是发送方窗口。 +为了便于讨论,做如下假设: +接收方有足够大的接收缓存,因此不会发生流量控制; +虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 + + +1.慢开始与拥塞避免 +发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... +注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 +如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 + +2.快重传与快恢复 +在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 +在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例 如收到三个 M2,则 M3 丢失,立即重传 M3。 +在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 +慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd +设定为 ssthresh。 + + +微信公众号 +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + + +应用层 +域名系统 +DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 +域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。 + + +DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输: +如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。 +区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 +文件传送协议 +FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: +控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 +数据连接:用来传送一个文件数据。 +根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式: +主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20,客户端的端口号随机,但是必须大于1024,因为 0~1023 是熟知端口号。 + + +被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。 + + +主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即 可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。 +动态主机配置协议 +DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要手动配置 IP 地址等信息。 +DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 +DHCP 工作过程如下: +1.客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP +中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 +2.DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 +3.如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 +4.DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 + + + + +远程登录协议 +TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。 +TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。 +电子邮件协议 +一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。 +邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。 + + +1.SMTP +SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。 + + +2.POP3 +POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 +3.IMAP +IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 +常用端口 + +应用 应用层协议 端口号 传输层协议 备注 +域名解析 DNS 53 UDP/TCP 长度超过 512 字节时使用 TCP +动态主机配置协议 DHCP 67/68 UDP +简单网络管理协议 SNMP 161/162 UDP +文件传送协议 FTP 20/21 TCP 控制连接 21,数据连接 20 +远程终端协议 TELNET 23 TCP +超文本传送协议 HTTP 80 TCP +简单邮件传送协议 SMTP 25 TCP +邮件读取协议 POP3 110 TCP +网际报文存取协议 IMAP 143 TCP +Web 页面请求过程 +1.DHCP 配置主机信息 +假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。 +主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。 + +该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报 +文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP +地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。 +主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。 +2.ARP 解析 MAC 地址 +主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。 +主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。 +该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。 +DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。 +主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备, 包括网关路由器。 +网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。 +3.DNS 解析域名 +知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。 +网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。 +因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。 +到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。 +找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。 +4.HTTP 请求页面 +有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。 +在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。 +HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。 +HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。 +浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。 +微信公众号 + +更多精彩内容将发布在微信公众号 Python看世界 上,公众号也提供了一份技术面试复习大纲,不仅系统整理了面试知识点,而且标注了各个知识点的重要程度,从而帮你理清多而杂的面试知识点,后台回复 "大纲"即可领取。可以根据大纲上列的知识点来进行复习,就不用看很多不重要的内容,也可以知道哪些内容很重要从而多安排一些 复习时间。 + +参考链接 +计算机网络, 谢希仁 +JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014. W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006. +Active vs Passive FTP Mode: Which One is More Secure? Active and Passive FTP Transfers Defined - KB Article #1138 Traceroute +ping +How DHCP works and DHCP Interview Questions and Answers What is process of DORA in DHCP? +What is DHCP Server ? +Tackling emissions targets in Tokyo What does my ISP know when I use Tor? +Technology-Computer Networking[1]-Computer Networks and the Internet P2P 网络概述. +Circuit Switching (a) Circuit switching. (b) Packet switching. +第 5.2 节 HTTP + +一 、基础概念 +URI +URI 包含 URL 和 URN。 + + + +请求和响应报文 +1.请求报文 + + +2.响应报文 + + +二、HTTP 方法 + +客户端发送的 请求报文 第一行为请求行,包含了方法字段。 +GET +获取资源 +当前网络请求中,绝大部分使用的是 GET 方法。 +HEAD +获取报文首部 +和 GET 方法类似,但是不返回报文实体主体部分。 +主要用于确认 URL 的有效性以及资源更新的日期时间等。 +POST +传输实体主体 +POST 主要用来传输数据,而 GET 主要用来获取资源。更多 POST 与 GET 的比较请见第九章。 +PUT +上传文件 +由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 + + +PATCH +对资源进行部分修改 +PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 + + +DELETE +删除文件 +与 PUT 功能相反,并且同样不带验证机制。 + + + + +OPTIONS +查询支持的方法 +查询指定的 URL 能够支持的方法。会返回 +CONNECT +要求在与代理服务器通信时建立隧道 + + + + +这样的内容。 + +使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 + + + +TRACE +追踪路径 +服务器会将通信路径返回给客户端。 +发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。 +三、HTTP 状态码 +服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 + +状态码 类别 含义 +1XX Informational(信息性状态码) 接收的请求正在处理 +2XX Success(成功状态码) 请求正常处理完毕 +3XX Redirection(重定向状态码) 需要进行附加操作以完成请求 +4XX Client Error(客户端错误状态码) 服务器无法处理请求 +5XX Server Error(服务器错误状态码) 服务器处理请求出错 + +1XX 信息 +100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 +2XX 成功 +200 OK +204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 +206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 +3XX 重定向 +301Moved Permanently :永久性重定向 +302Found :临时性重定向 +303See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 +注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 +304Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None- Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 +307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 +POST 方法改成 GET 方法。 +4XX 客户端错误 +400Bad Request :请求报文中存在语法错误。 +401Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 +403 Forbidden :请求被拒绝。 +404 Not Found +5XX 服务器错误 +500 Internal Server Error :服务器正在执行请求时发生错误。 +503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 +四、HTTP 首部 +有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。各种首部字段及其含义如下(不需要全记,仅供查阅): +通用首部字段 + +首部字段名 说明 +Cache-Control 控制缓存的行为 +Connection 控制不再转发给代理的首部字段、管理持久连接 +Date 创建报文的日期时间 +Pragma 报文指令 +Trailer 报文末端的首部一览 +Transfer-Encoding 指定报文主体的传输编码方式 +Upgrade 升级为其他协议 +Via 代理服务器的相关信息 +Warning 错误通知 + +请求首部字段 + +首部字段名 说明 +Accept 用户代理可处理的媒体类型 +Accept-Charset 优先的字符集 +Accept-Encoding 优先的内容编码 +Accept-Language 优先的语言(自然语言) +Authorization Web 认证信息 +Expect 期待服务器的特定行为 +From 用户的电子邮箱地址 +Host 请求资源所在服务器 +If-Match 比较实体标记(ETag) +If-Modified-Since 比较资源的更新时间 +If-None-Match 比较实体标记(与 If-Match 相反) +If-Range 资源未更新时发送实体 Byte 的范围请求 +If-Unmodified-Since 比较资源的更新时间(与 If-Modified-Since 相反) +Max-Forwards 最大传输逐跳数 +Proxy-Authorization 代理服务器要求客户端的认证信息 +Range 实体的字节范围请求 +Referer 对请求中 URI 的原始获取方 +TE 传输编码的优先级 +User-Agent HTTP 客户端程序的信息 + +响应首部字段 + +首部字段名 说明 +Accept-Ranges 是否接受字节范围请求 +Age 推算资源创建经过时间 +ETag 资源的匹配信息 +Location 令客户端重定向至指定 URI +Proxy-Authenticate 代理服务器对客户端的认证信息 +Retry-After 对再次发起请求的时机要求 +Server HTTP 服务器的安装信息 +Vary 代理服务器缓存的管理信息 +WWW-Authenticate 服务器对客户端的认证信息 + +实体首部字段 + +首部字段名 说明 +Allow 资源可支持的 HTTP 方法 +Content-Encoding 实体主体适用的编码方式 +Content-Language 实体主体的自然语言 +Content-Length 实体主体的大小 +Content-Location 替代对应资源的 URI +Content-MD5 实体主体的报文摘要 +Content-Range 实体主体的位置范围 +Content-Type 实体主体的媒体类型 +Expires 实体主体过期的日期时间 +Last-Modified 资源的最后修改日期时间 +五、具体应用 +连接管理 + + + +1.短连接与长连接 +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 +从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 +在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 Connection : Keep-Alive 。 +2.流水线 +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 +流水线是在同一条长连接上连续发出请求,而不用等待响应返回,这样可以减少延迟。 +Cookie +HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 +Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 +1.用途 +会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) 个性化设置(如用户自定义设置、主题等) +浏览器行为跟踪(如跟踪分析用户行为等) +2.创建过程 +服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 + + +客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 + + +3.分类 +会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 +持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 + + +4.作用域 +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。 +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: +/docs +/docs/Web/ +/docs/Web/HTTP + +5.JavaScript + +浏览器通过 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 + + + +6.HttpOnly +标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 +API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 + + +7.Secure +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 +8.Session +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 +使用 Session 维护用户登录状态的过程如下: +用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 +应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 +9.浏览器禁用 Cookie +此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 +10.Cookie 与 Session 选择 +Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 +Session; +Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 +缓存 +1.优点 + +缓解服务器压力; +降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存服务器在地理位置上也有 可能比源服务器来得近,例如浏览器缓存。 +2.实现方法 +让代理服务器进行缓存; 让客户端浏览器进行缓存。 +3.Cache-Control +HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 +3.1禁止进行缓存 +no-store 指令规定不能对请求或响应的任何一部分进行缓存。 + + +3.2强制确认缓存 +no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应。 + + +3.3私有缓存和公共缓存 +private 指令规定了将资源作为私有缓存,只能被单独用户使用,一般存储在用户浏览器中。 + + +public 指令规定了将资源作为公共缓存,可以被多个用户使用,一般存储在代理服务器中。 + + +3.4缓存过期机制 +max-age 指令出现在请求报文,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 +max-age 指令出现在响应报文,表示缓存资源在缓存服务器中保存的时间。 + + +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 + + +在 HTTP/1.1 中,会优先处理 max-age 指令; 在 HTTP/1.0 中,max-age 指令会被忽略掉。 +4.缓存验证 + +需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 +有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 + + +可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 +ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 + + +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有实体主体的 304 Not Modified 响应报文。 + + + +内容协商 +通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 +1.类型 +1.1服务端驱动型 +客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。 +它存在以下问题: +服务器很难知道客户端浏览器的全部信息; +客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); +给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 +1.2代理驱动型 +服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 +2.Vary + + +在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器 请求该资源。 + +例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 +内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 +Accept-Language 与缓存中的对应的值相同时才会返回该缓存。 +内容编码 +内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。 +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一 +种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,响应报文的 Vary 首部字段至少要包含 Content- +Encoding。 +范围请求 +如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而 避免服务器重新发送所有数据。 +1.Range +在请求报文中添加 Range 首部字段指定请求的范围。 + + +请求成功的话服务器返回的响应包含 206 Partial Content 状态码。 + + +2.Accept-Ranges +响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。 + + +3.响应状态码 +在请求成功的情况下,服务器会返回 206 Partial Content 状态码。 +在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。在不支持范围请求的情况下,服务器会返回 200 OK 状态码。 +分块传输编码 +Chunked Transfer Encoding,可以把数据分割成多块,让浏览器逐步显示页面。多部分对象集合 + +一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 +例如,上传多个表单时可以使用如下方式: + + +虚拟主机 +HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。通信数据转发 +1.代理 +代理服务器接受客户端的请求,并且转发给其它服务器。使用代理的主要目的是: +缓存 +负载均衡 +网络访问控制访问日志记录 +代理服务器分为正向代理和反向代理两种: 用户察觉得到正向代理的存在。 + + +而反向代理一般位于内部网络中,用户察觉不到。 + + + + +2.网关 +与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。 +3.隧道 +使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 +六、HTTPS +HTTP 有以下安全性问题: +使用明文进行通信,内容可能会被窃听; +不验证通信方的身份,通信方的身份有可能遭遇伪装; 无法证明报文的完整性,报文有可能遭篡改。 +HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 +HTTPS 使用了隧道进行通信。 +通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + + + + +加密 +1.对称密钥加密 +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。优点:运算速度快; +缺点:无法安全地将密钥传输给通信方。 + + + + +2.非对称密钥加密 +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 +公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通 信内容后使用私有密钥解密。 +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥 进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 +优点:可以更安全地将公开密钥传输给通信发送方; 缺点:运算速度慢。 + + + +3.HTTPS 采用的加密方式 +HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥) + + + + + + + +认证 +通过使用 证书 来对通信方进行认证。 +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证, 如果验证通过,就可以开始通信了。 + + + +完整性保护 +SSL 提供报文摘要功能来进行完整性保护。 +HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 +HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 +HTTPS 的缺点 +因为需要进行加密解密等过程,因此速度会更慢; 需要支付证书授权的高额费用。 +七、HTTP/2.0 +HTTP/1.x 缺陷 +HTTP/1.x 实现简单是以牺牲性能为代价的: +客户端需要使用多个连接才能实现并发和缩短延迟; +不会压缩请求和响应首部,从而导致不必要的网络流量; +不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 +二进制分帧层 + +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 + + +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 +一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。 +帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重 新组装。 + + +服务端推送 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 + + +首部压缩 +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 +HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 + + + +八、HTTP/1.1 新特性 +详细内容请见上文 +默认是长连接支持流水线 +支持同时打开多个 TCP 连接支持虚拟主机 +新增状态码 100 +支持分块传输编码 +新增缓存处理指令 max-age +九、GET 和 POST 比较 +作用 +GET 用于获取资源,而 POST 用于传输实体主体。参数 +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具 +(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 +%E4%B8%AD%E6%96%87 ,而空格会转换为 %20 。POST 参数支持标准字符集。 + +会转换为 + + + + + +安全 +安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 +GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 +安全的方法除了 GET 之外还有:HEAD、OPTIONS。不安全的方法除了 POST 之外还有 PUT、DELETE。幂等性 +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 +所有的安全方法也都是幂等的。 +在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 +GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的: + + +POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录: + + +DELETE /idX/delete HTTP/1.1 是幂等的,即使不同的请求接收到的状态码不一样: + +DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists +DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted +DELETE /idX/delete HTTP/1.1 -> Returns 404 +可缓存 +如果要对响应进行缓存,需要满足以下条件: + +请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。响应报文的 Cache-Control 首部字段没有指定不进行缓存。 +XMLHttpRequest +为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: +XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 A JAX 中被大量使用。 +在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +而 GET 方法 Header 和 Data 会一起发送。 +参考资料 +上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014. MDN : HTTP +HTTP/2 简介htmlspecialchars +Difference between file URI and URL in java +How to Fix SQL Injection Using Java PreparedStatement & CallableStatement +浅谈 HTTP 中 Get 与 Post 的区别 +Are http:// and www really necessary? HTTP (HyperText Transfer Protocol) +Web-VPN: Secure Proxies with SPDY & Chrome File:HTTP persistent connection.svg +Proxy server +What Is This HTTPS/SSL Thing And Why Should You Care? What is SSL Offloading? +Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption An Introduction to Mutual SSL Authentication +The Difference Between URLs and URIs Cookie 与 Session 的区别 +COOKIE 和 SESSION 有什么区别 +Cookie/Session 的机制与安全HTTPS 证书原理 +What is the difference between a URI, a URL and a URN? XMLHttpRequest +XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST? Symmetric vs. Asymmetric Encryption – What are differences? +Web 性能优化与 HTTP/2 +HTTP/2 简介 From 42a8c898824cfe9aa74eb0382fc2ed9387c800a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:40:21 +0800 Subject: [PATCH 74/97] =?UTF-8?q?Create=20=E5=8D=81=E4=B8=80=E3=80=81?= =?UTF-8?q?=E5=88=86=E5=B8=83=E5=BC=8F=E7=90=86=E8=AE=BA---=E9=97=AE?= =?UTF-8?q?=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...350\256\272---\351\227\256\351\242\230.md" | 489 ++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 "docs/\345\215\201\344\270\200\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\351\227\256\351\242\230.md" diff --git "a/docs/\345\215\201\344\270\200\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\351\227\256\351\242\230.md" "b/docs/\345\215\201\344\270\200\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\351\227\256\351\242\230.md" new file mode 100644 index 00000000..99c8a95e --- /dev/null +++ "b/docs/\345\215\201\344\270\200\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\351\227\256\351\242\230.md" @@ -0,0 +1,489 @@ +# 访问一个网站的全过程 +# DNS +- 先尝试从host文件中读取域名对应的IP地址,如果找到,则完毕;如果未找到,则使用DNS进行查找。 +# TCP +- 三次握手建立连接 +# 负载均衡服务器 +- Nginx? +# 应用服务器 +- Tomcat? +# 浏览器渲染 +- 缓存? +- + +# 大型网站架构演进 +# 1)单机 + +# 2)单机负载告警,数据库与应用分离 + +# 3)应用服务器负载告警,让应用服务器走向集群 + +# 1)引入负载均衡设备 + +# 2)分布式Session +## Session Sticky 会话粘滞 + + +## Session Replication 会话复制 + + +## Session 集中存储 + + +## Cookie Based + + +# 4)数据库读压力变大,读写分离 +# 1)数据库主从复制 + + + +# 2)搜索引擎 + + + +# 3)分布式缓存 +- 数据缓存: + +- 页面缓存:Apache或Nginx +# 5)弥补关系数据库的不足,引入分布式存储系统 + + +# 6)数据库又遇瓶颈,分库分表 +# 1)垂直拆分——分库 +- 按业务分库 + +- 需要解决表关联和分布式事务问题 +# 2)水平拆分——分表 + + + +# 7)应用拆分与服务化 +# 1)应用拆分 + + + +# 2)服务化 + + + +# 8)消息中间件(异步+解耦) + +# 9)CDN +- 内容分发网络。作用是把用户需要的内容分发到离用户近的地方,这样可以使用户能够就近获取所需内容。整个CDN系统分为CDN源站和CDN节点,CDN源站提供CDN节点使用的数据源头,而CDN节点则部署在距离最终用户比较近的地方,加速用户对站点的访问。 +- CDN其实就是一种网络缓存技术,能够把一些相对稳定的资源放到距离最终用户较近的机房,一方面可以节省整个广域网的带宽开销,另一方面可以提升用户的访问速度,改进用户体验。我们一般把一些相对静态的文件放在CDN中。 + +# 引入CDN后浏览器访问网站的流程 + - 1)用户向浏览器提交域名 + - 2)浏览器对域名进行解析,由于CDN对域名解析过程进行了调整,所以得到的是该域名对应的CNAME记录。 + - 3)对CNAME再次进行解析,得到实际IP地址。在这次的解析中,会使用全局负载均衡DNS解析,也就是我们需要返回具体IP地址,需要根据地理位置信息以及所在的ISP来确定返回的结果,这个过程才能让身处不同地域、连接不同接入商的用户得到最适合自己访问的CDN地址,才能做到就近访问,从而提升速度。 + - 4)得到实际的IP地址后,向服务器发出访问请求。 + - 5)CDN会根据请求的内容是否在本地缓存进行不同处理: +- 如果存在,则直接返回结果 +- 如果不存在,则CDN请求源站,获取内容,然后再返回结果。 + +- CDN的几个关键技术: +# 全局调度 +- 全局调度是完成用户就近访问的第一步,我们需要根据用户地域、接入运营商以及CDN机房的负载情况去调度。前面两个调度因素需要一个尽可能精准的IP地址库,这是正确调用的前提。IP地址库的维护是一个持续和变化的过程,并且调度的策略随着CDN机房的增加也会变化。除了距离,CDN的负载也是调度中的一个影响因素。 +# 缓存技术 +- 如果CDN机房的请求命中率不高的话,那么起到的加速效果也是相对有限的。 +- 要提升命中率,就需要CDN机房中有尽可能全面的数据,这就要求CDN机房的缓存容量要足够大,可以使用内存+SSD+机械硬盘的混合存储方式来提升整体的缓存容量,并且需要做好冷热数据的交换,在提升命中率时也尽量降低缓存的响应数据。 +- 此外,当CDN的cache没有命中要回源加载数据时,合并同样数据的请求也是一个很重要的优化,这样可以减少重复的请求,降低源站的压力。 +- 最后,新增、变更数据后的CDN预加载也是一个提升命中率的办法,也就是在没有请求进来时,CDN主动去加载数据,做好准备。当然这个主动加载一般也需要源站有一个通知过来。 +# 内容分发 +- 内容分发主要是对内容全部在CDN上不用回源的数据的管理和分发,例如一些静态页面等。具体做法是在内容管理系统中进行编辑修改后,通过分发系统分发到各个CDN的节点上。分发的效率以及对分发文件一致性、正确性的校验是需要关注的点。 +# 带宽优化 +- CDN提供了内容加速,很多请求和浏览都压到了CDN上,那么如何能够比较有效地节省贷款是一个很重要的事情,因为这直接关系到流量成本。优化的思路是只返回必要的数据,用更好的压缩算法等。 +- 我们可以利用CDN机房距离最终用户近的特点,让一些上传的工作从CDN接入,然后再从CDN传到源站,这一方面可以提升用户的上传速度,另一方面也很好地利用了从CDN机房到源站的上行带宽。 + +# 整体结构 + + +- + +# 大型网站架构模式 +# 分层 +- 大型网站架构中采用分层解雇,将网站软件系统分为应用层、服务层、数据层等。 + +# 分隔 +- 如果说分层是将软件在横向方面进行切分,那么分隔就是在纵向方面对软件系统切分。 +- 网站越大,功能越复杂,服务和数据处理的种类越多,将这些不同的功能和服务分割开来,包装成高内聚低耦合的模块单元,一方面有助于软件的开发和维护;另一方面,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。 + +# 分布式 +- 对于大型网站,分层和分隔的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,,通过远程调用协同工作。分布式意味着可以使用更多的计算机完成同样的工作。 +- 常见的分布式有分布式应用,分布式静态资源(分布式文件系统),分布式数据和存储(分布式数据库和NoSQL),分布式计算 + +# 集群 +- 对于用户访问集中模块,还需要将独立部署的服务器集群化,即多台服务器部署相同应用构成一个集群。通过负载均衡设备共同对外提供服务。 +- 因为服务器集群有更多服务器提供相同服务,因此可以提供更好的并发特性,当有更多用户访问时,只需要向集群中加入新的机器即可。并且如果某台服务器发生故障时,负载均衡设备会将请求转移到集群中的其他服务器中,使服务器故障不影响用户使用。 + +# 缓存 +- 比如CDN,反向代理(缓存静态资源),本地缓存,分布式缓存 +# 异步 +- 内存队列;分布式消息队列 + - 1)提供系统可用性 + - 2)加快网站响应速度 + - 3)削峰 +# 冗余 +- 访问和负载很小的服务也必须部署至少两台服务器构成一个集群。数据库除了冷备份外,还需要进行主从分离,实现热备份。 +# 自动化 +- 自动化主要是在发布运维方面。比如自动化发布、自动化代码管理、自动化测试。 +# 安全 +- + +# 大型网站核心架构要素 +# 性能 +# 网站性能指标(PV QPS TPS 吞吐量 响应时间) +- 网站并发量是多少? +## 响应时间 +- 指应用执行一个操作需要的时间。响应时间是系统最重要的性能指标,直观地反应了系统的快慢。 + +- 测试程序通过模拟应用程序,记录收到响应和发出请求之间的时间差来计算系统响应时间。 +## 并发数 +- 指系统能够同时处理请求的数目。对于网站而言,对于网站而言,并发数即网站并发用户数,指同时提交请求的用户数目。 +- 网站系统用户数(注册用户数)>>网站在线用户数(当前登录网站的用户总数)>>网站并发用户数 +- 测试程序通过多线程模拟并发用户的办法来测试系统的并发处理能力。为了真实模拟用户行为,测试程序并不是启动多线程然后不停地发送请求,而是在两次请求之间加入一个随机等待时间,这个时间被称作思考时间。 +## 吞吐量(还有QPS TPS) +- 指单位时间内系统处理的请求数量,体现系统的整体处理能力。对于网站,可以用请求数/秒,或者页面数/每秒来衡量,也可以用访问人数/天,或者处理的业务数/小时等来衡量。TPS(每秒事务数)是吞吐量的一个常用量化指标,此外还有QPS(每秒查询数)、HPS(每秒HTTP请求数)等。 +## 性能计数器 +- 它是描述服务器或操作系统性能的一些数据指标,包括System Load、对象与线程数、内存使用、CPU使用、磁盘与网络IO等指标。这些指标也是系统监控的重要参数,对这些指标设置报警阈值,当监控系统发现性能计数器超过阈值时,就向运维和开发人员报警,及时发现处理系统异常。 +- System Load:系统负载,指当前正在被CPU执行和等待被CPU执行的进程数目总和,是反应系统忙闲程度的重要指标。多核CPU的情况下,完美情况是指所有CPU都在使用,没有进程在等待处理,所以Load的理想值是CPU的数目。当Load值低于CPU数目的时候,表示CPU有空闲,资源存在浪费;高于时表示进程在排队等待CPU调度,表示系统资源不足。 +- 在Linux系统中使用top命令查看,该值是三个浮点数,表示最近1分钟、5分钟、15分钟的运行队列平均进程数。 + +# Web性能优化 +## 前端优化 + - 1)浏览器缓存 + - 2)HTTP请求合并 + - 3)CSS、JS、图片等资源合并压缩 + - 4)CDN + - 5)页面静态化 + - 6)减少Cookie传输 +## 服务端优化 + - 1)接入层:反向代理,负载均衡 + - 2)应用服务器集群 + - 3)缓存:分布式缓存 + - 4)数据库优化,查询优化,索引,主从复制,读写分离,分库分表 + - 5)分布式消息队列,异步操作 +# 影响单机并发量的因素与优化 + - CPU消耗严重的解决办法:1)减少阻塞,同步转异步,队列等 2)减少线程数,避免大量时间消耗着线程上下文切换上3)降低锁的竞争,尽可能采用lock-free、non-blocking的方式 + - 文件IO消耗严重的解决办法:1)异步写文件2)批量读写3)限流,比如放入队列4)限制文件大小 5)缓存6)RAID + - 内存消耗严重的解决办法:1)使用不必要的引用2)使用对象缓存池3)使用合理的缓存失效算法 + - 网络IO消耗严重的解决办法:1)减少网络交互次数2)减少网络传输数据量的大小3)尽量采用异步非阻塞的方式 + +- 本地缓存;数据库优化,索引等 +# 抢购/超卖问题 +## 前端 +- 页面静态化 +- CDN +- 禁止用户重复提交请求 +## 接入层 +- 限制同一个IP的访问频率 +- 页面缓存 +## 服务层 +- 避免恶意刷单,直接绕过App/网页:每个表单分配Token, +- 可以考虑随机将部分请求直接返回失败 +- 数据库悲观锁更新库存 +## 数据层 +- 主从复制,读写分离,分库分表 +# 抢红包问题 +- 一万个人抢100个红包,如何实现(不用队列),如何保证2个人不能抢到同一个红包,可用分布式锁 + +- 微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到redis集群里,app端将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的逻辑处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。 + +- 红包如何计算被抢完? cache会抵抗无效请求,将无效的请求过滤掉,实际进入到后台的量不大。cache记录红包个数,原子操作进行个数递减,到0表示被抢光。 +- + +# 可用性/高可用 +# 网站可用性度量 +- 网站不可用时间=故障修复时间点-故障发现时间点 +- 网络年度可用性指标=(1-网站不可用时间/年度总时间) * 100% +- 对于大多数网站来说,2个9是基本可用,3个9是较高可用,4个9是具有自动恢复能力的高可用。 + + +# 高可用的网站架构(冗余备份+失效转移) +- 实现高可用架构的主要手段是数据和服务的冗余备份和失效转移。 +- 位于应用层的服务器通常会使用负载均衡设备,通过心跳检测监控某台服务器不可用时,会将其从集群列表中剔除,使整个集群保持可用。 +- 位于服务层的服务器也是使用集群,使用服务注册查找中心对提供服务的服务器进行心跳检测。 +- 位于数据层的服务器需要在数据写入时进行同步复制,实现数据冗余备份。 +# 高可用的应用(集群) +- 负载均衡;Session管理 +# 高可用的服务(集群) +- 除了使用服务注册查找中心进行心跳检测外,还可以: + - 1)分级管理:核心应用和服务优先使用更好的硬件,在运维响应速度上也格外迅速。 + - 2)超时设置:在应用程序中设置服务调用的超时时间,一旦超时,通信框架就抛出异常,应用程序根据服务调度策略,可以选择继续重试或者将请求转移到提供相同服务的其他服务器上。 + - 3)异步调用:消息队列 + - 4)服务降级:为了保证高并发下核心应用和功能的正常运行,需要对服务进行降级。降级有两种手段:拒绝服务和关闭服务。 +- 拒绝服务:拒绝低优先级应用的调用,减少服务调用并发数,或者随机拒绝部分服务额调用 +- 关闭服务:关闭部分不重要的服务,或者服务内部关闭部分不重要的功能 + - 5)幂等性设计 +# 高可用的数据(主备+互备) +## 主备模式 +- Active-Standby模式。当主机宕机时,备机接管主机的一切工作,待主机恢复正常后,按使用者的的设定以热备(自动)或冷备(手动)方式将服务切换到主机上运行。 +- 在数据库部分,称之为MS模式(Master-Slave模式)。 +## 互备模式 +- 两台主机同时运行各自的服务工作且相互检测情况。在数据库部分,常见的互备是MM(多主模式),指一个系统有多个master,每个master都可以读写,需根据时间戳或业务逻辑合并版本。 +## 集群模式 +- 有多个节点在运行,同时可以通过主控节点分担服务请求,比如Zookeeper。集群节点要特别解决主控节点的高可用问题。 +## Fail* +### Fail-Over 失效转移 +- Fail-Over的含义为“失效转移”,是一种备份操作模式,当主要组件异常时,其功能转移到备份组件。其要点在于有主有备,且主故障时备可启用,并设置为主。如Mysql的双Master模式,当正在使用的Master出现故障时,可以拿备Master做主使用。 + +### Fail-Safe 失效安全 +- Fail-Safe的含义为“失效安全”,即使在故障的情况下也不会造成伤害或者尽量减少伤害。维基百科上一个形象的例子是红绿灯的“冲突监测模块”当监测到错误或者冲突的信号时会将十字路口的红绿灯变为闪烁错误模式,而不是全部显示为绿灯。 +### Fail-Fast 快速失败 +- 从字面含义看就是“快速失败”,让可能的错误尽早的被发现,对应的方式是“fault-tolerant(错误容忍)”。以JAVA集合(Collection)的快速失败为例,当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。 +# 高可用网站的软件质量保证 +- 网站发布;自动化测试;预发布验证;代码控制(主干发布、分支开发) + +- ;自动化发布;灰度发布 +# 网站运行监控 + +# 伸缩性 +# 网站架构的伸缩性设计 +## 不同功能进行物理分离实现伸缩 + +- 纵向分离(分层后分离):将业务处理流程上的不同部分分离部署 + +- 横向分离(业务分隔后分离):将不同的业务模块分离部署 + +## 单一功能通过集群实现伸缩 +- 将相同服务部署在多台服务器上构成一个集群整体对外提供服务。 +- 具体来说,集群伸缩性又可分为应用服务器集群伸缩性和数据服务器集群伸缩性。这两周集群由于对数据状态管理的不同,技术实现也有很大区别。而数据服务器集群也可以分为缓存数据服务器集群和存储数据服务器集群,这两种集群的伸缩性也不同。 +# 应用服务器集群的伸缩性设计 +- 无状态服务器集群,主要依赖于负载均衡。 + +## 1)HTTP重定向负载均衡 +- 利用HTTP重定向协议实现负载均衡。 + +- HTTP重定向服务器是一台普通的应用服务器,其唯一的功能就是根据用户的HTTP请求计算一台真实的Web服务器的地址,并将该Web服务器地址写入HTTP重定向响应中返回给用户浏览器。 +- 优点是比较简单,缺点是浏览器需要两次请求服务器才能完成一次访问,性能较差;重定向服务器自身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限;使用HTTP302重定向,有可能使搜索引擎判断为SEO作弊,降低搜索排名。 +## 2)DNS域名解析负载均衡 +- 这是利用DNS处理域名解析请求的同时进行负载均衡处理的一种方案。 + +- 浏览器访问A记录,DNS服务器保存如下信息: +- www.mysite.com IN A 114.100.80.1 +- www.mysite.com IN A 114.100.80.2 +- www.mysite.com IN A 114.100.80.3 + +- 每次域名解析请求都会根据负载均衡算法计算一个不同的IP地址返回,这样多个服务器就构成一个集群,并可以实现负载均衡。 +- 优点是将负载均衡的工作转交给DNS,省掉了网站管理维护负载均衡服务器的麻烦,同时很多DNS还支持基于地理位置的解析,即会将域名解析成距离用户最近的一个服务器地址,可以加快用户访问速度。但是缺点是DNS是多级解析,每一级DNS都可能缓存A记录,当下线某台服务器后,即使修改DNS的A记录,要使其生效也需要较长时间。并且DNS负载均衡的控制权在域名服务商那里,网站无法对其做更多改善和更强大的管理。 +- 事实上,大型网站总是部分使用DNS域名解析,利用域名解析作为第一层负载均衡手段,即域名解析得到的一组服务器是同样提供负载均衡服务的内部服务器,这组内部负载均衡服务器再进行负载均衡,将请求分发到真实的Web服务器上。 +## 3)反向代理负载均衡 +- 利用反向代理服务器进行负载均衡。 + +- 反向代理服务器一般也提供负载均衡的概念,管理一组Web服务器。由于Web服务器不直接对外提供访问,因此Web服务器不需要使用外部IP地址,而反向代理服务器则需要配置双网卡和内部外部两套IP地址。 +- 由于反向代理服务器转发请求在HTTP协议层面,因此也叫应用层负载均衡,优点是和反向代理服务器功能集中在一起,部署简单,缺点是反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。 + +### 正向代理与反向代理 +- 正向代理隐藏了真实的客户端,反向代理隐藏了真实的服务器。 +### 反向代理与负载均衡 +- 现在许多大型web网站都用到反向代理。除了可以防止外网对内网服务器的恶性攻击、缓存以减少服务器的压力和访问安全控制之外,还可以进行负载均衡,将用户请求分配给多个服务器。 +## 4)IP负载均衡 +- 在网络层通过修改请求目标地址进行负载均衡。 + +- 用户请求IP数据报到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法的得到一台真实Web服务器,然后将数据目的IP地址修改为真实Web服务器的IP地址,不需要通过用户进程处理。真实Web服务器处理完毕后,响应数据包回到负载均衡服务器,负载均衡服务器再将数据包源地址修改为自身的IP地址发送给用户浏览器。 + +- 这里的关键在于Web服务器响应数据包如何返回给负载均衡服务器。一种方案是负载均衡服务器在修改目的IP地址的同时修改源地址,将数据包源地址设为自身IP,即源地址转换SNAT;另一种方案是将负载均衡服务器作为真实Web服务器集群的网关服务器,这样所有响应数据都会到达负载均衡服务器。 +- IP负载均衡在内核进程完成数据分发,较反向代理负载均衡(应用层负载均衡)有更好的处理性能,但是由于所有请求响应都需要经过负载均衡服务器,集群的最大响应数据吞吐量不得不受制于负载均衡服务器网卡带宽。对于提供下载服务或视频服务等需要传输大量数据的网站而言,难以满足需求。能否让负载均衡服务器只分发请求,而使响应数据从真实服务器直接返回给用户呢? +## 5)数据链路层负载均衡 +- 数据链路层负载均衡是指在通信协议的数据链路层修改MAC地址进行负载均衡。 + +- 这种数据传输方式又称为三角传输模式。负载均衡数据分发过程中不修改IP地址,只修改目的MAC地址,通过配置真实Web服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据报的源地址和目的地址就可以进行数据分发的目的。因为它们的IP地址是一致的(真实Web服务器IP==数据报目的IP),不需要通过负载均衡服务器进行地址转换,可以将响应数据包直接返回给浏览器。这种负载均衡方式又称为直接路由方式DR。 +- 使用三角传输模式的数据链路层负载均衡是目前大型网站使用最广的一种负载均衡手段。在Linux平台上最好的数据链路层负载均衡产品是LVS。 +## 四层负载均衡与七层负载均衡 +- 四层负载均衡,也就是主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 + +- 以常见的TCP为例,负载均衡设备在接收到第一个来自客户端的SYN 请求时,即通过上述方式选择一个最佳的服务器,并对报文中目标IP地址改为后端服务器IP,直接转发给该服务器。TCP的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。 +- 上面的IP负载均衡,数据链路层负载均衡都是属于四层负载均衡 + +- 七层负载均衡:也称为“内容交换”,主要通过报文中真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 + +- 以常见的HTTP为例,负载均衡设备要根据真正的应用层内容再选择服务器,必须先代理实际服务器和客户端建立连接(三次握手)后,才可能接受到客户端发送的真正应用层内容的报文,然后再根据该报文中的特定字段,加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 + +- 在这种情况下,负载类似于一个代理服务器与前端的客户端以及后端的服务器会分别建立TCP连接。所以,从技术原理上来看,七层负载均衡明显的对负载均衡设备的要求更高,设备性能消耗也更大。 +- 上面的反向代理负载均衡,属于七层负载均衡 + +## 负载均衡的演进整合 +### 硬负载均衡——F5 + +- 由于小型机和前面的 F5 负载均衡硬件都比较贵,所以出于可靠性、可维护性和成本的综合考虑,一般应用部署两套跑在两台小型机上,在前面共享一个 F5 做负载均衡。而一般 F5 和小型机这类硬件设备都至少是 5 个 9 的可靠性保障,所以整体的系统可靠性基本有保障。 + +- 进入互联网时代后,应用开发拥抱开源,部署使用更廉价的 PC Server 和免费开源的应用容器。负载均衡也逐步从硬负载向软负载变迁,由于互联网应用的海量特性和部署规模的急剧膨胀,前端负载均衡也开始变得丰富起来。 +### 软负载均衡——LVS+Keepalived + + +- 负载均衡(DNS/nginx/LVS/vipserver) +- 高可用集群(heardbead/keepalived ) +- heardbead与keepalived的区别,脑裂问题的解决,高可用方案的优劣 +- LVS四种模式 +### 两层负载均衡——LVS(+Keepalived)+Nginx + +- 为了方便做按域名的分流和适配切流量上线,中间又加了一层 Nginx。这样就变成了两层软负载结构了,LVS 负责 4 层(传输层),Nginx 负责 7 层(应用层)。 但 Nginx 只负责了单机内多实例的负载均衡,这里主要是因为当时 PC Server 是物理机,CPU 16/32 core,内存 32/64G 不等,为了更充分的利用资源,一台物理机上都部署了多个应用服务实例,而考虑到 Nginx 工作在 7 层的开销远高于 LVS/DR 模式,所以一般在一个 Nginx 后面挂的实例数也不会超过 10 个。 +### 三层负载均衡——LVS(+Keepalived)+HAProxy+Nginx + +- 随着业务发展和用户流量上升,机器规模也在不断扩张,导致一个网段内的 IP 都不够用了,这套负载结构又遇到了横向扩展的瓶颈,因为 LVS/DR 模式下跨不了网段。所以后来又在 LVS 和 Nginx 之间加了一层 HAProxy。其实加了 HAProxy 之后,它也是工作在 7 层(应用层),这样 Nginx 这层看起来就不是很有必要。但三层的负载结构能支撑更大规模的集群,而原本在 Nginx 层做了一套方便研发切流量上线的运维管理系统,所以牺牲一点性能换取现在的可维护性和将来扩展性,Nginx 这层就一直保留下来了。而且 Nginx 相比 HAProxy 不是纯粹的负载均衡器,它还能提供 cache 功能,对于某些 HTTP 请求实际只走到 Nginx 这层就可以通过缓存命中而返回。 +### 四层负载均衡——DNS+LVS(+Keealived)+HAProxy+Nginx + +- 随着业务发展,公司开始了多个 IDC 的建设,考虑到 IDC 级别的容灾,集群开始部署到多个 IDC。跨 IDC 的负载均衡方案可以简单通过 DNS 轮询来实现,但可控性不好。所以我们没有采用这种,而是采用一主加多子域名的方式来基于业务场景实现动态域名调度和负载。主域名下实际是一个动态流量调度器,跨多个 IDC 部署,对于 HTTP 请求基于重定向方式跳子域名,对于 TCP 方式每次建立长连接前请求分配实际连接的子域名。 +### 五层负载均衡——CDN+DNS+LVS(+Keepalived)+HAProxy+Nginx +- 最后再加上互联网应用必不可少的 CDN 将静态资源请求的负载分流,那么整个负载的层次结构就完整了。 +### SSL带来的负载均衡变化 +- 随着互联网的普及,安全问题益发严重,原本早期只有银行网银等使用 HTTPS 方式访问,现在电商类网站也开始启用全站 HTTPS 了。引入 SSL 后对负载结构带来了什么影响么?SSL 属于应用层的协议,所以只能在 7 层上来做,而 HAProxy 也是支持 SSL 协议的,所以一种方式是只需简单的让 HAProxy 开启 SSL 支持完成对内解密对外加密的处理。 + +- 但 HAProxy 的作者不太赞同这种方案,因为引入 SSL 处理是有额外的性能开销的。那么在承担确定流量的情况下,假设原本需要 M 台 HAProxy,在开启了 SSL 后可能需要 M + N 台 HAProxy。随着流量增长,这种方式的横向扩展成本较高(毕竟 SSL 证书按服务器数量来收费的)。他给出的解决方案是再独立一层 SSL 代理缓存层,像下面这样。 + +- L4 和 L7 之间独立的 SSL 代理缓存层只负责 SSL 协议的处理,把 HTTPS 转换成 HTTP,并检查本地缓存是否命中。若未命中再转发请求到后端的 L7 层应用负载均衡层。这样的好处是每个层次都可以根据流量来独立伸缩,而且 SSL 层显然可以跨多个应用共享,更节省成本。如果按这个思路来重新调整我们前面的负载均衡结构层次,将会演变成下面这样。 + +- 其实,这时我觉得应用前面的那层 Nginx 可能就显得多余了点,不是必需的。但如果现实这么演进下来很可能就会有这么一层冗余的东西存在很长一段时间,这就是理想和现实之间的差距吧。 +## 负载均衡算法 +### 轮询(Round Robin,RR) +- 所有请求被依次分发到每台应用服务器上,即每台服务器需要处理的请求数目都相同,适用于所有服务器硬件都相同的场景。 +### 加权轮询(Weighted Round Robin,WRR) +- 根据应用服务器硬件性能的情况,在轮询的基础上,按照配置的权重将请求分发到每个服务器,高性能的服务器能分配更多请求。 +### 随机(Random) +- 请求被随机分配到各个应用服务器,在许多场合下,这个方案都很简单实用,因为好的随机数本身就很均衡。即使应用服务器硬件配置不同,也可以使用加权随机算法。 +### 最少连接(Least Connections) +- 记录每个应用服务器正在处理的连接数/请求数,将新到的请求分发到最少连接的服务器上。 +### 源地址散列(Source Hashing) +- 根据请求来源的IP地址进行Hash计算,得到应用服务器,这样来自同一个IP地址的请求总会在同一个服务器上处理,该请求的上下文信息可以存储在这台应用服务器上,这一个会话周期内重复使用,实现会话粘滞(Session Sticky)。 + +# 分布式缓存集群的伸缩性设计 +- 与应用服务器集群的伸缩性设计不同,分布式缓存服务器集群中不同服务器中缓存的数据不同,缓存访问请求不可以在缓存服务器集群中的任意一台处理,必须先找到有所需数据的缓存服务器,然后才能访问。这个特点会严重制约分布式缓存服务器集群的伸缩性设计,因为新上线的缓存服务器没有缓存任何数据,而已下线的缓存服务器上还缓存着许多热点数据。 +- 必须让新上线的缓存服务器对整个分布式缓存集群影响最小,也就是说新加入缓存服务器后应使整个缓存服务器集群中已经缓存的数据尽可能还被访问到,这是分布式缓存集群伸缩性设计的主要目标。 +- 使用传统哈希算法(比如余数)的话,假设是3台服务器扩容至4台服务器,大约有3/4被缓存的数据不能命中,随着服务器集群规模的扩大,这个比例线性上升。 +- 一致性哈希算法可以解决这个问题。 +## 一致性哈希 +- 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。 +- 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义: +- 1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。 +- 2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 +- 3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 +- 4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。 + +- 环形Hash空间 +- 按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即【0,2^32-1】的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。然后根据需要缓存的数据的Key值计算其hash值,然后在Hash环上顺时针查找距离这个key的hash值最近的节点,完成key到节点的hash映射查找。 + +- 一致性哈希所带来的最大变化是把节点对应的哈希值变成了一个范围。我们会把整个哈希值的范围定义得非常大,然后把这个范围分配给现有的节点。如果有节点加入,那么这个新节点会从原有的某个节点上分管一部分范围的哈希值;如果有节点退出,那么这个节点原来管理的哈希值会给它的下一个节点来管理。 +- 3台服务器扩容至4台服务器,可以继续命中原有缓存数据的概率是75%。随着集群规模的扩大,继续命中原有缓存数据的概率也逐渐增大。 + +- 具体应用中,这个长度为2^32的一致性Hash环通常使用二叉查找树来实现,Hash查找过程实际上是在二叉查找树中查找不小于查找树的最小数值。当然这个二叉树的最右边叶子节点与最左边的叶子节点相连接,构成环。 + +- 仍存在问题:新增节点时,除了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载是明显比其他节点低的;减少节点时,除了减去的节点外,只有一个节点受影响,它要承担自己原来的和减去的节点的工作,压力明显比其他结点要高。 +## 虚拟节点对一致性哈希的改进 +- 每个虚拟节点支持连续的哈希环上的一段,这时如果加入一个物理节点,相应就会加入很多虚拟节点,这些新的虚拟节点是相对均匀地插入到整个哈希环上的,这样,就可以很好地分担现有物理节点的压力了;如果减少一个物理节点,对应的很多虚拟节点就会失效,这样,就会有很多剩余的虚拟节点来承担之前虚拟节点的工作,但对于物理节点来说,增加的负载相对是均衡的。所以可以通过一个物理节点对应非常多的虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布的方式来解决增加或减少节点时负载不均衡的情况。 + + +- 新加入节点NODE3对应的一组虚拟节点为V30,V31,V32,加入到一致性Hash环上后,影响V01,V12,V22三个虚拟节点,而这三个虚拟节点分别对应NODE0,NODE1,NODE2三个物理节点。因此加入一个节点会影响集群中已存在的三个节点。 +## 分片 +- 分片将哈希环切割为相同大小的分片,然后将这些分片交给不同的节点负责。注意这里跟上面提到的虚拟节点有着很本质的区别,分片的划分和分片的分配被解耦,一个节点退出时,其所负责的分片并不需要顺时针合并给之后节点,而是可以更灵活的将整个分片作为一个整体交给任意节点,实践中,一个分片多作为最小的数据迁移和备份单位。 + +- 而也正是由于上面提到的解耦,相当于将原先的key到节点的映射拆成两层,需要一个新的机制来进行分片到存储节点的映射,由于分片数相对key空间已经很小并且数量确定,可以更精确地初始设置,并引入中心目录服务来根据节点存活修改分片的映射关系,同时将这个映射信息通知给所有的存储节点和客户端。 + +- 常见的存储系统大多采用类似于分片的数据分布和定位方式: + +- Dynamo及Cassandra采用分片的方式并通过Gossip在对等节点间同; +- Redis Cluster将key space划分为slots,同样利用Gossip通信; +- Zeppelin将数据分片为Partition,通过Meta集群提供中心目录服务; +- Bigtable将数据切割为Tablet,类似于可变的分片,Tablet Server可以进行分片的切割,最终分片信息记录在Chubby中; +- Ceph采用CRUSH方式,由中心集群Monitor维护并提供集群拓扑的变化。 + +- + +# 数据存储服务器集群的伸缩性设计 +- 分库分表 +- 扩容时数据迁移可以利用一致性Hash算法。路由模块使用一致性Hash算法进行路由,尽量使需要迁移的数据最少。但是迁移数据需要遍历数据库中每条记录的索引,重新进行路由计算确定其是否需要迁移,这会对数据库访问造成压力,并且需要解决迁移过程中数据的一致性、可访问性、迁移过程中服务器宕机时的可用性等诸多问题。 +# 扩展性 +- 主要依赖于分布式消息队列和服务化。 +# 分布式消息队列 +- 见《面试---8.分布式理论---组件》 +# SOA +## 1、简介 +- SOA是面向服务架构,它强调系统之间以标准的服务方式进行交互,各系统可采用不同的语言、不同的框架来实现,交互则全部通过服务的方式进行。 +## 2、实现 +- SCA +- ESB +- Tuscany +- Mule +# 微服务 +- 微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部,实现真正的组件化。 +- 微服务只是一种为经过良好架构设计的SOA解决方案,是面向服务的交付方案。 +- 微服务更趋向于以自治的方式产生价值。 +- 微服务与敏捷开发的思想高度结合在一起,服务的定义更加清晰,同时减少了企业ESB开发的复杂性。 +- 微服务是soa思想的一种提炼! +- SOA是重ESB,微服务是轻网关。 + +- 微服务可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程。 +- 微服务不需要像普通服务那样成为一种独立的功能或者独立的资源。定义中称,微服务是需要与业务能力相匹配,这种说法完全正确。不幸的是,仍然意味着,如果能力模型粒度的设计是错误的,那么,我们就必须付出很多代价。如果你阅读了Fowler的整篇文章,你会发现,其中的指导建议是非常实用的。在决定将所有组件组合到一起时,开发人员需要非常确信这些组件都会有所改变,并且规模也会发生变化。服务粒度越粗,就越难以符合规定原则。服务粒度越细,就越能够灵活地降低变化和负载所带来的影响。然而,利弊之间的权衡过程是非常复杂的,我们要在配置和资金模型的基础上考虑到基础设施的成本问题。 + +- Hystrix服务熔断、Zuul服务网关、Ribbon客户端负载均衡、Stream消息驱动、Eureka服务发现、Config配置中心、Consul服务注册 + +# 安全性 +- 可以参考《面试---5. JavaWeb&HTTP&安全》 +# 网站攻防 +## XSS +## SQL注入 +## CSRF +## 其他 +## Web应用防火墙 +# 信息加密与密钥管理 +## 单向散列加密 +## 对称加密 +## 非对称加密 +## 密钥安全管理 +# 信息过滤与反垃圾 +## 文本匹配 +- 敏感词过滤 +- 可以使用正则表达式,但效率较低。 +- 一般采用Trie树的变种。 + +## 分类算法 +- 贝叶斯分类算法 +## 黑名单 +- hash表,但是会占用很多内存,如果hash表过大就无法解决了。 +### 布隆过滤器 +- 通过一个二进制列表和一组随机数映射函数实现。 + + - 如果需要处理10亿邮件列表黑名单,在内存中建立一个2GB大小的存储空间,即16G个bit,并全部初始化为0。要将一个邮箱地址加入黑名单时,使用8个随机映射函数(F1~F8)得到0~16G范围内的8个随机数,从而将该邮箱地址勇摄到16Gbit的8个位置上,然后将这些位置值1。当要检查一个邮箱地址是否在黑名单时,使用同样的映射函数,得到16G空间8个位置上的bit,如果这些值都为1,那么该邮箱地址在黑名单上。 +- 处理同样数量的信息,布隆过滤器只需要哈希表所需内存的1/8。 +- 缺点是可能会误算,比如一个邮箱地址映射的8个bit正好都被其他邮箱地址设为1了。这种可能性极小,但如果需要精确的判断,则不适合布隆过滤器。 + +- 底层使用的是位图。当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了: +- 1、如果这些点有任何一个 0,则被检索元素一定不在; +- 2、如果都是 1,则被检索元素很可能在。 +- 布隆过滤器的优点 : 空间效率和查询时间都远远超过一般的算法,布隆过滤器存储空间和插入 / 查询时间都是常数O(k)。另外, 散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。 +- 布隆过滤器的缺点:误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。(误判补救方法是:再建立一个小的白名单,存储那些可能被误判的信息。) +- 另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。 + +- + +# 大型网站典型故障案例分析 +# 写日志也会引发故障 +- log等级设置较低导致大量写入log。 +- 经验教训: + - 1)应用程序自己的日志输出配置和第三方组件日志输出要分别配置 + - 2)检查log配置文件,日志输出级别至少为WARN + - 3)有可能需要关闭第三方库的日志输出 +# 高并发访问数据库引发的故障 +- 某条SQL频繁执行导致数据库Load居高不下 +- 经验教训: + - 1)首页不应该访问数据库,首页需要的数据可以从缓存服务器或者搜索引擎服务获取 + - 2)首页最好是静态的 +# 高并发情况下锁引发的故障 +- 某个单例对象使用了synchronized(this)导致排队。 +- 经验教训:使用锁要谨慎。 +# 缓存引发的故障 +- 缓存服务不受重视,缓存服务器被失误关闭,导致数据库崩溃。 +- 经验教训:缓存已经成为网站架构不可或缺的一部分时,对缓存的管理就需要提高到和其他服务器一样的级别。 +# 应用启动不同步引发的故障 +- 后台服务准备好之前,前台应用就启动了,导致大量请求阻塞。 +- 经验教训:在应用程序中加入一个特定的动态页面,启动脚本先启动后台服务,然后在脚本中不断用curl命令访问这个特定页面,直到可以访问成功,才启动前台应用。 +# 大文件读写独占磁盘引发的故障 +- 数百M的大文件读写时独占磁盘,导致其他用户的文件操作缓慢。 +- 经验教训:存储的使用需要根据不同文件类型和用途进行管理,小文件和大文件应该使用专门的存储服务器。 +# 滥用生产环境引发的故障 +- 线上生产环境进行压测导致网站访问延迟过高。 +- 经验教训:访问线上生产环境要规范,不小心就会导致大事故 +# 不规范的流程引发的故障 +- 注释代码后在上线前忘记把注释去掉,直接提交代码库被发布到线上环境 +- 经验教训:代码提交前使用diff命令进行代码比较,确认没有提交不该提交的代码; +- 加强code review,代码在正式提交前必须被至少一个其他工程师做过code review,并且共同承担因代码引起的故障责任。 From fba60309038a667a1bf7b385b6faa9b98ffc7e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:42:07 +0800 Subject: [PATCH 75/97] =?UTF-8?q?Create=20=E5=8D=81=E4=BA=8C=E3=80=81?= =?UTF-8?q?=E5=88=86=E5=B8=83=E5=BC=8F=E7=90=86=E8=AE=BA---=E7=BB=84?= =?UTF-8?q?=E4=BB=B6.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...350\256\272---\347\273\204\344\273\266.md" | 1675 +++++++++++++++++ 1 file changed, 1675 insertions(+) create mode 100644 "docs/\345\215\201\344\272\214\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\347\273\204\344\273\266.md" diff --git "a/docs/\345\215\201\344\272\214\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\347\273\204\344\273\266.md" "b/docs/\345\215\201\344\272\214\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\347\273\204\344\273\266.md" new file mode 100644 index 00000000..3bdc59c7 --- /dev/null +++ "b/docs/\345\215\201\344\272\214\343\200\201\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272---\347\273\204\344\273\266.md" @@ -0,0 +1,1675 @@ +# 分布式存储系统 +- 分布式存储系统包括分布式文件系统、NoSQL和分布式数据库。 +# 分布式文件系统 +- 对一些图片、大文本的存储,使用数据库就不合适了。可以考虑的一个方案是NAS网络存储设备。它是一种专用数据存储服务器。它以数据为中心,将存储设备与服务器彻底分离,集中管理数据,从而释放带宽、提高性能、降低总拥有成本、保护投资。其成本远远低于使用服务器存储,而效率却远远高于后者。 +- 不过NAS本身的IO吞吐性能及扩展性在大型网站中会表现出明显的不足,另外的一个方案是分布式文件系统。 +- 比如GFS,HDFS,FastDFS。 +# 块存储&文件存储&对象存储 +# 块存储 +- 块级是指以扇区为基础,一个或连续的扇区组成一个块,也叫物理块。它是在文件系统与块设备(例如:磁盘驱动器)之间。 +- 典型设备:磁盘阵列,硬盘,虚拟硬盘 +- 存储方案: + - 1) DAS(Direct Attach STorage):是直接连接于主机服务器的一种储存方式,每一台主机服务器有独立的储存设备,每台主机服务器的储存设备无法互通,需要跨主机存取资料时,必须经过相对复杂的设定,若主机服务器分属不同的操作系统,要存取彼此的资料,更是复杂,有些系统甚至不能存取。通常用在单一网络环境下且数据交换量不大,性能要求不高的环境下,可以说是一种应用较为早的技术实现。 + + - 2)SAN(Storage Area Network):是一种用高速(光纤)网络联接专业主机服务器的一种储存方式,此系统会位于主机群的后端,它使用高速I/O 联结方式, 如 SCSI, ESCON 及 Fibre- Channels。一般而言,SAN应用在对网络速度要求高、对数据的可靠性和安全性要求高、对数据共享的性能要求高的应用环境中,特点是代价高,性能好。例如电信、银行的大数据量关键应用。它采用SCSI 块I/O的命令集,通过在磁盘或FC(Fiber Channel)级的数据访问提供高性能的随机I/O和数据吞吐率,它具有高带宽、低延迟的优势,在高性能计算中占有一席之地,但是由于SAN系统的价格较高,且可扩展性较差,已不能满足成千上万个CPU规模的系统。 +# 文件存储 +- 文件级是指文件系统,单个文件可能由于一个或多个逻辑块组成,且逻辑块之间是不连续分布。逻辑块大于或等于物理块整数倍。扇区→物理块→逻辑块→文件系统 +- 通常,NAS产品都是文件级存储。 + + + +# 对象存储 +- 对象存储同兼具SAN高速直接访问磁盘特点及NAS的分布式共享特点。 + +- 核心是将数据通路(数据读或写)和控制通路(元数据)分离,并且基于对象存储设备(Object-based Storage Device,OSD)构建存储系统,每个对象存储设备具有一定的智能,能够自动管理其上的数据分布。 +- 对象存储结构组成部分(对象、对象存储设备、元数据服务器、对象存储系统的客户端): +## 对象 +- 对象是系统中数据存储的基本单位,一个对象实际上就是文件的数据和一组属性信息(Meta Data)的组合,这些属性信息可以定义基于文件的RAID参数、数据分布和服务质量等,而传统的存储系统中用文件或块作为基本的存储单位,在块存储系统中还需要始终追踪系统中每个块的属性,对象通过与存储系统通信维护自己的属性。在存储设备中,所有对象都有一个对象标识,通过对象标识OSD命令访问该对象。通常有多种类型的对象,存储设备上的根对象标识存储设备和该设备的各种属性,组对象是存储设备上共享资源管理策略的对象集合等。 +## 对象存储设备 +- 对象存储设备具有一定的智能,它有自己的CPU、内存、网络和磁盘系统,OSD同块设备的不同不在于存储介质,而在于两者提供的访问接口。OSD的主要功能包括数据存储和安全访问。目前国际上通常采用刀片式结构实现对象存储设备。OSD提供三个主要功能: + - (1) 数据存储。OSD管理对象数据,并将它们放置在标准的磁盘系统上,OSD不提供块接口访问方式,Client请求数据时用对象ID、偏移进行数据读写。 + - (2) 智能分布。OSD用其自身的CPU和内存优化数据分布,并支持数据的预取。由于OSD可以智能地支持对象的预取,从而可以优化磁盘的性能。 + - (3) 每个对象元数据的管理。OSD管理存储在其上对象的元数据,该元数据与传统的inode元数据相似,通常包括对象的数据块和对象的长度。而在传统的NAS系统中,这些元数据是由文件服务器维护的,对象存储架构将系统中主要的元数据管理工作由OSD来完成,降低了Client的开销。 +## 元数据服务器(Metadata Server,MDS) +- MDS控制Client与OSD对象的交互,主要提供以下几个功能: + - (1) 对象存储访问。 +- MDS构造、管理描述每个文件分布的视图,允许Client直接访问对象。MDS为Client提供访问该文件所含对象的能力,OSD在接收到每个请求时将先验证该能力,然后才可以访问。 + - (2) 文件和目录访问管理。 +- MDS在存储系统上构建一个文件结构,包括限额控制、目录和文件的创建和删除、访问控制等。 + - (3) Client Cache一致性。 +- 为了提高Client性能,在对象存储系统设计时通常支持Client方的Cache。由于引入Client方的Cache,带来了Cache一致性问题,MDS支持基于Client的文件Cache,当Cache的文件发生改变时,将通知Client刷新Cache,从而防止Cache不一致引发的问题。 +## 对象存储系统的客户端Client +- 为了有效支持Client支持访问OSD上的对象,需要在计算节点实现对象存储系统的Client,通常提供POSIX文件系统接口,允许应用程序像执行标准的文件系统操作一样。 + +- + +# GFS +- HDFS就是基于Java的类GFS的实现。 + + + +# NoSQL +- NoSQL涵盖的范围很广,基本上处于分布式文件系统和关系型数据库之间的系统都被归为NoSQL的范畴。 +# NoSQL数据模型 + +# Key-Value +- 键值对,没有办法进行高效的范围查询 +# Ordered Key-Value +- Key是有序的,可以解决基于Key的范围查询的效率问题。不过在这个模型中,Value本身的内容和结构是由应用来负责解析和存储的,如果在多个应用中去使用的话,这种方式并不直观也不方便。 + +# BigTable +- BigTable对Value进行了Schema的支持,Value是由多个Column Family组成,Column Family内部是Column,Column Family不能动态扩展,而Column Family内部的Column是可以动态扩展的。 +# Document +- Document数据库有两个非常大的进步,一个是可以在Value中自定义复杂的Schema,而不再仅仅是Map的嵌套;另一个是对索引的支持 +# Graph +- 图数据库可以看作是从Ordered Key-Value数据库发展而来的一个分支。主要是支持图结构的数据模型。 + +- + +# 分布式数据库 +# 分布式数据库解决方案 +- RDBMS -> NoSQL -> NewSQL + +- 在RDBMS基础上有一些分布式数据库中间件,比如MyCat;另一种则是新型数据库,称为NewSQL,一般是兼容某一种RDBMS以保证用户使用无障碍(比如兼容MySQL),比如TiDB。 + + +- TiDB +- TiDB 是新一代开源分布式 NewSQL 数据库,模型受 Google Spanner / F1 论文的启发, 实现了自动的水平伸缩,强一致性的分布式事务,基于 Raft 算法的多副本复制等重要 NewSQL 特性。 TiDB 结合了 RDBMS 和 NoSQL 的优点,部署简单,在线弹性扩容和异步表结构变更不影响业务, 真正的异地多活及自动故障恢复保障数据安全,同时兼容 MySQL 协议,使迁移使用成本降到极低。 +- + +# 中间件的局限性 +## 性能 +- 基于 MySQL 的方案它的天花板在哪里,它的天花板特别明显。有一个思路是能不能通过 MySQL 的 server 把 InnoDB 变成一个分布式数据库,听起来这个方案很完美,但是很快就会遇到天花板。因为 MySQL 生成的执行计划是个单机的,它认为整个计划的 cost 也是单机的,我读取一行和读取下一行之间的开销是很小的,比如迭代 next row 可以立刻拿到下一行。实际上在一个分布式系统里面,这是不一定的。 + +- 另外,你把数据都拿回来计算这个太慢了,很多时候我们需要把我们的 expression 或者计算过程等等运算推下去,向上返回一个最终的计算结果,这个一定要用分布式的 plan,前面控制执行计划的节点,它必须要理解下面是分布式的东西,才能生成最好的 plan,这样才能实现最高的执行效率。 + +- 比如说你做一个 sum,你是一条条拿回来加,还是让一堆机器一起算,最后给我一个结果。 例如我有 100 亿条数据分布在 10 台机器上,并行在这 10台机器我可能只拿到 10 个结果,如果把所有的数据每一条都拿回来,这就太慢了,完全丧失了分布式的价值。聊到 MySQL 想实现分布式,另外一个实现分布式的方案就是 Proxy。但是 Proxy 本身的天花板在那里,就是它不支持分布式的 transaction,它不支持跨节点的 join,它无法理解复杂的 plan,一个复杂的 plan 打到 Proxy 上面,Proxy 就傻了,我到底应该往哪一个节点上转发呢,如果我涉及到 subquery sql 怎么办?所以这个天花板是瞬间会到,在传统模型下面的修改,很快会达不到我们的要求。 +## 高可用(运维) +- 另外一个很重要的是,MySQL 支持的复制方式是半同步或者是异步,但是半同步可以降级成异步,也就是说任何时候数据出了问题你不敢切换,因为有可能是异步复制,有一部分数据还没有同步过来,这时候切换数据就不一致了。前一阵子出现过某公司突然不能支付了这种事件,今年有很多这种类似的 case,所以微博上大家都在说“说好的异地多活呢?”…… +- 为什么传统的方案在这上面解决起来特别的困难,天花板马上到了,基本上不可能解决这个问题。另外是多数据中心的复制和数据中心的容灾,MySQL 在这上面是做不好的。 + +## SQL支持 +- 基于中间件来进行分库, 确实对 SQL 有阉割的情况,并不是所有sql都能够支持。主要原因是数据被拆分了。而数据一旦被拆分到多个节点,则: 1.复杂的join查询2. 同时更新多个数据库节点的sql语句这两类SQL的支持难度,就比较高。这也是目前市面上所有中间件都无法满足的两点。复杂的join查询之所以难以支持,是因为要跨节点join;同时更新多个节点的sql难以支持,是因为很难解决多个节点的并发一致性问题。但是除了这两点之外,其他的sql类型,一款中间件是能够努力做到的。 + +- + +# NoSQL的局限性 +- 无法支持ACID,强一致性事务 +- 不支持SQL,需要手工设计数据分布与查询 +- + +# 数据库从单机到分布式的挑战和应对 +# 垂直拆分 +- 将同一个库中的表分到不同的库中 +- 垂直拆分的影响: + - 1)单机ACID被打破,要么放弃原来的单机事务,修改实现,要么引入分布式事务 + - 2)一些JOIN操作会变得比较困难,需要应用或者其他方式来解决 + - 3)靠外键去进行约束的场景会受影响 +# 水平拆分 +- 水平拆分会带来如下影响: + - 1)ACID被打破 + - 2)JOIN操作被影响 + - 3)靠外键去进行约束的场景会有影响 + - 4)依赖单库的自增序列生成唯一ID会受影响 + - 5)针对单个逻辑意义上的表的查询要跨库了 +- + +# 分布式事务 +## 2PC(2-Phase-Commit) +### 介绍 +- 是在分布式环境下保证事务原子性和一致性设计的算法,也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前,绝大多数数据库都是采用2PC协议来完成分布式事务处理的。 +- 它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(协调者),其他参与决议节点称为参与者(参与者s, 或cohorts)。 + + +- 另一种情况: + +- 回滚所有资源。 +#### 阶段一:提交事务请求 投票阶段 +- 1、事务询问 +- 协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 +- 2、执行事务 +- 各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中。 +- 3、各参与者向协调者反馈事务询问的响应 +- 如果参与者成功执行了事务操作,那么就反馈给协调者yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者no响应,表示事务不可以执行。 +#### 阶段二:执行事务提交 执行阶段 +- 在阶段二中,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包括以下两种可能: +##### 执行事务提交 +- 假如协调者从所有的参与者获得反馈都是yes,那么就会执行事务提交。 +- 1、发送提交请求 +- 协调者向所有参与者节点发出commit请求 +- 2、事务提交 +- 参与者接收到commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。 +- 3、反馈事务提交结果 +- 参与者在完成事务提交之后,向协调者发送ACK消息。 +- 4、完成事务 +- 协调者接收到所有参与者反馈的ACK消息,完成事务 + +##### 中断事务 +- 假如任何一个参与者向协调者反馈了no响应,或者在等待超时之后,协调者无法接收到所有参与者的反馈响应,那么就会中断事务。 +- 1、发送回滚请求 +- 协调者向所有参与者节点发出rollback请求 +- 2、事务回滚 +- 参与者接收到rollback请求后,会利用其在阶段一中记录的Undo信息来中事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。 +- 3、反馈事务回滚结果 +- 参与者在完成事务回滚之后,向协调者发送ACK消息 +- 4、中断事务 +- 协调者接收到所有参与者反馈的ACK消息,完成事务中断。 + +### 优缺点 +- 优点是原理简单,实现方便 +- 缺点:同步阻塞、单点问题、数据不一致、太过保守 +#### 同步阻塞 +- 两阶段提交中的第二阶段, 协调者需要等待所有参与者发出yes请求, 或者一个参与者发出no请求后, 才能执行提交或者中断操作. 这会造成长时间同时锁住多个资源, 造成性能瓶颈, 如果参与者有一个耗时长的操作, 性能损耗会更明显. +#### 单点问题 +- 协调者的角色在整个2PC中起到了非常重要的作用。一旦协调者出现问题,那么整个2PC流程将无法运转。如果协调者是在阶段二出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。 +#### 数据不一致/脑裂 +- 在2PC的阶段二,即在执行事务提交的时候,当协调者向所有的参与者发送commit请求之后,发生了局部网络异常或者是协调者在尚未完全发送完commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了commit请求。于是,这部分收到了commit请求的参与者就会进行事务的提交,而其他没有收到commit请求的参与者则无法进行事务提交,于是出现数据不一致的情况。 +#### 太过保守 +- 如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。2PC没有涉及较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。 + +- + +### XA 基于2PC的分布式事务规范 +- X/Open组织提出了一个分布式事务的规范——XA。了解XA先要了解X/Open组织定义的分布式事务处理模型——X/Open DTP(X/Open Distributed Transaction Processing Reference Model)模型。在该模型中定义了三个组件,即Application Progream,Resource Manager和Transaction Manager。 +- AP:应用程序,可以理解为使用DTP模型的程序,它定义了事务边界,并定义了构成该事务的应用程序的特点操作 +- RM:资源管理器,可以理解为一个DBMS系统。应用程序通过资源管理器对资源进行控制,资源必须实现XA定义的接口,资源管理器提供了存储共享资源的支持。 +- TM:事务管理器,负责协调和管理事务,提供给AP应用程序编程接口并管理资源管理器。事务管理器向事务指定标识,监视它们的进程,并负责处理事务的完成和失败。事务分支标识(XID)由TM指定,以标识一个RM内的全局事务和指定分支。它是TM中日志与RM中日志之间的相关标记,2PC提交或回滚时需要XID,以便在系统启动时执行再同步操作,或在需要时允许管理员执行试探操作。 +- 在这三个组件中,AP可以与TM、RM通信,TM与RM之间可以互相通信。DTP模型中定义了XA接口,TM和RM通过XA接口进行双向的通信。 + +- AP和RM是一定需要的,而事务管理器TM是我们额外引入的。之所以要引入TM,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。RM控制着全局事务,管理事务的生命周期,并协调资源。 +- DTP中还定义了几个概念: + - 1)事务:一个事务是一个完整的工作单位,由多个独立的计算任务组成,这多个任务在逻辑上是原子的 + - 2)全局事务:一次性操作多个RM的事务就是全局事务 + - 3)分支事务:在全局事务中,每一个RM有自己独立的任务,这些任务的集合就是这个资源管理器的分支任务 + - 4)控制线程:用来表示一个工作线程,主要是关联AP、TM和RM三者的线程,也就是事务上下文环境。 + + - 1)AP和RM之间,可以使用RM自身提供的native API进行交互,这种方式就是使用RM的传统方式,并且这个交互不在TM的管理范围内。另外,当AP和RM之间需要进行分布式事务时,AP需要得到对RM的连接(由TM管理),然后使用XA的native API来进行交互。 + - 2)AP和TM之间,使用的是TX接口,它用于对事务进行控制,包括启动事务、提交事务和回滚事务。 + - 3)TM和RM之间是通过XA接口进行交互的。TM管理到了RM的连接,并实现了2PC +### 实现 +- Atomikos +- 在JavaEE平台下,WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而在Tomcat下是没有实现的,这就需要借助第三方的框架Jotm、Automikos等来实现,两者均支持spring事务整合。 + +## TCC(Try-Confirm-Commit) +### 介绍 +- TCC, 是基于补偿型事务的AP系统的一种实现, 具有最终一致性. +下面以客户购买商品时的付款操作为例进行讲解: + +- Try: +- 完成所有的业务检查(一致性),预留必需业务资源(准隔离性); +- 体现在本例中, 就是确认客户账户余额足够支付(一致性), 锁住客户账户, 商户账户(准隔离性). +- Confirm: +- 使用Try阶段预留的业务资源执行业务(业务操作必须是幂等的), 如果执行出现异常, 要进行重试. +- 在这里就是执行客户账户扣款, 商户账户入账操作. +- Cancle: +- 释放Try阶段预留的业务资源, 在这里就是释放客户账户和商户账户的锁; +- 如果任一子业务在Confirm阶段有操作无法执行成功, 会造成对业务活动管理器的响应超时, 此时要对其他业务执行补偿性事务. 如果补偿操作执行也出现异常, 必须进行重试, 若实在无法执行成功, 则事务管理器必须能够感知到失败的操作, 进行log(用于事后人工进行补偿性事务操作或者交由中间件接管在之后进行补偿性事务操作). + + + +### 优点 +- 对比与前面提到的两阶段提交法, 有两大优势: + +- TCC能够对分布式事务中的各个资源进行分别锁定, 分别提交与释放, 例如, 假设有AB两个操作, 假设A操作耗时短, 那么A就能较快的完成自身的try-confirm-cancel流程, 释放资源. 无需等待B操作. 如果事后出现问题, 追加执行补偿性事务即可. +- TCC是绑定在各个子业务上的(除了cancel中的全局回滚操作), 也就是各服务之间可以在一定程度上”异步并行”执行。 +### 适用场景 +- • 严格一致性 +- • 执行时间短 +- • 实时性要求高 +- 举例: 红包, 收付款业务. +### 实现 +- https://github.com/liuyangming/ByteTCC +## 异步确保型/可靠消息最终一致(基于消息中间件,要求MQ支持事务消息->目前阿里闭源版MQ支持) +### 介绍 + +- 通过将一系列同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响. +- 这个方案真正实现了两个服务的解耦, 解耦的关键就是异步消息和补偿性事务。 + +- 执行步骤如下: + +- MQ发送方发送远程事务消息到MQ Server; +- MQ Server给予响应, 表明事务消息已成功到达MQ Server. +- MQ发送方Commit本地事务. +- 若本地事务Commit成功, 则通知MQ Server允许对应事务消息被消费; 若本地事务失败, 则通知MQ Server对应事务消息应被丢弃. +- 若MQ发送方超时未对MQ Server作出本地事务执行状态的反馈, 那么需要MQ Server向MQ发送方主动回查事务状态, 以决定事务消息是否能被消费. +- 当得知本地事务执行成功时, MQ Server允许MQ订阅方消费本条事务消息. +- 需要额外说明的一点, 就是事务消息投递到MQ订阅方后, 并不一定能够成功执行. 需要MQ订阅方主动给予消费反馈(ack) + +- 如果MQ订阅方执行远程事务成功, 则给予消费成功的ack, 那么MQ Server可以安全将事务消息移除; +- 如果执行失败, MQ Server需要对消息重新投递, 直至消费成功. +### 适用场景 +- • 执行周期较长 +- • 实时性要求不高 +### 实现 +- RocketMQ +- + +## 最大努力通知型(基于消息中间件,定期校对) +### 介绍 + +- 这是分布式事务中要求最低的一种, 也可以通过消息中间件实现, 与前面异步确保型操作不同的一点是, 在消息由MQ Server投递到消费者之后, 允许在达到最大重试次数之后正常结束事务. + +- 1.业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。 +- 2.主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。 +- 3.主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。 +- 4.业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。 +- 5.如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。 +### 适用场景 +- 交易结果消息的通知等. +### 实现 +- 相比于可靠消息最终一致方案,最大努力通知方案设计上比较简单,主要是由两部分构成。 +- 1.实时消息服务(MQ):接收主动方发送的MQ消息。 +- 2.通知服务子系统:监听MQ消息,当收到消息后,向被动方发送通知(一般是URL方式),同时生成通知记录。如果没有接收到被动方的返回消息,就根据通知记录进行重复通知。 + +- 最大努力通知方案实现方式比较简单,本质上就是通过定期校对,适用于数据一致性时间要求不太高的场合,其实不把它看作是分布式事务方案,只认为是一种跨平台的数据处理方案也是可以的。 +- + +# 多机Sequence/分布式全局唯一序列号 + - 1)唯一性 + - 2)连续性 +- 如果仅考虑ID的唯一性,那么可以使用UUID,但是连续性不好 +- 如果要追求连续性,可以考虑:把所有ID集中放在一个地方进行管理,对每个ID序列独立管理,每台机器上使用ID时就从这个ID生成器中取。这里有如下几个问题需要解决: + - 1)性能问题:每次都远程取ID会有资源损耗。一种改进方案是一次取一段ID,然后缓存在本地,这样就不需要每次都去远程的生成器上取ID了。但是也会带来问题,如果应用取了一段ID,正在用时完全宕机了,那么一些ID号就浪费不可用了。 + - 2)生成器的稳定问题:ID生成器作为一个无状态的集群存在,其可用性要靠整个集群来保证。 + - 3)存储的问题。底层存储的选择空间较大,需要根据不同类型进行对应的容灾方案。下面介绍两种方式: + +- 独立ID生成器方案:在底层使用一个独立的存储来记录每个ID序列当前的最大值,并控制并发更新。 + +- 生成器嵌入到应用:在每个应用上完成生成器要做的工作,即读取可用的ID或ID段,然后给应用的请求使用。 + +- 因为这种方式没有中心的控制节点,并且不希望生成器之间还有通信,因此数据的ID并不是严格按照进入数据库的顺序而增大的。 +## Snowflake +### 介绍 + + - 41-bit 的时间可以表示(1L<<41)/(1000L*3600*24*365)=69 年的时间, +- 10-bit 机器可以分别表示1024 台机器。如果我们对IDC 划分有需求,还可以将 +- 10-bit 分5-bit 给IDC,分5-bit 给工作机器。这样就可以表示32 个IDC,每个 +- IDC 下可以有32 台机器,可以根据自身需求定义。12 个自增序列号可以表示2^12 +- 个ID,理论上snowflake 方案的QPS 约为409.6w/s,这种分配方式可以保证在任何一个IDC 的任何一台机器在任意毫秒内生成的ID 都是不同的。 +### 源码 + +``` +/** + * SnowFlake的结构如下(每部分用-分开):<br> + * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> + * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> + * <p> + * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) + * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。 + * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> + * <p> + * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br> + * <p> + * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> + * 加起来刚好64位,为一个Long型。<br> + * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 + */ +public class SnowflakeIdWorker { + /** + * 开始时间截 (2015-01-01) + */ + private final long twepoch = 1420041600000L; + + /** + * 机器id所占的位数 + */ + private final long workerIdBits = 5L; + + /** + * 数据标识id所占的位数 + */ + private final long dataCenterIdBits = 5L; + + /** + * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) + */ + private final long maxWorkerId = -1L ^ (-1L << workerIdBits); + + /** + * 支持的最大数据标识id,结果是31 + */ + private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); + + /** + * 序列在id中占的位数 + */ + private final long sequenceBits = 12L; + + /** + * 机器ID向左移12位 + */ + private final long workerIdShift = sequenceBits; + + /** + * 数据标识id向左移17位(12+5) + */ + private final long dataCenterIdShift = sequenceBits + workerIdBits; + + /** + * 时间截向左移22位(5+5+12) + */ + private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; + + /** + * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) + */ + private final long sequenceMask = -1L ^ (-1L << sequenceBits); + + /** + * 工作机器ID(0~31) + */ + private long workerId; + + /** + * 数据中心ID(0~31) + */ + private long dataCenterId; + + /** + * 毫秒内序列(0~4095) + */ + private long sequence = 0L; + + /** + * 上次生成ID的时间截 + */ + private long lastTimestamp = -1L; + + //==============================Constructors===================================== + + /** + * 构造函数 + * + * @param workerId 工作ID (0~31) + * @param dataCenterId 数据中心ID (0~31) + */ + public SnowflakeIdWorker(long workerId, long dataCenterId) { + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (dataCenterId > maxDataCenterId || dataCenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDataCenterId)); + } + this.workerId = workerId; + this.dataCenterId = dataCenterId; + } + + // ==============================Methods========================================== + + /** + * 获得下一个ID (该方法是线程安全的) + * + * @return SnowflakeId + */ + public synchronized long nextId() { + long timestamp = timeGen(); + + //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + //如果是同一时间生成的,则进行毫秒内序列 + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + //毫秒内序列溢出 + if (sequence == 0) { + //阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } + //时间戳改变,毫秒内序列重置 + else { + sequence = 0L; + } + + //上次生成ID的时间截 + lastTimestamp = timestamp; + + //移位并通过或运算拼到一起组成64位的ID + return ((timestamp - twepoch) << timestampLeftShift) // + | (dataCenterId << dataCenterIdShift) // + | (workerId << workerIdShift) // + | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + + + /** + * 测试 + */ + public static void main(String[] args) { + SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); + for (int i = 0; i < 1000; i++) { + long id = idWorker.nextId(); + System.out.println(Long.toBinaryString(id)); + System.out.println(id); + } + } +} +``` + + +### 优点 +- ● 毫秒数在高位,自增序列在低位,整个ID 都是趋势递增的。 +- ● 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。 +- ● 可以根据自身业务特性分配 bit位,非常灵活。 +### 缺点 +- ● 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 +- 为什会时间回拨? +- 第一:人为操作,在真实环境一般不会有那个傻逼干这种事情,所以基本可以排除。 +- 第二: 由于有些业务等需要,机器需要同步时间服务器(在这个过程中可能会存在时间回拨,查了下我们服务器一般在10ms以内(2小时同步一次))。 +## Flicker +- 基于MySQL自增ID的机制。 +- 创建一张表 +- create table tickets ( +- id bigint primary key auto_increment, + - stub char(1) not null default ‘’ unique +- ); +- 在一个事务中 +- replace into tickets(stub) values(‘a’); 不存在(主键索引或唯一索引)则插入 +- select last_insert_id(); + +- 这样我们就能拿到不断增长且不重复的id了。如果要解决单点问题,可以启动多台数据库生成id,通过区分auto_increment的起始值和步长来生成奇偶数的ID。 +- + +# 应对多机的数据查询 +## 跨库JOIN +- 解决方案: + - 1)在应用层把原来数据库的JOIN操作分成多次的数据库操作 + - 2)数据冗余 + - 3)借助外部系统(例如搜索引擎)来解决一些跨库的问题 +## 外键约束 +- 如果要对分库后的单库做外键约束,就要求分库后每个单库的数据是内聚的,否则就只能靠应用层的判断、容错等方式了。 +## 跨库查询 +- 分库分表后要对查询结果在应用上合并: + - 1)排序,即多个来源的数据查询出来后,在应用层进行排序的工作。如果从数据库中查询出的数据是已经排好序的,那么在应用层要进行的就是多路归并排序;如果查询的数据未排序,就要进行一个全排序。 + - 2)函数处理,即使用max、min、sum、count等函数对多个数据来源的值进行相应的函数处理 + - 3)求平均值,从多个数据来源进行查询时,需要把SQL改为查询sum和count,然后对多个数据来源的sum求和、count求和后,计算平均值 + - 4)非排序分页:是同等步长地在多个数据源上分页处理,还是同等比例地分页处理。同等步长是指分页的每页中,来自不同数据源的记录数是一样的;同等比例的意思是,分页的每页中,来自不同数据源的数据占这个数据源符合条件的数据总数的比例是一样的。 + - 5)排序后分页:取第一页时,应该考虑的最极端情况是最终合并后的结果可能都来自一个数据源,所以我们需要从每个数据源取足一页的数据。对于第二页,需要把是每个数据源的前两页都取回来进行归并排序。越往后,承受的负担越重。 + +- + +# 分布式数据库中间件的设计与实现 +# 对外提供数据访问层功能的方式 +- 第一种是为用户提供专有API,它的通用性很差。 +- 第二种是通用的方式,数据层自身可以作为一个JDBC的实现,暴露出JDBC的接口给应用,此时应用的成本就很低了,和使用远程数据库的JDBC驱动的方式是一样的,迁移成本也很低。 +- 第三种是基于ORM或类ORM接口的方式,比如MyBatis。 + +# 按照数据层流程的顺序看数据层设计 + +## SQL解析 +- 有两个问题,一个是对SQL支持的程度,是否需要支持所有的SQL; +- 二是支持多少SQL的方言。 +- 具体解析可以用antle、javacc。 +- 通过SQL解析或者提示方式得到了相关信息后,下一步就是进行规则处理,从而确定要执行这个SQL的目标库。 + +## 规则处理 +- 如何将数据分散到不同的数据库和表中? + +### 固定哈希算法 +- 固定哈希的方式为,根据某个字段比如id取模,然后将数据分散到不同的数据库和表中。除了根据id取模,还经常会根据时间维度来存储数据,这一般用于数据产生后相关日期不进行修改的情况。根据时间取模多用在日志类或者其他与时间维度密切相关的场景。通常将周期性的数据放在一起,这样进行数据备份、迁移或者现有数据的清空都会很方便。 + +### 一致性哈希 +- 见《面试---7. 分布式理论---问题》 +## SQL改写 +- 对于应用给数据层执行的SQL,除了根据规则确定数据源外,我们可能还需要修改SQL。 +- 我们的数据表从原来的单库单表变为了多库多表,这些分布在不同数据库中的表的结构一样,但是表名未必一样。如果把原来的表分布在多库并且每个库只有一个表的话,那么这些表是可以同名的,但是如果单库中不止一个表,那就不能用同样的名字,一般是在逻辑表名后加后缀。 +- 在命名表时有一个需要作出选择之处,就是不同库中的表名是否要一样?如果每个表的名字都是唯一的,看起来似乎不太优雅,但是可以避免很多误操作。另外表名唯一在进行路由和数据迁移时比较便利。 +- 除了修改表名,SQL中一些提示中用到的索引名,在分库分表时也需要进行相应的修改,需要从逻辑上的名字变为对应数据库中物理的名字。 +- 另外,还有一个需要修改SQL的地方,就是在进行跨库计算平均值的时候,需要分别在各个库求sum和count,然后合并求平均。 +## 选择数据源 +- 规则部分可以确定一组数据源,而在这里需要确定是具体的某个数据源。一张表经历了分库分表后,我们会给分库后的库都提供备库。分库是把数据分到了不同的数据分组中。我们决定了数据分组后,还需要决定访问分组中的哪个库。这些库一般是一写多读的,根据当前要执行的SQL特点(读,写)、是否在事务中以及各个库的权重规则,计算得到这次SQL请求要访问的数据库。 + +### 完整数据源 + +- 单个DataSource下面对应了多个数据库,以及一组规则。 + +### 分组数据源 + +- groupDataSource,分组的DataSource,用于管理整个业务数据库集群中的一组数据库。groupDataSource相对于完整的DataSource来说,可以不管理具体的规则,也可以不进行SQL的解析。它是作为一个相对基础的数据源提供给业务的,那么groupDataSource重点解决的问题是,要在访问这个分组中的数据库时,解决访问具体数据库的选择问题,具体ide选择策略是groupDataSource要完成的重点工作,包括根据事务、读/写等特性选择主备、以及根据权重的不同在库间进行选择。 + +### 对比 +- 如果采用完整的DataSource,对于应用来说只会看到一个DataSource,可以少关心很多事情,不过可能会收到DataSource本身的限制;如果采用groupDataSource会有更大的自主权。 +- 如果采用完整的DataSource,对于后端业务的数据库集群的管理会更方便,例如我们可以进行一些扩容、缩容的工作而不需要应用太多的感知;而使用groupDataSource则意味着绑定了分组数量,这样要进行扩容、缩容时是需要应用进行较多配合的。虽然使用groupDataSource不能进行整体的扩容、缩容,但是可以进行组内的扩容、缩容、主备切换等工作,这也是groupDataSource最大的价值。在一些活动或者可预期的访问高峰前,可以给每个分组挂载上备库,通过配置管理中心的配置更新下线数据库,以及进行主备库的切换。 +### 数据源封装 +- 对于某个具体数据库,可以使用第三方数据源的组件配置。这种方式的最大缺点是不够动态,并且对于进行SQL执行的降级隔离等业务稳定性方面没有很多支持。如果我们通过AtomDataSource把单个数据库的数据源的配置集中存储,那么在定期更换密码、进行机房迁移等需要更改IP地址或改变端口时就会非常方便。另外,通过AtomDataSource也可以帮助我们完成在单表上的SQL连接隔离,以及禁止某些SQL的执行等和稳定性相关的工作。 + +- 把整体DataSource分层后为应用提供的三层数据源实现: + +## 执行SQL和结果处理 +- 在SQL执行的部分,比较重要的是对异常的处理和哦按段,需要能够从异常中明确判断出数据库不可用的情况,而关于执行结果的处理,在之前一些特殊情况中都已经提及,这里不再重复了。 + +# 独立部署的数据访问层实现方式 +- 首先从数据层的物理部署来说可以分为jar包方式和Proxy的方式。 +- 如果采用Proxy方式的话,客户端与Proxy之间的协议有两种选择:数据库协议和私有协议。 + + - 1)采用数据库协议时,应用就会把Proxy看做一个数据库,然后使用数据库本身提供的JDBC的实现就可以连接Proxy。因为应用到Proxy、Proxy到DB采用的都是数据库协议,所以,如果使用的是同样的协议,例如都是MySQL协议,那么在一些场景下就可以减少一次MySQL协议到对象再到MySQL协议的转换。不过采用这种方式时Proxy要完全实现一套相关数据库的协议,这个成本是比较高的,此外,应用到Proxy之间也没有办法做到连接复用。 + - 2)采用私有协议时,Proxy对外的通信协议是我们自己设计的,并且需要一个独立的数据层客户端,这个协议的好处是,Proxy的实现会相对简单一些,并且应用到Proxy之前的连接是可以复用的。 + + +# 读写分离的挑战和应对 +- 读写分离的场景下,需要解决把主库的数据复制到备库中去。 +- 如果是多从库对应一主库的情况下,并且数据结构相同,可以使用MySQL的Replication来解决这个问题。 +- 假如主从库数据结构不同,那么需要将主库上的写操作都存放至数据同步服务器,然后由数据同步服务器将数据同步到从库。 +- Extractor负责把数据源变更的信息加入到数据分发平台,Applier的作用是把这些变更应用到相应的目标上。 +# 平滑迁移 +- 迁移过程中不停机,保证系统持续服务。 + - 1)双写 + - 2)全量+增量:先复制全量数据,再复制增量数据 +- 在开始进行数据迁移时,记录增量的日志,在迁移结束后,再对增量的变化进行处理。在最后,可以把要被迁移的数据的写暂停,保证增量日志都处理完毕后,再切换规则,放开所有的写,完成迁移工作。 + - 1)开始迁移,开始记录数据库的数据变更的增量日志 + - 2)数据开始复制到新库,并且也有更新进来 + - 3)当全量迁移结束后,把增量日志中的数据也进行迁移 + - 4)进行数据比对,记录源库和新库的数据不同 + - 5)停止对源库中的写操作,进行增量日志的处理 + - 6)更新路由规则,所有新数据的读写就到了新库 + +- + +# 分布式缓存 +- 缓存穿透 + +- 分布式文件系统 分布式存储系统(GFS、HDFS、fastDFS)、存储模型(skipList、LSM等) +- 自己实现一个缓存 LinkedHashMap?按访问顺序排序 +# 缓存分类 +- 1、按宿主层次划分 + + - 1)本地缓存/进程间缓存:可以分为堆内缓存,堆外缓存。堆内缓存对GC影响较大,堆外缓存又会增加额外的序列化和反序列化开销 + - 2)进程间缓存:可以在本机单独启动一个进程专门放缓存,通过Domain Socket通信 + - 3)远程缓存:跨服务器访问的缓存,如Redis + - 4)二级缓存:本地缓存与远程缓存的结合,对于不易改变但访问量巨大的数据,可以放到本地缓存中 + +- 2、按存储介质划分 + - 1)内存缓存 + - 2)持久化缓存 + +- 3、按架构层次划分 +- 页面缓存、浏览器缓存、Web服务器缓存、反向代理缓存、应用级缓存 +# 缓存使用场景 +# 用缓存来管理存储 + +- 这种方式中,应用是不直接操作存储的,存储由缓存来控制。对于应用的逻辑来说这很简单,但是对于缓存来说,因为需要保证数据写入缓存后能够存入存储中,所以缓存本身的逻辑会复杂写,需要有很多操作日志及故障恢复。 +# 应用直接管理缓存和存储 + +- 在这种方式中,应用直接与缓存和存储进行交互,一般的做法是应用在写数据时更新存储,然后失效缓存数据,而在读数据时首先读缓存,如果缓存中没有数据,那么再去读存储,并且把数据写入缓存。 +# 合理使用缓存 +# 频繁修改的数据 +- 如果缓存中保存的是频繁修改的数据,就会出现数据写入缓存后,应用还来不及读取缓存,数据就已经失效的情形,徒增系统负担。一般来说,数据的读写比在2:1以上,即写入一次缓存,在数据更新前至少读取两次,缓存才有意义。 +# 没有热点的访问 +- 不可能将所有数据都缓存起来,只能将最新访问的数据缓存起来,而将历史数据清理出缓存。如果应用系统访问数据没有热点,不遵循二八定律,那么缓存没有意义。 +# 数据不一致与脏读 +- 一般会对缓存设置失效时间,一旦超过失效时间,就要从数据库中重新加载,因此应用要容忍一定时间的数据不一致。如卖家已经编辑了商品属性,但是需要过一段时间才能被买家看到。在互联网应用中,这种延迟通常是可以接受的。还有一种策略是数据更新时立即更新缓存,不过这也会带来更多系统开销和事务一致性的问题。 +# 缓存可用性/缓存雪崩 +- 当缓存服务崩溃时,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况被称为缓存雪崩。 +- 实践中,有的网站通过缓存热备等手段提高缓存可用性。 + +- 由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 + +- 解决:将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 + + +# 缓存预热 +- 缓存中存放过的是热点数据,热点数据又是缓存系统利用LRU对不断访问的数据筛选淘汰出来的。这个过程需要花费较长的时间。新启动的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫做缓存预热(warm up)。 +# 缓存穿透 +- 如果因为不恰当的业务,或者恶意攻击持久高并发地请求某个不存在的数据(或者缓存失效),由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来(value置为null)。 +- 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 +- + +# 分布式消息队列 +- 作用: + - 1)解耦:A系统完成后,需要通知B系统,此时可以用RPC的方式来解决。 +- AService#doSomething(){ +- BService.doSomething(); +- } +- 但是如果修改需求,还要通知C、D系统,怎么实现? +- 还是用RPC的话,修改AService#doSomething() +- AService#doSomething() { +- BService.doSomething(); +- CService.doSomething(); +- DService.doSomething(); +- } +- 如果使用消息队列,那么可以这样做: +- AService#doSomething() { +- mq.send(topic: “doSomething” ,data: data); +- } +- B、C、D系统均订阅该topic +- mq.subscibe(topic:”doSomething”,callback); +- 需要增加其他系统时,只需要在该系统中增加订阅该topic的操作即可,不需要修改既往代码。 + - 2)异步:削峰 +- 短时间大量的任务可以先放入队列中慢慢处理,避免引起阻塞。 + +# 消息发送一致性/事务消息 +- 消息发送一致性是指产生消息的业务动作与消息发送的一致。如果业务操作成功了,那么由这个操作产生的消息一定要发送出去,否则就丢失消息了。如果这个行为没有发生或者失败,那么就不应该把消息发送出去。 +- 一种实现是JMS中的XA支持,引入了分布式事务。 +- 事务消息也可以解决这个问题。 +- 一个分布式事务被拆为一个本地事务和一个消息发送。 +- 而消息发送的前提是本地事务执行成功,本地事务提交后,消息才会发送出去;否则会取消该消息的发送。 +- 流程如下: +- 1. Producer向Broker发送Prepared消息,可能会发送失败 +- 2. 执行本地事务 +- 3. 如果本地事务执行成功,则发送Confirm消息;如果失败,那么回滚本地事务,取消发送Confirm消息,Broker会删除Prepared消息。 +- 4. Producer发送Confirm消息时可能会发送失败,此时消息的状态仍为Prepared。 +- 5. Broker接收到Confirm消息时,会将该消息推送给Consumer。 +- 6. 设置Scheduler去向Producer轮询Prepared消息的当前状态,称为消息回查。消息回查主要目的是检测Confirm消息发送失败的情况。 +- 7. Consumer接收到消息,执行本地事务。 +- 8. 本地事务执行成功时,会返回给Broker一个ACK,执行失败时,Broker会定期重新发送给Consumer该消息,超过重试次数时可以选择不再重试。 + +- RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。 +- 为解决确认消息发送失败的问题(消息回查),RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。 + + +# 解决消息中间件与使用者的强依赖问题 +- 解决消息发送一致性时,会造成消息中间件变成了业务应用的必要依赖。如果消息中间件系统出现问题,就会导致业务操作无法继续进行,即便当时业务应用和业务操作的资源都是可用的。 +- 解决这个问题有三种思路: + - 1)提供消息中间件系统的可靠性,但无法实现完全可靠 + - 2)对于消息中间件系统中影响业务操作进行的部分,使其可靠性与应用自身的可靠性相同 + - 3)可以提供弱依赖的支持,能够较好地保证一致性 +- 第二种方案就是要保证业务能操作成功,就需要消息能够入库成功。 +- 该方案有几种设计思路: + - 1)应用和消息中间件一起操作消息表结构 + - 2)消息中间件不直接操作消息表结构 + - 3)应用本地记录消息结构 + +# 应用和消息中间件一起操作消息表结构 + +- 我们把消息中间件所需要的消息表与业务数据表放到同一个业务数据库中,这样,业务应用就可以把业务操作和写入消息作为一个本地事务来完成,然后再通知消息中间件有消息可以发送,这样就解决了一致性的问题。消息中间件会定时去轮询业务数据库,找到需要放松的消息,取出内容后进行发送。这个方案对业务系统有如下三个影响: + - 1)需要用业务自己的数据库承载消息数据 + - 2)需要让消息中间件去访问业务数据库 + - 3)需要业务操作的对象是一个数据库 + +# 消息中间件不直接操作消息表结构 + +- 消息中间件不再直接与业务数据库打交道。消息表还是放在业务数据库中,完全由业务数据库来控制消息的生成、获取、发送及重试的策略。比较多的逻辑从消息中间件的服务端移动到消息中间件的客户端,并且在业务应用上执行。消息中间件更多的是管理接收消息的应用,并且当有消息从业务应用发过来后就只管理投递,把原来的调度、重头、投递等逻辑分到了客户端和服务端两边。 +- 这两种方式已经解决了大部分的问题,但是它们都要求业务操作是支持事务的数据库操作。 + +# 应用本地记录消息结构 + +- 可以考虑把本地磁盘作为一个消息存储,如果消息中间件不可用,又不愿或不能侵入业务自己的数据库时,可以把本地磁盘作为存储消息的地方,等待消息中间件回复后,再把消息送到消息中间件中。所有的投递、重试等管理,仍然是在消息中间件中进行,而本地磁盘的定位只是对业务应用上发送消息一定成功的一个保证。 +- 这种方式的风险是,如果消息中间件不可用,而且写入本地磁盘的数据也坏了的话,那么消息就丢失了。这确实是个问题,所以,从业务数据上进行消息补发才是最彻底的容灾的手段,因为只有这样才能保证只要业务数据在,就一定可以有办法恢复消息。 +- 将本地磁盘作为消息存储的格式有两种用法,一是作为一致性发送消息的解决方案的容灾手段,该方式平时不工作,出现问题时才切换到该方式上;二是直接使用该方式来工作,这样可以控制业务操作本身调用发送消息的接口的处理时间(异步发送?),此外也有机会在业务应用和消息中间件间做一些批处理的工作。 + +- + +# 消息模型 +# JMS Queue模型 / P2P + +- 应用1和2发送消息到JMS服务器,这些消息根据到达的顺序形成一个队列,应用3和4进行消息的消费。注意,应用3和4收到的消息是不同的,在JMS Queue方式下,如果Queue里面的消息被一个应用消费了,那么连接到JMS Queue上的另一个应用是收不到这个消息的,也就是说所有连接到这个JMS Queue上的应用共同消费了所有的消息。消息从发送端发送出来时不能确定最终会被哪个应用消费,但是可以明确的是只有一个应用回去消费这条消息。也被称为Peer to Peer模型。 + +# JMS Topic模型 + +- 从发送消息的部分和JMS Topic内部的逻辑来看,JMS Topic和JMS Queue是一样的,二者最大的差别在于消息接收的部分,在Topic模型中,接收消息的应用3和4是可以独立收到所有到达Topic的消息的。也被称为Pub/Sub模型。 + +# 我们需要什么样的消息模型 + - 1)消息发送方和接收方都是集群 + - 2)同一个消息的接收方可能有多个集群进行消息的处理 + - 3)不同集群对于同一条消息的处理不能相互干扰 + +- Queue和Topic都无法满足我们的需求,前者不支持独立消费,后者同一个集群内部各个机器会收到重复的消息。 +- 我们可以把集群和集群之间对消息的消费当做Topic模型来处理,而集群内部的各个具体应用处理对消息的消费当做Queue模型来处理。可以引入ClusterID,用它来标识不同的集群,集群内部的各个应用实例的连接使用同样的ClusterID。 + +- 如果一定要使用JMS的话,有一个变通的做法。 + +- 不过这种级联方式相对比较繁重,是多个独立的JMS服务器之间的连接,这比在消息中间件服务器内部进行处理要复杂的多。好处是基本可以直接使用JMS的实现,这里需要注意的是从Topic中发消息分派到不同的Queue中时,需要由独立的中转的消息订阅者来完成,并且对同一个Queue的中转只能由一个连接完成。 +# 消息订阅 +# 非持久订阅 + +- 非持久订阅是消息接收者和消息中间件之间的消息订阅的关系的存续,与消息接收者自身是否处于运行状态有直接关系。也就是说,当消息接收者应用启动时,就建立了订阅关系,这时可以收到消息,而如果消息接收者应用结束了,那么消息订阅关系也就不存在了,这时的消息是不会为消息接收者保留的。 +# 持久订阅 + +- 持久订阅的含义是,消息订阅关系一旦建立,除非应用显式地取消订阅关系,否则这个订阅关系将一直存在;而订阅关系建立后,消息接收者会接收到所有消息,如果消息接收者应用停止,那么这个消息也会保留,等待下次应用启动后再投递给消息接收者。 +# 消息可靠性 +- 在持久订阅前提下,整个消息系统是如何保证消息可靠的呢? + +- 消息从发送端应用到接收端应用,中间有三个阶段需要保证可靠: + - 1)消息发送者将消息发送到消息中间件 + - 2)消息中间件将消息存入消息存储 + - 3)消息中间件把消息投递给消息接收者 +# 消息发送端的可靠性保证 +- 消息从发送者发送到消息中间件,只有当消息中间件及时、明确地返回成功,才能确定消息可靠到达消息中间件了;返回错误、出现异常、超时等情况,都表示消息发送到消息中间件这个动作失败。这里需要注意的是对异常的处理,可能出现的问题是在不注意的情况下吃掉了异常,从而导致错误的判断结果。 + +# 消息存储的可靠性保证 +- 要么是完全自主实现持久存储部分的代码,要么利用现有的存储系统实现。 +- 现有的存储系统有比较多的选择,有关系型数据库、分布式文件系统和NoSQL。 +## 基于文件的消息存储 + +## 基于数据库的消息存储 +- 就消息中间件而言,我们希望尽量避免获取数据时的表关联查询,所以希望一个消息只用一个单行的数据来解决。 + +- 如果单机出现硬件故障,需要考虑数据的容灾方案。 + - 1)单机Raid + - 2)多机的数据同步,要求不能有延迟 + - 3)应用双写 + +## 基于双机内存的消息存储 +- 使用文件系统或数据库来进行消息存储时,因为磁盘IO的原因,系统性能都会受到限制。一个改进方案是用混合方式进行存储的管理。可以采用的一个方法是用双机的内存来保证数据的可靠性。正常情况下,消息持久存储是不工作的,而基于内存来存储消息则能够提供很高的吞吐量。一旦一个机器出现故障,则停止另一台机器的数据写操作,并把当前数据落盘。 + +- 只要不遇到两台基于内存的消息中间件机器同时出故障的情况,并且当一台出问题时,另一台将当前内存的消息写入持久存储的过程中不出问题的话,消息是很安全的。这种方式适用于消息到了消息中间件后大部分消息能够及时被消费掉的情况,它可以很好地提高性能。 + +# 消息投递的可靠性保证 +- 消息中间件需要显式地接收到接收者确认消息处理完毕的信号(ACK)才能删除消息。 +- 投递处理的第一个可优化之处是,在进行投递时一定要采用多线程的方式处理。 +- 一种方式是每个线程处理一条消息,并且等待处理结束后再进行下一条消息的处理。这种方式在正常情况下没有问题,而遇到异常情况时,例如订阅者集群有一个很慢的订阅者,复杂投递的所有线程会慢慢地被堵死,因此都需要等待这个慢的订阅者返回。 +- 另一种方式是把处理消息结果返回的处理工作放到另外的线程池中完成,也就是投递线程完成消息到网络的投递后就可以接着处理下一个消息,保证投递的环节不会被堵死。而等待返回结果的消息可以先放到内存中,不占用线程资源,等有了最后的结果时,再放入另外的线程池中处理。这种方式把占用线程池的等待方式变为了靠网络收到消息处理结果后的主动响应方式。 +- 更新数据库也可以采用batch的方式。 +- 第二个可优化之处是一个应用上有多个订阅者订阅同样的信息,如果不加以优化,我们会向这个机器发送多次同样的信息。 + - 1)单机多订阅者共享连接 + - 2)消息只发送一次,然后传到单机的多订阅者生成多个实例处理。 + +# 消息系统扩容 +# 消息中间件扩容 +- 消息中间件本身没有持久状态,扩容比较容易。主要是让消息的发送者和订阅者能够感知到有新的消息中间件机器加入到了集群,这是通过注册查找中心实现的。 + +- 在同一个存储中如何区分存储的消息是来自于哪个消息中间件应用的?可以给每条消息加一个server字段,当有新加入的消息中间件时,会使用新的server值。 +- 如果有消息中间件应用长期不可用的话,我们就需要加入一个和它具有相同server标识的机器来代替它,或者通过这个消息中间件进入到消息系统中但还没有完成投递的消息分给其他机器处理,也就是让另一台机器承担剩余消息的投递工作。 +# 消息存储扩容 +- 假设:不用保证消息顺序;不提供Pull方式获取消息 +- 为了在分库分表情况下主动根据某些条件进行数据查询,就必须确切知道数据存储在哪个数据库的哪张表,对这种情况的扩容就比较复杂。但在假设前提下,我们不需要支持外部主动根据条件查询消息的,因为: + - 1)消息发送到消息中间件时,消息中间件将消息入库,这时消息中间件是明确知道消息存储在哪里的,并会进行消息的投递调度,所以一定能找到消息。 + - 2)由于在内存中进行调度的消息数量有限,因此我们会调度存储在数据库中的消息。而在调度时,我们更关心的是那些符合发送条件的消息,所以这个调度必然是需要跨库跨表的。在这个过程中,需要投递的消息会把相关索引信息加载到内存,在这个过程后,内存中的调度信息就自然有了存储节点信息。 +# 消息重复 +- 消息重复的产生原因: + - 1)消息发送端应用的消息重复发送: +- 消息发送端发送消息给消息中间件,消息中间件收到消息并成功存储,而此时消息中间件出了问题,导致应用没有收到消息发送成功的返回,因而进行重试 +- 消息中间件因为负载高,响应变慢,成功把消息存储到消息存储中后,返回“成功”这个结果时超时 +- 消息中间件将消息成功写入消息存储,在返回结果时网络出现问题,导致应用发送端重试,而重试时网络恢复,由此导致重复 +- 一个解决办法是,重试发送消息时使用同样的消息id,而不要在消息中间件产生消息id。 + - 2)消息到了消息存储,由消息中间件向外投递时产生重复: +- 消息被投递到消息接收者进行处理,处理完毕后应用出问题了,消息中间件不知道消息处理结果,会进行重试 +- 消息被投递到消息接收者进行处理,处理完毕后网络出问题了,消息中间件没有收到消息处理结果,会进行重试 +- 消息被投递到消息接收者进行处理,处理时间比较长,消息中间件因为消息超时会重试 +- 消息被投递到消息接收者进行处理,处理完毕后消息中间件收到结果,但是遇到消息存储故障,没能更新投递状态,会进行重试 +- 一种解决办法是引入分布式事务,还有一种是保证消息接收者的消息处理是幂等的。 + +- 在JMS中,消息接收者对收到的消息进行确认,有以下几种选择: + - 1)AUTO_ACKNOWLEDGE:自动确认,当JMS的消息接收者收到消息后,JMS的客户端会自动进行确认。但是消息确认时可能消息还没来得及处理或者尚未处理完成,所以这种确认方式对于消息投递处理来说是不可靠的。 + - 2)CLIENT_ACKONWLEDGE:客户端确认,客户端如果要确认消息处理成功,告诉服务器确认消息时,需要主动调用Message#acknowledge方法以确认。 + - 3)DUPS_OK_ACKNOWLEDGE:在消息接收方的消息处理函数执行结束后进行确认,一方面保证了消息处理结束后才进行确认,另一方面也不需要客户端主动调用acknowledge方法了。 +# 消息投递的其他属性支持 +# 消息优先级 +- 一般情况下消息是先到先投递,消息优先级的属性可以支持根据优先级来确定投递顺序。 +# 订阅者消息处理顺序和分级订阅 +- 对于同样的消息,可能会希望有些订阅者处理结束后再让其他订阅者进行处理,一种方案是可以设定优先处理的订阅集群,也就是我们这里的订阅者消息处理顺序的属性,可以在这个字段上设置有些处理的集群ID,另一种方案是分级订阅。 + +- 我们把优先接收者和一般接收者的接收分开,优先接收者处理成功后主动把消息投递到另外的消息中间件,然后一般接收者接收新产生的消息。 +# 自定义属性 +- 消息自身的创建时间、类型、投递次数等熟悉属于消息的基础属性,在消息体外,支持自动以的属性会很方便,比如消息过滤,以及接收端对消息的处理。 +# 局部顺序 +- 局部顺序是指在众多的消息中,和某件事情相关的多条消息之间有顺序,而多件事情之间的消息则没有顺序。 + +- 在消息中间件内部,有非常多的逻辑上独立的队列。支持局部有序需要消息上有一个属性,即区分某个消息应该与哪些消息一起排队的属性字段。 +# 顺序消息 +- 在顺序消息的场景下,对于消息接收端的设计从原来的Push变为了Pull,这是为了让消息接收者更好地控制消息的接收和处理,而消息中间件自身的逻辑也进行了简化。 +- 在消息中间件内部,有多个物理上的队列,进入到每个队列的消息则是严格按照顺序被接收和消费的,而消息中间件单机内部的队列之间是互不影响的。 + +- 具体实现中,消息的存储就写到本地文件中,采用的是顺序写入的方式,其基本思路和前面基于文件的存储比较类似。二者的差别是,这个场景不存在文件的空洞,因为消息必须按照顺序去消息,所以,一个消息接收者在每一个它所接收的消息队列上有一个当前消费消息的位置,对于这个接收者来说,这个位置之前的消息就已经完成消费了。在同一个队列中,不同的消费者分别维护自己的指针,并且通过指针的回溯,可以把消息的消费恢复到之前的某个位置继续处理。 +- 如果需要对消息进行补发,那么移动接收端的消费消息的指针就可以完成了。 + +# 单机多队列的问题与优化 +- 如果单机的队列数量特别多,性能就会明显地下降,原因是队列数量过多时,消息接入就接近于随机写了。一个改进措施是把发送到这台机器的消息数据进行顺序写入,然后再根据队列做一个索引,每个队列的索引是独立的,其中保存的只是相对于存储数据的物理队列的索引位置。 + +- 这样改进的好处是: + - 1)队列轻量化,单个队列数据量非常小 + - 2)对磁盘的访问串行化,避免磁盘竞争,不会因为队列增加导致IOWAIT增高 +- 缺点: + - 1)写虽然是顺序写,但读却变成了随机读 + - 2)读一条消息时,会先读逻辑队列,再读物理队列,增加了开销 + - 3)需要保证物理队列和逻辑队列完全一致性,增加了编程复杂度。 + +# 解决本地消息存储的可靠性 +- 复制 + - 1)把单个的消息中间件机器变为主备两个节点,slave节点订阅master节点上的所有消息。这是一个异步的操作,存在着丢失消息的可能,比较类似于MySQL的Replication。(异步复制?) + - 2)同步复制而非订阅,master收到消息后会主动写往slave,并且收到slave的响应后才向消息发送者返回“成功”。(同步双写?) +- 第二种方式更加安全和保险。 +# 队列扩容 +- 基本的策略是让向一个队列写入数据的消息发送者能够知道应该把消息写入迁移到新的队列中,并且也需要让消息订阅者知道,当前的队列消费完数据后需要迁移到新队列去消息消息。 + +- 关键点: + - 1)原队列在开始扩容后需要有一个标志,即便有新消息过来,也不再接收 + - 2)通知消息发送端新的队列的位置 + - 3)对于消息接收端,对原来队列的定位会收到新旧两个位置,当旧队列的数据接收完毕后,则会只关心新队列的位置,完成切换。 +# Pull/Pull + +# 高并发下的幂等策略分析 +# 为什么需要幂等 +- 业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 +- 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如: + +- 用户在APP上连续点击了多次提交订单,后台应该只产生一个订单; +- 向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 +- 很显然,幂等接口认为,外部调用者会存在多次调用的场景,为了防止重试对数据状态的改变,需要将接口的设计为幂等的。 +# 什么情况下需要保证幂等性 +- 以SQL为例,有下面三种场景,只有第三种场景需要开发人员使用其他策略保证幂等性: + +- SELECT col1 FROM tab1 WHER col2=2,无论执行多少次都不会改变状态,是天然的幂等。 +- UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,因此也是幂等操作。 +- UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。 +# 保证幂等策略 +- 幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。 +- 下面以支付为例,在不考虑并发的情况下,实现幂等很简单:①先查询一下订单是否已经支付过,②如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。 +# 防重复提交策略 +- 上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。 +## 乐观锁 +- 如果只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,一般通过version来做乐观锁,这样既能保证执行效率,又能保证幂等。例如: +- UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# +- 不过,乐观锁存在失效的情况,就是常说的ABA问题,不过如果version版本一直是自增的就不会出现ABA的情况。(从网上找了一张图片很能说明乐观锁,引用过来,出自Mybatis对乐观锁的支持) + +## 防重表 +- 使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。 + +## 分布式锁 +- 这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。 +## token令牌 +- 这种方式分成两个阶段:申请token阶段和支付阶段。 +- 第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。 +- 第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。 +- + +# 分布式RPC/服务框架 +- RPC框架(包括整体的一些框架理论,通信的netty,序列化协议thrift,protobuff等) + +- 服务化的好处: + - 1)模块化拆分,独立维护,减少重复代码,提高代码质量 + - 2)核心相对稳定,修改和发布次数减少 + - 3)底层资源由服务层管理,结构更加清晰,利于提高效率 + +# 服务调用端的设计与实现 +# 服务框架的使用方式 +- 使用Spring进行引入。 + +- 因为Java有动态代理的支持,所以在完成远程调用的时候,使用一个通用的对象就可以解决问题了,而不需要像很多语言一样需要通过类似IDL(Interface Description Language,接口描述语言)的方式定义,然后生成代理存根代码,再分别与调用端和被调用端一起编译。 +- 需要配置三个基础的属性 +- interfaceName +- version +- group +## 配置 +### interfaceName +- 接口名称。在进行远程通信时CustomerBean必须知道被调用的接口是哪一个,才能生成对这个接口的代理,以供本地调用,所以这是一个必备的属性。 +### version +- 版本号。在实际的场景中,接口是存在变化的可能性的,有的是因为实现代码本身重构的原因,也有的是因为业务的发展变化需要修改接口中已有方法的参数或者返回值,以满足新的需求。如果直接这样变化,那就要求所有使用的地方一起修改,一起升级,这在一个大型的分布式系统中代价是非常高的。解决这个问题的方法有两种,一是如果需要修改方法的参数或返回值,就新增一个方法,始终保持已有方法不变,不过这样对导致在过渡期间代码相对臃肿;另一种方案就是通过版本号进行区分隔离。 +### group +- 分组。分组的好处 是如果对同一个接口的远程服务有很多机器,我们可以把这些远程服务的机器归组,然后调用者可以选择不同的分组来调用,这样就可以将不同调用者对于同一服务的调用进行隔离了。 + +## 容器 +- 还有两个问题需要解决,一是服务框架自身的部署方式问题,二是实现自己的服务框架所依赖的一些外部jar包与应用自身依赖jar包的冲突问题。 +- 第一个问题:服务框架自身的部署方式问题。一种方案是把服务框架作为应用的一个依赖包并与应用一起打包。通过这种方式,服务框架就变为了应用的一个库,并随应用启动。存在的问题是,如果要升级服务框架,就需要更新应用本身,因为服务框架是与应用打包放在一起的,并且服务框架没有办法接管classloader,也就不能做一些隔离以及包的实现替换工作。 +- 另外一种方案是把服务框架作为容器一部分,这里是针对Web应用来说的,而Web应用一般用JBoss、Tomcat等作为容易,我们就要遵循不同容器所支持的方法,把服务框架作为容器的一部分。 +- Jar包冲突问题:将服务框架自身用的类与应用用到的类控制在User-Defined Class Loader,这样就实现了相互间的隔离。Web容器对于多个Web应用的处理,以及OSGi对于不同Bundle的处理都采用了类似的方法。此外,我们在实际中还会遇到需要在运行时统一版本的情况,那就需要服务框架比应用优先启动,并且把一些需要统一的jar包放到User-Defined Class Loader锁公用的祖先“ClassLoader”中。 +# 通信方式 +- 动态代理对象被调用后会进行如同服务请求方的处理,要完成寻址等工作。 + +- 有两种方式进行远程服务调用,第一种是通过中间的代理来解决。 + +- 我们这里的服务框架的设计采用的是另一种控制方案:调用者和提供者直接建立连接的方式,并且引入了一个服务注册查找中心的服务。 + +- 服务注册查找中心并不处在调用者和服务提供者之间,服务注册查找中心对于调用者来说,只是提供可用的服务提供者的列表。处于效率的考虑,并不是在每次调用远程服务前都通过这个服务注册查找中心来查找可用地址,而是把地址缓存在调用者本地,当有变化时主动从服务注册查找中心发起通知,告诉调用者可用的服务提供者列表的变化。 +- 当客户端拿到可用的服务提供者的地址列表后,如何为当次的调用进行选择就是路由要解决的问题了。首先要考虑的就是集群的负载均衡。具体到负载均衡的实现,随机、轮询、权重是比较常见的实现方式,其中权重方式一般是指动态权重的方式,可以根据响应时间等参数来进行计算。在服务提供者的机器能力对等的情况下,采用随机和轮询这两种方式比较容易实现;在被调用的服务集群的机器能力不对等的情况下,使用权重计算的方式来进行路由比较合适。具体策略可以参考硬件负载均衡设备以及LVS、HAProxy等替代硬件负载均衡设备的系统所支持的策略。 +# 基于接口、方法、参数的路由 +- 从系统可用性和经济性的角度考虑,控制执行速度较慢的方法对正常情况的营销是比较合理的思路,第一种思路是增加资源保证系统的能力是超出需要的,第二种思路是隔离这些资源,从而使得快慢不同、重要级别不同的方法之间互不影响。 +- 从客户端的角度来说,控制同一个集群中不同服务的路由并进行请求的隔离是一种可行方案。虽然集群中每台机器部署的代码是一样的,提供的服务也是一样的,但是通过路由的策略,我们让对于某些服务的请求到一部分机器,让另一些服务的请求到另一部分机器。 + +- 客户端的路由导致了请求的分流。在具体实现上,我们一般采用的方式把路由规则进行集中管理,在具体调用端的服务框架上获取规则后进行路由的处理,具体来说,是根据服务定位提供服务的那个集群的地址,然后与接口路由规则中的地址一起取交集,得到的地址列表再进行接下来的负载均衡算法,最后得到一个可用的地址去调用。 + +- 以上是基于接口的路由。可以基于接口的具体方法来进行路由。该方式只是在通过接口定位到服务地址列表后,根据接口加方法名从规则中得到一个服务地址列表,再和刚才的地址列表取交集。 +- 一般到基于方法的路由就够用了,需要对一些特定参数进行特殊处理的情况才会使用基于参数的路由。 + +# 多机房场景 +- 每个机房都有自己的容量上线,如果网站的规模非常大,那就需要多个机房了。机房之间的距离和分工决定了我们应该采用什么样的架构和策略,这里进讨论距离比较近的同城机房的情况。 + +- 如果不做任何处理,服务注册查找中心会把服务提供者1的所有机器看做一个集群,尽管它们分布在两个机房。这样,分布在两个机房的调用者1就会对等地看待分布在不同机房的服务提供者1的机器。如果能避免跨机房调用,就能提升系统稳定性。 +- 有两种方案可以实现这个想法:一是在服务注册查找中心做一些工作,通过它来甄别不同机房的调用者集群,给它们不同服务提供者的地址。另一种方式是通过路由来完成。大概思路是,服务注册查找中心给不同机房的调用者相同的服务提供者列表,我们在服务框架内部进行地址过滤,过滤的原则一般是基于接口等路由规则进行集中管理配置。在具体实践中,一方面需要考虑两个甚至多个机房的部署能力是否对等,也就是说通过路由使服务都走本地的话,负载是否均衡。此外还有一个异常的情况需要考虑,即如果某个机房的服务提供者大面积不可用,而另外机房的服务提供者是正常运行并且有余量提供服务,那么如何让服务提供者大面积不可用的机房的调用者调用远程的服务呢,这个是需要解决的问题。 +# 服务调用者的流控处理 +- 流量控制一般来说是有两种,一种是0-1开关,也就是说完全打开不进行流控;另一种是设定一个固定的值,表示每秒可以进行的请求次数,超过这个请求次数的话就拒绝对远程的请求。那些被流量控制拒绝的请求,可以直接返回给调用者,也可以进行排队。 +- 基于下面两个维度进行控制: + - 1)根据服务端自身的接口、方法做控制,针对不同的接口、方法设置不同的阈值,这是为了使服务端的不同接口、方法之间的负载不相互影响。 + - 2)根据来源做控制,也就是对于同样的接口、方法,根据不同来源设置不同的限制。这一般用在比较基础的服务上,也就是在多个集群使用同样的服务时,根据请求来源的不同等级等进行不同的流控处理。 +# 序列化 +- 如果在整个分布式系统中的调用者或者服务听着要使用Java以外的语言来实现,那么序列化和反序列化的方式就要支持跨语言。第二,性能开销也是需要注意的点。第三,还需要足以序列化后的长度。 +- 我们从两个方面来看协议的部分,一个是用于通信的数据报文的自定义协议,另一个是远程过程调用本身的协议。 +- 前者比如XML或者JSON,后者比如HTTP或TCP。 + +# 网络通信实现 +- BIO、NIO、AIO。 + +# 异步服务调用 + - 1)Oneway:只管发送请求而不关心结果的方式。 + +- 可以看到Oneway方式非常简单,只需要把要发送的数据放入数据队列,然后就可以继续处理后续的任务了;而IO线程也只需要从数据队列中得到数据,然后通过Socket连接送出去就好了。Oneway方式不关心对方是否收到了数据,也不关心对方收到数据后做什么或有什么返回,这就基本等价于一个不保证可靠送达的通知。 + - 2)Callback:这种方式下请求方发送请求后会继续执行自己的操作,等对方有响应时给一个回调。 + + - 3)Future + + - 4)可靠异步:消息中间件 + +# 服务提供端的设计与实现 +# 暴露远程服务 + +- 服务需要注册到服务注册查找中心后才能被服务调用者发现。所以ProviderBean需要将自己所代表的服务注册到服务注册查找中心。另外,当请求调用端定位到提供服务的机器并且请求被送达到提供服务的机器上后,在本机也需要有一个服务与具体对象的对应关系。ProviderBean也需要在本地注册服务和对应服务实例的关系。 +# 请求处理 + +- 接收到请求后,通过协议解析及反序列化,可以得到请求发送端调用服务方法的具体信息,再根据其中的服务名称、版本号找到本地提供服务的具体对象,然后再用传过来的参数调用相关对象的方法就可以了。 +# 线程池隔离 +- 服务提供端的工作线程是一个线程池,路由到本地的服务请求会被放入这个线程池执行,如果客户端没有提供接口或方法进行路由,我们就可以在服务提供端进行控制,也就是进行服务端线程池隔离。具体的做法其实十分类似于请求调用方根据接口、方法、参数进行的路由。在服务提供端,工作线程池不止一个,而是多个,当定位到服务后,我们根据服务名称、方法、参数来确定具体执行服务调用的线程池是哪个。这样不同的线程池就是隔离的,不会出现争抢线程资源的情况。 + +# 服务提供端的流控处理 +- 在服务提供者看来,不同来源的服务调用者、0-1的开关以及限制具体数值的QPS的方式都需要实现。并且在服务提供者这里,某个服务或者方法可以对不同服务调用者进行不同对待。这样的做法就是对不同的服务调用者进行分级,确保优先级高的服务调用者而被优先提供服务。 + +# 服务升级 +- 对于服务升级,会遇到两种情况。第一种情况是接口不变,只是代码本身进行完善。这样的情况处理起来比较简单,因为提供给使用者的接口、方法都没有变,只是内部的服务实现有变化。这种情况下,采用灰度发布的方式验证然后全部发布就可以了。第二种情况是需要修改原有的接口,这又分成以下两种情况: +- 一是在接口中添加方法,这个情况比较简单,直接增加方法就行了。并且需要使用新方法的调用者就使用新方法,原来的调用者继续使用原来的方法即可。 +- 二是要对接口的某些方法修改调用的参数列表,有几种方式来应对: + - 1)对使用原来方法的代码都进行修改,,然后和服务端一起发布。这种方法不太可行,因为这要求我们同时发布多个系统,而且一些系统可能并不会从调整参数后的方法那里受益。 + - 2)通过版本号来解决。使用老方法的系统继续调用原来版本的服务,而需要使用新方法的系统则使用新版本的服务。 + - 3)从设计方法上考虑参数的扩展性,比如参数为Map<String,Object>。这个方法不太好,不直观,而且对参数的校验会比较复杂。 + +- + +# 服务治理 +- 服务治理可以分为管理服务和查看服务。 +# 服务查看 +- +# 服务管理 +- +# 服务框架与ESB的对比 + +- +ESB的概念是从SOA发展过来的,它是对多样系统中的服务调用者和服务提供者的解耦。ESB本身也可以解决服务化的问题,它提供了服务暴露、接入、协议转换、数据格式转换、路由等方面的支持。ESB和服务框架主要有两个差异。第一,服务框架是一个点对点的模型,而ESB是一个总线式的模型;第二,服务框架基本上是面向同构的系统,不会重点考虑整合的需求,而ESB会更多地考虑不同厂商所提供服务的整合。 +- + +# 分布式一致性 +# CAP+BASE +# CAP +- CAP定理是2000年,由 Eric Brewer 提出来的。Brewer认为在分布式的环境下设计和部署系统时,有3个核心的需求,以一种特殊的关系存在。这里的分布式系统说的是在物理上分布的系统。 +- CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。 +- Consistency(一致性):所有的节点在同一时间读到相同的数据。这就是数据上的一致性,当数据写入成功后,所有的节点会同时看到这个新的数据 +- Availability(可用性):保证无论是成功还是失败,每个请求都能够收到一个反馈。这就是数据的可用性。重点是系统一定要有响应。 +- Partition-Tolerance(分区容错性):即使系统中有部分问题或者有消息的丢失,系统仍能够继续运行,这被称为分区容错性,也就是在系统的一部分出问题时,系统仍能继续工作。 +- 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类: + +- CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。 +- 一种较为简单的做法是将所有的数据(或者是仅仅与事务相关的数据)都放在一个分布式节点上。这样的做法虽然无法100%地保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响,放弃P的同时也意味着放弃了系统的可扩展性。 + +- CP - 满足一致性,分区容错性的系统。 +- 一旦系统遇到网络分区或者其他故障时,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常服务,即不可用。 + +- AP - 满足可用性,分区容错性的系统,通常可能对一致性要求低一些。 +放弃一致性指的是放弃强一致性,而保留数据的最终一致性,这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。这就引入了一个时间窗口的概念,具体多久能够达到数据一致性取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短。 + +- CA 传统单机数据库 +- AP 很多NoSQL +- CP 网络的问题可能会让整个系统不可用 +# BASE +- BASE就是为了解决关系数据库强一致性引起的问题而造成可用性降低而提出的解决方案。 +- BASE其实是下面三个术语的缩写: +- 基本可用(Basically Available) +- 软状态(Soft state) +- 最终一致(Eventually consistent) +- 它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上改观。为什么这么说呢,缘由就在于大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用另外一种方式来完成,这里BASE就是解决这个问题的办法。 +## 基本可用 +- 分布式系统在出现不可预知故障的时候,允许损失部分可用性,比如 + - 1)响应时间上的损失:出现故障时响应稍慢 + - 2)功能上的损失:部分用户可能会被引导到一个降级页面 + +## 软状态 +- 允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 +## 最终一致性 +- 系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 +- 在实际工程实践中,最终一致性存在以下五类主要变种: + +# 数据一致性模型 +-   一些分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器,由于维护数据副本的一致性代价高,因此许多系统采用弱一致性来提高性能,一些不同的一致性模型也相继被提出。 + +- 强一致性: 要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。 +- 弱一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。 +- 最终一致性:是弱一致性的一种特例,保证用户最终能够读取到某操作对系统特定数据的更新。 +- 常用的锁实现算法有Lamport bakery algorithm (俗称面包店算法), 还有Paxos算法以及乐观锁。 +- Paxos算法是2PC的升级版,比2PC更加轻量级的保证一致性的协议。 +# 一致性协议 +# 2PC +- 见上面的2PC +# 3PC(3 Phase Coimmit) +- 是2PC的改进版,其将2PC的提交事务请求过程一分为二,形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。 +- 在2PC中一个参与者的状态只有它自己和协调者知晓,假如协调者提议后自身宕机,一个参与者又宕机,其他参与者就会进入既不能回滚、又不能强制commit的阻塞状态,直到参与者宕机恢复。这引出两个疑问: +- 能不能去掉阻塞,使系统可以在commit/abort前回滚(rollback)到决议发起前的初始状态? +- 当次决议中,参与者间能不能相互知道对方的状态,又或者参与者间根本不依赖对方的状态? + + +## 阶段一:CanCommit +- 1、事务询问 +- 协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应 +- 2、各参与者向协调者反馈事务询问的响应 +- 参与者接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈yes响应,并进入预备状态,否则反馈no响应。 +## 阶段二:PreCommit +- 在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正确情况下,包含两种可能。 +### 执行事务预提交 +- 假如协调者从所有的参与者获得的反馈都是yes,那么就会执行事务预提交。 +- 1、发送预提交请求 +- 协调者向所有参与者节点发出preCommit请求,并进入Prepared阶段 +- 2、事务预提交 +- 参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中 +- 3、各参与者向协调者反馈事务执行的响应 +- 如果参与者成功执行了事务操作,那么就会反馈给协调者ACK响应,同时等待最终的指令:提交(commit)或中止(abort)。如果失败,那么会反馈给协调者no响应。 +### 中断事务 +- 假如任何一个参与者向协调者反馈了no响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。 +- 1、发送中断请求 +- 协调者向所有参与者节点发出abort请求 +- 2、中断事务 +- 无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务 +## 阶段三:doCommit +- 该阶段将进行真正的事务提交,会存在以下两种可能的情况 +### 执行提交 +- 1、发送提交请求 +- 假设协调者处于正常工作状态,并且它接收到了来自所有参与者的ACK响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求。 +- 2、事务提交 +- 参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行阶段占用的事务资源 +- 3、反馈事务提交结果 +- 参与者在完成事务提交之后,向协调者发送ACK消息 +- 4、完成事务 +- 协调者接收到所有参与者反馈的ACK消息后,完成事务。 +### 中断事务 +- 进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈no响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。 +- 1、发送中断请求 +- 协调者向所有的参与者发送abort请求 +- 2、事务回滚 +- 参与者接收到abort请求后,会利用其在阶段二记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。 +- 3、反馈事务回滚结果 +- 参与者在完成事务回滚之后,向协调者发送ACK消息 +- 4、中断事务 +- 协调者接收到所有参与者反馈的ACK消息后,中断事务。 + +- 一旦进入阶段三,可能会存在以下两种故障: + - 1)协调者出现问题 + - 2)协调者和参与者之间的网络出现故障 +- 无论出现哪种情况,都会导致参与者无法及时接收到来自协调者的doCommit或abort请求,针对这种异常情况,参与者会在等待超时之后,继续进行事务提交。 +## 优缺点 +- 优点:相较于2PC,最大的优点是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致 +- 缺点:在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,此时该参与者依然会进行事务的提交,这必然会出现数据的不一致性。 + +# Paxos +- 如何在一个可能发生机器宕机或者网络异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,不会破坏数据一致性。 +## 拜占庭将军问题 +- 拜占庭帝国有许多支军队,不同军队的将军之间必须制定一个统一的行动计划,从而做出仅供或者撤退的决定,同时,各个将军在地理上都是被分隔开的,只能依靠军队的通讯员来进行通讯。然而,在所有的通讯员中可能会存在叛徒,这些叛徒可以篡改信息,从而达到欺骗将军的目的。 + +- 事实上,大多数系统都是部署在同一个局域网中的,因此消息被篡改的情况非常罕见。另一方面,由于硬件和网络原因造成消息不完整的问题,只需一套简单的校验算法既可避免。因此,在实际工程时间中,可以假设不存在拜占庭将军问题,也就是假设所有消息都是完整的,没有被篡改的,那么,在这种情况下,需要什么样的算法来保证一致性呢? +## Paxos场景 +- 在古希腊有一个叫做Paxos的小岛,岛上采用议会的形式来通过法令,议会中的议员通过信使进行消息的传递。议员和信使都是兼职的,他们随时有可能会离开议会厅,并且信使可能会重复地传递消息,也可能一去不复返。因此,议会协议要保证在这种情况下法令仍然能够正确的产生,并且不会出现冲突。 +## 算法 +- 将所有节点都写入同一个值,且被写入后不再更改。 +- proposer将发起提案(value)给所有accpetor,超过半数accpetor获得批准后,proposer将提案写入accpetor内,最终所有accpetor获得一致性的确定性取值,且后续不允许再修改。 + +- 一、两个操作: +- Proposal Value:提议的值; +- Proposal Number:提议编号,可理解为提议版本号,要求不能冲突; +- 二、三个角色: +- Proposer:提议发起者。Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,可以是任何操作,比如“设置某个变量的值为value”。不同的 Proposer 可以提出不同的 value,例如某个Proposer 提议“将变量 X 设置为 1”,另一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos过程,最多只有一个 value 被批准。 + - Acceptor:提议接受者;Acceptor 有 N 个,Proposer 提出的 value 必须获得超过半数(N/2+1)的 Acceptor批准后才能通过。Acceptor 之间完全对等独立。 +- Learner:提议学习者。上面提到只要超过半数accpetor通过即可获得通过,那么learner角色的目的就是把通过的确定性取值同步给其他未确定的Acceptor。 + +- 三、协议过程 +### 准备阶段 +#### 第一阶段A:Proposer发送Prepare +- Proposer生成全局唯一且递增的提案ID(比如时间戳+IP+序列号),向Acceptor集群发送请求,无需携带提案内容,只携带提案ID即可(提案ID称为Pn)。 + + +#### 第一阶段B:Acceptor应答Proposer +- Acceptor收到提案请求Pn过后,做出以下约定: + - 1)不再应答<= Pn的请求 + - 2)对于Accept请求也不做处理 +- Acceptor会做的处理: + - 1)应答前要保存/持久化当前提案ID proposalID + - 2)如果当前请求的提案ID(Pn)大于此前存放的proposalID,则将proposalID更新为当前ID(应答)。 + +### 接受阶段 +#### 第二阶段A:Proposer发送Accept +- Proposer收集到多数派(过半)应答Prepare阶段的返回值后,从中选择proposalID最大的提案内容,作为要发起Accept的提案,如果这个提案内容为空值,则可以随意决定提案内容。然后携带上当前proposalID,向Acceptor集群发送Accept请求。 +#### 第二阶段B:Acceptor应答Accept +- Acceptor接收到Accept请求后,检查不违背自己之前作出约定的情况下,保存/持久化当前proposalID和提案内容。最后Proposer收集到多数派应答的Accept回复后,形成决议。 + +- + +# Quorum/NWR +- 借鉴了Paxos的思想,实现上更加简洁,同样解决了在多个节点并发写入时的数据一致性问题。 +- 这个协议有三个关键字N、R、W。 +- • N代表数据所具有的副本数。 +- • R表示读取一个数据需要读取的拷贝的份数(值越大读性能越差) +- • W表示更新一个数据对象时需要确保成功的份数(值越大写性能越差) + +- 该策略中,只需要保证R+W>N,就可以保证强一致性。 +- 例如:N=3,W=2,R=2,那么表示系统中数据有3个不同的副本,当进行写操作时,需要等待至少有2个副本完成了该写操作系统才会返回执行成功的状态,对于读操作,系统有同样的特性。由于R + W > N,因此该系统是可以保证强一致性的。 +-   R + W> N会产生类似Quorum的效果。该模型中的读(写)延迟由最慢的R(W)副本决定,有时为了获得较高的性能和较小的延迟,R和W的和可能小于N,这时系统不能保证读操作能获取最新的数据。 +-   如果R + W > N,那么分布式系统就会提供强一致性的保证,因为读取数据的节点和被同步写入的节点是有重叠的。在关系型数据管理系统中,如果N=2,可以设置为W=2,R=1,这是比较强的一致性约束,写操作的性能比较低,因为系统需要2个节点上的数据都完成更新后才将确认结果返回给用户。 +-   如果R + W ≤ N,这时读取和写入操作是不重叠的,系统只能保证最终一致性,而副本达到一致的时间则依赖于系统异步更新的实现方式,不一致性的时间段也就等于从更新开始到所有的节点都异步完成更新之间的时间。 + +- R和W的设置直接影响系统的性能、扩展性与一致性。如果W设置为1,则一个副本完成更改就可以返回给用户,然后通过异步的机制更新剩余的N-W的副本;如果R设置为1,只要有一个副本被读取就可以完成读操作,R和W的值如较小会影响一致性,较大则会影响性能,因此对这两个值的设置需要权衡。 +- 下面为不同设置的几种特殊情况: +- 1. 当W=1,R=N时,系统对写操作有较高的要求,但读操作会比较慢,若N个节点中有节点发生故障,那么读操作将不能完成。 +- 2. 当R=1,W=N时,系统对读操作有较高性能、高可用,但写操作性能较低,用于需要大量读操作的系统,若N个节点中有节点发生故障,那么些操作将不能完成。 + - 3. 当R=Q,W=Q(Q=N/2+1)时,系统在读写性能之间取得平衡,兼顾了性能和可用性。 + +- + +# Raft +- Raft把一致性问题分解成为三个小问题: + +- leader election 选举 +- log replication 日志复制 +- safety 安全性 +## 基本概念 +- 每个Server有三个状态: leader, follower, candidate + +- follower: 不发request而只会回复leader和candidate的request. +- leader: 处理client发过来的请求 +- candidate: leader的候选人 +- Raft把时间分为terms. 每一个term开始时都进行一次选举. 每一个term里最多有一个leader, 或者没有leader. + +## RPC实现 +- 算法需要两种RPC +### RequestVote RPC +- 由candidates在选举过程中发起,当另外一个server收到这个RPC之后, 只有当对方term和log都至少和自己的一样新的时候才会投赞成票,收到多数赞成票的candidate会当选leader. + +### AppendEntries RPC +- 由leader发起用来分发日志, 强迫follwer的log和自己一致. + +## Leader election +- 如果一个follower在election timeout的时间里没有收到leader的信息,就进入新的term,转成candidate,给自己投票,发起选举 RequestVote RPC. 这个状态持续到发生下面三个中的任意事件: + - 1)它赢得选举 + - 2)另外有Server赢得选举 + - 3)1个term过去了,还是没有选举结果(Split Votes) + + - 为什么会有3)这个情况呢,就是当如果大家同时发起选举,都投给自己,那就没有Server能够得到多数选票了,这个时候就要进入下一个term,再选一次. 为了避免这个情况持续发生,每个Server的election timeout被随机的设成不同的值,所以先timeout的就可以先发起下一次选举. + +- 初始状态 ABC 都是 Follower,然后发起选举这时有三种可能情形发生。下图中前二种都能选出 Leader,第三种则表明本轮投票无效(Split Votes),每方都投给了自己,结果没有任何一方获得多数票。之后每个参与方随机休息一阵(Election Timeout)重新发起投票直到一方获得多数票。这里的关键就是随机 timeout,最先从 timeout 中恢复发起投票的一方向还在 timeout 中的另外两方请求投票,这时它们就只能投给对方了,很快达成一致。 +## Log replication +- 选好leader之后就可以分发log啦. +- 每一个log都有一个log index 和 term number. 当大多数的follower都复制好这个log时,就说这个log是committed,可以执行了. Leader 记住已经commit的最大log index, 用它来分发下一个 AppendEntries RPC. 这个和TCP里段的编号的作用是一样的. +- 当一个leader重新选出来时,它的log和follower的log可能不一致,那么它会强制所有的follower都和自己的log一致.首先leader要找到和follower之间的最大的编号一致的log,然后覆盖掉那之后的log. + +- 数据的流向只能从 Leader 节点向 Follower 节点转移。当 Client 向集群 Leader 节点提交数据后,Leader 节点接收到的数据处于未提交状态(Uncommitted),接着 Leader 节点会并发向所有 Follower 节点复制数据并等待接收响应,确保至少集群中超过半数节点已接收到数据后再向 Client 确认数据已接收。一旦向 Client 发出数据接收 Ack 响应后,表明此时数据状态进入已提交(Committed),Leader 节点再向 Follower 节点发通知告知该数据状态已提交。 + +## Safety +### Follower宕机 +- 但是到目前为止仍然不能保证安全性.比如说, 当leader在commit log时, 某follower宕机,然后这个follower后来被选为leader,它会覆盖掉现在follwer那些已经committed log, 由于这些log是已经执行过的,所以结果不同的机器就执行不同的指令. 在选举过程中,再加多一个限制就可以防止这种情况发生, 即: + +- Leader completeness property: +- 对于任意一个term, leader都要包含所有在之前term里committed的log。 +### Leader宕机 +#### 数据到达 Leader 节点前 +- 这个阶段 Leader 挂掉不影响一致性 + +#### 数据到达 Leader 节点,但未复制到 Follower 节点 +- 这个阶段 Leader 挂掉,数据属于未提交状态,Client 不会收到 Ack 会认为超时失败可安全发起重试。Follower 节点上没有该数据,重新选主后 Client 重试重新提交可成功。原来的 Leader 节点恢复后作为 Follower 加入集群重新从当前任期的新 Leader 处同步数据,强制保持和 Leader 数据一致。 + +#### 数据到达 Leader 节点,成功复制到 Follower 所有节点,但还未向 Leader 响应接收 +- 这个阶段 Leader 挂掉,虽然数据在 Follower 节点处于未提交状态(Uncommitted)但保持一致,重新选出 Leader 后可完成数据提交,此时 Client 由于不知到底提交成功没有,可重试提交。针对这种情况 Raft 要求 RPC 请求实现幂等性,也就是要实现内部去重机制。 + +#### 数据到达 Leader 节点,成功复制到 Follower 部分节点,但还未向 Leader 响应接收 +- 这个阶段 Leader 挂掉,数据在 Follower 节点处于未提交状态(Uncommitted)且不一致,Raft 协议要求投票只能投给拥有最新数据的节点。所以拥有最新数据的节点会被选为 Leader 再强制同步数据到 Follower,数据不会丢失并最终一致。 + +#### 数据到达 Leader 节点,成功复制到 Follower 所有或多数节点,数据在 Leader 处于已提交状态,但在 Follower 处于未提交状态 +- 这个阶段 Leader 挂掉,重新选出新 Leader 后的处理流程和阶段 3 一样。 + +#### 数据到达 Leader 节点,成功复制到 Follower 所有或多数节点,数据在所有节点都处于已提交状态,但还未响应 Client +- 这个阶段 Leader 挂掉,Cluster 内部数据其实已经是一致的,Client 重复重试基于幂等策略对一致性无影响。 + +#### 网络分区导致的脑裂情况,出现双 Leader +- 网络分区将原先的 Leader 节点和 Follower 节点分隔开,Follower 收不到 Leader 的心跳将发起选举产生新的 Leader。这时就产生了双 Leader,原先的 Leader 独自在一个区,向它提交数据不可能复制到多数节点所以永远提交不成功。向新的 Leader 提交数据可以提交成功,网络恢复后旧的 Leader 发现集群中有更新任期(Term)的新 Leader 则自动降级为 Follower 并从新 Leader 处同步数据达成集群数据一致。 + + +# MVCC +- 多版本并发控制(Multiversion concurrency control) +- 是一种宽松的设计,读写相互不阻塞。 +- 与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control。 + +- MVCC协议中,每个用户在连接数据库时看到的是一个具有一致性状态的镜像,每个事务在提交到数据库之前对其他用户均是不可见的。当事务需要更新数据时,不会直接覆盖以前的数据,而是生成一个新的版本的数据,因此一条数据会有多个版本存储,但是同一时刻只有最新的版本号是有效的。因此,读的时候就可以保证总是以当前时刻的版本的数据可以被读到,不论这条数据后来是否被修改或删除。 +# Gossip +- 在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。 + +- 要注意到的一点是,即使有的节点因宕机而重启,有新节点加入,但经过一段时间后,这些节点的状态也会与其他节点达成一致,也就是说,Gossip天然具有分布式容错的优点。 +- Gossip是一个带冗余的容错算法,更进一步,Gossip是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在”最终“所有节点一致,”最终“是一个现实中存在,但理论上无法证明的时间点。 + +- 因为Gossip不要求节点知道所有其他节点,因此又具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。实际上Gossip可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。 + +- 但Gossip的缺点也很明显,冗余通信会对网路带宽、CUP资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度,后面我们会讲在各种场合下的优化方法。 + +## 状态的传播 +- A节点首先知道了msg,它首先将此msg传播给集群中的部分节点(比如相邻节点),B和C。后者再将其传递到它们所选择的部分节点。以此类推,最终来自于A的msg在数轮交互后被传播到了集群中的所有节点。 +- 在分布式系统中,msg可能是某个节点所感知的关于其他节点是否宕机的认识;也可能是数据水平拆分的缓存集群中,关于哪些hash桶分布在哪些节点上的信息。每个结点起初只掌握部分状态信息,不断从其他节点收到gossip msg,每个节点逐渐地掌握到了整个集群的状态信息,因此解决了状态同步的第一个问题:全局状态的获取。 + +- 对于集群中出现的部分网络分隔,因为信息也能通过别的路径传播到整个集群,所以可以解决。 + +## 状态的一致 +- 状态同步的第二个问题:对于同一条状态信息,不同的节点可能掌握的值不同,也能通过基于gossip通信思路构建的协议包版本得到解决。 +- 以Reids水平拆分的集群为例: + +- 此时各个节点预先通过某种协议(比如gossip)得知了集群的状态全集,此时新加入了节点D。 + + - D分担了C的的8号哈希桶,此时C/D和集群中的其他节点就C所拥有哪些hash桶这件事产生了产生了分歧:A、B认为C目前有6、7、8号哈希桶。此时通过gossip消息体引入版本号,使得关于C的最新状态信息(只有6、7)在全集群达到一致。 + - 例如B收到来自A和C的gossip消息时会将版本号最新的消息(来自C的v2)更新到自己的本地副本中。 +- 各个节点的本地副本保存的集群全量状态也可以用来表示各个节点的存活状态。 + +- 例如A和C的网络断开,但A和C本身都正常运行,此时A和C互相无法通信,C会将A标记为dead、对于中心化思路的协议,如果C恰好是中心节点,那么A不可用的信息将会同步到集群的所有节点上,使得这些节点将其实可用的A也标记为宕机。而基于gossip这类去中心化的协议进行接收到消息后实现逻辑扩展(例如只有当接收到大多数的节点关于A已经宕机的消息时,才更新A的状态),最终保证A不被误判为宕机 +## 特性总结 +- gossip的核心是在去中心化结构下,通过信息的部分传递,达到全集群的状态信息传播,传播的时间收敛在O(logn)以内,N是结点数量。同时基于gossip协议,可以构建出状态一致的各种方案。 + +- + +# 分布式锁 +- 分布式锁 zk实现分布式锁的方式;redis实现;数据库乐观锁 + +- 从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库 +- 从性能角度(从高到低): 缓存 > Zookeeper >= 数据库 +- 从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库 +# 数据库悲观锁 +- 直接建一张表,里面记录锁定的方法名 时间 即可。 +- CREATE TABLE `methodLock` ( + - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', + - `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', + - `desc` varchar(1024) NOT NULL DEFAULT '备注信息', +- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', +- PRIMARY KEY (`id`), +- UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE +- ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; + +- 获得锁: +- select * from methodLock where method_name = #{currentMethod} for update; +- 释放锁: +- commit +## 缺点 + - 1)这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 + - 2)这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 + +- 这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。 +# 数据库乐观锁 +- select state,version from methodLock where method_name = #{currentMethod}; + +- update methodLock set state = ‘locked’,version = version + 1,update_time = now() where method_name = #{currentMethod} and state = ‘unlock’ and version = #{version} +- 乐观锁只能解决持久化是DB数据的一次更新问题。假如你的数据不是在DB,或者一个过程有三个数据的更新操作,线程A更新了数据1和数据2,线程B更新了数据3,乐观锁就不能起作用。 +# 缓存 +- 相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。 +- 目前有很多成熟的分布式产品,包括Redis、memcache、Tair等。 + +- 获取锁的使用,使用setnx加锁,将值设为当前的时间戳,再使用expire设置一个过期值。 +- 获取到锁则执行同步代码块,没获取则根据业务场景可以选择自旋、休眠、或做一个等待队列等拥有锁进程来唤醒(类似Synchronize的同步队列),在等待时使用ttl去检查是否有过期值,如果没有则使用expire设置一个。 +- 执行完毕后,先根据value的值来判断是不是自己的锁,如果是的话则删除,不是则表明自己的锁已经过期,不需要删除。(此时出现由于过期而导致的多进程同时拥有锁的问题) + + +## 优点 +- 性能好,实现起来较为方便。 + +## 缺点 +- 通过超时时间来控制锁的失效时间并不是十分的靠谱。 +- 非可重入 +# Zookeeper +- 基于zookeeper临时有序节点可以实现的分布式锁。 + +- 大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。 +## 优点 + - 1)无单点问题。ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。 + + - 2)持有锁任意长的时间,可自动释放锁。使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。 + + - 3)可阻塞。使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。 + + - 4)可重入。客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。 + +- zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。 + +## 缺点 +- zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。 +- + +# 注册查找中心/软负载中心 +# 职责 +- 软负载中心有两个最基础的职责: + - 1)聚合地址信息 + + - 2)生命周期感知 +- 注册查找中心需要对服务的上下线自动感知,并且根据这个变化去更新服务地址数据,形成新的地址列表后,把数据传给需要数据的调用者或者消息的发送者和接收者。 + +# 结构 +- 包括客户端和服务端。服务端主要负责感知提供服务的机器是否在线,聚合提供者的机器信息,并且负责把数据传给使用数据的应用。客户端承载了两个角色,作为服务提供者,客户端主要是把服务提供者挺服务的具体信息主动传给服务端,并且随着提供服务的变化去更新数据;而作为服务使用者,客户端主要是向服务端告知自己所需要的数据并负责去更新数据,还要进行本地的缓存。 + +- 注册查找中心中内部有三部分重要的数据: + - 1)聚合数据:聚合后的地址信息列表通过dataId和group就可以定位到一个唯一的键值对 + - 2)订阅关系:服务使用者/数据订阅者把自己所需要的数据信息告诉注册查找中心,这就是一个订阅关系。订阅的粒度和聚合树的粒度是一致的,就是通过dataId和group来确定数据,那么会有dataId、group到数据订阅者的分组Id(consumerGroupId)的一个映射关系。当聚合的数据有变化时,也是通过订阅关系的数据找到需要通知的数据订阅者,然后去进行数据更新通知。 + - 3)连接数据:连接到注册查找中心的节点和注册查找中心已经建立的连接管理。连接数据以groupId作为key,然后对应管理这个物理连接的,基于长连接。当订阅的数据发生变化时,通过订阅关系找到需要通知的groupId,然后进行数据发送,完成对应用的数据更新。 +# 内容聚合 +- 内容聚合需要完成的工作主要有是两个: + - 1)保证数据正确性:并发场景下的数据聚合的正确性;另外需要考虑的是发布数据的机器短时间上下线的问题。 + - 2)高效地聚合数据 +- 可以用一个Map来存储数据,用dataId和groupId作为key,对应的value就是聚合后的数据。 +- Map<dataId,Map<groupId,List<String>address>> +- 有几个关键点要注意 +## 并发下数据正确性的保证 +- 加锁或并发容器 +## 数据更新、删除的顺序保证 + +- NIO+Selector时更新、新增数据和连接断开要去删除数据就可能在两个线程中处理。而如果是发布数据后很快断开,那么保证在内部按照顺序来处理就很关键。因为如果顺序不保证,我们就可能先处理了删除数据,然后再处理新增,这样数据就不对了。一个解决的办法是在插入数据时判断当前产生数据的发布方的连接是否存在。 +## 大量数据同时插入、更新时的性能保证 +- 并发容器选用ConcurrentHashMap。 +- 对于同样的dataId,group对应的数据保存,采用LinkedList需要加锁。根据dataId、group进行分线程的处理,我们可以保证同一个dataId,group的数据是在同一个线程中处理,这样可以把整个数据结构变成一个不需要锁的数据结构。 +- 对于同样dataId、group的增改删是可以分线程处理的,读取自然也可以分线程。分线程的话可以实现任务队列来实现。 + +# 服务上下线的感知 +- 当服务可用时,需要自动把服务加到地址列表中,而服务不可用时,需要自动从列表中删除。 +- 主要有两种实现方式: +## 通过客户端和服务器的连接感知 +- 无论是消息发布者还是接收者都与注册查找中心的服务器维持一个长连接,可以通过长连接上的心跳来判断服务发布者是否还在线。 +- 可能存在的问题: + - 1)注册查找中心自身的负载很高时,可能会来不及处理心跳树,会以为心跳超时而判定服务不在线。 + - 2)如果服务发布者到注册查找中心的网络有问题,而服务发布者到服务使用者的网络没有问题,也会造成感知的问题。 + +- 解决办法是在注册查找中心的客户端上增加逻辑,当收到注册查找中心通知应用下线数据时,需要服务调用者进行验证才能接收这个通知。但是这个方法带来的是对每个服务提供者的一次额外验证。 +## 通过对于发布数据中提供的地址接口进行连接的检查 +- 通过外部的一个主动检查的方式去进行判断是一个补偿的方式,也就是当长连接的相关感知判断服务应用已经下线时,不直接认定这个服务已经下线,而是交给另一个独立的监控应用去验证这个服务是否已经不在了,方法一般是通过之前发布的地址、端口进行一下连接的验证,如果不能连接,则确认机器下线了。 +- 不过这种方式同样存在一个问题,即进行检查确认的这个系统也可能与服务提供者之间存在问题,同样需要服务调用者进行最终确认才能解决。 +# 数据分发 +## 数据分发与消息订阅的区别 +- 消息中间件的订阅、消息的接收和注册查找中心的订阅、消息的接收有什么区别? + - 1)消息中间件需要保证数据不丢失,每条消息都应该送到相关的订阅者;注册查找中心只需要保证最新数据送到相关的订阅者,不需要保证每次的数据变化都能让最终订阅者感知。 + - 2)订阅者分组。在消息中间件中,同一个集群中的不同机器是分享所有消息的(Queue模型),因为这个消息只要同一集群中的一台机器去处理就行了。而在注册查找中心中,需要把这个数据分发给所有的机器(Topic)。 +## 提升数据分发性能 + - 1)数据压缩 + - 2)全量和增量的选择 +- 建议刚开始的实现中采用简单的方式,也就是传送全量数据,当全量的数据很大时,就需要考虑采用增量传送的方式来实现了。 +# 针对服务化的特性支持 +## 数据分组 +- 分组group主要是为了隔离。分组本身就是一个命名空间,用来把相同dataId的内容分开。分组主要用在下面两种场景: + - 1)根据环境进行区分:适用于线下的环境,我们在线下开发、测试的环境中,需要对不同的环境、项目进行隔离和区分,而分组就可以很好地支持这一功能。 + - 2)分优先级的隔离:适用于线上运行系统的隔离。可以把提供相同服务的提供者用组的概念分开,重要的服务使用者会有专门的组来提供服务,而其他的服务使用者可能会共用一个默认的组。 +- 关于分组的方式,需要支持指定分组的API设置方式,以及根据IP地址自动归组的方式,根据IP地址自动进行归组可以带来更大的灵活性和运维的便利性。 +## 提供自动感知以外的上下线开关 +- 机器的上下线还需要通过指令而非机器状态来控制,通过指令直接从注册查找中心使机器下线。 +- 之所在机器的状态外进行控制,主要有以下两点考虑: + - 1)优雅地停止应用:应该先从服务列表中去掉这个机器,等待当时正在执行的服务结束,然后再停止应用。 + - 2)保持应用场景,用于排错:遇到服务的问题时,可以把出问题的服务留下一台进行故障定位和场景分析。这时需要把这台机器从服务列表中拿下来,以免有新的请求进来造成服务的失败。 +## 维护管理路由规则 +- 注册查找中心可以维护路由规则,不过这些数据与服务地址列表的特性不同。前者是持久化的,后者是非持久化的。 +# 集群 +- 集群会带来的问题: + - 1)数据管理问题:集群下数据应该怎么维护保存 + - 2)连接管理问题:数据发布者与数据订阅者的连接应该怎么管理 +## 数据统一管理方案 +- 把数据聚合放在一个地方,这样负责管理连接的机器就是无状态的了。 + +- 整个结构分为三层,聚合数据这一层就是在管理数据;注册查找中心的机器是无状态的;对于数据发布者和订阅者来说,选择注册查找中心集群中的任何一台机器连接皆可。 +- 对这个方案可以做一个修改,即把注册查找中心集群中的机器职责分开,就是把聚合数据的任务和推送数据的任务分到专门的机器上处理。 + +- 数据发布者和订阅者的连接是分开管理的,而集群中的应用分工更加明确。为了提升性能,在注册查找中心负责数据推送的机器上是可以对聚合数据做缓存的。 + +## 数据对等管理方案 + +- 将数据分散到注册查找中心的节点上,并且把自己节点管理的数据分发到其他节点上,从而保证每个结点都有整个集群的全部数据,并且这些节点的角色是对等的。 +- 同样的,使用注册查找中心的数据发布者和订阅者只需要去连接注册查找中心中的任何一台机器就可以了,数据发布者只需要把数据发布给这一台机器,而数据订阅者只需要从这一台机器上进行订阅。 + +- 在注册查找中心内部,各个节点之间会进行数据的同步。如果注册查找中心A需要把数据同步给注册查找中心B,那么A就作为一个数据发布者把数据发布给B就可以了。B基本可以按照一个普通的数据发布者来处理A,差别是当B需要把自己的数据发布给其他节点时,从A收到的数据是不需要发布的,因为A自己会去发布。 +- 这个方式可以复用现有的注册查找中心的客户端,不过也带来了同步效率的问题,可以通过批处理来提高效率。 +- 同样可以把集群内部的节点进行职责划分,一种是进行数据分发的节点,另一种是进行数据聚合的节点。负责数据聚合的节点之间是没有连接的,负责数据分发的节点之间也是没有连接的,负责数据聚合和负责数据分发的节点是有连接的。 + +# Zookeeper ectd Consul +- Zookeeper(ZAB,Paxos) +- Zookeeper是这种类型的项目中历史最悠久的之一,它起源于Hadoop,帮助在Hadoop集群中维护各种组件。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。其数据存储的格式类似于文件系统,如果运行在一个服务器集群中,Zookeper将跨所有节点共享配置状态,每个集群选举一个领袖,客户端可以连接到任何一台服务器获取数据。 + +- Zookeeper的主要优势是其成熟、健壮以及丰富的特性,然而,它也有自己的缺点,其中采用Java开发以及复杂性是罪魁祸首。尽管Java在许多方面非常伟大,然后对于这种类型的工作还是太沉重了,Zookeeper使用Java以及相当数量的依赖使其对于资源竞争非常饥渴。因为上述的这些问题,Zookeeper变得非常复杂,维护它需要比我们期望从这种类型的应用程序中获得的收益更多的知识。这部分地是由于丰富的特性反而将其从优势转变为累赘。应用程序的特性功能越多,就会有越大的可能性不需要这些特性,因此,我们最终将会为这些不需要的特性付出复杂度方面的代价。 + +- etcd(Raft) +- etcd是一个采用HTTP协议的健/值对存储系统,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。其很容易部署、安装和使用,提供了可靠的数据持久化特性。它是安全的并且文档也十分齐全。 +- etcd比Zookeeper是比更好的选择,因为它很简单,然而,它需要搭配一些第三方工具才可以提供服务发现功能。 + +- Consul(Gossip) +- Consul是强一致性的数据存储,使用gossip形成动态集群。它提供分级键/值存储方式,不仅可以存储数据,而且可以用于注册器件事各种任务,从发送数据改变通知到运行健康检查和自定义命令,具体如何取决于它们的输出。 + +- 与Zookeeper和etcd不一样,Consul内嵌实现了服务发现系统,所以这样就不需要构建自己的系统或使用第三方系统。这一发现系统除了上述提到的特性之外,还包括节点健康检查和运行在其上的服务。 + +- Zookeeper和etcd只提供原始的键/值队存储,要求应用程序开发人员构建他们自己的系统提供服务发现功能。而Consul提供了一个内置的服务发现的框架。客户只需要注册服务并通过DNS或HTTP接口执行服务发现。其他两个工具需要一个亲手制作的解决方案或借助于第三方工具。 + +- Consul为多种数据中心提供了开箱即用的原生支持,其中的gossip系统不仅可以工作在同一集群内部的各个节点,而且还可以跨数据中心工作。 +- + +# 配置管理中心 +- 开源的配置管理中心比较流行是的disconf。 +- 在最初的时候,只有注册查找中心,除了管理服务地址列表外,路由规则、消息的订阅关系等也都在注册查找中心保存。但这些数据的特性并不相同,可以从数据是否需要持久以及数据是否需要聚合两个维度对数据进行分类。 +- 持久指的是数据本身与发布者的生命周期无关的,典型的是持久订阅关系、路由规则、数据访问层的分库分表规则和数据库配置等;非持久指的是和发布者生命周期有关的,比如服务地址列表。此外,服务地址列表、订阅关系等数据是需要聚合的,而路由规则及一些设置项的内容则不需要聚合。 +- 注册查找中心管理的是非持久的数据,配置管理中心管理的是持久数据,二者都可以支持聚合的数据。 +- 对于配置管理中心来说,最为关心是稳定性和各种异常情况下的容灾策略,其次是性能和数据分发的延迟。配置管理中心存储的基本都是各个应用集群、中间件产品的关键管理配置信息,以及一些配置开关。 + +- 我们通过主备的持久存储来保存持久数据,一般采用关系型数据库。 +- 配置管理中心集群是由多个节点组成,这些节点是对等地,都可以提供数据给应用端,也都可以接收数据的更新请求并更改数据库,这些节点之间互不依赖。 +- 在配置管理中心的单个节点中,我们部署了Nginx和一个Web应用,Web应用主要负责完成相关的程序逻辑(数据库操作)以及根据IP等的分组操作。单机的本地文件则是为了容灾和提升性能,客户端进行数据获取时,最后都是从Nginx直接获取本地文件并把数据返回给请求段。 +- 对于配置管理中心的使用分为两部分: + - 1)提供给应用使用的客户端。主要是业务应用通过客户端去获取配置信息和数据,用于数据的读取。应用本身不去修改配置数据,而是根据配置来决定和更改自身应用的行为。 + - 2)为控制台或者控制脚本提供管理SDK +- 这个SDK包括了对数据的读写,提供管理SDK可以进行配置数据的更改。 +# 客户端实现和容灾策略 +- 客户端通过HTTP协议与配置管理中心进行通信。采用HTTP协议而不是私有协议可以更方便地支持多种语言的客户端,而且可以方便地进行测试和问题定位。可以采用HTTP长轮询的方式,数据分发的实时性比短轮询要好很多,和Socket长连接方式大体相同。HTTP长轮询是HTTP短轮询和Socket长连接的折中。 +- 容灾: + - 1)数据缓存:每次收到服务端更新后对数据的缓存。当服务端因忙而不能及时响应数据获取请求时,为应用提供一个可选的获取数据的方案。使用本地的缓存不能保证获取最新的数据,但是能保证获得比较新的数据。在一些场景下,应用需要的是获得相应的数据然后继续业务逻辑,是否是当下最细的数据可能不那么关键。 + - 2)数据快照:数据快照保存的是最近几次更新数据,数据是比缓存的数据旧一些,但是会保持最近的多个版本。数据快照用于服务端出现问题并且由于各种原因不能使用数据缓存时,例如缓存的最新的数据配置是一个有问题的配置,如果这是服务端不正常的话,就可以从更早几个版本的数据快照中进行恢复。 +# 服务端实现和容灾策略 +- 相比通过Web应用从数据库中获取数据,然后再把数据传给Nginx,通过Nginx返回本地文件的数据要快很多,能够很好地提高系统的吞吐量。除了作为静态化进行加速意外,本地文件处理还有一个重要的职责是进行数据库的容灾。有了本地文件,数据的读取就不再走数据库了,读取配置数据不需要数据库的参数。 +- 数据库是共享的,文件是本地私有的。后者是缓存。 + +- 在服务端需要做的另一件事情是和数据库的数据同步: + - 1)通过当前服务端更新数据库。由管理SDK的请求送到当前的服务端,服务端需要去更新数据库的数据,同时服务端也更新自身的本地文件,还可以通知其他机器去更新数据,不过只是传送一个更新数据的通知,而不是传送所有数据,并且这个通知也不是更新其他服务端数据的唯一方式。 + - 2)定时检查服务端的数据与数据库中数据的一致性。这是为了确保服务端本地文件和数据库内容的一致性,前面提到的如果数据更新通知不能送达其他服务端,那么其他服务端就要靠定时地检查来保证与数据库中数据的一致性。 +- 此外,根据IP地址的分组处理也是服务端的Web应用需要处理好的逻辑。 + +# 数据库策略 +- 数据库在设计时需要支持配置的版本管理,也就是随着配置内容的更改,老的版本是需要保留,这主要是为了方便进行配置变更的比对和回滚。而数据库本身需要主备进行数据的容灾考虑。 +# Disconf +- http://disconf.readthedocs.io/zh_CN/latest/ +- Distributed Configuration Management Platform(分布式配置管理平台) + +- 专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」。 +- Disconf是百度开源出来的一款基于Zookeeper的分布式配置管理软件。目前很多公司都在使用,包括滴滴、百度、网易、顺丰等公司。通过简单的界面操作就可以动态修改配置属性,还是很方便的。使用Disconf后发现的一大好处是省却应用很多配置,而且配置可以自动load,实时生效。 +- 通过简单的注解类方式 托管配置。托管后,本地不需要此配置文件,统一从配置中心服务获取。 +- 当配置被更新后,注解类的数据自动同步。 +- 基于Nginx、Tomcat、Zookeeper、Redis和MySQL。 + +- + +# 分布式搜索引擎 +# 爬虫问题 +- 对于全网搜索来说,需要通过爬虫去获取被检索的网站的网页信息。在站内搜索中,我们同样需要可以发现、获取要被搜索的内容的系统。对于内部搜索来说,进入搜索系统中的数据的来源、格式及要求更新的频率都是已知的,这位我们根据数据变化来更新索引带来了很大的便利。 +- 更新索引的方式一般有如下两种: + - 1)定时从数据库拉取,称为增量dump。这要求数据库记录中有一个记录变更时间的字段,而这个字段需要有索引。增量dump开始前,需要进行全量dump构造初始化数据。增量的时间间隔一般会在分钟级,这会引起明显延时。 + - 2)通过数据变更的通知,及时通知搜索引擎构建索引,即时性会很好,不过带来的系统压力也比较大,适用于对实时性要求很高的场景。 +# 倒排索引 +- 正排索引: + +- 可以通过文章找到这篇文章中的关键词,但是如果给定关键词,要找到该词出现在哪些文章中? + +- 相对于正排索引,倒排索引是把原来作为值的内容拆分为索引的key,而原来用作索引的key则变成了value。搜索引擎比数据库的like查询更高效的原因也在于倒排索引。 +- 如何确定建立倒排索引的关键字呢?取决于如何对要索引的内容进行分词。 + +# 查询预处理 +- 查询预处理主要负责对用户输入的搜索内容进行分词及分词后的分析,包括一些同义词的替换及纠错等,这一部分是在使用搜索引擎前对于要搜索内容的梳理环节,而这部分的工作也会影响到最后搜索结果的质量。 +# 相关度计算 +- 当经过了查询分析器的处理后,查询会在搜索引擎上被执行,对于返回的结果,我们需要计算和搜索内容的相关度后展示给用户。相关度计算是在不指定按照某个字段排序的基础上对搜索结果的排序,排序的规则就是被搜索到的内容与要搜索的内容之间的相关度。 +- 相关度的计算方式很多,例如有向量空间模型、概率模型等方法。 +# 分布式数据计算 +- 从实时性角度来讲,我们可以把计算分为实时计算和离线计算。 +# 离线计算 +- 离线计算是业务产生的数据离开生产环境后进行的计算,就是把业务数据从在线存储中移动到离线存储中,然后进行数据处理的过程。MapReduce模型是非常著名和常用的。 +# MapReduce +- 在Map阶段,我们根据设定的规则把整体数据集映射给不同的Worker来处理,并且声称各自的处理结果。而在Reduce阶段,是对前面处理过的数据进行聚合,形成最后的结果。 +- MapReduce模型让我们能够使用统一的模型和方式来使用集群中多机,降低了使用成本。 +- Hadoop是MapReduce的一个来源实现,Hadoop使用HDFS进行数据存储, 而Spark提供了基于内存的集群计算的支持。Spark本身是为集群计算中特定类型的工作而设计,例如进行机器学习的算法训练,而基于内存的方式使得Spark的速度非常快。 + +# 在线计算 +- 比较常见的方式是流式计算,Storm是使用的比较广泛的一个框架。 + + +- + +# 发布系统 +- 发布系统应该完成的任务 +# 分发应用 +- 我们需要提供自动高效并且容易操作的机制来把经过测试的程序包分发到线上的应用中,这里我们一般会采用Web的操作方式,通过专用通道把应用程序包从线下环境传送到线上的发布服务器。 + +# 启动校验 +- 当我们完成应用程序包的分发工作后,需要去停止当前应用上的程序,并完成新应用的启动。应用重新启动后,我们需要进行校验从而完成这台应用服务器上的应用发布。对应用的校验一般是由应用自身提供一个检测脚本或者页面,发布系统执行这个脚本或者访问页面后来判断返回的结果。 +- 在停止应用时,如果采用暴力方式,就会影响当时正在执行的请求,所以需要优雅地关闭。但是如果持续有新请求进入的话,是很难优雅关闭应用的,所以需要控制不能有新请求进入。这就需要在负载均衡或者注册查找中心上将当前应用移除,之后再关闭应用(结束所有请求后关闭),然后进行新应用的启动及检查,检查通过后,再把这个应用加入到负载均衡或者注册查找中心上,并对外提供应用。 + +# 灰度发布 +- 会对新应用进行分批发布,逐步扩大新应用在整个集群中的比例直至最后全部完成。 +# 产品改版Beta +- 面向最终用户的应用产品的改版会改变用户的习惯,对于这样的改变我们不会一刀切地直接推行,而会提供新旧应用的共存。应用本身会根据策略引流用户,对于发布系统来说,把新旧两个应用作为两个应用集群处理就行了。 +- + +# 应用监控系统 +- 监控系统主要分为监视和控制两部分 +# 监视 +# 数据监视维度 +- 监视的数据主要包括系统数据和应用自身的数据。系统数据比如CPU使用率,内存使用情况,交换分区使用情况,当前系统负载,IO情况等;应用自身的数据比如调用次数、成功率、响应时间、异常数量等维度的数据。 +# 数据记录方式 +- 系统自身的数据已经被记录到了本地磁盘上,应用的数据一般也是存放在应用自身的目录中。对于应用数据的记录,我们首先会用定时统计的方式记录一些量很大的信息。对于一个提供服务的应用,我们一般并不直接记录每次调用的信息,而是记录一段时间内的总调用次数、总响应时间这样的信息,而对于异常等信息,则每条都会予以记录。 +# 数据采集方式 +- 采集方式有应用服务器主动推送给监控中心以及等待监控中心拉取两种方式。 +# 展现与告警 +- 监控中心采集服务器收集的数据会集中存储,采用图表的方式可以提供Web页面的展示,并且根据设置的告警条件和接收人进行告警。 +# 控制 +- 控制是应用启动后在运行期对应用的行为改变。对于应用的运维,最低的要求是出现问题时 可以通过重启应用解决,但是我们还是需要更加精细化地控制应用,其实比较多的控制是进行降级和一些切换。 +- 降级是我们遇到大量请求且不能扩容的情况时所进行的功能限制的行为,可能针对某个功能的所有使用者进行限制,也可能是根据不同使用者来进行限制。 +- 切换更多的是当依赖的下层系统出现故障并且需要手工进行切换时的一个管理。这些控制一般是通过开关、参数设置来完成。 +- + +# 接口网关 +# 定义 + +- 接口网关,顾名思义,是企业 IT 在系统边界上提供给外部访问内部接口服务的统一入口。这里的外部可以指客户端、浏览器或者第三方应用等,在这种情况下,接口网关可以有多种定位: +- 提供后端服务面向 Web App 或者 Mobile App 的 APIGateway +- 作为开放平台面向 Partner 的 OpenAPI +- 主要功能:请求路由,安全认证 +# 请求路由 +- 企业提供内外两网,在没有接口网关时,提供外部服务的应用需要部署在外网。随着服务的增多,部署在外网的应用越来越多,在服务的安全压力与维护成本增大的情况下,需要一个统一的接口网关“隔离”内外服务。企业提供的服务(无论内部服务还是外部服务)均部署在内网,而由部署在外网的网关接受请求,并路由到内网服务。在这种情况下,既有利于对外屏蔽企业内部服务部署细节,提供统一的服务访问地址,又便于管理与维护内外部服务接口,便于演进与重构服务。这是接口网关提供请求路由的作用。 +# 安全认证 +- 在没有接口网关时,企业对外服务直接由外部访问,身份验证与数据加解密等工作都需要每一个对外服务本身去处理,增加了服务本不该有的职责,并且增加了服务开发的难度与工作量。实际在大多数情况下,可以将身份验证与数据加解密等安全工作可以从服务抽离,统一由接口网关负责处理。接口网关作为入口,对外验证调用方的 IP,身份以及接口访问权限等,并且可以解密数据后再将请求路由到服务。这是接口网关提供安全认证的功能。 +# Nginx+OpenResty +- OpenResty 是一个通过 Lua 扩展 Nginx 实现的可伸缩的 Web 平台。其核心是基于 Nginx 一个 C 模块将 Lua 语言嵌入到 Nginx 服务器中,对外提供一套完整的 Lua Web 的 API,并透明支持非阻塞 I/O,提供协程 —— “轻量级线程”、定时器等,从而极大地降低了高性能服务端的开发难度和开发周期。 + +- OpenResty 将两个极为优秀的组件 Nginx 与 Lua 进行糅合,一方面保留了 Nginx 高性能 web 服务特征,另一方面有提供 Lua 特性在极少损失性能情况下便于业务功能的开发。根据官网介绍,OpenResty 非常便于用来搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 + +- + +# 限流 +- 在实际的系统架构中,用户请求可能会经过多级才会到达应用节点,比如:nginx-->gateway-->应用。如果条件允许,可以在尽量靠前的位置做限流设置,这样可以尽早的给用户反馈,也可以减少后续层级的资源浪费。不过毕竟在应用内增加限流配置的开发成本相对来说较低,并且可能会更灵活,所以需要根据团队实际情况而定了。nginx做限流设置可以使用Lua+Redis配合来实现;应用内限流可以使用RateLimiter来做。当然都可以通过封装来实现动态配置限流的功能,比如【ratelimiter-spring-boot-starter】 +# 基于方法调用的限流 +- 限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。 +## 限制瞬时并发数 + - AtomicInteger atomic = new AtomicInteger(1) + +- function(){ +- try { +- if(atomic.incrementAndGet() > 限流数) { +- //熔断逻辑 +- } else { +- //处理逻辑 +- } +- } finally { +- atomic.decrementAndGet(); +- } +- } + +## 限制时间窗最大请求数 +- 即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速; + +``` +LoadingCache<Long, AtomicLong> counter = + CacheBuilder.newBuilder() + .expireAfterWrite(2, TimeUnit.SECONDS) + .build(new CacheLoader<Long, AtomicLong>() { + @Override + public AtomicLong load(Long seconds) throws Exception { + return new AtomicLong(0); + } + }); +long limit = 1000; +while(true) { + //得到当前秒 + long currentSeconds = System.currentTimeMillis() / 1000; + if(counter.get(currentSeconds).incrementAndGet() > limit) { + System.out.println("限流了:" + currentSeconds); + continue; + } + //业务处理 +} +``` + +- 使用Guava的Cache来存储计数器,过期时间设置为2秒(保证1秒内的计数器是有的),然后我们获取当前时间戳然后取秒数来作为KEY进行计数统计和限流,这种方式也是简单粗暴,刚才说的场景够用了。 + +## 令牌桶 + +- 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中 +- 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃 +- 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑 +- 参考令牌桶的算法描述,一般思路是在RateLimiter-client放一个重复执行的线程,线程根据配置往令牌桶里添加令牌,这样的实现由如下缺点: + +- 需要为每个令牌桶配置添加一个重复执行的线程 +- 重复的间隔精度不够精确:线程需要每1/r秒向桶里添加一个令牌,当r >1000 时间线程执行的时间间隔根本没办法设置(从后面性能测试的变现来看RateLimiter-client 是可以承担 QPS > 5000 的请求速率) + +- 基于上述的令牌桶算法 +- 将添加令牌改成触发式的方式,取令牌时做添加令牌的动作 +- 在取令牌的时候,通过计算上一次添加令牌和当前的时间差,计算出这段时间应该添加的令牌数,然后往桶里添加 +- curr_mill_second = 当前毫秒数 +- last_mill_second = 上一次添加令牌的毫秒数 +- r = 添加令牌的速率 +- reserve_permits = (curr_mill_second-last_mill_second)/1000 * r +- 添加完令牌之后再执行取令牌逻辑 + +- 单节点下GOOGLE GUAVA 提供的工具库中 RATELIMITER 类是非常好用的,底层也是基于令牌桶的思想。 + +# 基于IP的限流 +- 使用Lua+Redis来限制IP的访问频率 +- --查询redis中保存的ip的计数器 +- ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr) +-   +- if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out + -     res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1) +-     res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) +- else +-     ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1 +-    +-     if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time + -         res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1) +-         res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time) +-     else +-         res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count) +-         res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) +-     end +- end +# Kong +- Kong是一款基于Nginx_Lua模块写的高可用,易扩展由Mashape公司开源的API Gateway项目。由于Kong是基于Nginx的,所以可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。 + +- Kong主要有三个组件: + +- Kong Server :基于nginx的服务器,用来接收API请求。 +- Apache Cassandra/PostgreSQL :用来存储操作数据。 +- Kong dashboard:官方推荐UI管理工具,当然,也可以使用 restfull 方式 管理admin api。 + +- + From fd45d80f2083f2b99e3c3a3913db834ad39e227a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:44:09 +0800 Subject: [PATCH 76/97] =?UTF-8?q?Rename=20=E4=B8=89=E3=80=81Java=20?= =?UTF-8?q?=E9=9B=86=E5=90=88=20to=20=E4=B8=89=E3=80=81Java=20=E9=9B=86?= =?UTF-8?q?=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\270\211\343\200\201Java \351\233\206\345\220\210.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" => "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210.md" (100%) diff --git "a/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" "b/docs/\344\270\211\343\200\201Java \351\233\206\345\220\210.md" similarity index 100% rename from "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210" rename to "docs/\344\270\211\343\200\201Java \351\233\206\345\220\210.md" From d620d88ea003d1b8206b63c2925780fc48a88450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:44:41 +0800 Subject: [PATCH 77/97] =?UTF-8?q?Rename=20=E4=B8=83=E3=80=81JavaWeb=20to?= =?UTF-8?q?=20=E4=B8=83=E3=80=81JavaWeb.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\270\203\343\200\201JavaWeb.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\270\203\343\200\201JavaWeb" => "docs/\344\270\203\343\200\201JavaWeb.md" (100%) diff --git "a/docs/\344\270\203\343\200\201JavaWeb" "b/docs/\344\270\203\343\200\201JavaWeb.md" similarity index 100% rename from "docs/\344\270\203\343\200\201JavaWeb" rename to "docs/\344\270\203\343\200\201JavaWeb.md" From a8a2973595f25f0184c8b291e0ab0767c33654ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:45:03 +0800 Subject: [PATCH 78/97] =?UTF-8?q?Rename=20=E4=B9=9D=E3=80=81=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E7=B3=BB=E7=BB=9F=20to=20=E4=B9=9D=E3=80=81=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E7=B3=BB=E7=BB=9F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...43\200\201\346\223\215\344\275\234\347\263\273\347\273\237.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" => "docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237.md" (100%) diff --git "a/docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" "b/docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237.md" similarity index 100% rename from "docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237" rename to "docs/\344\271\235\343\200\201\346\223\215\344\275\234\347\263\273\347\273\237.md" From 9c905f7bd97f28825a4fb06dc5594c68459a83ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:45:26 +0800 Subject: [PATCH 79/97] =?UTF-8?q?Rename=20=E4=BA=94=E3=80=81JVM=20to=20?= =?UTF-8?q?=E4=BA=94=E3=80=81JVM.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\272\224\343\200\201JVM.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\344\272\224\343\200\201JVM" => "docs/\344\272\224\343\200\201JVM.md" (100%) diff --git "a/docs/\344\272\224\343\200\201JVM" "b/docs/\344\272\224\343\200\201JVM.md" similarity index 100% rename from "docs/\344\272\224\343\200\201JVM" rename to "docs/\344\272\224\343\200\201JVM.md" From 2a0a9b0398463b13ed6c7a782c08ec82dba54457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:45:43 +0800 Subject: [PATCH 80/97] =?UTF-8?q?Rename=20=E5=85=AB=E3=80=81Mysql=20to=20?= =?UTF-8?q?=E5=85=AB=E3=80=81Mysql.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\345\205\253\343\200\201Mysql.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\345\205\253\343\200\201Mysql" => "docs/\345\205\253\343\200\201Mysql.md" (100%) diff --git "a/docs/\345\205\253\343\200\201Mysql" "b/docs/\345\205\253\343\200\201Mysql.md" similarity index 100% rename from "docs/\345\205\253\343\200\201Mysql" rename to "docs/\345\205\253\343\200\201Mysql.md" From 5431da3972724aadc17d3b826e7f5313f082db4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 20:46:07 +0800 Subject: [PATCH 81/97] =?UTF-8?q?Rename=20=E5=BF=85=E8=AF=BB=EF=BC=81?= =?UTF-8?q?=EF=BC=81=EF=BC=81.md=20to=20=E9=9B=B6=E3=80=81=E5=BF=85?= =?UTF-8?q?=E8=AF=BB=EF=BC=81=EF=BC=81=EF=BC=81.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...45\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" => "docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" (100%) diff --git "a/docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" "b/docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" similarity index 100% rename from "docs/\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" rename to "docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" From 521d206e53d6bb58bb58ce4d113f84c52f135d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:13:00 +0800 Subject: [PATCH 82/97] =?UTF-8?q?Create=20=E5=8D=81=E4=B8=89=E3=80=81Linix?= =?UTF-8?q?-git-=E5=89=8D=E7=AB=AF.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\201Linix-git-\345\211\215\347\253\257.md" | 2892 +++++++++++++++++ 1 file changed, 2892 insertions(+) create mode 100644 "docs/\345\215\201\344\270\211\343\200\201Linix-git-\345\211\215\347\253\257.md" diff --git "a/docs/\345\215\201\344\270\211\343\200\201Linix-git-\345\211\215\347\253\257.md" "b/docs/\345\215\201\344\270\211\343\200\201Linix-git-\345\211\215\347\253\257.md" new file mode 100644 index 00000000..a746427e --- /dev/null +++ "b/docs/\345\215\201\344\270\211\343\200\201Linix-git-\345\211\215\347\253\257.md" @@ -0,0 +1,2892 @@ +# Linux +# CPU +# top +- top:查看每个进程的情况 + +- 在top模式下,输入1:查看每个CPU的性能数据,注意观察是否有CPU100%占用率 + + +- CPU参数含义: + + - 1)us过高表示Java应用程序消耗了大量CPU,需要定位是哪一个线程,并分析线程堆栈。 + +- 在top模式下,输入H:查看每个线程的性能信息 +- 如果某个线程CPU利用率一直100%,则说明这个线程可能有死循环,也有可能是GC的问题。jstat命令可以查看GC情况,是不是产生了FullGC。 + +- 还可以把线程dump下来,看看是哪个线程、执行什么代码造成的CPU利用率高。 +- jstack PID | grep ‘nid=十六进制线程id’ > /dir +- dump出来的线程ID(nid)是十六进制,TOP命令看到的线程ID是十进制的 + + - 2)sy过高表示花费了更多时间进行线程切换。同样需要查看Java线程的状态。 + +- + +# 文件IO 磁盘 +# ls +- 显示文件夹下的文件详情,包括大小 +- ls –al +- a表示all,包括隐藏文件 + +# stat(查看文件详情) +- stat filepath +# 软硬连接 +- 软硬连接(inode这块,ln / ln -s) +- 硬链接 (hard link) 与软链接(又称符号链接,即 soft link 或 symbolic link)。链接为 Linux 系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若一个 inode 号对应多个文件名,则称这些文件为硬链接。换言之,硬链接就是同一个文件使用了多个别名。硬链接可由命令 link 或 ln 创建。 + +- 软链接与硬链接不同,若文件用户数据块中存放的内容是另一文件的路径名的指向,则该文件就是软连接。软链接就是一个普通文件,只是数据块内容有点特殊。软链接有着自己的 inode 号以及用户数据块。 +- ln -s source target +# 文本操作命令 +- cat 由第一行开始显示内容,并将所有内容输出 +- tac 从最后一行倒序显示内容,并将所有内容输出 +- more 根据窗口大小,一页一页的显示文件内容 +- less 和more类似,但其优点可以往前翻页,而且进行可以搜索字符 +- head 只显示头几行 +- tail 只显示最后几行 +- sort、uniq +- sort 进行排序。 + +- $ sort [-fbMnrtuk] [file or stdin] +- -f :忽略大小写 +- -b :忽略最前面的空格 +- -M :以月份的名字来排序,例如 JAN, DEC +- -n :使用数字 +- -r :反向排序 +- -u :相当于 unique ,重复内容只出现一次 +- -t :分隔符,默认为tab +- -k :指定排序的区间 +- 范例:/etc/passwd 内容是以 : 来分隔的,以第三栏来排序。 + +- $ cat /etc/passwd | sort -t ':' -k 3 +- root:x:0:0:root:/root:/bin/bash +- dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash +- alex:x:1001:1002::/home/alex:/bin/bash +- arod:x:1002:1003::/home/arod:/bin/bash +- uniq 可以将重复的数据只取一个。 + +- $ uniq [-ic] +- -i :忽略大小写 +- -c :进行计数 + +- 使用cat命令可以显示文本文件内容,或 把几个文件内容附加到另一个文件中。 +- 使用more命令可以分页显示文本文件的内容。 +- 使用less命令可以回卷显示文本文件的内容。 +- 使用head命令可以显示指定文件的前若干行文件内容。 +- 使用tail命令可以查看文件的末尾数据。 +- 使用sort命令可以对文件中的数据进行排序,并将结果显示在标准输出上。 +- 使用uniq命令可以将文件内的重复行数据从输出文件中删除,只留下每条记录的唯一样本。 +- 使用cut命令可以从文件的每行中显示出选定的字节、字符或字段。 +- 使用comm命令可以比较两个已排过序的文件,并将其结果显示出来。 +- 使用diff命令可以逐行比较两个文本文件,列出其不同之处。它比comm命令完成更复杂的检查。它对给出的文件进行系统的检查,并显示出两个文件中所有不同的行,不要求事先对文件进行排序。 + +# vim + +- vi编辑器有3种基本工作模式,分别是命令行模式、插入模式和末行模式。 +- 命令行模式控制屏幕光标的移动,字符、字或行的删除,移动、复制某区域及进入插入模式,或者到末行模式。 +- 只有在插入模式下,才可以做文字输入,按“Esc”键可回到命令行模式。 +- 末行模式将文件保存或退出vi编辑器,也可以设置编辑环境,如寻找字符串、列出行号等。 + +- 在指令列模式下,有以下命令用于离开或者存储文件。 + +- 命令 作用 +- :w 写入磁盘 +- :w! 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 +- :q 离开 +- :q! 强制离开不保存 +- :wq 写入磁盘后离开 +- :wq! 强制写入磁盘后离开 +# 实时查看日志命令 +- tail -f path +- -f 循环读取 + +# 系统日志 +- /var/log/message 系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一 +- /var/log/secure 与安全相关的日志信息 +- /var/log/maillog 与邮件相关的日志信息 +- /var/log/cron 与定时任务相关的日志信息 +- /var/log/spooler 与UUCP和news设备相关的日志信息 +- /var/log/boot.log 守护进程启动和停止相关的日志消息 +- /var/log/wtmp 该日志文件永久记录每个用户登录、注销及系统的启动、停机的事件 +# find +- 查找磁盘上最大的文件的命令 +- find / -type f -size +10G +- 列出当前目录及子目录下所有文件和文件夹 + +- 在/home目录下查找以.txt结尾的文件名 find /home -name "*.txt" + +- 同上,但忽略大小写 find /home -iname "*.txt" +- 当前目录及子目录下查找所有以.txt和.pdf结尾的文件 find . \( -name "*.txt" -o -name "*.pdf" \) 或 find . -name "*.txt" -o -name "*.pdf" +- 匹配文件路径或者文件 find /usr/ -path "*local*" +- 基于正则表达式匹配文件路径 find . -regex ".*\(\.txt\|\.pdf\)$" +- 同上,但忽略大小写 find . -iregex ".*\(\.txt\|\.pdf\)$" +- 找出/home下不是以.txt结尾的文件 find /home ! -name "*.txt" +- 根据文件类型进行搜索find . -type 类型参数 + +- 类型参数列表: +- f 普通文件 +- l 符号连接 +- d 目录 +- c 字符设备 +- b 块设备 +- s 套接字 +- p Fifo + +- 基于目录深度搜索 +- 向下最大深度限制为3 find . -maxdepth 3 -type f +- 搜索出深度距离当前目录至少2个子目录的所有文件 find . -mindepth 2 -type f +- 根据文件时间戳进行搜索 find . -type f 时间戳 + +- UNIX/Linux文件系统每个文件都有三种时间戳: + +- 访问时间(-atime/天,-amin/分钟):用户最近一次访问时间。 +- 修改时间(-mtime/天,-mmin/分钟):文件最后一次修改时间。 +- 变化时间(-ctime/天,-cmin/分钟):文件数据元(例如权限等)最后一次修改时间。 +- 搜索最近七天内被访问过的所有文件 + +- find . -type f -atime -7 搜索恰好在七天前被访问过的所有文件 + +- find . -type f -atime 7 搜索超过七天内被访问过的所有文件 + +- find . -type f -atime +7 搜索访问时间超过10分钟的所有文件 + +- find . -type f -amin +10 找出比file.log修改时间更长的所有文件 + +- find . -type f -newer file.log 根据文件大小进行匹配 +- find . -type f -size 文件大小单元 文件大小单元: + +- b —— 块(512字节) +- c —— 字节 +- w —— 字(2字节) +- k —— 千字节 +- M —— 兆字节 +- G —— 吉字节 +- 搜索大于10KB的文件find . -type f -size +10k +- 搜索小于10KB的文件find . -type f -size -10k +- 搜索等于10KB的文件find . -type f -size 10k + +- 删除匹配文件 +- 删除当前目录下所有.txt文件 find . -type f -name "*.txt" -delete +- 根据文件权限/所有权进行匹配 +- 当前目录下搜索出权限为777的文件 find . -type f -perm 777 +- 找出当前目录下权限不是644的php文件 find . -type f -name "*.php" ! -perm 644 +- 找出当前目录用户tom拥有的所有文件 find . -type f -user tom + +- 将当前目录下所有以“.txt”结尾的文件打印出来,再追问,除了“.txt”再加上“.abc”结尾的也打印出来。 +- find . -name "*.txt" +- + +# grep +- 在文件内查找字符串 +- grep “字符串” filename +# 文件权限 + - r:可读(4) + - w:可写(2),对于目录来说表示可在目录中新建文件 + - x:可执行(1),对于目录来说为可进入到该目录中 +- -:表示无对应位上的权限 +- 4代表读权限,2代表写权限,1代表执行权限 + +- 7=4+2+1,表示拥有可读可写可执行权限 +- 5=4+1,表示拥有可读可执行权限,但是没有写权限 +- 4 代表拥有可读权限 +- 0 代表没有任何权限 + +# chmod 修改文件属性 +- chmod 777 /home/berry +- chmod u+x /home/berry + +- u 表示“用户(user)”,即文件或目录的所有者。 +- g 表示“同组(group)用户”,即与文件属主有相同组ID的所有用户。 +- o 表示“其他(others)用户”。 +- a 表示“所有(all)用户”。它是系统默认值。 + +# chown 修改文件的属主与属组 +- chown guest:guest a.txt +- chown -R guest /home/berry (把berry文件下的所有文件都改成guest这个组) +# chgrp 修改文件的所属的用户组 +- chgrp -R guest /var/tmp/f.txt +- chgrp - R root /home/berry/file/a.txt +# iostat(查看各个设备的IO状态,查看磁盘读写性能) +- 直接输入iostat: +- tps是每秒的IO请求数,这是IO消耗情况值得关注的数字。 +- Blk_read/s是指每秒读取的块数量,通过块的大小是512字节 +- Blk_read是指总共读取的块数量 + +- iostat –x xvda 3 5 定时采样查看IO消耗情况 +- 首先要关注的是CPU中的iowait%所占的百分比,当iowait占据主要的百分比时,就表示要关注IO方面的消耗状况了。 +# pidstat(查看某个线程的IO状态) +- pidstat –d –t –p [pid] 1 100 +- KB_rd/s 表示每秒读取的KB数 +- KB_wd/s 表示每秒写入的KB数 +- 通过pidstat直接找到文件IO操作多的线程,之后结合jstack找到对应的Java代码。 +# cp/mv的区别 +- 1、功能上的区别 +- mv:用户可以使用该命令为文件或目录重命名或将文件由一个目录移入另一个目录中。 +- cp: 该命令的功能是将给出的文件或目录拷贝到另一文件或目录中。 + +- 2、从inode角度来区分 +- mv:会将存储于indoe索引节点上的文件元信息也移动到新文件中。 +- cp : 只会复制文件数据,不会复制inode索引节点上的文件元信息。 + +- cp 的时候是真正意义上的内容copy,对于 inode 节点却是不会变化的。 +- mv 的时候是把源文件直接删除了(inode 删除了),新的文件其实已经不是以前的文件了,只是名字一样而已。 +# 网络 +# netstat(端口) +- 查看tcp连接数状态 +- netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' + +- 统计8080端口上有多少个TCP连接,命令: +- netstat –nat | grep 8080 | wc –l + +- TCP连接中有多少个连接状态是ESTABLISHED,命令: +- netstat –nat | grep 8080 | grep ESTABLISHED| wc -l + +- TCP连接中有多少个连接状态是CLOSE_WAIT +- netstat –nat | grep 8080 | grep CLOSE_WAIT| wc -l + +- TCP连接中有多少个连接状态是TIME_WAIT +- netstat –nat | grep 8080 | grep TIME_WAIT| wc -l +- 使用awk来完成统计信息,命令如下 +- netstat –nat | grep 8080 | awk ‘{++S[$NF]} END {for (a in S) print a, S[a]}’ +# sar +- sar –n FULL 1 2 执行后以1s为频率,总共输出两次网络IO的消耗情况。 +- 输出信息包含三部分:网卡上成功的接包和发包的信息 网卡上失败的接包和发包信息 sockets上的统计信息(tcpsck、udpsck) +- 如需详细跟踪tcp/ip通信过程的信息,则可通过tcpdump来进行。 +# tcpdump +- tcpdump [ -DenNqvX ] [ -c count ] [ -F file ] [ -i interface ] [ -r file ] +- [ -s snaplen ] [ -w file ] [ expression ] + +- 抓包选项: +- -c:指定要抓取的包数量。注意,是最终要获取这么多个包。例如,指定"-c 10"将获取10个包,但可能已经处理了100个包,只不过只有10个包是满足条件的包。 +- -i interface:指定tcpdump需要监听的端口。若未指定该选项,将从系统接口列表中搜寻编号最小的已配置好的接口(不包括loopback端口,要抓取loopback接口使用tcpdump -i lo), +- :一旦找到第一个符合条件的端口,搜寻马上结束。可以使用'any'关键字表示所有网络接口。 +- -n:对地址以数字方式显式,否则显式为主机名,也就是说-n选项不做主机名解析。 +- -nn:除了-n的作用外,还把端口显示为数值,否则显示端口服务名。 +- -N:不打印出host的域名部分。例如tcpdump将会打印'nic'而不是'nic.ddn.mil'。 +- -P:指定要抓取的包是流入还是流出的包。可以给定的值为"in"、"out"和"inout",默认为"inout"。 +- -s len:设置tcpdump的数据包抓取长度为len,如果不设置默认将会是65535字节。对于要抓取的数据包较大时,长度设置不够可能会产生包截断,若出现包截断, +- :输出行中会出现"[|proto]"的标志(proto实际会显示为协议名)。但是抓取len越长,包的处理时间越长,并且会减少tcpdump可缓存的数据包的数量, +- :从而会导致数据包的丢失,所以在能抓取我们想要的包的前提下,抓取长度越小越好。 + +- 输出选项: +- -e:输出的每行中都将包括数据链路层头部信息,例如源MAC和目标MAC。 +- -q:快速打印输出。即打印很少的协议相关信息,从而输出行都比较简短。 +- -X:输出包的头部数据,会以16进制和ASCII两种方式同时输出。 +- -XX:输出包的头部数据,会以16进制和ASCII两种方式同时输出,更详细。 +- -v:当分析和打印的时候,产生详细的输出。 +- -vv:产生比-v更详细的输出。 +- -vvv:产生比-vv更详细的输出。 + +- 其他功能性选项: +- -D:列出可用于抓包的接口。将会列出接口的数值编号和接口名,它们都可以用于"-i"后。 +- -F:从文件中读取抓包的表达式。若使用该选项,则命令行中给定的其他表达式都将失效。 +- -w:将抓包数据输出到文件中而不是标准输出。可以同时配合"-G time"选项使得输出文件每time秒就自动切换到另一个文件。可通过"-r"选项载入这些文件以进行分析和打印。 +- -r:从给定的数据包文件中读取数据。使用"-"表示从标准输入中读取。 + + - (1).默认启动 +- tcpdump +- 默认情况下,直接启动tcpdump将监视第一个网络接口(非lo口)上所有流通的数据包。这样抓取的结果会非常多,滚动非常快。 + + - (2).监视指定网络接口的数据包 +- tcpdump -i eth1 +- 如果不指定网卡,默认tcpdump只会监视第一个网络接口,如eth0。 + + - (3).监视指定主机的数据包,例如所有进入或离开longshuai的数据包 +- tcpdump host longshuai + + - (4).打印helios<-->hot或helios<-->ace之间通信的数据包 +- tcpdump host helios and \( hot or ace \) + + - (5).打印ace与任何其他主机之间通信的IP数据包,但不包括与helios之间的数据包 +- tcpdump ip host ace and not helios + + - (6).截获主机hostname发送的所有数据 +- tcpdump src host hostname + - (7).监视所有发送到主机hostname的数据包 +- tcpdump dst host hostname + + - (8).监视指定主机和端口的数据包 +- tcpdump tcp port 22 and host hostname + + - (9).对本机的udp 123端口进行监视(123为ntp的服务端口) +- tcpdump udp port 123 + +- (10).监视指定网络的数据包,如本机与192.168网段通信的数据包,"-c 10"表示只抓取10个包 +- tcpdump -c 10 net 192.168 + + - (11).打印所有通过网关snup的ftp数据包(注意,表达式被单引号括起来了,这可以防止shell对其中的括号进行错误解析) +- shell> tcpdump 'gateway snup and (port ftp or ftp-data)' + + - (12).抓取ping包 +- [root@server2 ~]# tcpdump -c 5 -nn -i eth0 icmp +# 内存 +# vmstat、sar、top、pidstat等可以查看swap和物理内存的消耗情况。 +- vmstat和sar共同的弱点是不能分析进程所占用的内存量。 +- top可以查看进程所消耗的内存量,在top中看Java进程的内存包括了JVM已分配的内存加上JVM外的物理内存。 +- pidstat也可以查看进程所消耗的内存量。 +- pidstat –r –p [pid] [interval] [times] +# 进程 +# ps –ef/-aux +- ps aux 和ps -ef +- 两者的输出结果差别不大,但展示风格不同。aux是BSD风格,-ef是System V风格。这是次要的区别,一个影响使用的区别是aux会截断command列,而-ef不会。当结合grep时这种区别会影响到结果。 + +- ps -ef 显示出的结果: +- 1.UID 用户ID +- 2.PID 进程ID +- 3.PPID 父进程ID +- 4.C CPU占用率 +- 5.STIME 开始时间 +- 6.TTY 开始此进程的TTY----终端设备 +- 7.TIME 此进程运行的总时间 +- 8.CMD 命令名 +# lsof 进程打开的文件、端口 +- lsof [options] filename +- -a:列出打开文件存在的进程; +- -c<进程名>:列出指定进程所打开的文件; +- -g:列出GID号进程详情; +- -d<文件号>:列出占用该文件号的进程; +- +d<目录>:列出目录下被打开的文件; +- +D<目录>:递归列出目录下被打开的文件; +- -n<目录>:列出使用NFS的文件; +- -i<条件>:列出符合条件的进程。(4、6、协议、:端口、 @ip ) +- -p<进程号>:列出指定进程号所打开的文件; +- -u:列出UID号进程详情; +- -h:显示帮助信息; +- -v:显示版本信息。 + +- lsof输出各列信息的意义如下: +- COMMAND:进程的名称 +- PID:进程标识符 +- USER:进程所有者 +- FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等 +- TYPE:文件类型,如DIR、REG等 +- DEVICE:指定磁盘的名称 +- SIZE:文件的大小 +- NODE:索引节点(文件在磁盘上的标识) +- NAME:打开文件的确切名称 + +- cwd:表示current work dirctory,即:应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改 +- txt:该类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的 /sbin/init 程序 +- lnn:library references (AIX); +- er:FD information error (see NAME column); +- jld:jail directory (FreeBSD); +- ltx:shared library text (code and data); +- mxx :hex memory-mapped type number xx. +- m86:DOS Merge mapped file; +- mem:memory-mapped file; +- mmap:memory-mapped device; +- pd:parent directory; +- rtd:root directory; +- tr:kernel trace file (OpenBSD); +- v86 VP/ix mapped file; +- 0:表示标准输出 +- 1:表示标准输入 +- 2:表示标准错误 + +# kill +- 杀死进程 + +# 负载 Load Average +- 系统的load被定义为特定时间间隔内运行队列的平均线程数,如果一个线程满足以下条件,该线程就会处于运行队列中:  +1.- 没有处于I/O等待状态。 +2.- 没有主动进入等待状态,也就是没有调用wait操作; +3.- 没有被终止。 + +- 每个CPU的核都维护了一个运行队列,系统的load主要由运行队列来决定。load的值越大,也就意味着系统的CPU越繁忙,这样线程运行完以后等待操作系统分配下一个时间片段的时间也就越长。一般来说,只要每个CPU当前的活动线程数不大于3,我们认为它的负载是正常的,如果每个CPU的线程数大于5,则表示当前系统的负载已经非常高了,需要采取措施来减低系统的负载,以提高响应速度。 + +- 使用top命令查看,该值是三个浮点数,表示最近1分钟、5分钟、15分钟的运行队列平均进程数。 + + +# AWK/SED +# awk +- awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。 +- awk '{pattern + action}' {filenames} + +- 统计ip +- cat test.txt | awk '{print $2}' | sort | uniq -c | sort -n -r | head -n 1 +# sed 编辑文本 +- sed -e 's/foo/bar/' myfile +- 将 myfile 文件中每行第一次出现的foo用字符串bar替换,然后将该文件内容输出到标准输出 + +- sed -e 's/foo/bar/g' myfile + +- g 使得 sed 对文件中所有符合的字符串都被替换 + +- sed -i 's/foo/bar/g' myfile +- 选项 i 使得 sed 修改文件 + +- sed -i 's/foo/bar/g' ./m* +- 批量操作当前目录下以 m 开头的文件 + +- sed -i 's/foo/bar/g' `grep foo -rl --include="m*" ./` + +- ``括起来的grep命令,表示将grep命令的的结果作为操作文件 +- grep 命令中,选项r表示查找所有子目录,l表示仅列出符合条件的文件名,用来传给sed命令做操作,--include="m*" 表示仅查找 m 开头的文件 +# 管道 +- 管道是linux提供的一种常见的进程通信工具 +- 管道中的数据只能读取一次 +- 管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。 +# IO模型 +# mmap +- 零拷贝技术:让数据传输不需要经过user space 使用mmap +- mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。 +# sendfile +- ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); + +- 参数特别注意的是:in_fd必须是一个支持mmap函数的文件描述符,也就是说必须指向真实文件,不能使用socket描述符和管道。 +- out_fd必须是一个socket描述符。 +- 由此可见sendfile几乎是专门为在网络上传输文件而设计的。 + +- Sendfile 函数在两个文件描述符之间直接传递数据(完全在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率很高,被称之为零拷贝。 + +# select poll(NIO) +- select poll epoll都是IO多路复用的实现方式! +- select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常(异常不包括网络断开)事件。poll和select应该被归类为这样的系统调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。 +- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); + +- fd_set结构体是文件描述符集,该结构体实际上是一个整型数组,数组中的每个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,一般情况下,FD_SETSIZE等于1024,这就限制了select能同时处理的文件描述符的总量。 + + - 1)nfds参数指定被监听的文件描述符的总数。通常被设置为select监听的所有文件描述符中最大值加1; + - 2)readfds、writefds、exceptfds分别指向可读、可写和异常等事件对应的文件描述符集合。这三个参数都是传入传出型参数,指的是在调用select之前,用户把关心的可读、可写、或异常的文件描述符通过FD_SET函数分别添加进readfds、writefds、exceptfds文件描述符集,select将对这些文件描述符集中的文件描述符进行监听,如果有就绪文件描述符,select会重置readfds、writefds、exceptfds文件描述符集来通知应用程序哪些文件描述符就绪。这个特性将导致select函数返回后,再次调用select之前,必须重置我们关心的文件描述符,也就是三个文件描述符集已经不是我们之前传入 的了。 + - 3)timeout参数用来指定select函数的超时时间。 + + - 1)如果指定timeout为NULL,select会永远等待下去,直到有一个文件描述符就绪,select返回; + - 2)如果timeout的指定时间为0,select根本不等待,立即返回; + - 3)如果指定一段固定时间,则在这一段时间内,如果有指定的文件描述符就绪,select函数返回,如果超过指定时间,select同样返回。 + +- select的几大缺点: + - 1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 + - 2)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(不适合服务器,但客户端也可以使用) + - 3)select支持的文件描述符数量太小了,默认是1024 + + +- int poll(struct pollfd *fds, nfds_t nfds, int timeout); +- 与select非常类似,poll比select的好处就是没有描述符数量限制,select 有1024 的限制,描述符不能超过此值,poll不受限制。 +- 此函数在系统调用select内部被使用,作用是把当前的文件指针挂到设备内部定义的等待 +- 队列中。 + +- send不是立即发送数据,而是将数据放在本地网卡缓冲区中。 +# epoll(NIO) +- select,poll还是在应用中轮询Socket,容易浪费;OS会自动轮询,通过epoll_wait返回。 +- epoll 与select和poll在使用和实现上有很大区别。首先,epoll使用一组函数来完成,而不是单独的一个函数;其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,无须向select和poll那样每次调用都要重复传入文件描述符集合事件集。 + +- 系统调用: +- epoll_create 创建一个epoll对象,一般epollfd = epoll_create() + +- epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件 +- 比如 +- epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入 +- epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入 +- epoll_wait(epollfd,...)等待直到注册的事件发生 +- (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。 + +- 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 + +- 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。 + +- 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 + +- 总结: + - 1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。 + - 2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。 + +## epoll原理 +1.- epoll初始化时,会向内核注册一个文件系统,用于存储被监控的句柄文件,调用epoll_create时,会在这个文件系统中创建一个file节点。同时epoll会开辟自己的内核高速缓存区,以红黑树的结构保存句柄,以支持快速的查找、插入、删除。还会再建立一个list链表,用于存储准备就绪的事件。 +2.- 当执行epoll_ctl时,除了把socket句柄放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后,就把socket插入到就绪链表里。 +3.- 当epoll_wait调用时,仅仅观察就绪链表里有没有数据,如果有数据就返回,否则就sleep,超时时立刻返回。 +## 边缘触发模式ET(Edge_triggered)和水平触发模式LT(Level_triggered) +- Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!! + +- Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!! + +# IOCP(AIO) +- 微软在 Winsocket2 中引入了 IOCP(Input/Output Completion Port)模型。IOCP 是 Input/Output Completion Port(I/O 完成端口)的简称。简单的说,IOCP 是一种高性能的 I/O 模型,是一种应用程序使用线程池处理异步 I/O 请求的机制。Java7 中对 IOCP 有了很好的封装,程序员可以非常方便的时候经过封装的 channel 类来读写和传输数据。 +- 不仅和epoll一样接收到Socket的事件,并且接收时OS已经完成了IO,不需要在应用层进行IO。 + +- 首先我们创建一个完成端口 CreateIOCompletionPort,然后再创建一个或多个工作线程,并指定它们到这个完成端口上去读取数据。再将远程连接的套接字句柄关联到这个完成端口。工作线程调用 getQueuedCompletionStatus 方法在关联到这个完成端口上的所有套接字上等待 I/O 的完成,再判断完成了什么类型的 I/O,然后接着发出 WSASend 和 WSARecv,并继续下一次循环阻塞在 getQueuedCompletionStatus。 + +- 具体的说,一个完成端口大概的处理流程包括: + +- 创建一个完成端口; +- Port port = createIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, fixedThreadCount()); +- 创建一个线程 ThreadA; +- ThreadA 线程循环调用 GetQueuedCompletionStatus 方法来得到 I/O 操作结果,这个方法是一个阻塞方法; +- While(true){ +- getQueuedCompletionStatus(port, ioResult); +- } +- 主线程循环调用 accept 等待客户端连接上来; +- 主线程 accept 返回新连接建立以后,把这个新的套接字句柄用 CreateIoCompletionPort 关联到完成端口,然后发出一个异步的 Read 或者 Write 调用,因为是异步函数,Read/Write 会马上返回,实际的发送或者接收数据的操作由操作系统去做。 +- if (handle != 0L) { +- createIoCompletionPort(handle, port, key, 0); +- } +- 主线程继续下一次循环,阻塞在 accept 这里等待客户端连接。 +- 操作系统完成 Read 或者 Write 的操作,把结果发到完成端口。 +- ThreadA 线程里的 GetQueuedCompletionStatus() 马上返回,并从完成端口取得刚完成的 Read/Write 的结果。 +- 在 ThreadA 线程里对这些数据进行处理 ( 如果处理过程很耗时,需要新开线程处理 ),然后接着发出 Read/Write,并继续下一次循环阻塞在 GetQueuedCompletionStatus() 这里。 +# 分区 +# 磁盘的文件名 +- Linux 中每个硬件都被当做一个文件。 + +- 常见磁盘的文件名: + +- SCSI/SATA/USB 磁盘:/dev/sd[a-p] +- IDE 磁盘:/dev/hd[a-d] +- 其中文件名后面的序号的确定与磁盘插入的顺序有关,而与磁盘所插入的插槽位置无关。 + +- Linux系统使用字母和数字的组合来指代硬盘分区,Linux系统使用一种更加灵活的命名方案,该命名方案是基于文件的,文件名的格式为/dev/xxyN, +- /dev/:这是Linux系统下所有设备文件所在的目录名。 +- xx:分区名的前两个字母表示分区所在设备的类型,通常是hd(IDE硬盘)或sd(SCSI硬盘)。 +- y:这个字母表示分区所在的设备。 +- N:最后的数字N代表分区。 +- 挂载目录: +- Linux系统处理分区及磁盘存储的方法与Windows截然不同,Linux系统中的每一个分区都是构成支持一组文件和目录所必需的存储区的一部分。它是通过挂载来实现的,挂载是将分区关联到某一目录的过程,挂载分区使起始于这个指定目录(通称为挂载目录)的存储区能够被使用。 +# 分区表 +- 磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 + +- 1. MBR +- MBR 中,第一个扇区最重要,里面有:主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,partition table 占 64 bytes。 + +- 分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它空间用来记录分区表,可以记录更多的分区,因此通过扩展分区可以分出更多区分,这些分区称为逻辑分区。 + +- Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名+编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 + +- 2. GPT +- 不同的磁盘有不同的扇区大小,例如 512bytes 和最新磁盘的 4k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA)。 + +- GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。 + +- GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。 + +# 压缩 +- tar [主选项+辅选项][文件或者目录] + +- 备份/root/abc目录及其子目录下的全部文件,备份文件名为abc.tar。 +- [root@PC-LINUX ~]# touch /root/abc/a /root/abc/b /root/abc/c +- //在/root/abc目录中创建/root/abc/a、/root/abc/b和/root/abc/c文件 +- [root@PC-LINUX ~]# tar cvf abc.tar /root/abc + +- 查看abc.tar备份文件的内容,并显示在显示器上。 +- [root@PC-LINUX ~]# tar tvf abc.tar + +- 将文件/root/abc/d添加到abc.tar包里面去。 +- [root@PC-LINUX ~]# touch /root/abc/d +- [root@PC-LINUX ~]# tar rvf abc.tar /root/abc/d + +- 更新原来tar包abc.tar中的文件/root/abc/d。 +- [root@PC-LINUX ~]# tar uvf abc.tar /root/abc/d + +- tar调用gzip +- 把/root/abc目录包括其子目录全部做备份文件,并进行压缩,文件名abc.tar.gz。 +- [root@PC-LINUX ~]# tar zcvf abc.tar.gz /root/abc + +- 查看压缩文件abc.tar.gz的内容,并显示在显示器上。 +- [root@PC-LINUX ~]# tar ztvf abc.tar.gz + +- 将压缩文件abc.tar.gz解压缩出来。 +- [root@PC-LINUX ~]# tar zxvf abc.tar.gz + +- tar调用bzip2 +- 将目录/root/abc及该目录所有文件压缩成abc.tar.bz2文件。 +- [root@PC-LINUX ~]# tar cjf abc.tar.bz2 /root/abc + +- 查看压缩文件abc.tar.bz2的内容,并显示在显示器上。 +- [root@PC-LINUX ~]# tar tjf abc.tar.bz2 + +- 将abc.tar.bz2文件解压缩。 +- [root@PC-LINUX ~]# tar xjf abc.tar.bz2 + +# 其他 +# 关机 +- 1. 数据同步写入磁盘 sync + +- 为了加快对磁盘上文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 + +- 2. shutdown + +- # /sbin/shutdown [-krhc] [时间] [警告讯息] +- -k : 不会关机,只是发送警告讯息,通知所有在线的用户 +- -r : 将系统的服务停掉后就重新启动 +- -h : 将系统的服务停掉后就立即关机 +- -c : 取消已经在进行的 shutdown 指令内容 +- 3. 其它关机指令 +- reboot、halt、poweroff。 +# 运行等级 +- 0:关机模式 1:单用户模式(可用于破解root密码) 2:无网络支持的多用户模式 3:有网络支持的多用户模式(文本模式,工作中最常用的模式) 4:保留,未使用 5:有网络支持的 X-windows 支持多用户模式(桌面) 6:重新引导系统,即重启 +# BIOS +- BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序的会加载操作系统的核心文件。 + +- MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 + +# 指令搜索顺序 +- 1. 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ; +- 2. 由别名找到该指令来执行; +- 3. 由 Bash 内建的指令来执行; +- 4. 按 $PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 +# 数据流重定向 +- 重定向有5种方式,分别是: 输出重定向、输入重定向、错误重定向、追加重定向以及同时实现输出和错误的重定向。 + +- 重定向就是使用文件代替标准输入、标准输出和标准错误输出。 + +- 标准输入 (stdin) :代码为 0 ,使用 < 或 << ; +- 标准输出 (stdout) :代码为 1 ,使用 > 或 >> ; +- 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>> ; +- 其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。 + +- 可以将不需要的标准输出以及标准错误输出重定向到 /dev/null ,相当于扔进垃圾箱。 + +- 如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。 + +- $ find /home -name .bashrc > list 2>&1 +# 静态链接库和动态链接库的区别 +- 静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。 + +- 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但是如果有多处对lib文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。 + +# 孤儿进程和僵死进程 +## 孤儿进程 + - 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害 + +## 僵尸进程 +- 一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 + +- 僵死进程通过 ps 命令显示出来的状态为 Z。 + +- 系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。 + +- 要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。 + + + + + + + + + + + + + + + + + + + +# JDBC +- JDBC的桥接设计模式 jdbc的详细链接过程;底层源码;在Java中调用存储过程的方法 + +- 为什么要使用JDBC + +- JDBC可以跨数据库平台 +- 系统可以 小规模时使用MySQL 规模变大时使用Oracle +- 如果不需要改动API函数,就需要分层。 + + +- ODBC统一与数据库的接口,ADO是.NET统一与数据库的接口 +- JDBC是java与数据库统一的接口 +- 但是注意JDBC还是要求将SQL语句插入到代码中,而SQL语句各数据库有所不同,所以不能完全实现移植。如果要实现完全的移植,需要使用Hibernate技术 +- Hibernate将不同数据库微小的区别也屏蔽掉了 +- EJB也实现了屏蔽数据库间微小的区别 + +- +- JDBC对于java的接口是一致,但对于不同数据库JDBC所用的类库是不同的,对于程序猿而言是透明的。 + +- JDBC编程步骤 + +- 先需要找JDBC的类库 java.sql +- Driver 驱动,用来提供给JDBC连接到数据库 +- java并不知道使用的是哪种数据库,而java有一个管理数据库的管家DriverManager,它来管理使用哪种数据库的连接。 +- 连接到某个数据库,需要先向DriverManager注册。然后通过DriverManager连接到数据库。 + +- 代码示例: +- import java.sql.*; + +``` +public class TestJDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection conn = null; +- Statement stmt = null; +- ResultSet rs = null; +- try { +- Class.forName("com.mysql.jdbc.Driver"); +- conn = DriverManager.getConnection("jdbc:mysql://localhost/mydata?user=root&password=130119"); +- stmt = conn.createStatement(); +- rs = stmt.executeQuery("select * from emp"); +- while(rs.next()){ +- System.out.println(rs.getString("deptno")); +- } +- } catch (SQLException | InstantiationException | IllegalAccessException | ClassNotFoundException e) { +- e.printStackTrace(); +- }finally{ +- try{ +- if(rs != null){ +- rs.close(); +- rs = null; +- } +- if(stmt != null ){ +- stmt.close(); +- stmt= null; +- } +- if(conn != null){ +- conn.close(); +- conn = null; +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- } +- } +- +- } +- } +- + +- 总结:JDBC连接流程 +1.- 注册:驱动实例化 Class forName() +2.- 连接:获取连接 Connection DriverManager getConnection() +3.- 执行:通过连接执行SQL语句 Statement createStatement() executeQuery() +4.- 接收并输出:循环遍历取出数据 ResultSet next() getString() +5.- 关闭:close 先打开的后关闭 close() + +- 除此之外,还需要抓Exception,并将关闭的代码放到finally里面 +- 关闭前先判断是否为null,如果为null那么关闭时会出错 +- 在close中也会抛Exception,也要写try catch + +- + +# JDBCUtils +- import java.io.IOException; +- import java.util.Properties; +- import java.sql.*; + + +``` +public final class JDBCUtils { +``` + + +``` + private static String driver; +``` + + +``` + private static String url; +``` + + +``` + private static String username; +``` + + +``` + private static String password; +``` + + + +``` + private JDBCUtils() { +``` + +- } + +- static { + +- Properties props = new Properties(); +- try { +- props.load(JDBCUtils.class.getClassLoader().getResourceAsStream( +- "dbinfo.properties")); +- driver = props.getProperty("driver"); +- url = props.getProperty("url"); +- username = props.getProperty("username"); +- password = props.getProperty("password"); +- } catch (IOException e) { +- throw new ExceptionInInitializerError(e); +- } + +- try { +- Class.forName(driver); +- } catch (ClassNotFoundException e) { +- throw new ExceptionInInitializerError(e); +- } +- } + + +``` + public static Connection getConn() { +``` + +- Connection conn = null; +- try { +- conn = DriverManager.getConnection(url, username, password); +- } catch (SQLException e) { +- e.printStackTrace(); +- } +- return conn; +- } + + +``` + public static void free(ResultSet rs, Statement stmt, Connection conn) { +``` + +- try { +- if (rs != null) { +- rs.close(); +- rs = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally { +- try { +- if (stmt != null) { +- stmt.close(); +- stmt = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally { +- try { +- if (conn != null) { +- conn.close(); +- conn = null; +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } +- } +- } +- } +- } + +- + +- 实际调用的代码: +- import java.sql.*; + + +``` +public class TestTemplate { +``` + + +``` + public static void main(String[] args) { +``` + +- read(); +- } + + +``` + public static void read(){ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- rs = stmt.executeQuery("select * from dept"); +- while(rs.next()){ +- System.out.println(rs.getString("deptno")); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, stmt, c); +- } +- } +- } + +- 以下可以忽略 +- 工具类可以采用单例模式 + +``` +类中有一个静态私有的实例,有一个公开的静态的获取该实例的方法,其他方法是public但不是静态的 +``` + +- 当其他的类要调用该工具类的方法时,先获取该类的实例,再调用其方法 + +- 还可以采用延迟加载,当第一次使用该类时new出实例;一开始是没有实例的成员变量的 +- 以后使用该类就不会再new第二个实例了。 +- 构造实例的成本很高时多采用该方法 + + + +- 还可以再完善,并发(多线程)控制时将new实例的部分加锁,避免出现两个实例 +- 永远保持单个实例 +- 这是双重检查 + +- + +- DML增删改 +- 注意sql语句不建议用*,并且get获取值时不应该写1,2 ;写为字段名可读性更高 +- 最好将sql语句中列出所需要的字段,全部取出效率较低 + +- 这是为了降低维护成本 +- executeUpdate 返回值是int类型,是执行成功的影响行数 + +- 注意关闭执行DML语句的资源也可以使用封装的free方法,因为会判断是否为空,空即不关闭 + +- 注意与之前步骤不同,不需要ResultSet这个类,因为不需要接收结果集 +- 示例模板(使用了JDBCUtils工具类): + +- import java.sql.*; + + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + +- add(); +- } + +``` + private static void add() { +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- String sql = "insert into dept2 values(14,'lala','china')"; +- int result = stmt.executeUpdate(sql); +- System.out.println(result); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, stmt, c); +- } +- } +- } +# JDBC进阶 + + + +- SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。 +- select * from emp where ename = '' or 1 = 1; +- PreparedStatement类 + +- 是Statement接口的子接口 +- 表示预编译的 SQL 语句的对象。 +- SQL 语句(增改删DML)被预编译并存储在 PreparedStatement 对象中。然后可以使用此对象多次高效地执行该SQL语句。 +- 与Statement不同的是通过preparedStatement方法来获得PreparedStatement对象。 + +- 参数: +- sql - 可能包含一个或多个 '?' IN 参数占位符的 SQL 语句 +- 返回: +- 包含预编译 SQL 语句的新的默认 PreparedStatement 对象 + +- 不同于createStatement不需要参数,这个方法需要一个参数,即预编译的SQL语句,其插入的数据用?占位符表示 +- PreparedStatement pstmt = conn.prepareStatement(“insert into dept values(?,?,?)”); +- 之后再使用pstmt来为这三个?赋值 +- 使用的方法有: + +- 这些方法的第一个参数都是表示第几个占位符 +- 1表示第一个? +- 2表示第二个? + +- pstmt.setInt(1,deptno); +- pstmt.setString(2,dname); +- pstmt.setString(3,loc); +- 另一个区别是executeUpdate方法不再需要参数传入。 +- PreparedStatement类的好处是不必去考虑怎么才能拼凑出格式正确的SQL语句,而是调用方法就可以设置相应的值,十分方便;也易于修改占位符值。尽量使用;可以解决SQL注入问题(过滤掉特殊字符)(最大的优点);同时效率同Statement相比较高(避免频繁SQL语句,是预编译) +- 可以执行查询和DML操作。 +- 示例: +- import java.sql.*; + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + + - if(args.length != 3){ +- System.out.println("Input Error!"); + - System.exit(-1); +- } +- int deptno =0 ; +- try{ +- deptno = Integer.parseInt(args[0]); +- }catch(NumberFormatException e){ +- e.printStackTrace(); +- } +- String dname = args[1]; +- String loc = args[2]; +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement("insert into dept2 values(?,?,?)"); +- pstmt.setInt(1, deptno); +- pstmt.setString(2,dname); +- pstmt.setString(3,loc); +- pstmt.executeUpdate(); +- +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, pstmt, c);//这里仍可以执行,因为PreparedStatement是Statement的子类,而非超类 +- } +- } +- } +- 在java中SQL语句测定时间的一种方法,在执行之前和执行之后的时间打印出来 + + +- JDBC中最耗时间的是建立连接;使用的是Socket连接,三次握手机制 +- 发送用户名密码 +- 比发送执行SQL语句的时间要长得多 + +- 注意虽然PreparedStatement是Statement是子类,但是不能调用父类的executeQuery(sql); +- 会出错 +- 因为该方法会直接将参数原始的sql语句传到数据库,而不管之前的填充sql语句的步骤 + +- 示例: +- import java.sql.*; + +``` +public class TestTemplate { +``` + + +``` + public static void main(String []args){ +``` + +- read("SMITH"); +- } + + +``` + private static void read(String name) { +``` + +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- String sql = "select * from emp where ename = ?"; +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement(sql); +- pstmt.setString(1,name); +- rs = pstmt.executeQuery();//如果这里加上sql,那么会报错 +- +- while(rs.next()){ + - System.out.println(rs.getString(1)); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs, pstmt, c); +- } +- } +- } +- 如何选择? 一般带有参数的sql都使用PreparedStatement +- 如果没有条件或条件固定的可以使用Statement。 建议全部使用PreparedStatement +- CallableStatement(存储过程) + +- 是PreparedStatement的子接口,是Statement的孙子接口。 +- 用于执行 SQL 存储过程的接口。JDBC API 提供了一个存储过程 SQL 转义语法,该语法允许对所有 RDBMS 使用标准方式调用存储过程。此转义语法有一个包含结果参数的形式和一个不包含结果参数的形式。如果使用结果参数,则必须将其注册为 OUT 参数。其他参数可用于输入、输出或同时用于二者。参数是根据编号按顺序引用的,第一个参数的编号是 1。 + +- 继承自PreparedStatement + +- Connection + +- 可以创建一个CallableStatement对象 +- 参数是sql语句 +- sql语句是这样写的 +- CollableStatement cs = null; +- cs = conn.prepareCall(“{call pro1(?,?)}”); //参数以?表示,之后赋值 +- cs.setString(1,”SMITH”); +- cs.setIntFloat(2,456.7f);//f是表示float类型 + +- 第一个参数是1,2,3等 表示第一个、第二个、第三个? +- 第二个参数是所要设为的值 + +- 设置完后调用execute方法 + + +- 注意如果在sql 中没有commit提交事务,那么java中无法访问数据库,处于阻塞状态 + +- 代码: + +``` +public static void main(String []args){ +``` + +- Connection c = null; +- CallableStatement cs = null; +- try{ +- +- c =JDBCUtils.getConn(); +- cs = c.prepareCall("{call pro3(?,?)}"); +- cs.setString(1, "SMITH"); +- cs.setFloat(2, 7800f); //这个也可以是setString +- //注意oracle提供一种自动转换机制,如果该字段是数字类型,那么会自动将字符串转为数字 +- cs.execute(); + +- 可以在SQL工具类中封装一个可以调用存储过程的方法 +- 工具类中的成员变量均为静态变量,方法都是静态方法 +- SQLHelper: + + +``` +public static void callProcedure(String sql,String []parameters){ +``` + +- Connection c= null; +- CallableStatement cs= null; +- ResultSet rs = null; +- //所有含有参数的sql语句都使用PreparedStatement,传入时还包括一个字符串数据 +- 然后通过一个循环将参数使用setString将sql语句补充完毕 +- 然后调用 +- try{ +- c =JDBCUtils.getConn(); +- cs = c.prepareCall(sql); +- if(parameters != null && "".equals(parameters)){ +- for(int i = 0; i< parameters.length ;i++){ +- cs.setString(i+1,parameters[i]); +- } +- } +- cs.execute(); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,cs,c); +- } +- } + +- 调用此方法的代码: + + +- ------------------------------------------------------------------------------------------------------------------------------- +- 带返回值参数的存储过程 +- 分页查询 +- 存储过程代码: +- create or replace procedure paged_query +- (v_in_table in varchar2,v_in_record_per_page in number, v_in_now_page in number,v_in_col varchar2, +- v_out_records out number,v_out_pages out number, v_out_result out p1.my_cursor) is +- v_start number; +- v_end number; +- v_sql varchar2(2000); +- v_sql_get_record varchar2(300); + +- begin + - v_start := 1+v_in_record_per_page*( v_in_now_page-1) ; +- v_end := 1+ v_in_record_per_page * v_in_now_page; +- v_sql_get_record := 'select count(*) from '||v_in_table; +- execute immediate v_sql_get_record into v_out_records; +- if mod(v_out_records,v_in_record_per_page) = 0 then +- v_out_pages := v_out_records / v_in_record_per_page; +- else +- v_out_pages := v_out_records / v_in_record_per_page + 1; +- end if; +- v_sql := 'select t2.* from (select t1.* ,rownum rn from +- (select * from '||v_in_table||' order by '||v_in_col||') t1 +- where rownum <= '||v_end||' +- ) t2 +- where rn >= '||v_start; +- open v_out_result for v_sql; +- end; +- / + +- + +- 使用CallableStatement来接收Connection的prepareCall方法,得到statement +- 传入的是含有等同于参数个数的sql语句 +- String sql = “{call pro1(?,?)}”; +- 然后set方法设置每个?所对应的值;注意数值类型也可以用setString +- oracle会自动转为相应的数值类型 + +- 与不含输出参数的过程的第一个不同是还需要调用这个方法registerOutParameter + +- 注册输出参数的类型 +- 注意不同数据库,参数类型也是不同的 +- 第一个参数是给第n个?赋值;第二个参数是对应的返回值的类型 +- 对oracle而言是oracle.jdbc.OracleTypes.某个类型 +- 最后execute +- --------------------------------------------------------------------------------------------------------------------------------- +- 第二个不同是还需要再取出返回值 +- get…数据类型(n) +- 比如getString(第几个?的返回值); + +- 如果返回的是结果集(游标),那么需要使用结果集来接收游标变量 +- oracle.jdbc.OracleTypes.CURSOR +- 取出返回值的集合 +- 方法是getObject 将游标视为一个对象 +- 接收的是ResltSet结果集(需要将Object类型强制转为ResultSet) +- ResultSet实际上就是游标 +- 之后就可以使用next和get…来获取值了 + +- + +- java调用代码: +- try { +- c = JDBCUtils.getConn(); +- cs = c.prepareCall("{call paged_query(?,?,?,?,?,?,?)}"); +- int page = 2; +- cs.setString(1, "emp"); +- cs.setString(2,"5"); +- cs.setString(3,""+page); +- cs.setString(4, "sal"); +- cs.registerOutParameter(5, oracle.jdbc.OracleTypes.NUMBER); +- //每个返回的变量都需要注册 +- cs.registerOutParameter(6, oracle.jdbc.OracleTypes.NUMBER); +- cs.registerOutParameter(7, oracle.jdbc.OracleTypes.CURSOR); +- cs.execute(); + - String recordCount = cs.getString(5); + - String pageCount = cs.getString(6); +- System.out.println("共有"+recordCount+"条记录"); +- //执行完后需要逐个取出返回值 +- System.out.println("共有"+pageCount+"页"); +- System.out.println("第"+page+"页:"); + - rs = (ResultSet)cs.getObject(7); +- while(rs.next()){ + - System.out.println(rs.getString(1)+","+rs.getString(2)); +- }catch ……略 + + +- getGeneratedKeys产生主键 +- 用于拿出插入的记录的主键 + + +- + +- getGeneratedKeys(获取主键) +- API介绍: +- Connection: + +- prepareStatement +- PreparedStatement prepareStatement(String sql, +- int autoGeneratedKeys) +- throws SQLException +- 创建一个默认 PreparedStatement 对象,该对象能获取自动生成的键。给定常量告知驱动程序是否可以获取自动生成的键。如果 SQL 语句不是一条 INSERT 语句,或者 SQL 语句能够返回自动生成的键(这类语句的列表是特定于供应商的),则忽略此参数。 +- 该SQL语句是插入语句,可以立刻获得插入记录的主键 +- 第二个参数是Statement中的常量 + + +- PreparedStatement: +- ResultSet getGeneratedKeys() throws SQLException +- 获取由于执行此 Statement 对象而创建的所有自动生成的键。如果此 Statement 对象没有生成任何键,则返回空的 ResultSet 对象。 +- 注:如果未指定表示自动生成键的列,则 JDBC 驱动程序实现将确定最能表示自动生成键的列。 +- 返回:包含通过执行此 Statement 对象自动生成的键的 ResultSet 对象 +- 执行的是insert语句,返回的是ResultSet +- ResultSet 可能是多个主键(组合主键),所以需要ResultSet + +- 代码: + +``` +public static void add() { +``` + +- Connection c = null; +- PreparedStatement ps = null; +- ResultSet rs = null; +- String sql = "insert into UserTable(user_name,user_password) values(?,?)"; +- try { +- c = JDBCUtils.getConn(); +- ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); +- ps.setString(1, "wulitaotao"); +- ps.setString(2, "666"); +- ps.executeUpdate(); +- rs = ps.getGeneratedKeys(); +- if(rs.next()){ + - System.out.println(“刚插入记录的id为”+rs.getInt(1)); +- } +- } catch (SQLException e) { +- e.printStackTrace(); +- } finally{ +- JDBCUtils.free(rs,ps,c); +- } +- } + +- 实际作用是如果数据库的表是自动主键,那么插入之后是不知道id的。只能从数据库中根据其他的唯一键来找到这条记录再取出主键 +- 而这种方式可以在插入之后立刻得到主键,然后取出赋给对象,ok + +- 优化UserDAOJDBCImpl中的addUser方法 + +- try { +- c = JDBCUtils.getConn(); +- String sql = "insert into UserTable(user_name,user_password,user_birthday) +- values(?,?,?)"; +- pstmt = c.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS); +- pstmt.setString(1, user.getUsername()); +- pstmt.setString(2, user.getPassword()); +- pstmt.setDate(3, new java.sql.Date(user.getBirthday().getTime())); +- pstmt.executeUpdate(); +- rs = pstmt.getGeneratedKeys(); +- if(rs.next()){ + - user.setId(rs.getInt(1)); +- } +- } catch (SQLException e) { +- throw new DAOException(e.getMessage(),e); +- } finally { +- JDBCUtils.free(rs, pstmt, c); +- } + +- + +- 批处理Batch +- 可以一次执行多条SQL语句,调用Statement接口的addBatch方法 +- 不必建立多次的连接(建立连接成本很高),只建立一次连接就可以执行多条语句 + +- 将给定的 SQL 命令添加到此 Statement 对象的当前命令列表中。通过调用方法 executeBatch 可以批量执行此列表中的命令。 +- 参数: +- sql - 通常此参数为 SQL INSERT 或 UPDATE 语句 + +- 执行语句时调用 + +- 将一批命令提交给数据库来执行 +- 如果全部命令执行成功,则返回更新计数组成的数组。返回数组的 int 元素的排序对应于批中的命令,批中的命令根据被添加到批中的顺序排序。 +- 返回: +- 包含批中每个命令的一个元素的更新计数所组成的数组。数组的元素根据将命令添加到批中的顺序排序。 + +- 批处理适用于Statement,当然适用于其子类PreparedStatement + +- 示例1 Statement: +- import java.sql.*; + +``` +public class JDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- stmt = c.createStatement(); +- stmt.addBatch("insert into dept2 values(88,'aaa','aaa')"); +- stmt.addBatch("insert into dept2 values(89,'aaa','aaa')"); +- stmt.addBatch("insert into dept2 values(90,'aaa','aaa')"); +- stmt.executeBatch(); +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } +- } +- ------------------------------------------------------------------------------------------------------ +- 示例2 PreparedStatement: +- import java.sql.*; + +``` +public class JDBC { +``` + + +``` + public static void main(String []args){ +``` + +- Connection c = null; +- PreparedStatement pstmt = null; +- ResultSet rs = null; +- try{ +- c = JDBCUtils.getConn(); +- pstmt = c.prepareStatement("insert into +- UserTable(user_name,user_password,user_birthday) values(?,?,?)"); +- for(int i = 0 ; i<100;i++){//一次性插入100条记录 + - pstmt.setString(1, "user"+i+1); +- pstmt.setString(2, "111"); +- pstmt.setDate(3, new Date(System.currentTimeMillis())); +- pstmt.addBatch(); +- //设置完一条记录添加一次,然后重新设置下一条记录 字段的值 +- } +- int []id = pstmt.executeBatch();//返回值全部是1 +- for(int i:id){ +- System.out.println(i); +- } +- }catch(SQLException e){ +- e.printStackTrace(); +- }finally{ +- JDBCUtils.free(rs,pstmt,c); +- } +- } +- } +- addBatch(sql) 也是存在的,可以将sql语句传入 + +- 如果记录太多可能会内存溢出 + +- + +- 事务处理 +- 在java程序中将多个DML语句视为一个事务,统一提交和回滚 + +- 注意java中事务是自动提交的,也就是执行一条DML语句就commit一下 +- 需要我们去设置不自动提交事务,由自己来设定事务 +- Connection有一个方法setAutoCommit +- 参数为true or false +- 当想提交时,执行Connection的commit方法 +- 在执行commit方法之前的DML语句都还未提交,当执行commit方法时将之前的DML语句视为一个事务,整体地执行 +- 当事务执行过程中出现异常,可以在catch到exception后回滚 +- 可以Connection的rollback方法,有重载的方法,可以无参数,对应sql中的rollback; +- rollback方法可以有参数,是保存点,回滚到这个保存点 + + + +- + + +``` +public static void testSavepoint() throws Exception{ +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- String sql1 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql2 = "update emp2 set sal = sal - 800 where empno = 7369"; + +- try { +- c = JDBCUtils.getConn(); +- c.setAutoCommit(false); +- stmt = c.createStatement(); + - stmt.executeUpdate(sql1); +- int i = 8/0; //故意制造出一些错误 + - stmt.executeUpdate(sql2); +- c.commit(); +- c.setAutoCommit(true);//恢复现场 +- }catch (Exception e) { //捕捉到任何异常,立刻返回到初始状态 +- //不能只写SQLException,应该是所有异常,否则ArithmeticException就捕捉不到了,捕捉不到也就无法执行catch块中的代码了 +- if(c != null) //如果没有建立连接那么没有必要去rollback +- c.rollback(); +- c.setAutoCommit(true);//无论如何都应该恢复现场 +- throw e;//交给上一级去处理 +- } finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } +- } +- 设置保存点: +- 假如在执行了第一条sql之后设置保存点,如果在执行第二条sql时出错,回滚到第一条sql之后 +- Connection: + + +- + + +``` +public static void testSavepoint() throws SQLException { +``` + +- Connection c = null; +- Statement stmt = null; +- ResultSet rs = null; +- Savepoint sp = null; +- String sql1 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql2 = "update emp2 set sal = sal - 800 where empno = 7369"; +- String sql3 = "select sal from emp2 where empno = 7369"; +- try { +- c = JDBCUtils.getConn(); +- c.setAutoCommit(false); +- stmt = c.createStatement(); + - stmt.executeUpdate(sql1); +- sp = c.setSavepoint(); + - rs = stmt.executeQuery(sql3); +- int sal = 0; +- if(rs.next()) +- sal = rs.getInt("sal"); +- if(sal < 1000) +- throw new RuntimeException("工资过低!"); +- //自己new一个异常,以便于与SQLException区分开来 + - stmt.executeUpdate(sql2); +- c.commit(); +- c.setAutoCommit(true);//恢复现场 +- }catch (RuntimeException e) { +- if(c != null && sp != null){ +- c.rollback(sp); +- //如果工资减去800之后小于1000,那么不再减少,保留第一次的结果 +- } +- c.setAutoCommit(true); +- throw e; +- } catch (SQLException e) { +- if(c != null){ +- c.rollback(); +- c.setAutoCommit(true); +- } +- throw e; +- } finally{ +- JDBCUtils.free(rs,stmt,c); +- } +- } + +- JTA类似指挥官,第一阶段给所有数据库发送一个准备提交的请求,如果有数据库提出要回滚,那么JTA会通知其他数据库,一起回滚 +- 如果没有数据库没有提出要回滚,那么第二阶段JTA发送提交的命令 +- + +# JDBC桥接模式源码分析(以MySQL为例) +# Class.forName +- Class.forName("com.mysql.jdbc.Driver"); +- Driver#static{} +- 注册MySQL的Driver + +``` +public class Driver extends NonRegisteringDriver implements java.sql.Driver { + // ~ Static fields/initializers + // --------------------------------------------- + + // + // Register ourselves with the DriverManager + // + static { + try { + java.sql.DriverManager.registerDriver(new Driver()); + } catch (SQLException E) { + throw new RuntimeException("Can't register driver!"); + } + } +``` + +- } + +- DriverManager#registerDriver + +``` +public static synchronized void registerDriver(java.sql.Driver driver) + throws SQLException { + + registerDriver(driver, null); +} +``` + + + +``` +public static synchronized void registerDriver(java.sql.Driver driver, + DriverAction da) + throws SQLException { + + /* Register the driver if it has not already been added to our list */ + if(driver != null) { + registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); + } else { + // This is for compatibility with the original DriverManager + throw new NullPointerException(); + } + + println("registerDriver: " + driver); + +} +``` + + +- DriverManager#getConnection + +``` +public static Connection getConnection(String url) + throws SQLException { + + java.util.Properties info = new java.util.Properties(); + return (getConnection(url, info, Reflection.getCallerClass())); +} +``` + + + + +``` +private static Connection getConnection( + String url, java.util.Properties info, Class<?> caller) throws SQLException { + /* + * When callerCl is null, we should check the application's + * (which is invoking this class indirectly) + * classloader, so that the JDBC driver class outside rt.jar + * can be loaded from here. + */ + ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; + synchronized(DriverManager.class) { + // synchronize loading of the correct classloader. + if (callerCL == null) { + callerCL = Thread.currentThread().getContextClassLoader(); + } + } + + if(url == null) { + throw new SQLException("The url cannot be null", "08001"); + } + + println("DriverManager.getConnection(\"" + url + "\")"); + + // Walk through the loaded registeredDrivers attempting to make a connection. + // Remember the first exception that gets raised so we can reraise it. + SQLException reason = null; + + for(DriverInfo aDriver : registeredDrivers) { + // If the caller does not have permission to load the driver then + // skip it. + if(isDriverAllowed(aDriver.driver, callerCL)) { + try { + println(" trying " + aDriver.driver.getClass().getName()); + Connection con = aDriver.driver.connect(url, info); + if (con != null) { + // Success! + println("getConnection returning " + aDriver.driver.getClass().getName()); + return (con); + } + } catch (SQLException ex) { + if (reason == null) { + reason = ex; + } + } + + } else { + println(" skipping: " + aDriver.getClass().getName()); + } + + } + + // if we got here nobody could connect. + if (reason != null) { + println("getConnection failed: " + reason); + throw reason; + } + + println("getConnection: no suitable driver found for "+ url); + throw new SQLException("No suitable driver found for "+ url, "08001"); +} +``` + + +- Driver#connect + +``` +public java.sql.Connection connect(String url, Properties info) + throws SQLException { + if (url != null) { + if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { + return connectLoadBalanced(url, info); + } else if (StringUtils.startsWithIgnoreCase(url, + REPLICATION_URL_PREFIX)) { + return connectReplicationConnection(url, info); + } + } + + Properties props = null; + + if ((props = parseURL(url, info)) == null) { + return null; + } + + if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) { + return connectFailover(url, info); + } + + try { + Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance( + host(props), port(props), props, database(props), url); + + return newConn; + } catch (SQLException sqlEx) { + // Don't wrap SQLExceptions, throw + // them un-changed. + throw sqlEx; + } catch (Exception ex) { + SQLException sqlEx = SQLError.createSQLException(Messages + .getString("NonRegisteringDriver.17") //$NON-NLS-1$ + + ex.toString() + + Messages.getString("NonRegisteringDriver.18"), //$NON-NLS-1$ + SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null); + + sqlEx.initCause(ex); + + throw sqlEx; + } +} +``` + +# 总结 +- 有了抽象部分——JDBC的API,有了具体实现部分——驱动程序,那么它们如何连接起来呢?就是如何桥接呢? +- 就是前面提到的DriverManager来把它们桥接起来,从某个侧面来看,DriverManager在这里起到了类似于简单工厂的功能,基于JDBC的应用程序需要使用JDBC的API,如何得到呢?就通过DriverManager来获取相应的对象。 +- JDBC的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。而且,JDBC的这种架构,还合理的划分了应用程序开发人员和驱动程序开发人员的边界。 + +- Class.forName是用MySql还是Oracle,这个Driver一定会实现接口java.sql.Driver,然后通过DriverManager.registerDriver(new Driver());使DriverManager类持有一个Driver,是否可以把DriverManager当成桥,当成桥连接中的抽象类?然后持有一个接口Driver,至于是MySql还是Oracle,不关心,坐等传参。因为DriverManager持有的是一个Driver接口,你传过来什么,我就得到什么的实例化,然后我再通过getConnection用你的实例,去调用你自己的方法connect,去获得Connection的一个实例。 +- + +# 安全 +# XSS攻击 +# 原理 +- 跨站脚本攻击(Cross-Site Scripting, XSS):主要是指在用户浏览器内运行了JavaScript 脚本。比如富文本编辑器,如果不过滤用户输入的数据直接显示用户输入的HTML内容的话,就会有可能运行恶意的 JavaScript 脚本,导致页面结构错乱,Cookies 信息被窃取等问题。 + +- XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。 +- 常见的XSS攻击类型有两种,一种是反射型,一种是持久型。 +## 反射型 +- 攻击者诱使用户点击一个嵌入恶意脚本的链接,达到工具的目的。 +- 比如新浪微博中,攻击者发布的微博中含有一个恶意脚本的URL(URL中包含脚本的链接),用户点击该URL,脚本会自动关注攻击者的新浪微博ID,发布含有恶意脚本URL的微博,攻击就被扩散了。 +- 现实中,攻击者可以采用XSS攻击,偷取用户Cookie、密码等重要数据,进而伪造交易、盗窃用户财产、窃取情报。 + +## 持久型 +- 黑客提交含有恶意脚本的请求,保存在被攻击的Web站点的数据库中,用户浏览网页时,恶意脚本被包含在正常页面中,达到攻击的目的。此种攻击经常使用在论坛、博客等Web应用中。 + + +# 预防 +- Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。 +- 后端在入库前应该选择不相信任何前端数据,将所有的字段统一进行转义处理。 +- 后端在输出给前端数据统一进行转义处理。 +- 前端在渲染页面 DOM 的时候应该选择不相信任何后端数据,任何字段都需要做转义处理。 +## 消毒 +- XSS攻击者一般都是在请求中嵌入恶意脚本达到攻击的目的,这些脚本是一般在用户输入中不常用的,如果进行过滤和消毒处理,即对某些HTML危险字符转义,如”>”转义为”>”,就可以防止大部分的攻击。为了避免对不必要的内容错误转义,如”3<5”中的”<”需要进行文本匹配后再转义,如”<img src=”这样上下文中的”<”才转义。 +## HttpOnly +- 浏览器禁止页面JavaScript访问带有HttpOnly属性的Cookie。 +# SQL注入 +# 原理 +- 针对 Web 应用使用的数据库,通过运行非法的SQL而产生的攻击。 + + +# 预防 +- 所有的查询语句建议使用数据库提供的参数化查询接口,使用SQL预编译和参数绑定。 +- 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测。 +# CSRF攻击 +# 原理 +- CSRF(Cross-site request forgery)跨站请求伪造,利用跨站请求,在用户不知情的情况下,以用户的身份伪造请求,其核心是利用了浏览器Cookie或者Session,盗取用户身份。 +- 下面是CSRF的常见特性: +- 依靠用户标识危害网站 +- 利用网站对用户标识的信任,欺骗用户的浏览器发送HTTP请求给目标站点 + +- 攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过 QQ 等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使 Web 应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个 QQ 好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。 + +- 所以遇到 CSRF 攻击时,将对终端用户的数据和操作指令构成严重的威胁。当受攻击的终端用户具有管理员帐户的时候,CSRF 攻击将危及整个 Web 应用程序。 +- + +# 预防 +- 防御手段主要是识别请求者身份。 + +- 1、重要数据交互采用POST进行接收,当然是用POST也不是万能的,伪造一个form表单即可破解 +- 2、使用验证码,只要是涉及到数据交互就先进行验证码验证,这个方法可以完全解决CSRF。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。 +- 3、验证HTTP Referer字段,该字段记录了此次HTTP请求的来源地址,最常见的应用是图片防盗链。PHP中可以采用APache URL重写规则进行防御,可参考:http://www.cnblogs.com/phpstudy2015-6/p/6715892.html(但是这个也是可以绕过的) +- 4、为每个表单添加令牌token并验证(推荐) +- (可以使用cookie或者session进行构造。当然这个token仅仅只是针对CSRF攻击,在这前提需要解决好XSS攻击,否则这里也将会是白忙一场【XSS可以偷取客户端的cookie】) +- 可以为每一个表单生成一个随机数秘钥,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝该请求。 + +- 1. 用户访问某个表单页面。 +- 2. 服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。【这里已经不考虑XSS攻击】 +- 3. 在页面表单附带上Token参数。 +- 4. 用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。 +# DDoS 攻击 +# 原理 +- DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用。 +- TCP的半连接 +# 预防 +- 网络架构上做好优化,采用负载均衡分流。 +- 确保服务器的系统文件是最新的版本,并及时更新系统补丁。 +- 添加抗 DDos 设备,进行流量清洗。 +- 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。 +- 限制单 IP 请求频率。 +- 防火墙等防护设置禁止 ICMP 包等。 +- 严格限制对外开放的服务器的向外访问。 +- 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。 +- 关闭不必要的服务。 +- 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。 +- 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。 +# 加密算法 +- 什么是对称加密,什么是非对称加密,知道的加密算法有哪些 +- BCrypt算法 +- 对称加密在加密和解密的过程中,使用相同的密钥;而非对称加密在加密过程中使用公钥进行加密,使用私钥进行解密。 +# 对称加密 + +- 所谓常规密钥密码体制,即加密密钥与解密密钥是相同的密码体制。这种加密系统又称为对称密钥系统。 +- 对称加密算法有:DES、AES等。 +- DES 的保密性仅取决于对密钥的保密,而算法是公开的。尽管人们在破译 DES 方面取得了许多进展,但至今仍未能找到比穷举搜索密钥更有效的方法。 +- DES 是世界上第一个公认的实用密码算法标准,它曾对密码学的发展做出了重大贡献。 +- 目前较为严重的问题是 DES 的密钥的长度。 +- 现在已经设计出来搜索 DES 密钥的专用芯片。 + +- 对称加密的加密和解密需要使用相同的密钥,所以需要解决密钥配送问题。 + +# 非对称加密 +- 公钥密码体制使用不同的加密密钥与解密密钥,是一种“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。 +- 公钥密码体制的产生主要是因为两个方面的原因,一是由于常规密钥密码体制的密钥分配问题,另一是由于对数字签名的需求。 +- 现有最著名的公钥密码体制是RSA 体制,它基于数论中大数分解问题的体制,由美国三位科学家 Rivest, Shamir 和 Adleman 于 1976 年提出并在 1978 年正式发表。 + + +- 可以用公钥加密,私钥解密;也可以用私钥加密、公钥解密。 +- 若密钥能够实现安全交换,那么有可能会考虑仅使用公开密钥加密来通信。但是公开密钥加密与共享密钥加密相比,其处理速度要慢。 +- 任何加密方法的安全性取决于密钥的长度,以及攻破密文所需的计算量 +- 在这方面,公钥密码体制并不比传统加密体制更加优越 +- 由于目前公钥加密算法的开销较大,在可见的将来还不会放弃传统的加密方法 +- 公钥需要密钥分配协议,具体的分配过程并不比采用传统加密方法时更简单 + +# 消息摘要/单向散列 +- 单向散列函数也称为消息摘要函数(message digest function),哈希函数,适用于检查消息完整性的加密技术。 + +- 单向散列函数有一个输入和一个输出,其中输入称为信息,输出称为散列值。单向散列函数可以根据消息的内容计算出散列值,篡改后的信息的散列值计算结果会不一样,所以散列值可以被用来检查消息的完整性 。 +- 常见消息摘要技术:MD5、SHA-1、SHA-256 + +- CRC、MD5、SHA1都是通过对数据进行计算,来生成一个校验值,该校验值用来校验数据的完整性。 + +- 不同点: +- 1. 算法不同。CRC采用多项式除法,MD5和SHA1使用的是替换、轮转等方法; +- 2. 校验值的长度不同。CRC校验位的长度跟其多项式有关系,一般为16位或32位;MD5是16个字节(128位);SHA1是20个字节(160位); +- 3. 校验值的称呼不同。CRC一般叫做CRC值;MD5和SHA1一般叫做哈希值(Hash)或散列值; +- 4. 安全性不同。这里的安全性是指检错的能力,即数据的错误能通过校验位检测出来。CRC的安全性跟多项式有很大关系,相对于MD5和SHA1要弱很多;MD5的安全性很高,不过大概在04年的时候被山东大学的王小云破解了;SHA1的安全性最高(现在SHA-256安全性比较高)。 +- 5. 效率不同,CRC的计算效率很高;MD5和SHA1比较慢。 +- 6. 用途不同。CRC一般用作通信数据的校验;MD5和SHA1用于安全(Security)领域,比如文件校验、数字签名等。 + +# 密码 + +- 利用单向散列加密的特性,可以进行密码加密保存,即用户注册时输入的密码不直接保存到数据库,而是对密码进行单向散列加密,将密文存入数据库,用户登录时,进行密码验证,同样计算得到输入密码的密文,并和数据库中的密文比较,如果一致,则密码验证成功。 + +- 这样保存在数据库中的是用户输入的密码的密文,而且不可逆地计算得到密码的明文,因此即使数据库被拖库(指网站遭到入侵后,黑客窃取其数据库),也不会泄露用户的密码信息。 +- 虽然不能通过算法将单向散列密文反算得到明文,但是由于人们设置密码具有一定的模式吗,因此通过彩虹表(建立一个 源数据与加密数据之间对应的hash表。这样在获得加密数据后通过比较,查询或者一定的运算,可以快速定位源数据)等手段可以进行猜测式破解。 + +- 为了加强单向散列计算的安全性,还会给散列算法加点盐,salt相当于加密的密钥,增加破解的难度。盐一般都是跟hash一起保存在数据库里,或者作为hash字符串的一部分。salt是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。 +# 算法介绍 +- sha比md5更安全一些,sha比md5哈希碰撞的概率更小一些。 + +- 到目前为止,我们已经了解如何为密码生成安全的 Hash 值以及通过利用 salt 来加强它的安全性。但今天的问题是,硬件的速度已经远远超过任何使用字典或彩虹表进行的暴力攻击,并且任何密码都能被破解,只是使用时间多少的问题。 + +- 为了解决这个问题,主要想法是尽可能降低暴力攻击速度来保证最小化的损失。我们下一个算法同样是基于这个概念。目标是使 Hash 函数足够慢以妨碍攻击,并对用户来说仍然非常快且不会感到有明显的延时。 + +- 要达到这个目的通常是使用某些 CPU 密集型算法来实现,比如 PBKDF2, Bcrypt 或 Scrypt 。这些算法采用 work factor(也称之为 security factor)或迭代次数作为参数来确定 Hash 函数将变的有多慢,并且随着日后计算能力的提高,可以逐步增大 work factor 来使之与计算能力达到平衡。 + +- bcrypt是单向的,而且经过salt和cost的处理,使其受rainbow攻击破解的概率大大降低,同时破解的难度也提升不少。 +- 因为bcrypt采用了一系列各种不同的Blowfish加密算法,并引入了一个work factor,这个工作因子可以让你决定这个算法的代价有多大。因为这些,这个算法不会因为计算机CPU处理速度变快了,而导致算法的时间会缩短了。因为,你可以增加work factor来把其性能降下来。 +# 数字签名 +- 别人不能冒充我的签名(不可伪造),我也不能否认上面的签名是我的(不可抵赖)。 +- 数字签名又是靠什么保证不可伪造和不可抵赖两个特性呢? +- 答案是利用公钥加密系统。 +- RSA既可以用公钥加密然后私钥解密,也可以用私钥加密然后公钥解密(对称性)。 +- 因为RSA中的每一个公钥都有唯一的私钥与之对应,任一公钥只能解开对应私钥加密的内容。换句话说,其它私钥加密的内容,这个公钥是解不开的。 +- 这样,如果你生成了一对RSA密钥,你把公钥公布出去,并告诉全世界人这个公钥是你的。之后你只要在发送的消息,比如“123456”,后面加上用私钥加密过的密文,其他人拿公钥解密,看解密得到的内容是不是“123456”就可以知道这个“123456”是不是你发的。 + +- 其他人因为没有对应的私钥,所以没法生成公钥可以解密的密文,所以是不可伪造的。 +- 又因为公钥对应的私钥只有一个,所以只要能成功解密,那么发消息的一定是你,不会是其他人,所以是不可抵赖的。 + +- 由于直接对原消息进行签名有安全性问题,而且原消息往往比较大,直接使用RSA算法进行签名速度会比较慢,所以我们一般对消息计算其摘要(使用SHA-256等安全的摘要算法),然后对摘要进行签名。只要使用的摘要算法是安全的(MD5、SHA-1已经不安全了),那么这种方式的数字签名就是安全的。 + +- 一个具体的RSA签名过程如下: + +- 小明对外发布公钥,并声明对应的私钥在自己手上 +- 小明对消息M计算摘要,得到摘要D +- 小明使用私钥对D进行签名,得到签名S +- 将M和S一起发送出去 +- 验证过程如下: + +- 接收者首先对M使用跟小明一样的摘要算法计算摘要,得到D +- 使用小明公钥对S进行解签,得到D’ +- 如果D和D’相同,那么证明M确实是小明发出的,并且没有被篡改过。 + + +- 报文鉴别——接收者能够核实发送者对报文的签名 +- 报文的完整性——发送者事后不能抵赖对报文的签名 +- 不可否认——接收者不能伪造对报文的签名 +# 密钥管理 +- 对称密码的密钥、非对称加密的私钥、salt等都需要保证不被泄露。 +- 改善密钥安全性的方式有两种: + - 1)把密钥和算法放在一个单独的服务器上,对外提供加密和解密服务,应用系统通过调用这个服务实现数据的加解密。容易成为应用瓶颈,系统性能开销较高。 + - 2)将加解密算法放到应用系统中,密钥放在独立服务器中。实际存储时,密钥被切分成薯片,加密后分别保存在不同存储介质中,兼顾密钥安全性的同时又改善了性能。 + + + +- + +# HTTP +# HTTP概述 +- HTTP(hypertext transport protocol),即超文本传输协议。这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。 +- HTTP就是一个通信规则,通信规则规定了客户端发送给服务器的内容格式,也规定了服务器发送给客户端的内容格式。其实我们要学习的就是这个两个格式!客户端发送给服务器的格式叫“请求协议”;服务器发送给客户端的格式叫“响应协议”。 +# 请求协议 +- 请求协议的格式如下: +请求首行; 包括请求类型,要访问的资源以及所使用的HTTP版本. +请求头信息; +空行; \r\n DOS/Windows:’\r\n’ UNIX/Linux:’\n’ Mac:’\r’ +请求体。 + +-   浏览器发送给服务器的内容就这个格式的,如果不是这个格式服务器将无法解读!在HTTP协议中,请求有很多请求方法,其中最为常用的就是GET和POST。不同的请求方法之间的区别,后面会一点一点的介绍。 + +# GET请求 +-   打开IE,在访问hello项目的index.jsp之间打开HttpWatch,并点击“Record”按钮。然后访问index.jsp页面。查看请求内容如下: + +GET /hello/index.jsp HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: zh-cn,zh;q=0.5 +Accept-Encoding: gzip, deflate +Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7 +Connection: keep-alive +Cookie: JSESSIONID=369766FDF6220F7803433C0B2DE36D98 +   + +- GET /hello/index.jsp HTTP/1.1:GET请求,请求服务器路径为/hello/index.jsp,协议为1.1; +- Host:localhost:请求的主机名为localhost; +- User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0:与浏览器和OS相关的信息。有些网站会显示用户的系统版本和浏览器版本信息,这都是通过获取User-Agent头信息而来的; + +``` +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8:告诉服务器,当前客户端可以接收的文档类型,其实这里包含了*/*,就表示什么都可以接收; +``` + +- Accept-Language: zh-cn,zh;q=0.5:当前客户端支持的语言,可以在浏览器的工具选项中找到语言相关信息; +- Accept-Encoding: gzip, deflate:支持的压缩格式。数据在网络上传递时,可能服务器会把数据压缩后再发送; +- Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7:客户端支持的编码; +- Connection: keep-alive:客户端支持的链接方式,保持一段时间链接,默认为3000ms;(无状态的) +- Cookie: JSESSIONID=369766FDF6220F7803433C0B2DE36D98:因为不是第一次访问这个地址,所以会在请求中把上一次服务器响应中发送过来的Cookie在请求中一并发送去过 + +# POST请求 +- 为了演示POST请求,我们需要修改index.jsp页面,即添加一个表单: +<form action="" method="post"> + 关键字:<input type="text" name="keyword"/> + <input type="submit" value="提交"/> +</form> + + +- 打开HttpWatch,输入hello后点击提交,查看请求内容如下: +POST /hello/index.jsp HTTP/1.1 +Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, */* +Referer: http://localhost:8080/hello/index.jsp +Accept-Language: zh-cn,en-US;q=0.5 +User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) +Content-Type: application/x-www-form-urlencoded +Accept-Encoding: gzip, deflate +Host: localhost:8080 +Content-Length: 13 +Connection: Keep-Alive +Cache-Control: no-cache +Cookie: JSESSIONID=E365D980343B9307023A1D271CC48E7D + +keyword=hello + +- POST请求是可以有体的,而GET请求不能有请求体。 +- Referer: http://localhost:8080/hello/index.jsp:请求来自哪个页面,例如你在百度上点击链接到了这里,那么Referer:http://www.baidu.com;如果你是在浏览器的地址栏中直接输入的地址,那么就没有Referer这个请求头了; +- Content-Type: application/x-www-form-urlencoded:表单的数据类型,说明会使用url格式编码数据;url编码的数据都是以“%”为前缀,后面跟随两位的16进制,例如“传智”这两个字使用UTF-8的url编码用为“%E4%BC%A0%E6%99%BA”; +- Content-Length:13:请求体的长度,这里表示13个字节。 +- keyword=hello:请求体内容!hello是在表单中输入的数据,keyword是表单字段的名字。 + +- Referer请求头是比较有用的一个请求头,它可以用来做统计工作,也可以用来做防盗链。 +- 统计工作:我公司网站在百度上做了广告,但不知道在百度上做广告对我们网站的访问量是否有影响,那么可以对每个请求中的Referer进行分析,如果Referer为百度的很多,那么说明用户都是通过百度找到我们公司网站的。 +- 防盗链:我公司网站上有一个下载链接,而其他网站盗链了这个地址,例如在我网站上的index.html页面中有一个链接,点击即可下载JDK7.0,但有某个人的微博中盗链了这个资源,它也有一个链接指向我们网站的JDK7.0,也就是说登录它的微博,点击链接就可以从我网站上下载JDK7.0,这导致我们网站的广告没有看,但下载的却是我网站的资源。这时可以使用Referer进行防盗链,在资源被下载之前,我们对Referer进行判断,如果请求来自本网站,那么允许下载,如果非本网站,先跳转到本网站看广告,然后再允许下载。 +# GET和POST请求的区别 +- 幂等意味着对同一URL的多个请求应该返回同样的结果。 + - 1)前者将请求参数放在URL中,文本格式;后者将请求参数放在请求体中,可以是文本、二进制等格式 + - 2)前者语义上是从服务器获取资源,安全(无副作用)、幂等、可缓存;后者语义上是向服务器提交资源,不安全(有副作用)、不幂等、不可缓存 + - 3)前者的URL是明文传输,会保存在浏览器历史记录中,安全性不足,可能会受到CSRF攻击;后者较为安全(但是如果没有加密的话,都是可以明文获取的) +# 其他请求方法 +- GET(SELECT):从服务器取出资源(一项或多项)。 +- POST(CREATE):在服务器新建一个资源。 +- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源,数据输入必须与由 GET 接收的数据表示保持一致)。 +- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 +- DELETE(DELETE):从服务器删除资源。 +- HEAD:获取资源的元数据。 +- OPTIONS:1、获取服务器支持的HTTP请求方法 +- 2、用来检查服务器的性能 +# 响应内容 +- 响应协议的格式如下: +响应首行;由HTTP协议版本号, 状态码, 状态消息 三部分组成。 +响应头信息; +空行; +响应体。 + +- 响应内容是由服务器发送给浏览器的内容,浏览器会根据响应内容来显示。 +HTTP/1.1 200 OK +Server: Apache-Coyote/1.1 +Content-Type: text/html;charset=UTF-8 +Content-Length: 724 +Set-Cookie: JSESSIONID=C97E2B4C55553EAB46079A4F263435A4; Path=/hello +Date: Wed, 25 Sep 2012 04:15:03 GMT + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> + <head> + <base href="http://localhost:8080/hello/"> + + <title>My JSP 'index.jsp' starting page + + + + + + + + + +
+ 关键字: + +
+ + + +- HTTP/1.1 200 OK:响应协议为HTTP1.1,状态码为200,表示请求成功,OK是对状态码的解释; +- Server: Apache-Coyote/1.1:服务器的版本信息; +- Content-Type: text/html;charset=UTF-8:响应体使用的编码为UTF-8; +- Content-Length: 724:响应体为724字节; +- Set-Cookie: JSESSIONID=C97E2B4C55553EAB46079A4F263435A4; Path=/hello:响应给客户端的Cookie; +- Date: Wed, 25 Sep 2012 04:15:03 GMT:响应的时间,这可能会有8小时的时区差; + +# 响应码 +- 响应头对浏览器来说很重要,它说明了响应的真正含义。例如200表示响应成功了,302表示重定向,这说明浏览器需要再发一个新的请求。 +- 2xx表示成功,3xx表示重定向,4xx表示客户端出错,5xx表示服务器出错。 +- 200:请求成功,浏览器会把响应体内容(通常是html)显示在浏览器中; +- 404:请求的资源没有找到,说明客户端错误的请求了不存在的资源; +- 500:请求资源找到了,但服务器内部出现了错误; +- 302:重定向,当响应码为302时,表示服务器要求浏览器重新再发一个请求,服务器会发送一个响应头Location,它指定了新请求的URL地址; +- 304:(缓存) +## 301&302 +- 301 Move Permanently +- 302 Found +- 301是永久性重定向。当网站需要改版时,多域名指向同一个页面时,为了不让网站被降低和分散权重,就需要使用301重定向来实现,同时在搜索引擎索引库中彻底废弃掉原先的老地址。 +- 302是临时性重定向,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的,不会传递权重。 +# 其他响应头 +- 告诉浏览器不要缓存的响应头: +- Expires: -1;(过期时间,-1表示马上过期) +- Cache-Control: no-cache;(不缓存) +- Pragma: no-cache;(不缓存) + +- 自动刷新响应头,浏览器会在3秒之后请求http://www.baidu.com: +- Refresh: 3;url=http://www.baidu.com + +# HTML中指定响应头 +- 在HTMl页面中可以使用来指定响应头,例如在index.html页面中给出,表示浏览器只会显示index.html页面3秒,然后自动跳转到http://www.itcast.cn。 +- + +# 缓存 +# 强缓存与协商缓存 +- 浏览器HTTP缓存可以分为强缓存和协商缓存。强缓存和协商缓存最大也是最根本的区别是:强缓存命中的话不会发请求到服务器(比如chrome中的200 from memory cache),协商缓存一定会发请求到服务器,通过资源的请求首部字段验证资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的实体,而是通知客户端可以从缓存中加载这个资源(304 not modified)。 + +# 控制强缓存的字段按优先级介绍 +1.- Pragma        Pragma是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。虽然它是一个通用首部,但是它在响应报文中时的行为没有规范,依赖于浏览器的实现。RFC中该字段只有no-cache一个可选值,会通知浏览器不直接使用缓存,要求向服务器发请求校验新鲜度。因为它优先级最高,当存在时一定不会命中强缓存。 +2.- Cache-Control        Cache-Control是一个通用首部字段,也是HTTP/1.1控制浏览器缓存的主流字段。和浏览器缓存相关的是如下几个响应指令: +指令 参数 说明 +private 无 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它) +public 可省略 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存 +no-cache 可省略 缓存前必需确认其有效性 +no-store 无 不缓存请求或响应的任何内容 +max-age=[s] 必需 响应的最大值 +- max-age(单位为s)设置缓存的存在时间,相对于发送请求的时间。只有响应报文首部设置Cache-Control为非0的max-age或者设置了大于请求日期的Expires(下文会讲)才有可能命中强缓存。当满足这个条件,同时响应报文首部中Cache-Control不存在no-cache、no-store且请求报文首部不存在Pragma字段,才会真正命中强缓存。 +- no-cache 表示请求必须先与服务器确认缓存的有效性,如果有效才能使用缓存(协商缓存),无论是响应报文首部还是请求报文首部出现这个字段均一定不会命中强缓存。Chrome硬性重新加载(Command+shift+R)会在请求的首部加上Pragma:no-cache和Cache-Control:no-cache。 +- no-store 表示禁止浏览器以及所有中间缓存存储任何版本的返回响应,一定不会出现强缓存和协商缓存,适合个人隐私数据或者经济类数据。 + +``` +public 表明响应可以被浏览器、CDN等等缓存。 +``` + + +``` +private 响应只作为私有的缓存,不能被CDN等缓存。如果要求HTTP认证,响应会自动设置为private。 +``` + +3.- Expires        Expires是一个响应首部字段,它指定了一个日期/时间,在这个时间/日期之前,HTTP缓存被认为是有效的。无效的日期比如0,表示这个资源已经过期了。如果同时设置了Cache-Control响应首部字段的max-age,则Expires会被忽略。它也是HTTP/1.1之前版本遗留的通用首部字段,仅作为于HTTP/1.0的向后兼容而使用。 +# 控制协商缓存的字段 +1.- Last-Modified/If-Modified-Since        If-Modified-Since是一个请求首部字段,并且只能用在GET或者HEAD请求中。Last-Modified是一个响应首部字段,包含服务器认定的资源作出修改的日期及时间。当带着If-Modified-Since头访问服务器请求资源时,服务器会检查Last-Modified,如果Last-Modified的时间早于或等于If-Modified-Since则会返回一个不带主体的304响应,否则将重新返回资源。 +- If-Modified-Since: , :: GMT Last-Modified: , :: GMT +2.- ETag/If-None-Match        ETag是一个响应首部字段,它是根据实体内容生成的一段hash字符串,标识资源的状态,由服务端产生。If-None-Match是一个条件式的请求首部。如果请求资源时在请求首部加上这个字段,值为之前服务器端返回的资源上的ETag,则当且仅当服务器上没有任何资源的ETag属性值与这个首部中列出的时候,服务器才会返回带有所请求资源实体的200响应,否则服务器会返回不带实体的304响应。ETag优先级比Last-Modified高,同时存在时会以ETag为准。 +- 因为ETag的特性,所以相较于Last-Modified有一些优势: +- 1. 某些情况下服务器无法获取资源的最后修改时间 +- 2. 资源的最后修改时间变了但是内容没变,使用ETag可以正确缓存 +- 3. 如果资源修改非常频繁,在秒以下的时间进行修改,Last-Modified只能精确到秒 + +# 协商缓存细节 +- 当用户第一次请求index.html时,服务器会添加一个名为Last-Modified响应头,这个头说明了index.html的最后修改时间,浏览器会把index.html内容,以及最后响应时间缓存下来。当用户第二次请求index.html时,在请求中包含一个名为If-Modified-Since请求头,它的值就是第一次请求时服务器通过Last-Modified响应头发送给浏览器的值,即index.html最后的修改时间,If-Modified-Since请求头就是在告诉服务器,我这里浏览器缓存的index.html最后修改时间是这个,您看看现在的index.html最后修改时间是不是这个,如果还是,那么您就不用再响应这个index.html内容了,我会把缓存的内容直接显示出来。而服务器端会获取If-Modified-Since值,与index.html的当前最后修改时间比对,如果相同,服务器会发响应码304,表示index.html与浏览器上次缓存的相同,无需再次发送,浏览器可以显示自己的缓存页面,如果比对不同,那么说明index.html已经做了修改,服务器会响应200。(只有html等静态资源可以做缓存,动态资源不做缓存) + +- 响应头: +- Last-Modified:最后的修改时间; +- 请求头: +- If-Modified-Since:把上次请求的index.html的最后修改时间还给服务器; +- 状态码:304,比较If-Modified-Since的时间与文件真实的时间一样时,服务器会响应304,而且不会有响应正文,表示浏览器缓存的就是最新版本! + + +# 幂等性(并非是HTTP的问题,而是服务器API设计问题) +- 幂等性是http层面的问题吗,还是服务器要处理和解决的内容? + +- 对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。这里所讨论的HTTP幂等性主要针对RESTful风格的,但幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。 + +- HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。 +- 比如:GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。 + +- HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 +- 比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。 + +- 比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。而PUT所对应的URI是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。 + +# 无状态 +- 客户端和服务器在某次会话中产生的数据,从而【无状态】就意味着,这些数据不会被保留;协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。但是通过增加cookie和session机制,现在的网络请求其实是有状态的。在没有状态的http协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话。 +- 与之相对的是TCP,TCP是有状态的,因为每一条消息的seq和ack(还有一堆滑动窗口,拥塞的控制参数,等)都和前面消息相关。 +- HTTP并不会在内存里保留前次请求相关的任何状态,仅仅以协议逻辑(打包解包)存在,所以是它无状态的。 + +- 无状态的设计会加强透明度(visibility),稳定度(reliability)和伸缩度(scalability)。提高透明度是因为系统无需通过请求内容以外的信息判断请求的完整内容;提高稳定度是指在部分失败的情况下,减轻了恢复的难度;提高伸缩度的原因是无需储存请求间的状态使服务器端可以很快释放资源并简化实现。 +- 优点在于解放了服务器,每一次请求“点到为止”不会造成不必要连接占用,缺点在于每次请求会传输大量重复的内容信息。 +# 跨域 CORS 跨域资源共享 +- 之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 +- 同源策略具体限制些什么呢? +- 1. 不能向工作在不同源的的服务请求数据(client to server)这里有个问题之前也困扰了我很久,就是为什么home.com加载的cdn.home.com/index.js可以向home.com发请求而不会跨域呢?其实home.com加载的JS是工作在home.com的,它的源不是提供JS的cdn,所以这个时候是没有跨域的问题的,并且script标签能够加载非同源的资源,不受同源策略的影响。 +- 2. 无法获取不同源的document/cookie等BOM和DOM,可以说任何有关另外一个源的信息都无法得到 (client to client)。 + +- 跨域最常用的方法,应当属CORS,如下图所示: + +- 只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。 +- CORS把请求分为两种,一种是简单请求,另一种是需要触发预检请求,这两者是相对的,怎样才算“不简单”?只要属于下面的其中一种就不是简单请求: + - (1)使用了除GET/POST/HEAD之外的请求方式,如PUT/DELETE + - (2)使用了除Content-Type/Accept等几个常用的http头这个时候就认为需要先发个预检请求 +# 简单请求 +- 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 + +- 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。 + +- GET /cors HTTP/1.1 +- Origin: http://api.bob.com +- Host: api.alice.com +- Accept-Language: en-US +- Connection: keep-alive +- User-Agent: Mozilla/5.0... +- 上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。 + +- 如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。 + +- 如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。 + + +- Access-Control-Allow-Origin: http://api.bob.com +- Access-Control-Allow-Credentials: true +- Access-Control-Expose-Headers: FooBar +- Content-Type: text/html; charset=utf-8 +- 上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。 + - (1)Access-Control-Allow-Origin +- 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 + - (2)Access-Control-Allow-Credentials +- 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 + - (3)Access-Control-Expose-Headers +- 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。 +# 非简单请求 +- 简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。 + +- 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 + +- 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式XMLHttpRequest请求,否则就报错。 +- "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。 +- 除了Origin字段,"预检"请求的头信息包括两个特殊字段。 + - (1)Access-Control-Request-Method +- 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法 + - (2)Access-Control-Request-Headers +- 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段 +- 服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。 + +- 如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。 + +- 一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。 + +- CORS与JSONP的使用目的相同,但是比JSONP更强大。 + +- JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。 +# 长轮询与短轮询 +- 短轮询相信大家都不难理解,比如你现在要做一个电商中商品详情的页面,这个详情界面中有一个字段是库存量(相信这个大家都不陌生,随便打开淘宝或者京东都能找到这种页面)。而这个库存量需要实时的变化,保持和服务器里实际的库存一致。 + +- 这个时候,你会怎么做? + +- 最简单的一种方式,就是你用JS写个死循环,不停的去请求服务器中的库存量是多少,然后刷新到这个页面当中,这其实就是所谓的短轮询。 + +- 这种方式有明显的坏处,那就是你很浪费服务器和客户端的资源。客户端还好点,现在PC机配置高了,你不停的请求还不至于把用户的电脑整死,但是服务器就很蛋疼了。如果有1000个人停留在某个商品详情页面,那就是说会有1000个客户端不停的去请求服务器获取库存量,这显然是不合理的。 + +- 那怎么办呢? + +- 长轮询这个时候就出现了,其实长轮询和短轮询最大的区别是,短轮询去服务端查询的时候,不管库存量有没有变化,服务器就立即返回结果了。而长轮询则不是,在长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒,Object.wait)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回(Object.notify),否则就一直等到超时为止。 + +- 而对于客户端来说,不管是长轮询还是短轮询,客户端的动作都是一样的,就是不停的去请求,不同的是服务端,短轮询情况下服务端每次请求不管有没有变化都会立即返回结果,而长轮询情况下,如果有变化才会立即返回结果,而没有变化的话,则不会再立即给客户端返回结果,直到超时为止。 +- 这样一来,客户端的请求次数将会大量减少(这也就意味着节省了网络流量,毕竟每次发请求,都会占用客户端的上传流量和服务端的下载流量),而且也解决了服务端一直疲于接受请求的窘境。 + +- 但是长轮询也是有坏处的,因为把请求挂起同样会导致资源的浪费,假设还是1000个人停留在某个商品详情页面,那就很有可能服务器这边挂着1000个线程,在不停检测库存量,这依然是有问题的。 + +- 因此,从这里可以看出,不管是长轮询还是短轮询,都不太适用于客户端数量太多的情况,因为每个服务器所能承载的TCP连接数是有上限的,这种轮询很容易把连接数顶满。 + +- + +# 长连接与短连接 +- HTTP的短连接和长连接;长连接与短连接的区别(LVS是通过长连接作负载均衡) +- HTTP的长连接和短连接本质上是TCP长连接和短连接。 +- 在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 + +- 但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive。 +- 在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。 + +- 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损。 + +- 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。 +# URL +- url有最大长度限制,就问长度有限制是get的原因还是url的原因,为什么长度会有限制,是http数据包的头的字段原因还是内容字段的原因 +- 是GET的原因,长度受到服务器和客户端的限制。 + +- URL编解码 +- Url的编码格式采用的是ASCII码,而不是Unicode,这也就是说你不能在Url中包含任何非ASCII字符,例如中文。 + - Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符; +- RFC3986中指定了以下字符为保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ] + +- Url编码通常也被称为百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的十六进制字符。 +# URI&URL +- URL(Uniform ResourceLocator)统一资源定位符,是专门为标识网络上的资源位置而设计的一种编址方式。URL一般由3个部分组成: +- 应用层协议 +- 主机IP地址或域名 +- 资源所在路径/文件名 + +- 统一资源标识符(Uniform Resource Identifier,或URI)是一个用于标识某一互联网资源名称的字符串。 +- URI :Uniform Resource Identifier,统一资源标识符; +- URL:Uniform Resource Locator,统一资源定位符; +- URN:Uniform ResourceName,统一资源名称。 +- 其中,URL,URN是URI的子集。 +- URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位。 +# HTTPS +# HTTP缺点 +- 明文传输,内容可能会被窃听 +- 不验证通信方的身份,因此有可能遭遇伪装 +- 无法证明报文的完整性,所以有可能已遭篡改 +- HTTP+加密+认证+完整性保护=HTTPS +- + +- 用SSL将通信的报文主体内容进行加密,使用SSL建立http的安全通信线路,SSL处于http与TCP通信之间,这样的SSL与HTTP组合被称为HTTPS。 +- HTTPS 采用对称加密和非对称加密两者并用的混合加密机制 + +- HTTPS 公钥能用公钥解吗?在客户端抓包,看到的是加密的还是没加密 是没加密的 +- https ssl tcp三者关系,其中哪些用到了对称加密,哪些用到了非对称加密,非对称加密密钥是如何实现的 +- 加密的私钥和公钥各自如何分配(客户端拿公钥,服务器拿私钥) +- 客户端是如何认证服务器的真实身份,详细说明一下过程,包括公钥如何申请,哪一层加密哪一层解密 +- 怎么攻击https +- TLS改进,如果session ticket被偷听到会怎样,如何防止中间人攻击 + +# SSL/TLS +- SSL(Secure Socket Layer安全套接字层) +- TLS(Transport Layer Security) + +- SSL发展到3.0版本后改成了TLS。 +- TLS主要提供三个基本服务 +- 加密 +- 身份验证 +- 消息完整性校验 + +- 通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信了。用 SSL 建立安全通信线路之后,就可以在这条线路上进行 HTTP 通信了。 +- SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP 和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全技术。 +- 虽然使用 HTTP 协议无法确定通信方,但如果使用 SSL 则可以。SSL 不仅提供加密处理,而且还使用了一种被称为证书的手段,可用于确定双方身份。 +- 证书由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的。另外,伪造证书从技术角度来说是异常困难的一件事。所以只要能够确认通信方(服务器或客户端)持有的证书,即可判断通信方的真实意图。 +# 中间人攻击 +- mim 就是man in the middle,中间人攻击正常情况下浏览器与服务器在TLS连接下内容是加密的,第三方即使可以嗅探到所有的数据,也不能解密。中间人可以与你建立连接,然后中间人再与服务器建立连接,转发你们之间的内容。这时候中间人就获得了明文的信息。 +- 有什么危害?你与服务器的通信被第三方解密、查看、修改。如何防范?如果确定是否被攻击?在访问https连接的时候,查看一下服务器提供的证书是不是正确的。除非入侵并取得服务器的证书私钥,否则中间人是不能完全伪装成服务器的样子的。 +- 数字证书可以保证服务器发来的公钥是真的来自服务器的 +# 服务器保证其提供的公钥的正确性——数字证书 +- 公钥是由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。 +- 数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的立场上。服务器会将这份由数字证书认证机构颁发的公钥证书发送给客户端,以进行公开密钥加密方式通信。公钥证书也可叫做数字证书或直接称为证书。 +- 接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书上的数字签名进行验证,一旦验证通过,客户端便可明确两件事: +- 认证服务器的公开密钥的是真实有效的数字证书认证机构 +- 服务器的公开密钥是值得信赖的 + +- 此处认证机关的公开密钥必须安全地转交给客户端。使用通信方式时,如何安全转交是一件很困难的事,因此,多数浏览器开发商发布版本时,会事先在内部植入常用认证机关的公开密钥。 + + +# 过程 +-  客户端发起HTTPS请求 这个没什么好说的,就是用户在浏览器里输入一个HTTPS网址,然后连接到服务端的443端口。 +-  服务端的配置 采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。 +-  传送证书 这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。 +-  客户端解析证书 这部分工作是由客户端的SSL/TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警示框,提示证书存在的问题。如果证书没有问题,那么就生成一个随机值。然后用证书(也就是公钥)对这个随机值进行加密。 +-  传送加密信息 这部分传送的是用证书加密后的随机值,目的是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。 +-  服务端解密信息 服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该随机值(密钥)进行对称加密,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够复杂,私钥够复杂,数据就够安全。 +-  传输加密后的信息 这部分信息就是服务端用私钥加密后的信息,可以在客户端用随机值解密还原。 +-  客户端解密信息 客户端用之前生产的私钥解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。 + +- 客户端获得服务器的公钥的过程是基于非对称加密实现的(数字证书) +- 而之后客户端和服务器之间的数据交换是基于对称加密实现的。 +# 更具体的过程 +-  客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等) +-  服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收 到的客户端加密组件内筛选出来的。 +-  之后服务器发送 Certificate 报文。报文中包含公开密钥证书。 +-  最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的 SSL 握手协商部分结束。 +-  SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该 报文已用步骤 3 中的公开密钥进行加密。 +-  接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。 +-  客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。 +-  服务器同样发送 Change Cipher Spec 报文。 +-  服务器同样发送 Finished 报文。 +-  服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发 送 HTTP 请求。 +-  应用层协议通信,即发送 HTTP 响应。 +-  最后由客户端断开连接。断开连接时,发送 close_notify 报文。 + +- + +# WebSocket +- web浏览器和web服务器之间全双工通信标准。 +- 优点是,直接发送数据,不用等待客户端请求,一直保持连接状态,且首部信息量少,通信量减少。 + +- + +# HTTP 2.0 +# 二进制分帧 +- 在应用层(HTTP2.0)和传输层(TCP or UDP)之间增加一个二进制分帧层。 +- 在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。Frame 由 Frame Header 和 Frame Payload 两部分组成。不论是原来的 HTTP Header 还是 HTTP Body,在 HTTP/2 中,都将这些数据存储到 Frame Payload,组成一个个 Frame,再发送响应/请求。通过 Frame Header 中的 Type 区分这个 Frame 的类型。由此可见语义并没有太大变化,而是数据的格式变成二进制的 Frame。 +- HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。 + +# 首部压缩 +- HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。 + +- 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。 +- HTTP/2 使用了专门为首部压缩而设计的 HPACK 算法。 + +# 服务器推送 +- HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,服务器除了对最初请求的响应外,还可以额外向客户端推送资源,而无需客户端明确地请求。 + +- 当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。我们常用的内嵌图片也可以理解为一种强制的服务器推送:我请求html,却内嵌了张图。 + +- 有了HTTP2.0的服务器推送,HTTP1.x时代的内嵌资源的优化手段也变得没有意义了。而且使用服务器推送的资源的方式更加高效,因为客户端还可以缓存起来,甚至可以由不同的页面共享(依旧遵循同源策略)。当然,浏览器是可以决绝服务器推送的资源的。 +# 多路复用 +- 多路复用允许同时通过单一的HTTP/2连接发起多重的请求-响应信息。 + +- 每个 Frame Header 都有一个 Stream ID 就是被用于实现该特性。每次请求/响应使用不同的 Stream ID。就像同一个 TCP 链接上的数据包通过 IP:PORT来区分出数据包去往哪里一样。通过 Stream ID 标识,所有的请求和响应都可以欢快的同时跑在一条 TCP 链接上了。 + +# SOA +- Web Service也叫XML Web Service WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术。是通过SOAP在Web上提供的软件服务,使用WSDL文件进行说明,并通过UDDI进行注册。 +- SOA是一种架构风格,包括两个方面的内容: + - 1)抽象出服务,这些服务满足离散、松耦合、可复用、自治、无状态等特征; + - 2)服务可以灵活地组装和编排,满足流程整合和业务变化的需要 + +- WebService是SOA的一种实现技术,跨语言,跨平台,提供了标准的服务定义、服务注册、服务接入和访问的方式。使用了XML、SOAP、WSDL、UDDI等技术。 + +# SOA三角操作模型 + - 1)三种角色 +- 服务提供者:发布自己的服务,并且对服务请求进行响应 +- 服务请求者:利用服务注册中心查找所需要的服务,然后使用该服务 +- 服务注册中心:注册已经发布的服务,对其进行分类,并提供搜索服务 + - 2)三个操作: +- 发布:为了使服务可访问,需要发布服务描述以使服务使用者可以发现它 +- 查找:服务请求者查询服务注册中心来找到满足其要求的服务 +- 绑定:检索到服务描述后,服务请求者继续根据服务描述中的信息调用服务 +# XML +- XML:(Extensible Markup Language)扩展型可标记语言。面向短期的临时数据处理、面向万维网络,是Soap的基础。 +# SOAP +- SOAP:(Simple Object Access Protocol)简单对象传输协议。是XML Web Service 的通信协议。当用户通过UDDI找到你的WSDL描述文档后,他通过可以SOAP调用你建立的Web服务中的一个或多个操作。SOAP是XML文档形式的调用方法的规范,它可以支持不同的底层接口,像HTTP(S)或者SMTP。 + +- SOAP=RPC+HTTP+XML:采用HTTP作为底层通讯协议;RPC作为一致性的调用途径,XML作为数据传送的格式,允许服务提供者和服务客户经过防火墙在INTERNET进行通讯交互。 + +- 简单对象传输协议,是轻量级的、简单的、基于XML的用于交换数据的协议。 +- SOAP本质上是一个 XML文档,包含以下元素: + - 1)Envelope元素:必需元素,根元素,标识此XML文档为一条SOAP消息 +- 可以包含命名空间和声明额外的属性 + - 2)Header元素:可选元素,有关SOAP消息的应用程序专用消息 + - 3)Body元素:必需元素,包含所有的请求和响应信息 + - 4)Fault元素:可选元素,提供有关在处理此消息所发生错误的信息 + +- SOAP处理模型: + - 1)用XML打包请求 + - 2)将请求发送给服务器 + - 3)服务器接收到请求,解码XML,处理请求,以XML格式返回响应 + +- SOAP并不假定传输数据的下层协议,因此必须设计为能在各种协议上运行。即使绝大多数SOAP是运行在HTTP上,使用URI标识服务,SOAP也仅仅使用POST方法发送请求,用一个唯一的URI标识服务的入口。 + +- 使用 HTTP 协议的 SOAP,由于其设计原则上并不像 REST 那样强调与 Web 的工作方式相一致,所以,基于 SOAP 应用很难充分发挥 HTTP 本身的缓存能力。 +- HTTP是其通信协议/传输协议,SOAP是其应用协议 + +# WSDL +- WSDL:(Web Services Description Language) WSDL 文件是一个 XML 文档,用于说明一组 SOAP 消息以及如何交换这些消息。大多数情况下由软件自动生成和使用。 +- 网络服务描述语言,是基于XML的,用于描述网络服务、服务定位和服务提供的操作的协议。 + +# UDDI +- UDDI (Universal Description, Discovery, and Integration) 是一个主要针对Web服务供应商和使用者的新项目。在用户能够调用Web服务之前,必须确定这个服务内包含哪些商务方法,找到被调用的接口定义,还要在服务端来编制软件,UDDI是一种根据描述文档来引导系统查找相应服务的机制。UDDI利用SOAP消息机制(标准的XML/HTTP)来发布,编辑,浏览以及查找注册信息。它采用XML格式来封装各种不同类型的数据,并且发送到注册中心或者由注册中心来返回需要的数据。 + +- 统一描述、发现、集成协议,提供基于网络服务的注册和发现机制 + +# REST +- SOAP协议属于复杂的、重量级的协议,当前随着Web2.0的兴起,表述性状态转移(Representational State Transfer,REST)逐步成为一个流行的架构风格。REST是一种轻量级的Web Service架构风格,其实现和操作比SOAP和XML-RPC更为简洁,可以完全通过HTTP协议实现,还可以利用缓存Cache来提高响应速度,性能、效率和易用性上都优于SOAP协议。REST架构对资源的操作包括获取、创建、修改和删除资源的操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法,这种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。REST架构尤其适用于完全无状态的CRUD(Create、Read、Update、Delete,创建、读取、更新、删除)操作。 +- REST简单而直观,把HTTP协议利用到了极限,在这种思想指导下,它甚至用HTTP请求的头信息来指明资源的表示形式(如果一个资源有多种形式的话,例如人类友善的页面还是机器可读的数据?),用HTTP的错误机制来返回访问资源的错误。由此带来的直接好处是构建的成本减少了,例如用URI定位每一个资源可以利用通用成熟的技术,而不用再在服务器端开发一套资源访问机制。又如只需简单配置服务器就能规定资源的访问权限,例如通过禁止非GET访问把资源设成只读。 + +- 1.面向资源的接口设计 +- 所有的接口设计都是针对资源来设计的,也就很类似于我们的面向对象和面向过程的设计区别,只不过现在将网络上的操作实体都作为资源来看待,同时URI的设计也是体现了对于资源的定位设计。后面会提到有一些网站的API设计说是REST设计,其实是RPC-REST的混合体,并非是REST的思想。 + +- 2.抽象操作为基础的CRUD +- 这点很简单,Http中的get,put,post,delete分别对应了read,update,create,delete四种操作,如果仅仅是作为对于资源的操作,抽象成为这四种已经足够了,但是对于现在的一些复杂的业务服务接口设计,可能这样的抽象未必能够满足。其实这也在后面的几个网站的API设计中暴露了这样的问题,如果要完全按照REST的思想来设计,那么适用的环境将会有限制,而非放之四海皆准的。 +- 3.Http是应用协议而非传输协议 +- 这点在后面各大网站的API分析中有很明显的体现,其实有些网站已经走到了SOAP的老路上,说是REST的理念设计,其实是作了一套私有的SOAP协议,因此称之为REST风格的自定义SOAP协议。 + +- 4.无状态,自包含 +- 这点其实不仅仅是对于REST来说的,作为接口设计都需要能够做到这点,也是作为可扩展和高效性的最基本的保证,就算是使用SOAP的WebService也是一样。 + +# Git +# git init +- 在本地新建一个repo,进入一个项目目录,执行git init,会初始化一个repo,并在当前文件夹下创建一个.git文件夹. +# git clone +- 获取一个url对应的远程Git repo, 创建一个local copy. +- 一般的格式是git clone [url]. +- clone下来的repo会以url最后一个斜线后面的名称命名,创建一个文件夹,如果想要指定特定的名称,可以git clone [url] newname指定. +# git status +- 查询repo的状态. +- git status -s: -s表示short, -s的输出标记会有两列,第一列是对staging区域而言,第二列是对working目录而言. +- +# git log +- show commit history of a branch. +- git log --oneline --number: 每条log只显示一行,显示number条. +- git log --oneline --graph:可以图形化地表示出分支合并历史. +- git log branchname可以显示特定分支的log. +- git log --oneline branch1 ^branch2,可以查看在分支1,却不在分支2中的提交.^表示排除这个分支(Window下可能要给^branch2加上引号). +- git log --decorate会显示出tag信息. +- git log --author=[author name] 可以指定作者的提交历史. +- git log --since --before --until --after 根据提交时间筛选log. +- --no-merges可以将merge的commits排除在外. +- git log --grep 根据commit信息过滤log: git log --grep=keywords +- 默认情况下, git log --grep --author是OR的关系,即满足一条即被返回,如果你想让它们是AND的关系,可以加上--all-match的option. +- git log -S: filter by introduced diff. +- 比如: git log -SmethodName (注意S和后面的词之间没有等号分隔). +- git log -p: show patch introduced at each commit. +- 每一个提交都是一个快照(snapshot),Git会把每次提交的diff计算出来,作为一个patch显示给你看. +- 另一种方法是git show [SHA]. +- git log --stat: show diffstat of changes introduced at each commit. +- 同样是用来看改动的相对信息的,--stat比-p的输出更简单一些. +- +# git add +- 在提交之前,Git有一个暂存区(staging area),可以放入新添加的文件或者加入新的改动. commit时提交的改动是上一次加入到staging area中的改动,而不是我们disk上的改动. +- git add . +- 会递归地添加当前工作目录中的所有文件. +- +# git diff +- 不加参数的git diff: +- show diff of unstaged changes. +- 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容. +- +- 若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用: +- git diff --cached 命令. +- show diff of staged changes. +- (Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的). +- +- git diff HEAD +- show diff of all staged or unstated changes. +- 也即比较woking directory和上次提交之间所有的改动. +- +- 如果想看自从某个版本之后都改动了什么,可以用: +- git diff [version tag] +- 跟log命令一样,diff也可以加上--stat参数来简化输出. +- +- git diff [branchA] [branchB]可以用来比较两个分支. +- 它实际上会返回一个由A到B的patch,不是我们想要的结果. +- 一般我们想要的结果是两个分支分开以后各自的改动都是什么,是由命令: +- git diff [branchA]…[branchB]给出的. +- 实际上它是:git diff $(git merge-base [branchA] [branchB]) [branchB]的结果. +- +- +# git commit +- 提交已经被add进来的改动. +- git commit -m “the commit message" +- git commit -a 会先把所有已经track的文件的改动add进来,然后提交(有点像svn的一次提交,不用先暂存). 对于没有track的文件,还是需要git add一下. +- git commit --amend 增补提交. 会使用与当前提交节点相同的父节点进行一次新的提交,旧的提交将会被取消. +- +# git reset +- undo changes and commits. +- 这里的HEAD关键字指的是当前分支最末梢最新的一个提交.也就是版本库中该分支上的最新版本. +- git reset HEAD: unstage files from index and reset pointer to HEAD +- 这个命令用来把不小心add进去的文件从staged状态取出来,可以单独针对某一个文件操作: git reset HEAD - - filename, 这个- - 也可以不加. +- git reset --soft +- move HEAD to specific commit reference, index and staging are untouched. +- git reset --hard +- unstage files AND undo any changes in the working directory since last commit. +- 使用git reset —hard HEAD进行reset,即上次提交之后,所有staged的改动和工作目录的改动都会消失,还原到上次提交的状态. +- 这里的HEAD可以被写成任何一次提交的SHA-1. +- 不带soft和hard参数的git reset,实际上带的是默认参数mixed. +- +- 总结: +- git reset --mixed id,是将git的HEAD变了(也就是提交记录变了),但文件并没有改变,(也就是working tree并没有改变). 取消了commit和add的内容. +- git reset --soft id. 实际上,是git reset –mixed id 后,又做了一次git add.即取消了commit的内容. +- git reset --hard id.是将git的HEAD变了,文件也变了. +- 按改动范围排序如下: +- soft (commit) < mixed (commit + add) < hard (commit + add + local working) +- +# git revert +- 反转撤销提交.只要把出错的提交(commit)的名字(reference)作为参数传给命令就可以了. +- git revert HEAD: 撤销最近的一个提交. +- git revert会创建一个反向的新提交,可以通过参数-n来告诉Git先不要提交. +- +# git rm +- git rm file: 从staging区移除文件,同时也移除出工作目录. +- git rm --cached: 从staging区移除文件,但留在工作目录中. +- git rm --cached从功能上等同于git reset HEAD,清除了缓存区,但不动工作目录树. +- +# git clean +- git clean是从工作目录中移除没有track的文件. +- 通常的参数是git clean -df: +- -d表示同时移除目录,-f表示force,因为在git的配置文件中, clean.requireForce=true,如果不加-f,clean将会拒绝执行. +- +# git mv +- git rm - - cached orig; mv orig new; git add new +- +# git stash +- 把当前的改动压入一个栈. +- git stash将会把当前目录和index中的所有改动(但不包括未track的文件)压入一个栈,然后留给你一个clean的工作状态,即处于上一次最新提交处. +- git stash list会显示这个栈的list. +- git stash apply:取出stash中的上一个项目(stash@{0}),并且应用于当前的工作目录. +- 也可以指定别的项目,比如git stash apply stash@{1}. +- 如果你在应用stash中项目的同时想要删除它,可以用git stash pop +- +- 删除stash中的项目: +- git stash drop: 删除上一个,也可指定参数删除指定的一个项目. +- git stash clear: 删除所有项目. +- +# git branch +- git branch可以用来列出分支,创建分支和删除分支. +- git branch -v可以看见每一个分支的最后一次提交. +- git branch: 列出本地所有分支,当前分支会被星号标示出. +- git branch (branchname): 创建一个新的分支(当你用这种方式创建分支的时候,分支是基于你的上一次提交建立的). +- git branch -d (branchname): 删除一个分支. +- 删除remote的分支: +- git push (remote-name) :(branch-name): delete a remote branch. +- 这个是因为完整的命令形式是: +- git push remote-name local-branch:remote-branch +- 而这里local-branch的部分为空,就意味着删除了remote-branch +- +# git checkout +-   git checkout (branchname) +- 切换到一个分支. +- git checkout -b (branchname): 创建并切换到新的分支. +- 这个命令是将git branch newbranch和git checkout newbranch合在一起的结果. +- checkout还有另一个作用:替换本地改动: +- git checkout -- +- 此命令会使用HEAD中的最新内容替换掉你的工作目录中的文件.已添加到暂存区的改动以及新文件都不会受到影响. +- 注意:git checkout filename会删除该文件中所有没有暂存和提交的改动,这个操作是不可逆的. +- +# git merge +- 把一个分支merge进当前的分支. +- git merge [alias]/[branch] +- 把远程分支merge到当前分支. +- +- 如果出现冲突,需要手动修改,可以用git mergetool. +- 解决冲突的时候可以用到git diff,解决完之后用git add添加,即表示冲突已经被resolved. +- +# git tag +- tag a point in history as import. +- 会在一个提交上建立永久性的书签,通常是发布一个release版本或者ship了什么东西之后加tag. +- 比如: git tag v1.0 +- git tag -a v1.0, -a参数会允许你添加一些信息,即make an annotated tag. +- 当你运行git tag -a命令的时候,Git会打开一个编辑器让你输入tag信息. +- +- 我们可以利用commit SHA来给一个过去的提交打tag: +- git tag -a v0.9 XXXX +- +- push的时候是不包含tag的,如果想包含,可以在push时加上--tags参数. +- fetch的时候,branch HEAD可以reach的tags是自动被fetch下来的, tags that aren’t reachable from branch heads will be skipped.如果想确保所有的tags都被包含进来,需要加上--tags选项. +- +# git remote +- list, add and delete remote repository aliases. +- 因为不需要每次都用完整的url,所以Git为每一个remote repo的url都建立一个别名,然后用git remote来管理这个list. +- git remote: 列出remote aliases. +- 如果你clone一个project,Git会自动将原来的url添加进来,别名就叫做:origin. +- git remote -v:可以看见每一个别名对应的实际url. +- git remote add [alias] [url]: 添加一个新的remote repo. +- git remote rm [alias]: 删除一个存在的remote alias. +- git remote rename [old-alias] [new-alias]: 重命名. +- git remote set-url [alias] [url]:更新url. 可以加上—push和fetch参数,为同一个别名set不同的存取地址. +- +# git fetch +- download new branches and data from a remote repository. +- 可以git fetch [alias]取某一个远程repo,也可以git fetch --all取到全部repo +- fetch将会取到所有你本地没有的数据,所有取下来的分支可以被叫做remote branches,它们和本地分支一样(可以看diff,log等,也可以merge到其他分支),但是Git不允许你checkout到它们. +- +# git pull +- fetch from a remote repo and try to merge into the current branch. +- pull == fetch + merge FETCH_HEAD +- git pull会首先执行git fetch,然后执行git merge,把取来的分支的head merge到当前分支.这个merge操作会产生一个新的commit. +- 如果使用--rebase参数,它会执行git rebase来取代原来的git merge. +- +- +# git rebase +- --rebase不会产生合并的提交,它会将本地的所有提交临时保存为补丁(patch),放在”.git/rebase”目录中,然后将当前分支更新到最新的分支,最后把保存的补丁应用到分支上。本地的所有提交记录会被丢弃。 +- rebase的过程中,也许会出现冲突,Git会停止rebase并让你解决冲突,在解决完冲突之后,用git add去更新这些内容,然后无需执行commit,只需要: +- git rebase --continue就会继续打余下的补丁. +- git rebase --abort将会终止rebase,当前分支将会回到rebase之前的状态. +- +# git push +- push your new branches and data to a remote repository. +- git push [alias] [branch] +- 将会把当前分支merge到alias上的[branch]分支.如果分支已经存在,将会更新,如果不存在,将会添加这个分支. +- 如果有多个人向同一个remote repo push代码, Git会首先在你试图push的分支上运行git log,检查它的历史中是否能看到server上的branch现在的tip,如果本地历史中不能看到server的tip,说明本地的代码不是最新的,Git会拒绝你的push,让你先fetch,merge,之后再push,这样就保证了所有人的改动都会被考虑进来. +- +# git reflog +- git reflog是对reflog进行管理的命令,reflog是git用来记录引用变化的一种机制,比如记录分支的变化或者是HEAD引用的变化. +- 当git reflog不指定引用的时候,默认列出HEAD的reflog. +- HEAD@{0}代表HEAD当前的值,HEAD@{3}代表HEAD在3次变化之前的值. +- git会将变化记录到HEAD对应的reflog文件中,其路径为.git/logs/HEAD, 分支的reflog文件都放在.git/logs/refs目录下的子目录中. + +- + +# AJAX +# 1、AJAX概述 +# 1.1 什么是AJAX +- AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML)。 +- AJAX还有一个最大的特点就是,当服务器响应时,不用刷新整个浏览器页面,而是可以局部刷新。这一特点给用户的感受是在不知不觉中完成请求和响应过程。 +- 与服务器异步交互; +- 浏览器页面局部刷新; +# 1.2. 同步交互与异步交互 +- 同步交互:客户端发出一个请求后,需要等待服务器响应结束后,才能发出第二个请求; +- 异步交互:客户端发出一个请求后,无需等待服务器响应结束,就可以发出第二个请求。 + +# 1.3. AJAX常见应用情景 + + +- 当我们在百度中输入一个“传”字后,会马上出现一个下拉列表!列表中显示的是包含“传”字的10个关键字。 +- 其实这里就使用了AJAX技术!当文件框发生了输入变化时,浏览器会使用AJAX技术向服务器发送一个请求,查询包含“传”字的前10个关键字,然后服务器会把查询到的结果响应给浏览器,最后浏览器把这10个关键字显示在下拉列表中。 +- 整个过程中页面没有刷新,只是刷新页面中的局部位置而已! +- 当请求发出后,浏览器还可以进行其他操作,无需等待服务器的响应! + + + +- 当输入用户名后,把光标移动到其他表单项上时,浏览器会使用AJAX技术向服务器发出请求,服务器会查询名为zhangSan的用户是否存在,最终服务器返回true表示名为zhangSan的用户已经存在了,浏览器在得到结果后显示“用户名已被注册!”。 +- 整个过程中页面没有刷新,只是局部刷新了; +- 在请求发出后,浏览器不用等待服务器响应结果就可以进行其他操作; +# 1.4 AJAX的优缺点 +- 优点: +- AJAX使用Javascript技术向服务器发送异步请求; +- AJAX无须刷新整个页面; +- 因为服务器响应内容不再是整个页面,而是页面中的局部,所以AJAX性能高; +- 缺点: +- AJAX并不适合所有场景,很多时候还是要使用同步交互; +- AJAX虽然提高了用户体验,但无形中向服务器发送的请求次数增多了,导致服务器压力增大; +- 因为AJAX是在浏览器中使用Javascript技术完成的,所以还需要处理浏览器兼容性问题; + +# 2、AJAX技术 +# 2.1 AJAX第一例(发送GET请求) +## 2.1.1 准备工作 +- 因为AJAX也需要请求服务器,异步请求也是请求服务器,所以我们需要先写好服务器端代码,即编写一个Servlet! +- 这里,Servlet很简单,只需要输出“Hello AJAX!”。 +public class AServlet extends HttpServlet { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println("Hello AJAX!"); + response.getWriter().print("Hello AJAX!"); + } +} + +## 2.1.2 AJAX核心(XMLHttpRequest) +- 其实AJAX就是在Javascript中多添加了一个对象:XMLHttpRequest对象。所有的异步交互都是使用XMLHttpRequest对象完成的。也就是说,我们只需要学习一个Javascript的新对象即可。 +- 注意,各个浏览器对XMLHttpRequest的支持也是不同的!大多数浏览器都支持DOM2规范,都可以使用:var xmlHttp = new XMLHttpRequest()来创建对象 +- 为了处理浏览器兼容问题,给出下面方法来创建XMLHttpRequest对象: + function createXMLHttpRequest() { + var xmlHttp; + // 适用于大多数浏览器,以及IE7和IE更高版本 + try{ + xmlHttp = new XMLHttpRequest(); + } catch (e) { + // 适用于IE6 + try { + xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + // 适用于IE5.5,以及IE更早版本 + try{ + xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e){} + } + } + return xmlHttp; + } + +## 2.1.3 打开与服务器的连接(open方法) +- 当得到XMLHttpRequest对象后,就可以调用该对象的open()方法打开与服务器的连接了。open()方法的参数如下: +- open(method, url, async): +- method:请求方式,通常为GET或POST; +- url:请求的服务器地址,例如:/ajaxdemo1/AServlet,若为GET请求,还可以在URL后追加参数; +- async:这个参数可以不给,默认值为true,表示异步请求; + + var xmlHttp = createXMLHttpRequest(); + xmlHttp.open("GET", "/ajaxdemo1/AServlet", true); + +## 2.1.4 发送请求 +- 当使用open打开连接后,就可以调用XMLHttpRequest对象的send()方法发送请求了。send()方法的参数为POST请求参数,即对应HTTP协议的请求体内容,若是GET请求,需要在URL后连接参数。 +- 注意:若没有参数,需要给出null为参数!若不给出null为参数,可能会导致FireFox浏览器不能正常发送请求! + xmlHttp.send(null); + +## 2.1.5 接收服务器响应 +- 当请求发送出去后,服务器端Servlet就开始执行了,但服务器端的响应还没有接收到。接下来我们来接收服务器的响应。 +- XMLHttpRequest对象有一个onreadystatechange事件,它会在XMLHttpRequest对象的状态发生变化时被调用。下面介绍一下XMLHttpRequest对象的5种状态: +- 0:初始化未完成状态,只是创建了XMLHttpRequest对象,还未调用open()方法; +- 1:请求已开始,open()方法已调用,但还没调用send()方法; +- 2:请求发送完成状态,send()方法已调用; +- 3:开始读取服务器响应; +- 4:读取服务器响应结束。 + +- onreadystatechange事件会在状态为1、2、3、4时引发。 +-   下面代码会被执行四次!对应XMLHttpRequest的四种状态! + xmlHttp.onreadystatechange = function() { + alert('hello'); + }; + +- 但通常我们只关心最后一种状态,即读取服务器响应结束时,客户端才会做出改变。我们可以通过XMLHttpRequest对象的readyState属性来得到XMLHttpRequest对象的状态。 + xmlHttp.onreadystatechange = function() { + if(xmlHttp.readyState == 4) { + alert('hello'); + } + }; + +- 其实我们还要关心服务器响应的状态码是否为200,其服务器响应为404,或500,那么就表示请求失败了。我们可以通过XMLHttpRequest对象的status属性得到服务器的状态码。 +- 最后,我们还需要获取到服务器响应的内容,可以通过XMLHttpRequest对象的responseText得到服务器响应内容。 +- responsXML是xml格式的文本,是document对象 + xmlHttp.onreadystatechange = function() { + if(xmlHttp.readyState == 4 && xmlHttp.status == 200) { + alert(xmlHttp.responseText); + } + }; + +## 2.1.6 AJAX第一例小结 +- 创建XMLHttpRequest对象; +- 调用open()方法打开与服务器的连接; +- 调用send()方法发送请求; +- 为XMLHttpRequest对象指定onreadystatechange事件函数,这个函数会在XMLHttpRequest的1、2、3、4,四种状态时被调用; +- XMLHttpRequest对象的5种状态: +- 0:初始化未完成状态,只是创建了XMLHttpRequest对象,还未调用open()方法; +- 1:请求已开始,open()方法已调用,但还没调用send()方法; +- 2:请求发送完成状态,send()方法已调用; +- 3:开始读取服务器响应; +- 4:读取服务器响应结束。 +- 通常我们只关心4状态。 +- XMLHttpRequest对象的status属性表示服务器状态码,它只有在readyState为4时才能获取到。 +- XMLHttpRequest对象的responseText属性表示服务器响应内容,它只有在readyState为4时才能获取到! +- +- +-

+- + + +``` +public class AServlet extends HttpServlet { +``` + + +``` + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +``` + +- response.setContentType("text/html;charset=utf-8"); +- response.getWriter().print("hehe"); +- } +- } + + +# 2.2 AJAX第二例(发送POST请求) +## 2.2.1 发送POST请求注意事项 +- POST请求必须设置ContentType请求头的值为application/x-www.form-encoded。表单的enctype默认值就是为application/x-www.form-encoded!因为默认值就是这个,所以大家可能会忽略这个值!当设置了
的enctype=” application/x-www.form-encoded”时,等同与设置了Cotnent-Type请求头。 +- 但在使用AJAX发送请求时,就没有默认值了,这需要我们自己来设置请求头: +xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + +- 当没有设置Content-Type请求头为application/x-www-form-urlencoded时,Web容器会忽略请求体的内容。所以,在使用AJAX发送POST请求时,需要设置这一请求头,然后使用send()方法来设置请求体内容。 +xmlHttp.send("b=B"); + +-   这时Servlet就可以获取到这个参数!!! + +- AServlet + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + System.out.println(request.getParameter("b")); + System.out.println("Hello AJAX!"); + response.getWriter().print("Hello AJAX!"); + } + +- ajax2.jsp + +

AJAX2

+ +
+ +# 2.3 AJAX第三例(用户名是否已被注册) +## 2.3.1 功能介绍 +- 在注册表单中,当用户填写了用户名后,把光标移开后,会自动向服务器发送异步请求。服务器返回true或false,返回true表示这个用户名已经被注册过,返回false表示没有注册过。 +- 客户端得到服务器返回的结果后,确定是否在用户名文本框后显示“用户名已被注册”的错误信息! + +## 2.3.2 案例分析 +- regist.jsp页面中给出注册表单; +- 在username表单字段中添加onblur事件,调用send()方法; +- send()方法获取username表单字段的内容,向服务器发送异步请求,参数为username; +- RegistServlet:获取username参数,判断是否为“itcast”,如果是响应true,否则响应false; + +## 2.3.3 代码 +- regist.jsp + + + 用户名:
+ +
+ +- RegistServlet.java + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String username = request.getParameter("username"); + System.out.println(username); + if(username.equals("admin")){ + response.getWriter().print(false); + }else{ + response.getWriter().print(true); + } + } + +# 前端 +- HTML的DOM对象说几个,Document的对象和方法 +document.body 返回元素 1 +document.cookie 返回或设置与当前文档相关的cookie 1 +document.domain 返回当前文档的服务器域名 1 +document.referrer 返回连接至当前文档的文档连接 1 +document.title 返回当前文档的元素 1 +document.URL 返回当前文档的完整URL 1 + + + From 2dd15a18500da89c275c292b412df48e49b3aed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:31:09 +0800 Subject: [PATCH 83/97] =?UTF-8?q?Create=20=E5=8D=81=E5=9B=9B=E3=80=81Dubbo?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=8E=E5=AE=9E=E7=8E=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\344\270\216\345\256\236\347\216\260.md" | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 "docs/\345\215\201\345\233\233\343\200\201Dubbo\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" diff --git "a/docs/\345\215\201\345\233\233\343\200\201Dubbo\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" "b/docs/\345\215\201\345\233\233\343\200\201Dubbo\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 00000000..63fea441 --- /dev/null +++ "b/docs/\345\215\201\345\233\233\343\200\201Dubbo\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,432 @@ +# Dubbo +# 简介 + + + + + +# 架构 + +- 节点角色说明 +节点 角色说明 +Provider 暴露服务的服务提供方 +Consumer 调用远程服务的服务消费方 +Registry 服务注册与发现的注册中心 +Monitor 统计服务的调用次调和调用时间的监控中心 +Container 服务运行容器 + +- 调用关系说明 +1.- 服务容器负责启动,加载,运行服务提供者。 +2.- 服务提供者在启动时,向注册中心注册自己提供的服务。 +3.- 服务消费者在启动时,向注册中心订阅自己所需的服务。 +4.- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +5.- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +6.- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 +# 连通性 +1.- 注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动 +- 时与注册中心交互,注册中心不转发请求,压力较小 +2.- 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送 +- 到监控中心服务器,并以报表展示 +3.- 服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含 +- 网络开销 +4.- 服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同 +- 时汇报调用时间到监控中心,此时间包含网络开销 +5.- 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外 +6.- 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事 +- 件通知消费者 +7.- 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了 +- 提供者列表 +8.- 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者 + +# 健状性 +1.- 监控中心宕掉不影响使用,只是丢失部分采样数据 +2.- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务 +3.- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台 +4.- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯 +5.- 服务提供者无状态,任意一台宕掉后,不影响使用 +6.- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复 +# 伸缩性 +1.- 注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心 +2.- 服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给 +- 消费者 + +# 集群容错 + +- 各节点关系: +1.- 这里的 Invoker 是 Provider 的一个可调用 Service 的抽象, Invoker 封装了 +- Provider 地址及 Service 接口信息 +2.- Directory 代表多个 Invoker ,可以把它看成 List<Invoker>,但与 List 不同的 +- 是,它的值可能是动态变化的,比如注册中心推送变更 +3.- Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个 +4.- Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等 +5.- LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选 + +- +# 负载均衡 +# Random LoadBalance +- 随机,按权重设置随机概率。 +- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较 +- 均匀,有利于动态调整提供者权重。 +# RoundRobin LoadBalance +- 轮循,按公约后的权重设置轮循比率。 +- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台 +- 时就卡在那,久而久之,所有请求都卡在调到第二台上。 +- LeastActive LoadBalance +- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 +- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 + +- 每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使慢的机器A收到少的请求。 +# ConsistentHash LoadBalance +- 一致性 Hash,相同参数的请求总是发到同一提供者。 +- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者, +- 不会引起剧烈变动。 +- 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing +- 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter +- key="hash.arguments" value="0,1" /> +- 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" +- value="320" /> +# 线程模型 +- 如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识, +- 则直接在 IO 线程上处理更快,因为减少了线程池调度。 +- 但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。 +- 如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。 + +- 需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景: +- <dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" /> +# Dispatcher +- all 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 +- direct 所有消息都不派发到线程池,全部在 IO 线程上直接执行。 +- message 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO +- 线程上执行。 +- execution 只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消 +- 息,直接在 IO 线程上执行。 +- connection 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到 +- 线程池。 + +# ThreadPool +- fixed 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省) +- cached 缓存线程池,空闲一分钟自动删除,需要时重建。 +- limited 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。 + +# 协议 + +# dubbo +- Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。 +- 反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 +- + +## 特性 +- 缺省协议,使用基于 mina 1.1.7 和 hessian 3.2.1 的 tbremoting 交互。 +- 连接个数:单连接 +- 连接方式:长连接 +- 传输协议:TCP +- 传输方式:NIO 异步传输 +- 序列化:Hessian 二进制序列化 +- 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一 +- 消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。 +- 适用场景:常规远程服务方法调用 +## 约束 +- 参数及返回值需实现 Serializable 接口 +- 参数及返回值不能自定义实现 List , Map , Number , Date , Calendar 等接口,只能用 +- JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。 +- Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况 + +- 接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。 +- 输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部 +- 署。 +- 输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管 +- 输入还是输出,属性名变化的属性值是获取不到的。 +- 总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。 + +## 常见问题 +### 为什么要消费者比提供者个数多? +- 因 dubbo 协议采用单一长连接,假设网络为千兆网卡 ,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样,供参考),理论上 1 个服务提供者需要 20 个服务消费者才能压满网卡。 + +### 为什么不能传大包? +- 因 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡 ,每条连接最大 7MByte(不同的环境可能不一样,供参考),单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。 +### 为什么采用异步单一长连接? +- 为什么采用异步单一长连接? +- 因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网 +- 站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题 +- 网络服务在处理数以万计的客户端连接时,往往出现效率底下甚至完全瘫痪,这被成为C10K问题。(C10K = connection 10 kilo 问题)。k 表示 kilo,即 1000 比如:kilometer(千米), kilogram(千克)。 + +# 附加功能 +# 服务分组 +- 当一个接口有多种实现时,可以用 group 区分。 +## 服务 +- <dubbo:service group="feedback" interface="com.xxx.IndexService" /> +- <dubbo:service group="member" interface="com.xxx.IndexService" /> +## 引用 +- <dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexSe +- rvice" /> +- <dubbo: + +# 多版本 +- 当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引 +- 用。 +- 可以按照以下的步骤进行版本迁移: +- 1. 在低压力时间段,先升级一半提供者为新版本 +- 2. 再将所有消费者升级为新版本 +- 3. 然后将剩下的一半提供者升级为新版本 + +- 老版本服务提供者配置: +- <dubbo:service interface="com.foo.BarService" version="1.0.0" /> +- 新版本服务提供者配置: +- <dubbo:service interface="com.foo.BarService" version="2.0.0" /> +- 老版本服务消费者配置: +- <dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" /> +- 新版本服务消费者配置: +- <dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" /> + +# 参数验证 +- 参数验证功能 是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并 +- 通过声明 filter 来实现验证 。 +# 异步调用 +- 基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服 +- 务,相对多线程开销较小。 + +# 事件通知 +- 在调用之前、调用之后、出现异常时,会触发 oninvoke 、 onreturn 、 onthrow 三个事件, +- 可以配置当事件发生时,通知哪个类的哪个方法 。 +- 服务消费者 Callback 配置 +- <bean id ="demoCallback" class = "com.alibaba.dubbo.callback.implicit.NofifyImpl" /> +- <dubbo:reference id="demoService" interface="com.alibaba.dubbo.callback.implicit.IDemo +- Service" version="1.0.0" group="cn" > +- <dubbo:method name="get" async="true" onreturn = "demoCallback.onreturn" onthrow= +- "demoCallback.onthrow" /> +- </dubbo:reference> +- callback 与 async 功能正交分解, async=true 表示结果是否马上返回, onreturn 表示是 +- 否需要回调。 +- 两者叠加存在以下几种组合情况 : +- 异步回调模式: async=true onreturn="xxx" +- 同步回调模式: async=false onreturn="xxx" +- 异步无回调 : async=true +- 同步无回调 : async=false’ +# 本地伪装 +- 本地伪装 通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出 +- 异常,而是通过 Mock 数据返回授权失败。 +- 在 spring 配置文件中按以下方式配置: +- <dubbo:service interface="com.foo.BarService" mock="true" /> +- 或 +- <dubbo:service interface="com.foo.BarService" mock="com.foo.BarServiceMock" /> +- 如果服务的消费方经常需要 try-catch 捕获异常,如: +- Offer offer = null; +- try { +- offer = offerService.findOffer(offerId); +- } catch (RpcException e) { +- logger.error(e); +- } +- 请考虑改为 Mock 实现,并在 Mock 实现中 return null。如果只是想简单的忽略异常,在 +- 2.0.11 以上版本可用: +- <dubbo:service interface="com.foo.BarService" mock="return null" /> +# 令牌验证 +- 通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过 +- 注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者 + +- 可以全局设置开启令牌验证: +- <!--随机token令牌,使用UUID生成--> +- <dubbo:provider interface="com.foo.BarService" token="true" /> +- 或 +- <!--固定token令牌,相当于密码--> +- <dubbo:provider interface="com.foo.BarService" token="123456" /> +- 也可在服务级别设置: +- <!--随机token令牌,使用UUID生成--> +- <dubbo:service interface="com.foo.BarService" token="true" /> +- 或 +- <!--固定token令牌,相当于密码--> +- <dubbo:service interface="com.foo.BarService" token="123456" /> + +- 还可在协议级别设置: +- <!--随机token令牌,使用UUID生成--> +- <dubbo:protocol name="dubbo" token="true" /> +- 或 +- <!--固定token令牌,相当于密码--> +- <dubbo:protocol name="dubbo" token="123456" /> +# 写入路由规则 +- 向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成 + +- condition:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 +- 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 +- com.foo.BarService 表示只对指定服务生效,必填。 +- category=routers 表示该数据为动态配置类型,必填。 +- dynamic=false 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心, +- 必填。 +- enabled=true 覆盖规则是否生效,可不填,缺省生效。 +- force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路 +- 由规则将自动失效,可不填,缺省为 flase 。 +- runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先 +- 执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 +- true ,需要注意设置会影响调用的性能,可不填,缺省为 flase 。 +- priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 +- 0 。 +- rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11") 表示路由规则的内容, +- 必填。 + +# 优雅停机 +- ‘Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果用户使用 kill -9 PID +- 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 +## 原理 +### 服务提供方 +- 停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。 +- 然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超 +- 时,则强制关闭。 +### 服务消费方 +- 停止时,不再发起新的调用请求,所有新的调用在客户端即报错。 +- 然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。 + +## 设置方式 +- 设置优雅停机超时时间,缺省超时时间是 10 秒,如果超时则强制关闭。 +- <dubbo:application ...> +- <dubbo:parameter key="shutdown.timeout" value="60000" /> <!-- 单位毫秒 --> +- </dubbo:application> +# +注册中心 + +- 流程说明: +- 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。 + +- 支持以下功能: +- 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息 +- 当注册中心重启时,能自动恢复注册数据,以及订阅请求 +- 当会话过期时,能自动恢复注册数据,以及订阅请求 +- 当设置 <dubbo:registry check="false" /> 时,记录失败注册和订阅请求,后台定时重试 +- 可通过 <dubbo:registry username="admin" password="1234" /> 设置 zookeeper 登录信息 +- 可通过 <dubbo:registry group="dubbo" /> 设置 zookeeper 的根节点,不设置将使用无 +- 根树支持 * 号通配符 <dubbo:reference group="*" version="*" /> ,可订阅服务的所有分组和所有版本的提供者使用 + +# 最佳实践 +# 分包 +- 建议将服务接口,服务模型,服务异常等均放在 API 包中,因为服务模型及异常也是 API 的一部分,同时,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。 +- 如果需要,也可以考虑在 API 包中放置一份 spring 的引用配置,这样使用方,只需在 spring加载过程中引用此配置即可,配置建议放在模块的包目录下,以免冲突, +- 如: com/alibaba/china/xxx/dubbo-reference.xml 。 +# 粒度 +- 服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将 +- 面临分布式事务问题,Dubbo 暂未提供分布式事务支持。 +- 服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。 +- 不建议使用过于抽象的通用接口,如: Map query(Map) ,这样的接口没有明确语义,会给后 +- 期维护带来不便。 + +# 版本 +- 每个接口都应定义版本号,为后续不兼容升级提供可能,如: <dubbo:service +- interface="com.xxx.XxxService" version="1.0" /> 。 +- 建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务 +- 版本。 +- 当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半 +- 提供者升为新版本。 + +# 兼容性 +- 服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容, +- 枚举类型新增字段也不兼容,需通过变更版本号升级。 +# 枚举值 +- 如果是完备集,可以用 Enum ,比如: ENABLE , DISABLE 。 +- 如果是业务种类,以后明显会有类型增加,不建议用 Enum ,可以用 String 代替。 +- 如果是在返回值中用了 Enum ,并新增了 Enum 值,建议先升级服务消费方,这样服务提供方不会返回新值。 +- 如果是在传入参数中用了 Enum ,并新增了 Enum 值,建议先升级服务提供方,这样服务消费方不会传入新值。 + +# 序列化 +- 服务参数及返回值建议使用 POJO 对象,即通过 setter , getter 方法表示属性的对象。 +- 服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实 +- 现类的元信息,并不能起到隐藏实现的意图。 +- 服务参数及返回值都必需是 byValue 的,而不能是 byReference 的,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo 不支持引用远程对象。 + +# 异常 +- 建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,以及语义更友好。 +- 如果担心性能问题,在必要时,可以通过 override 掉异常类的 fillInStackTrace() 方法为空 +- 方法,使其不拷贝栈信息。 +- 查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try...catch ,并且不能 +- 进行有效处理。 +- 服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。 + +# 调用 +- 不要只是因为是 Dubbo 调用,而把调用 try...catch 起来。 try...catch 应该加上合适的回滚边界上。 +- 对于输入参数的校验逻辑在 Provider 端要有。如有性能上的考虑,服务实现者可以考虑在 +- API 包上加上服务 Stub 类来完成检验。 + +# Provider 上尽量多配置 Consumer 端属性 +- 原因如下: +- 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试 +- 次数,等等 +- 在 Provider 配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可 +- 以作为 Consumer 的缺省值 。否则,Consumer 会使用 Consumer 端的全局设置,这 +- 对于 Provider 不可控的,并且往往是不合理的 +- Provider 上尽量多配置 Consumer 端的属性,让 Provider 实现者一开始就思考 Provider 服务特点、服务质量的问题。 + +# Provider 上配置合理的 Provider 端属性 + +- Provider 上可以配置的 Provider 端属性有: +- 1. threads 服务线程池大小 +- 2. executes 一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用到 +- 上限后,新调用会 Wait,这个时候 Consumer可能会超时。在方法上配置 dubbo:method +- 则并发限制针对方法,在接口上配置 dubbo:service ,则并发限制针对服务 + +- + +# 使用建议 +- Dubbo服务划分 +- 1、服务划分目标 +- 抽取系统中独立的业务模块服务化,按业务独立性进行垂直划分,抽象出基础服务层 +- 2、子系统划分把控:合理划分,过细过粗都不行 +- 3、注意事项 + - 1)表:避免出现A服务关联B服务的表的数据操作;服务一旦划分了,那么数据库即便没分开,也要当成db表分开了来进行编码;否则AB服务难以进行垂直拆库 + - 2)避免服务耦合度高,依赖调用;如果出现,考虑服务调优。 + - 3)避免分布式事务,不要拆分过细。 +- Dubbo接口划分 +- 1、接口尽可能大粒度,接口中的方法不要以业务流程来,这个流程尽量在方法逻辑中调用,接口应代表一个完整的功能对外提供; +- 2、接口应以业务为单位,业务相近的进行抽象,避免接口数量爆炸 +- 3、参数先做校验,在传入接口。 +- 4、要做到在设计接口时,已经确定这个接口职责、预测调用频率 +- 依赖 +- web应用大致分为两层:biz和web,实际上biz可能由内部多个工程组成,这里biz只是一个抽象概念。impl依赖api和biz,web依赖impl和biz,没有其他依赖关系,严禁biz依赖api。关系如下: +- + +- 注意事项 +- 服务化接口涉及的入参类型和返回类型都必须实现序列化接口,并且必须放到api包。 + +- - 在提供者的dubbo配置文件中,一般都配置了<dubbo:protocol name="dubbo" port="20880"/>,表明用dubbo协议在20880端口暴露服务,当然如果你不配置,dubbo默认使用20880端口暴露服务,所有消费者都是通过20880端口进行,对于消费者而言,提供者服务器8080端口是透明的,也就是说提供者服务器端口号可以任意改变,服务也不会有任何影响,消费者无需关心。  + +- - zookeeper的2181开放给provider、consumer、dubbo-admin  + +- provider的20880开放给所有consumer,但8080服务器端口可以完全屏蔽  + +- consumer的8080开放给所有provider  + +- dubbo-admin的8080开放给管理员用户,便于通过浏览器监控注册中心服务的情况  + +- 分包 +- biz-service +- biz-api +- biz-web + +- web依赖于api +- service依赖于api +- service和web没有依赖 +- controller是一个工程,service+dao是一个工程 + +- biz-service放converter,dao,service.impl,application,config, properties +- biz-api放domain,enumeration, exception +- biz-web放controller,config,properties +- common放公共代码 +- + +- 序列化 +- 默认的hession2方式不支持LocalDateTime等时间类的序列化,会出现StackOverflowError +- dubbo=com.alibaba.dubbo.common.serialize.support.dubbo.DubboSerialization +- hessian2=com.alibaba.dubbo.common.serialize.support.hessian.Hessian2Serialization +- java=com.alibaba.dubbo.common.serialize.support.java.JavaSerialization +- compactedjava=com.alibaba.dubbo.common.serialize.support.java.CompactedJavaSerialization +- json=com.alibaba.dubbo.common.serialize.support.json.JsonSerialization +- fastjson=com.alibaba.dubbo.common.serialize.support.json.FastJsonSerialization +- nativejava=com.alibaba.dubbo.common.serialize.support.nativejava.NativeJavaSerialization +- kryo=com.alibaba.dubbo.common.serialize.support.kryo.KryoSerialization +- fst=com.alibaba.dubbo.common.serialize.support.fst.FstSerialization +- jackson=com.alibaba.dubbo.common.serialize.support.json.JacksonSerialization + +- + +# 事务 +- 如果仅仅是应用拆分,而没有数据库的拆分,那么仍可视为单机事务。 +- 如果数据库也进行了拆分,每个应用使用的表被放在相应机器的数据库中,那么需要考虑分布式事务问题。 +- 分布式事务的前提是多个数据库之间难以保证一致性,单一数据库本身的事务机制可以保证数据一致性。 + +- + From 763d9efa836ab0a30370de543399338c9f479393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:32:30 +0800 Subject: [PATCH 84/97] =?UTF-8?q?Create=20=E5=8D=81=E4=BA=94=E3=80=81MyBat?= =?UTF-8?q?is=E4=BD=BF=E7=94=A8=E4=B8=8E=E5=AE=9E=E7=8E=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\344\270\216\345\256\236\347\216\260.md" | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 "docs/\345\215\201\344\272\224\343\200\201MyBatis\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" diff --git "a/docs/\345\215\201\344\272\224\343\200\201MyBatis\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" "b/docs/\345\215\201\344\272\224\343\200\201MyBatis\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 00000000..b0c4bb31 --- /dev/null +++ "b/docs/\345\215\201\344\272\224\343\200\201MyBatis\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,300 @@ +# MyBatis +- 让你自己实现一个orm框架会如何实现 +- MyBatis/Hibernate 原理,源码,区别;批量操作;MyBatis缓存(一级、二级); mybatis的#和$号区别 ;mybatis一级缓存及可能存在的问题,两个机器能否共用同一个SqlSession实现一级缓存;mybatis如何映射表结构 +- 在Hibernate中java的对象状态有哪些;hibernate主键生成策略 +- mybatis和hibernate各自的缓存原理和比较,hibernate的一级二级和查询缓存,还有针对缓存的miss率,置换策略,容量设置和性能的平衡 +- mybatis和ibatis的区别(配置文件格式) +# 架构设计 + +- 接口层: +- MyBatis和数据库的交互有两种方式: +- a.使用传统的MyBatis提供的API; +- b. 使用Mapper接口 + +- 数据处理层: +- a. 通过传入参数构建动态SQL语句; +- b. SQL语句的执行以及封装查询结果集成List<E> + +- 动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis 通过传入的参数值,使用 Ognl 来动态地构造SQL语句,使得MyBatis 有很强的灵活性和扩展性。 + +- 参数映射指的是对于java 数据类型和jdbc数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是对resultset查询结果集的jdbcType 数据转换成java 数据类型。 + +- OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。 + + +- 框架支撑层: +- 事务管理;连接池管理;缓存等 + +- MyBatis的主要的核心部件有以下几个: + +- SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 +- Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护 +- StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。 +- ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数, +- ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合; +- TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换 +- MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装, +- SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回 +- BoundSql 表示动态生成的SQL语句以及相应的参数信息 +- Configuration MyBatis所有的配置信息都维持在Configuration对象之中。 + + + +# 初始化过程 +- MyBatis初始化的过程,就是创建 Configuration对象的过程。 + +- MyBatis的初始化可以有两种方式: + +- 基于XML配置文件:基于XML配置文件的方式是将MyBatis的所有配置信息放在XML文件中,MyBatis通过加载并XML配置文件,将配置文信息组装成内部的Configuration对象 +- 基于Java API:这种方式不使用XML配置文件,需要MyBatis使用者在Java代码中,手动创建Configuration对象,然后将配置参数set 进入Configuration对象中。 + +- mybatis初始化 -->创建SqlSession -->执行SQL语句 + +- SqlSessionFactoryBuilder根据传入的数据流生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。 + +- 1. 调用SqlSessionFactoryBuilder对象的build(inputStream)方法; +- 2. SqlSessionFactoryBuilder会根据输入流inputStream等信息创建XMLConfigBuilder对象; +- 3. SqlSessionFactoryBuilder调用XMLConfigBuilder对象的parse()方法; +- 4. XMLConfigBuilder对象返回Configuration对象; +- 5. SqlSessionFactoryBuilder根据Configuration对象创建一个DefaultSessionFactory对象; +- 6. SqlSessionFactoryBuilder返回 DefaultSessionFactory对象给Client,供Client使用。 +# 数据源与连接池 +- MyBatis把数据源DataSource分为三种: +- UNPOOLED 不使用连接池的数据源 +- POOLED 使用连接池的数据源 +- JNDI 使用JNDI实现的数据源 + + +# DataSource创建 + +- MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,通过其getDataSource()方法返回数据源DataSource。 + +- 上述三种不同类型的type,则有对应的以下dataSource工厂: +- POOLED PooledDataSourceFactory +- UNPOOLED UnpooledDataSourceFactory +- JNDI JndiDataSourceFactory + + +# Connection创建 +- 当我们需要创建SqlSession对象并需要执行SQL语句时,这时候MyBatis才会去调用dataSource对象来创建java.sql.Connection对象。也就是说,java.sql.Connection对象的创建一直延迟到执行SQL语句的时候。 +## Unpooled +- 当 <dataSource>的type属性被配置成了”UNPOOLED”,MyBatis首先会实例化一个UnpooledDataSourceFactory工厂实例,然后通过.getDataSource()方法返回一个UnpooledDataSource实例对象引用,我们假定为dataSource。 +- 使用UnpooledDataSource的getConnection(),每调用一次就会产生一个新的Connection实例对象。 + +- UnpooledDataSource会做以下事情: +- 1. 初始化驱动:判断driver驱动是否已经加载到内存中,如果还没有加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。 +- 2. 创建Connection对象:使用DriverManager.getConnection()方法创建连接。 +- 3. 配置Connection对象:设置是否自动提交autoCommit和隔离级别isolationLevel。 +- 4. 返回Connection对象。 + + +- 我们每调用一次getConnection()方法,都会通过DriverManager.getConnection()返回新的java.sql.Connection实例。 +- 对于需要频繁地跟数据库交互的应用程序,可以在创建了Connection对象,并操作完数据库后,可以不释放掉资源,而是将它放到内存中,当下次需要操作数据库时,可以直接从内存中取出Connection对象,不需要再创建了,这样就极大地节省了创建Connection对象的资源消耗。由于内存也是有限和宝贵的,这又对我们对内存中的Connection对象怎么有效地维护提出了很高的要求。我们将在内存中存放Connection对象的容器称之为 连接池(Connection Pool)。 +## Pooled +- PooledDataSource将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护。 MyBatis将连接池中的PooledConnection分为两种状态: 空闲状态(idle)和活动状态(active),这两种状态的PooledConnection对象分别被存储到PoolState容器内的idleConnections和activeConnections两个List集合中: + +- idleConnections:空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从此集合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。 + +- activeConnections:活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,如果没有,则看此集合是否已满,如果未满,PooledDataSource会创建出一个PooledConnection,添加到此集合中,并返回。 + + +- popConnection()方法到底做了什么: + +- 1. 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。 + +- 2. 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步; + +- 3. 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。 + +- 4. 线程等待 + + +- 当我们的程序中使用完Connection对象时,如果不使用数据库连接池,我们一般会调用 connection.close()方法,关闭connection连接,释放资源。 +- 我们希望当Connection使用完后,调用.close()方法,而实际上Connection资源并没有被释放,而实际上被添加到了连接池中。 +- 这里要使用代理模式,为真正的Connection对象创建一个代理对象,代理对象所有的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到连接池中。 +- MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库连接java.sql.Connection实例对象的包裹器。 +- PooledConenction实现了InvocationHandler接口,并且proxyConnection对象也是根据这个它来生成的代理对象。 +- + +# 一级缓存(Session级别的缓存) +- 每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。 +- 在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。 +- 为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。 +- 当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。SqlSession、Executor、Cache之间的关系如下列类图所示: + +- Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,则对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存。 + +- Perpetual Cache(永久的) +- PerpetualCache实现原理其实很简单,其内部就是通过一个简单的HashMap<k,v> 来实现的,没有其他的任何限制。 +# 生命周期 +- a. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。 +- b. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用; +- c. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用; +- d.SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用; + + +# 工作流程 +- 1.对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果; +- 2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中; +- 3. 如果命中,则直接将缓存结果返回; +- 4. 如果没命中: +- 4.1 去数据库中查询数据,得到查询结果; +- 4.2 将key和查询到的结果分别作为key,value对存储到Cache中; +- 4.3. 将查询结果返回; + +- 怎样判断某两次查询是完全相同的查询?也可以这样说:如何确定Cache中的key值? +- MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询: +- 1. 传入的 statementId +- 2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示); +- 3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() ) +- 4. 传递给java.sql.Statement要设置的参数值 + +- CacheKey由以下条件决定: statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值 + +- 问题: +- 如果我一直使用某一个SqlSession对象查询数据,这样会不会导致HashMap太大,而导致 java.lang.OutOfMemoryError错误啊? 读者这么考虑也不无道理,不过MyBatis的确是这样设计的。 +- MyBatis这样设计也有它自己的理由: +- a. 一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多,执行完就会消亡; +- b. 对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题; +- c. 可以手动地释放掉SqlSession对象中的缓存。 + +- 一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念 + +# 二级缓存(Mapper级别的缓存) + +- 当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。 + +- CachingExecutor是Executor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式。 + +# 分类 +- MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下: +- a.为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置); +- b.多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置); + +- 对于每一个Mapper.xml,如果在其中使用了<cache> 节点,则MyBatis会为这个Mapper创建一个Cache缓存对象,如下图所示: + + +- 上述的每一个Cache对象,都会有一个自己所属的namespace命名空间,并且会将Mapper的 namespace作为它们的ID; + + +- 如果你想让多个Mapper公用一个Cache的话,你可以使用<cache-ref namespace="">节点,来指定你的这个Mapper使用到了哪一个Mapper的Cache缓存。 + + +- MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。 +- 虽然在Mapper中配置了<cache>,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在<select> 节点中配置useCache="true",Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache缓存。 +- 要想使某条Select查询支持二级缓存,你需要保证: +- 1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true +- 2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效 +- 3. 该select语句的参数 useCache=true + + +- 总之,要想使某条Select查询支持二级缓存,你需要保证: +- 1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true +- 2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效 +- 3. 该select语句的参数 useCache=true + +- 请注意,如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是: + +- 二级缓存 ———> 一级缓存——> 数据库 + +- 使用MyBatis的二级缓存有三个选择: +- 1.MyBatis自身提供的缓存实现; +- 2. 用户自定义的Cache接口实现; +- 3.跟第三方内存缓存库的集成; +- #和$号区别 +- #是一个占位符,接收输入参数,类型可以是基本数据类型、POJO类型、Map类型 +- 会转为JDBC的?,是预编译的形式PreparedStatement + +- $是一个拼接符,会引起SQL注入,所以不建议使用,而#不会引起。 +- 可以接收基本数据类型、POJO类型、Map类型。 +- 直接拼接在SQL语句中 + +- 大部分情况下都应该使用#{} + +- 以下情况使用${} + +- 比如分表:财务表每年一张表 +- 此时表名可以拼接出来,但无法使用占位符填充 +- select * from ${year}_salary where ... +- 2016_salary + +- 比如排序:按某个字段排序,升降序也需要使用${}来指定,因为SQL是不支持将排序填充进去的,必须一开始就指定(同表名) +- select * .. from .. order by xxx xxx +# 接口代理对象的创建 +- openSession.getMapper(Mapper.class) +- 会调用Configuration的getMapper的方法,它又调用MapperRegistery的getMapper的方法。 +- 它会获取接口的代理对象MapperProxy(实现了JDK的InvocationHandler接口)。 +- 持有DefaultSqlSession对象(它又持有Executor),可以进行增改删查操作。 + +# 执行方法(以查询为例) +- 调用find方法后,会被invoke方法拦截。 +- 如果是Object类的方法,那么直接执行;如果不是,那么会把method对象包装为MapperMethod,然后执行该方法(传入SqlSession)。 +- 执行时,会判断是增改删查的哪一种方法,然后解析入口参数(依赖于paramNameResolver),封装为一个Map。 +- 然后调用sqlSession的selectOne方法 +- 在sqlSession中仍会转到selectList方法,只是返回结果集的第一个对象。 +- selectList中会从Configuration中取出对应的MappedStatement(封装了SQL语句),然后把这个MappedStatement交给Executor,由Executor来执行真正的查询(query方法)。 +- 在query方法中调用getBoundSql(封装了SQL的信息),创建缓存的key,有二级缓存则使用缓存,没有就去查找一级缓存,如果还没有,那么去查询数据库,查询后将查询结果放入一级缓存。 +- 查询数据库调用的是doQuery方法,在这个方法中会使用JDBC的stratement,在创建statement时会创建并传入StatementHandler(可以创建Statement对象)。 +- StatementHandler在创建时会读取MapperStatement中关于statementType的配置属性,根据该属性创建对应的StatementHandler(Statement、Prepared、Callable),注意在创建时又会将InterceptorChain挂接到StatementHandler上,同时会创建ParameterHandler/ResultSetHandler,还会将InterceptorChain挂接到ParameterHandler/ ResultSetHandler上。 +- 创建真正的PreparedStatement时会预编译SQL,此时会调用ParameterHandler的设置参数,设置参数时会调用TypeHandler的setParameter方法。 +- 查询完后,会使用ResultSetHandler来封装查询结果(其中又会调用TypeHandler)。 +- 最后会执行清理工作。 + + +- + +# MyBatis与Hibernate比较 +- 第一方面:开发速度的对比 + +- 就开发速度而言,Hibernate的真正掌握要比Mybatis来得难些。Mybatis框架相对简单很容易上手,但也相对简陋些。个人觉得要用好Mybatis还是首先要先理解好Hibernate。 + +- 比起两者的开发速度,不仅仅要考虑到两者的特性及性能,更要根据项目需求去考虑究竟哪一个更适合项目开发,比如:一个项目中用到的复杂查询基本没有,就是简单的增删改查,这样选择hibernate效率就很快了,因为基本的sql语句已经被封装好了,根本不需要你去写sql语句,这就节省了大量的时间,但是对于一个大型项目,复杂语句较多,这样再去选择hibernate就不是一个太好的选择,选择mybatis就会加快许多,而且语句的管理也比较方便。 + +- 第二方面:开发工作量的对比 + +- Hibernate和MyBatis都有相应的代码生成工具。可以生成简单基本的DAO层方法。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap。而Hibernate有良好的映射机制,开发者无需关心SQL的生成与结果映射,可以更专注于业务流程。 + +- 第三方面:sql优化方面 + +- Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。 + +- Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。 + +- 第四方面:对象管理的对比 + +- Hibernate 是完整的对象/关系映射解决方案,它提供了对象状态管理(state management)的功能,使开发者不再需要理会底层数据库系统的细节。也就是说,相对于常见的 JDBC/SQL 持久层方案中需要管理 SQL 语句,Hibernate采用了更自然的面向对象的视角来持久化 Java 应用中的数据。 + +- 换句话说,使用 Hibernate 的开发者应该总是关注对象的状态(state),不必考虑 SQL 语句的执行。这部分细节已经由 Hibernate 掌管妥当,只有开发者在进行系统性能调优的时候才需要进行了解。而MyBatis在这一块没有文档说明,用户需要对对象自己进行详细的管理。 +- 第五方面:缓存机制 +- Hibernate一级缓存是Session缓存,利用好一级缓存就需要对Session的生命周期进行管理好。建议在一个Action操作中使用一个Session。一级缓存需要对Session进行严格管理。 + +- Hibernate二级缓存是SessionFactory级的缓存。 SessionFactory的缓存分为内置缓存和外置缓存。内置缓存中存放的是SessionFactory对象的一些集合属性包含的数据(映射元素据及预定SQL语句等),对于应用程序来说,它是只读的。外置缓存中存放的是数据库数据的副本,其作用和一级缓存类似.二级缓存除了以内存作为存储介质外,还可以选用硬盘等外部存储设备。二级缓存称为进程级缓存或SessionFactory级缓存,它可以被所有session共享,它的生命周期伴随着SessionFactory的生命周期存在和消亡。 + +# Spring对SqlSession的管理 +- 开发的时候肯定会用到Spring,也会用到mybatis-spring框架,在使用MyBatis与Spring集成的时候我们会用到了SqlSessionTemplate这个类,例如下边的配置,注入一个单例的SqlSessionTemplate对象。 + +- SqlSessionTemplate实现了SqlSession接口,也就是说我们可以使用SqlSessionTemplate来代理以往的DefaultSqlSession完成对数据库的操作,但是DefaultSqlSession这个类不是线程安全的,所以DefaultSqlSession这个类不可以被设置成单例模式的。 + +- 如果是常规开发模式的话,我们每次在使用DefaultSqlSession的时候都从SqlSessionFactory当中获取一个就可以了。但是与Spring集成以后,Spring提供了一个全局唯一的SqlSessionTemplate对象来完成DefaultSqlSession的功能,问题就是:无论是多个Dao使用一个SqlSessionTemplate,还是一个Dao使用一个SqlSessionTemplate,SqlSessionTemplate都是对应一个sqlSession对象,当多个web线程调用同一个Dao时,它们使用的是同一个SqlSessionTemplate,也就是同一个SqlSession,这可能存在着线程安全问题。 +# 动态代理SqlSessionFactory + +- SqlSessionTemplate中使用的SqlSessionFactory是经过动态代理的,实现动态代理接口的是SqlSessionInterceptor类。 + +- 注意getSqlSession和closeSqlSession方法。 +- getSqlSession为: +- 其实本质上就是ThreadLocal,每个线程有着自己对应的SqlSession,不同线程间不会共用同一个SqlSession。SqlSession被包在SqlSessionHolder,它使用了引用计数。关闭session时不是关闭session,而是减少引用计数值。 + + + +``` +public abstract class TransactionSynchronizationManager { + + private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class); + + private static final ThreadLocal<Map<Object, Object>> resources = + new NamedThreadLocal<Map<Object, Object>>("Transactional resources"); +``` + +- } + +- closeSqlSession为 From bace3e128fab4fdd554ff6d2d20cb2c48afbbd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:36:32 +0800 Subject: [PATCH 85/97] =?UTF-8?q?Update=20=E9=9B=B6=E3=80=81=E5=BF=85?= =?UTF-8?q?=E8=AF=BB=EF=BC=81=EF=BC=81=EF=BC=81.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" | 2 ++ 1 file changed, 2 insertions(+) diff --git "a/docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" "b/docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" index f77ef675..685fceb5 100644 --- "a/docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" +++ "b/docs/\351\233\266\343\200\201\345\277\205\350\257\273\357\274\201\357\274\201\357\274\201.md" @@ -2,4 +2,6 @@ - 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 - 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 +>github必须md格式才能看得舒服些,花了很多时间找word转md的工具,找了几款不太好用,于是自己手动把word改成md格式,后来发现有些重复性工作可以写个程序处理,就写了个程序,把word中的标题、代码都变成md格式,虽然能处理不少,但是还是需要人工校对,还有图片需要上传,真的超级费事,要搞吐了。。。各位也别抱怨我的github格式不好了,毕竟也还没完全处理完,体谅一下~ + ![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) From eb9bd3b48032d95241c84ac32bab7d12bb34da3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:39:21 +0800 Subject: [PATCH 86/97] =?UTF-8?q?Create=20=E5=8D=81=E5=85=AD=E3=80=81Netty?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=8E=E5=AE=9E=E7=8E=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\344\270\216\345\256\236\347\216\260.md" | 4861 +++++++++++++++++ 1 file changed, 4861 insertions(+) create mode 100644 "docs/\345\215\201\345\205\255\343\200\201Netty\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" diff --git "a/docs/\345\215\201\345\205\255\343\200\201Netty\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" "b/docs/\345\215\201\345\205\255\343\200\201Netty\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 00000000..d0f1f836 --- /dev/null +++ "b/docs/\345\215\201\345\205\255\343\200\201Netty\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,4861 @@ +# Netty Source +# Reactor模式(具有分发功能的Selector) +- 两种IO多路复用方案:Reactor and Proactor。 +- Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。 + +- 类似于生产者消费者模式,但没有缓冲区Queue,每个事件出现后会立即分发给相应的Handler处理。 + + +# 经典Reactor模式 + +- 在Reactor模式中,包含如下角色: +- Reactor 将I/O事件发派给对应的Handler +- Acceptor 处理客户端连接请求 +- Handlers 执行非阻塞读/写 + + +- 一般情况下Reactor指的是一个Acceptor/Reactor线程去循环监听IO事件(ServerSocketChannel在Selector上注册了Accept事件),如果是连接事件,那么在本线程中建立连接,在建立连接时,SocketChannel在Selector上注册Read事件; +- 如果是读就绪事件,那么将读这个IO任务交给Worker线程池处理。 +- 全程只有一个Selector,上面注册了ServerSocket的Accept事件和各个Socket的读事件。 +# Netty Reactor模式 +- 而Netty中使用的Reactor模式,引入了多Reactor,也即一个主Reactor负责监控所有的连接请求,多个子Reactor负责监控并处理读/写请求,减轻了主Reactor的压力,降低了主Reactor压力太大而造成的延迟。并且每个子Reactor分别属于一个独立的线程,每个成功连接后的Channel的所有操作由同一个线程处理。这样保证了同一请求的所有状态和上下文在同一个线程中,避免了不必要的上下文切换,同时也方便了监控请求响应状态。 + +- 多Reactor下,mainReactor是一个,subReactor是多个。mainReqactor对应着一个Selector,也是注册了ServerSocket的Accept事件,当接收到连接事件时,接收到Socket,把它交给subReactor,每个subReactor对应着自己的Selector,把Socket的读事件注册到自己的Selector中。 +- mainReactor上有一个Selector,注册了ServerSocketChannel的Accept事件;各个subReactor各自对应着自己的Selector,注册了自己对应的SocketChannel的Read事件。 + + +- + +# 分类 +- Reactor模式基于事件驱动,特别适合处理海量的I/O事件。Reactor模型主要可以分为: +- 单线程模型(所有IO操作都在同一个NIO线程上面完成) +- 多线程模型(有一组NIO线程处理IO操作) +- 主从多线程模型(服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池) + +## 单线程模型 +- Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成,NIO线程的职责如下: + - 1)作为NIO服务端,接收客户端的TCP连接; + - 2)作为NIO客户端,向服务端发起TCP连接; + - 3)读取通信对端的请求或者应答消息; + - 4)向通信对端发送消息请求或者应答消息。 + + +- 从架构层面看,一个NIO线程确实可以完成其承担的职责。例如,通过Acceptor类接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户线程可以通过消息编码通过NIO线程将消息发送给客户端。 +- 对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下: + + - 1)一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送; + - 2)当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈; + - 3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。 + +- 为了解决这些问题,演进出了Reactor多线程模型。 +## 多线程模型 +- Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理IO操作。 + + +- Reactor多线程模型的特点: + + - 1)有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求; + - 2)网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送; + - 3)1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。 + +- 在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。 +### 示例 + +``` +public class Reactor { + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.bind(new InetSocketAddress(9000)); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + if (selector.selectNow() < 0) { + continue; + } + Set<SelectionKey> keys = selector.selectedKeys(); + Iterator<SelectionKey> iterator = keys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + iterator.remove(); + if (key.isAcceptable()) { + ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = acceptServerSocketChannel.accept(); + socketChannel.configureBlocking(false); + System.out.println("Accept request from " + socketChannel.getRemoteAddress()); + SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ); + readKey.attach(new Processor()); + } else if (key.isReadable()) { + Processor processor = (Processor) key.attachment(); + processor.process(key); + } + } + } + } +} +``` + + + + +``` +*/ +public class Processor { + private static final ExecutorService service = Executors.newFixedThreadPool(16); + + public void process(SelectionKey selectionKey) { + service.submit(() -> { + ByteBuffer buffer = ByteBuffer.allocate(1024); + SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); + int count = socketChannel.read(buffer); + if (count < 0) { + socketChannel.close(); + selectionKey.cancel(); + System.out.println(socketChannel + "\t Read ended"); + return null; + } else if (count == 0) { + return null; + } + System.out.println(socketChannel + "\t Read message " + new String(buffer.array())); + return null; + }); + } +} +``` + + +## 主从多线程模型 +- 主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。 + + +- 利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。 + +- 它的工作流程总结如下: +1.- 从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接; +2.- Acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作; +3.- 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理I/O的读写操作。 + +### 示例 + + +``` +public class Reactor { + + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.bind(new InetSocketAddress(9000)); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + int coreNum = Runtime.getRuntime().availableProcessors(); + Processor[] processors = new Processor[coreNum]; + for (int i = 0; i < processors.length; i++) { + processors[i] = new Processor(); + } + System.out.println("initialized ..."); + int index = 0; + while (selector.select() > 0) { + Set<SelectionKey> keys = selector.selectedKeys(); + for (SelectionKey key : keys) { + keys.remove(key); + if (key.isAcceptable()) { + ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = acceptServerSocketChannel.accept(); + socketChannel.configureBlocking(false); + System.out.println("Accept request from " + socketChannel.getRemoteAddress()); + Processor processor = processors[(int) ((index++) / coreNum)]; + processor.addChannel(socketChannel); + } + } + } + } +} +``` + + + + +``` +public class Processor { + private static final ExecutorService executor = + Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors()); + + private Selector selector; + + public Processor() throws IOException { + this.selector = Selector.open(); + start(); + } + + public void addChannel(SocketChannel socketChannel) throws ClosedChannelException { + socketChannel.register(this.selector, SelectionKey.OP_READ); + } + + public void start() { + executor.submit(() -> { + while (true) { + if (selector.selectNow() <= 0) { + continue; + } + Set<SelectionKey> keys = selector.selectedKeys(); + Iterator<SelectionKey> iterator = keys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + iterator.remove(); + if (key.isReadable()) { +``` + + - // 这部分可以交给用户设置的业务线程池处理,在Netty里应该是对应着ChannelHandler的channelRead方法 + ByteBuffer buffer = ByteBuffer.allocate(1024); + SocketChannel socketChannel = (SocketChannel) key.channel(); + int count = socketChannel.read(buffer); + if (count < 0) { + socketChannel.close(); + key.cancel(); + System.out.println(socketChannel + "\t Read ended"); + continue; + } else if (count == 0) { + System.out.println(socketChannel + "\t Message size is 0"); + continue; + } else { + System.out.println(socketChannel + "\t Read message " + new String(buffer.array())); + } + } + } + } + }); + } +} + + +- + +# Netty Reactor实现 + + +- 服务端启动时创建了两个NioEventLoopGroup,一个是boss,一个是worker。实际上他们是两个独立的Reactor线程池。 +- Boss线程池职责如下: + - (1)接收客户端的连接,初始化Channel参数 + - (2)将链路状态变更时间通知给ChannelPipeline + +- Worker线程池作用是: + - (1)异步读取通信对端的数据报,发送读事件到ChannelPipeline + - (2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口 + - (3)执行系统调用Task; + - (4)执行定时任务Task; + +- Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换,这种串行化设计避免了多线程操作导致的锁竞争,性能角度看是最优的。 +# Netty 最佳实践 +- Netty是个异步高性能的NIO框架,它并不是个业务运行容器,因此它不需要也不应该提供业务容器和业务线程。合理的设计模式是Netty只负责提供和管理NIO线程,其它的业务层线程模型由用户自己集成,Netty不应该提供此类功能,只要将分层划分清楚,就会更有利于用户集成和扩展。 + + +- 消息队列也可以不设置,Handler直接将Task交给用户设置的业务线程池也是可以的。 + + - (1)创建两个NioEventLoopGroup,隔离NIO Acceptor和NIO的IO线程。 + - (2)尽量不要在ChannelHandler中启动用户线程(解码之后,将POJO消息派发到后端的业务线程池除外)。 + - (3)解码要放在NIO线程调用的Handler中,不要放在用户线程中解码。 + - (4)如果IO操作非常简单,不涉及复杂的业务逻辑计算,没有可能导致阻塞的磁盘操作、数据库操作、网络操作等,可以再NIO线程调用的Handler中完成业务逻辑,不要切换到用户线程。 + - (5)如果IO业务操作比较复杂,就不要在NIO线程上完成,因为阻塞可能会导致NIO线程假死,严重降低性能。这时候可以把POJO封装成Task,派发到业务线程池中由业务线程处理,以保证NIO,线程被尽快的释放,处理其余的IO操作。 + +- + +# Netty 使用 +# 为什么选择 Netty +- NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。 +- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。 +- 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。 +- JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有得到根本性解决。 +# 简介 +- Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。 +- Netty是一个高性能、异步事件驱动的NIO框架,提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步(编程模型上的)非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 + +- 通过对Netty的分析,我们将它的优点总结如下。 + +- API使用简单,开发门槛低; +- 功能强大,预置了多种编解码功能,支持多种主流协议; +- 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展; +- 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优; +- 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼; +- 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入; +- 经历了大规模的商业应用考验,质量得到验证。Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。 + +- 正是因为这些优点,Netty逐渐成为了Java NIO编程的首选框架。 +# Netty与NIO +- 不选择bio模型我们知道,那么为什么不选择aio模式呢?而还是选择nio模式呢?这是一个值得思考的问题,我就一直很好奇,因为在网络 I/O模型里面介绍的,明显AIO要比NIO模型还要好。 + +- 为何不使用AIO的官方解释: + + +# Netty 的使用场景 +- 互联网行业:随着网站规模的不断扩大,系统并发访问量也越来越高,传统基于 Tomcat 等 Web 容器的垂直架构已经无法满足需求,需要拆分应用进行服务化,以提高开发和维护效率。从组网情况看,垂直的架构拆分之后,系统采用分布式部署,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。  典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。 + +- 游戏行业:无论是手游服务端、还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信。 +- 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现。 +- 企业软件:企业和 IT 集成需要 ESB,Netty 对多协议支持、私有协议定制的简洁性和高性能是 ESB RPC 框架的首选通信组件。事实上,很多企业总线厂商会选择 Netty 作为基础通信组件,用于企业的 IT 集成。 +- 通信行业:Netty 的异步高性能、高可靠性和高成熟度的优点,使它在通信行业得到了大量的应用。 +# NIO BUG +- 原生的 NIO 在 JDK 1.7 版本存在 epoll bug +- 若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100% +- Netty的解决办法 +- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数, + +- 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。 + +- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。 +# 缓冲 +- Netty 使用自建的 buffer API,而不是使用 NIO 的 ByteBuffer 来表示一个连续的字节序列。与 ByteBuffer 相比这种方式拥有明显的优势。Netty 使用新的 buffer 类型 ByteBuf,被设计为一个可从底层解决 ByteBuffer 问题,并可满足日常网络应用开发需要的缓冲类型。这些很酷的特性包括: + +- 如果需要,允许使用自定义的缓冲类型。 +- 复合缓冲类型中内置的透明的零拷贝实现。 +- 开箱即用的动态缓冲类型,具有像 StringBuffer 一样的动态缓冲能力。 +- 不再需要调用的flip()方法。 +- 正常情况下具有比 ByteBuffer 更快的响应速度。 +## 零拷贝 +- 为什么叫零拷贝?因为在数据传输时,最终处理的数据会需要对单个传输层的报文,进行组合或者拆分。NIO原生的ByteBuffer要做到这件事,需要对ByteBuffer内容进行拷贝,产生新的ByteBuffer,而Netty通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝。这部分代码在org.jboss.netty.buffer包中。 + +- 例如,一个信息可以由两部分组成;header 和 body。在一个模块化的应用,当消息发送出去时,这两部分可以由不同的模块生产和装配。 +- 如果你使用的是 ByteBuffer ,你必须要创建一个新的大缓存区用来拷贝这两部分到这个新缓存区中。或者,你可以在 NiO做一个收集写操作,但限制你将复合缓冲类型作为 ByteBuffer 的数组而不是一个单一的缓冲区,打破了抽象,并且引入了复杂的状态管理。此外,如果你不从 NIO channel 读或写,它是没有用的。 + +- // 复合类型与组件类型不兼容。 +- ByteBuffer[] message = new ByteBuffer[] { header, body }; +- 通过对比, ByteBuf 不会有警告,因为它是完全可扩展并有一个内置的复合缓冲区。 + +- // 复合类型与组件类型是兼容的。 +- ByteBuf message = Unpooled.wrappedBuffer(header, body); + +- // 因此,你甚至可以通过混合复合类型与普通缓冲区来创建一个复合类型。 +- ByteBuf messageWithFooter = Unpooled.wrappedBuffer(message, footer); + +- // 由于复合类型仍是 ByteBuf,访问其内容很容易, +- //并且访问方法的行为就像是访问一个单独的缓冲区, +- //即使你想访问的区域是跨多个组件。 +- //这里的无符号整数读取位于 body 和 footer +- messageWithFooter.getUnsignedInt( + - messageWithFooter.readableBytes() - footer.readableBytes() - 1); + +## 自动容量扩展 +- // 一种新的动态缓冲区被创建。在内部,实际缓冲区是被“懒”创建,从而避免潜在的浪费内存空间。 + - ByteBuf b = Unpooled.buffer(4); + +- // 当第一个执行写尝试,内部指定初始容量 4 的缓冲区被创建 +- b.writeByte('1'); + +- b.writeByte('2'); +- b.writeByte('3'); +- b.writeByte('4'); + +- // 当写入的字节数超过初始容量 4 时, +- //内部缓冲区自动分配具有较大的容量 +- b.writeByte('5'); +## 更好的性能 +- 最频繁使用的缓冲区 ByteBuf 的实现是一个非常薄的字节数组包装器(比如,一个字节)。与 ByteBuffer 不同,它没有复杂的边界和索引检查补偿,因此对于 JVM 优化缓冲区的访问更加简单。更多复杂的缓冲区实现是用于拆分或者组合缓存,并且比 ByteBuffer 拥有更好的性能。 + +- + +# 统一的IO API +- OIO就是指BIO。 +- 传统的 Java I/O API 在应对不同的传输协议时需要使用不同的类型和方法。例如:java.net.Socket 和 java.net.DatagramSocket 它们并不具有相同的超类型,因此,这就需要使用不同的调用方式执行 socket 操作。 + +- 这种模式上的不匹配使得在更换一个网络应用的传输协议时变得繁杂和困难。由于(Java I/O API)缺乏协议间的移植性,当你试图在不修改网络传输层的前提下增加多种协议的支持,这时便会产生问题。并且理论上讲,多种应用层协议可运行在多种传输层协议之上例如TCP/IP,UDP/IP,SCTP和串口通信。 + +- 让这种情况变得更糟的是,Java 新的 I/O(NIO)API与原有的阻塞式的I/O(OIO)API 并不兼容,NIO.2(AIO)也是如此。由于所有的API无论是在其设计上还是性能上的特性都与彼此不同,在进入开发阶段,你常常会被迫的选择一种你需要的API。 + +- 例如,在用户数较小的时候你可能会选择使用传统的 OIO(Old I/O) API,毕竟与 NIO 相比使用 OIO 将更加容易一些。然而,当你的业务呈指数增长并且服务器需要同时处理成千上万的客户连接时你便会遇到问题。这种情况下你可能会尝试使用 NIO,但是复杂的 NIO Selector 编程接口又会耗费你大量时间并最终会阻碍你的快速开发。 + +- Netty 有一个叫做 Channel 的统一的异步 I/O 编程接口,这个编程接口抽象了所有点对点的通信操作。也就是说,如果你的应用是基于 Netty 的某一种传输实现,那么同样的,你的应用也可以运行在 Netty 的另一种传输实现上。Netty 提供了几种拥有相同编程接口的基本传输实现: + +- 基于 NIO 的 TCP/IP 传输 (见 io.netty.channel.nio), +- 基于 OIO 的 TCP/IP 传输 (见 io.netty.channel.oio), +- 基于 OIO 的 UDP/IP 传输, 和 +- 本地传输 (见 io.netty.channel.local). +- 切换不同的传输实现通常只需对代码进行几行的修改调整,例如选择一个不同的 ChannelFactory 实现。 + +- 此外,你甚至可以利用新的传输实现没有写入的优势,只需替换一些构造器的调用方法即可,例如串口通信。而且由于核心 API 具有高度的可扩展性,你还可以完成自己的传输实现。 + +# 事件驱动 +- 一个定义良好并具有扩展能力的事件模型是事件驱动开发的必要条件。Netty 具有定义良好的 I/O 事件模型。由于严格的层次结构区分了不同的事件类型,因此 Netty 也允许你在不破坏现有代码的情况下实现自己的事件类型。这是与其他框架相比另一个不同的地方。很多 NIO 框架没有或者仅有有限的事件模型概念;在你试图添加一个新的事件类型的时候常常需要修改已有的代码,或者根本就不允许你进行这种扩展。 + +- 在一个 ChannelPipeline 内部一个 ChannelEvent 被一组ChannelHandler 处理。这个管道是 Intercepting Filter (拦截过滤器)模式的一种高级形式的实现,因此对于一个事件如何被处理以及管道内部处理器间的交互过程,你都将拥有绝对的控制力。 + +# Netty服务器创建过程 + + +``` +public void run() { + //两个事件循环器,第一个用于接收客户端连接,第二个用于处理客户端的读写请求 + //是线程组,持有一组线程 + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + //服务器辅助类,用于配置服务器 + ServerBootstrap bootstrap = new ServerBootstrap(); + //配置服务器参数 + bootstrap.group(bossGroup, workerGroup) + //使用这种类型的NIO通道,现在是基于TCP协议的 + .channel(NioServerSocketChannel.class) + //对Channel进行初始化,绑定实际的事件处理器,要么实现ChannelHandler接口,要么继承ChannelHandlerAdapter类 + .childHandler(new ChannelInitializer<SocketChannel>() { + protected void initChannel(SocketChannel ch) throws Exception { + //编码是其他格式转为字节 + //解码是从字节转到其他格式 + //服务器是把先请求转为POJO(解码),再把响应转为字节(编码) + //而客户端是先把请求转为字节(编码),再把响应转为POJO(解码) + ch.pipeline() + .addLast(new IdleStateHandler(10, 0, 0)) + // 将 RPC 请求进行解码(为了处理请求) + .addLast(new RPCDecoder(RPCRequest.class)) + // 将 RPC 响应进行编码(为了返回响应) + .addLast(new RPCEncoder(RPCResponse.class)) + // 处理 RPC 请求 + .addLast(new RPCServerHandler()); + } + }) + //服务器配置项 + //BACKLOG + //TCP维护有两个队列,分别称为A和B + //客户端发送SYN,服务器接收到后发送SYN ACK,将客户端放入到A队列 + //客户端接收到后再次发送ACK,服务器接收到后将客户端从A队列移至B队列,服务器的accept返回。 + //A和B队列长度之和为backlog + //当A和B队列长度之和大于backlog时,新的连接会被TCP内核拒绝 + //注意:backlog对程序的连接数并无影响,影响的只是还没有被accept取出的连接数。 + .option(ChannelOption.SO_BACKLOG, 128) + //指定发送缓冲区大小 + .option(ChannelOption.SO_SNDBUF, 32 * 1024) + //指定接收缓冲区大小 + .option(ChannelOption.SO_RCVBUF, 32 * 1024) + //这里的option是针对于上面的NioServerSocketChannel + //复杂的时候可能会设置多个Channel + .childOption(ChannelOption.SO_KEEPALIVE, true); + //.sync表示是一个同步阻塞执行,普通的Netty的IO操作都是异步执行的 + //一个ChannelFuture代表了一个还没有发生的I/O操作。这意味着任何一个请求操作都不会马上被执行 + //Netty强烈建议直接通过添加监听器的方式获取I/O结果,而不是通过同步等待(.sync)的方式 + //如果用户操作调用了sync或者await方法,会在对应的future对象上阻塞用户线程 + + String address = PropertyUtil.getProperty("server.address"); + if (address == null) { + throw new IllegalStateException("server.address未找到"); + } + + String host = address.split(":")[0]; + Integer port = Integer.parseInt(address.split(":")[1]); + //绑定端口,开始监听 + //注意这里可以绑定多个端口,每个端口都针对某一种类型的数据(控制消息,数据消息) + ChannelFuture future = bootstrap.bind(host, port).sync(); + log.info("服务器启动"); + + registry = new ServiceRegistry(); + registry.register(address); + log.info("服务器向Zookeeper注册完毕"); + + //应用程序会一直等待,直到channel关闭 + future.channel().closeFuture().sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + registry.close(); + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } +} +``` + + +- 1、NioEventLoopGroup 是用来处理I/O操作的线程池,Netty对 EventLoopGroup 接口针对不同的传输协议提供了不同的实现。在本例子中,需要实例化两个NioEventLoopGroup,通常第一个称为“boss”,用来accept客户端连接,另一个称为“worker”,处理客户端数据的读写操作。 + +- 2、ServerBootstrap 是启动服务的辅助类,有关socket的参数可以通过ServerBootstrap进行设置。 + +- 3、这里指定NioServerSocketChannel类初始化channel用来接受客户端请求。 + +- 4、通常会为新SocketChannel通过添加一些handler,来设置ChannelPipeline。ChannelInitializer 是一个特殊的handler,其中initChannel方法可以为SocketChannel 的pipeline添加指定handler。 + +- 5、通过绑定端口8080,就可以对外提供服务了。 + +- 通常情况下,服务端的创建是在用户进程启动的时候进行,因此一般由Main函数或者启动类负责创建,服务端的创建由业务线程负责完成。在创建服务端的时候实例化了2个EventLoopGroup,1个EventLoopGroup实际就是一个EventLoop线程组,负责管理EventLoop的申请和释放。 + +- EventLoopGroup管理的线程数可以通过构造函数设置,如果没有设置,默认取-Dio.netty.eventLoopThreads,如果该系统参数也没有指定,则为可用的CPU内核数 × 2。 + +- bossGroup线程组实际就是Acceptor线程池,负责处理客户端的TCP连接请求,如果系统只有一个服务端端口需要监听,则建议bossGroup线程组线程数设置为1。 + +- workerGroup是真正负责I/O读写操作的线程组,通过ServerBootstrap的group方法进行设置,用于后续的Channel绑定。 +# Netty客户端创建过程 + +- 只有一个eventloopgroup + +``` +public RPCClient() { + log.info("初始化RPC客户端"); + this.discovery = new ServiceDiscovery(); + this.responses = new ConcurrentHashMap<>(); + this.group = new NioEventLoopGroup(); + this.bootstrap = new Bootstrap(); + this.bootstrap.group(group).channel(NioSocketChannel.class) + .handler(new ChannelInitializer<SocketChannel>() { + @Override + public void initChannel(SocketChannel channel) throws Exception { + channel.pipeline() + .addLast(new IdleStateHandler(0, 0, 5)) + // 将 RPC 请求进行编码(为了发送请求) + .addLast(new RPCEncoder(Message.class, RPCRequest.class)) + // 将 RPC 响应进行解码(为了处理响应) + .addLast(new RPCDecoder(Message.class, RPCResponse.class)) + // 使用 RpcClient 发送 RPC 请求 + .addLast(new RPCClientHandler(RPCClient.this,responses)); + } + }) + .option(ChannelOption.SO_KEEPALIVE, true); + + try { + this.futureChannel = connect(); + log.info("客户端初始化完毕"); + } catch (Exception e) { + log.error("与服务器的连接出现故障"); + handleException(); + } +} +``` + + + +``` +private Channel connect() throws Exception { + log.info("向ZK查询服务器地址中..."); + String serverAddress = discovery.discover(); + if (serverAddress == null) { + throw new ServerNotAvailableException(); + } + String host = serverAddress.split(":")[0]; + Integer port = Integer.parseInt(serverAddress.split(":")[1]); + ChannelFuture future = bootstrap.connect(host, port).sync(); + log.info("客户端已连接"); + return future.channel(); +} +``` + +1.- 由用户线程负责初始化客户端资源,发起连接操作; +2.- 如果连接成功,将SocketChannel注册到IO线程组的NioEventLoop线程中,监听读操作位; +3.- 如果没有立即连接成功,将SocketChannel注册到IO线程组的NioEventLoop线程中,监听连接操作位; +4.- 连接成功之后,修改监听位为READ,但是不需要切换线程。 +- + +# Netty 概念 +# EventLoop +- 运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编 +- 程上的构造通常被称为事件循环—一个Netty 使用了interface io.netty.channel.EventLoop 来适配的术语。 + +- 事件/任务的执行顺序 事件和任务是以先进先出(FIFO)的顺序执行的。这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性。 +## 继承体系 + +- 一个EventLoop 将由一个永远都不会改变的Thread 驱动,同时任务 +- (Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行。 +- 根据配置和可用核心的不同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个Channel。 +- 事件的性质通常决定了它将被如何处理;它可能将数据从网络栈中传递到你的应用程序中, +- 或者进行逆向操作,或者执行一些截然不同的操作。但是事件的处理逻辑必须足够的通用和灵活,以处理所有可能的用例。因此,在Netty 4 中,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。 +## 任务调度 +- ScheduledExecutorService 的实现具有局限性,例如,事实上作为线程池管理的一部 +- 分,将会有额外的线程创建。如果有大量任务被紧凑地调度,那么这将成为一个瓶颈。Netty 通过Channel 的EventLoop 实现任务调度解决了这一问题。 + + +## EventLoopGroup +- Netty的服务端使用了两个EventLoopGroup,而第一个EventLoopGroup通常只有一个EventLoop,通常叫做bossGroup,负责客户端的连接请求,然后打开Channel,交给后面的EventLoopGroup(称为workerGroup)中的一个EventLoop来负责这个Channel上的所有读写事件,一个Channel只会被一个EventLoop处理,而一个EventLoop可能会被分配给多个Channel来负责上面的事件,当然,Netty不仅支持NI/O,还支持OI/O,所以两者的EventLoop分配方式有所区别。 + +- 服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费。 + +### NIO(EventLoop多路复用,与Channel一对多) + + +- 在NIO非阻塞模式下,Netty将负责为每个Channel分配一个EventLoop,一旦一个EventLoop被分配给了一个Channel,那么在它的整个生命周期中都使用这个EventLoop,但是多个Channel将可能共享一个EventLoop,所以和Thread相关的ThreadLocal的使用就要特别注意,因为有多个Channel在使用该Thread来处理读写时间。在阻塞IO模式下,考虑到一个Channel将会阻塞,所以不太可能将一个EventLoop共用于多个Channel之间,所以,每一个Channel都将被分配一个EventLoop,并且反过来也成立,也就是一个EventLoop将只会被绑定到一个Channel上来处理这个Channel上的读写事件。无论是非阻塞模式还是阻塞模式,一个Channel都将会保证一个Channel上的所有读写事件都只会在一个EventLoop上被处理。 + +- 每一个连接对应一个 Channel(多路指多个 Channel,复用指多个连接复用了一个线程或少量线程,在 Netty 指 EventLoop),一个 Channel 对应唯一的 ChannelPipeline,多个 Handler 串行的加入到 Pipeline 中,每个 Handler 关联唯一的 ChannelHandlerContext。 +- 在默认的情况下,ChannelHandler会把对它的方法的调用转发给链中的下一个ChannelHandler实例链。因此,如果exceptionCaught方法没有被该链中的某处实现,那么所接收到的异常将会被传递到ChannelPipeline的尾端并被记录。为此,你的应用程序应该提供至少有一个实现了exceptionCaught方法的ChannelHandler。 + +- 总结: + - 1)一个EventLoopGroup包含一个或者多个EventLoop + - 2)一个EventLoop在其生命周期内只和一个线程绑定 + - 3)所有由EventLoop处理的IO事件都将在它专有的线程中被处理 + - 4)一个Channel在它的生命周期只注册于一个EventLoop + - 5)一个EventLoop可能会被分配给一个或多个Channel + - 6)一个 Channel 对应唯一的 ChannelPipeline + - 7)多个 ChannelHandler 串行地加入到 ChannelPipeline 中 + +### OIO(EventLoop与Channel一对一) + +- 得到的保证是每个Channel 的I/O 事件都将只会被一个Thread(用于支撑该Channel 的EventLoop 的那个Thread)处理。 +## 线程模型 +- EventLoopGroup类似于一个线程池,EventLoop类似于一个线程的封装。 +- EventLoop继承了Java的ScheduledExecutorService,也就是调度线程池,所以,EventLoop应当有ScheduledExecutorService提供的所有功能。那为什么需要继承ScheduledExecutorService呢,也就是为什么需要延时调度功能,那是因为,在Netty中,有可能用户线程和Netty的I/O线程同时操作网络资源,而为了减少并发锁竞争,Netty将用户线程的任务包装成Netty的task,然后向Netty的I/O任务一样去执行它们。有些时候我们需要延时执行任务,或者周期性执行任务,那么就需要调度功能。这是Netty在设计上的考虑,为我们极大的简化的编程方法。 +- +- Netty线程模型的卓越性能取决于对于当前执行的Thread的身份的确定,也就是说,确定 +- 它是否是分配给当前Channel以及它的EventLoop的那一个线程。 +- 如果(当前)调用线程正是支撑EventLoop 的线程,那么所提交的代码块将会被(直接) +- 执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。这也就解释了任何的Thread 是如何与Channel 直接交互而无需在ChannelHandler 中进行额外同步的。 +- 注意,每个EventLoop 都有它自已的任务队列,独立于任何其他的EventLoop。 + +- “永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任其他任务。”如果必须要进行阻塞调用或者执行长时间运行的任务,我们建议使用一个专门的EventExecutor。 + +# Channel + +- 每个Channel 都将会被分配一个ChannelPipeline 和ChannelConfig。 +- ChannelConfig 包含了该Channel 的所有配置设置,并且支持热更新。由于特定的传输可能 +- 具有独特的设置,所以它可能会实现一个ChannelConfig 的子类型。 +- 由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang. +- Comparable 的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。 +- ChannelPipeline 持有所有将应用于入站和出站数据以及事件的ChannelHandler 实例,这些ChannelHandler 实现了应用程序用于处理状态变化以及数据处理的逻辑。 +- ChannelHandler 的典型用途包括: +- 将数据从一种格式转换为另一种格式; +- 提供异常的通知; +- 提供Channel 变为活动的或者非活动的通知; +- 提供当Channel 注册到EventLoop 或者从EventLoop 注销时的通知; +- 提供有关用户自定义事件的通知。 +- ChannelPipeline使用了责任链模式。 + +- Netty 的Channel 实现是线程安全的,因此你可以存储一个到Channel 的引用,并且每当 +- 你需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它。 +- 需要注意的是,消息将会被保证按顺序发送。 +## Channel的生命周期 +- + +# ChannelFuture +- 正如我们已经解释过的那样,Netty 中所有的I/O 操作都是异步的。因为一个操作可能不会 +- 立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了 +- ChannelFuture 接口,其addListener()方法注册了一个ChannelFutureListener,以 +- 便在某个操作完成时(无论是否成功)得到通知。 +# ChannelHandler&ChannelPipeline +- 从应用程序开发人员的角度来看,Netty 的主要组件是ChannelHandler,它充当了所有 +- 处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler 的方法是 +- 由网络事件(其中术语“事件”的使用非常广泛)触发的。事实上,ChannelHandler 可专 +- 门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中所抛出的异常。 +## ChannelHandler的生命周期 + + +## ChannelInboundHandler 接口 +- ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler 冲刷数据。你的应用程序的业务逻辑通常驻留在一个或者多个ChannelInboundHandler 中。 + +- ChannelPipeline 提供了ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel 被创建时,它会被自动地分配到它专属的ChannelPipeline。 +- ChannelHandler 安装到ChannelPipeline 中的过程如下所示: +1.- 一个ChannelInitializer的实现被注册到了ServerBootstrap中; +2.- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline 中安装一组自定义的ChannelHandler; +3.- ChannelInitializer 将它自己从ChannelPipeline 中移除。 + +- ChannelHandler 是专为支持广泛的用途而设计的,可以将它看作是处理往来Channel- +- Pipeline 事件(包括数据)的任何代码的通用容器。图3-2 说明了这一点,其展示了从ChannelHandler 派生的ChannelInboundHandler 和ChannelOutboundHandler 接口。 + +- 使得事件流经ChannelPipeline 是ChannelHandler 的工作,它们是在应用程序的初 +- 始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为ChannelPipeline 的是这些ChannelHandler 的编排顺序。 + +- 如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,并被传递给第一个ChannelInboundHandler。这个ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。 +- 数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。 + +- 鉴于出站操作和入站操作是不同的,你可能会想知道如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline 中会发生什么。虽然ChannelInboundHandle 和ChannelOutboundHandle 都扩展自ChannelHandler,但是Netty 能区分ChannelInboundHandler +- 实现和ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。 +- 当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandler-Context,其代表了ChannelHandler 和ChannelPipeline 之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。 +- 在Netty 中,有两种发送消息的方式。你可以直接写到Channel 中,也可以写到和ChannelHandler相关联的ChannelHandlerContext 对象中。前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从ChannelPipeline 中的下一个ChannelHandler 开始流动。 + +- 编程时注意: +- 1、ChannelInboundHandler之间的传递,通过调用 ctx.fireChannelRead(msg) 实现;调用ctx.write(msg) 将传递到ChannelOutboundHandler。 +- 2、ctx.write()方法执行后,需要调用flush()方法才能令它立即执行。 +- 3、ChannelOutboundHandler 在注册的时候需要放在最后一个ChannelInboundHandler之前,否则将无法传递到ChannelOutboundHandler。 +- 4、Handler的消费处理放在最后一个处理。 + +- Netty 以适配器类的形式提供了大量默认的ChannelHandler 实现,其旨在简化应用程序处理逻辑的开发过程。你已经看到了,ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个ChannelHandler。这些适配器类(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。 +- 有一些适配器类可以将编写自定义的ChannelHandler 所需要的努力降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。 +- 下面这些是编写自定义ChannelHandler 时经常会用到的适配器类: +-  ChannelHandlerAdapter +-  ChannelInboundHandlerAdapter +-  ChannelOutboundHandlerAdapter +-  ChannelDuplexHandler +### SimpleChannelInboundHandler +- 最常见的情况是,你的应用程序会利用一个ChannelHandler 来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler<T>,其中T 是你要处理的消息的Java 类型。在这个ChannelHandler 中,你将需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext 的引用,这个引用将作为输入参数传递给ChannelHandler 的所有方法。在这种类型的ChannelHandler 中, 最重要的方法是channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的I/O 线程之外,其具体实现完全取决于你。 + +- 当某个ChannelInboundHandler 的实现重写channelRead()方法时,它将负责显式地 +- 释放与池化的ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法ReferenceCount- +- Util.release()。 +- Netty 将使用WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用Simple- +- ChannelInboundHandler。由于SimpleChannelInboundHandler 会自动释放资源,所以你不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。 +## ChannelOutboundHandler 接口 +- 出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、Channel- +- Pipeline 以及ChannelHandlerContext 调用。 +- ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可 +- 以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续。 + +- ChannelPromise与ChannelFuture ChannelOutboundHandler中的大部分方法都需要一个 +- ChannelPromise参数,以便在操作完成时得到通知。ChannelPromise是ChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()和setFailure(),从而使ChannelFuture不可变。 +## ChannelHandlerAdapter +- 你可以使用ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter +- 类作为自己的ChannelHandler 的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler 的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler 的方法。 + +- ChannelHandlerAdapter 还提供了实用方法isSharable()。如果其对应的实现被标 +- 注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline +- 中。 +- 在ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter 中所 +- 提供的方法体调用了其相关联的ChannelHandlerContext 上的等效方法,从而将事件转发到 +- 了ChannelPipeline 中的下一个ChannelHandler 中。 +- 你要想在自己的ChannelHandler 中使用这些适配器类,只需要简单地扩展它们,并且重 +- 写那些你想要自定义的方法。 +## 避免内存泄露 +- class ResourceLeakDetector它将对你应用程序的缓冲区分配做大约1%的采样来检测内存泄露,其利用了JDK 提供的PhantomReference<T>类来实现这一点。 +- 实现ChannelInboundHandler.channelRead()和ChannelOutboundHandler.write() +- 方法时,应该如何使用这个诊断工具来防止泄露呢? + +- +## ChannelPipeline +- 每一个新创建的Channel 都将会被分配一个新的ChannelPipeline。这项关联是永久性 +- 的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。在Netty 组件 +- 的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。根据事件的起源,事件将会被ChannelInboundHandler 或者ChannelOutboundHandler处理。随后,通过调用ChannelHandlerContext 实现,它将被转发给同一超类型的下一个ChannelHandler。 + +- ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的 +- ChannelHandler 交互。ChannelHandler 可以通知其所属的ChannelPipeline 中的下一个 +- ChannelHandler,甚至可以动态修改它所属的ChannelPipeline。 +- 在ChannelPipeline 传播事件时,它会测试ChannelPipeline 中的下一个ChannelHandler 的类型是否和事件的运动方向相匹配。如果不匹配,ChannelPipeline 将跳过该ChannelHandler 并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止。 + +- ChannelHandler 可以通过添加、删除或者替换其他的ChannelHandler 来实时地修改 +- ChannelPipeline 的布局。(它也可以将它自己从ChannelPipeline 中移除。)这是Channel- +- Handler 最重要的能力之一,所以我们将仔细地来看看它是如何做到的。 + + + +### ChannelHandler 的执行和阻塞 +- 通常ChannelPipeline 中的每一个ChannelHandler 都是通过它的EventLoop(I/O 线程)来处理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的I/O 处理产生负面的影响。 +- 但有时可能需要与那些使用阻塞API 的遗留代码进行交互。对于这种情况,ChannelPipeline 有一些接受一个EventExecutorGroup 的add()方法。如果一个事件被传递给一个自定义的EventExecutorGroup,它将被包含在这个EventExecutorGroup 中的某个EventExecutor 所处理,从而被从该Channel 本身的EventLoop 中移除。对于这种用例,Netty 提供了一个叫DefaultEventExecutorGroup 的默认实现。 +### 触发事件 + + +- 总结一下: +- ChannelPipeline 保存了与Channel 相关联的ChannelHandler; +- ChannelPipeline 可以根据需要,通过添加或者删除ChannelHandler 来动态地修改; +- ChannelPipeline 有着丰富的API 用以被调用,以响应入站和出站事件。 +## ChannelHandlerContext 接口 +- ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关 +- 联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandler- +- Context。ChannelHandlerContext 的主要功能是管理它所关联的ChannelHandler 和在 +- 同一个ChannelPipeline 中的其他ChannelHandler 之间的交互。 +- ChannelHandlerContext 有很多的方法,其中一些方法也存在于Channel 和ChannelPipeline 本身上,但是有一点重要的不同。如果调用Channel 或者ChannelPipeline 上的这 +- 些方法,它们将沿着整个ChannelPipeline 进行传播。而调用位于ChannelHandlerContext +- 上的相同方法,则将从当前所关联的ChannelHandler 开始,并且只会传播给位于该 +- ChannelPipeline 中的下一个能够处理该事件的ChannelHandler。 + + +- 当使用ChannelHandlerContext 的API 的时候,请牢记以下两点: +- ChannelHandlerContext 和ChannelHandler 之间的关联(绑定)是永远不会改 +- 变的,所以缓存对它的引用是安全的; +- 相对于其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。 + + + + + +- 为什么会想要从ChannelPipeline 中的某个特定点开始传播事件呢? +- 为了减少将事件传经对它不感兴趣的ChannelHandler 所带来的开销。 +- 为了避免将事件传经那些可能会对它感兴趣的ChannelHandler。 +- 要想调用从某个特定的ChannelHandler 开始的处理过程,必须获取到在(Channel- +- Pipeline)该ChannelHandler 之前的ChannelHandler 所关联的ChannelHandler- +- Context。这个ChannelHandlerContext 将调用和它所关联的ChannelHandler 之后的 +- ChannelHandler。 +## ChannelHandler 和ChannelHandlerContext 的高级用法 +- 可以通过调用ChannelHandlerContext 上的pipeline()方法来获得被封闭的ChannelPipeline 的引用。这使得运行时得以操作ChannelPipeline 的ChannelHandler,我们可以利用这一点来实现一些复杂的设计。例如,你可以通过将ChannelHandler 添加到ChannelPipeline 中来实现动态的协议切换。 +- 另一种高级的用法是缓存到ChannelHandlerContext 的引用以供稍后使用,这可能会发 +- 生在任何的ChannelHandler 方法之外,甚至来自于不同的线程。 + +- 因为一个ChannelHandler 可以从属于多个ChannelPipeline,所以它也可以绑定到多 +- 个ChannelHandlerContext 实例。对于这种用法指在多个ChannelPipeline 中共享同一 +- 个ChannelHandler,对应的ChannelHandler 必须要使用@Sharable 注解标注;否则, +- 试图将它添加到多个ChannelPipeline 时将会触发异常。显而易见,为了安全地被用于多个 +- 并发的Channel(即连接),这样的ChannelHandler 必须是线程安全的。 + +- 只应该在确定了你的ChannelHandler 是线程安全的时才使用@Sharable 注解。 +- 为何要共享同一个ChannelHandler 在多个ChannelPipeline中安装同一个ChannelHandler的一个常见的原因是用于收集跨越多个Channel 的统计信息。 +## 异常处理 +### 处理入站异常 +- 如果在处理入站事件的过程中有异常被抛出,那么它将从它在ChannelInboundHandler +- 里被触发的那一点开始流经ChannelPipeline。要想处理这种类型的入站异常,你需要在你 +- 的ChannelInboundHandler 实现中重写下面的方法。 + +``` +public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception +``` + +- 因为异常将会继续按照入站方向流动(就像所有的入站事件一样),所以实现了前面所示逻 +- 辑的ChannelInboundHandler 通常位于ChannelPipeline 的最后。这确保了所有的入站 +- 异常都总是会被处理,无论它们可能会发生在ChannelPipeline 中的什么位置。 +- 你应该如何响应异常,可能很大程度上取决于你的应用程序。你可能想要关闭Channel(和 +- 连接),也可能会尝试进行恢复。如果你不实现任何处理入站异常的逻辑(或者没有消费该异常),那么Netty将会记录该异常没有被处理的事实。 +- 总结一下: +- ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给ChannelPipeline 中的下一个ChannelHandler; +- 如果异常到达了ChannelPipeline 的尾端,它将会被记录为未被处理; +- 要想定义自定义的处理逻辑,你需要重写exceptionCaught()方法。然后你需要决定 +- 是否需要将该异常传播出去。 +### 处理出站异常 +- 用于处理出站操作中的正常完成以及异常的选项,都基于以下的通知机制。 +- 每个出站操作都将返回一个ChannelFuture。注册到ChannelFuture 的Channel- +- FutureListener 将在操作完成时被通知该操作是成功了还是出错了。 +- 几乎所有的ChannelOutboundHandler 上的方法都会传入一个ChannelPromise +- 的实例。作为ChannelFuture 的子类,ChannelPromise 也可以被分配用于异步通 +- 知的监听器。但是,ChannelPromise 还具有提供立即通知的可写方法: +- ChannelPromise setSuccess(); +- Chan nelPromise setFailure(Throwable cause); + +- 添加ChannelFutureListener 只需要调用ChannelFuture 实例上的addListener +- (ChannelFutureListener)方法,并且有两种不同的方式可以做到这一点。其中最常用的方式是, +- 调用出站操作(如write()方法)所返回的ChannelFuture 上的addListener()方法。 + +- 第二种方式是将ChannelFutureListener 添加到即将作为参数传递给ChannelOutboundHandler的方法的ChannelPromise。 + +- ChannelPromise 的可写方法 +- 通过调用ChannelPromise 上的setSuccess()和setFailure()方法,可以使一个操作的状 +- 态在ChannelHandler 的方法返回给其调用者时便即刻被感知到。 +- 为何选择一种方式而不是另一种呢?对于细致的异常处理,你可能会发现,在调用出站操 +- 作时添加ChannelFutureListener 更合适。而对于一般的异常处理,你可能会发现,代码清单6-14 所示的自定义的ChannelOutboundHandler 实现的方式更加的简单。 +- 如果你的ChannelOutboundHandler 本身抛出了异常会发生什么呢?在这种情况下, +- Netty 本身会通知任何已经注册到对应ChannelPromise 的监听器。 + +# Bootstrap + +- 有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。 + +- 实际上,ServerBootstrap 类也可以只使用一个EventLoopGroup,此时其将在两个场景下共用同一个EventLoopGroup。 +- 这两种类型的引导类之间的第一个区别已经讨论过了:ServerBootstrap 将绑定到一个 +- 端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。 +- 第二个区别可能更加明显。引导一个客户端只需要一个EventLoopGroup,但是一个 +- ServerBootstrap 则需要两个(也可以是同一个实例)。为什么呢? +- 因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务 +- 器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。 + + +- 与ServerChannel 相关联的EventLoopGroup 将分配一个负责为传入连接请求创建 +- Channel 的EventLoop。一旦连接被接受,第二个EventLoopGroup 就会给它的Channel分配一个EventLoop。 +## Bootstrap客户端 +- Bootstrap 类被用于客户端或者使用了无连接协议的应用程序中。表8-1 提供了该类的一 +- 个概览,其中许多方法都继承自AbstractBootstrap 类。 + + + + + + + +## ServerBoostrap服务器 + +- 表8-2 中列出了一些在表8-1 中不存在的方法:childHandler()、 +- childAttr()和childOption()。这些调用支持特别用于服务器应用程序的操作。具体来说, +- ServerChannel 的实现负责创建子Channel,这些子Channel 代表了已被接受的连接。因此,负责引导ServerChannel 的ServerBootstrap 提供了这些方法,以简化将设置应用到已被接受的子Channel 的ChannelConfig 的任务。 + + +- protected abstract void initChannel(C ch) throws Exception; +- 这个方法提供了一种将多个ChannelHandler 添加到一个ChannelPipeline 中的简便 +- 方法。你只需要简单地向Bootstrap 或ServerBootstrap 的实例提供你的Channel- +- Initializer 实现即可,并且一旦Channel 被注册到了它的EventLoop 之后,就会调用你的 +- initChannel()版本。在该方法返回之后,ChannelInitializer 的实例将会从Channel- +- Pipeline 中移除它自己。 + +## 关闭 +- 最重要的是,你需要关闭EventLoopGroup,它将处理任何挂起的事件和任务,并且随后 +- 释放所有活动的线程。这就是调用EventLoopGroup.shutdownGracefully()方法的作用。 +- 这个方法调用将会返回一个Future,这个Future 将在关闭完成时接收到通知。需要注意的是,shutdownGracefully()方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向所返回的Future 注册一个监听器以在关闭完成时获得通知。 + +# 编解码器 +- 当你通过Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解 +- 码;也就是说,从字节转换为另一种格式,通常是一个Java 对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。 +- 对应于特定的需要,Netty 为编码器和解码器提供了不同类型的抽象类。例如,你的应用程 +- 序可能使用了一种中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是 +- 它将派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单的命名约定。 +- 通常来说,这些基类的名称将类似于ByteToMessageDecoder 或MessageToByteEncoder。 + +- 严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化ChannelHandler 的创建的适配器类一样,所有由Netty 提供的编码器/解码器适配器类都实现了ChannelOutboundHandler 或者ChannelInboundHandler 接口。 +- 你将会发现对于入站数据来说,channelRead 方法/事件已经被重写了。对于每个从入站 +- Channel 读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的decode()方法,并将已解码的字节转发给ChannelPipeline 中的下一个ChannelInboundHandler。 +- 出站消息的模式是相反方向的:编码器将消息转换为字节,并将它们转发给下一个 +- ChannelOutboundHandler。 +## 抽象类ByteToMessageDecoder +- 将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。 + +- 引用计数需要特别的注意。对于编码器和解码器来说,其过程 +- 也是相当的简单:一旦消息被编码或者解码,它就会被ReferenceCountUtil.release(message)调用自动释放。如果你需要保留引用以便稍后使用,那么你可以调用ReferenceCountUtil.retain(message)方法。这将会增加该引用计数,从而防止该消息被释放。 + +- 由于Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能 +- 让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提供了 +- TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。 +- 为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一 +- 个TooLongFrameException(随后会被ChannelHandler.exceptionCaught()方法捕 +- 获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。 + +## 抽象类MessageToByteEncoder + +- 这个类只有一个方法,而解码器有两个。原因是解码器通常需要在Channel 关闭之后产生最后一个消息(因此也就有了decodeLast()方法)。这显然不适用于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的。 +# 空闲的连接和超时 + +# TCP粘包与半包 +- 粘包: +- 应用程序Write写入的字节大小大于套接口发送缓冲区的大小。 +- 进行MSS大小的TCP分段。 +- 以太网帧的payload大于MTU进行IP分片。 +## 基于分隔符的协议 + +## 基于长度的协议 + +# 序列化 + + +# 传输方式 + + +- 注意Epoll! +- Linux作为高性能网络编程的平台,其重要性与日俱增,这催生了大量先进特性的开发,其 + - 中包括epoll——一个高度可扩展的I/O事件通知特性。这个API自Linux内核版本2.5.44(2002)被引入,提供了比旧的POSIX select和poll系统调用更好的性能,同时现在也是Linux上非阻塞网络编程的事实标准。Linux JDK NIO API使用了这些epoll调用。 +- Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并且以一种更加轻量的方式使用中断。 + +- Netty是如何能够使用和用于异步传输相同的API来支持OIO的呢。 +- 答案就是,Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeout Exception。Netty将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试。这实际上也是类似于Netty这样的异步框架能够支持OIO的唯一方式。 + + + +# ByteBuf +- Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。 +- Netty 的ByteBuffer 替代品是ByteBuf,一个强大的实现,既解决了JDK API 的局限性, +- 又为网络应用程序的开发者提供了更好的API。 +- Netty 的数据处理API 通过两个组件暴露——abstract class ByteBuf 和interface +- ByteBufHolder。 +- 下面是一些ByteBuf API 的优点: +- 它可以被用户自定义的缓冲区类型扩展; +- 通过内置的复合缓冲区类型实现了透明的零拷贝; +- 容量可以按需增长(类似于JDK 的StringBuilder); +- 在读和写这两种模式之间切换不需要调用ByteBuffer 的flip()方法; +- 读和写使用了不同的索引; +- 支持方法的链式调用; +- 支持引用计数; +- 支持池化。 +## 索引操作 +- ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf 读取时, +- 它的readerIndex 将会被递增已经被读取的字节数。同样地,当你写入ByteBuf 时,它的 +- writerIndex 也会被递增。图5-1 展示了一个空ByteBuf 的布局结构和状态。 + + +- 虽然ByteBuf 同时具有读索引和写索引,但是JDK 的ByteBuffer 却只有一个索引,这 +- 也就是为什么必须调用flip()方法来在读模式和写模式之间进行切换的原因。 + +- 如果打算读取字节直到readerIndex 达到和writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOfBoundsException。 +- 名称以read 或者write 开头的ByteBuf 方法,将会推进其对应的索引,而名称以set 或 +- 者get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。 +- 可以指定ByteBuf 的最大容量。试图移动写索引(即writerIndex)超过这个值将会触 +- 发一个异常。(默认的限制是Integer.MAX_VALUE。) + +- 通过调用discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex 中,会随着read 操作的执行而增加(get*操作不会移动readerIndex)。 + +- 可丢弃字节分段中的空间已经变为可写的了。注意,在调用discardReadBytes()之后,对 +- 可写分段的内容并没有任何的保证。 +- 虽然你可能会倾向于频繁地调用discardReadBytes()方法以确保可写分段的最大化,但是 +- 请注意,这将极有可能会导致内存复制,因为可读字节(图中标记为CONTENT 的部分)必须被移动到缓冲区的开始位置。我们建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。 + +- JDK 的InputStream 定义了mark(int readlimit)和reset()方法,这些方法分别 +- 被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。 +- 同样,可以通过调用markReaderIndex()、markWriterIndex()、resetWriterIndex() +- 和resetReaderIndex()来标记和重置ByteBuf 的readerIndex 和writerIndex。这些和 +- InputStream 上的调用类似,只是没有readlimit 参数来指定标记什么时候失效。 +- 也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。试 +- 图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException。 +- 可以通过调用clear()方法来将readerIndex 和writerIndex 都设置为0。注意,这 +- 并不会清除内存中的内容。 + +- 查找操作:indexOf或者ByteProcessor#process + +## 缓冲区 +### 堆缓冲区 +- 最常用的ByteBuf 模式是将数据存储在JVM 的堆空间中。这种模式被称为支撑数组 +- (backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式非常适合于有遗留的数据需要处理的情况。 + + +### 直接缓冲区 +- 直接缓冲区是另外一种ByteBuf 模式。我们期望用于对象创建的内存分配永远都来自于堆 +- 中,但这并不是必须的——NIO 在JDK 1.4 中引入的ByteBuffer 类允许JVM 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地I/O 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。 + + +### 复合缓冲区 +- 为多个ByteBuf 提供一个聚合视图。在这里你可以根据需要添加或者删除ByteBuf 实例。 +- Netty 通过一个ByteBuf 子类——CompositeByteBuf——实现了这个模式,它提供了一 +- 个将多个缓冲区表示为单个合并缓冲区的虚拟表示。 +- 警告:CompositeByteBuf 中的ByteBuf 实例可能同时包含直接内存分配和非直接内存分配。 +- 如果其中只有一个实例,那么对CompositeByteBuf 上的hasArray()方法的调用将返回该组 +- 件上的hasArray()方法的值;否则它将返回false。 + + +### 派生缓冲区 +- 派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方 +- 法被创建的: +- duplicate(); +- slice(); +- slice(int, int); +- Unpooled.unmodifiableBuffer(…); +- order(ByteOrder); +- readSlice(int)。 +- 每个这些方法都将返回一个新的ByteBuf 实例,它具有自己的读索引、写索引和标记 +- 索引。其内部存储和JDK 的ByteBuffer 一样也是共享的。这使得派生缓冲区的创建成本 +- 是很低廉的,但是这也意味着,如果你修改了它的内容,也同时修改了其对应的源实例,所以要小心。 +- ByteBuf 复制 如果需要一个现有缓冲区的真实副本,请使用copy()或者copy(int, int)方 +- 法。不同于派生缓冲区,由这个调用所返回的ByteBuf 拥有独立的数据副本。 + +## 读写操作 +- get()和set()操作,从给定的索引开始,并且保持索引不变; +- read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。 + + + +## ByteBufHolder +- 我们经常发现,除了实际的数据负载之外,我们还需要存储各种属性值。HTTP 响应便是一个很好的例子,除了表示为字节的内容,还包括状态码、cookie 等。 +- 为了处理这种常见的用例,Netty 提供了ByteBufHolder。ByteBufHolder 也为Netty 的 +- 高级特性提供了支持,如缓冲区池化,其中可以从池中借用ByteBuf,并且在需要时自动释放。 +- ByteBufHolder 只有几种用于访问底层数据和引用计数的方法。 + +## 分配 +### ByteBufAllocator 接口 +- 为了降低分配和释放内存的开销,Netty 通过interface ByteBufAllocator 实现了 +- (ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的ByteBuf 实例。使用池化是特定于应用程序的决定,其并不会以任何方式改变ByteBuf API(的语义)。 + +- 可以通过Channel(每个都可以有一个不同的ByteBufAllocator 实例)或者绑定到 +- ChannelHandler 的ChannelHandlerContext 获取一个到ByteBufAllocator 的引用。 + +- Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和Unpooled- +- ByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。 +- 虽然Netty默认使用了PooledByteBufAllocator,但这可以很容易地通过Channel- +- Config API或者在引导你的应用程序时指定一个不同的分配器来更改 +### Unpooled 缓冲区 +- 可能某些情况下,你未能获取一个到ByteBufAllocator 的引用。对于这种情况,Netty 提 +- 供了一个简单的称为Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf实例。 + +### ByteBufUtil 类 +- ByteBufUtil 提供了用于操作ByteBuf 的静态的辅助方法。因为这个API 是通用的,并 +- 且和池化无关,所以这些方法已然在分配类的外部实现。 +- 这些静态方法中最有价值的可能就是hexdump()方法,它以十六进制的表示形式打印 +- ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。 +- 另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf +- 实例的相等性。如果你实现自己的ByteBuf 子类,你可能会发现ByteBufUtil 的其他有用方法。 +## 引用计数 +- 引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时,释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第4 版中为ByteBuf 和ByteBufHolder 引入了 +- 引用计数技术,它们都实现了interface ReferenceCounted。 +- 引用计数背后的想法并不是特别的复杂;它主要涉及跟踪到某个特定对象的活动引用的数 +- 量。一个ReferenceCounted 实现的实例将通常以活动的引用计数为1 作为开始。只要引用计数大于0,就能保证对象不会被释放。当活动引用的数量减少到0 时,该实例就会被释放。注意,虽然释放的确切语义可能是特定于实现的,但是至少已经释放的对象应该不可再用了。 +- 引用计数对于池化实现(如PooledByteBufAllocator)来说是至关重要的,它降低了内存分配的开销。 + +- 试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCount- +- Exception。 +- 注意,一个特定的(ReferenceCounted 的实现)类,可以用它自己的独特方式来定义它 +- 的引用计数规则。例如,我们可以设想一个类,其release()方法的实现总是将引用计数设为零,而不用关心它的当前值,从而一次性地使所有的活动引用都失效。 + + +## 零拷贝 +1.- Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝. +2.- 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作. +3.- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝. +4.- 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题. + +### CompositeByteBuf +- 除了上面直接使用 CompositeByteBuf 类外, 我们还可以使用 Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操作, 因此使用起来更加方便: + +- + +# 客户端启动过程 +1.- EventLoopGroup: 不论是服务器端还是客户端, 都必须指定 EventLoopGroup. 在这个例子中, 指定了 NioEventLoopGroup, 表示一个 NIO 的EventLoopGroup. +2.- ChannelType: 指定 Channel 的类型. 因为是客户端, 因此使用了 NioSocketChannel. +3.- Handler: 设置数据的处理器. + + - 1) NioEventLoopGroup#constructor +1.- EventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children 数组, 其大小是 nThreads, 这样就构成了一个线程池 +2.- 如果我们在实例化 NioEventLoopGroup 时, 如果指定线程池大小, 则 nThreads 就是指定的值, 反之是处理器核心数 * 2 +3.- MultithreadEventExecutorGroup 中会调用 newChild 抽象方法来初始化 children 数组 +4.- 抽象方法 newChild 是在 NioEventLoopGroup 中实现的, 它返回一个 NioEventLoop 实例. +5.- NioEventLoop 属性: +6.- SelectorProvider provider 属性: NioEventLoopGroup 构造器中通过 SelectorProvider.provider() 获取一个 SelectorProvider +7.- Selector selector 属性: NioEventLoop 构造器中通过调用通过 selector = provider.openSelector() 获取一个 selector 对象. + + +``` +public NioEventLoopGroup() { + this(0); +} +``` + + + +``` +public NioEventLoopGroup(int nThreads) { + this(nThreads, (Executor) null); +} +``` + + + +``` +public NioEventLoopGroup(int nThreads, Executor executor) { + this(nThreads, executor, SelectorProvider.provider()); +} +``` + + + +``` +public NioEventLoopGroup( + int nThreads, Executor executor, final SelectorProvider selectorProvider) { + this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); +} +``` + + + +``` +public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); +} +``` + + +- protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { + super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); +} + + + +``` +private static final int DEFAULT_EVENT_LOOP_THREADS; + +static { + DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( + "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); + + if (logger.isDebugEnabled()) { + logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); + } +} +``` + + + +- protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) { + this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args); +} + + +``` +public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup { + + private final EventExecutor[] children; + private final Set<EventExecutor> readonlyChildren; + private final AtomicInteger terminatedChildren = new AtomicInteger(); + private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE); + private final EventExecutorChooserFactory.EventExecutorChooser chooser; +``` + +- } + + +``` +/** + * Create a new instance. + * + * @param nThreads the number of threads that will be used by this instance. + * @param executor the Executor to use, or {@code null} if the default should be used. + * @param chooserFactory the {@link EventExecutorChooserFactory} to use. + * @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call + */ +protected MultithreadEventExecutorGroup(int nThreads, Executor executor, + EventExecutorChooserFactory chooserFactory, Object... args) { + if (nThreads <= 0) { + throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads)); + } + + if (executor == null) { + executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); + } + + children = new EventExecutor[nThreads]; + + for (int i = 0; i < nThreads; i ++) { + boolean success = false; + try { + children[i] = newChild(executor, args); + success = true; + } catch (Exception e) { + // TODO: Think about if this is a good exception type + throw new IllegalStateException("failed to create a child event loop", e); + } finally { + if (!success) { + for (int j = 0; j < i; j ++) { + children[j].shutdownGracefully(); + } + + for (int j = 0; j < i; j ++) { + EventExecutor e = children[j]; + try { + while (!e.isTerminated()) { + e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + } + } catch (InterruptedException interrupted) { + // Let the caller handle the interruption. + Thread.currentThread().interrupt(); + break; + } + } + } + } + } + + chooser = chooserFactory.newChooser(children); + + final FutureListener<Object> terminationListener = new FutureListener<Object>() { + @Override + public void operationComplete(Future<Object> future) throws Exception { + if (terminatedChildren.incrementAndGet() == children.length) { + terminationFuture.setSuccess(null); + } + } + }; + + for (EventExecutor e: children) { + e.terminationFuture().addListener(terminationListener); + } + + Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length); + Collections.addAll(childrenSet, children); + readonlyChildren = Collections.unmodifiableSet(childrenSet); +} +``` + + + - 1.1) ThreadPerTaskExecutor#constructor(EventLoopGroup所依赖的) +- protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory(getClass()); +} + + +``` +public final class ThreadPerTaskExecutor implements Executor { + private final ThreadFactory threadFactory; + + public ThreadPerTaskExecutor(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException("threadFactory"); + } + this.threadFactory = threadFactory; + } + + @Override + public void execute(Runnable command) { + threadFactory.newThread(command).start(); + } +} +``` + + + - 1.2) NioEventLoopGroup#newChild(executor, args) +- protected EventLoop newChild(Executor executor, Object... args) throws Exception { + return new NioEventLoop(this, executor, (SelectorProvider) args[0], + ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); +} + + - 1.2.1) NioEventLoop#constructor + + +``` +private Selector selector; +private Selector unwrappedSelector; +private SelectedSelectionKeySet selectedKeys; + +private final SelectorProvider provider; + +/** + * Boolean that controls determines if a blocked Selector.select should + * break out of its selection process. In our case we use a timeout for + * the select method and the select method will block for that time unless + * waken up. + */ +private final AtomicBoolean wakenUp = new AtomicBoolean(); + +private final SelectStrategy selectStrategy; + +private volatile int ioRatio = 50; +private int cancelledKeys; +private boolean needsToSelectAgain; +``` + + + +- NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); + if (selectorProvider == null) { + throw new NullPointerException("selectorProvider"); + } + if (strategy == null) { + throw new NullPointerException("selectStrategy"); + } + provider = selectorProvider; + final SelectorTuple selectorTuple = openSelector(); + selector = selectorTuple.selector; + unwrappedSelector = selectorTuple.unwrappedSelector; + selectStrategy = strategy; +} + - 1.2.1.1) SingleThreadEventLoop#constructor +- protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, + boolean addTaskWakesUp, int maxPendingTasks, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); + tailTasks = newTaskQueue(maxPendingTasks); +} + - 1.2.1.1.1) SingleThreadEventExecutor#constructor +- protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedHandler) { + super(parent); + this.addTaskWakesUp = addTaskWakesUp; + this.maxPendingTasks = Math.max(16, maxPendingTasks); + this.executor = ObjectUtil.checkNotNull(executor, "executor"); + taskQueue = newTaskQueue(this.maxPendingTasks); + rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler"); +} + +- protected AbstractScheduledEventExecutor(EventExecutorGroup parent) { + super(parent); +} + +- protected AbstractEventExecutor(EventExecutorGroup parent) { + this.parent = parent; +} + - 1.2.1.1.2) SingleThreadEventExecutor#newTaskQueue + +``` +private final Queue<Runnable> tailTasks; +``` + + +- protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { + return new LinkedBlockingQueue<Runnable>(maxPendingTasks); +} + +#### 1.2.1.2) openSelector + +``` +private SelectorTuple openSelector() { + final Selector unwrappedSelector; + try { + unwrappedSelector = provider.openSelector(); + } catch (IOException e) { + throw new ChannelException("failed to open a new selector", e); + } + + if (DISABLE_KEYSET_OPTIMIZATION) { + return new SelectorTuple(unwrappedSelector); + } + + final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); + + Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + try { + return Class.forName( + "sun.nio.ch.SelectorImpl", + false, + PlatformDependent.getSystemClassLoader()); + } catch (Throwable cause) { + return cause; + } + } + }); + + if (!(maybeSelectorImplClass instanceof Class) || + // ensure the current selector implementation is what we can instrument. + !((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) { + if (maybeSelectorImplClass instanceof Throwable) { + Throwable t = (Throwable) maybeSelectorImplClass; + logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t); + } + return new SelectorTuple(unwrappedSelector); + } + + final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass; + + Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + try { + Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys"); + Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); + + Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true); + if (cause != null) { + return cause; + } + cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true); + if (cause != null) { + return cause; + } + + selectedKeysField.set(unwrappedSelector, selectedKeySet); + publicSelectedKeysField.set(unwrappedSelector, selectedKeySet); + return null; + } catch (NoSuchFieldException e) { + return e; + } catch (IllegalAccessException e) { + return e; + } + } + }); + + if (maybeException instanceof Exception) { + selectedKeys = null; + Exception e = (Exception) maybeException; + logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e); + return new SelectorTuple(unwrappedSelector); + } + selectedKeys = selectedKeySet; + logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector); + return new SelectorTuple(unwrappedSelector, + new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet)); +} +``` + + - 1.3) DefaultEventExecutorChooserFactory#newChooser + +``` +public EventExecutorChooser newChooser(EventExecutor[] executors) { + if (isPowerOfTwo(executors.length)) { + return new PowerOfTwoEventExecutorChooser(executors); + } else { + return new GenericEventExecutorChooser(executors); + } +} +``` + +- + + +``` +private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { + private final AtomicInteger idx = new AtomicInteger(); + private final EventExecutor[] executors; + + PowerOfTwoEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[idx.getAndIncrement() & executors.length - 1]; + } +} + +private static final class GenericEventExecutorChooser implements EventExecutorChooser { + private final AtomicInteger idx = new AtomicInteger(); + private final EventExecutor[] executors; + + GenericEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[Math.abs(idx.getAndIncrement() % executors.length)]; + } +} +``` + + + + - 2) AbstractBootstrap#group +- AbstractBootstrap + +``` +volatile EventLoopGroup group; +@SuppressWarnings("deprecation") +private volatile ChannelFactory<? extends C> channelFactory; +private volatile SocketAddress localAddress; +private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>(); +private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>(); +private volatile ChannelHandler handler; +``` + + + + +``` +public B group(EventLoopGroup group) { + if (group == null) { + throw new NullPointerException("group"); + } + if (this.group != null) { + throw new IllegalStateException("group set already"); + } + this.group = group; + return self(); +} +``` + + + - 3) AbstractBootstrap#channel(NioSocketChannel) + +``` +public B channel(Class<? extends C> channelClass) { + if (channelClass == null) { + throw new NullPointerException("channelClass"); + } + return channelFactory(new ReflectiveChannelFactory<C>(channelClass)); +} +``` + + + +- 3.1 + +``` +public ReflectiveChannelFactory(Class<? extends T> clazz) { + if (clazz == null) { + throw new NullPointerException("clazz"); + } + this.clazz = clazz; +} +``` + + +- 3.2 + +``` +public B channelFactory(io.netty.channel.ChannelFactory<? extends C> channelFactory) { + return channelFactory((ChannelFactory<C>) channelFactory); +} +``` + +- 3.2.1 + +``` +public B channelFactory(ChannelFactory<? extends C> channelFactory) { + if (channelFactory == null) { + throw new NullPointerException("channelFactory"); + } + if (this.channelFactory != null) { + throw new IllegalStateException("channelFactory set already"); + } + + this.channelFactory = channelFactory; + return self(); +} +``` + + + - 4) AbstractBootstrap#handler + +``` +public B handler(ChannelHandler handler) { + if (handler == null) { + throw new NullPointerException("handler"); + } + this.handler = handler; + return self(); +} +``` + + + - 5) AbstractBootstrap#option + +``` +private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>(); +``` + + + + +``` +public <T> B option(ChannelOption<T> option, T value) { + if (option == null) { + throw new NullPointerException("option"); + } + if (value == null) { + synchronized (options) { + options.remove(option); + } + } else { + synchronized (options) { + options.put(option, value); + } + } + return self(); +} +``` + + + - 6) Bootstrap#connect + +``` +public ChannelFuture connect(String inetHost, int inetPort) { + return connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); +} +``` + + + + +``` +public static InetSocketAddress createUnresolved(String host, int port) { + return new InetSocketAddress(checkPort(port), checkHost(host)); +} +``` + + + +``` +public ChannelFuture connect(SocketAddress remoteAddress) { + if (remoteAddress == null) { + throw new NullPointerException("remoteAddress"); + } + + validate(); + return doResolveAndConnect(remoteAddress, config.localAddress()); +} +``` + + + - 6.1) Bootstrap#doResolveAndConnect + + +``` +private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) { + final ChannelFuture regFuture = initAndRegister(); + final Channel channel = regFuture.channel(); + + if (regFuture.isDone()) { + if (!regFuture.isSuccess()) { + return regFuture; + } + return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise()); + } else { + // Registration future is almost always fulfilled already, but just in case it's not. + final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); + regFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + // Directly obtain the cause and do a null check so we only need one volatile read in case of a + // failure. + Throwable cause = future.cause(); + if (cause != null) { + // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an + // IllegalStateException once we try to access the EventLoop of the Channel. + promise.setFailure(cause); + } else { + // Registration was successful, so set the correct executor to use. + // See https://github.com/netty/netty/issues/2586 + promise.registered(); + doResolveAndConnect0(channel, remoteAddress, localAddress, promise); + } + } + }); + return promise; + } +} +``` + + - 6.1.1) AbstractBoostrap#initAndRegister(返回值是DefaultChannelPromise) + - final ChannelFuture initAndRegister() { + Channel channel = null; + try { + channel = channelFactory.newChannel(); + init(channel); + } catch (Throwable t) { + if (channel != null) { + // channel can be null if newChannel crashed (eg SocketException("too many open files")) + channel.unsafe().closeForcibly(); + // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor + return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); + } + // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor + return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t); + } + + ChannelFuture regFuture = config().group().register(channel); + if (regFuture.cause() != null) { + if (channel.isRegistered()) { + channel.close(); + } else { + channel.unsafe().closeForcibly(); + } + } + + // If we are here and the promise is not failed, it's one of the following cases: + // 1) If we attempted registration from the event loop, the registration has been completed at this point. + // i.e. It's safe to attempt bind() or connect() now because the channel has been registered. + // 2) If we attempted registration from the other thread, the registration request has been successfully + // added to the event loop's task queue for later execution. + // i.e. It's safe to attempt bind() or connect() now: + // because bind() or connect() will be executed *after* the scheduled registration task is executed + // because register(), bind(), and connect() are all bound to the same thread. + + return regFuture; +} + + - 6.1.1.1) (创建Channel和对应的pipeline)ReflectiveChannelFactory#newChannel + +``` +public T newChannel() { + try { + return clazz.getConstructor().newInstance(); + } catch (Throwable t) { + throw new ChannelException("Unable to create Channel from class " + clazz, t); + } +} +``` + + - 6.1.1.1.1) NioSocketChannel#construactor + +``` +private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider(); +``` + + + +``` +public NioSocketChannel() { + this(DEFAULT_SELECTOR_PROVIDER); +} +``` + + + +``` +public NioSocketChannel(SelectorProvider provider) { + this(newSocket(provider)); +} +``` + + + + +``` +private static SocketChannel newSocket(SelectorProvider provider) { + try { + /** + * Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in + * {@link SelectorProvider#provider()} which is called by each SocketChannel.open() otherwise. + * + * See <a href="https://github.com/netty/netty/issues/2308">#2308</a>. + */ + return provider.openSocketChannel(); + } catch (IOException e) { + throw new ChannelException("Failed to open a socket.", e); + } +} +``` + + + +``` +public NioSocketChannel(SocketChannel socket) { + this(null, socket); +} +``` + + + +``` +public NioSocketChannel(Channel parent, SocketChannel socket) { + super(parent, socket); + config = new NioSocketChannelConfig(this, socket.socket()); +} +``` + + +- protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) { + super(parent, ch, SelectionKey.OP_READ); + + + +``` +/** + * Create a new instance + * + * @param parent the parent {@link Channel} by which this instance was created. May be {@code null} + * @param ch the underlying {@link SelectableChannel} on which it operates + * @param readInterestOp the ops to set to receive data from the {@link SelectableChannel} + */ +protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { + super(parent); + this.ch = ch; + this.readInterestOp = readInterestOp; + try { + ch.configureBlocking(false); + } catch (IOException e) { + try { + ch.close(); + } catch (IOException e2) { + if (logger.isWarnEnabled()) { + logger.warn( + "Failed to close a partially initialized socket.", e2); + } + } + + throw new ChannelException("Failed to enter non-blocking mode.", e); + } +} +``` + + +- protected AbstractChannel(Channel parent) { + this.parent = parent; + id = newId(); + unsafe = newUnsafe(); + pipeline = newChannelPipeline(); +} + +- protected ChannelId newId() { + return DefaultChannelId.newInstance(); +} + +- protected DefaultChannelPipeline newChannelPipeline() { + return new DefaultChannelPipeline(this); +} + +- 双向链表结构: +- final AbstractChannelHandlerContext head; +final AbstractChannelHandlerContext tail; + +- protected DefaultChannelPipeline(Channel channel) { + this.channel = ObjectUtil.checkNotNull(channel, "channel"); + succeededFuture = new SucceededChannelFuture(channel, null); + voidPromise = new VoidChannelPromise(channel, true); + + tail = new TailContext(this); + head = new HeadContext(this); + + head.next = tail; + tail.prev = head; +} + - 6.1.1.2) (向pipeline中添加handler,并创建其对应的ChannelHandlerContext)Boostrap#init +- void init(Channel channel) throws Exception { + ChannelPipeline p = channel.pipeline(); + p.addLast(config.handler()); + + final Map<ChannelOption<?>, Object> options = options0(); + synchronized (options) { + setChannelOptions(channel, options, logger); + } + + final Map<AttributeKey<?>, Object> attrs = attrs0(); + synchronized (attrs) { + for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { + channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); + } + } +} + + - 6.1.1.2.1) DefaultChannelPipeline#addLast +- AbstractBootstrapConfig#handler + +``` +public final ChannelHandler handler() { + return bootstrap.handler(); +} +``` + + + +``` +public final ChannelPipeline addLast(ChannelHandler... handlers) { + return addLast(null, handlers); +} +``` + + + +``` +public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) { + if (handlers == null) { + throw new NullPointerException("handlers"); + } + + for (ChannelHandler h: handlers) { + if (h == null) { + break; + } + addLast(executor, null, h); + } + + return this; +} +``` + + + +``` +public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) { + final AbstractChannelHandlerContext newCtx; + synchronized (this) { + checkMultiplicity(handler); + + newCtx = newContext(group, filterName(name, handler), handler); + + addLast0(newCtx); + + // If the registered is false it means that the channel was not registered on an eventloop yet. + // In this case we add the context to the pipeline and add a task that will call + // ChannelHandler.handlerAdded(...) once the channel is registered. + if (!registered) { + newCtx.setAddPending(); + callHandlerCallbackLater(newCtx, true); + return this; + } + + EventExecutor executor = newCtx.executor(); + if (!executor.inEventLoop()) { + newCtx.setAddPending(); + executor.execute(new Runnable() { + @Override + public void run() { + callHandlerAdded0(newCtx); + } + }); + return this; + } + } +``` + +- return this; +} + + - 6.1.1.2.1.1) (创建context)DefaultChannelPipeline#newContext + +``` +private String filterName(String name, ChannelHandler handler) { + if (name == null) { + return generateName(handler); + } + checkDuplicateName(name); + return name; +} +``` + + + +``` +private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) { + return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler); +} +``` + + + +``` +private EventExecutor childExecutor(EventExecutorGroup group) { + if (group == null) { + return null; + } + Boolean pinEventExecutor = channel.config().getOption(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP); + if (pinEventExecutor != null && !pinEventExecutor) { + return group.next(); + } + Map<EventExecutorGroup, EventExecutor> childExecutors = this.childExecutors; + if (childExecutors == null) { + // Use size of 4 as most people only use one extra EventExecutor. + childExecutors = this.childExecutors = new IdentityHashMap<EventExecutorGroup, EventExecutor>(4); + } + // Pin one of the child executors once and remember it so that the same child executor + // is used to fire events for the same channel. + EventExecutor childExecutor = childExecutors.get(group); + if (childExecutor == null) { + childExecutor = group.next(); + childExecutors.put(group, childExecutor); + } + return childExecutor; +} +``` + + - 6.1.1.2.1.1.1) DefaultChannelHandlerContext#constructor +- DefaultChannelHandlerContext( + DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) { + super(pipeline, executor, name, isInbound(handler), isOutbound(handler)); + if (handler == null) { + throw new NullPointerException("handler"); + } + this.handler = handler; +} + + +``` +private static boolean isInbound(ChannelHandler handler) { + return handler instanceof ChannelInboundHandler; +} +``` + + + +``` +private static boolean isOutbound(ChannelHandler handler) { + return handler instanceof ChannelOutboundHandler; +} +``` + + +- AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name, + boolean inbound, boolean outbound) { + this.name = ObjectUtil.checkNotNull(name, "name"); + this.pipeline = pipeline; + this.executor = executor; + this.inbound = inbound; + this.outbound = outbound; + // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor. + ordered = executor == null || executor instanceof OrderedEventExecutor; +} + + + - 6.1.1.2.1.2) DefaultChannelPipeline#addLast0 + +``` +private void addLast0(AbstractChannelHandlerContext newCtx) { + AbstractChannelHandlerContext prev = tail.prev; + newCtx.prev = prev; + newCtx.next = tail; + prev.next = newCtx; + tail.prev = newCtx; +} +``` + + - 6.1.1.2.1.3) DefaultChannelPipeline#callHandlerCallbackLater +- added=true + +``` +private PendingHandlerCallback pendingHandlerCallbackHead; +``` + +- 挂起handler的callback是一个链表结构 + + +``` +private void callHandlerCallbackLater(AbstractChannelHandlerContext ctx, boolean added) { + assert !registered; + + PendingHandlerCallback task = added ? new PendingHandlerAddedTask(ctx) : new PendingHandlerRemovedTask(ctx); + PendingHandlerCallback pending = pendingHandlerCallbackHead; + if (pending == null) { + pendingHandlerCallbackHead = task; + } else { + // Find the tail of the linked-list. + while (pending.next != null) { + pending = pending.next; + } + pending.next = task; + } +} +``` + + +- PendingHandlerAddedTask(AbstractChannelHandlerContext ctx) { + super(ctx); +} + +- PendingHandlerCallback(AbstractChannelHandlerContext ctx) { + this.ctx = ctx; +} + - 6.1.1.3) MultithreadEventLoopGroup#register +1.- 首先在 AbstractBootstrap.initAndRegister中, 通过 group().register(channel), 调用 MultithreadEventLoopGroup.register 方法 +2.- 在MultithreadEventLoopGroup.register 中, 通过 next() 获取一个可用的 SingleThreadEventLoop, 然后调用它的 register +3.- 在 SingleThreadEventLoop.register 中, 通过 channel.unsafe().register(this, promise) 来获取 channel 的 unsafe() 底层操作对象, 然后调用它的 register. +4.- 在 AbstractUnsafe.register 方法中, 调用 register0 方法注册 Channel +5.- 在 AbstractUnsafe.register0 中, 调用 AbstractNioChannel.doRegister 方法 +6.- AbstractNioChannel.doRegister 方法通过 javaChannel().register(eventLoop().selector, 0, this) 将 Channel 对应的 Java NIO SockerChannel 注册到一个 eventLoop 的 Selector 中, 并且将当前 Channel 作为 attachment. + + +``` +public ChannelFuture register(Channel channel) { + return next().register(channel); +} +``` + + - 6.1.1.3.1) MultithreadEventLoopGroup#next + +``` +public EventLoop next() { + return (EventLoop) super.next(); +} +``` + + + +``` +public EventExecutor next() { + return chooser.next(); +} +``` + + +- DefaultEventExecutorChooserFactory#next(RR) + +``` +public EventExecutor next() { + return executors[idx.getAndIncrement() & executors.length - 1]; +} +``` + + + - 6.1.1.3.2) SingleThreadEventLoop#register(向EventLoop添加一个注册任务) +- 返回值是一个DefaultChannelPromise + +``` +public ChannelFuture register(Channel channel) { + return register(new DefaultChannelPromise(channel, this)); +} +``` + + + +``` +public DefaultChannelPromise(Channel channel, EventExecutor executor) { + super(executor); + this.channel = checkNotNull(channel, "channel"); +} +``` + + + +``` +public DefaultPromise(EventExecutor executor) { + this.executor = checkNotNull(executor, "executor"); +} +``` + + + +``` +public ChannelFuture register(final ChannelPromise promise) { + ObjectUtil.checkNotNull(promise, "promise"); + promise.channel().unsafe().register(this, promise); + return promise; +} +``` + + + - 6.1.1.3.2.1) AbstractChannel#register + +``` +public final void register(EventLoop eventLoop, final ChannelPromise promise) { + if (eventLoop == null) { + throw new NullPointerException("eventLoop"); + } + if (isRegistered()) { + promise.setFailure(new IllegalStateException("registered to an event loop already")); + return; + } + if (!isCompatible(eventLoop)) { + promise.setFailure( + new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName())); + return; + } + + AbstractChannel.this.eventLoop = eventLoop; + + if (eventLoop.inEventLoop()) { + register0(promise); + } else { + try { + eventLoop.execute(new Runnable() { + @Override + public void run() { + register0(promise); + } + }); + } catch (Throwable t) { + logger.warn( + "Force-closing a channel whose registration task was not accepted by an event loop: {}", + AbstractChannel.this, t); + closeForcibly(); + closeFuture.setClosed(); + safeSetFailure(promise, t); + } + } +} +``` + + + - 6.1.1.3.2.1.1) AbstractChannel#register0(将Channel注册到Selector中,初始化EventLoop线程,注册自定义Handler) + + +``` +private void register0(ChannelPromise promise) { + try { + // check if the channel is still open as it could be closed in the mean time when the register + // call was outside of the eventLoop + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + boolean firstRegistration = neverRegistered; + doRegister(); + neverRegistered = false; + registered = true; + + // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the + // user may already fire events through the pipeline in the ChannelFutureListener. + pipeline.invokeHandlerAddedIfNeeded(); + + safeSetSuccess(promise); +``` + +- // 添加自定义Handler + pipeline.fireChannelRegistered(); + // Only fire a channelActive if the channel has never been registered. This prevents firing + // multiple channel actives if the channel is deregistered and re-registered. + if (isActive()) { + if (firstRegistration) { + pipeline.fireChannelActive(); + } else if (config().isAutoRead()) { + // This channel was registered before and autoRead() is set. This means we need to begin read + // again so that we process inbound data. + // + // See https://github.com/netty/netty/issues/4805 + beginRead(); + } + } + } catch (Throwable t) { + // Close the channel directly to avoid FD leak. + closeForcibly(); + closeFuture.setClosed(); + safeSetFailure(promise, t); + } +} + - 6.1.1.3.2.1.1.1) AbstractNioChannel#doRegister(将Channel注册到Selector中) +- protected void doRegister() throws Exception { + boolean selected = false; + for (;;) { + try { + selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); + return; + } catch (CancelledKeyException e) { + if (!selected) { + // Force the Selector to select now as the "canceled" SelectionKey may still be + // cached and not removed because no Select.select(..) operation was called yet. + eventLoop().selectNow(); + selected = true; + } else { + // We forced a select operation on the selector before but the SelectionKey is still cached + // for whatever reason. JDK bug ? + throw e; + } + } + } +} + + - 6.1.1.3.2.1.1.1.1) AbstractSeletableChannel#register + +``` +public final SelectionKey register(Selector sel, int ops, + Object att) + throws ClosedChannelException +{ + synchronized (regLock) { + if (!isOpen()) + throw new ClosedChannelException(); + if ((ops & ~validOps()) != 0) + throw new IllegalArgumentException(); + if (blocking) + throw new IllegalBlockingModeException(); + SelectionKey k = findKey(sel); + if (k != null) { + k.interestOps(ops); + k.attach(att); + } + if (k == null) { + // New registration + synchronized (keyLock) { + if (!isOpen()) + throw new ClosedChannelException(); + k = ((AbstractSelector)sel).register(this, ops, att); + addKey(k); + } + } + return k; + } +} +``` + + + - 6.1.1.3.2.1.2) DefaultChannelPipeline#invokeHandlerAddedIfNeeded +- final void invokeHandlerAddedIfNeeded() { + assert channel.eventLoop().inEventLoop(); + if (firstRegistration) { + firstRegistration = false; + // We are now registered to the EventLoop. It's time to call the callbacks for the ChannelHandlers, + // that were added before the registration was done. + callHandlerAddedForAllHandlers(); + } +} + + +``` +private void callHandlerAddedForAllHandlers() { + final PendingHandlerCallback pendingHandlerCallbackHead; + synchronized (this) { + assert !registered; + + // This Channel itself was registered. + registered = true; + + pendingHandlerCallbackHead = this.pendingHandlerCallbackHead; + // Null out so it can be GC'ed. + this.pendingHandlerCallbackHead = null; + } + + // This must happen outside of the synchronized(...) block as otherwise handlerAdded(...) may be called while + // holding the lock and so produce a deadlock if handlerAdded(...) will try to add another handler from outside + // the EventLoop. + PendingHandlerCallback task = pendingHandlerCallbackHead; + while (task != null) { + task.execute(); + task = task.next; + } +} +``` + + + - 6.1.1.3.2.1.3) DefaultChannelPipeline#fireChannelRegistered(添加自定义Handler) + +``` +public final ChannelPipeline fireChannelRegistered() { + AbstractChannelHandlerContext.invokeChannelRegistered(head); + return this; +} +``` + + + +``` +static void invokeChannelRegistered(final AbstractChannelHandlerContext next) { + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeChannelRegistered(); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRegistered(); + } + }); + } +} +``` + + + +``` +private void invokeChannelRegistered() { + if (invokeHandler()) { + try { + ((ChannelInboundHandler) handler()).channelRegistered(this); + } catch (Throwable t) { + notifyHandlerException(t); + } + } else { + fireChannelRegistered(); + } +} +``` + + +- 下面要着重关注ChannelInitializer +- ChannelInitializer#channelRegistered + +``` +public final void channelRegistered(ChannelHandlerContext ctx) throws Exception { + // Normally this method will never be called as handlerAdded(...) should call initChannel(...) and remove + // the handler. + if (initChannel(ctx)) { + // we called initChannel(...) so we need to call now pipeline.fireChannelRegistered() to ensure we not + // miss an event. + ctx.pipeline().fireChannelRegistered(); + } else { + // Called initChannel(...) before which is the expected behavior, so just forward the event. + ctx.fireChannelRegistered(); + } +} +``` + + + +``` +private boolean initChannel(ChannelHandlerContext ctx) throws Exception { + if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance. + try { + initChannel((C) ctx.channel()); + } catch (Throwable cause) { + // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...). + // We do so to prevent multiple calls to initChannel(...). + exceptionCaught(ctx, cause); + } finally { +``` + +- // 将ChannelInitializer从pipeline中移除 + remove(ctx); + } + return true; + } + return false; +} + + +``` +private void remove(ChannelHandlerContext ctx) { + try { + ChannelPipeline pipeline = ctx.pipeline(); + if (pipeline.context(this) != null) { + pipeline.remove(this); + } + } finally { + initMap.remove(ctx); + } +} +``` + + + - 6.1.2) DefaultPromise#isDone + +``` +public boolean isDone() { + return isDone0(result); +} +``` + + + +``` +private static boolean isDone0(Object result) { + return result != null && result != UNCANCELLABLE; +} +``` + + - 6.1.3) Boostrap#doResolveAndConnect0(真正连接) + +``` +public final ChannelPromise newPromise() { + return new DefaultChannelPromise(channel); +} +``` + + + +``` +public DefaultChannelPromise(Channel channel) { + this.channel = checkNotNull(channel, "channel"); +} +``` + + + +``` +private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress, + final SocketAddress localAddress, final ChannelPromise promise) { + try { + final EventLoop eventLoop = channel.eventLoop(); + final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop); + + if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) { + // Resolver has no idea about what to do with the specified remote address or it's resolved already. + doConnect(remoteAddress, localAddress, promise); + return promise; + } + + final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress); + + if (resolveFuture.isDone()) { + final Throwable resolveFailureCause = resolveFuture.cause(); + + if (resolveFailureCause != null) { + // Failed to resolve immediately + channel.close(); + promise.setFailure(resolveFailureCause); + } else { + // Succeeded to resolve immediately; cached? (or did a blocking lookup) + doConnect(resolveFuture.getNow(), localAddress, promise); + } + return promise; + } + + // Wait until the name resolution is finished. + resolveFuture.addListener(new FutureListener<SocketAddress>() { + @Override + public void operationComplete(Future<SocketAddress> future) throws Exception { + if (future.cause() != null) { + channel.close(); + promise.setFailure(future.cause()); + } else { + doConnect(future.getNow(), localAddress, promise); + } + } + }); + } catch (Throwable cause) { + promise.tryFailure(cause); + } + return promise; +} +``` + + + - 6.1.3.1) AbstractAddressResolver#resolve + +``` +public final Future<T> resolve(SocketAddress address) { + if (!isSupported(checkNotNull(address, "address"))) { + // Address type not supported by the resolver + return executor().newFailedFuture(new UnsupportedAddressTypeException()); + } + + if (isResolved(address)) { + // Resolved already; no need to perform a lookup + @SuppressWarnings("unchecked") + final T cast = (T) address; + return executor.newSucceededFuture(cast); + } + + try { + @SuppressWarnings("unchecked") + final T cast = (T) address; + final Promise<T> promise = executor().newPromise(); + doResolve(cast, promise); + return promise; + } catch (Exception e) { + return executor().newFailedFuture(e); + } +} +``` + + + - 6.1.3.1.1) InetSocketAddressResolver#doResolve + +``` +protected void doResolve(final InetSocketAddress unresolvedAddress, final Promise<InetSocketAddress> promise) + throws Exception { + // Note that InetSocketAddress.getHostName() will never incur a reverse lookup here, + // because an unresolved address always has a host name. + nameResolver.resolve(unresolvedAddress.getHostName()) + .addListener(new FutureListener<InetAddress>() { + @Override + public void operationComplete(Future<InetAddress> future) throws Exception { + if (future.isSuccess()) { + promise.setSuccess(new InetSocketAddress(future.getNow(), unresolvedAddress.getPort())); + } else { + promise.setFailure(future.cause()); + } + } + }); +} +``` + + +- SimpleNameResolver#resolve + +``` +public final Future<T> resolve(String inetHost) { + final Promise<T> promise = executor().newPromise(); + return resolve(inetHost, promise); +} +``` + + + +``` +public Future<T> resolve(String inetHost, Promise<T> promise) { + checkNotNull(promise, "promise"); + + try { + doResolve(inetHost, promise); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } +} +``` + + +- protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception { + try { + promise.setSuccess(SocketUtils.addressByName(inetHost)); + } catch (UnknownHostException e) { + promise.setFailure(e); + } +} + + +``` +public static InetAddress addressByName(final String hostname) throws UnknownHostException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<InetAddress>() { + @Override + public InetAddress run() throws UnknownHostException { + return InetAddress.getByName(hostname); + } + }); + } catch (PrivilegedActionException e) { + throw (UnknownHostException) e.getCause(); + } +} +``` + + + - 6.1.3.2) Bootstrap#doConnect(向eventLoop中添加一个connect的任务) + + +``` +private static void doConnect( + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) { + + // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up + // the pipeline in its channelRegistered() implementation. + final Channel channel = connectPromise.channel(); + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (localAddress == null) { + channel.connect(remoteAddress, connectPromise); + } else { + channel.connect(remoteAddress, localAddress, connectPromise); + } + connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } + }); +} +``` + + + - 6.1.3.2.1) AbstractChannel#connect + +``` +public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return pipeline.connect(remoteAddress, promise); +} +``` + +- DefaultChannelPipeline#connect + +``` +public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return tail.connect(remoteAddress, promise); +} +``` + + - 6.1.3.2.1.1) AbstractChannelHandlerContext#connect + +``` +public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return connect(remoteAddress, null, promise); +} +``` + + + +``` +public ChannelFuture connect( + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { + + if (remoteAddress == null) { + throw new NullPointerException("remoteAddress"); + } + if (isNotValidPromise(promise, false)) { + // cancelled + return promise; + } + // 从pipeline的后面往前面遍历,找到第一个outBoundHandler + final AbstractChannelHandlerContext next = findContextOutbound(); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeConnect(remoteAddress, localAddress, promise); + } else { + safeExecute(executor, new Runnable() { + @Override + public void run() { + next.invokeConnect(remoteAddress, localAddress, promise); + } + }, promise, null); + } + return promise; +} +``` + + - 6.1.3.2.1.1.1) AbstractChannelHandlerContext#invokeConnect +- HeadContext 重写了 connect 方法, 因此实际上调用的是 HeadContext.connect. + +``` +private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + if (invokeHandler()) { + try { + ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise); + } catch (Throwable t) { + notifyOutboundHandlerException(t, promise); + } + } else { + connect(remoteAddress, localAddress, promise); + } +} +``` + + - 6.1.3.2.1.1.1.1) DefaultChannelPipeline#connect +- Channel 的 unsafe 字段是 AbstractNioByteChannel.NioByteUnsafe 内部类. + +``` +public void connect( + ChannelHandlerContext ctx, + SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + unsafe.connect(remoteAddress, localAddress, promise); +} +``` + + - 6.1.3.2.1.1.1.1.1) NioSocketChannel#connect + +``` +public final void connect( + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + + try { + if (connectPromise != null) { + // Already a connect in process. + throw new ConnectionPendingException(); + } + + boolean wasActive = isActive(); + if (doConnect(remoteAddress, localAddress)) { + fulfillConnectPromise(promise, wasActive); + } else { + connectPromise = promise; + requestedRemoteAddress = remoteAddress; + + // Schedule connect timeout. + int connectTimeoutMillis = config().getConnectTimeoutMillis(); + if (connectTimeoutMillis > 0) { + connectTimeoutFuture = eventLoop().schedule(new Runnable() { + @Override + public void run() { + ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise; + ConnectTimeoutException cause = + new ConnectTimeoutException("connection timed out: " + remoteAddress); + if (connectPromise != null && connectPromise.tryFailure(cause)) { + close(voidPromise()); + } + } + }, connectTimeoutMillis, TimeUnit.MILLISECONDS); + } + + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isCancelled()) { + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + close(voidPromise()); + } + } + }); + } + } catch (Throwable t) { + promise.tryFailure(annotateConnectException(t, remoteAddress)); + closeIfClosed(); + } +} +``` + + +- protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (localAddress != null) { + doBind0(localAddress); + } + + boolean success = false; + try { + boolean connected = SocketUtils.connect(javaChannel(), remoteAddress); + if (!connected) { + selectionKey().interestOps(SelectionKey.OP_CONNECT); + } + success = true; + return connected; + } finally { + if (!success) { + doClose(); + } + } +} + + - 6.1.3.2.1.1.1.1.1.1) SocketUtils#connect + +``` +public static boolean connect(final SocketChannel socketChannel, final SocketAddress remoteAddress) + throws IOException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() { + @Override + public Boolean run() throws IOException { + return socketChannel.connect(remoteAddress); + } + }); + } catch (PrivilegedActionException e) { + throw (IOException) e.getCause(); + } +} +``` + + +- + +# 服务器启动过程 + - 1) NioEventLoopGroup#constructor +- 同客户端的这部分 + - 2) ServerBootstrap#group + +``` +private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>(); +private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>(); +private final ServerBootstrapConfig config = new ServerBootstrapConfig(this); +private volatile EventLoopGroup childGroup; +private volatile ChannelHandler childHandler; +``` + + + +``` +public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { + super.group(parentGroup); + if (childGroup == null) { + throw new NullPointerException("childGroup"); + } + if (this.childGroup != null) { + throw new IllegalStateException("childGroup set already"); + } + this.childGroup = childGroup; + return this; +} +``` + + + - 2.1) AbstractGroup#group + +``` +public B group(EventLoopGroup group) { + if (group == null) { + throw new NullPointerException("group"); + } + if (this.group != null) { + throw new IllegalStateException("group set already"); + } + this.group = group; + return self(); +} +``` + + + - 3) AbstractBoostrap#channel +- 同客户端的这部分 + - 4) ServerBoostrap#childHandler + +``` +public ServerBootstrap childHandler(ChannelHandler childHandler) { + if (childHandler == null) { + throw new NullPointerException("childHandler"); + } + this.childHandler = childHandler; + return this; +} +``` + + + - 5) AbstractBootstrap#option +- 同客户端的这部分 + - 6) AbstractBoostrap#bind + + +``` +public ChannelFuture bind(String inetHost, int inetPort) { + return bind(SocketUtils.socketAddress(inetHost, inetPort)); +} +``` + + + +``` +public ChannelFuture bind(SocketAddress localAddress) { + validate(); + if (localAddress == null) { + throw new NullPointerException("localAddress"); + } + return doBind(localAddress); +} +``` + + - 6.1) AbstractBoostrap#doBind + +``` +private ChannelFuture doBind(final SocketAddress localAddress) { +``` + + +``` +// 同客户端的6.1.1,但部分代码有重写 + final ChannelFuture regFuture = initAndRegister(); + final Channel channel = regFuture.channel(); + if (regFuture.cause() != null) { + return regFuture; + } + + if (regFuture.isDone()) { + // At this point we know that the registration was complete and successful. + ChannelPromise promise = channel.newPromise(); + doBind0(regFuture, channel, localAddress, promise); + return promise; + } else { + // Registration future is almost always fulfilled already, but just in case it's not. + final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); + regFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + Throwable cause = future.cause(); + if (cause != null) { + // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an + // IllegalStateException once we try to access the EventLoop of the Channel. + promise.setFailure(cause); + } else { + // Registration was successful, so set the correct executor to use. + // See https://github.com/netty/netty/issues/2586 + promise.registered(); + + doBind0(regFuture, channel, localAddress, promise); + } + } + }); + return promise; + } +} +``` + + - 6.1.1) AbstractBootstrap#initAndRegister +- ServerBootstrap重写了init方法 + +``` +void init(Channel channel) throws Exception { + final Map<ChannelOption<?>, Object> options = options0(); + synchronized (options) { + setChannelOptions(channel, options, logger); + } + + final Map<AttributeKey<?>, Object> attrs = attrs0(); + synchronized (attrs) { + for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) { + @SuppressWarnings("unchecked") + AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); + channel.attr(key).set(e.getValue()); + } + } + + ChannelPipeline p = channel.pipeline(); + + final EventLoopGroup currentChildGroup = childGroup; + final ChannelHandler currentChildHandler = childHandler; + final Entry<ChannelOption<?>, Object>[] currentChildOptions; + final Entry<AttributeKey<?>, Object>[] currentChildAttrs; + synchronized (childOptions) { + currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size())); + } + synchronized (childAttrs) { + currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size())); + } + + p.addLast(new ChannelInitializer<Channel>() { + @Override + public void initChannel(final Channel ch) throws Exception { + final ChannelPipeline pipeline = ch.pipeline(); + ChannelHandler handler = config.handler(); + if (handler != null) { + pipeline.addLast(handler); + } + + ch.eventLoop().execute(new Runnable() { + @Override + public void run() { + pipeline.addLast(new ServerBootstrapAcceptor( + ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); + } + }); + } + }); +} +``` + + +- 这里为pipeline添加了一个ServerBootstrapAcceptor。 +#### ServerBootstrapAcceptor +- ServerBootstrapAcceptor 中的 childGroup 是构造此对象是传入的 currentChildGroup, 即我们的 workerGroup, 而 Channel 是一个 NioSocketChannel 的实例, 因此这里的 childGroup.register 就是将 workerGroup 中的每个 EventLoop 和 NioSocketChannel 关联了。 +- 在服务器 NioServerSocketChannel 的 pipeline 中添加的是 handler 与 ServerBootstrapAcceptor. +- 当有新的客户端连接请求时, ServerBootstrapAcceptor.channelRead 中负责新建此连接的 NioSocketChannel 并添加 childHandler 到 NioSocketChannel 对应的 pipeline 中, 并将此 channel 绑定到 workerGroup 中的某个 eventLoop 中. +- handler 是在 accept 阶段起作用, 它处理客户端的连接请求. +- childHandler 是在客户端连接建立以后起作用, 它负责客户端连接的 IO 交互. + + + +``` +private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter { + + private final EventLoopGroup childGroup; + private final ChannelHandler childHandler; + private final Entry<ChannelOption<?>, Object>[] childOptions; + private final Entry<AttributeKey<?>, Object>[] childAttrs; + private final Runnable enableAutoReadTask; + + ServerBootstrapAcceptor( + final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler, + Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) { + this.childGroup = childGroup; + this.childHandler = childHandler; + this.childOptions = childOptions; + this.childAttrs = childAttrs; + + // Task which is scheduled to re-enable auto-read. + // It's important to create this Runnable before we try to submit it as otherwise the URLClassLoader may + // not be able to load the class because of the file limit it already reached. + // + // See https://github.com/netty/netty/issues/1328 + enableAutoReadTask = new Runnable() { + @Override + public void run() { + channel.config().setAutoRead(true); + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public void channelRead(ChannelHandlerContext ctx, Object msg) { +``` + +- // 客户端的连接 + final Channel child = (Channel) msg; + // 为其加入handler + child.pipeline().addLast(childHandler); + + setChannelOptions(child, childOptions, logger); + + for (Entry<AttributeKey<?>, Object> e: childAttrs) { + child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); + } + + try { +- // 类似于客户端中的将客户端连接注册到eventLoopGroup中,实际上是将channel绑定到一个eventLoop中 + +``` +// 见客户端部分的6.1.1.3 + childGroup.register(child).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + forceClose(child, future.cause()); + } + } + }); + } catch (Throwable t) { + forceClose(child, t); + } + } + + private static void forceClose(Channel child, Throwable t) { + child.unsafe().closeForcibly(); + logger.warn("Failed to register an accepted channel: {}", child, t); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + final ChannelConfig config = ctx.channel().config(); + if (config.isAutoRead()) { + // stop accept new connections for 1 second to allow the channel to recover + // See https://github.com/netty/netty/issues/1328 + config.setAutoRead(false); + ctx.channel().eventLoop().schedule(enableAutoReadTask, 1, TimeUnit.SECONDS); + } + // still let the exceptionCaught event flow through the pipeline to give the user + // a chance to do something with it + ctx.fireExceptionCaught(cause); + } +} +``` + +- 那么现在的问题是, ServerBootstrapAcceptor.channelRead 方法是怎么被调用的呢? 其实当一个 client 连接到 server 时, Java 底层的 NIO ServerSocketChannel 会有一个 SelectionKey.OP_ACCEPT 就绪事件, 接着就会调用到 NioServerSocketChannel.doReadMessages。 +- 在 doReadMessages 中, 通过 javaChannel().accept() 获取到客户端新连接的 SocketChannel, 接着就实例化一个 NioSocketChannel, 并且传入 NioServerSocketChannel 对象(即 this), 由此可知, 我们创建的这个 NioSocketChannel 的父 Channel 就是 NioServerSocketChannel 实例。 +- 接下来就经由 Netty 的 ChannelPipeline 机制, 将读取事件逐级发送到各个 handler 中, 于是就会触发前面我们提到的 ServerBootstrapAcceptor.channelRead 方法。 + - 6.1.2) AbstractBootstrap#doBind0 + +``` +private static void doBind0( + final ChannelFuture regFuture, final Channel channel, + final SocketAddress localAddress, final ChannelPromise promise) { + + // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up + // the pipeline in its channelRegistered() implementation. + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (regFuture.isSuccess()) { + channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + promise.setFailure(regFuture.cause()); + } + } + }); +} +``` + + - 6.1.2.1) DefaultChannelPipeline#bind + +``` +public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { + return tail.bind(localAddress, promise); +} +``` + + - 6.1.2.1.1) AbstractChannelHandlerContext#bind + +``` +public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) { + if (localAddress == null) { + throw new NullPointerException("localAddress"); + } + if (isNotValidPromise(promise, false)) { + // cancelled + return promise; + } + + final AbstractChannelHandlerContext next = findContextOutbound(); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeBind(localAddress, promise); + } else { + safeExecute(executor, new Runnable() { + @Override + public void run() { + next.invokeBind(localAddress, promise); + } + }, promise, null); + } + return promise; +} +``` + + + - 6.1.2.1.1.1) DefaultChannelPipne#bind + +``` +public void bind( + ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) + throws Exception { + unsafe.bind(localAddress, promise); +} +``` + + + - 6.1.2.1.1.1.1) AbstractChannel#bind + +``` +public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { + assertEventLoop(); + + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + + // See: https://github.com/netty/netty/issues/576 + if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) && + localAddress instanceof InetSocketAddress && + !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() && + !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) { + // Warn a user about the fact that a non-root user can't receive a + // broadcast packet on *nix if the socket is bound on non-wildcard address. + logger.warn( + "A non-root user can't receive a broadcast packet if the socket " + + "is not bound to a wildcard address; binding to a non-wildcard " + + "address (" + localAddress + ") anyway as requested."); + } + + boolean wasActive = isActive(); + try { + doBind(localAddress); + } catch (Throwable t) { + safeSetFailure(promise, t); + closeIfClosed(); + return; + } + + if (!wasActive && isActive()) { + invokeLater(new Runnable() { + @Override + public void run() { + pipeline.fireChannelActive(); + } + }); + } + + safeSetSuccess(promise); +} +``` + + - 6.1.2.1.1.1.1.1) NioServerSocketChannel#doBind + - protected void doBind(SocketAddress localAddress) throws Exception { + if (PlatformDependent.javaVersion() >= 7) { + javaChannel().bind(localAddress, config.getBacklog()); + } else { + javaChannel().socket().bind(localAddress, config.getBacklog()); + } +} + +- + +- + +# EventLoop +- NioEventLoop在其父类SingleThreadEventExecutor中有一个thread属性,代表了该EventLoop所绑定的线程。 +- 那么该线程是如何启动的呢? +# 启动 + - 6.1.1.3.2.1) AbstractChannel#register中调用了executor.execute(),这里的executor就是NioEventLoop。 + - 1) SingleThreadEventExecutor#execute +- 操作系统会给每个线程分配一小段CPU时间,得到调度的线程可以占有一段时间的CPU时间,而时间用完了就需要再次排队等候调度,SingleThreadEventExecutor将分配给他的线程存储起来,只是作为一种标志,在进行任务执行之前,它都会进行一次判断,当前线程是否处于分配给自己的线程之中,如果不在,那么就不能执行。当然这种管理是在存在多个SingleThreadEventExecutor的时候进行的,在当个SingleThreadEventExecutor内部,如果在执行任务之前判断发现当前线程不在分配给自己的线程之中,那么只有一种可能,那就是当前的SingleThreadEventExecutor还没有被分配线程,那么就执行上面提到的startThread方法来分配一个线程给自己。 + + +``` +public void execute(Runnable task) { + if (task == null) { + throw new NullPointerException("task"); + } + + boolean inEventLoop = inEventLoop(); + if (inEventLoop) { + addTask(task); + } else { + startThread(); + addTask(task); + if (isShutdown() && removeTask(task)) { + reject(); + } + } + + if (!addTaskWakesUp && wakesUpForTask(task)) { + wakeup(inEventLoop); + } +} +``` + + + +``` +public boolean inEventLoop() { + return inEventLoop(Thread.currentThread()); +} +``` + + + +``` +public boolean inEventLoop(Thread thread) { + return thread == this.thread; +} +``` + + + - 1.1) SingleThreadEventExecutor#startThread(进入事件循环) + +``` +private void startThread() { + if (state == ST_NOT_STARTED) { + if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { + try { + doStartThread(); + } catch (Throwable cause) { + STATE_UPDATER.set(this, ST_NOT_STARTED); + PlatformDependent.throwException(cause); + } + } + } +} +``` + + + +``` +private void doStartThread() { + assert thread == null; +``` + + +``` +// 在一个新线程中执行该runnable + executor.execute(new Runnable() { + @Override + public void run() { +``` + +- // 得到线程 + thread = Thread.currentThread(); + if (interrupted) { + thread.interrupt(); + } + + boolean success = false; + updateLastExecutionTime(); + try { +- // 开始事件循环 + SingleThreadEventExecutor.this.run(); + success = true; + } catch (Throwable t) { + logger.warn("Unexpected exception from an event executor: ", t); + } finally { + for (;;) { + int oldState = state; + if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet( + SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) { + break; + } + } + + // Check if confirmShutdown() was called at the end of the loop. + if (success && gracefulShutdownStartTime == 0) { + logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " + + SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " + + "before run() implementation terminates."); + } + + try { + // Run all remaining tasks and shutdown hooks. + for (;;) { + if (confirmShutdown()) { + break; + } + } + } finally { + try { + cleanup(); + } finally { + STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED); + threadLock.release(); + if (!taskQueue.isEmpty()) { + logger.warn( + "An event executor terminated with " + + "non-empty task queue (" + taskQueue.size() + ')'); + } + + terminationFuture.setSuccess(null); + } + } + } + } + }); +} + + - 1.1.1) ThreadPerTaskExecutor#execute(创建EventLoop所对应线程) + +``` +public void execute(Runnable command) { + threadFactory.newThread(command).start(); +} +``` + + +- DefaultThreadFactory#newThread + +``` +public Thread newThread(Runnable r) { + Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet()); + try { + if (t.isDaemon() != daemon) { + t.setDaemon(daemon); + } + + if (t.getPriority() != priority) { + t.setPriority(priority); + } + } catch (Exception ignored) { + // Doesn't matter even if failed to set. + } + return t; +} +``` + + +- static Runnable wrap(Runnable runnable) { + return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable); +} + +- protected Thread newThread(Runnable r, String name) { + return new FastThreadLocalThread(threadGroup, r, name); +} + + +``` +public FastThreadLocalThread(ThreadGroup group, Runnable target, String name) { + super(group, FastThreadLocalRunnable.wrap(target), name); + cleanupFastThreadLocals = true; +} +``` + + +``` +public Thread(ThreadGroup group, Runnable target, String name) { + init(group, target, name, 0); +} +``` + + +- 在该新线程执行时,会调用NioEventLoop#run方法 + - 1.1.1) NioEventLoop#run(开始事件循环) + - protected void run() { + for (;;) { + try { + switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { + case SelectStrategy.CONTINUE: + continue; + case SelectStrategy.SELECT: + select(wakenUp.getAndSet(false)); + + // 'wakenUp.compareAndSet(false, true)' is always evaluated + // before calling 'selector.wakeup()' to reduce the wake-up + // overhead. (Selector.wakeup() is an expensive operation.) + // + // However, there is a race condition in this approach. + // The race condition is triggered when 'wakenUp' is set to + // true too early. + // + // 'wakenUp' is set to true too early if: + // 1) Selector is waken up between 'wakenUp.set(false)' and + // 'selector.select(...)'. (BAD) + // 2) Selector is waken up between 'selector.select(...)' and + // 'if (wakenUp.get()) { ... }'. (OK) + // + // In the first case, 'wakenUp' is set to true and the + // following 'selector.select(...)' will wake up immediately. + // Until 'wakenUp' is set to false again in the next round, + // 'wakenUp.compareAndSet(false, true)' will fail, and therefore + // any attempt to wake up the Selector will fail, too, causing + // the following 'selector.select(...)' call to block + // unnecessarily. + // + // To fix this problem, we wake up the selector again if wakenUp + // is true immediately after selector.select(...). + // It is inefficient in that it wakes up the selector for both + // the first case (BAD - wake-up required) and the second case + // (OK - no wake-up required). + + if (wakenUp.get()) { + selector.wakeup(); + } + // fall through + default: + } + + cancelledKeys = 0; + needsToSelectAgain = false; + final int ioRatio = this.ioRatio; +- // Netty中可以设置用于I/O操作和非I/O操作的时间占比,默认各位50%,也就是说,如果某次I/O操作的时间花了100ms,那么这次循环中非I/O得任务也可以花费100ms。Netty中的I/O时间处理通过processSelectedKeys方法来进行,而非I/O操作通过runAllTasks反复来进行 +- if (ioRatio == 100) { + try { + processSelectedKeys(); + } finally { + // Ensure we always run tasks. + runAllTasks(); + } + } else { + final long ioStartTime = System.nanoTime(); + try { + processSelectedKeys(); + } finally { + // Ensure we always run tasks. + final long ioTime = System.nanoTime() - ioStartTime; + runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + } + } + } catch (Throwable t) { + handleLoopException(t); + } + // Always handle shutdown even if the loop processing threw an exception. + try { + if (isShuttingDown()) { + closeAll(); + if (confirmShutdown()) { + return; + } + } + } catch (Throwable t) { + handleLoopException(t); + } + } +} + +# 任务添加 +- 每个SingleThreadEventExecutor仅有一个线程来进行事件处理,而每个SingleThreadEventExecutor包含了一个任务队列,所有需要该SingleThreadEventExecutor处理的任务都需要添加到该任务队列中去,添加的任务会在事件循环的过程中被不断消费,下面来看一下一个任务时如何被SingleThreadEventExecutor执行的。 + +- 其中持有了一个任务队列,类型为LinkedBlockingQueue。 +- SingleThreadEventExecutor#addTask +- protected void addTask(Runnable task) { + if (task == null) { + throw new NullPointerException("task"); + } + if (!offerTask(task)) { + reject(task); + } +} + +- final boolean offerTask(Runnable task) { + if (isShutdown()) { + reject(); + } + return taskQueue.offer(task); +} + +- protected final void reject(Runnable task) { + rejectedExecutionHandler.rejected(task, this); +} + +# 任务执行 +- 通过 Selector.open() 打开一个 Selector. +- 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set) +- 不断重复: +- 调用 select() 方法 +- 调用 selector.selectedKeys() 获取 selected keys +- 迭代每个 selected key: + + - 1) 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话) + - 2) 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()" 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中. + - 3) 根据需要更改 selected key 的监听事件. + - 4) 将已经处理过的 key 从 selected keys 集合中删除. + +- 在NioEventLoop#run中,会先processSelectedKeys,然后runAllTasks。 + - 1) NioEventLoop#processSelectedKeys(处理IO事件) + +``` +private void processSelectedKeys() { + if (selectedKeys != null) { + processSelectedKeysOptimized(); + } else { + processSelectedKeysPlain(selector.selectedKeys()); + } +} +``` + + + +``` +private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) { + // check if the set is empty and if so just return to not create garbage by + // creating a new Iterator every time even if there is nothing to process. + // See https://github.com/netty/netty/issues/597 + if (selectedKeys.isEmpty()) { + return; + } + + Iterator<SelectionKey> i = selectedKeys.iterator(); + for (;;) { + final SelectionKey k = i.next(); + final Object a = k.attachment(); + i.remove(); + + if (a instanceof AbstractNioChannel) { + processSelectedKey(k, (AbstractNioChannel) a); + } else { + @SuppressWarnings("unchecked") + NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a; + processSelectedKey(k, task); + } + + if (!i.hasNext()) { + break; + } + + if (needsToSelectAgain) { + selectAgain(); + selectedKeys = selector.selectedKeys(); + + // Create the iterator again to avoid ConcurrentModificationException + if (selectedKeys.isEmpty()) { + break; + } else { + i = selectedKeys.iterator(); + } + } + } +} +``` + + + +``` +private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { + final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); + if (!k.isValid()) { + final EventLoop eventLoop; + try { + eventLoop = ch.eventLoop(); + } catch (Throwable ignored) { + // If the channel implementation throws an exception because there is no event loop, we ignore this + // because we are only trying to determine if ch is registered to this event loop and thus has authority + // to close ch. + return; + } + // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop + // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is + // still healthy and should not be closed. + // See https://github.com/netty/netty/issues/5125 + if (eventLoop != this || eventLoop == null) { + return; + } + // close the channel if the key is not valid anymore + unsafe.close(unsafe.voidPromise()); + return; + } + + try { + int readyOps = k.readyOps(); + // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise + // the NIO JDK channel implementation may throw a NotYetConnectedException. + if ((readyOps & SelectionKey.OP_CONNECT) != 0) { + // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking + // See https://github.com/netty/netty/issues/924 + int ops = k.interestOps(); + ops &= ~SelectionKey.OP_CONNECT; + k.interestOps(ops); + + unsafe.finishConnect(); + } + + // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. + if ((readyOps & SelectionKey.OP_WRITE) != 0) { + // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write + ch.unsafe().forceFlush(); + } + + // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead + // to a spin loop + if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { + unsafe.read(); + } + } catch (CancelledKeyException ignored) { + unsafe.close(unsafe.voidPromise()); + } +} +``` + + - 1.1) AbstractNioChannel#finishConnect(关闭连接) + +``` +public final void finishConnect() { + // Note this method is invoked by the event loop only if the connection attempt was + // neither cancelled nor timed out. + + assert eventLoop().inEventLoop(); + + try { + boolean wasActive = isActive(); + doFinishConnect(); + fulfillConnectPromise(connectPromise, wasActive); + } catch (Throwable t) { + fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress)); + } finally { + // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used + // See https://github.com/netty/netty/issues/1770 + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + } +} +``` + + + - 1.1.1) NioSocketChannel#doFinishConnect +- protected void doFinishConnect() throws Exception { + if (!javaChannel().finishConnect()) { + throw new Error(); + } +} + - 1.1.2) AbstractNioChannel#fulfillConnectPromise + +``` +private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) { + if (promise == null) { + // Closed via cancellation and the promise has been notified already. + return; + } + + // Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel. + // We still need to ensure we call fireChannelActive() in this case. + boolean active = isActive(); + + // trySuccess() will return false if a user cancelled the connection attempt. + boolean promiseSet = promise.trySuccess(); + + // Regardless if the connection attempt was cancelled, channelActive() event should be triggered, + // because what happened is what happened. + if (!wasActive && active) { + pipeline().fireChannelActive(); + } + + // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive(). + if (!promiseSet) { + close(voidPromise()); + } +} +``` + + + - 1.2) AbstractNioChannel#forceFlush(处理写事件) + +``` +public final void forceFlush() { + // directly call super.flush0() to force a flush now + super.flush0(); +} +``` + + + +``` +protected void flush0() { + if (inFlush0) { + // Avoid re-entrance + return; + } + + final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; + if (outboundBuffer == null || outboundBuffer.isEmpty()) { + return; + } + + inFlush0 = true; + + // Mark all pending write requests as failure if the channel is inactive. + if (!isActive()) { + try { + if (isOpen()) { + outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true); + } else { + // Do not trigger channelWritabilityChanged because the channel is closed already. + outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } + } finally { + inFlush0 = false; + } + return; + } + + try { + doWrite(outboundBuffer); + } catch (Throwable t) { + if (t instanceof IOException && config().isAutoClose()) { + /** + * Just call {@link #close(ChannelPromise, Throwable, boolean)} here which will take care of + * failing all flushed messages and also ensure the actual close of the underlying transport + * will happen before the promises are notified. + * + * This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()} + * may still return {@code true} even if the channel should be closed as result of the exception. + */ + close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } else { + try { + shutdownOutput(voidPromise(), t); + } catch (Throwable t2) { + close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } + } + } finally { + inFlush0 = false; + } +} +``` + + - 1.2.1) NioSocketChannel#doWrite +- protected void doWrite(ChannelOutboundBuffer in) throws Exception { + SocketChannel ch = javaChannel(); + int writeSpinCount = config().getWriteSpinCount(); + do { + if (in.isEmpty()) { + // All written so clear OP_WRITE + clearOpWrite(); + // Directly return here so incompleteWrite(...) is not called. + return; + } + + // Ensure the pending writes are made of ByteBufs only. + int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite(); + ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite); + int nioBufferCnt = in.nioBufferCount(); + + // Always us nioBuffers() to workaround data-corruption. + // See https://github.com/netty/netty/issues/2761 + switch (nioBufferCnt) { + case 0: + // We have something else beside ByteBuffers to write so fallback to normal writes. + writeSpinCount -= doWrite0(in); + break; + case 1: { + // Only one ByteBuf so use non-gathering write + // Zero length buffers are not added to nioBuffers by ChannelOutboundBuffer, so there is no need + // to check if the total size of all the buffers is non-zero. + ByteBuffer buffer = nioBuffers[0]; + int attemptedBytes = buffer.remaining(); + final int localWrittenBytes = ch.write(buffer); + if (localWrittenBytes <= 0) { + incompleteWrite(true); + return; + } + adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite); + in.removeBytes(localWrittenBytes); + --writeSpinCount; + break; + } + default: { + // Zero length buffers are not added to nioBuffers by ChannelOutboundBuffer, so there is no need + // to check if the total size of all the buffers is non-zero. + // We limit the max amount to int above so cast is safe + long attemptedBytes = in.nioBufferSize(); + final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt); + if (localWrittenBytes <= 0) { + incompleteWrite(true); + return; + } + // Casting to int is safe because we limit the total amount of data in the nioBuffers to int above. + adjustMaxBytesPerGatheringWrite((int) attemptedBytes, (int) localWrittenBytes, + maxBytesPerGatheringWrite); + in.removeBytes(localWrittenBytes); + --writeSpinCount; + break; + } + } + } while (writeSpinCount > 0); + + incompleteWrite(writeSpinCount < 0); +} + +- 1.3-a) (服务器接收连接,从属于bossGroup,read只是表面上的)NioMessageUnsafe#read + +``` +public void read() { + assert eventLoop().inEventLoop(); + final ChannelConfig config = config(); + final ChannelPipeline pipeline = pipeline(); + final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); + allocHandle.reset(config); + + boolean closed = false; + Throwable exception = null; + try { + try { + do { + int localRead = doReadMessages(readBuf); + if (localRead == 0) { + break; + } + if (localRead < 0) { + closed = true; + break; + } + + allocHandle.incMessagesRead(localRead); + } while (allocHandle.continueReading()); + } catch (Throwable t) { + exception = t; + } + + int size = readBuf.size(); + for (int i = 0; i < size; i ++) { + readPending = false; +``` + +- //对于server端来说,第一个handler为io.netty.bootstrap.ServerBootstrap$ServerBootstrapAcceptor + pipeline.fireChannelRead(readBuf.get(i)); + } + readBuf.clear(); + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (exception != null) { + closed = closeOnReadError(exception); + + pipeline.fireExceptionCaught(exception); + } + + if (closed) { + inputShutdown = true; + if (isOpen()) { + close(voidPromise()); + } + } + } finally { + // Check if there is a readPending which was not processed yet. + // This could be for two reasons: + // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method + // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method + // + // See https://github.com/netty/netty/issues/2254 + if (!readPending && !config.isAutoRead()) { + removeReadOp(); + } + } +} + - 1.3.1) NioServerSocketChannel#doReadMessages(接收连接) + - protected int doReadMessages(List<Object> buf) throws Exception { + SocketChannel ch = SocketUtils.accept(javaChannel()); + + try { + if (ch != null) { + buf.add(new NioSocketChannel(this, ch)); + return 1; + } + } catch (Throwable t) { + logger.warn("Failed to create a new channel from an accepted socket.", t); + + try { + ch.close(); + } catch (Throwable t2) { + logger.warn("Failed to close a socket.", t2); + } + } + + return 0; +} + - 1.3.1.1) SocketUtils#accept + +``` +public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() { + @Override + public SocketChannel run() throws IOException { + return serverSocketChannel.accept(); + } + }); + } catch (PrivilegedActionException e) { + throw (IOException) e.getCause(); + } +} +``` + +- 1.3-b) (服务器接收数据,从属于workerGroup)AbstractNioByteChannel#read +1.- 分配 ByteBuf +2.- 从 SocketChannel 中读取数据 +3.- 调用 pipeline.fireChannelRead 发送一个 inbound 事件,沿着pipeline不断传递,经过解码器,最终到达用户handler。 + +``` +public final void read() { + final ChannelConfig config = config(); + final ChannelPipeline pipeline = pipeline(); + final ByteBufAllocator allocator = config.getAllocator(); + final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); + allocHandle.reset(config); + + ByteBuf byteBuf = null; + boolean close = false; + try { + do { + byteBuf = allocHandle.allocate(allocator); + allocHandle.lastBytesRead(doReadBytes(byteBuf)); + if (allocHandle.lastBytesRead() <= 0) { + // nothing was read. release the buffer. + byteBuf.release(); + byteBuf = null; + close = allocHandle.lastBytesRead() < 0; + if (close) { + // There is nothing left to read as we received an EOF. + readPending = false; + } + break; + } + + allocHandle.incMessagesRead(1); + readPending = false; + pipeline.fireChannelRead(byteBuf); + byteBuf = null; + } while (allocHandle.continueReading()); + + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (close) { + closeOnRead(pipeline); + } + } catch (Throwable t) { + handleReadException(pipeline, byteBuf, t, close, allocHandle); + } finally { + // Check if there is a readPending which was not processed yet. + // This could be for two reasons: + // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method + // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method + // + // See https://github.com/netty/netty/issues/2254 + if (!readPending && !config.isAutoRead()) { + removeReadOp(); + } + } +} +``` + + - 1.3.1) NioSocketChannel#doReadBytes +- protected int doReadBytes(ByteBuf byteBuf) throws Exception { + final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); + allocHandle.attemptedBytesRead(byteBuf.writableBytes()); + return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead()); +} + + - 2) SingleThreadEventExecutor#runAllTasks +- protected boolean runAllTasks(long timeoutNanos) { + fetchFromScheduledTaskQueue(); + Runnable task = pollTask(); + if (task == null) { + afterRunningAllTasks(); + return false; + } + + final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos; + long runTasks = 0; + long lastExecutionTime; + for (;;) { + safeExecute(task); + + runTasks ++; + + // Check timeout every 64 tasks because nanoTime() is relatively expensive. + // XXX: Hard-coded value - will make it configurable if it is really a problem. + if ((runTasks & 0x3F) == 0) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + if (lastExecutionTime >= deadline) { + break; + } + } + + task = pollTask(); + if (task == null) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + break; + } + } + + afterRunningAllTasks(); + this.lastExecutionTime = lastExecutionTime; + return true; +} + + +### 2.1) fetchFromScheduledTaskQueue + +``` +private boolean fetchFromScheduledTaskQueue() { + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + Runnable scheduledTask = pollScheduledTask(nanoTime); + while (scheduledTask != null) { + if (!taskQueue.offer(scheduledTask)) { + // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again. + scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask); + return false; + } + scheduledTask = pollScheduledTask(nanoTime); + } + return true; +} +``` + + + +- protected final Runnable pollScheduledTask(long nanoTime) { + assert inEventLoop(); + + Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue; + ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek(); + if (scheduledTask == null) { + return null; + } + + if (scheduledTask.deadlineNanos() <= nanoTime) { + scheduledTaskQueue.remove(); + return scheduledTask; + } + return null; +} + +### 2.2) pollTask +- protected Runnable pollTask() { + assert inEventLoop(); + return pollTaskFrom(taskQueue); +} + +- protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) { + for (;;) { + Runnable task = taskQueue.poll(); + if (task == WAKEUP_TASK) { + continue; + } + return task; + } +} + +### 2.3) safeExecute +- protected static void safeExecute(Runnable task) { + try { + task.run(); + } catch (Throwable t) { + logger.warn("A task raised an exception. Task: {}", task, t); + } +} +- + +# 请求处理:服务器先读后写 +- 在EventLoop的processSelectedKeys方法中,检测到读事件,于是调用ServerSocketChannel的accept方法,获得客户端的SocketChannel,然后调用pipeline的fireChannelRead方法,传播读事件。 +- 注意,pipeline中的链表元素类型为ChannelHandlerContext,并不是ChannelHandler。 +- Pipeline流水线的开始就是HeadContxt,流水线的结束就是TailConext,HeadContxt中调用Unsafe做具体的操作,TailConext中用于向用户抛出pipeline中未处理异常以及对未处理消息的警告 + +# 一、服务器接收连接 +- I/O Request +- via Channel or +- ChannelHandlerContext +- | +- +---------------------------------------------------+---------------+ +- | ChannelPipeline | | +- | \|/ | +- | +---------------------+ +-----------+----------+ | +- | | Inbound Handler N | | Outbound Handler 1 | | +- | +----------+----------+ +-----------+----------+ | +- | /|\ | | +- | | \|/ | +- | +----------+----------+ +-----------+----------+ | +- | | Inbound Handler N-1 | | Outbound Handler 2 | | +- | +----------+----------+ +-----------+----------+ | +- | /|\ . | +- | . . | +- | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()| +- | [ method call] [method call] | +- | . . | +- | . \|/ | +- | +----------+----------+ +-----------+----------+ | +- | | Inbound Handler 2 | | Outbound Handler M-1 | | +- | +----------+----------+ +-----------+----------+ | +- | /|\ | | +- | | \|/ | +- | +----------+----------+ +-----------+----------+ | +- | | Inbound Handler 1 | | Outbound Handler M | | +- | +----------+----------+ +-----------+----------+ | +- | /|\ | | +- +---------------+-----------------------------------+---------------+ +- | \|/ +- +---------------+-----------------------------------+---------------+ +- | | | | +- | [ Socket.read() ] [ Socket.write() ] | +- | | +- | Netty Internal I/O Threads (Transport Implementation) | +- +-------------------------------------------------------------------+ + +- 对于 Inbound 事件: + +- Inbound 事件是通知事件, 当某件事情已经就绪后, 通知上层. + +- Inbound 事件发起者是 unsafe + +- Inbound 事件的处理者是 Channel, 如果用户没有实现自定义的处理方法, 那么Inbound 事件默认的处理者是 TailContext, 并且其处理方法是空实现. + +- Inbound 事件在 Pipeline 中传输方向是 head -> tail + +- 在 ChannelHandler 中处理事件时, 如果这个 Handler 不是最后一个 Hnalder, 则需要调用 ctx.fireIN_EVT (例如 ctx.fireChannelActive) 将此事件继续传播下去. 如果不这样做, 那么此事件的传播会提前终止. + +- Outbound 事件流: Context.fireIN_EVT -> Connect.findContextInbound -> nextContext.invokeIN_EVT -> nextHandler.IN_EVT -> nextContext.fireIN_EVT + +- NioEventLoop#processSelectedKeys->read +- 发生在NioMessageUnsafe#read处理完毕之后 + - 1) DefaultChannelPipeline#fireChannelRead + +``` +public final ChannelPipeline fireChannelRead(Object msg) { + AbstractChannelHandlerContext.invokeChannelRead(head, msg); + return this; +} +``` + + +- 从head开始沿着链表方向传递。 +- head是HeadContext + - 2)(head)AbstractChannelHandlerContext#invokeChannelRead(静态) + +``` +static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { + final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeChannelRead(m); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRead(m); + } + }); + } +} +``` + + + - 3)(head)AbstractChannelHandlerContext#invokeChannelRead(实例,调用其对应Handler的channelRead) + +``` +private void invokeChannelRead(Object msg) { + if (invokeHandler()) { + try { + ((ChannelInboundHandler) handler()).channelRead(this, msg); + } catch (Throwable t) { + notifyHandlerException(t); + } + } else { + fireChannelRead(msg); + } +} +``` + + + - 4)(head,其handler就是this)DefaultChannelPipeline#channelRead + +``` +public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ctx.fireChannelRead(msg); +} +``` + + + - 5)(head,传播)AbstractChannelHandlerContext#fireChannelRead + +``` +public ChannelHandlerContext fireChannelRead(final Object msg) { + invokeChannelRead(findContextInbound(), msg); + return this; +} +``` + + + - 5.1) AbstractChannelHandlerContext#findContextInbound(得到下一个Inbound的ctx) + +``` +private AbstractChannelHandlerContext findContextInbound() { + AbstractChannelHandlerContext ctx = this; + do { + ctx = ctx.next; + } while (!ctx.inbound); + return ctx; +} +``` + + + + - 之后再去调用2),此时ctx就不是head了,而是head.next,是自定义handler对应的ctx。 +- 就服务器而言,head.next可能是ServerBootstrapAcceptor。 + - 6) (ServerBootstrapAcceptor) AbstractChannelHandlerContext#invokeChannelRead(静态) +- 1.boos reactor线程轮询到有新的连接进入 +- 2.通过封装jdk底层的channel创建 NioSocketChannel以及一系列的netty核心组件 +- 3.将该条连接通过chooser,选择一条worker reactor线程绑定上去 +- 4.注册读事件,开始新连接的读写 + - 6.1) ServerBootstrapAcceptor#channelRead(将接收到的连接注册到childGroup中,类似于客户端的6.1.1.3) + - 1)将childHandler放入新连接的pipeline中(ChannelInitializer) + - 2)将Channel注册到Selector中,初始化EventLoop线程,注册自定义Handler(调用ChannelInitializer的initChannel,再将自己移除) + +``` +public void channelRead(ChannelHandlerContext ctx, Object msg) { + final Channel child = (Channel) msg; + + child.pipeline().addLast(childHandler); + + setChannelOptions(child, childOptions, logger); + + for (Entry<AttributeKey<?>, Object> e: childAttrs) { + child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue()); + } + + try { + childGroup.register(child).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + forceClose(child, future.cause()); + } + } + }); + } catch (Throwable t) { + forceClose(child, t); + } +} +``` + + +- + +# 二、服务器接收数据 +- 发生在AbstractNioByteChannel#read处理完毕之后 +- 同样也是先经过HeadContext,只不过之后的handler就是用户指定的handler了,比如Decoder、自定义Handler等InboundHandler。 +- pipeline中handler依次处理,直至遇到TailContext。其处理方法是空实现。 +# 三、服务器发送数据 +- 在用户自定义Handler处理完毕后,会调用对应的ctx的writeAndFlush方法。 +- AbstractChannelHandlerContext#writeAndFlush +- 此时当前线程可能是在用户自定义的线程池中,也可能就是在IO线程中。 +- 假设当前线程是用户自定义线程池中的某个线程,并不在EventLoop线程中。 +- 以下将AbstractChannelHandlerContext简写为CTX + +``` +public ChannelFuture writeAndFlush(Object msg) { + return writeAndFlush(msg, newPromise()); +} +``` + + + +``` +public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + if (msg == null) { + throw new NullPointerException("msg"); + } + + if (isNotValidPromise(promise, true)) { + ReferenceCountUtil.release(msg); + // cancelled + return promise; + } + + write(msg, true, promise); + + return promise; +} +``` + + - 1) (next为首个Outbound Handler)CTX#write(msg,flush,promise) + + +``` +private void write(Object msg, boolean flush, ChannelPromise promise) { + AbstractChannelHandlerContext next = findContextOutbound(); + final Object m = pipeline.touch(msg, next); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + if (flush) { + next.invokeWriteAndFlush(m, promise); + } else { + next.invokeWrite(m, promise); + } + } else { + AbstractWriteTask task; + if (flush) { + task = WriteAndFlushTask.newInstance(next, m, promise); + } else { + task = WriteTask.newInstance(next, m, promise); + } + safeExecute(executor, task, promise, m); + } +} +``` + + + - 2) WriteAndFlushTask#newInstance + + +``` +private static WriteAndFlushTask newInstance( + AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + WriteAndFlushTask task = RECYCLER.get(); + init(task, ctx, msg, promise); + return task; +} +``` + + +- protected static void init(AbstractWriteTask task, AbstractChannelHandlerContext ctx, + Object msg, ChannelPromise promise) { + task.ctx = ctx; + task.msg = msg; + task.promise = promise; + + if (ESTIMATE_TASK_SIZE_ON_SUBMIT) { + task.size = ctx.pipeline.estimatorHandle().size(msg) + WRITE_TASK_OVERHEAD; + ctx.pipeline.incrementPendingOutboundBytes(task.size); + } else { + task.size = 0; + } +} + +- 该runnable的run方法为: + + - 3) CTX#safeExecute(将写数据封装为任务放到EventLoop的队列中) + +``` +private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) { + try { + executor.execute(runnable); + } catch (Throwable cause) { + try { + promise.setFailure(cause); + } finally { + if (msg != null) { + ReferenceCountUtil.release(msg); + } + } + } +} +``` + + + - 4) AbstractWriteTask#run + +``` +public final void run() { + try { + // Check for null as it may be set to null if the channel is closed already + if (ESTIMATE_TASK_SIZE_ON_SUBMIT) { + ctx.pipeline.decrementPendingOutboundBytes(size); + } + write(ctx, msg, promise); + } finally { + // Set to null so the GC can collect them directly + ctx = null; + msg = null; + promise = null; + handle.recycle(this); + } +} +``` + + +### 5) WriteAndFlushTask + +``` +public void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + super.write(ctx, msg, promise); + ctx.invokeFlush(); +} +``` + + +- protected void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + ctx.invokeWrite(msg, promise); +} + +### WRITE + - 6) CTX#invokeWrite + +``` +private void invokeWrite(Object msg, ChannelPromise promise) { + if (invokeHandler()) { + invokeWrite0(msg, promise); + } else { + write(msg, promise); + } +} +``` + + + + +``` +private void invokeWrite0(Object msg, ChannelPromise promise) { + try { + ((ChannelOutboundHandler) handler()).write(this, msg, promise); + } catch (Throwable t) { + notifyOutboundHandlerException(t, promise); + } +} +``` + + +- 此时会调用用户指定的Outbound Handler写数据,比如Encoder。 + - 7) (首个Outbound Handler)MessageToByteEncoder#write + +``` +public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + ByteBuf buf = null; + try { + if (acceptOutboundMessage(msg)) { + @SuppressWarnings("unchecked") + I cast = (I) msg; + buf = allocateBuffer(ctx, cast, preferDirect); + try { + encode(ctx, cast, buf); + } finally { + ReferenceCountUtil.release(cast); + } + + if (buf.isReadable()) { + ctx.write(buf, promise); + } else { + buf.release(); + ctx.write(Unpooled.EMPTY_BUFFER, promise); + } + buf = null; + } else { + ctx.write(msg, promise); + } + } catch (EncoderException e) { + throw e; + } catch (Throwable e) { + throw new EncoderException(e); + } finally { + if (buf != null) { + buf.release(); + } + } +``` + + + - 8) CTX#write(msg,promise) + +``` +public ChannelFuture write(final Object msg, final ChannelPromise promise) { + if (msg == null) { + throw new NullPointerException("msg"); + } + + try { + if (isNotValidPromise(promise, true)) { + ReferenceCountUtil.release(msg); + // cancelled + return promise; + } + } catch (RuntimeException e) { + ReferenceCountUtil.release(msg); + throw e; + } + write(msg, false, promise); + + return promise; +} +``` + + + - 9) (next为第二个Outbound Handler)CTX#write(msg,flush,promise) + - 同1),但是此时: + - 1)flush为false + - 2)已经处于EventLoop中 +- 继续调用第二个Outbound Handler的write +- 10)(第二个Outbound Handler) MessageToMessageEncoder#write + +``` +public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + CodecOutputList out = null; + try { + if (acceptOutboundMessage(msg)) { + out = CodecOutputList.newInstance(); + @SuppressWarnings("unchecked") + I cast = (I) msg; + try { + encode(ctx, cast, out); + } finally { + ReferenceCountUtil.release(cast); + } + + if (out.isEmpty()) { + out.recycle(); + out = null; + + throw new EncoderException( + StringUtil.simpleClassName(this) + " must produce at least one message."); + } + } else { + ctx.write(msg, promise); + } + } catch (EncoderException e) { + throw e; + } catch (Throwable t) { + throw new EncoderException(t); + } finally { + if (out != null) { + final int sizeMinusOne = out.size() - 1; + if (sizeMinusOne == 0) { + ctx.write(out.get(0), promise); + } else if (sizeMinusOne > 0) { + // Check if we can use a voidPromise for our extra writes to reduce GC-Pressure + // See https://github.com/netty/netty/issues/2525 + ChannelPromise voidPromise = ctx.voidPromise(); + boolean isVoidPromise = promise == voidPromise; + for (int i = 0; i < sizeMinusOne; i ++) { + ChannelPromise p; + if (isVoidPromise) { + p = voidPromise; + } else { + p = ctx.newPromise(); + } + ctx.write(out.getUnsafe(i), p); + } + ctx.write(out.getUnsafe(sizeMinusOne), promise); + } + out.recycle(); + } + } +} +``` + + + - 在这里会调用8),如此循环直至next为HeadContext。 + - 11) (head)DefaultChannelPipeline#write + +``` +public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + unsafe.write(msg, promise); +} +``` + + - 12) (unsafe)AbstractChannel#write(放在tailEntry链表中) + +``` +public final void write(Object msg, ChannelPromise promise) { + assertEventLoop(); + + ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; + if (outboundBuffer == null) { + // If the outboundBuffer is null we know the channel was closed and so + // need to fail the future right away. If it is not null the handling of the rest + // will be done in flush0() + // See https://github.com/netty/netty/issues/2362 + safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION); + // release message now to prevent resource-leak + ReferenceCountUtil.release(msg); + return; + } + + int size; + try { + msg = filterOutboundMessage(msg); + size = pipeline.estimatorHandle().size(msg); + if (size < 0) { + size = 0; + } + } catch (Throwable t) { + safeSetFailure(promise, t); + ReferenceCountUtil.release(msg); + return; + } + + outboundBuffer.addMessage(msg, size, promise); +} +``` + + + +``` +public void addMessage(Object msg, int size, ChannelPromise promise) { + Entry entry = Entry.newInstance(msg, size, total(msg), promise); + if (tailEntry == null) { + flushedEntry = null; + tailEntry = entry; + } else { + Entry tail = tailEntry; + tail.next = entry; + tailEntry = entry; + } + if (unflushedEntry == null) { + unflushedEntry = entry; + } + + // increment pending bytes after adding message to the unflushed arrays. + // See https://github.com/netty/netty/issues/1619 + incrementPendingOutboundBytes(entry.pendingSize, false); +} +``` + + +- 把数据串在一个Entry链中。 + +### FLUSH + - 13) CTX#invokeFlush + +``` +private void invokeFlush() { + if (invokeHandler()) { + invokeFlush0(); + } else { + flush(); + } +} +``` + + - 14) CTX#invokeFlush0 + +``` +private void invokeFlush0() { + try { + ((ChannelOutboundHandler) handler()).flush(this); + } catch (Throwable t) { + notifyHandlerException(t); + } +} +``` + + + + - 15) ChannelOutboundHandlerAdatper#flush + +``` +public void flush(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); +} +``` + + - 16) CTX#flush(从后往前flush,直至head) + +``` +public ChannelHandlerContext flush() { + final AbstractChannelHandlerContext next = findContextOutbound(); + EventExecutor executor = next.executor(); + if (executor.inEventLoop()) { + next.invokeFlush(); + } else { + Runnable task = next.invokeFlushTask; + if (task == null) { + next.invokeFlushTask = task = new Runnable() { + @Override + public void run() { + next.invokeFlush(); + } + }; + } + safeExecute(executor, task, channel().voidPromise(), null); + } + + return this; +} +``` + + + - 17)(head)DefaultChannelPipeline#flush + +``` +public void flush(ChannelHandlerContext ctx) throws Exception { + unsafe.flush(); +} +``` + + - 18) (unsafe)AbstractChannel#flush + +``` +public final void flush() { + assertEventLoop(); + + ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; + if (outboundBuffer == null) { + return; + } + + outboundBuffer.addFlush(); + flush0(); +} +``` + + + - 19.1) ChannelOutboundBuffer#addFlush + +``` +/** + * Add a flush to this {@link ChannelOutboundBuffer}. This means all previous added messages are marked as flushed + * and so you will be able to handle them. + */ +public void addFlush() { + // There is no need to process all entries if there was already a flush before and no new messages + // where added in the meantime. + // + // See https://github.com/netty/netty/issues/2577 + Entry entry = unflushedEntry; + if (entry != null) { + if (flushedEntry == null) { + // there is no flushedEntry yet, so start with the entry + flushedEntry = entry; + } + do { + flushed ++; + if (!entry.promise.setUncancellable()) { + // Was cancelled so make sure we free up memory and notify about the freed bytes + int pending = entry.cancel(); + decrementPendingOutboundBytes(pending, false, true); + } + entry = entry.next; + } while (entry != null); + + // All flushed so reset unflushedEntry + unflushedEntry = null; + } +} +``` + + - 19.2) AbstractChannel#flush0 +- protected final void flush0() { + // Flush immediately only when there's no pending flush. + // If there's a pending flush operation, event loop will call forceFlush() later, + // and thus there's no need to call it now. + if (!isFlushPending()) { + super.flush0(); + } +} + + +``` +protected void flush0() { + if (inFlush0) { + // Avoid re-entrance + return; + } + + final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer; + if (outboundBuffer == null || outboundBuffer.isEmpty()) { + return; + } + + inFlush0 = true; + + // Mark all pending write requests as failure if the channel is inactive. + if (!isActive()) { + try { + if (isOpen()) { + outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true); + } else { + // Do not trigger channelWritabilityChanged because the channel is closed already. + outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } + } finally { + inFlush0 = false; + } + return; + } + + try { + doWrite(outboundBuffer); + } catch (Throwable t) { + if (t instanceof IOException && config().isAutoClose()) { + /** + * Just call {@link #close(ChannelPromise, Throwable, boolean)} here which will take care of + * failing all flushed messages and also ensure the actual close of the underlying transport + * will happen before the promises are notified. + * + * This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()} + * may still return {@code true} even if the channel should be closed as result of the exception. + */ + close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } else { + try { + shutdownOutput(voidPromise(), t); + } catch (Throwable t2) { + close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false); + } + } + } finally { + inFlush0 = false; + } +} +``` + + + - 19.2.1) NioSocketChannel#doWrite + - 代码同EventLoop 1.2.1) NioSocketChannel#doWrite +- 底层是调用的SocketChannel的write方法。 +- + +# ByteBuf +# IdleStateHandler +- channelActive()方法调用Initialize()方法,根据配置的readerIdleTime,WriteIdleTIme等超时事件参数往任务队列taskQueue中添加定时任务task ; + +- 定时任务添加到对应线程EventLoopExecutor对应的任务队列taskQueue中,在对应线程的run()方法中循环执行 +- 用当前时间减去最后一次channelRead方法调用的时间判断是否空闲超时; +- 如果空闲超时则创建空闲超时事件并传递到channelPipeline中; + +- IdleStateHandler心跳检测主要是通过向线程任务队列中添加定时任务,判断channelRead()方法或write()方法是否调用空闲超时,如果超时则触发超时事件执行自定义userEventTrigger()方法; + +- Netty通过IdleStateHandler实现最常见的心跳机制不是一种双向心跳的PING-PONG模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源;如果服务端一段时间内内有收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费;在这种心跳模式下服务端可以感知客户端的存活情况,无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线; + +- 要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty中的channelInactive()方法是通过Socket连接关闭时挥手数据包触发的,因此可以通过channelInactive()方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知; From b3938832b5e4e6085d730061deb198253b9664bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:46:47 +0800 Subject: [PATCH 87/97] =?UTF-8?q?Create=20=E5=8D=81=E4=B8=83=E3=80=81Redis?= =?UTF-8?q?=E4=BD=BF=E7=94=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...3\200\201Redis\344\275\277\347\224\250.md" | 695 ++++++++++++++++++ 1 file changed, 695 insertions(+) create mode 100644 "docs/\345\215\201\344\270\203\343\200\201Redis\344\275\277\347\224\250.md" diff --git "a/docs/\345\215\201\344\270\203\343\200\201Redis\344\275\277\347\224\250.md" "b/docs/\345\215\201\344\270\203\343\200\201Redis\344\275\277\347\224\250.md" new file mode 100644 index 00000000..c423bebb --- /dev/null +++ "b/docs/\345\215\201\344\270\203\343\200\201Redis\344\275\277\347\224\250.md" @@ -0,0 +1,695 @@ +# Redis + +- 怎么保证redis和db中的数据一致 +- redis实现原理 ;持久化;redis cluster实现原理 ;redis数据类型 string和list都有什么适用场景 ;Codis相关 +- redis与memcached区别 memcache如何保持缓存一致性 +- redis中SortedSet结果 +- redis主从复制过程,同步还是异步等; redis主从是怎么选取的 +- redis插槽的分配;redis主节点宕机了怎么办,还有没有同步的数据怎么办? +- redis集群的话数据分片怎么分,然后就是如果并发很高,几十万并发,可以做哪些优化 + +- Jedis源码 + +# Redis简介 +- Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。 +- 特点: +- 开源 +- 多种数据结构 +- 基于键值的存储服务系统 +- 高性能,功能服务 + +- 它可以存储键与5种不同类型的值之前的映射;可以进行持久化;可以使用复制来扩展读性能;还可以使用分片来扩展写性能。 +# 与Memcached区别 +- 1、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。 +- 2、Redis支持数据的备份,即master-slave模式的数据备份。 +- Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。 +- Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。 +- +- 3、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 +- 4、内存管理机制不同: +- Memcached默认使用Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。Memcached的内存管理制效率高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。因为每个Chunk都分配了特定长度的内存空间,所以变长数据无法充分利用这些空间。 + +- Redis采用的是包装的malloc/free,相较于Memcached的内存管理方法来说,要简单很多。 +# 优点 +- 数据类型丰富 +- 效率高 +- 支持集群 +- 支持持久化 +# 缺点 +- 单进程单线程,长命令会导致Redis阻塞 +- 集群下多key操作(事务、MGET、MSET)无法使用 +- 无法自动迁移 +# 数据类型 + +# String +- 值可以是字符串、数字(整数、浮点数)或者二进制。 +- 整数范围与系统的长整型的取值范围相同(32位系统是32位,64位系统是64位) +- 浮点数的精度与double相同 +命令 说明 时间复杂度 +get key 获取key对应的value O(1) +set key value 设置key value O(1) +del key 删除key-value O(1) +incr key自增1, 如果key不存在,自增后get(key) = 1 O(1) +decr key自减1, 如果key不存在,自增后get(key) = -1 O(1) +incrby key k key自增k, 如果key不存在,自增后get(key) = k O(1) +decr key k key自减k, 如果key不存在,自增后get(key) = -k O(1) +set key value 不管可以是否存在 O(1) +setnx key value key不存在,才设置 O(1) +set key value xx key存在,才设置 O(1) +mget key1 key2 key3 批量获取key,原子操作 O(N) +1次网络时间+n次执行命令时间 +如果是n次get,那么是n次网络时间+n次执行命令时间 +mset key1 value1 key2 value2 批量设置key-value O(1) +getset key newvalue set key newvalue并返回旧的value O(1) +append key value 将value追加到旧的value O(1) +strlen key 返回字符串的长度(注意中文,utf8下一个中文占用3个字符) O(1) +incrbyfloat key 3.5 增加key对应的值3.5 O(1) +getrange key start end 获取字符串指定下标所有的值 O(1) +setrange key index value 设置指定下标所有对应的值 O(1) + +# Hash + +hget key field 获取hash key对应field的value O(1) +hset key field value 设置has key 对应的field的value O(1) +hexists key field 判断hash key 是否有field O(1) +hlen key 获取hash key field的数量 O(1) +hmget key field1 field2…fieldN 批量获取hash key的一批field对应的值 O(N) +hset key field1 value1 field2 value2…fieldN valueN 批量设置hash key的一批field value O(1) +hgetall key 返回hash key对应所有的field和value O(N) +hvals key 返回hash key对应所有的field的value O(N) +hkeys key 返回hash key对应所有的field O(N) +hsetnx key field value 设置has key 对应的field的value(如果field已经存在,则失败) O(1) +hincrby key field intCounter hash key对应的field的value自增intCounter O(1) +hincrbyfloat key field floatCounter 浮点数版本 O(1) +- 小心使用hgetall(牢记单线程) +# List + +- 有序 +- 可以重复 +- 左右两边插入弹出 + +命令 说明 例子 时间复杂度 +rpush key value1 value2…valueN 从列表右边插入(1-N个) rpush listkey c b a O(1-N) +lpush key value1 value2…valueN 从列表左边插入(1-N个) lpush listkey c b a O(1-N) +linsert key before/after value newValue 在list指定的值前/后插入newValue insert listkey before b java O(N) +lpop key 从列表左侧弹出一个item lpop listKey O(1) +rpop key 从列表右侧弹出一个item rpop listKey O(1) +lrem key count value (1)count>0,从左到右,删除最多count个value相等的项;(2)count<0,从右到左,删除最多count个value相等的项;(3)count=0,删除所有value相等的项 lrem listkey 0 a; lrem listkey -1 c O(N) +ltrim key start end 按照索引范围修剪列表 ltrim listkey 1 4 O(N) +lrange key start end(包含end) 获取列表指定索引范围所有item lrange list key 0 2 ; lrange listkey 1 -1 O(N) +lindex key index 获取列表指定索引的item lindex listkey 0; lindex listkey -1 O(1) +llen key 获取列表长度 llen listkey O(1) +lset key index newValue 设置列表指定索引值为newValue lset listkey 2 java O(n) +blpop key timeout lpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远阻塞 O(1) +brpop key timeout rpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远阻塞 O(1) + +# Set +- Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 + - Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 +- 特点 +1.- 无序 +2.- 无重复 +3.- 集合间操作 + +## 集合内操作 +命令 说明 时间复杂度 +sadd key element 向集合key添加element(如果element已经存在,添加失败) O(1) +srem key element 将集合key中的element移除掉 O(1) +scard key 计算集合大小 O(1) +sismember key element 判断element 是否在集合中 O(1) +srandmember key count 从集合中随机挑count个元素 O(1) +spop key 从集合中随机弹出一个元素 O(1) +smembers key 获取集合所有元素 O(1) +srem key element 将集合key中的element移除掉 O(1) + +## 集合间操作 +命令 说明 时间复杂度 +sdiff key1 key2 差集 O(1) +sinter key1 key2 交集 O(1) +sunion key1 key2 并集 O(1) +sidff/sinter/suion + store destkey 将差集、交集、并集保存在destkey中 O(1) +- srandmember不会改变集合 +- spop会改变集合(抽奖) +- smembers 返回的是无序集合,并且要注意量很大的时候回阻塞 +- 交集可以用在比如共同关注等地方 +# Sorted set +- Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。 +命令 说明 时间复杂度 +zadd key score element 添加score和element O(logN) +zrem key element(可以是多个) 将集合key中的element移除掉 O(1) +zscore key element 返回元素的分数 O(1) +zincrby key increScore element 增加或减少元素的分数 O(1) +zcard key 返回元素的总个数 O(1) +zrank(zrevrank) key member 返回元素的排名 O(1) +zrange(zrevrank) key start end [WITHSCORES] 返回指定索引范围内的升序元素[分值] O(logN + m) +zrangebyscore(zrevrangebyscore) key minScore maxScore 返回指定分数范围内的升序元素 O(logN + m) +zcount key minScore maxScore 返回有序集合内在指定分数范围内的个数 O(logN + m) +zremrangebyrank key start end 删除指定排名内的升序元素 O(logN + m) +zremrangebyscore key minScore maxScore 删除指定分数内的升序元素 O(logN + m) +ZINTERSTORE destination numkeys(表示key的个数) key [key …] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 +ZUNIONSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的并集,并存储在新的 key 中 +- 适用于各种排行榜 + +- Redis用一个Sorted Set解决按两个字段排序的问题,也就是按照热度+时间作为排序字段,关键在于怎么拼接score的问题。这种特点的场景,解决方法是组装一个浮点数,整数部分是热度的值,小数部分是时间。这里要注意的是,redis里面精度应该是小数6位,所以不能把整个日期作为小数部分。例如有这样一组数据: +- | 热度 | 时间 | +- | 2 | 2016-03-31 13:41:01 | +- | 5 | 2016-03-31 13:41:01 | +- | 2 | 2016-03-31 13:42:01 | +- | 1 | 2016-03-31 13:41:01 | +- 那么score的值可以组装成: +- | 热度 | 时间 | score +- | 2 | 2016-03-31 13:41:01 | 2.134101 +- | 5 | 2016-03-31 13:41:01 | 5.134101 +- | 2 | 2016-03-31 13:42:01 | 2.134201 +- | 1 | 2016-03-31 13:41:01 | 1.134101 +- 这样的局限性是每个zset只能存一天的数据 +# 事务 +# MULTI&EXEC(原子执行,并非互斥) +- 基本事务只需要MULTI和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。被multi和exec命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。(注意,是原子执行,但其他客户端仍可能会修改正在操作的数据) +- 在输入命令时如果中间有一个命令有语法错误(类似于编译时异常),那么该命令及其之后的命令都不会被执行,之前的命令会被执行。 + +- WATCH&UNWATCH(原子执行+乐观锁) +- 在事务开启前watch了某个key,在事务提交时会检查key的值与watch的时候其值是否发生变化,如果发生变化,那么事务的命令队列不会被执行。 +- 如果使用unwatch命令,那么之前的对所有key的监控一律取消,哪怕之前检测到watch的key的值发生变化,也不会对之后的事务产生影响。 +# 分布式锁 +- watch&multi&exec并不是一个好的主意,因为可能会不断循环重试,在竞争激烈时性能很差。 + +# 排他锁 SETNX +- setnx:如果不存在,那么设置一个键值对,它是一个原子性的操作 + + +- 释放锁 + +- 函数首先是要WATCH命令监视代表锁的键,接着检查键目前的值是否和加锁时设置的值相同,并在确认值没有变化之后删除该键。可以防止程序错误地释放一个锁多次。 +- 主要因为后面带有超时特性的锁其他客户端会修改锁的超时时间, +# 带有超时特性的锁 +- 目前的锁在持有者崩溃的时候不会自动被释放,这将导致锁一直处于已被获取的状态。 +- 为了给锁加上超时限制特性,程序将在取得锁之后,调用expire命令来为锁设置过期时间,使得Redis可以自动删除超时的锁。 +- 为了确保锁在客户端已经崩溃(有可能是在获得锁、设置超时时间之后崩溃,也有可能在设置超时之前崩溃)的情况下仍然能够自动被释放,客户端会在尝试获取锁失败后,检查锁的超时时间,并为未设置超时时间的锁设置超时时间。因此锁总会带有超时时间,并最终因为超时而自动被释放,使得其他客户端可以继续尝试获取已被释放的锁。 + + +- 释放锁的函数和之前一样。 +- + +# 持久化 +- 快照 +1.- mysql dump +2.- redis RDB +- 写日志 +1.- mysql binlog +2.- hbase hLog +3.- redis AOF +# RDB(Redis Database,全量模式) + +- RDB是Redis内存到硬盘的快照,用于持久化 +- save通常会阻塞Redis +- bgsave不会阻塞redis,但是会fork新进程 +- save自动配置满足任一就会被执行 +- 有些触发机制不容忽视 + +## 触发方式 +o- save(同步) + +- * 文件策略:如存在老的RDB文件,新替换老 +- * 复杂度:O(N) +o- bgsave(异步) + +o- 自动配置 + +- # In the example below the behaviour will be to save: +- # after 900 sec (15 min) if at least 1 key changed +- # after 300 sec (5 min) if at least 10 keys changed +- # after 60 sec if at least 10000 keys changed + +## 缺点 +- 1、耗时 + +- 2、不可控,丢失数据 + +# AOF(Append Only File,增量模式) +- 日志的形式,类似于MySQL的binlog +- 备份: + +- 恢复: + +## 策略 +o- always + +o- everysec + +o- no + + +## 重写 + +- 减少硬盘占用量 +- 加速恢复速度 + + +- 配置: + + +### 流程 + + +# 比较 + +# RDB最佳策略 +o- 关 +o- 集中管理 +o- 主从,从开 +# AOF最佳策略 +o- 开,缓存和存储 +o- AOF重写集中管理 +o- everysec + +# 消息队列 +- publish [channel] message + +- subscribe [channel] 一个或者多个 + +- unsubscribe [channel] 一个或者多个 + + +# 高级数据结构 +# BitMap(String的一些其他命令) +命令 说明 时间复杂度 +setbit key offset value 给位图指定索引设置值 O(1) +getbit key offset 获取位图指定索引的值 O(1) +bitcount key start end 获取位图指定范围(start 到end,单位为字节,如果不指定就获取全部)位值为1的个数 O(1) +bitop op destkey key [key…] 做多个bitmap的and,or,not,xor操作并将结果保存在destkey中 O(1) +bitpos key targetBit [start][end 计算位图指定范围(start到end,单位为字节,如果不指定就是获取全部)第一个偏移量对应的值等于targetBit的位置 O(1) + +- 独立用户统计 +- 使用位图去记录用户uid,其实就是记录索引值,比如userid=100代表位图下标100的值为1 + + +- 使用经验 +- type=string,最大512MB +- 注意setbit时的偏移量,可能有较大耗时 +- 位图不是绝对好 +# GEO + +命令 说明 +geoadd key longitude latitude member [longitude latitude member …] 增加地理位置信息 +geopos key member[member… 获取地理位置信息 +geodist key member1 member2[unit] 获取两个地理位置的距离,unit:m,km,mi,ft +georadius 获取指定位置范围内的地理位置信息集合 + +# 过期策略 +- 当key时expires超时时,怎么处理这个key,有三种过期策略: +# 定时删除 +- 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除 +- 优点:保证内存被尽快释放 +- 缺点: +- 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key +- 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重 +- 没人用 +# 惰性删除 +- 含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。 +- 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了) +- 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存) +# 定期删除 +- 含义:每隔一段时间执行一次删除过期key操作 +- 优点: +- 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点 +- 定期删除过期key--处理"惰性删除"的缺点 +- 缺点 +- 在内存友好方面,不如"定时删除" +- 在CPU时间友好方面,不如"惰性删除" +- 难点 +- 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了) + +- Redis中同时使用了惰性过期和定期过期两种过期策略。 +# 内存淘汰策略 +- Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。 + +- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 +- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 +- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 +- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 +- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 +- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。 +# 主从复制/哨兵/集群 +# 主从复制(数据是同步的,类似于MySQL Replication) +## 特点 +- 1、master可以拥有多个slave +- 2、多个slave可以连接到同一个master,还可以连接到其他的slave +- 3、主从复制不会阻塞master,master仍可以接收客户端请求 +- 4、提高系统的伸缩性 + +## 过程 +- 1、slave与master建立连接,发送sync同步命令(slaveof命令) +- 2、master会开启一个后台进程,将数据库快照保存到文件中(bgsave),同时master主进程会开始收集新的写命令并缓存到backlog队列 +- 3、后台进程完成保存后,就将文件发送slave +- 4、slave会丢弃所有旧数据,开始载入master发来的快照文件 +- 5、master向slave发送存储在backlog队列中的写命令,发送完毕后,每执行一个写命令,就向slave发送相同的写命令(异步复制) +- 6、slave执行master发来的所有存储在缓冲区的写命令,并从现在开始,接收并执行master传来的每个写命令 + +- 在接收到master发送的数据初始副本之后,客户端 向master写入时,slave都会实时得到更新。在部署好slave之后,客户端就可以向任意一个slave发送读请求了,而不必总是把读请求发送给master。(负载均衡) + +- 注意,快照指的就是RDB方式。 +- slave是不能写的,是只读的;只有master可以写。 + + + +- 注意,Redis不支持主主复制。 +- 通过同时使用主从复制和AOF持久化,用户可以增强对于系统崩溃的抵抗能力。 + +- master节点挂了以后,redis就不能对外提供写服务了,因为剩下的slave不能成为master +- 这个缺点影响是很大的,尤其是对生产环境来说,是一刻都不能停止服务的,所以一般的生产坏境是不会单单只有主从模式的。所以有了下面的Sentinal模式。 + +- 如果有多个slave节点并 并发发送SYNC命令给master,企图建立主从关系,只要第二个slave的SYNC命令发生在master完成BGSAVE之前,第二个slave将受到和第一个slave相同的快照和后续backlog;否则,第二个slave的SYNC将触发master的第二次BGSAVE。 +# 哨兵 sentinel(数据是同步的) +- 既然主从模式中,当master节点挂了以后,slave节点不能主动选举一个master节点出来,那么我就安排一个或多个Sentinal来做这件事,当Sentinal发现master节点挂了以后,Sentinal就会从slave中重新选举一个master。 + +- 对Sentinal模式的理解: + +- Sentinal模式是建立在主从模式的基础上,如果只有一个Redis节点,Sentinal就没有任何意义 +- 当master节点挂了以后,Sentinal会在slave中选择一个作为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master +- 当master节点重新启动后,它将不再是master而是作为slave接收新的master节点的同步数据 +- Sentinal因为也是一个进程有挂掉的可能,所以Sentinal也会启动多个形成一个Sentinal集群 +- 当主从模式配置密码时,Sentinal也会同步将配置信息修改到配置文件中,不许要担心。 +- 一个Sentinal或Sentinal集群可以管理多个主从Redis。 +- Sentinal最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,Sentinal也挂了。 + +- 当使用Sentinal模式的时候,客户端就不要直接连接Redis,而是连接Sentinal的ip和port,由Sentinal来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,Sentinal就会感知并将新的master节点提供给使用者。 + +- Sentinal模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或Sentinal模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中,就是下面要讲的。 +## 原理 +- ①sentinel集群通过给定的配置文件发现master,启动时会监控master。通过向master发送info信息获得该服务器下面的所有从服务器。 +- ②sentinel集群通过命令连接向被监视的主从服务器发送hello信息(每秒一次),该信息包括sentinel本身的ip、端口、id等内容,以此来向其他sentinel宣告自己的存在。 +- ③sentinel集群通过订阅连接接收其他sentinel发送的hello信息,以此来发现监视同一个主服务器的其他sentinel;集群之间会互相创建命令连接用于通信,因为已经有主从服务器作为发送和接收hello信息的中介,sentinel之间不会创建订阅连接。 +- ④sentinel集群使用ping命令来检测实例的状态,如果在指定的时间内(down-after-milliseconds)没有回复或则返回错误的回复,那么该实例被判为下线。 +- ⑤当failover主备切换被触发后,failover并不会马上进行,还需要sentinel中的大多数sentinel授权后才可以进行failover,即进行failover的sentinel会去获得指定quorum个的sentinel的授权,成功后进入ODOWN状态。如在5个sentinel中配置了2个quorum,等到2个sentinel认为master死了就执行failover。 +- ⑥sentinel向选为master的slave发送SLAVEOF NO ONE命令,选择slave的条件是sentinel首先会根据slaves的优先级来进行排序,优先级越小排名越靠前。如果优先级相同,则查看复制的下标,哪个从master接收的复制数据多,哪个就靠前。如果优先级和下标都相同,就选择进程ID较小的。 +- ⑦sentinel被授权后,它将会获得宕掉的master的一份最新配置版本号(config-epoch),当failover执行结束以后,这个版本号将会被用于最新的配置,通过广播形式通知其它sentinel,其它的sentinel则更新对应master的配置。 + +- ①到③是自动发现机制: + +- 以10秒一次的频率,向被监视的master发送info命令,根据回复获取master当前信息。 +- 以1秒一次的频率,向所有redis服务器、包含sentinel在内发送PING命令,通过回复判断服务器是否在线。 +- 以2秒一次的频率,通过向所有被监视的master,slave服务器发送当前sentinel,master信息的消息。 +- ④是检测机制,⑤和⑥是failover机制,⑦是更新配置机制。 +## Sentinel职责 +- Redis Sentinel的以下几个功能。 +1.- 监控:Sentinel节点会定期检测Redis数据节点和其余Sentinel节点是否可达。 +2.- 通知:Sentinel节点会将故障转移通知给应用方。 +3.- 主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系(Raft主从选举)。 +4.- 配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息。 + +- FailOver 故障转移/失效转移 + +# 集群(数据是分片的,sharing) +- cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。对cluster的一些理解: + +- cluster可以说是Sentinal和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。 +- 配置集群需要至少3个主节点(每个主节点对应一个从节点,这样一共6个节点) + +- 因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容 +- 这种模式适合数据量巨大的缓存要求,当数据量不是很大使用Sentinal即可。 + +- Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。 + +- Redis 集群提供了以下两个好处: + +- 将数据自动切分(split)到多个节点的能力。 +- 当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。 + +- 不同的master存放不同的数据,所有的master数据的并集是所有的数据。 +- master与之对应的slave数据是一样的。 +## 数据分片 +- 取决于客户端,有多种算法; +### 1) Hash映射(并非一致性哈希,而是哈希槽) +- Redis采用哈希槽(hash slot)的方式在服务器端进行分片。 + - HASH_SLOT = CRC16(key) mod 16384(2^14=16384) + +- 在redis官方给出的集群方案中,数据的分配是按照槽位来进行分配的,每一个数据的键被哈希函数映射到一个槽位,redis-3.0.0规定一共有16384个槽位,当然这个可以根据用户的喜好进行配置。当用户put或者是get一个数据的时候,首先会查找这个数据对应的槽位是多少,然后查找对应的节点,然后才把数据放入这个节点。这样就做到了把数据均匀的分配到集群中的每一个节点上,从而做到了每一个节点的负载均衡,充分发挥了集群的威力。 +- 计算key字符串对应的映射值,redis采用了crc16函数然后与0x3FFF取低16位的方法。crc16以及md5都是比较常用的根据key均匀的分配的函数,就这样,用户传入的一个key我们就映射到一个槽上,然后经过gossip协议,周期性的和集群中的其他节点交换信息,最终整个集群都会知道key在哪一个槽上。 + +- Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么: + +- 节点 A 包含 0 到 5500号哈希槽. +- 节点 B 包含5501 到 11000 号哈希槽. +- 节点 C 包含11001 到 16384号哈希槽. +- 这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态. + +- 位序列结构 +- Master节点维护着一个16384/8字节的位序列,Master节点用bit来标识对于某个槽自己是否拥有。比如对于编号为1的槽,Master只要判断序列的第二位(索引从0开始)是不是为1即可。 + +### 2) 范围映射 +- 范围映射通常选择key本身而非key的函数计算值来作为数据分布的条件,且每个数据节点存放的key的值域是连续的一段范围。 +- key的值域是业务层决定的,业务层需要清楚每个区间的范围和Redis实例数量,才能完整地描述数据分布。这使业务层的key值域与系统层的实例数量耦合,数据分片无法在纯系统层实现。 +### 3) Hash和范围结合 +- 典型方式是一致性hash。首先对key进行哈希计算,得到值域有限的hash值,再对hash值只做范围映射,确定该key对应的业务数据存放的具体实例。这种方式的优势是节点新增或退出时,涉及的数据迁移量小——变更的节点上涉及的数据只需和相邻节点发生迁移关系。 +### 哈希标签 +- 键哈希标签是一种可以让用户指定将一批键都能够被存放在同一个槽中的实现方法,用户唯一要做的就是按照既定规则生成key即可,这个规则是这样的,如果我有对于同一个用户有两种不同含义的两份数据,我只要将他们的键设置为下面即可: +- abc{userId}def和ghi{userId}jkl +- redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。 +### 重定向客户端 +- Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端MOVED 槽数 所在节点地址。 +- 如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果。没有代理的Redis Cluster可能会导致客户端两次连接集群中的节点才能找到正确的服务,推荐客户端缓存连接,这样最坏的情况是两次往返通信。 +## 节点间通信协议——Gossip +- 通过Gossip协议来进行节点之间通信。 +- gossip protocol,简单地说就是集群中每个节点会由于网络分化、节点抖动等原因而具有不同的集群全局视图。节点之间通过gossip protocol进行节点信息共享。这是业界比较流行的去中心化的方案。 +- 其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由两个clusterMsgDataGossip结构组成。 + +- 共享以下关键信息: + - 1)数据分片和节点的对应关系 + - 2)集群中每个节点可用状态 + - 3)集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、故障发生时的主备切换决策、单点master的发现和其发生主备关系的变更等场景均会导致集群结构变化。 + - 4)pub/sub功能在cluster的内部实现所需要交互的信息 +## 主从选举——Raft +- Redis Cluster重用了Sentinel的代码逻辑,不需要单独启动一个Sentinel集群,Redis Cluster本身就能自动进行Master选举和Failover切换。 + +- 集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。 + +- 选新主的过程基于Raft协议选举方式来实现的。 + +- 以下是故障转移的执行步骤: + + - 1)从下线主节点的所有从节点中选中一个从节点 + - 2)被选中的从节点执行SLAVEOF NO NOE命令,成为新的主节点 + - 3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己 + - 4)新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点 + - 5)新的主节点开始接收和处理槽相关的请求 + + +## 功能限制 +- Redis集群相对单机在功能上有一定限制。 +1.- key批量操作支持有限。如:MSET``MGET,目前只支持具有相同slot值的key执行批量操作。 +2.- key事务操作支持有限。支持多key在同一节点上的事务操作,不支持分布在多个节点的事务功能。 +3.- key作为数据分区的最小粒度,因此不能将一个大的键值对象映射到不同的节点。如:hash、list。 +4.- 不支持多数据库空间。单机下Redis支持16个数据库,集群模式下只能使用一个数据库空间,即db 0。 +5.- 复制结构只支持一层,不支持嵌套树状复制结构。 + +## 数据迁移/在线扩容 +- redis-trib.rb +- 随着业务的发展,redis的节点承载的压力也会增大,redis的集群可通过水平横向的拓展,在集群中加入新的master-slave去分担集群中其他节点的压力。由于redis cluster中数据存放在slot中,可以将线上的reids数据slot迁移到新加入的master-slave。 +- 迁移的slot的数量可以根据节点配置不同而不同,若各节点配置相同,则可以平均分配slot(n=16384/主节点数量) + +- Pre-Sharding +- 假设有N台主机,每台主机上部署M个实例,整个系统有T = N x M个实例 +- 由于一个Redis实例的资源消耗非常小,所以一开始就可以部署比较多的Redis实例,比如128个实例 +- 在前期业务量比较低的时候,N可以比较少,M比较多,而且主机的配置(CPU+内存)可以较低 +- 在后期业务量较大的时候,N可以较多,M变小 + +- 总之,通过这种方法,在容量增长过程可以始终保持Redis实例数(T)不变,所以避免了重新Sharding的问题 + +- 实际就是在同一台机器上部署多个Redis实例的方式,当容量不够时将多个实例拆分到不同的机器上,这样实际就达到了扩容的效果。Pre-Sharding方法是将每一个台物理机上,运行多个不同端口的Redis实例,假如有三个物理机,每个物理机运行三个Redis实例,那么我们的分片列表中实际有9个Redis实例,当我们需要扩容时,增加一台物理机来代替9个中的一个redis,有人说,这样不还是9个么,是的,但是以前服务器上面有三个redis,压力很大的,这样做,相当于单独分离出来并且将数据一起copy给新的服务器。值得注意的是,还需要修改客户端被代替的redis的IP和端口为现在新的服务器,只要顺序不变,不会影响一致性哈希分片。 + +- 拆分过程如下: + +- 在新机器上启动好对应端口的Redis实例。 +- 配置新端口为待迁移端口的从库。 +- 待复制完成,与主库完成同步后,切换所有客户端配置到新的从库的端口。 +- 配置从库为新的主库。 +- 移除老的端口实例。 +- 重复上述过程迁移好所有的端口到指定服务器上。 +- 以上拆分流程是Redis作者提出的一个平滑迁移的过程,不过该拆分方法还是很依赖Redis本身的复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。所以做这个拆分的过程最好选择为业务访问低峰时段进行。 +# Codis +- 基本和twemproxy一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新hash节点。 +# twemproxy +- 使用方法和普通redis无任何区别,设置好它下属的多个redis实例后,使用时在本需要连接redis的地方改为连接twemproxy,它会以一个代理的身份接收请求 并使用一致性hash算法,将请求转接到具体redis,将结果再返回twemproxy。 +- 问题:twemproxy自身单端口实例的压力, +- 使用一致性hash后,对redis节点数量改变时候的 计算值的改变,数据无法自动移动到新的节点。 +- + +# 配置文件 +- redis.conf 配置项说明如下: +- 1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 +- daemonize no +- 2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定 +- pidfile /var/run/redis.pid +- 3. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字 +- port 6379 +- 4. 绑定的主机地址 +- bind 127.0.0.1 +- 5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能 +- timeout 300 +- 6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose +- loglevel verbose +- 7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null +- logfile stdout +- 8. 设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id +- databases 16 +- 9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 +- save <seconds> <changes> +- Redis默认配置文件中提供了三个条件: +- save 900 1 +- save 300 10 +- save 60 10000 +- 分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。 +- +- 10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大 +- rdbcompression yes +- 11. 指定本地数据库文件名,默认值为dump.rdb +- dbfilename dump.rdb +- 12. 指定本地数据库存放目录 +- dir ./ +- 13. 设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步 +- slaveof <masterip> <masterport> +- 14. 当master服务设置了密码保护时,slav服务连接master的密码 +- masterauth <master-password> +- 15. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭 +- requirepass foobared +- 16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息 +- maxclients 128 +- 17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区 +- maxmemory <bytes> +- 18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no +- appendonly no +- 19. 指定更新日志文件名,默认为appendonly.aof +- appendfilename appendonly.aof +- 20. 指定更新日志条件,共有3个可选值: +- no:表示等操作系统进行数据缓存同步到磁盘(快) +- always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全) +- everysec:表示每秒同步一次(折衷,默认值) +- appendfsync everysec +- +- 21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制) +- vm-enabled no +- 22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享 +- vm-swap-file /tmp/redis.swap +- 23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0 +- vm-max-memory 0 +- 24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值 +- vm-page-size 32 +- 25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。 +- vm-pages 134217728 +- 26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4 +- vm-max-threads 4 +- 27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 +- glueoutputbuf yes +- 28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法 +- hash-max-zipmap-entries 64 +- hash-max-zipmap-value 512 +- 29. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍) +- activerehashing yes +- 30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 +- include /path/to/local.conf +# 应用场景 +- 缓存 +- 计数 +- 消息队列 +- 排行榜 +- 社交网络 +# Lua脚本 +- Redis中lua脚本的执行是原子的,不可中断。 + +- + +# 与DB保持一致 +- 1、订阅数据库的binlog,比如阿里的canal +- 2、更新数据库后,异步更新缓存 +- 3、时间敏感数据可以设置很短的过期时间 +- 4、 +# 源码 +# 线程模型——单线程 +- 对于命令处理是单线程的,在IO层面同时面向多个客户端并发地提供服务,IO多路复用。 + +- 单线程为什么这么快 +1.- 纯内存 +2.- 非阻塞IO +3.- 避免线程切换和竞态消耗 +4.- 使用单线程要注意什么 +5.- 一次只能运行一条命令 + + +- 拒绝长(慢)命令 +1.- keys +2.- flushall +3.- flushdb +4.- slow lua script +5.- mutil/exec +6.- operate big value(collection) + +# RedisObject +- typedef struct redisObject { +- unsigned type:4; +- unsigned encoding:4; + +``` + unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ +``` + +- int refcount; +- void *ptr; +- } robj; + +1.- 4位的type表示具体的数据类型。Redis中共有5中数据类型。2^4 = 8足以表示这些类型。 +2.- 4位的encoding表示该类型的物理编码方式,同一种数据类型可能有不同的编码方式。目前Redis中主要有8种编码方式: + +``` +/* Objects encoding. Some kind of objects like Strings and Hashes can be +``` + +- * internally represented in multiple ways. The 'encoding' field of the object +- * is set to one of this fields for this object. */ + +``` +#define REDIS_ENCODING_RAW 0 /* Raw representation */ +``` + + +``` +#define REDIS_ENCODING_INT 1 /* Encoded as integer */ +``` + + +``` +#define REDIS_ENCODING_HT 2 /* Encoded as hash table */ +``` + + +``` +#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ +``` + + +``` +#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */ +``` + + +``` +#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ +``` + + +``` +#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */ +``` + + +``` +#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ +``` + +3.- lru字段表示当内存超限时采用LRU算法清除内存中的对象。 +4.- refcount表示对象的引用计数。 +5.- ptr指针指向真正的存储结构。 + + + +- ZSET底层是skiplist+hashtable或者ziplist From eeb8a1ce9d8026dd454c3592a54f52bed88648dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:49:08 +0800 Subject: [PATCH 88/97] =?UTF-8?q?Create=20=E5=8D=81=E5=85=AB=E3=80=81Rocke?= =?UTF-8?q?tMQ=E4=BD=BF=E7=94=A8=E4=B8=8E=E5=AE=9E=E7=8E=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\344\270\216\345\256\236\347\216\260.md" | 541 ++++++++++++++++++ 1 file changed, 541 insertions(+) create mode 100644 "docs/\345\215\201\345\205\253\343\200\201RocketMQ\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" diff --git "a/docs/\345\215\201\345\205\253\343\200\201RocketMQ\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" "b/docs/\345\215\201\345\205\253\343\200\201RocketMQ\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" new file mode 100644 index 00000000..f926fd12 --- /dev/null +++ "b/docs/\345\215\201\345\205\253\343\200\201RocketMQ\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260.md" @@ -0,0 +1,541 @@ +# RocketMQ +# 简介 +- RocketMQ 是一款分布式、队列模型的消息中间件,具有以下特点: +- 能够保证严格的消息顺序 +- 提供丰富的消息拉取模式 +- 高效的订阅者水平扩展能力 +- 实时的消息订阅机制 +- 亿级消息堆积能力 + +- 选用理由: +-  强调集群无单点,可扩展,任意一点高可用,水平可扩展。 +-  海量消息堆积能力,消息堆积后,写入低延迟。 +-  支持上万个队列 +-  消息失败重试机制 +-  消息可查询 +-  开源社区活跃 +-  成熟度(经过双十一考验) + +- + +# 基本概念 + + +# 功能 +# 发布订阅 + +# 消息优先级(支持多个队列,每个队列有着不同的优先级) + + +# 顺序消息(Producer单线程顺序发送,且发送到同一队列) + +- 缺点: + +# 消息过滤(Broker端过滤) + +# 消息持久化(文件) + + +# 消息可靠性(同步双写保证数据不丢,异步复制保证最多丢失少量数据) + + + +# 低延迟消息投递(长轮询) + + +# 消费确认(消费者消费后为消息进行确认) + +# 消息重复(无法保证消息不重复,业务需要保证幂等) + +- 消息有一个Key属性,用于消息去重,但是要手动保证。(比如存到数据库) +# 回溯消费 + +# 消息堆积 + + + +# 定时消息 + +# 消费重试 + + + + +# 部署 +# 物理结构 + + + + + +# 逻辑结构 + + +- + +- + +# 配置方式 +- 推荐使用双Master模式 +- 如果要严格保证实时,那么需要使用多主多从的方式(异步复制or同步双写)。 + + + + + + + + +- RT是响应时间的意思 + +# Producer使用 + +# 普通模式 +- send发送消息 +# 顺序模式 +- 负载均衡与顺序消费不冲突,如果采用顺序消费,那么消息会经由一个broker中转,而非多个。实现是通过单个队列实现的。Producer把消息发送到同一个队列(添加一个队列selector,保证该类消息都发送到某个队列中),Consumer从同一个队列中取消息进行消费。 +- SendResult sendResult = producer.send(msg, new MessageQueueSelector() { +- @Override + +``` + public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { +``` + +- Integer id = (Integer) arg; +- int index = id % mqs.size(); +- return mqs.get(index); +- } +- }, orderId); +# 事务模式 +- 流程图 +- MQ可以避免分布式事务 + + + + + + +- 事务(修改数据库)提交之前,先发送消息。 +- 数据库事务提交后,才发送消息。 +- 这样可以避免出现事务回滚而消息已发送的情况。 +- 这就是一个再次确认的过程。 + +- 关键 +- RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。 +- 为解决确认消息发送失败的问题(消息回查),RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。 +- sendMessageInTransaction方法的源码,总共分为3个阶段:发送Prepared消息、执行本地事务、发送确认消息。 + +``` +public TransactionSendResult sendMessageInTransaction(.....) { +``` + +- // 逻辑代码,非实际代码 +- // 1.发送消息 +- sendResult = this.send(msg); +- // sendResult.getSendStatus() == SEND_OK +- // 2.如果消息发送成功,处理与消息关联的本地事务单元 +- LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg); +- // 3.结束事务 +- this.endTransaction(sendResult, localTransactionState, localException); +- } +- endTransaction方法会将请求发往broker(mq server)去更新事务消息的最终状态: +- 1. 根据sendResult找到Prepared消息  +2. 根据localTransaction更新消息的最终状态 +- 如果endTransaction方法执行失败,导致数据没有发送到broker,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法,最后调用endTransactionOneway让broker来更新消息的最终状态。 + +- 在3.2.6以及之后版本中移除了消息回查的实现,所以此版本不支持事务消息。 +- Producer存在的问题(确认消息发送失败时的消息回查和消息发送失败) +- 其实事务消息开始是prepare状态,然后RMQ会将其持久化到MySQL当中,然后如果收到确认消息,就删除掉这条prepare消息,如果迟迟收不到确认消息,那么RMQ会定时的扫描prepare消息,发送给produce group进行回查确认! + +- 如果endTransaction方法执行失败,数据没有发送到broker,导致事务消息的状态更新失败,broker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用 +- DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法来决定是回滚事务还是继续执行,最后调用endTransactionOneway让broker来更新消息的最终状态。 + +- Producer的send方法本身支持内部重试,重试逻辑如下: +- 1. 至多重试3次。 +- 2. 如果发送失败,则轮转到下一个Broker。 +- 3. 这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。 + +- 所以,如果本身向broker发送消息产生超时异常,就不会再做重试。 +- 以上策略仍然不能保证消息一定发送成功,为保证消息一定成功,建议应用这样做: +- 如果调用send同步方法发送失败,则尝试将消息存储到db,由后台线程定时重试,保证消息一定到达Broker。 +- Consumer存在的问题(消费失败和消费超时) +- 会出现消费失败和消费超时两个问题,解决超时问题的思路就是一直重试,直到消费端消费消息成功,整个过程中有可能会出现消息重复的问题,按照前面的思路解决即可(去重表)。 +- 这样基本上可以解决消费端超时问题,但是如果消费失败怎么办?阿里提供给我们的解决方法是:人工解决。 + +- + +- 阿里提供的示例代码(基于闭源RocketMQ) +- Producer 执行事务 + +``` +public class TransactionProducerClient { +``` + + +``` + private final static Logger log = ClientLogger.getLog(); // 用户需要设置自己的 log, 记录日志便于排查问题 +``` + + + +``` + public static void main(String[] args) throws InterruptedException { +``` + +- final BusinessService businessService = new BusinessService(); // 本地业务 Service +- Properties properties = new Properties(); +- // 您在控制台创建的 Producer ID。注意:事务消息的 Producer ID 不能与其他类型消息的 Producer ID 共用 +- properties.put(PropertyKeyConst.ProducerId, ""); +- // 阿里云身份验证,在阿里云服务器管理控制台创建 +- properties.put(PropertyKeyConst.AccessKey, ""); +- // 阿里云身份验证,在阿里云服务器管理控制台创建 +- properties.put(PropertyKeyConst.SecretKey, ""); +- // 设置 TCP 接入域名(此处以公共云生产环境为例) +- properties.put(PropertyKeyConst.ONSAddr, +- "http://onsaddr-internal.aliyun.com:8080/rocketmq/nsaddr4client-internal"); + +- TransactionProducer producer = ONSFactory.createTransactionProducer(properties, +- new LocalTransactionCheckerImpl()); +- producer.start(); +- Message msg = new Message("Topic", "TagA", "Hello MQ transaction===".getBytes()); +- try { +- SendResult sendResult = producer.send(msg, new LocalTransactionExecuter() { +- @Override + +``` + public TransactionStatus execute(Message msg, Object arg) { +``` + +- // 消息 ID(有可能消息体一样,但消息 ID 不一样, 当前消息 ID 在控制台无法查询) +- String msgId = msg.getMsgID(); +- // 消息体内容进行 crc32, 也可以使用其它的如 MD5 +- long crc32Id = HashUtil.crc32Code(msg.getBody()); +- // 消息 ID 和 crc32id 主要是用来防止消息重复 +- // 如果业务本身是幂等的, 可以忽略, 否则需要利用 msgId 或 crc32Id 来做幂等 +- // 如果要求消息绝对不重复, 推荐做法是对消息体 body 使用 crc32或 md5来防止重复消息 +- Object businessServiceArgs = new Object(); +- TransactionStatus transactionStatus = TransactionStatus.Unknow; +- try { +- boolean isCommit = +- // 执行本地事务 +- businessService.execbusinessService(businessServiceArgs); +- if (isCommit) { +- // 本地事务成功、提交消息 +- transactionStatus = TransactionStatus.CommitTransaction; +- } else { +- // 本地事务失败、回滚消息 +- transactionStatus = TransactionStatus.RollbackTransaction; +- } +- } catch (Exception e) { +- log.error("Message Id:{}", msgId, e); +- } +- System.out.println(msg.getMsgID()); +- log.warn("Message Id:{}transactionStatus:{}", msgId, transactionStatus.name()); +- return transactionStatus; +- } +- }, null); +- } +- catch (Exception e) { +- // 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理 +- System.out.println(new Date() + " Send mq message failed. Topic is:" + msg.getTopic()); +- e.printStackTrace(); +- } +- // demo example 防止进程退出(实际使用不需要这样) +- TimeUnit.MILLISECONDS.sleep(Integer.MAX_VALUE); +- } +- } +- + +- Producer 消息回查 + +``` +public class LocalTransactionCheckerImpl implements LocalTransactionChecker { +``` + + +``` + private final static Logger log = ClientLogger.getLog(); +``` + +- final BusinessService businessService = new BusinessService(); + +- @Override + +``` + public TransactionStatus check(Message msg) { +``` + +- //消息 ID(有可能消息体一样,但消息 ID 不一样, 当前消息属于 Half 消息,所以消息 ID 在控制台无法查询) +- String msgId = msg.getMsgID(); +- //消息体内容进行 crc32, 也可以使用其它的方法如 MD5 +- long crc32Id = HashUtil.crc32Code(msg.getBody()); +- //消息 ID、消息本 crc32Id 主要是用来防止消息重复 +- //如果业务本身是幂等的, 可以忽略, 否则需要利用 msgId 或 crc32Id 来做幂等 +- //如果要求消息绝对不重复, 推荐做法是对消息体使用 crc32或 md5来防止重复消息. +- //业务自己的参数对象, 这里只是一个示例, 实际需要用户根据情况来处理 +- Object businessServiceArgs = new Object(); +- TransactionStatus transactionStatus = TransactionStatus.Unknow; +- try { +- boolean isCommit = businessService.checkbusinessService(businessServiceArgs); +- if (isCommit) { +- //本地事务已成功、提交消息 +- transactionStatus = TransactionStatus.CommitTransaction; +- } else { +- //本地事务已失败、回滚消息 +- transactionStatus = TransactionStatus.RollbackTransaction; +- } +- } catch (Exception e) { +- log.error("Message Id:{}", msgId, e); +- } +- log.warn("Message Id:{}transactionStatus:{}", msgId, transactionStatus.name()); +- return transactionStatus; +- } +- } + +- Consumer 远程事务 + +``` +public class ConsumerTest { +``` + + +``` + public static void main(String[] args) { +``` + +- Properties properties = new Properties(); +- // 您在控制台创建的 Consumer ID +- properties.put(PropertyKeyConst.ConsumerId, "XXX"); +- // AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建 +- properties.put(PropertyKeyConst.AccessKey, "XXX"); +- // SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建 +- properties.put(PropertyKeyConst.SecretKey, "XXX"); +- // 设置 TCP 接入域名(此处以公共云生产环境为例) +- properties.put(PropertyKeyConst.ONSAddr, +- "http://onsaddr-internal.aliyun.com:8080/rocketmq/nsaddr4client-internal"); +- // 集群订阅方式 (默认) +- // properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING); +- // 广播订阅方式 +- // properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING); + +- Consumer consumer = ONSFactory.createConsumer(properties); +- consumer.subscribe("TopicTestMQ", "TagA||TagB", new MessageListener() { //订阅多个 Tag + +``` + public Action consume(Message message, ConsumeContext context) { +``` + +- // 执行本地事务 +- System.out.println("Receive: " + message); +- return Action.CommitMessage; +- } +- }); + +- //订阅另外一个 Topic +- consumer.subscribe("TopicTestMQ-Other", "*", new MessageListener() { //订阅全部 Tag + +``` + public Action consume(Message message, ConsumeContext context) { +``` + +- System.out.println("Receive: " + message); +- return Action.CommitMessage; +- } +- }); + +- consumer.start(); +- System.out.println("Consumer Started"); +- } +- } +- 外围实现消息回查+消费端消息去重 + + +- A持有所有本地事务执行成功的消息,B持有远程事务执行成功、已被消费的消息,两者的差集为确认消息发送失败的消息。 +- 同时 +- A也知道超过重试发送次数的消息 +- B也知道超过重试消费次数的消息 +- 这两份消息都需要人工介入处理(重试或放弃,不能直接放弃,需要人工回滚A的本地事务)。 +- 当重试时,为了能更新A的数据,需要把消息状态设置为未被消费,并将sendTimes重置为0。 +- 当回滚时,需要把消息状态设置为已被回滚。 +- 注意A和B的数据不一定是最终一致的,比如A中某消息回滚后不会通知B(否则这又构成一个分布式事务),但起码A的数据是准确的。 +- A + - 1) db +producer_msg(msgId,body,message_status,create_time,update_time,send_times,topic) msgId这里为orderId + + - 2) mq +- 作为producer时,注册Topic :account:当执行本地事务时同时插入producer_msg,默认status都是未被消费。如果本地事务执行失败,那么直接回滚,不插入。当消息发送失败时,我们已经在producer_msg插入了记录,可以进行回查。 + + - 3) scheduler +- A需要同步B的数据库,使得两个数据库数据一致,不同的即为确认信息发送失败的。 +- 消息状态有未被消费、已被消费、消费失败、超过消费失败重试次数、超过确认消息发送失败的重试次数和已被回滚。 +- A和B数据库同步维护所有消息,只是A数据库保存内容更多,比如会保存消息的body。 + +- 如果消息已经是超过重试次数或已被消费,那么A不会再去考虑它。 +- A的Scheduler会遍历A数据库,找出未被消费和消费失败的id且创建时间距离当前时间超过1min,发送给B(作为一个消息,topic可以共用之前的topic,在消费端用keys去区分是事务消息还是回查消息;因为如果使用RPC的话会造成producer与consumer存在耦合,使用消息可以解耦)。 + +- B会遍历这些id +- for(id in ids){ +- 如果 id 不存在,说明确认消息发送失败, +- 如果 id 存在,则将该id对应的status一并返回,map.put(id,status) +- } +- 返回时也是发送一条消息,重新选取一个topic,比如是之前那个topic+”-check” + +- A 设置一个consumer,接收到topic+”check”的消息后,接收到map后,keySet取得所有id,拿发送过去的id减去这些id(差集),就是确认消息发送失败的消息,进行重新发送;遍历map,将本地数据库同步为B数据库。 +- +- 这个方法可能会出现消息重复,因为A刚发送消息,B还没有处理,A的Scheduler就去查询了,当然消息都没有被消费,因为A会重发刚才的消息,但是B有做消息去重,所以不会影响。 + + + +- B + - 1) db +- consumer_msg(msgId,create_time. message_status,topic) msgId这里是orderId + + - 2) mq +- 作为consumer,注册Topic: account: +- 当接收到消息后,查询是否被执行过,如果没有被消费过(id未找到)或者消费失败了,则执行远程事务后插入/更新consumer_ msg(status为已被消费),已被消费则跳过。 +- 远程事务执行失败时,插入/更新consumer_ msg(status为消费失败) +- 超过重试消费次数的消息也更新consumer_ msg,status为超过消费的重试次数。 +- B这里就维护它所接收的消息的状态。 +- + +- 消息管理系统 + +- 就Producer而言: +- 消息管理子系统就是在网页上查看数据库中存储的消息(包括所有发送过的消息)。 +- 按消息id搜索消息,按消息状态搜索消息,按topic搜索 + +- 而且还可以处理超过(确认消息)重试次数的消息,比如(批量)重新发送,或者回滚A事务。 +- 另外还可以处理超过(消费)重试次数的消息,比如重新发送消息,或者回滚A事务。 + +- 而RocketMQ等客户端只能看到成功发送的消息,不能查看未被确认发送的消息和重发次数。 + +- + +# 最佳实践 +## 发送消息 + + +## 发送消息失败 + + +## oneway + + +- + +# Consumer使用 +- 分类 + +- 一般使用PushConsumer。 +- PushConsumer是监听消息,而PullConsumer是主动拉取。 + +``` +@Slf4j +public class PullConsumer { + + private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>(); + + public static void main(String[] args) throws MQClientException { + + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5"); + consumer.setNamesrvAddr("192.168.0.113:9876;192.168.0.114:9876"); + consumer.start(); + Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest"); + for (MessageQueue mq : mqs) { + System.out.println("Consume from the queue: " + mq); + SINGLE_MQ: + while (true) { + try { + PullResult pullResult = + consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); + log.info("{}",pullResult); + putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); + switch (pullResult.getPullStatus()) { + case FOUND: + List<MessageExt> list = pullResult.getMsgFoundList(); + for (MessageExt msg : list) { + log.info("{}",new String(msg.getBody())); + } + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break SINGLE_MQ; + case OFFSET_ILLEGAL: + break; + default: + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + consumer.shutdown(); + } + + private static void putMessageQueueOffset(MessageQueue mq, long offset) { + offseTable.put(mq, offset); + } + + private static long getMessageQueueOffset(MessageQueue mq) { + Long offset = offseTable.get(mq); + if (offset != null) + return offset; + return 0; + } +} +``` + +- + +- 配置 + + +# 最佳实践 +## 消费端去重 + +## 消息消费并行度 +- + +## 批量消费 + + +- + +# 原理 +# 架构 + + +# 存储 +## 零拷贝(mmap或sendfile) + +## 文件系统 + +## 数据存储结构 + +- +- consume queue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件commit log上的位置。 +- 我们可以在配置中指定consumequeue与commitlog存储的目录 +每个topic下的每个queue都有一个对应的consumequeue文件,比如: +- ${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName} + +# 负载均衡 +## Producer负载均衡 + + +## Consumer负载均衡 +- +# 单队列并行消费 + +# 消息过滤 + + + +- +## Filter + + +- 总结: + + From bea74cfbeb46e28c2ab9f3f7c62bfdf0a95846e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:50:10 +0800 Subject: [PATCH 89/97] =?UTF-8?q?Create=20=E5=8D=81=E4=B9=9D=E3=80=81Sprin?= =?UTF-8?q?g=E4=BD=BF=E7=94=A8=E4=B8=8E=E5=AE=9E=E7=8E=B0=E6=80=BB?= =?UTF-8?q?=E7=BB=93.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\346\200\273\347\273\223.md" | 1021 +++++++++++++++++ 1 file changed, 1021 insertions(+) create mode 100644 "docs/\345\215\201\344\271\235\343\200\201Spring\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260\346\200\273\347\273\223.md" diff --git "a/docs/\345\215\201\344\271\235\343\200\201Spring\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260\346\200\273\347\273\223.md" "b/docs/\345\215\201\344\271\235\343\200\201Spring\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260\346\200\273\347\273\223.md" new file mode 100644 index 00000000..effac5e0 --- /dev/null +++ "b/docs/\345\215\201\344\271\235\343\200\201Spring\344\275\277\347\224\250\344\270\216\345\256\236\347\216\260\346\200\273\347\273\223.md" @@ -0,0 +1,1021 @@ +# IOC +## IOC/DI +- 依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念,具体的讲:当某个角色 需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在spring中 创建被调用者的工作不再由调用者来完成,因此称为控制反转。创建被调用者的工作由spring来完成,然后注入调用者,因此也称为依赖注入。 IOC侧重于原理,DI 侧重于实现。 +- 依赖倒置原则/面向接口编程:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 +## Spring介绍 +- Spring 是轻量级的JavaEE的开源框架,具有IOC/DI,AOP,MVC,TX,ORM等功能。 +- 优点是: +- DI有效的降低了耦合度。 +- AOP提供了通用的任务的集中管理。 +- ORM简化了对数据库的访问。 +- 低侵入式设计,代码污染极低。 +## BeanFactory +- BeanFactory 只是对IOC容器最基本行为作了定义,而不关心 Bean 是怎样定义和加载的。 +- 定义了一些getBean、containsBean、isSingleton、isPrototype等基本容器方法。 +## XmlBeanFactory +- xmlBeanFactory是BeanFactory的一个实现类。xmlBeanFactory 的功能是建立在 DefaultListablexmlBeanFactory 这个基本容器的基础上的,并在这个基本容器的基础上添加了其他诸如 xml 读取的附加功能。 +## Resource +- 仅仅使用 java 标准 java.net.URL 和针对不同 URL 前缀的标准处理器并不能满足我们对各种底层资源的访问,比如:我们就不能通过 URL 的标准实现来访问相对类路径或者相对 ServletContext 的各种资源。虽然我们可以针对特定的 url 前缀来注册一个新的 URLStreamHandler(和现有的针对各种特定前缀的处理器类似,比如 http:),然而这往往会是一件比较麻烦的事情(要求了解 url 的实现机制等),而且 url 接口也缺少了部分基本的方法,如检查当前资源是否存在的方法。 +- 相对标准 url 访问机制,Spring 的 Resource 接口对抽象底层资源的访问提供了一套更好的机制。 + +- Resource 是 Spring 中对外部资源的抽象,最常见的是文件的抽象,特别是 xml 文件,而且 Resource 里面通常是保存了 Spring 使用者的 Bean 定义。 +- 其实现类有:ByteArrayResouece,BeanDefinitionResource,InputStreamResource, ClassPathResource等 + +- ResourceLoader接口用于返回Resource对象;其实现可以看作是一个生产Resource的工厂类。Spring提供了一个适用于所有环境的DefaultResourceLoader实现,可以返回ClassPathResource、UrlResource。 +- ResourceLoader在进行加载资源时需要使用前缀来指定需要加载:“classpath:path”表示返回ClasspathResource,“http://path”和“file:path”表示返回UrlResource资源,如果不加前缀则需要根据当前上下文来决定,DefaultResourceLoader默认实现可以加载classpath资源。 + +- 对于目前所有ApplicationContext都实现了ResourceLoader,因此可以使用其来加载资源。 +- ClassPathXmlApplicationContext:不指定前缀将返回默认的ClassPathResource资源,否则将根据前缀来加载资源; +- WebApplicationContext:不指定前缀将返回ServletContextResource,否则将根据前缀来加载资源; +## ApplicationContext +- ApplicationContext是spring中较高级的容器。和BeanFactory类似,它可以加载配置文件中定义的bean,将所有的bean集中在一起,当有请求的时候分配bean。 另外,它增加了企业所需要的功能。 + +``` +public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, +``` + +- MessageSource, ApplicationEventPublisher, ResourcePatternResolver + +- 1.支持不同的信息源。继承了MessageSource接口,这个接口为ApplicationContext提供了很多信息源的扩展功能,比如:国际化的实现为多语言版本的应用提供服务。 +- 2.访问资源。这一特性主要体现在ResourcePatternResolver接口上,对Resource和ResourceLoader的支持,这样我们可以从不同地方得到Bean定义资源。这种抽象使用户程序可以灵活地定义Bean定义信息,尤其是从不同的IO途径得到Bean定义信息。这在接口上看不出来,不过一般来说,具体ApplicationContext都是继承了DefaultResourceLoader的子类。因为DefaultResourceLoader是AbstractApplicationContext的基类。 +- 3.支持应用事件。继承了接口ApplicationEventPublisher,为应用环境引入了事件机制,这些事件和Bean的生命周期的结合为Bean的管理提供了便利。 +- 4.附件服务。EnvironmentCapable里的服务让基本的Ioc功能更加丰富。 +- 5.ListableBeanFactory和HierarchicalBeanFactory是继承的主要容器。 +- 最常被使用的ApplicationContext接口实现类: +- 1,FileSystemXmlApplicationContext:该容器从XML文件中加载已被定义的bean。在这里,你需要提供给构造器XML文件的完整路径。 +- 2,ClassPathXmlApplicationContext:该容器从XML文件中加载已被定义的bean。在这里,你不需要提供XML文件的完整路径,只需正确配置CLASSPATH环境变量即可,因为,容器会从CLASSPATH中搜索bean配置文件。 +- 3,WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在XML文件中 +## ClassPathXmlApplicationContext + +## 容器初始化过程 +### Bean的注册 +- +以上的这些过程都发生在new ClassPathXmlApplicationContext中调用的AbstractApplicationContext#refresh方法中。 +- AbstractApplicationContext#refresh +- 逻辑: + - 1)初始化前的准备工作,比如对系统属性或者环境变量进行准备及验证 + - 2)初始化BeanFactory,并进行XML文件读取(component-scan->包括class文件) + - 3)对BeanFactory进行各种功能填充,比如@Qualifier和@Autowired + - 4)子类覆盖方法做额外的处理 + - 5)激活各种BeanFactory处理器 + - 6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean的时候 + - 7)为上下文初始化Message源,即为不同语言的消息体进行国际化处理 + - 8)初始化应用消息广播器,并放入applicationEventMulticaster bean中 + - 9)留给子类来初始化其他的bean +- 10)在所有注册的bean中查找listener bean,注册到消息广播器中 + - 11)初始化剩下的代理实例(非lazy-init)(bean的加载) + - 12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。 + +``` +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // 准备 刷新的上下文环境 + prepareRefresh(); + + // 初始化BeanFactory,并进行XML文件的读取 +``` + +- ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // 对BeanFactory进行各种功能填充 +- prepareBeanFactory(beanFactory); + + try { + // 子类覆盖方法做额外的处理 + postProcessBeanFactory(beanFactory); + + // 激活各种BeanFactory处理器 + invokeBeanFactoryPostProcessors(beanFactory); + + // 注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用是在getBean的时候 + registerBeanPostProcessors(beanFactory); + + // 为上下文初始化Message源,即不同语言的消息体,国际化处理 + initMessageSource(); + + // 初始化应用消息广播器,并放入applicationEventMulticaster bean中 + initApplicationEventMulticaster(); + + // 留给子类来初始化其他bean + onRefresh(); + + // 在所有注册的bean中查找Listener bean,注册到消息广播器中 + registerListeners(); + + // 初始化剩下的单例实例(除了lazy-init) + finishBeanFactoryInitialization(beanFactory); + + // 完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + destroyBeans(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + } + } +} + +#### obtainFreshBeanFactory(创建beanFactory,解析XML) +- 在后面会调用到loadBeanDefinitions(beanFactory)方法,该方法会创建XmlBeanDefinitionReader,后面会调用reader#loadBeanDefinitions方法。 + +- 该方法会: + - 1)Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); +- 将applicationContext.xml转为Resource。 + - 2)然后会调用Resource#getInputStream,然后将该资源转为XML的Document对象。 +- 最终是使用jaxp的dom方式读取的XML配置文件。(JAXP是一种标准,SUN API对其提供了实现) + - 3)使用BeanDefinitionDocumentReader#registerBeanDefinitions将XML对应的Document对象转为BeanDefinitions。 +- 最终会调用parseBeanDefinitions(解析标签),它会分开解析默认标签和自定义标签。 +- 默认标签是import、alias、bean和beans四种。 +- 自定义标签会先找到其对应的parser,比如component-scan标签对应着ComponentScanBeanDefinitionParser。该Parser会调用 +- PathMatchingResourcePatternResolver#getResources从包名得到Resource封装了的class文件(底层是调用了File API)。MetadataReader会读入class文件,如果有@Component注解,那么返回其对应的BeanDefinition。 + - 4)标签解析完毕后会将beanName和beanDefinition作为key和value放入beanfactory的beanDefinitionMap中。 + +- + +### Bean的加载 +- 在Bean的注册refresh中除了调用了obtainFreshBeanFactory完成了XML2BeanDefinition,还调用了finishBeanFactoryInitialization(初始化非lazy-load且singleton的bean)(BeanDefinition2BeanInstance),该方法会获取所有beanName,逐个调用getBean方法。 + +#### FactoryBean(工厂Bean,用户定制) +- Spring通过反射机制利用bean的class属性指定实现类来实例化bean。 +- Spring提供了一个FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化bean的逻辑。 +- FactoryBean + +``` +public interface FactoryBean<T> { +``` + +- // 返回bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单例缓存池中 + T getObject() throws Exception; + Class<?> getObjectType(); + boolean isSingleton(); +} +- FactoryBean: +- 这个接口使你可以提供一个复杂的逻辑来生成Bean。它本质是一个Bean,但这个Bean不是用来注入到其它地方像Service、Dao一样使用的,它是用来生成其它Bean使用的。实现了这个接口后,Spring在容器初始化时,把实现这个接口的Bean取出来,使用接口的getObject()方法来生成我们要想的Bean。当然,那些生成Bean的业务逻辑也要写getObject()方法中。 +- 其返回的对象不是指定类的一个实例,其返回的是该工厂Bean的getObject方法所返回的对象。创建出来的对象是否属于单例由isSingleton中的返回决定。 +- 使用场景:1、通过外部对类是否是单例进行控制,该类自己无法感知 2、在创建Object对象之前进行初始化的操作,在afterPropertiesSet()中完成。(一次性的初始化,保存在成员变量中,并不是每次getObject都会调用afterPropertiesSet,afterPropertiesSet只会被调用一次) + +#### ObjectFactory(Spring使用) + +``` +public interface ObjectFactory<T> { + T getObject() throws BeansException; +} +``` + + + +- ObjectFactory: +- 它的目的也是作为一个工厂,来生成Object(这个接口只有一个方法getObject())。这个接口一般被用来,包装一个factory,通过个这工厂来返回一个新实例(prototype类型)。这个接口和FactoryBean有点像,但FactoryBean的实现是被当做一个SPI(Service Provider Interface)实例来使用在BeanFactory里面;ObjectFactory的实现一般被用来注入到其它Bean中,作为API来使用。就像ObjectFactoryCreatingFactoryBean的例子,它的返回值就是一个ObjectFactory,这个ObjectFactory被注入到了Bean中,在Bean通过这个接口的实例,来取得我们想要的Bean。 +- 总的来说,FactoryBean和ObjectFactory都是用来取得Bean,但使用的方法和地方不同,FactoryBean被配置好后,Spring调用getObject()方法来取得Bean,ObjectFactory配置好后,在Bean里面可以取得ObjectFactory实例,需要我们手动来调用getObject()来取得Bean。 +#### InitializingBean +- InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法。 +- + +#### doGetBean +- 中有三个方法非常关键:getSingleton,createBean和getObjectForBeanInstance。 +- 逻辑梳理: + - 1)getSingleton(beanName) +- 直接尝试从缓存获取或者从singletonFactories中的ObjectFactory中获取sharedInstance。 +- 如果已经创建过了,那么调用getObjectForBeanInstance返回对应的实例(从缓存中只得到了bean的原始状态,还需要对bean进行实例化)。 + - 2)没有创建过,那么检查BeanFactory中是否包含该beanName对应的beanDefinition,没有找到则从父BeanFactory中继续查找,直至找到为止(递归)。 + - 3)如果存在依赖的bean,那么递归实例化所依赖的bean + - 4)根据scope分别处理: + + - 4.1) 如果是singleton,那么调用getSingleton(beanName,objectFactory)(记录加载状态,通过this.singletonsCurrentlyInCreation.add(beanName)将当前正在创建的bean记录在缓存中,这样便可以对循环依赖进行检测)得到sharedInstance,其中调用了createBean、之后调用getObjectForBeanInstance返回对应的实例。 + - 4.2)如果是prototype,那么调用createBean,之后调用getObjectForBeanInstance。 + - 4.3) 如果是其他的scope,那么调用scope#get(beanName,objectFactory)得到scopedInstance,其中调用了createBean,之后调用getObjectForBeanInstance。 + + - 5)createBean方法(创建单例或多例的bean): +- AbstractBeanDefinition#prepareMethodOverrides(验证及准备覆盖的方法,决定实例化策略->反射 or CGLIB) +- 如果beanDefinition#getMethodOverrides()为空,即用户没有使用replace或lookup的配置方法,那么直接使用反射的方式;如果使用了,需要将这两个配置通过的功能切入进去,所以就必须要使用CGLIB动态代理的方式将包含两个特性所对应的逻辑的拦截增强器设置进去,这样才可以保证在调用方法的时候会被相应的拦截器增强,返回值为包含拦截器的代理实例。 +- 给BeanPostProcessor一个机会来返回代理来替代真正的实例,与AOP有关。如果该方法返回bean不为空,则跳过后续实际创建bean的过程,直接返回代理后的bean。 +- doCreateBean + - 1)如果是单例,则需要首先清除缓存factoryBeanInstanceCache + - 2)实例化bean,将BeanDefinition转换为BeanWrapper。 +- 转换过程: +- ①如果存在工厂方法(factory-method),则使用工厂方法进行初始化(instantiateUsingFactoryMethod) +- ②一个类有多个构造函数,每个构造函数都有不同的参数,所以需要根据参数锁定构造函数并进行初始化(autowireConstructor) +- ③如果既不存在工厂方法也不存在带有参数的构造函数,则使用默认的构造函数进行bean的初始化(instantiateBean)。 + - 3)MergedBeanDefinitionPostProcessor的应用 +- bean合并后的处理,Autowired注解正是通过此方法实现诸如类型的预解析。 + - 4)更新singletonFactories + - 5)属性填充populateBean +- 属性填充时,会autowireByName(按名获取待注入的属性)或autowireByType(按类型获取待注入的属性) +- 填充完毕后会调用客户定制的初始化方法。除了使用配置init-method外,还可以使自定义的bean实现InitializingBean接口,并在afterPropertiesSet中实现自己的初始化业务逻辑。 +- init-method和afterPropertiesSet都是在ininitalBean时执行,执行顺序是afterPropertiesSet先执行,init-method后执行,在它们前后分别执行BeanPostProcesser的两个方法。 + - 6)代理bean循环依赖检查 + - 7)注册DisposableBean +- 如果配置了destroy-method,这里需要注册以便于在销毁时候调用。 + - 6)getObjectForBeanInstance +- 无论是从缓存中获取到的bean还是通过不同的scope策略加载的bean都只是最原始的bean状态,并不一定是我们最终想要的bean。 +- 比如,我们需要对FactoryBean进行处理,那么这里得到的其实是FactoryBean的初始状态,但是我们真正需要的是FactoryBean中定义的factory-method(getObject方法)方法中返回的bean,而getObjectForBeanInstance就是完成这个工作的。 + +## 注入方式 +- Spring的IOC有三种注入方式 : +- 第一是根据属性注入,也叫setter注入; +- 第二种是根据构造方法进行注入; +- 第三种是工厂注入,分为静态工厂注入和动态工厂注入 +- <bean id="car1" class="com.lzj.spring.beans.factory.StaticFactory" +- factory-method="getCar"> +- <constructor-arg value="baoma"></constructor-arg> +- </bean> +- 在XML文件中配置bean时,要指定class的属性为工厂的类;factory-method属性指定工厂类中工厂方法,用于创建bean;constrctor-arg用于给工厂方法传递参数。 + +## 循环依赖 + +- Spring容器循环依赖包括构造器循环依赖和setter循环依赖。 +- 核心就是singletonObjects、singletonFactories、earlySingletonObjects和singletonsCurrentlyInCreation四个集合。 + - 1)singletonObjects是beanName与beanInstance的Map,是真正的缓存,beanInstance是构造完毕的,凡是正常地构造完毕的单例bean都会放入缓存中。 + - 2)earlySingletonObjects也是beanName与beanInstance的Map,beanInstance是已经调用了createBean方法,但是没有清除加载状态和加入至缓存的bean。仅在当前bean创建时存在,用于检测代理bean循环依赖。 + - 3)singleFactories是beanName与ObjectFactory的Map,仅在当前bean创建时存在,是尚未调用createBean的bean。用于setter循环依赖时实现注入。 + - 4)singletonsCurrentlyInCreation是beanName的集合,用于检测构造器循环依赖。 + +- getBean在循环依赖时所执行的步骤是这样的: + - 1)检测当前bean是否在singletonObjects中,在则直接返回缓存好的bean;不在则检测是否在singletonFactories中,在,则调用其getObject方法,返回,并从singletonFactories中移除,加入到earlySingletonObjects中。 + + - 2)正常创建,beforeSingletonCreation:检测当前bean是否在singletonsCurrentlyInCreation,如果存在,抛出异常。表示存在构造器循环依赖。如果不存在,则将当前bean加入。 + + - 3)bean初始化,分为构造方法初始化、工厂方法初始化和简单初始化。如果是构造方法初始化,那么递归地获取参数bean。其他情况不会递归获取bean。 + + - 4)addSingletonFactory:如果当前bean不在singletonObjects中,则将当前bean加入到singletonFactories中,并从earlySingletonObjects中移除。 + + - 5)填充属性,简单初始化的话会递归创建所依赖的bean。 + - 6)调用用户初始化方法,比如BeanPostProcesser、InitializingBean、init-method,有可能返回代理后的bean。 + - 6) 检测循环依赖,如果当前bean在singletonObjects中,则判断当前bean(current bean)与singletonObjects中的bean(cached bean)是否是同一个,如果不是,那么说明当前bean是被代理过的,由于依赖当前bean的bean持有的是对cached bean的引用,这是不被允许的,所以会抛出BeanCurrentlyInCreationException异常。 + + - 7)afterSingletonCreation:将当前bean从singletonsCurrentlyInCreation中删除 + - 8)addSingleton:将当前bean加入到singletonObjects,然后从singletonFactories, earlySingletonObjects中移除,结束 +### 构造器循环依赖 +- 表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。 +- 1、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有,于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,没有,则将A放入。构造方法初始化时需要B实例(A尚未放入到singletonFactories中),于是调用了getBean(B)方法、 +- 2、Spring容器创建单例“B” Bean,首先检测singletonFactories是否包含B,发现没有,于是正常创建,然后检测B是否包含在singletonsCurrentlyInCreation中,没有,则将B放入。构造方法初始化时需要C实例(B尚未放入到singletonFactories中),于是调用了getBean(C)方法、 +- 3、Spring容器创建单例“C” Bean,首先检测singletonFactories是否包含C,发现没有,于是正常创建,然后检测C是否包含在singletonsCurrentlyInCreation中,没有,则将C放入。构造方法初始化时需要A实例(C尚未放入到singletonFactories中),于是调用了getBean(A)方法、 +- 4、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,有,抛出BeanCurrentlyInCreationException异常。 +### setter循环依赖 +- 表示通过setter注入方式构成的循环依赖。 +- 对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。 +- 1、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有,于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,没有,则将A放入。将A放入到singletonFactories中。注入属性时需要B实例,于是调用了getBean(B)方法、 +- 2、Spring容器创建单例“B” Bean,首先检测singletonFactories是否包含B,发现没有,于是正常创建,然后检测B是否包含在singletonsCurrentlyInCreation中,没有,则将B放入。将B放入到singletonFactories中。注入属性时需要C实例,于是调用了getBean(C)方法、 +- 3、Spring容器创建单例“C” Bean,首先检测singletonFactories是否包含C,发现没有,于是正常创建,然后检测C是否包含在singletonsCurrentlyInCreation中,没有,则将C放入。将C放入到singletonFactories中。注入属性时需要A实例,于是调用了getBean(A)方法、 +- 4、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现有,于是返回缓存了的bean,并将A从singletonFactories删除,返回A实例。 +- 5、C得到A实例。set进来,B、A也是这样。结束。 + +- 对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。 + +## 常用注解 + - 1)将使用了以下注解的类纳入到Spring的容器管理中。 +- @Component +- @Controller +- @Service +- @Repository +- @Scope注解 作用域 + - 2)注入注解 +- @Resource 默认按名称装配(autowireByName),如果没有指定name属性,当注解写在字段上时,默认取字段名。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。 +- @Autowired 默认按类型装配(autowireByType),也可以搭配@Qualifier实现按名称装配。 +## Bean的作用域 +- IOC容器中bean默认是单例的 +- 可以使用scope配置作用域 +- 作用域有两种:singleton,prototype(每次获取都创建一个新的bean) + +## Bean生命周期 +- 有三种方式在Bean初始化后和销毁前添加一些操作。 +- Bean的方法加上@PostConstruct和@PreDestroy注解(必须有component-scan才有效) +- 在xml中定义init-method和destory-method方法 +- Bean实现InitializingBean和DisposableBean接口 +- +其执行顺序关系为: + +- Bean在实例化的过程中:constructor > @PostConstruct >InitializingBean > init-method + +- Bean在销毁的过程中:@PreDestroy > DisposableBean > destroy-method + +- 还可以通过BeanPostProcessor在bean初始化前后添加一些操作。 +- 初始化执行顺序为:constructor > BeanPostProcessor#postProcessBeforeInitialization > @PostConstructor > InitializingBean > init-method > BeanPostProcessor# postProcessAfterInitialization + +- 调用用户自定义初始化方法之前和之后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法。 +- 参见getBean中的initializeBean方法。 + +### 示例 + +``` +public class Car implements InitializingBean, DisposableBean { + + public Car() { + System.out.println("constructor..."); + } + + + public void initCar() { + System.out.println("init-method..."); + } + + @PostConstruct + public void postConstructMethod() { + System.out.println("post construct .."); + } + + @PreDestroy + public void beforeDestroy() { + System.out.println("before destroy..."); + } + + public void destroyCar() { + System.out.println("destroy-method...."); + } + + + @Override + public void destroy() throws Exception { + System.out.println("disposable bean..."); + } + + @Override + public void afterPropertiesSet() throws Exception { + System.out.println("initializing bean..."); + } +} +``` + +- + + +``` +public class MyBeanPostProcessor implements BeanPostProcessor{ + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + System.out.println("postProcessAfterInitialization "+ bean +" ,"+ beanName); + return bean; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + System.out.println("postProcessBeforeInitialization "+ bean +" ,"+ beanName); + return bean; + } +} +``` + + + +``` +public class Main { + public static void main(String[] args) { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans-cycle.xml"); + Car car = (Car) ctx.getBean("car"); + System.out.println(car); + ctx.close(); + } +} +``` + + +- <context:component-scan base-package="cycle" ></context:component-scan> +<bean id="car" class="cycle.Car" init-method="initCar" destroy-method="destroyCar"> +</bean> +<bean class="scope.MyBeanPostProcessor"></bean> + +- 输出为: +- constructor... +- postProcessBeforeInitialization cycle.Car@2db7a79b ,car +- post construct .. +- initializing bean... +- init-method... +- postProcessAfterInitialization cycle.Car@2db7a79b ,car +- cycle.Car@2db7a79b +- // 关闭了 +- before destroy... +- disposable bean... +- destroy-method.... + +- + +## 自动装配 + +- <?xml version="1.0" encoding="UTF-8"?> +- <beans xmlns="http://www.springframework.org/schema/beans" +- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +- xmlns:p="http://www.springframework.org/schema/p" +- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> +- <bean id="car" class="autowire.Car" +- p:brand="audi" p:price="300000"></bean> +- <bean id="address" class="autowire.Address" +- p:city="Nanjing" p:street="Xianlin"></bean> +- <bean id="person" class="autowire.Person" +- p:name="NewSong" p:car-ref="car" p:addr-ref="address"></bean> +- </beans> +- 不使用自动装配 + +- 可以使用p命名空间中的autowire属性,它的值是byName或者byType +- 如果是byName,就会根据当前这个Bean的类的属性名(由setter分隔出来的)在xml文件中找同名(id)的bean,如果是,那么就匹配。 +- 因此,我们推荐将id设置为首字母小写的类名(类的属性也是如此) +- 并且这个autowire可以将多个符合的装配,不仅仅是匹配一个;如果没有匹配的就不装配。 +- 如果是byType,就会根据Bean的类型和当前Bean的属性的类型进行匹配,前提是每个类只可以对应一个Bean。如果有重名,那么会出错。 +- 而对byName而言,可以修改setter的方法名,使得由setter分隔出来的属性名与id一致。 + +- autowire一旦使用,就会对所有的引用属性使用自动装配;不能同时使用byType和byName. +- 实际上很少使用自动装配 +- <bean id="car" class="autowire.Car" +- p:brand="audi" p:price="300000"></bean> +- <bean id="address" class="autowire.Address" +- p:city="Nanjing" p:street="Xianlin"></bean> +- <bean id="person" class="autowire.Person" +- p:name="NewSong" autowire="byName"></bean> +- 注意Person持有了Car和Address,只在Person上加autowire即可 +- + +# AOP +## 简介 + + +## AOP术语 +- 1、切面(aspect/advisor) +- 类是对物体特征的抽象,切面就是对横切关注点的抽象。组合了Pointcut与Advice,在Spring中有时候也称为Advisor。 +- 2、连接点(join point) +- 被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。 +- 3、切入点(pointcut) +- 描述的一组符合某个条件的join point,通常使用pointcut表达式来限定join point。 +- 4、通知(advice) +- 所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、返回、环绕通知五类。 +- 5、目标对象 +- 代理的目标对象 +- 6、织入(weave) +- 将Advice织入join point的这个过程 +- 7、引介(introduction) +- 在不修改代码的前提下,引介可以在运行期为类动态地添加一些方法或字段 +## Advisor +- 通知Advice是Spring提供的一种切面(Aspect)。但其功能过于简单,只能将切面织入到目标类的所有目标方法中,无法完成将切面织入到指定目标方法中。 +- 顾问Advisor是Spring提供的另一种切面。其可以完成更为复杂的切面织入功能。PointcutAdvisor是顾问的一种,可以指定具体的切入点。顾问对通知进行了包装,会根据不同的通知类型,在不同的时间点,将切面织入到不同的切入点。 +- Advisor组合了Pointcut与Advice。 +- 除了引介Advisor外,几乎所有的advisor都是PointcutAdvisor。 + +``` +public interface Advisor { + Advice getAdvice(); + /** + * @return whether this advice is associated with a particular target instance + */ + boolean isPerInstance(); +} +``` + + + + +``` +public interface PointcutAdvisor extends Advisor { + + /** + * Get the Pointcut that drives this advisor. + */ + Pointcut getPointcut(); + +} +``` + + +## Advice + +``` +public interface Advice { + +} +``` + + +- 增强(advice)主要包括如下五种类型 +- 1. 前置增强(BeforeAdvice):在目标方法执行前实施增强 +- 2. 后置增强(AfterAdvice):在目标方法执行后(无论是否抛出遗产)实施增强 +- 3. 环绕增强(MethodInterceptor):在目标方法执行前后实施增强 +- 4. 异常抛出增强(ThrowsAdvice):在目标方法抛出异常后实施增强 +- 5. 返回增强(AfterReturningAdvice):在目标方法正常返回后实施增强 +- 6. 引介增强(IntroductionIntercrptor):在目标类中添加一些新的方法和属性 +## JDK动态代理与CGLIB代理 +- JDK动态代理: +- 其代理对象必须是某个接口的实现,它是通过在运行时创建一个接口的实现类来完成对目标对象的代理 +- CGLIB代理:在运行时生成的代理对象是针对目标类扩展的子类。 +- CGLIB是高效的代码生产包,底层是依靠ASM操作字节码实现的,性能比JDK强。 +- 相关标签 +- <aop:aspectj-autoproxy proxy-target-class=”true”/> +- true表示使用CGLIB代理。 +## 源码 +### 解析AOP标签 +- <aop:aspectj-autoproxy /> +- 解析配置文件时,一旦遇到aspectj-autoproxy注解时就会使用解析器 +- AspectJAutoProxyBeanDefinitionParser进行解析。 +- 解析结果是注册了一个bean:AnnotationAwareAspectJAutoProxyCreator。 + + + +### 创建AOP代理 +- 针对于实现了InstantiationAwareBeanPostProcessor接口的BeanPostProcessor,在doCreateBean之前调用,如果需要进行代理,则直接返回代理后的bean。 +- 前面解析AOP标签时注册了AnnotationAwareAspectJAutoProxyCreator,它继承了AbstractAdvisorAutoProxyCreator,是一个InstantiationAwareBeanPostProcessor。 + +- +### wrapIfNecessary +- 逻辑: + - 1)获取可以应用到该bean的所有advisor + - 2)创建代理 +- AbstractAutoProxyCreator.createProxy + +- 在获取了所有对应的bean的增强后,便可以进行代理的创建了。 +- 逻辑: + - 1)获取当前类中的属性 + - 2)添加代理接口 + - 3)封装Advisor并加入到ProxyFactory中 + - 4)设置proxyFactory的target要代理的类 + - 5)进行获取代理操作 ProxyFactory#getProxy + +- createAopProxy().getProxy(classLoader); + +- 如果目标对象实现了接口,默认情况下会使用JDK的动态代理 +- 如果目标对象实现了接口,可以强制使用CGLIB(proxy-target-class=false) +- 如果目标对象没有实现接口,必须采用CGLIB + +- + +# Transaction +## 事务 +- 编程式事务:所谓编程式事务指的是通过编码方式实现事务,即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate。 +- 声明式事务:管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。 +- 显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。 +- 声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。 + +- AOP可以帮助实现声明式事务,将事务管理从业务代码中分离 + +- • Spring 从不同的事务管理 API 中抽象了一整套的事务机制. 开发人员不必了解底层的事务 API, 就可以利用这些事务机制. 有了这些事务机制, 事务管理代码就能独立于特定的事务技术了. +- • Spring 的核心事务管理抽象是PlatformTransactionManager,它为事务管理封装了一组独立于技术的方法. 无论使用 Spring 的哪种事务管理策略(编程式或声明式), 事务管理器都是必须的。 + +## 事务管理器 + +- 这些都是PlatformTransactionManager +## 使用注解声明式管理事务 +•- 除了在带有切入点, 通知和增强器的 Bean 配置文件中声明事务外, Spring 还允许简单地用 @Transactional 注解来标注事务方法. +•- 为了将方法定义为支持事务处理的, 可以为方法添加 @Transactional 注解. 根据 Spring AOP 基于代理机制, 只能标注公有方法. +•- 可以在方法或者类级别上添加 @Transactional 注解. 当把这个注解应用到类上时, 这个类中的所有公共方法都会被定义成支持事务处理的. +•- 在 Bean 配置文件中只需要启用 <tx:annotation-driven> 元素, 并为之指定事务管理器就可以了. +•- 如果事务处理器的名称是 transactionManager, 就可以在<tx:annotation-driven> 元素中省略 transaction-manager 属性. 这个元素会自动检测该名称的事务处理器. +## 事务隔离级别 + +- 设置回滚事务属性 +- 默认情况下只对RuntimeException和Error回滚 +- 继承自Exception的不会回滚 +- rollbackFor和noRollbackFor可以设置对某些特定异常是否回滚 +- 值是一个Class对象的数组 + + + +## 事务传播行为 +- 当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行. +- 事务的传播行为可以由传播属性指定. Spring 定义了 7种类传播行为. + +### PROPAGATION_REQUIRED(重要) +- 如果当前存在一个事务,则加入当前事务;如果不存在任何事务,则创建一个新的事务。总之,要至少保证在一个事务中运行。PROPAGATION_REQUIRED通常作为默认的事务传播行为。 +- REQUIRED(内层事务newTransaction为false,内层事务回滚时仅设置回滚标记,外层事务进行外层回滚) +### PROPAGATION_SUPPORTS +- 如果当前存在一个事务,则加入当前事务;如果当前不存在事务,则直接执行。 对于一些查询方法来说,PROPAGATION_SUPPORTS通常是比较合适的传播行为选择。 如果当前方法直接执行,那么不需要事务的支持;如果当前方法被其他方法调用,而其他方法启动了一个事务的时候,使用PROPAGATION_SUPPORTS可以保证当前方法能够加入当前事务并洞察当前事务对数据资源所做的更新。 比如说,A.service()会首先更新数据库,然后调用B.service()进行查询,那么,B.service()如果是PROPAGATION_SUPPORTS的传播行为, 就可以读取A.service()之前所做的最新更新结果,而如果使用稍后所提到的PROPAGATION_NOT_SUPPORTED,则B.service()将无法读取最新的更新结果,因为A.service()的事务在这个时候还没有提交(除非隔离级别是read uncommitted)。 +### PROPAGATION_MANDATORY +- PROPAGATION_MANDATORY强制要求当前存在一个事务,如果不存在,则抛出异常。 如果某个方法需要事务支持,但自身又不管理事务提交或者回滚的时候,比较适合使用 +- PROPAGATION_MANDATORY。 +### PROPAGATION_REQUIRES_NEW(重要,两事务完全独立,独立提交、独立回滚) +- 不管当前是否存在事务,都会创建新的事务。如果当前存在事务的话,会将当前的事务挂起(suspend)。 如果某个业务对象所做的事情不想影响到外层事务的话,PROPAGATION_REQUIRES_NEW应该是合适的选择,比如,假设当前的业务方法需要向数据库中更新某些日志信息, 但即使这些日志信息更新失败,我们也不想因为该业务方法的事务回滚而影响到外层事务的成功提交,因为这种情况下,当前业务方法的事务成功与否对外层事务来说是无关紧要的。 +- REQUIRES_NEW(内外层事务平级,内层事务newTransaction为true,suspend外层事务,抛出异常后内层事务进行内层回滚,resume外层事务,外层事务捕获到内层抛出的异常后进行外层回滚) +### PROPAGATION_NOT_SUPPORTED +- 不支持当前事务,而是在没有事务的情况下执行。如果当前存在事务的话,当前事务原则上将被挂起(suspend),但要依赖于对应的PlatformTransactionManager实现类是否支持事务的挂起(suspend),更多情况请参照TransactionDefinition的javadoc文档。 PROPAGATION_NOT_SUPPORTED与PROPAGATION_SUPPORTS之间的区别,可以参照PROPAGATION_SUPPORTS部分的实例内容。 +### PROPAGATION_NEVER +- 永远不需要当前存在事务,如果存在当前事务,则抛出异常。 + +### PROPAGATION_NESTED(重要,内部事务是外部事务的子事务。子事务出现异常会回滚到savepoint,外部事务提交/回滚时子事务也进行提交/回滚) +- 如果存在当前事务,则在当前事务的一个嵌套事务中执行,否则与PROPAGATION_REQUIRED的行为类似,即创建新的事务,在新创建的事务中执行。 PROPAGATION_NESTED粗看起来好像与PROPAGATION_REQUIRES_NEW的行为类似,实际上二者是有差别的。 PROPAGATION_REQUIRES_NEW创建的新事务与外层事务属于同一个“档次”,即二者的地位是相同的,当新创建的事务运行的时候,外层事务将被暂时挂起(suspend); 而PROPAGATION_NESTED创建的嵌套事务则不然,它是寄生于当前外层事务的,它的地位比当前外层事务的地位要小一号,当内部嵌套事务运行的时候,外层事务也是处于active状态。是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交,外部事务回滚,它也会被回滚。 + +- NESTED(内外层事务嵌套,内层事务newTransaction为false,并创建还原点,抛出异常后rollback至还原点,外层事务捕获到内层抛出的异常后进行外层回滚) +## 超时和只读属性 +•- 由于事务可以在行和表上获得锁, 因此长事务会占用资源, 并对整体性能产生影响. +•- 如果一个事物只读取数据但不做修改, 数据库引擎可以对这个事务进行优化. +•- 超时事务属性: 事务在强制回滚之前可以保持多久. 这样可以防止长期运行的事务占用资源. +•- 只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务. + +- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性; +- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持 +- read-only="true"表示该事务为只读事务,比如上面说的多条查询的这种情况可以使用只读事务,由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。 + - (1)在JDBC中,指定只读事务的办法为: connection.setReadOnly(true); + - (2)在Hibernate中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER); +- 此时,Hibernate也会为只读事务提供Session方面的一些优化手段 + - (3)在Spring的Hibernate封装中,指定只读事务的办法为: bean配置文件中,prop属性增加“read-Only”或者用注解方式@Transactional(readOnly=true) +- Spring中设置只读事务是利用上面两种方式(根据实际情况) +- 在将事务设置成只读后,相当于将数据库设置成只读数据库,此时若要进行写的操作,会出现错误。 + +## 源码 + +### 解析事务标签 +- <tx:annotation-driven /> +- 同AOP标签,需要一个对应的BeanDefinitionParser,AnnotationDrivenBeanDefinitionParser。 + +- InfrastructureAdvisorAutoProxyCreator作为一个AbstractAutoProxyCreator,会在getBean时调用其postProcessAfterInstantiation方法,该方法会创建事务代理。 +- 我们之前注册了BeanFactoryTransactionAttributeSourceAdvisor这个类,这个类实现了Advisor接口。BeanFactoryTransactionAttributeSourceAdvisor作为一个advisor,用于对事务方法进行增强。只要类或方法实现了@Transactional接口,该Advisor一定会被加到拦截器链中,对原方法进行事务增强。 +- 返回的Advisor类型是BeanFactoryTransactionAttributeSourceAdvisor,而其beanName是TransactionInterceptor。 + +- 如果方法中存在事务属性(TransactionAttribute),则使用方法上的属性,否则使用方法所在的类上的属性。如果方法所在类的属性上还是没有搜寻到对应的事务属性,那么再搜寻接口中的方法,再没有的话,最后尝试搜寻接口的类上面的声明。 +- 解析@Transactional中的各个属性,并封装到TransactionAttribute中返回。 +### 创建事务代理 +- 对于标记了@Transactional的方法而言,会被代理,增强事务功能。 +- 这些方法的Advisor增强中包括了TransactionInterceptor +- (BeanFactoryTransactionAttributeSourceAdvisor对应的bean)。 +- TransactionInterceptor支撑着整个事务功能的架构,它继承了MethodInterceptor。 + +- TransactionAspectSupport#invokeWithinTransaction(最主要的方法) +- 逻辑: + - 1)获取事务属性 TransactionAttribute + - 2)加载配置中的TransactionManager + - 3)不同的事务处理方式使用不同的逻辑,就声明式事务而言,会获取方法信息并创建事务信息TransactionInfo(此时已经创建了事务) +- 事务信息(TransactionInfo)与事务属性(TransactionAttribute)并不相同。 +- 前者包含了后者,且包含了其他事务信息,比如PlatformTransactionManager以及TransactionStatus相关信息。 + - 4)try:执行原始方法 + - 5)catch:异常,回滚事务,再次抛出异常,7)及以后的不会执行 + - 6)finally:清除事务信息 + - 7)提交事务 + - 8)返回原始方法的返回值 + +#### 开启事务 +- 逻辑: + - 1)获取事务,创建对应的事务实例 + - 2)如果当前线程存在事务,那么根据传播行为进行相应处理,处理完成后返回 +- 值得注意的有两点: + - 1)REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动。而如果有一个事务正在运行的话,则在这个方法运行期间被挂起(suspend)。 + - 2)NESTED表示如果当前正在有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚。如果封装事务不存在,行为就像REQUIRES_NEW。 +- Spring主要有两种处理NESTED的方式: +- 首选设置保存点的方式作为异常处理的回滚 +- JTA无法使用保存点,那么处理方式和REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。 + + - 3)事务超时的验证 + - 4)事务传播行为的验证 + - 5)构建DefaultTransactionStatus,创建当前事务的状态 + - 6)完善transaction,包括设置ConnectionHolder、隔离级别、timeout,如果是新连接,则绑定到当前线程 + - 7)将事务信息记录在当前线程中。 + +#### 回滚事务 +- 一旦事务执行失败,Spring会通过TransactionInfo实例来进行回滚等后续工作。 + - 1)默认情况下判断是否回滚默认的依据是 抛出的异常是否是RuntimeException或者是Error的类型 + - 2)如果不满足回滚条件,即使抛出异常也会提交。 + - 3)有保存点则回滚到保存点,是新事务则回滚整个事务;存在事务又不是新事务,则做回滚标记,等到事务链执行完毕后统一回滚。 + - 4)将挂起事务恢复。 + +#### 提交事务 +- 某个事务是另一个事务的嵌入事务,但是这些事务又不在Spring的管理范围之内,或者无法设置保存点,那么Spring会通过设置回滚标识的方式来禁止提交。首先当某个嵌入事务发生回滚的时候会设置回滚标识,而等到外部事务提交时,一旦判断出当前事务流被设置了回滚标识,则由外部事务来统一进行整体事务的回滚。 + +- 在提交过程中也不是直接提交的,而是考虑了诸多方面。 +- 符合提交的条件如下: +- 当事务状态中有保存点信息的话便不会提交事务; +- 当事务不是新事务的时候也不会提交事务 + +- 原因是: +- 对于内嵌事务,在Spring中会将其在开始之前设置保存点,一旦内嵌事务出现异常便根据保存点信息进行回滚,但是如果没有出现异常,内嵌事务并不会单独提交,而是根据事务流由最外层事务负责提交,所以如果当前存在保存点信息便不是最外层事务,不做提交操作。 + +# MVC +## 面试题 +### 与Struts的区别 +- SpringMVC的入口是一个Servlet即前端控制器,而Struts2入口是一个Filter过虑器。 +- SpringMVC是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 +### SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决 +- 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段 +### SpingMVC中的控制器的注解一般用那个,有没有别的注解可以替代 +- 一般用@Conntroller注解,表示是表现层,不能用用别的注解代替. +### @RequestMapping注解用在类上面有什么作用 +- 用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 +### 怎么样把某个请求映射到特定的方法上面 +- 直接在方法上面加上注解@RequestMapping,并且在这个注解里面写上要拦截的路径 +### 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置 +- 可以在@RequestMapping注解里面加上method=RequestMethod.GET +### 如果在拦截请求中,我想拦截提交参数中包含"type=test"字符串,怎么配置 +- 可以在@RequestMapping注解里面加上params="type=test" +### 我想在拦截的方法里面得到从前台传入的参数,怎么得到 +- 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样 +### 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象 +- 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面 +### 怎么样在方法里面得到Request,或者Session +- 直接在方法的形参中声明request,SpringMVC就自动把request对象传入 +### SpringMVC中函数的返回值是什么. +- 返回值可以有很多类型,有String, ModelAndView,当一般用String比较好 +### SpringMVC怎么处理返回值的 +- SpringMVC根据配置文件中InternalResourceViewResolver的前缀和后缀,用前缀+返回值+后缀组成完整的返回值 +### SpringMVC怎么样设定重定向和转发的 +- 在返回值前面加"forward:"就可以让结果转发,譬如"forward:user.do?name=method4" 在返回值前面加"redirect:"就可以让返回值重定向,譬如"redirect:http://www.baidu.com" +- forward/redirect:除了跳转至某个页面,也可以跳转至某个controller处理方法。 +- 并且使用时需要以/开头, 默认是相对于项目的根目录的,不加就是相对于当前controller的@RequestMapping值的路径(没有这个注解就是相对于项目根目录)。 + +### SpringMVC用什么对象从后台向前台传递数据的 +- 通过ModelMap对象,可以在这个对象里面用put方法,把对象加到里面,前台就可以通过el表达式拿到 +### SpringMVC中有个类把视图和数据都合并的一起的,叫什么 +- 叫ModelAndView +### 怎么样把ModelMap里面的数据放入Session里面 +- 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key +### SpringMVC怎么和AJAX相互调用的 +- 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象 +- 具体步骤如下 +- 1.加入Jackson.jar +- 2.在配置文件中配置json的映射 +- 3.在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解 +### 当一个方法向AJAX返回特殊对象,譬如Object,List等,需要做什么处理 +- 要加上@ResponseBody注解 +- + +## Mapping +### @RequestMapping(映射) + +- RequestMapping是根据该访问路径来决定调用哪个方法来处理,访问路径是由类上的RequestMapping+方法上的RequestMapping决定的。 +- 返回值是转发到哪个路径下,转发路径是由 prefix+返回值+suffix决定的。 +### @PathVariable(注入) + + +### @RequestParam(注入) + +### @RequestHeader(注入) + +### @CookieValue(注入) + +### 使用POJO对象绑定请求参数值(注入) + + +### 使用Servlet原生API作为参数(注入) + + + +### 数据绑定(注入)/类型转换/校验 + + + +#### WebDataBinder + +#### 数据转换 + +#### 数据格式化 + +- 格式化和类型转换是同时进行的 + +#### 数据校验 + + + + + +#### HttpMessageConverter + + + + + + +- HttpMessageConverter使用 + + +- + +## Model +- +### ModelAndView + + +- 在handle方法中获取视图和模型,然后将域对象存放到视图和模型(ModelAndView)中。 +- 默认是放到request域中。 +### Map&Model + +- 参数的真实类型为ExtendedModelMap对象的子类,既实现了Model接口,又继承了ModelMap类 +- 该参数的类型也可以写为Model或者ModelMap,不过一般使用Map类型就足够了 + +## View +- <!-- 配置视图解析器 将controller方法返回值解析为实际的物理视图--> +- <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> +- <property name="prefix" value="/WEB-INF/views/"></property> +- <property name="suffix" value=".jsp"></property> +- </bean> + +- 解析流程 + +- 无论返回值是一个ModelAndView、String、View,SpringMVC都会将其转为一个ModelAndView对象。 + +- 通过Controller实现URL映射得到ModelAndView,然后ViewResolver实例化View对象,得到物理视图,然后调用View的render方法,将Model数据渲染到View中,然后转发至View。 +### 视图 +- 每个请求都会创建一个新的视图对象 + + +### 视图解析器 + + +### InternalResourceViewResolver + +- InternalResourceView +- InternalResourceViewResolver +- 这两个接口实现和JSP有关的渲染和转发功能。 +### annotation-driven + +- 前两个bean是与RequestMapping有关,第三个bean与异常处理有关。 +- 还可以处理数据类型转换、数据格式化、数据校验、AJAX。 + +- 以下是DispatcherServlet的持有的handlerAdapters的数据情况: + + +### default-servlet-handler + +## 异常处理 +### HandlerExceptionResolver + + + +### ExceptionHandlerExceptionResolver + +- 可以把处理异常的代码放到一个类中,比如ExceptionControllerAdvice +- 为这个类加一个注解@ControllerAdvice + +- 当在当前类找不到对应的注解了@ExceptionHandler的方法,那么将会在注解了@ControllerAdvice的类中注解了@ExceptionHandler方法去处理。 + +## 拦截器 +### 使用 +- 第一步:定义一个拦截器,实现HandlerInterceptor接口 + +``` +public class FirstInterceptor implements HandlerInterceptor { +``` + + +- @Override + +``` + public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) +``` + +- throws Exception { +- System.out.println("[FirstInterceptor] afterCompletion"); +- } +- @Override + +``` + public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) +``` + +- throws Exception { +- System.out.println("[FirstInterceptor] postHandle"); +- } +- @Override + +``` + public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception { +``` + +- System.out.println("[FirstInterceptor] preHandle"); +- return true; +- } +- } + +- 第二步:在springmvc配置文件中进行如下配置: +- <mvc:interceptors> +- <!-- 配置自定义拦截器 --> +- <bean class="me.newsong.interceptors.FirstInterceptor"></bean> +- <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean> +- </mvc:interceptors> + +- [FirstInterceptor] preHandle +- //controller处理方法被调用 +- [FirstInterceptor] postHandle +- [FirstInterceptor] afterCompletion + +### 顺序 + +- 类似于Filter,当返回true时,继续检测其他拦截器,返回false,直接调到放行过的拦截器的afterComplication方法。 +- preHandle是在调用处理方法之前调用 +- postHandle是在调用处理方法之后,渲染视图之前被调用。 +- afterCompletion是在渲染视图之后被调用 +- preHandle:权限;日志;事务等 +- handle +- postHandle 修改request域的属性;修改视图 +- render +- afterCompltion 释放资源 + +- 对于preHandle方法,按照拦截器的配置顺序的顺序执行 +- 对于postHandle方法,按照拦截器的配置顺序的逆序执行 +- afterComplication同postHandle方法 + + +## 源码 + +- DispatcherServlet: SpringMVC总的拦截器 +- HandlerMapping:请求和处理器之间的映射,用于获取HandlerExecutionChain +- HandlerExecutionChain:持有一组Interceptor和实际请求处理器HandlerAdapter,负责执行Interceptor的各个方法和处理方法。 +- HandlerAdapter:实际的请求处理器,处理后返回ModelAndView +- HandlerExceptionResolver:异常处理器,当拦截器的postHandle方法调用后检查异常。 +- ViewResolver:视图解析器,解析视图名,得到View,由逻辑视图变为物理视图。 +- View :有render方法,渲染视图 +- 渲染完毕后调用转发 + +### 初始化ApplicationContext +- ContextLoaderListener(入口) +- ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。 +- 因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。 +- 在ContextLoaderListener中的核心逻辑是初始化WebApplicationContext实例并存放在ServletContext中。 + +``` +public void contextInitialized(ServletContextEvent event) { + initWebApplicationContext(event.getServletContext()); +} +``` + + +- 在初始化过程中,程序首先会读取ContextLoader类的同目录下的配置文件ContextLoader.properties,并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。 + + +``` +private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties"; +``` + +- 在ContextLoader目录下有一个配置文件ContextLoader.properties +- 内容为: +- org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext +### 初始化DispatcherServlet +- 第一次访问网站时,会初始化访问到的servlet。 +- 初始化阶段会调用servlet的init方法,在DispatcherServlet中是由其父类HttpServletBean实现的。 +- 逻辑: + - 1)封装及验证初始化参数 解析init-param并封装到PropertyValues中 + - 2)将DispatcherServlet转化为BeanWrapper实例 + - 3)注册相对于Resource的属性编辑器 + - 4)PropertyValues属性注入 + - 5)servletBean的初始化(initServletBean) + +- FrameworkServlet#initServletBean调用了FrameworkServlet#initWebApplicationContext +- 创建或刷新WebApplicationContext实例,并对servlet功能所使用的变量进行初始化 + +- 逻辑: + - 1)对已经创建的WebApplicationContext实例进行配置和刷新 +- (configureAndRefreshWebApplicationContext) +- 最后会调用ApplicationContext中的refresh方法(与IOC的衔接) + - 2)刷新Spring在Web功能实现中必须使用的全局变量(onRefresh) +- 初始化各个模块: +- // 初始化文件上传模块 + initMultipartResolver(context); +- // 初始化国际化模块 + initLocaleResolver(context); +- // 初始化主题模块 + initThemeResolver(context); +- // 初始化HandlerMappings + initHandlerMappings(context); +- // 初始化HandlerAdapters + initHandlerAdapters(context); +- // 初始化HandlerExceptionResolvers + initHandlerExceptionResolvers(context); + initRequestToViewNameTranslator(context); +- // 初始化ViewResolvers + initViewResolvers(context); + initFlashMapManager(context); + +- 重点注意初始化HandlerMappings和HandlerAdapters。 + +- 初始化HandlerMappings是将Controller的各个请求方法封装为HandlerMethod,注册到mappingRegistry(key是RequestMappingInfo,value是MappingRegistration,它封装了之前创建的handlerMethod)中。 +- +初始化HandlerAdapters是获取所有实现了HandlerAdapter接口的类保存到成员变量handlerAdapters中。 +- RequestMappingHandlerAdapter(请求映射处理器适配器)作为例子,初始化时会去初始化argumentResolvers、returnValueHandlers等。 +### 处理请求 +- FrameworkServlet#service(入口)所有请求都会调用processRequest方法。 +- 逻辑: + - 1)为了保证当前线程的LocaleContext和RequestAttributes可以在当前请求处理完毕后还能恢复,提取当前线程的两个属性 + - 2)根据当前request创建对应的LocaleContext和RequestAttributes,并绑定到当前线程 + - 3)委托给doService(DispatcherServlet#doService)方法进一步处理 + - 4)请求处理结束后恢复线程到原始状态 + - 5)请求处理结束后无论成功与否都会发布事件通知 + +- doService的主体是在doDispatch方法中进行。 +- 逻辑: + - 1)处理文件上传请求 + - 2)将handler实例(即HandlerMethod)和所有匹配的拦截器封装到HandlerExecutionChain mappedHandler中。如果没有找到,那么根据策略抛出异常或直接返回错误码。 + - 3)getHandlerAdapter(根据HandlerExecutionChain获取可以支持它的HandlerAdapter) +- 如果是HandlerMethod,那么返回的是RequestMappingHandlerAdapter。 + - 4)调用HandlerExecutionChain mappedHandler的applyPreHandle + - 5)执行HandlerAdapter的handle方法,处理请求。 +- 比如RequestMappingHandlerAdapter#handleInternal, +- 注入参数 getMethodArgumentValues +- 遍历请求参数,然后委托给HandlerMethodArgumentResolver#resolveArgument进行参数解析,先解析,后使用WebDataBinder进行数据转换(如有必要) +- 执行方法 +- 处理返回值 如果返回值是普通的对象(@ResponseBody),那么handler是 +- RequestResponseBodyMethodProcessor。 + + - 6)转换视图名称,加上前缀和后缀 + - 7)调用HandlerExecutionChain mappedHandler的applyPostHandle + - 8)processDispatchResult(处理ModelAndView请求结果) +- 如果返回的是纯数据(@ResponseBody),mv就是null,该方法基本上是空方法。 +- 处理异常:使用handlerExceptionResolver来处理异常 +- 如果handler处理结果中返回了view,那么需要对页面进行渲染,调用render方法渲染视图(需要跳转的话,在render中进行跳转)。 From 613029739f031680afedcb57e1ad2bfb42d91fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:51:10 +0800 Subject: [PATCH 90/97] =?UTF-8?q?Create=20=E4=BA=8C=E5=8D=81=E3=80=81Sprin?= =?UTF-8?q?g=E6=BA=90=E7=A0=81=E8=A7=A3=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\347\240\201\350\247\243\346\236\220.md" | 10170 ++++++++++++++++ 1 file changed, 10170 insertions(+) create mode 100644 "docs/\344\272\214\345\215\201\343\200\201Spring\346\272\220\347\240\201\350\247\243\346\236\220.md" diff --git "a/docs/\344\272\214\345\215\201\343\200\201Spring\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/docs/\344\272\214\345\215\201\343\200\201Spring\346\272\220\347\240\201\350\247\243\346\236\220.md" new file mode 100644 index 00000000..8f3503a5 --- /dev/null +++ "b/docs/\344\272\214\345\215\201\343\200\201Spring\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -0,0 +1,10170 @@ +- Spring Source +- 他人总结 + - Spring1)ioc 容器——BeanFactory 是最原始的 ioc 容器,有以下方法 1.getBean2.判断是否有 Bean,containsBean3.判断是否单例 isSingleton。BeanFactory 只是对 ioc 容器最基本行为作了定义,而不关心 Bean 是怎样定义和加载的。如果我们想要知道一个工厂具体产生对象的过程,则要看这个接口的实现类。在 spring 中实现这个接口有很多类,其中一个 是xmlBeanFactory。 +- xmlBeanFactory 的功能是建立在 DefaultListablexmlBeanFactory 这个基本容器的基础上的,并在这个基本容器的基础上实行了其他诸如 xml 读取的附加功能。 xmlBeanFactory(Resource resource)构造函数,resource 是 spring 中对与外部资源的抽象,最常见的是文件的抽象,特别是 xml 文件,而且 resource 里面通常是保存了 spring 使用者的 Bean 定义,eg.applicationContext.xml 在被加载时,就会被抽象为 resource 处理。 +- [我自己理解, resource 就是定义 Bean 的 xml 文件]。 + - loc 容器建立过程:1)创建 ioc 配置文件的抽象资源,这个抽象资源包含了 BeanDefinition 的定义信息。2)创建一个 BeanFactory,这里使用的是 DefaultListablexmlBeanFactory。3)创建一个载入 BeanDefinition 的读取器,这里使用 xmlBeanDefinitionReader 来载入 xml 文件形式的 BeanDefinition。4)然后将上面定义好的 resource 通过一个回调配置给 BeanFactory。 + - 5)从资源里读入配置信息,具体解析过程由 xmlBeanDefinitionReader 完成。6)ioc 容器建立起来。 +- BeanDefinition 类似于 resource 接口的功能,起到的作用就是对所有的 Bean 进行一层抽象的统一,把形式各样的对象统一封装为一个便于 spring 内部进行协调管理和调度的数据结构。BeanDefinition 屏蔽了不同对象对于 spring 框架的差异。 +- Resource 里有 inputStream。 +- 解析 xml,获得 document 对象,接下来只要再对 document 结构进行分析便可知道 Bean 在 xml 中是怎么定义的,也就可以将其转化为 BeanDefinition 对象。我们配置的 Bean 的信息经过解析,在 spring 内部已经转换为 BeanDefinition 这种统一的结构,但这些数据还不能供 ioc 容器直接使用,需要在 ioc 容器中对这些 BeanDefinition 数据进行注册,注册完成的 BeanDefinition,都会以 BeanName 为 Key,BeanDefinition 为 value,交由 map 管理。注册完之后,一个 ioc 容器就可以用了。 +- 自己理解的,xml 文件抽象为 resource 对象,Bean 抽象为 BeanDefinition 对象。 + - 2) 依赖注入——依赖注入发生在 getBean 方法中,getBean 又调用 dogetBean 方法。 getBean 是依赖注入的起点,之后调用 createBean 方法,创建过程又委托给了 docreateBean 方法。在 docreateBean 中有两个方法:1)createBeanInstance,生成 Bean 包含的 java 对象 2)populateBean 完成注入。在创建 Bean 的实例中,getInstantiationstrategy 方法挺重要,该方法作用是获得实例化策略对象,也就是指通过哪种方案进行实例化的过程。spring 当中提供两种方案进行实例化:BeanUtils 和 cglib。BeanUtils 实现机制是 java 反射,cglib 是一个第三方类库,采用的是一种字节码加强方式。Spring 中默认实例化策略为 cglib。populateBean 进行依赖注入,获得 BeanDefinition 中设置的 property 信息,简单理解依赖注入的过程就是对这些 property 进行赋值的过程,在配置 Bean 的属性时,属性可能有多种类型,我们在进行注入的时候,不同类型的属性我们不能一概而论地进行处理。集合类型属性和非集合类型属性差别很大,对不同的类型应该有不同的处理过程。所以要先判断 value 类型,再调用具体方法。 + - 3) aop——将那些与业务无关,却为业务模块所公共调用的逻辑或责任封装起来,称其为 aspect,便于减少系统的重复代码。使用模块技术,aop 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。实现 aop 的两大技术:1)采用动态代理,利用截取消息的方式,对该消息进行装饰,以获取原有对象行为的执行。2)采用静态织入,引入特定的语法创建切面,从而可以使编译器可在编译期间织入有关切面的代码。 +- spring 提供两种方式生成代理对象,jdkProxy 和 cglib。默认的策略是,若目标类是接口则用 jdk 动态代理技术,否则使用 cglib 生成代理。在 jdk 动态代理中使用 Proxy.newProxyInstance()生成代理对象( JdkDynamicAopProxy 类的 getProxy 方法), JdkDynamicAopProxy 也实现了 invocationhandler 接口,有 invoke 方法,就是在该方法中实现了切片织入。主流程可以简述为:获取可应用到此方法上的通知链(Interceptor chain),若有,则应用通知,并执行 joinpoint,若没有,则直接反射执行 joinpoint。 +- Introduction 是指给一个已有类添加方法或字段属性,Introduction 还可以在不改变现有类代码的情况下,让现有 java 类实现新的接口,或为其指定一个父类实现多继承,相对于 advice 可以动态改变程序的功能或流程来说,Introduction 用来改变类的静态结构。 +- 拦截器,是对连接点进行拦截,从而在连接点前后加入自定义的切面模块功能。作用于同一个连接点的多个拦截器组成一个拦截器链,拦截器链上的每一个拦截器,通常会调用下一个拦截器。 +- 连接点,程序执行过程中的行为,比如方法调用或特定异常被抛出。 +- 切入点,指定一个 advice 将被引发的一系列的连接点的集合。aop 框架必须允许开发者指定切入点。 +- 通知(advice):在特定的连接点,aop 框架执行的动作。Spring 以拦截器作通知模型,维护一个围绕连接点的拦截器链。 +- 拦截器(advisor),自己理解,在 invoke 前后加入的方法就是通知。使用 spring 的 PointCutadvisor,只拦截特定的方法,一个 advisor 定义订一个 PointCut 和一个 advice,满足 PointCut(指定哪些方面需要拦截),则执行相应的 advice(定义了增强的功能)。 +- PointCutadvisor 有 两 个 常 用 实 现 类 : NameMatchMethodPointCutadvisor 和 regexMethodPointCutadvisor。前者需要注入 mappedname 和 advice 属性,后者需要注入 pattern 和 advice 属性。mappedname 指明要拦截的方法名,pattern 按照正则表达式的方法指明了要拦截的方法名,advice 定义一个增强,即要加入的操作(需要自己实现 MethodBeforeAdvice、MethodafterAdvice、throwAdvice、Methodinterceptor 接口之一),然后在 ProxyBeanFactory 的拦截器中注入这个 PointCutadvisor。注:一个 ProxyFactoryBean 只能指定一个代理目标。 +- 在 spring 中配置 aop 很麻烦,首先需要编写 xxxadvice 类(需要自己实现 MethodBeforeAdvice、MethodafterAdvice、throwAdvice、Methodinterceptor 接口之一),然后在 xml 配置 advisor。还要在 advisor 中注入 advice,然后将 advisor 加入 ProxyFactoryBean 中。而在 spring2.x 以后引入了 aspect 注解,只需要定义一个 aspect 类,在 aspect 中声明 advice 类(可同时声明多个),然后在 xml 配置这个 aspect 类,最后添加一行<aop: aspect j-auto proxy>就可以搞定。 +- 通知类型 接口 描述 +- 前置通知 MethodBeforeAdvice 在目标方法调用前调用 +- 后置通知 MethodafterAdvice 在目标方法调用后调用 +- 异常通知 throwAdvice 在目标方法抛出异常时调用 +- 环绕通知 Methodinterceptor 拦截对目标方法调用 +- 还有一类是引入通知,用来定义切入点的。 + +- + +- Spring IOC +- IOC=ConfigReader+ReflectionUtil +- 容器继承体系 + + +- 1、从接口BeanFactory到HierarchicalBeanFactory,再到ConfigurableBeanFactory,这是一条主要的BeanFactory设计路径。在这条接口设计路径中,BeanFactory,是一条主要的BeanFactory设计路径。在这条接口设计路径中,BeanFactory接口定义了基本的Ioc容器的规范。在这个接口定义中,包括了getBean()这样的Ioc容器的基本方法(通过这个方法可以从容器中取得Bean)。而HierarchicalBeanFactory接口在继承了BeanFactory的基本接口后,增加了getParentBeanFactory()的接口功能,使BeanFactory具备了双亲Ioc容器的管理功能。在接下来的ConfigurableBeanFactory接口中,主要定义了一些对BeanFactory的配置功能,比如通过setParentBeanFactory()设置双亲Ioc容器,通过addBeanPostProcessor()配置Bean后置处理器,等等。通过这些接口设计的叠加,定义了BeanFactory就是最简单的Ioc容器的基本功能。 + +- 2、第二条接口设计主线是,以ApplicationContext作为核心的接口设计,这里涉及的主要接口设计有,从BeanFactory到ListableBeanFactory,再到ApplicationContext,再到我们常用的WebApplicationContext或者ConfigurableApplicationContext接口。我们常用的应用基本都是org.framework.context 包里的WebApplicationContext或者ConfigurableApplicationContext实现。在这个接口体系中,ListableBeanFactory和HierarchicalBeanFactory两个接口,连接BeanFactory接口定义和ApplicationContext应用的接口定义。在ListableBeanFactory接口中,细化了许多BeanFactory的接口功能,比如定义了getBeanDefinitionNames()接口方法;对于ApplicationContext接口,它通过继承MessageSource、ResourceLoader、ApplicationEventPublisher接口,在BeanFactory简单Ioc容器的基础上添加了许多对高级容器的特性支持。 + +- 3、.这个接口系统是以BeanFactory和ApplicationContext为核心设计的,而BeanFactory是Ioc容器中最基本的接口,在ApplicationContext的设计中,一方面,可以看到它继承了BeanFactory接口体系中的ListableBeanFactory、AutowireCapableBeanFactory、HierarchicalBeanFactory等BeanFactory的接口,具备了BeanFactory Ioc容器的基本功能;另一方面,通过继承MessageSource、ResourceLoader、ApplicationEventPublisher这些接口,BeanFactory为ApplicationContext赋予了更高级的Ioc容器特性。对于ApplicationContext而言,为了在Web环境中使用它,还设计了WebApplicationContext接口,而这个接口通过继承ThemeSource接口来扩充功能。 + +- BeanFactory(容器接口) + + +``` +public interface BeanFactory { +``` + +- //这里是对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象 +- String FACTORY_BEAN_PREFIX = "&"; +- //这里根据bean的名字,在IOC容器中得到bean实例,这个IOC容器就是一个大的抽象工厂。 +- Object getBean(String name) throws BeansException; +- //这里根据bean的名字和Class类型来得到bean实例,和上面的方法不同在于它会抛出异常:如果根据名字取得的bean实例的Class类型和需要的不同的话。 +- <T> T getBean(String name, Class<T> requiredType); +- <T> T getBean(Class<T> requiredType) throws BeansException; +- Object getBean(String name, Object... args) throws BeansException; +- //这里提供对bean的检索,看看是否在IOC容器有这个名字的bean +- boolean containsBean(String name); +- //这里根据bean名字得到bean实例,并同时判断这个bean是不是单件 +- boolean isSingleton(String name) throws NoSuchBeanDefinitionException; +- //这里根据bean名字得到bean实例,并同时判断这个bean是不是原型 +- boolean isPrototype(String name) throws NoSuchBeanDefinitionException; +- //这里对得到bean实例的Class类型 +- Class<?> getType(String name) throws NoSuchBeanDefinitionException; +- //这里得到bean的别名,如果根据别名检索,那么其原名也会被检索出来 +- String[] getAliases(String name); +- } +- XmlBeanFactory(基础容器实现) + +``` +public class XmlBeanFactory extends DefaultListableBeanFactory { + private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); + + + /** + * Create a new XmlBeanFactory with the given resource, + * which must be parsable using DOM. + * @param resource XML resource to load bean definitions from + * @throws BeansException in case of loading or parsing errors + */ + public XmlBeanFactory(Resource resource) throws BeansException { + this(resource, null); + } + + /** + * Create a new XmlBeanFactory with the given input stream, + * which must be parsable using DOM. + * @param resource XML resource to load bean definitions from + * @param parentBeanFactory parent bean factory + * @throws BeansException in case of loading or parsing errors + */ + public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); + } + +} +``` + +- 作为简单ioc容器系列最底层的实现XmlBeanFactory是建立在DefaultListableBeanFactory容器的基础之上的,并在这个基本容器的基础上实现了其他诸如xml读取的附加功能。 + +- Resource接口体系 + +- 仅仅使用 java 标准 java.net.URL 和针对不同 URL 前缀的标准处理器并不能满足我们对各种底层资源的访问,比如:我们就不能通过 URL 的标准实现来访问相对类路径或者相对 ServletContext 的各种资源。虽然我们可以针对特定的 url 前缀来注册一个新的 URLStreamHandler(和现有的针对各种特定前缀的处理器类似,比如 http:),然而这往往会是一件比较麻烦的事情(要求了解 url 的实现机制等),而且 url 接口也缺少了部分基本的方法,如检查当前资源是否存在的方法。 +- 相对标准 url 访问机制,Spring 的 Resource 接口对抽象底层资源的访问提供了一套更好的机制。 + +- Resource 是 Spring 中对外部资源的抽象,最常见的是文件的抽象,特别是 xml 文件,而且 Resource 里面通常是保存了 Spring 使用者的 Bean 定义。 +- 其实现类有:ByteArrayResouece,BeanDefinitionResource,InputStreamResource, ClassPathResource等 + +- ResourceLoader接口用于返回Resource对象;其实现可以看作是一个生产Resource的工厂类。Spring提供了一个适用于所有环境的DefaultResourceLoader实现,可以返回ClassPathResource、UrlResource。 +- ResourceLoader在进行加载资源时需要使用前缀来指定需要加载:“classpath:path”表示返回ClasspathResource,“http://path”和“file:path”表示返回UrlResource资源,如果不加前缀则需要根据当前上下文来决定,DefaultResourceLoader默认实现可以加载classpath资源。 + +- 对于目前所有ApplicationContext都实现了ResourceLoader,因此可以使用其来加载资源。 +- ClassPathXmlApplicationContext:不指定前缀将返回默认的ClassPathResource资源,否则将根据前缀来加载资源; +- WebApplicationContext:不指定前缀将返回ServletContextResource,否则将根据前缀来加载资源; +- + +- ApplicationContext接口(高级容器接口) +- ApplicationContext是spring中较高级的容器。和BeanFactory类似,它可以加载配置文件中定义的bean,将所有的bean集中在一起,当有请求的时候分配bean。 另外,它增加了企业所需要的功能,比如,从属性文件中解析文本信息和将事件传递给所指定的监听器。这个容器在org.springframework.context.ApplicationContext接口中定义。ApplicationContext包含BeanFactory所有的功能,一般情况下,相对于BeanFactory,ApplicationContext会被推荐使用。 +- 特点 +- 1.支持不同的信息源。继承了MessageSource接口,这个接口为ApplicationContext提供了很多信息源的扩展功能,比如:国际化的实现为多语言版本的应用提供服务。 +- 2.访问资源。这一特性主要体现在ResourcePatternResolver接口上,对Resource和ResourceLoader的支持,这样我们可以从不同地方得到Bean定义资源。这种抽象使用户程序可以灵活地定义Bean定义信息,尤其是从不同的IO途径得到Bean定义信息。这在接口上看不出来,不过一般来说,具体ApplicationContext都是继承了DefaultResourceLoader的子类。因为DefaultResourceLoader是AbstractApplicationContext的基类。 +- 3.支持应用事件。继承了接口ApplicationEventPublisher,为应用环境引入了事件机制,这些事件和Bean的生命周期的结合为Bean的管理提供了便利。 +- 4.附件服务。EnvironmentCapable里的服务让基本的Ioc功能更加丰富。 +- 5.ListableBeanFactory和HierarchicalBeanFactory是继承的主要容器。 +- 实现 +- 最常被使用的ApplicationContext接口实现类: +- 1,FileSystemXmlApplicationContext:该容器从XML文件中加载已被定义的bean。在这里,你需要提供给构造器XML文件的完整路径。 +- 2,ClassPathXmlApplicationContext:该容器从XML文件中加载已被定义的bean。在这里,你不需要提供XML文件的完整路径,只需正确配置CLASSPATH环境变量即可,因为,容器会从CLASSPATH中搜索bean配置文件。 +- 3,WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在XML文件中 +- ClassPathXmlApplicationContext(高级容器实现) + + + +- + + + +- + + +``` +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } +} +``` + +- Bean的注册 + +- AbstractApplicationContext#refresh(bean注册) +- refresh是AbstractApplicationContext中方法。 + +- 逻辑: + - 1)初始化前的准备工作,比如对系统属性或者环境变量进行准备及验证 + - 2)初始化BeanFactory,并进行XML文件读取(component-scan->包括class文件) + - 3)对BeanFactory进行各种功能填充,比如@Qualifier和@Autowired + - 4)子类覆盖方法做额外的处理 + - 5)激活各种BeanFactory处理器 + - 6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean的时候 + - 7)为上下文初始化Message源,即为不同语言的消息体进行国际化处理 + - 8)初始化应用消息广播器,并放入applicationEventMulticaster bean中 + - 9)留给子类来初始化其他的bean +- 10)在所有注册的bean中查找listener bean,注册到消息广播器中 + - 11)初始化剩下的代理实例(非lazy-init) + - 12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。 + +``` +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // 准备 刷新的上下文环境 + prepareRefresh(); + + // 初始化BeanFactory,并进行XML文件的读取 +``` + +- ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // 对BeanFactory进行各种功能填充 +- prepareBeanFactory(beanFactory); + + try { + // 子类覆盖方法做额外的处理 + postProcessBeanFactory(beanFactory); + + // 激活各种BeanFactory处理器 + invokeBeanFactoryPostProcessors(beanFactory); + + // 注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用是在getBean的时候 + registerBeanPostProcessors(beanFactory); + + // 为上下文初始化Message源,即不同语言的消息体,国际化处理 + initMessageSource(); + + // 初始化应用消息广播器,并放入applicationEventMulticaster bean中 + initApplicationEventMulticaster(); + + // 留给子类来初始化其他bean + onRefresh(); + + // 在所有注册的bean中查找Listener bean,注册到消息广播器中 + registerListeners(); + + // 初始化剩下的单例实例(除了lazy-init) + finishBeanFactoryInitialization(beanFactory); + + // 完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + destroyBeans(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + } + } +} + - 1) obtainFreshBeanFactory(创建beanFactory,解析XML) +- 看obtainFreshBeanFactory这个方法: +- protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + refreshBeanFactory(); + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} + - 1.1) refreshBeanFactory +- 看refreshBeanFactory这个方法,这个方法是抽象方法,有一种是在AbstractRefreshableApplicationContext这个类中实现的: +- @Override +protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + DefaultListableBeanFactory beanFactory = createBeanFactory(); + beanFactory.setSerializationId(getId()); + customizeBeanFactory(beanFactory); +- //该方法最终调用XmlBeanDefinitionReader类中loadBeanDefinitions(EncodedResource) + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); + } +} + - 1.1.1) loadBeanDefinitions(beanFactory)(创建reader) +- 看loadBeanDefinitions(beanFactory)这个方法: +- @Override +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); +} + - 1.1.1.1) loadBeanDefinitions(beanDefinitionReader)(调用reader的load) +- 看loadBeanDefinitions(beanDefinitionReader)的另一个重载: +- protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } +} + - 1.1.1.1.1) XmlBeanDefinitionReader.loadBeanDefinitions(解析标签) +- 看reader.loadBeanDefinitions方法: + +``` +@Override +public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { + Assert.notNull(locations, "Location array must not be null"); + int counter = 0; + for (String location : locations) { + counter += loadBeanDefinitions(location); + } + return counter; +} +``` + +- loadBeanDefinitions(location,null) +- 它最终调用了loadBeanDefinitions(location,null)方法 + +``` +public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException { + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader == null) { + throw new BeanDefinitionStoreException( + "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); + } + + if (resourceLoader instanceof ResourcePatternResolver) { + // Resource pattern matching available. + try { + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + int loadCount = loadBeanDefinitions(resources); + if (actualResources != null) { + for (Resource resource : resources) { + actualResources.add(resource); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]"); + } + return loadCount; + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "Could not resolve bean definition resource pattern [" + location + "]", ex); + } + } + else { + // Can only load single resources by absolute URL. + Resource resource = resourceLoader.getResource(location); + int loadCount = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); + } + return loadCount; + } +} +``` + + + +``` +public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { + Assert.notNull(resources, "Resource array must not be null"); + int counter = 0; + for (Resource resource : resources) { + counter += loadBeanDefinitions(resource); + } + return counter; +} +``` + + + +``` +public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(new EncodedResource(resource)); +} +``` + + + +``` +public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + if (logger.isInfoEnabled()) { + logger.info("Loading XML bean definitions from " + encodedResource.getResource()); + } + + Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); + if (currentResources == null) { + currentResources = new HashSet<EncodedResource>(4); + this.resourcesCurrentlyBeingLoaded.set(currentResources); + } + if (!currentResources.add(encodedResource)) { + throw new BeanDefinitionStoreException( + "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); + } + try { + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "IOException parsing XML document from " + encodedResource.getResource(), ex); + } + finally { + currentResources.remove(encodedResource); + if (currentResources.isEmpty()) { + this.resourcesCurrentlyBeingLoaded.remove(); + } + } +} +``` + + +- doLoadBeanDefinitions +- 继续向下找,最终调用了reader的doLoadBeanDefinitions方法: +- protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + try { + Document doc = doLoadDocument(inputSource, resource); + return registerBeanDefinitions(doc, resource); + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } +- ... +- } +- doLoadDocument方法中用到了documentLoader对象加载document,它最后又会用到domParser解析xml文件。 +- 最终是使用jaxp的dom方式读取的XML配置文件。(JAXP是一种标准,SUN公司对其提供了实现) +- registerBeanDefinitions +- 将XML对应的Document对象转为BeanDefinitions: + +``` +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + int countBefore = getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + return getRegistry().getBeanDefinitionCount() - countBefore; +} +``` + +- documentReader#registerBeanDefinitions +- 这是一个抽象方法,在DefaultBeanDefinitionDocumentReader类中得到实现: + +``` +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + logger.debug("Loading bean definitions"); + Element root = doc.getDocumentElement(); + doRegisterBeanDefinitions(root); +} +``` + +- doRegisterBeanDefinitions +- 实际解析文档的是doRegisterBeanDefinitions方法: +- protected void doRegisterBeanDefinitions(Element root) { + // Any nested <beans> elements will cause recursion in this method. In + // order to propagate and preserve <beans> default-* attributes correctly, + // keep track of the current (parent) delegate, which may be null. Create + // the new (child) delegate with a reference to the parent for fallback purposes, + // then ultimately reset this.delegate back to its original (parent) reference. + // this behavior emulates a stack of delegates without actually necessitating one. + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = createDelegate(getReaderContext(), root, parent); + + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray( + profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); + if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (logger.isInfoEnabled()) { + logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + + "] not matching: " + getReaderContext().getResource()); + } + return; + } + } + } + + preProcessXml(root); + parseBeanDefinitions(root, this.delegate); + postProcessXml(root); + + this.delegate = parent; +} +- parseBeanDefinitions(解析标签) +- 实际解析方法parseBeanDefinitions: +- protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + if (delegate.isDefaultNamespace(ele)) { + parseDefaultElement(ele, delegate); + } + else { + delegate.parseCustomElement(ele); + } + } + } + } + else { + delegate.parseCustomElement(root); + } +} +- 分开解析默认标签和自定义标签。 +- parseDefaultElement(解析默认标签) +- 就import、alias、bean和beans四种默认标签进行解析 + +``` +private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { + importBeanDefinitionResource(ele); + } + else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { + processAliasRegistration(ele); + } + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } +} +``` + +- parseCustomElement(解析自定义标签) +- 调用的是parseCustomElement(ele,null) + +``` +public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) { + String namespaceUri = getNamespaceURI(ele); + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +``` + + +- NamespaceHandlerSupport#parse + +``` +public BeanDefinition parse(Element element, ParserContext parserContext) { + return findParserForElement(element, parserContext).parse(element, parserContext); +} +``` + + +- a) findParserForElement + +``` +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + String localName = parserContext.getDelegate().getLocalName(element); + BeanDefinitionParser parser = this.parsers.get(localName); + if (parser == null) { + parserContext.getReaderContext().fatal( + "Cannot locate BeanDefinitionParser for element [" + localName + "]", element); + } + return parser; +} +``` + +- parser是ComponentScanBeanDefinitionParser类型。 +- b) ComponentScanBeanDefinitionParser#parse + +``` +public BeanDefinition parse(Element element, ParserContext parserContext) { + String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); + basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage); + String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + + // Actually scan for bean definitions and register them. + ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); + Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages); + registerComponents(parserContext.getReaderContext(), beanDefinitions, element); + + return null; +} +``` + + + - 1) ClassPathBeanDefinitionScanner.doScan(解析注解定义的bean) +- 根据自定义标签component-scan来扫描包,批量注册bean。 +- protected Set<BeanDefinitionHolder> doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>(); + for (String basePackage : basePackages) { + Set<BeanDefinition> candidates = findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) { + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + candidate.setScope(scopeMetadata.getScopeName()); + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + if (candidate instanceof AnnotatedBeanDefinition) { + AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); + } + if (checkCandidate(beanName, candidate)) { + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + definitionHolder = + AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + beanDefinitions.add(definitionHolder); + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; +} + + - 1.1) findCandidateComponents + +``` +public Set<BeanDefinition> findCandidateComponents(String basePackage) { + Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>(); + try { + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + resolveBasePackage(basePackage) + '/' + this.resourcePattern; +``` + +- // 这里是未加筛选的拿到了所有class文件 + Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (Resource resource : resources) { + if (traceEnabled) { + logger.trace("Scanning " + resource); + } + if (resource.isReadable()) { + try { +- // 这里会把class文件读进来 + MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource); +- // 判断class文件是否是注册在Spring中的bean类型 + if (isCandidateComponent(metadataReader)) { + ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); + sbd.setResource(resource); + sbd.setSource(resource); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Identified candidate component class: " + resource); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + resource); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not matching any filter: " + resource); + } + } + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to read candidate component class: " + resource, ex); + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not readable: " + resource); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; +} +- + + - 1.1.1) PathMatchingResourcePatternResolver.getResources + +``` +public Resource[] getResources(String locationPattern) throws IOException { + Assert.notNull(locationPattern, "Location pattern must not be null"); + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + // a class path resource (multiple resources for same name possible) + if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + // a class path resource pattern + return findPathMatchingResources(locationPattern); + } + else { + // all class path resources with the given name + return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); + } + } + else { + // Only look for a pattern after a prefix here + // (to not get fooled by a pattern symbol in a strange prefix). + int prefixEnd = locationPattern.indexOf(":") + 1; + if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { + // a file pattern + return findPathMatchingResources(locationPattern); + } + else { + // a single resource with the given name + return new Resource[] {getResourceLoader().getResource(locationPattern)}; + } + } +} +``` + +- + + - 1.1.1.1) findPathMatchingResources +- protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { + String rootDirPath = determineRootDir(locationPattern); + String subPattern = locationPattern.substring(rootDirPath.length()); + - // 根路径,component-scan中配置的包名 + Resource[] rootDirResources = getResources(rootDirPath); + Set<Resource> result = new LinkedHashSet<Resource>(16); + for (Resource rootDirResource : rootDirResources) { + rootDirResource = resolveRootDirResource(rootDirResource); + URL rootDirURL = rootDirResource.getURL(); + if (equinoxResolveMethod != null) { + if (rootDirURL.getProtocol().startsWith("bundle")) { + rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirURL); + rootDirResource = new UrlResource(rootDirURL); + } + } + if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher())); + } + else if (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) { + result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern)); + } + else { + result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result); + } + return result.toArray(new Resource[result.size()]); +} +- + + - 1.1.1.1.1) doFindPathMatchingFileResources +- protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) + throws IOException { + + File rootDir; + try { + rootDir = rootDirResource.getFile().getAbsoluteFile(); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot search for matching files underneath " + rootDirResource + + " because it does not correspond to a directory in the file system", ex); + } + return Collections.emptySet(); + } + return doFindMatchingFileSystemResources(rootDir, subPattern); +} +- +doFindMatchingFileSystemResources +- protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); + } + Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern); + Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size()); + for (File file : matchingFiles) { + result.add(new FileSystemResource(file)); + } + return result; +} + + - 1.1.1.1.1.1) retrieveMatchingFiles + - protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException { + if (!rootDir.exists()) { + // Silently skip non-existing directories. + if (logger.isDebugEnabled()) { + logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist"); + } + return Collections.emptySet(); + } + if (!rootDir.isDirectory()) { + // Complain louder if it exists but is no directory. + if (logger.isWarnEnabled()) { + logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory"); + } + return Collections.emptySet(); + } + if (!rootDir.canRead()) { + if (logger.isWarnEnabled()) { + logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + return Collections.emptySet(); + } + String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/"); + if (!pattern.startsWith("/")) { + fullPattern += "/"; + } + fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/"); + Set<File> result = new LinkedHashSet<File>(8); + doRetrieveMatchingFiles(fullPattern, rootDir, result); + return result; +} + + - 1.1.1.1.1.1.1) doRetrieveMatchingFiles(递归方法) +- protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Searching directory [" + dir.getAbsolutePath() + + "] for files matching pattern [" + fullPattern + "]"); + } +- // 拿到component-scan目录下的所有class文件 + File[] dirContents = dir.listFiles(); + if (dirContents == null) { + if (logger.isWarnEnabled()) { + logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]"); + } + return; + } + Arrays.sort(dirContents); + for (File content : dirContents) { + String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/"); + if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) { + if (!content.canRead()) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + } + else { + doRetrieveMatchingFiles(fullPattern, content, result); + } + } + if (getPathMatcher().match(fullPattern, currPath)) { + result.add(content); + } + } +} + +- + + - 1.1.2) CachingMetadataResourceFactory.getMetadataReader(读取class文件) + +``` +public MetadataReader getMetadataReader(Resource resource) throws IOException { + if (getCacheLimit() <= 0) { + return super.getMetadataReader(resource); + } + synchronized (this.metadataReaderCache) { + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } +} +``` + + + - 1.1.2.1) SimpleMetadataReader.getMetadataReader +- 该类封装了Class文件中的各种信息,保存在ClassMatadata和AnnotationMetadata中。 + +``` +final class SimpleMetadataReader implements MetadataReader { + + private final Resource resource; + + private final ClassMetadata classMetadata; + + private final AnnotationMetadata annotationMetadata; +``` + +- } + + + +``` +public MetadataReader getMetadataReader(Resource resource) throws IOException { + return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader()); +} +``` + + +- SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException { + InputStream is = new BufferedInputStream(resource.getInputStream()); + ClassReader classReader; + try { + classReader = new ClassReader(is); + } + catch (IllegalArgumentException ex) { + throw new NestedIOException("ASM ClassReader failed to parse class file - " + + "probably due to a new Java class file version that isn't supported yet: " + resource, ex); + } + finally { + is.close(); + } + + AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader); + classReader.accept(visitor, ClassReader.SKIP_DEBUG); + + this.annotationMetadata = visitor; + // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor) + this.classMetadata = visitor; + this.resource = resource; +} + + - 1.1.2.1.1) ClassReader.accept +- 源码比较奇怪,大概是按照class文件的格式解析,并将结果封装到visitor里面。 +- + + - 1.1.3) isCandidateComponent +- protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { + for (TypeFilter tf : this.excludeFilters) { + if (tf.match(metadataReader, this.metadataReaderFactory)) { + return false; + } + } + for (TypeFilter tf : this.includeFilters) { + if (tf.match(metadataReader, this.metadataReaderFactory)) { + return isConditionMatch(metadataReader); + } + } + return false; +} + +- 这里的includeFilters有一个AnnotationTypeFilter。 + + - 1.1.3.1) AbstractTypeHierarchyTraversingFilter.match + +``` +public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + // This method optimizes avoiding unnecessary creation of ClassReaders + // as well as visiting over those readers. + if (matchSelf(metadataReader)) { + return true; + } + ClassMetadata metadata = metadataReader.getClassMetadata(); + if (matchClassName(metadata.getClassName())) { + return true; + } + + if (this.considerInherited) { + if (metadata.hasSuperClass()) { + // Optimization to avoid creating ClassReader for super class. + Boolean superClassMatch = matchSuperClass(metadata.getSuperClassName()); + if (superClassMatch != null) { + if (superClassMatch.booleanValue()) { + return true; + } + } + else { + // Need to read super class to determine a match... + try { + if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + return true; + } + } + catch (IOException ex) { + logger.debug("Could not read super class [" + metadata.getSuperClassName() + + "] of type-filtered class [" + metadata.getClassName() + "]"); + } + } + } + } + + if (this.considerInterfaces) { + for (String ifc : metadata.getInterfaceNames()) { + // Optimization to avoid creating ClassReader for super class + Boolean interfaceMatch = matchInterface(ifc); + if (interfaceMatch != null) { + if (interfaceMatch.booleanValue()) { + return true; + } + } + else { + // Need to read interface to determine a match... + try { + if (match(ifc, metadataReaderFactory)) { + return true; + } + } + catch (IOException ex) { + logger.debug("Could not read interface [" + ifc + "] for type-filtered class [" + + metadata.getClassName() + "]"); + } + } + } + } + + return false; +} +``` + + +- protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); +} + +- this.annotationType是@Component类型,所以 +- metadata.hasAnnotation(this.annotationType.getName())当类上注解了@Component时为true。 +- 这里因为@Service等也注解了@Component了,所以@Service、@Controller等在这里都被视为@Component。 + +``` +public boolean hasMetaAnnotation(String metaAnnotationType) { + Collection<Set<String>> allMetaTypes = this.metaAnnotationMap.values(); + for (Set<String> metaTypes : allMetaTypes) { + if (metaTypes.contains(metaAnnotationType)) { + return true; + } + } + return false; +} +``` + + - 1.2) registerBeanDefinition(将beanDefinition记录到BeanFactory) +- protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); +} +- + + - 1.2.1) DefaultListableBeanFactory.registerBeanDefinition(保存beanDefinition) + +``` +public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + Assert.hasText(beanName, "Bean name must not be empty"); + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + + if (beanDefinition instanceof AbstractBeanDefinition) { + try { + ((AbstractBeanDefinition) beanDefinition).validate(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Validation of bean definition failed", ex); + } + } + + BeanDefinition oldBeanDefinition; + + oldBeanDefinition = this.beanDefinitionMap.get(beanName); + if (oldBeanDefinition != null) { + if (!isAllowBeanDefinitionOverriding()) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + + "': There is already [" + oldBeanDefinition + "] bound."); + } + else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { + // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + if (this.logger.isWarnEnabled()) { + this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + + "' with a framework-generated bean definition: replacing [" + + oldBeanDefinition + "] with [" + beanDefinition + "]"); + } + } + else if (!beanDefinition.equals(oldBeanDefinition)) { + if (this.logger.isInfoEnabled()) { + this.logger.info("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + this.beanDefinitionMap.put(beanName, beanDefinition); + } + else { + if (hasBeanCreationStarted()) { + // Cannot modify startup-time collection elements anymore (for stable iteration) + synchronized (this.beanDefinitionMap) { + this.beanDefinitionMap.put(beanName, beanDefinition); + List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1); + updatedDefinitions.addAll(this.beanDefinitionNames); + updatedDefinitions.add(beanName); + this.beanDefinitionNames = updatedDefinitions; + if (this.manualSingletonNames.contains(beanName)) { + Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames); + updatedSingletons.remove(beanName); + this.manualSingletonNames = updatedSingletons; + } + } + } + else { + // Still in startup registration phase + this.beanDefinitionMap.put(beanName, beanDefinition); + this.beanDefinitionNames.add(beanName); + this.manualSingletonNames.remove(beanName); + } + this.frozenBeanDefinitionNames = null; + } + + if (oldBeanDefinition != null || containsSingleton(beanName)) { + resetBeanDefinition(beanName); + } +} +``` + + +- 标签解析完毕后会将beanName和beanDefinition作为key和value放入beanfactory的beanDefinitionMap中。 + +- + + - 2) finishBeanFactoryInitialization(初始化非lazy-load且singleton的bean) + +``` +protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && + beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService( + beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(new StringValueResolver() { + @Override + public String resolveStringValue(String strVal) { + return getEnvironment().resolvePlaceholders(strVal); + } + }); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // Stop using the temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + beanFactory.preInstantiateSingletons(); +} +``` + + + + - 2.1) ConfigurableListableBeanFactory#preInstantiateSingletons +- DefaultListableBeanFactory.preInstantiateSingletons + +``` +public void preInstantiateSingletons() throws BeansException { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Pre-instantiating singletons in " + this); + } + + // Iterate over a copy to allow for init methods which in turn register new bean definitions. + // While this may not be part of the regular factory bootstrap, it does otherwise work fine. + List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames); + + // Trigger initialization of all non-lazy singleton beans... + for (String beanName : beanNames) { + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName); + boolean isEagerInit; + if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { + isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() { + @Override + public Boolean run() { + return ((SmartFactoryBean<?>) factory).isEagerInit(); + } + }, getAccessControlContext()); + } + else { + isEagerInit = (factory instanceof SmartFactoryBean && + ((SmartFactoryBean<?>) factory).isEagerInit()); + } + if (isEagerInit) { + getBean(beanName); + } + } + else { + getBean(beanName); + } + } + } + + // Trigger post-initialization callback for all applicable beans... + for (String beanName : beanNames) { + Object singletonInstance = getSingleton(beanName); + if (singletonInstance instanceof SmartInitializingSingleton) { + final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; + if (System.getSecurityManager() != null) { + AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + smartSingleton.afterSingletonsInstantiated(); + return null; + } + }, getAccessControlContext()); + } + else { + smartSingleton.afterSingletonsInstantiated(); + } + } + } +} +``` + + + +- + +- Bean的加载 + +- + +- FactoryBean(工厂Bean,用户定制) +- Spring通过反射机制利用bean的class属性指定实现类来实例化bean。 +- Spring提供了一个FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化bean的逻辑。 +- FactoryBean + +``` +public interface FactoryBean<T> { +``` + +- // 返回bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单例缓存池中 + T getObject() throws Exception; + Class<?> getObjectType(); + boolean isSingleton(); +} +- FactoryBean: +- 这个接口使你可以提供一个复杂的逻辑来生成Bean。它本质是一个Bean,但这个Bean不是用来注入到其它地方像Service、Dao一样使用的,它是用来生成其它Bean使用的。实现了这个接口后,Spring在容器初始化时,把实现这个接口的Bean取出来,使用接口的getObject()方法来生成我们要想的Bean。当然,那些生成Bean的业务逻辑也要写getObject()方法中。 +- 其返回的对象不是指定类的一个实例,其返回的是该工厂Bean的getObject方法所返回的对象。创建出来的对象是否属于单例由isSingleton中的返回决定。 +- 使用场景:1、通过外部对类是否是单例进行控制,该类自己无法感知 2、在创建Object对象之前进行初始化的操作,在afterPropertiesSet()中完成。(一次性的初始化,保存在成员变量中,并不是每次getObject都会调用afterPropertiesSet,afterPropertiesSet只会被调用一次) + +- 实例: + +``` +public class CarFactoryBean implements FactoryBean<Car> { +``` + + +``` + private String brand; +``` + + +``` + private double price; +``` + +- @Override + +``` + public Car getObject() throws Exception { +``` + +- return new Car(brand,price); +- } +- @Override + +``` + public Class<?> getObjectType() { +``` + +- return Car.class; +- } + +- @Override + +``` + public boolean isSingleton() { +``` + +- return true; +- } + +``` + public String getBrand() { +``` + +- return brand; +- } + + +``` + public void setBrand(String brand) { +``` + +- this.brand = brand; +- } + + +``` + public double getPrice() { +``` + +- return price; +- } + + +``` + public void setPrice(double price) { +``` + +- this.price = price; +- } +- } + +- <bean id="car" class="factorybean.CarFactoryBean"> +- <property name="brand" value="BMW"></property> +- <property name="price" value="300000"></property> +- </bean> + + +``` +public class Main { +``` + + +``` + public static void main(String[] args) { +``` + +- ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-factoryBean.xml"); +- Car car = (Car) ctx.getBean("car"); +- System.out.println(car); +- } +- } +- ObjectFactory(Spring使用) + +``` +public interface ObjectFactory<T> { + T getObject() throws BeansException; +} +``` + + + +- ObjectFactory: +- 它的目的也是作为一个工厂,来生成Object(这个接口只有一个方法getObject())。这个接口一般被用来,包装一个factory,通过个这工厂来返回一个新实例(prototype类型)。这个接口和FactoryBean有点像,但FactoryBean的实现是被当做一个SPI(Service Provider Interface)实例来使用在BeanFactory里面;ObjectFactory的实现一般被用来注入到其它Bean中,作为API来使用。就像ObjectFactoryCreatingFactoryBean的例子,它的返回值就是一个ObjectFactory,这个ObjectFactory被注入到了Bean中,在Bean通过这个接口的实例,来取得我们想要的Bean。 +- 总的来说,FactoryBean和ObjectFactory都是用来取得Bean,但使用的方法和地方不同,FactoryBean被配置好后,Spring调用getObject()方法来取得Bean,ObjectFactory配置好后,在Bean里面可以取得ObjectFactory实例,需要我们手动来调用getObject()来取得Bean。 +## InitializingBean +- InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法。 +- AbstractBeanFactory#getBean + +``` +public Object getBean(String name) throws BeansException { + return doGetBean(name, null, null, false); +} +``` + +- doGetBean +- 有三个方法非常关键:getSingleton,createBean和getObjectForBeanInstance。 +- protected <T> T doGetBean( + final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) + throws BeansException { + + final String beanName = transformedBeanName(name); + Object bean; + + // Eagerly check singleton cache for manually registered singletons. +- // 检查缓存中或者实例工厂中是否有对应的实例(解决循环依赖的问题) +- // Spring创建bean的原则是不等bean创建完成就会将创建bean的ObjectFactory提早曝光,也就是将ObjectFactory加入到缓存中,一旦下个bean创建时需要上个bean则直接使用ObjectFactory。 +- // 直接尝试从缓存获取或者从singletonFactories中的ObjectFactory中获取 + Object sharedInstance = getSingleton(beanName); + if (sharedInstance != null && args == null) { +- // 已经创建过了 + if (logger.isDebugEnabled()) { + if (isSingletonCurrentlyInCreation(beanName)) { + logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + + "' that is not fully initialized yet - a consequence of a circular reference"); + } + else { + logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); + } + } +- // 返回对应的实例(从缓存中只得到了bean的原始状态,还需要对bean进行实例化) + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + else { +- // 没有创建,需要创建 +- // Fail if we're already creating this bean instance: + // We're assumably within a circular reference. + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + + // Check if bean definition exists in this factory. + BeanFactory parentBeanFactory = getParentBeanFactory(); +- // 如果beanDefinitionMap(已经加载了的类)中不包含beanName,则尝试从parentBeanFactory处理 + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + String nameToLookup = originalBeanName(name); + if (args != null) { + // Delegation to parent with explicit args. +- // 递归 + return (T) parentBeanFactory.getBean(nameToLookup, args); + } + else { + // No args -> delegate to standard getBean method. + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + } + // 从这里开始创建bean,先进行记录 + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + + try { +- // 将存储XML配置文件的GenericBeanDefinition转换为RootBeanDefinition;转换的时候如果父类bean不为空的话,那么会合并父类的属性。 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + // Guarantee initialization of beans that the current bean depends on. // 若存在依赖则需要递归实例化依赖的bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + if (isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } +- // 缓存依赖调用 + registerDependentBean(dep, beanName); + getBean(dep); + } + } + + // Create bean instance. +- // 真正的创建bean + if (mbd.isSingleton()) { + +``` + // 单例 + sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { + @Override + public Object getObject() throws BeansException { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + else { +``` + + +``` + // 在指定的scope上实例化bean + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + try { + Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() { + @Override + public Object getObject() throws BeansException { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) { + try { + return getTypeConverter().convertIfNecessary(bean, requiredType); + } + catch (TypeMismatchException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert bean '" + name + "' to required type '" + + ClassUtils.getQualifiedName(requiredType) + "'", ex); + } + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } + return (T) bean; +} +``` + + + - 1) getSingleton(beanName)(借助缓存或singletonFactories) +- getSingleton(beanName,true) - > true表示允许早期依赖 +- 逻辑: + - 1)从singletonObjects中获取,它是一个真正的缓存,有就直接返回 + - 2)获取不到再从earlySingletonObjects里面获取 + - 3)还是获取不到,再尝试从singletonFactories里面获取beanName对应的ObjectFactory,然后调用这个ObjectFactory的getObject来获取之前创建的bean,并放到earlySingletonObjects里面去,并且从singletonFactories中remove掉这个ObjectFactory。 +- 成员变量Map: + - 1)singletonObjects是beanName与beanInstance的Map,是真正的缓存,beanInstance是构造完毕的,凡是正常地构造完毕的单例bean都会放入缓存中。 + - 2)earlySingletonObjects也是beanName与beanInstance的Map,beanInstance是已经调用了createBean方法,但是没有清除加载状态和加入至缓存的bean。仅在当前bean创建时存在,用于检测代理bean循环依赖。 + - 3)singleFactories是beanName与ObjectFactory的Map,仅在当前bean创建时存在,是尚未调用createBean的bean。用于setter循环依赖时实现注入。 + - 4)registeredSingletons:用来保存当前所有已注册的bean。 + +- singletonFactories和earlySingletonObjects都是一个临时工。在所有的对象创建完毕之后,此两个对象的size都为0。 +- protected Object getSingleton(String beanName, boolean allowEarlyReference) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return (singletonObject != NULL_OBJECT ? singletonObject : null); +} + + + - 2) getSingleton(beanName,ObjectFactory)(从头创建单例bean) +- 从头创建一个单例的bean需要经过getSingleton(beanName,ObjectFactory)和createBean两个关键方法。 +- 逻辑: + - 1)检查缓存是否已经加载过 + - 2)若没有加载,则记录beanName的正在加载状态 + - 3)加载单例前 记录加载状态 + - 4)通过ObjectFactory的getObject方法实例化bean + - 5)加载单例后 清除加载状态 + - 6)将结果记录至缓存并删除加载bean过程中所记录的各种辅助状态 + - 7)返回处理结果 + +``` +public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { + Assert.notNull(beanName, "'beanName' must not be null"); + synchronized (this.singletonObjects) { +``` + +- // 检查对应的bean是否已经被加载过 + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { +- // 没有被加载过 + if (this.singletonsCurrentlyInDestruction) { + throw new BeanCreationNotAllowedException(beanName, + "Singleton bean creation not allowed while singletons of this factory are in destruction " + + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); + } +- // 记录加载状态 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + boolean recordSuppressedExceptions = (this.suppressedExceptions == null); + if (recordSuppressedExceptions) { + this.suppressedExceptions = new LinkedHashSet<Exception>(); + } + try { +- // 初始化bean,在这里调用了createBean方法 + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + catch (IllegalStateException ex) { + // Has the singleton object implicitly appeared in the meantime -> + // if yes, proceed with it since the exception indicates that state. + singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + throw ex; + } + } + catch (BeanCreationException ex) { + if (recordSuppressedExceptions) { + for (Exception suppressedException : this.suppressedExceptions) { + ex.addRelatedCause(suppressedException); + } + } + throw ex; + } + finally { + if (recordSuppressedExceptions) { + this.suppressedExceptions = null; + } +- // 清除加载状态 + afterSingletonCreation(beanName); + } + if (newSingleton) { +- // 加入缓存 + addSingleton(beanName, singletonObject); + } + } + return (singletonObject != NULL_OBJECT ? singletonObject : null); + } +} + + - 2.1) beforeSingletonCreation (记录加载状态) +- 记录加载状态,通过this.singletonsCurrentlyInCreation.add(beanName)将当前正在创建的bean记录在缓存中,这样便可以对循环依赖进行检测。 +- protected void beforeSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } +} + + - 2.2) afterSingletonCreation(清除加载状态) +- 当bean加载结束后需要移除缓存中对该bean的正在加载状态的记录。 +- protected void afterSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) { + throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation"); + } +} + - 2.3) addSingleton(结果记录至缓存) +- 将结果记录至缓存中并删除加载bean过程中所记录的各种辅助状态 +- protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT)); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +- +- + + - 3) createBean(创建单例或多例的bean,在3中有被调用) +- protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException { + if (logger.isDebugEnabled()) { + logger.debug("Creating instance of bean '" + beanName + "'"); + } + RootBeanDefinition mbdToUse = mbd; + + // Make sure bean class is actually resolved at this point, and + // clone the bean definition in case of a dynamically resolved Class + // which cannot be stored in the shared merged bean definition. +- // 锁定class,根据设置的class属性或者根据className来解析Class + Class<?> resolvedClass = resolveBeanClass(mbd, beanName); + if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { + mbdToUse = new RootBeanDefinition(mbd); + mbdToUse.setBeanClass(resolvedClass); + } + + // Prepare method overrides. + try { +- // 验证及准备覆盖的方法 + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } + + try { + // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. +- // 给BeanPostProcessor一个机会来返回代理来替代真正的实例 + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + catch (Throwable ex) { + throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, + "BeanPostProcessor before instantiation of bean failed", ex); + } + // 实际创建bean + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + if (logger.isDebugEnabled()) { + logger.debug("Finished creating instance of bean '" + beanName + "'"); + } + return beanInstance; +} + - 3.1) AbstractBeanDefinition#prepareMethodOverrides(决定实例化策略->反射 or CGLIB) +- 验证及准备覆盖的方法 +- 在Spring配置中存在lookup-method和replace-method两个配置功能,而这两个配置的加载其实就是将配置统一存放在BeanDefinition中的methodOverrides属性里,这两个功能实现原理其实是在bean实例化的时候如果检测到存在methodOverrides属性,会动态地为当前bean生成代理并使用对应的拦截器为bean做增强处理,相关逻辑实现在bean的实例化部分详细介绍。 + +``` +public void prepareMethodOverrides() throws BeanDefinitionValidationException { + // Check that lookup methods exists. + MethodOverrides methodOverrides = getMethodOverrides(); + if (!methodOverrides.isEmpty()) { + Set<MethodOverride> overrides = methodOverrides.getOverrides(); + synchronized (overrides) { + for (MethodOverride mo : overrides) { + prepareMethodOverride(mo); + } + } + } +} +``` + + - 3.1.1) prepareMethodOverride +- protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException { + - //获取对应类中对应方法名的个数 + int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName()); + if (count == 0) { + throw new BeanDefinitionValidationException( + "Invalid method override: no method with name '" + mo.getMethodName() + + "' on class [" + getBeanClassName() + "]"); + } + else if (count == 1) { + // Mark override as not overloaded, to avoid the overhead of arg type checking. +- // 标记MethodOverride暂未被覆盖,避免参数类型检查的开销 + mo.setOverloaded(false); + } +} + - 3.2) resolveBeforeInstantiation(可能会创建代理过的bean) +- 如果该方法返回bean不为空,则跳过后续实际创建bean的过程,直接返回代理后的bean。 +- 与AOP有关! +- protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + Object bean = null; + if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { + // Make sure bean class is actually resolved at this point. + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + Class<?> targetType = determineTargetType(beanName, mbd); + if (targetType != null) { + bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); + if (bean != null) { + bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); + } + } + } + mbd.beforeInstantiationResolved = (bean != null); + } + return bean; +} + + - 3.2.1) applyBeanPostProcessorsBeforeInstantiation(实例化前的后处理器应用) +- protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName); + if (result != null) { + return result; + } + } + } + return null; +} + + - 3.2.2) applyBeanPostProcessorsAfterInitialization(实例化后的后处理器应用) +- Spring中的规则是在bean的初始化后尽可能保证将注册的后处理器的postProcessAfterInitialization方法应用到该bean中,因为如果返回的bean不为空,那么便不会再次经历普通bean的创建过程。 + +``` +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + +- + + - 3.3) doCreateBean(创建常规bean) +- 逻辑: + - 1)如果是单例,则需要首先清除缓存factoryBeanInstanceCache + - 2)实例化bean,将BeanDefinition转换为BeanWrapper。 +- 转换过程: +- 如果存在工厂方法,则使用工厂方法进行初始化 +- 一个类有多个构造函数,每个构造函数都有不同的参数,所以需要根据参数锁定构造函数并进行初始化 +- 如果既不存在工厂方法也不存在带有参数的构造函数,则使用默认的构造函数进行bean的初始化。 + - 3)MergedBeanDefinitionPostProcessor的应用 +- bean合并后的处理,Autowired注解正是通过此方法实现诸如类型的预解析。 + - 4)添加singletonFactories缓存 + - 5)属性填充 + - 6)代理bean循环依赖检查,对于已加载的bean,检测是否已经出现了循环依赖,并判断是否需要抛出异常。(仅针对于当前bean在initializeBean中被代理过的情况,正常的循环依赖在此之前就已经被检测出来的,只有代理后的bean的循环依赖是在这里检查的) + - 7)注册DisposableBean +- 如果配置了destroy-method,这里需要注册以便于在销毁时候调用。 + - 8)完成创建并返回 +- protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) + throws BeanCreationException { + + // Instantiate the bean. + BeanWrapper instanceWrapper = null; + if (mbd.isSingleton()) { + instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); + } +- // 根据指定bean使用对应的策略创建新的实例,如:工厂方法;构造器注入;简单初始化 + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null); + Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null); + mbd.resolvedTargetType = beanType; + + // Allow post-processors to modify the merged bean definition. + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + // Eagerly cache singletons to be able to resolve circular references + // even when triggered by lifecycle interfaces like BeanFactoryAware. +- // 是否需要提早曝光:单例&&允许循环依赖&&当前bean正在创建中 +- // 检测循环依赖 + boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); + if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } +- // 放入singletonFactories,下次重新获取当前bean时可以直接返回 + +``` +addSingletonFactory(beanName, new ObjectFactory<Object>() { + @Override + public Object getObject() throws BeansException { +``` + +- // 对bean再一次依赖引用,主要应用SmartInstantiationAware BeanPostProcessor ,其中AOP就是在这里将advice动态织入bean中,若没有则直接返回bean,不做任何处理 + return getEarlyBeanReference(beanName, mbd, bean); + } + }); + } + // 填充bean,将属性值注入 + // Initialize the bean instance. + Object exposedObject = bean; + try { +- // 3.3.3 + populateBean(beanName, mbd, instanceWrapper); + if (exposedObject != null) { +- // 3.3.4 +- // 调用初始化方法,比如init-method +- // 注意这里是重新赋值,因为初始化会调用BeanPostProcessor,可能会返回代理对象 + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + } + catch (Throwable ex) { + if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { + throw (BeanCreationException) ex; + } + else { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); + } + } + // 循环依赖检查 + if (earlySingletonExposure) { + Object earlySingletonReference = getSingleton(beanName, false); +- // 如果singletonObjects(缓存)中存在当前bean,earlySingletonReference不为空,它指向的是缓存中的当前bean,但未必和当前bean是同一个,因为上面initializeBean时可能会返回代理后的bean + if (earlySingletonReference != null) { + if (exposedObject == bean) { + exposedObject = earlySingletonReference; + } +- // bean不同,说明使用动态代理对其进行了增强,这是不被允许的 +- // 因为其他bean依赖于当前bean,注入的是从singletonFactories中取出来的,并不是当前bean(代理后的) + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { + String[] dependentBeans = getDependentBeans(beanName); + Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length); + for (String dependentBean : dependentBeans) { + if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { + actualDependentBeans.add(dependentBean); + } + } +- // 因为bean创建后它所依赖的bean一定是已经创建的,actualDependentBeans不为空表示当前bean创建后它依赖的bean却没有全部创建完,也就是说存在循环依赖 + if (!actualDependentBeans.isEmpty()) { + throw new BeanCurrentlyInCreationException(beanName, + "Bean with name '" + beanName + "' has been injected into other beans [" + + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + + "] in its raw version as part of a circular reference, but has eventually been " + + "wrapped. This means that said other beans do not use the final version of the " + + "bean. This is often the result of over-eager type matching - consider using " + + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); + } + } + } + } + // 注册DisposableBean + // Register bean as disposable. + try { + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); + } + + return exposedObject; +} + - 3.3.1) createBeanInstance(实例化bean) +- 逻辑: + - 1)如果在RootBeanDefinition中存在factoryMethodName属性,或者说在配置文件中配置了factory-method,那么Spring会尝试使用instantiateUsingFactoryMethod方法根据RootBeanDefinition中的配置生成bean的实例。 + - 2)解析构造方法并进行构造方法的实例化。因为一个bean对应的类中可能会有多个构造方法,而每个构造方法的参数不同,Spring再根据参数及类型去判断最终会使用哪个构造方法进行实例化。但是,判断的过程是个比较消耗性能的步骤,所以采用缓存机制,如果已经解析过,则不需要重复解析而是直接从RootBeanDefinition中的属性resolvedConstructorOrFactoryMethod缓存的值去取,否则需要再次解析,并将解析的结果添加至RootBeanDefinition中的属性resolvedConstructorOrFactoryMethod。 +- protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { + // Make sure bean class is actually resolved at this point. + +``` +// 解析class + Class<?> beanClass = resolveBeanClass(mbd, beanName); + + if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); + } + // 如果工厂方法不为空,则使用工厂方法初始化策略 + if (mbd.getFactoryMethodName() != null) { + return instantiateUsingFactoryMethod(beanName, mbd, args); + } + + // Shortcut when re-creating the same bean... + boolean resolved = false; + boolean autowireNecessary = false; + if (args == null) { + synchronized (mbd.constructorArgumentLock) { + if (mbd.resolvedConstructorOrFactoryMethod != null) { + resolved = true; + autowireNecessary = mbd.constructorArgumentsResolved; + } + } + } +``` + +- // 如果解析过,那么直接创建;否则要获取构造方法 + if (resolved) { + if (autowireNecessary) { + return autowireConstructor(beanName, mbd, null, null); + } + else { + return instantiateBean(beanName, mbd); + } + } + // 需要根据参数解析构造方法 + // Need to determine the constructor... + Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); + if (ctors != null || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || + mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { +- // 构造方法自动注入 + return autowireConstructor(beanName, mbd, ctors, args); + } + // 使用默认构造方法 + // No special handling: simply use no-arg constructor. + return instantiateBean(beanName, mbd); +} + + + - 3.3.1.1) instantiateUsingFactoryMethod(工厂方法的实例化) +- AbstractAutowireCapableBeanFactory. instantiateUsingFactoryMethod +- protected BeanWrapper instantiateUsingFactoryMethod( + String beanName, RootBeanDefinition mbd, Object[] explicitArgs) { + + return new ConstructorResolver(this).instantiateUsingFactoryMethod(beanName, mbd, explicitArgs); +} + + - 3.3.1.2) autowireConstructor(有参数的构造方法的实例化) +- protected BeanWrapper autowireConstructor( + String beanName, RootBeanDefinition mbd, Constructor<?>[] ctors, Object[] explicitArgs) { + + return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs); +} + +- ConstructorResolver.autowireConstructor +- 逻辑: + - 1)确定 构造方法的参数 +- 根据explicitArgs参数判断 +- 如果传入的参数explicitArgs不为空,那么可以直接确定参数,因为explicitArgs参数是在getBean的时候用户指定的。——getBean(String name,Object ...args) +- 在获取bean的时候,用户不但可以指定bean的名称,还可以指定bean所对应类的构造函数或者工厂方法的方法参数,主要用于静态工厂方法的调用,而这里是需要给定完全匹配的参数的。 +- 所以,如果传入参数explicitArgs不为空,则可以确定构造方法参数就是它。 + +- 从缓存中获取 +- 如果确定参数的办法之前已经分析过,即构造方法参数已经记录在缓存中,那么可以直接拿来使用。而且,在缓存中缓存的可能是参数的最终类型,也可能是参数的初始类型。 +- 从配置文件获取 +- 即从头开始分析。 +- 分析从获取配置文件中配置的构造方法信息开始,可以调用BeanDefinition.getConstructorArgumentValues()来获取配置的构造方法信息。 +- 有了配置中的信息便可以获取对应的参数值信息了,获取参数值的信息包括直接指定值,而这一处理委托给resolveConstructorArguments方法,并返回能解析到的参数的个数。 + + - 2)确定 构造方法 + +``` +根据构造方法参数在所有构造方法中锁定对应的构造方法,匹配的方法就是根据参数个数匹配,所以在匹配之前需要先对构造方法按照public构造方法优先参数数量降序、非public构造方法参数数量降序。这样可以在遍历的情况下迅速判断排在后面的构造方法参数个数是否符合条件。 +``` + + +- 由于在配置文件中并不是唯一限制使用参数位置索引的方法去创建,同样还支持指定参数名称进行设定参数值的情况,如<constructor-arg name=”aa”>,那么这种情况就需要首先确定构造方法中的参数名称。 +- 获取参数名称可以有两种方式,一种是通过注解的方式直接获取,另一种就是使用Spring中提供的工具类ParameterNameDiscoverer来获取。构造方法、参数名称、参数类型、参数值都确定后就可以锁定构造方法以及转换对应的参数类型了。 + + - 3)根据确定的构造方法转换对应的参数类型 +- 主要是使用Spring中提供的类型转换器或者用户提供的自定义类型转换器进行转换 + - 4)构造方法不确定性的验证 +- 有时候即使构造方法、参数名称、参数类型、参数值都确定后也不一定会直接锁定构造方法,所以Spring在最后又做了一次验证。 + - 5)根据实例化策略以及得到的构造方法及构造方法参数实例化bean。 + +``` +public BeanWrapper autowireConstructor(final String beanName, final RootBeanDefinition mbd, + Constructor<?>[] chosenCtors, final Object[] explicitArgs) { + + BeanWrapperImpl bw = new BeanWrapperImpl(); + this.beanFactory.initBeanWrapper(bw); + + Constructor<?> constructorToUse = null; + ArgumentsHolder argsHolderToUse = null; + Object[] argsToUse = null; + // explicitArgs通过getBean方法传入 +``` + +- // 如果getBean方法调用的时候指定方法参数,那么直接使用 + if (explicitArgs != null) { + argsToUse = explicitArgs; + } + else { +- // 如果在getBean方法时候没有指定则尝试从配置文件中解析 + Object[] argsToResolve = null; +- // 尝试从缓存中获取 + synchronized (mbd.constructorArgumentLock) { + constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod; + if (constructorToUse != null && mbd.constructorArgumentsResolved) { + // Found a cached constructor... +- // 从缓存中取 + argsToUse = mbd.resolvedConstructorArguments; + if (argsToUse == null) { +- // 配置的构造方法参数 + argsToResolve = mbd.preparedConstructorArguments; + } + } + } +- // 如果缓存中存在 + if (argsToResolve != null) { + - // 解析参数类型,如给定方法的构造方法A(int,int),则通过此方法后就会把配置中的(“1”,”1”)转换为(1,1) +- // 缓存中的值可能是原始值,也可能是最终值 + argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); + } + } + //没有被缓存 + if (constructorToUse == null) { + // Need to resolve the constructor. + boolean autowiring = (chosenCtors != null || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + ConstructorArgumentValues resolvedValues = null; + + int minNrOfArgs; + if (explicitArgs != null) { + minNrOfArgs = explicitArgs.length; + } + else { +- // 提取配置文件中的配置的构造方法参数 + ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues(); +- // 用于承载解析后的构造方法参数的值 + resolvedValues = new ConstructorArgumentValues(); +- // 能解析到的参数个数 + minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues); + } + + // Take specified constructors, if any. + Constructor<?>[] candidates = chosenCtors; + if (candidates == null) { + Class<?> beanClass = mbd.getBeanClass(); + try { + candidates = (mbd.isNonPublicAccessAllowed() ? + beanClass.getDeclaredConstructors() : beanClass.getConstructors()); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Resolution of declared constructors on bean Class [" + beanClass.getName() + + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); + } + } + +``` +// 排序给定的构造方法,public构造方法优先参数数量降序、非public构造方法参数数量降序 + AutowireUtils.sortConstructors(candidates); + int minTypeDiffWeight = Integer.MAX_VALUE; + Set<Constructor<?>> ambiguousConstructors = null; + LinkedList<UnsatisfiedDependencyException> causes = null; + + for (Constructor<?> candidate : candidates) { + Class<?>[] paramTypes = candidate.getParameterTypes(); + + if (constructorToUse != null && argsToUse.length > paramTypes.length) { +``` + +- // 如果已经找到选用的构造方法或者需要的参数个数小于当前的构造方法参数个数,则终止 + // Already found greedy constructor that can be satisfied -> + // do not look any further, there are only less greedy constructors left. + break; + } + if (paramTypes.length < minNrOfArgs) { +- // 参数个数不相等 + continue; + } + // 参数个数相等 + ArgumentsHolder argsHolder; +- // 如果构造方法有参数,则根据值构造对应参数类型的参数 + if (resolvedValues != null) { + try { +- // 从注解上获取参数名称 + String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length); + if (paramNames == null) { +- // 获取参数名称探索器 + ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); + if (pnd != null) { +- // 获取指定构造方法的参数名称 + paramNames = pnd.getParameterNames(candidate); + } + } +- // 根据名称和数据类型 创建参数持有者 + argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames, + getUserDeclaredConstructor(candidate), autowiring); + } + catch (UnsatisfiedDependencyException ex) { + if (this.beanFactory.logger.isTraceEnabled()) { + this.beanFactory.logger.trace( + "Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex); + } + // Swallow and try next constructor. + if (causes == null) { + causes = new LinkedList<UnsatisfiedDependencyException>(); + } + causes.add(ex); + continue; + } + } +- // 如果构造方法没有参数 + else { + // Explicit arguments given -> arguments length must match exactly. + if (paramTypes.length != explicitArgs.length) { + continue; + } + argsHolder = new ArgumentsHolder(explicitArgs); + } + // 探测是否有不确定性的构造方法存在,例如不同构造方法的参数为父子关系 + int typeDiffWeight = (mbd.isLenientConstructorResolution() ? + argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes)); + // Choose this constructor if it represents the closest match. +- // 如果它代表着当前最接近的匹配则选择作为构造方法 + if (typeDiffWeight < minTypeDiffWeight) { + constructorToUse = candidate; + argsHolderToUse = argsHolder; + argsToUse = argsHolder.arguments; + minTypeDiffWeight = typeDiffWeight; + ambiguousConstructors = null; + } + else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) { + if (ambiguousConstructors == null) { + ambiguousConstructors = new LinkedHashSet<Constructor<?>>(); + ambiguousConstructors.add(constructorToUse); + } + ambiguousConstructors.add(candidate); + } + } + + if (constructorToUse == null) { + if (causes != null) { + UnsatisfiedDependencyException ex = causes.removeLast(); + for (Exception cause : causes) { + this.beanFactory.onSuppressedException(cause); + } + throw ex; + } + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Could not resolve matching constructor " + + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)"); + } + else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Ambiguous constructor matches found in bean '" + beanName + "' " + + "(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " + + ambiguousConstructors); + } + + if (explicitArgs == null) { + +``` +// 将解析的构造方法加入缓存 + argsHolderToUse.storeCache(mbd, constructorToUse); + } + } + + try { + Object beanInstance; + + if (System.getSecurityManager() != null) { + final Constructor<?> ctorToUse = constructorToUse; + final Object[] argumentsToUse = argsToUse; + beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + return beanFactory.getInstantiationStrategy().instantiate( + mbd, beanName, beanFactory, ctorToUse, argumentsToUse); + } + }, beanFactory.getAccessControlContext()); + } + else { + beanInstance = this.beanFactory.getInstantiationStrategy().instantiate( + mbd, beanName, this.beanFactory, constructorToUse, argsToUse); + } + // 将构建的实例加入BeanWrapper中 + bw.setBeanInstance(beanInstance); + return bw; + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Bean instantiation via constructor failed", ex); + } +} +``` + + - 3.3.1.2.1) createArgumentArray(递归获取参数的bean) + +``` +private ArgumentsHolder createArgumentArray( + String beanName, RootBeanDefinition mbd, ConstructorArgumentValues resolvedValues, + BeanWrapper bw, Class<?>[] paramTypes, String[] paramNames, Object methodOrCtor, + boolean autowiring) throws UnsatisfiedDependencyException { + + String methodType = (methodOrCtor instanceof Constructor ? "constructor" : "factory method"); + TypeConverter converter = (this.beanFactory.getCustomTypeConverter() != null ? + this.beanFactory.getCustomTypeConverter() : bw); + + ArgumentsHolder args = new ArgumentsHolder(paramTypes.length); + Set<ConstructorArgumentValues.ValueHolder> usedValueHolders = + new HashSet<ConstructorArgumentValues.ValueHolder>(paramTypes.length); + Set<String> autowiredBeanNames = new LinkedHashSet<String>(4); + + for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) { + Class<?> paramType = paramTypes[paramIndex]; + String paramName = (paramNames != null ? paramNames[paramIndex] : null); + // Try to find matching constructor argument value, either indexed or generic. + ConstructorArgumentValues.ValueHolder valueHolder = + resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders); + // If we couldn't find a direct match and are not supposed to autowire, + // let's try the next generic, untyped argument value as fallback: + // it could match after type conversion (for example, String -> int). + if (valueHolder == null && !autowiring) { + valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders); + } + if (valueHolder != null) { + // We found a potential match - let's give it a try. + // Do not consider the same value definition multiple times! + usedValueHolders.add(valueHolder); + Object originalValue = valueHolder.getValue(); + Object convertedValue; + if (valueHolder.isConverted()) { + convertedValue = valueHolder.getConvertedValue(); + args.preparedArguments[paramIndex] = convertedValue; + } + else { + ConstructorArgumentValues.ValueHolder sourceHolder = + (ConstructorArgumentValues.ValueHolder) valueHolder.getSource(); + Object sourceValue = sourceHolder.getValue(); + try { + convertedValue = converter.convertIfNecessary(originalValue, paramType, + MethodParameter.forMethodOrConstructor(methodOrCtor, paramIndex)); + // TODO re-enable once race condition has been found (SPR-7423) + /* + if (originalValue == sourceValue || sourceValue instanceof TypedStringValue) { + // Either a converted value or still the original one: store converted value. + sourceHolder.setConvertedValue(convertedValue); + args.preparedArguments[paramIndex] = convertedValue; + } + else { + */ + args.resolveNecessary = true; + args.preparedArguments[paramIndex] = sourceValue; + // } + } + catch (TypeMismatchException ex) { + throw new UnsatisfiedDependencyException( + mbd.getResourceDescription(), beanName, paramIndex, paramType, + "Could not convert " + methodType + " argument value of type [" + + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + + "] to required type [" + paramType.getName() + "]: " + ex.getMessage()); + } + } + args.arguments[paramIndex] = convertedValue; + args.rawArguments[paramIndex] = originalValue; + } + else { + // No explicit match found: we're either supposed to autowire or + // have to fail creating an argument array for the given constructor. + if (!autowiring) { + throw new UnsatisfiedDependencyException( + mbd.getResourceDescription(), beanName, paramIndex, paramType, + "Ambiguous " + methodType + " argument types - " + + "did you specify the correct bean references as " + methodType + " arguments?"); + } + try { + MethodParameter param = MethodParameter.forMethodOrConstructor(methodOrCtor, paramIndex); + Object autowiredArgument = resolveAutowiredArgument(param, beanName, autowiredBeanNames, converter); + args.rawArguments[paramIndex] = autowiredArgument; + args.arguments[paramIndex] = autowiredArgument; + args.preparedArguments[paramIndex] = new AutowiredArgumentMarker(); + args.resolveNecessary = true; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException( + mbd.getResourceDescription(), beanName, paramIndex, paramType, ex); + } + } + } + + for (String autowiredBeanName : autowiredBeanNames) { + this.beanFactory.registerDependentBean(autowiredBeanName, beanName); + if (this.beanFactory.logger.isDebugEnabled()) { + this.beanFactory.logger.debug("Autowiring by type from bean name '" + beanName + + "' via " + methodType + " to bean named '" + autowiredBeanName + "'"); + } + } + + return args; +} +``` + + - 3.3.1.2.1.1) resolveAutowiredArgument +- protected Object resolveAutowiredArgument( + MethodParameter param, String beanName, Set<String> autowiredBeanNames, TypeConverter typeConverter) { + + return this.beanFactory.resolveDependency( + new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter); +} + + - 3.3.1.2.2) InstantiationStrategy.instantiate + +- 实例化的时候可以采用不同的策略进行实例化。 +- 如果beanDefinition.getMethodOverrides()为空,即用户没有使用replace或lookup的配置方法,那么直接使用反射的方式;如果使用了,需要将这两个配置通过的功能切入进去,所以就必须要使用动态代理的方式将包含两个特性所对应的逻辑的拦截增强器设置进去,这样才可以保证在调用方法的时候会被相应的拦截器增强,返回值为包含拦截器的代理实例。 +- 以SimpleInstantiationStrategy为例: + +``` +public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { + // Don't override the class with CGLIB if no overrides. +``` + +- // 如果有需要覆盖或者动态替换的方法,则使用cglib进行动态代理,因为可以在创建代理的同时将动态方法织入类中,但是如果没有需要动态改变的方法,为了方便直接反射就可以了 + if (bd.getMethodOverrides().isEmpty()) { + +``` +// 没有需要覆盖的方法 + Constructor<?> constructorToUse; + synchronized (bd.constructorArgumentLock) { + constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod; + if (constructorToUse == null) { + final Class<?> clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor<?>>() { + @Override + public Constructor<?> run() throws Exception { + return clazz.getDeclaredConstructor((Class[]) null); + } + }); + } + else { + constructorToUse = clazz.getDeclaredConstructor((Class[]) null); + } + bd.resolvedConstructorOrFactoryMethod = constructorToUse; + } + catch (Throwable ex) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + } + } +``` + +- // 反射实例化 + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // Must generate CGLIB subclass. + return instantiateWithMethodInjection(bd, beanName, owner); + } +} + +- 以CglibSubclassingInstantiationStrategy为例: + +``` +public Object instantiate(Constructor<?> ctor, Object... args) { + Class<?> subclass = createEnhancedSubclass(this.beanDefinition); + Object instance; + if (ctor == null) { + instance = BeanUtils.instantiateClass(subclass); + } + else { + try { + Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes()); + instance = enhancedSubclassConstructor.newInstance(args); + } + catch (Exception ex) { + throw new BeanInstantiationException(this.beanDefinition.getBeanClass(), + "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex); + } + } + // SPR-10785: set callbacks directly on the instance instead of in the + // enhanced class (via the Enhancer) in order to avoid memory leaks. + Factory factory = (Factory) instance; + factory.setCallbacks(new Callback[] {NoOp.INSTANCE, + new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner), + new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)}); + return instance; +} +``` + + +- createEnhancedSubclass: + +``` +private Class<?> createEnhancedSubclass(RootBeanDefinition beanDefinition) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(beanDefinition.getBeanClass()); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + if (this.owner instanceof ConfigurableBeanFactory) { + ClassLoader cl = ((ConfigurableBeanFactory) this.owner).getBeanClassLoader(); + enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); + } + enhancer.setCallbackFilter(new MethodOverrideCallbackFilter(beanDefinition)); + enhancer.setCallbackTypes(CALLBACK_TYPES); + return enhancer.createClass(); +} +``` + + +- + + - 3.3.1.3) instantiateBean(无参数的构造方法的实例化) + +``` +protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { + try { + Object beanInstance; + final BeanFactory parent = this; + if (System.getSecurityManager() != null) { + beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { + return getInstantiationStrategy().instantiate(mbd, beanName, parent); + } + }, getAccessControlContext()); + } + else { +``` + +- // 实例化即可,见3.3.1.2.1 + beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); + } + BeanWrapper bw = new BeanWrapperImpl(beanInstance); + initBeanWrapper(bw); + return bw; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); + } +} + + - 3.3.2) addSingletonFactory(singletonFactories缓存) +- 执行到这里时可以肯定当前bean已经构造完成,只是尚未填充属性,但是内存地址已经确定了。将当前bean加入到singletonFactories中,下次getBean时会先检测当前bean是否已经被加入到singletonFactories,如果已经存在,则返回缓存,否则就正常创建。 +- protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { + Assert.notNull(singletonFactory, "Singleton factory must not be null"); + synchronized (this.singletonObjects) { + if (!this.singletonObjects.containsKey(beanName)) { + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } + } +} + - 3.3.3) getEarlyBeanReference(从singletonFactories取出来时调用的getObject) +- protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { + Object exposedObject = bean; + if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { + SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; + exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); + if (exposedObject == null) { + return null; + } + } + } + } + return exposedObject; +} + + + - 3.3.4) polulateBean(属性值注入) +- 逻辑: + - 1)InstantiationAwareBeanPostProcessor处理器的postProcessAfterInstantiation函数的应用,此函数可以控制程序是否继续进行属性填充。 + - 2)根据注入类型(byName/byType),提取依赖的bean,并统一存入PropertyValues中。 + - 3)应用InstantiationAwareBeanPostProcessor处理器的postProcessPropertyValues方法,对属性获取完毕填充前 对属性的再次处理,典型应用是RequiredAnnotationBeanPostProcessor类中对属性的验证 + - 4)将所有PropertyValues中的属性填充至BeanWrapper中。 +- protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { + PropertyValues pvs = mbd.getPropertyValues(); + + if (bw == null) { + if (!pvs.isEmpty()) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); + } + else { +- // 没有可填充的属性 + // Skip property population phase for null instance. + return; + } + } + // 给InstantiationAwareBeanPostProcessor最后一次机会在属性设置前来改变bean +- // 如:可以用来支持属性注入的类型 + // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the + // state of the bean before properties are set. This can be used, for example, + // to support styles of field injection. + boolean continueWithPropertyPopulation = true; + + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; +- // 返回值为是否继续填充bean + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + // 如果后处理器发出停止填充命令则终止后续的执行 + if (!continueWithPropertyPopulation) { + return; + } + + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + MutablePropertyValues newPvs = new MutablePropertyValues(pvs); + + // Add property values based on autowire by name if applicable. + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { +- // 根据名称自动注入,存入PropertyValues + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { +- // 根据类型自动注入,存入PropertyValues + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + // 后处理器已经初始化 + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); +- // 需要依赖检查 + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); + if (hasInstAwareBpps) { +- // 对所有需要依赖检查的属性进行后处理 + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); + if (pvs == null) { + return; + } + } + } + } + if (needsDepCheck) { + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + // 将属性应用到bean中 + applyPropertyValues(beanName, mbd, bw, pvs); +} +- + + - 3.3.4.1) autowireByName(按名获取待注入的属性) +- protected void autowireByName( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + // 寻找bw中需要依赖注入的属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + if (containsBean(propertyName)) { +- // 递归初始化相关的bean + Object bean = getBean(propertyName); +- pvs.add(propertyName, bean); +- // 注册依赖 + registerDependentBean(propertyName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Added autowiring by name from bean name '" + beanName + + "' via property '" + propertyName + "' to bean named '" + propertyName + "'"); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName + + "' by name: no matching bean found"); + } + } + } +} + + - 3.3.4.2) autowireByType(按类型获取待注入的属性) + - protected void autowireByType( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + + Set<String> autowiredBeanNames = new LinkedHashSet<String>(4); +- // 寻找bw中需要依赖注入的属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + try { + PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); + // Don't try autowiring by type for type Object: never makes sense, + // even if it technically is a unsatisfied, non-simple property. +- if (Object.class != pd.getPropertyType()) { +- // 探测指定属性的set方法 + MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); + // Do not allow eager init for type matching in case of a prioritized post-processor. + boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass()); + DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); +- // 解析指定beanName的属性所匹配的值,并把解析到的属性名称存储在autowireBeanNames中 + Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); + if (autowiredArgument != null) { + pvs.add(propertyName, autowiredArgument); + } + +``` +// 如@Autowired private List<A> list; +``` + +- // 这时候会找到所有匹配A类型的bean并将其注入,所以每个属性可能会对应多个bean + for (String autowiredBeanName : autowiredBeanNames) { +- // 注册依赖 + registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" + + propertyName + "' to bean named '" + autowiredBeanName + "'"); + } + } + autowiredBeanNames.clear(); + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex); + } + } +} +- + + - 3.3.4.2.1) DefaultListableBeanFactory#resolveDependency(寻找类型匹配) + +``` +public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, + Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + + descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); +``` + +- // 处理特定的类的注入 + if (javaUtilOptionalClass == descriptor.getDependencyType()) { + return new OptionalDependencyFactory().createOptionalDependency(descriptor, requestingBeanName); + } + else if (ObjectFactory.class == descriptor.getDependencyType() || + ObjectProvider.class == descriptor.getDependencyType()) { + return new DependencyObjectProvider(descriptor, requestingBeanName); + } + else if (javaxInjectProviderClass == descriptor.getDependencyType()) { + return new Jsr330ProviderFactory().createDependencyProvider(descriptor, requestingBeanName); + } + else { + Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary( + descriptor, requestingBeanName); + if (result == null) { +- // 处理通用逻辑 + result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); + } + return result; + } +} + + - 3.3.4.2.1.1) doResolveDependency + +``` +public Object doResolveDependency(DependencyDescriptor descriptor, String beanName, + Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + + InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); + try { + Object shortcut = descriptor.resolveShortcut(this); + if (shortcut != null) { + return shortcut; + } + + Class<?> type = descriptor.getDependencyType(); + Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); + if (value != null) { + if (value instanceof String) { + String strVal = resolveEmbeddedValue((String) value); + BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); + value = evaluateBeanDefinitionString(strVal, bd); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return (descriptor.getField() != null ? + converter.convertIfNecessary(value, type, descriptor.getField()) : + converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + } + + Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); + if (multipleBeans != null) { + return multipleBeans; + } + + Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); + } + return null; + } + + String autowiredBeanName; + Object instanceCandidate; + + if (matchingBeans.size() > 1) { + autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); + if (autowiredBeanName == null) { + if (descriptor.isRequired() || !indicatesMultipleBeans(type)) { + return descriptor.resolveNotUnique(type, matchingBeans); + } + else { + // In case of an optional Collection/Map, silently ignore a non-unique case: + // possibly it was meant to be an empty collection of multiple regular beans + // (before 4.3 in particular when we didn't even look for collection beans). + return null; + } + } + instanceCandidate = matchingBeans.get(autowiredBeanName); + } + else { + // We have exactly one match. + Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next(); + autowiredBeanName = entry.getKey(); + instanceCandidate = entry.getValue(); + } + + if (autowiredBeanNames != null) { + autowiredBeanNames.add(autowiredBeanName); + } + return (instanceCandidate instanceof Class ? + descriptor.resolveCandidate(autowiredBeanName, type, this) : instanceCandidate); + } + finally { + ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint); + } +} +``` + + - 3.3.4.2.1.1.1) findAutowireCandidates(获取setter注入的bean) +- protected Map<String, Object> findAutowireCandidates( + String beanName, Class<?> requiredType, DependencyDescriptor descriptor) { + + String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this, requiredType, true, descriptor.isEager()); + Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length); + for (Class<?> autowiringType : this.resolvableDependencies.keySet()) { + if (autowiringType.isAssignableFrom(requiredType)) { + Object autowiringValue = this.resolvableDependencies.get(autowiringType); + autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType); + if (requiredType.isInstance(autowiringValue)) { + result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue); + break; + } + } + } + for (String candidateName : candidateNames) { + if (!isSelfReference(beanName, candidateName) && isAutowireCandidate(candidateName, descriptor)) { + result.put(candidateName, getBean(candidateName)); + } + } + if (result.isEmpty()) { + DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch(); + for (String candidateName : candidateNames) { + if (!candidateName.equals(beanName) && isAutowireCandidate(candidateName, fallbackDescriptor)) { + result.put(candidateName, getBean(candidateName)); + } + } + } + return result; +} + +- + + - 3.3.4.3) AutowiredAnnotationBeanPostProcesser#postProcessPropertyValues(@Autowired依赖注入) + +``` +public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { + + InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); + try { + metadata.inject(bean, beanName, pvs); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); + } + return pvs; +} +``` + + - 3.3.4.3.1) InjectionMetadata#inject + +``` +public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable { + Collection<InjectedElement> elementsToIterate = + (this.checkedElements != null ? this.checkedElements : this.injectedElements); + if (!elementsToIterate.isEmpty()) { + boolean debug = logger.isDebugEnabled(); + for (InjectedElement element : elementsToIterate) { + if (debug) { + logger.debug("Processing injected element of bean '" + beanName + "': " + element); + } + element.inject(target, beanName, pvs); + } + } +} +``` + + - 3.3.4.3.1.1) AutowiredFieldElement#inject + - protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { + Field field = (Field) this.member; + try { + Object value; + if (this.cached) { + value = resolvedCachedArgument(beanName, this.cachedFieldValue); + } + else { + DependencyDescriptor desc = new DependencyDescriptor(field, this.required); + desc.setContainingClass(bean.getClass()); + Set<String> autowiredBeanNames = new LinkedHashSet<String>(1); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + - // 3.3.4.2.1 + value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); + synchronized (this) { + if (!this.cached) { + if (value != null || this.required) { + this.cachedFieldValue = desc; + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (beanFactory.containsBean(autowiredBeanName)) { + if (beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { + this.cachedFieldValue = new RuntimeBeanReference(autowiredBeanName); + } + } + } + } + else { + this.cachedFieldValue = null; + } + this.cached = true; + } + } + } + if (value != null) { + ReflectionUtils.makeAccessible(field); + field.set(bean, value); + } + } + catch (Throwable ex) { + throw new BeanCreationException("Could not autowire field: " + field, ex); + } +} + + - 3.3.4.4) applyPropertyValues(注入属性值) +- AbstractAutowireCapableBeanFactory +- protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs == null || pvs.isEmpty()) { + return; + } + + MutablePropertyValues mpvs = null; + List<PropertyValue> original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; +- // 如果mpvs中的值已经被转换为对应的类型,则可以直接设置到beanwrapper中 + if (mpvs.isConverted()) { + // Shortcut: use the pre-converted values as-is. + try { + bw.setPropertyValues(mpvs); + return; + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } + } + original = mpvs.getPropertyValueList(); + } + else { +- // 如果pvs并不是使用MutablePropertyValues封装的类型,那么直接使用原始的属性获取方法 + original = Arrays.asList(pvs.getPropertyValues()); + } + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } +- // 获取对应的解析器 + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); + + // Create a deep copy, resolving any references for values. + List<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size()); + boolean resolveNecessary = false; +- // 遍历属性,将属性转换为对应类的对应属性的类型 + for (PropertyValue pv : original) { + if (pv.isConverted()) { + deepCopy.add(pv); + } + else { + String propertyName = pv.getName(); + Object originalValue = pv.getValue(); + Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); + Object convertedValue = resolvedValue; + boolean convertible = bw.isWritableProperty(propertyName) && + !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + if (convertible) { + convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); + } + // Possibly store converted value in merged bean definition, + // in order to avoid re-conversion for every created bean instance. + if (resolvedValue == originalValue) { + if (convertible) { + pv.setConvertedValue(convertedValue); + } + deepCopy.add(pv); + } + else if (convertible && originalValue instanceof TypedStringValue && + !((TypedStringValue) originalValue).isDynamic() && + !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { + pv.setConvertedValue(convertedValue); + deepCopy.add(pv); + } + else { + resolveNecessary = true; + deepCopy.add(new PropertyValue(pv, convertedValue)); + } + } + } + if (mpvs != null && !resolveNecessary) { + mpvs.setConverted(); + } + + // Set our (possibly massaged) deep copy. + try { + bw.setPropertyValues(new MutablePropertyValues(deepCopy)); + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } +} + +- + + - 3.3.5) initializeBean(调用init-method方法) +- AbstractAutowireCapableBeanFactory +- 主要是调用用户设定的初始化方法,还有一些其他工作 + +``` +protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged(new PrivilegedAction<Object>() { + @Override + public Object run() { +``` + +- // 激活Aware方法 + invokeAwareMethods(beanName, bean); + return null; + } + }, getAccessControlContext()); + } + else { + invokeAwareMethods(beanName, bean); + } + + Object wrappedBean = bean; + if (mbd == null || !mbd.isSynthetic()) { +- // 应用后处理器 + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + } + + try { +- // 激活用户自定义的init方法 + invokeInitMethods(beanName, wrappedBean, mbd); + } + catch (Throwable ex) { + throw new BeanCreationException( + (mbd != null ? mbd.getResourceDescription() : null), + beanName, "Invocation of init method failed", ex); + } + + if (mbd == null || !mbd.isSynthetic()) { + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + } + return wrappedBean; +} + - 3.3.5.1) invokeAwareMethods +- Aware +- Spring中提供一些Aware相关接口,比如BeanFactoryAware、ApplicationContextAware等,实现这些Aware接口的bean被初始化后,可以取得一些相对应的资源。 +- 如实现BeanFactoryAware的bean在初始化后,Spring容器将会注入BeanFactory的实例。 + +``` +private void invokeAwareMethods(final String beanName, final Object bean) { + if (bean instanceof Aware) { + if (bean instanceof BeanNameAware) { + ((BeanNameAware) bean).setBeanName(beanName); + } + if (bean instanceof BeanClassLoaderAware) { + ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); + } + if (bean instanceof BeanFactoryAware) { + ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); + } + } +} +``` + + + - 3.3.5.2) BeanPostProcessor +- 调用用户自定义初始化方法之前和之后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization方法,使 用户可以根据自己的业务需求进行响应的处理。 + +``` +public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessBeforeInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + + +``` +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + - 3.3.5.2.1) @PostConstructor +- 当使用该注解时,Spring会去注册一个BeanPostProcessor:InitDestroyAnnotationBeanPostProcessor。该bean会同自定义的BeanPostProcessor一样,在自定义初始化方法之前和之后被调用(当然@PostConstrcut只会在之前被调用)。 +- InitDestroyAnnotationBeanPostProcessor#postProcessBeforeInitialization + + +``` +public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass()); + try { + metadata.invokeInitMethods(bean, beanName); + } + catch (InvocationTargetException ex) { + throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Couldn't invoke init method", ex); + } + return bean; +} +``` + + + - 3.3.5.2.1.1) findLifecycleMetadata + +``` +private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) { + if (this.lifecycleMetadataCache == null) { + // Happens after deserialization, during destruction... + return buildLifecycleMetadata(clazz); + } + // Quick check on the concurrent map first, with minimal locking. + LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz); + if (metadata == null) { + synchronized (this.lifecycleMetadataCache) { + metadata = this.lifecycleMetadataCache.get(clazz); + if (metadata == null) { + metadata = buildLifecycleMetadata(clazz); + this.lifecycleMetadataCache.put(clazz, metadata); + } + return metadata; + } + } + return metadata; +} +``` + + + +``` +private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) { + final boolean debug = logger.isDebugEnabled(); + LinkedList<LifecycleElement> initMethods = new LinkedList<LifecycleElement>(); + LinkedList<LifecycleElement> destroyMethods = new LinkedList<LifecycleElement>(); + Class<?> targetClass = clazz; + + do { + final LinkedList<LifecycleElement> currInitMethods = new LinkedList<LifecycleElement>(); + final LinkedList<LifecycleElement> currDestroyMethods = new LinkedList<LifecycleElement>(); + + ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + if (initAnnotationType != null) { + if (method.getAnnotation(initAnnotationType) != null) { + LifecycleElement element = new LifecycleElement(method); + currInitMethods.add(element); + if (debug) { + logger.debug("Found init method on class [" + clazz.getName() + "]: " + method); + } + } + } + if (destroyAnnotationType != null) { + if (method.getAnnotation(destroyAnnotationType) != null) { + currDestroyMethods.add(new LifecycleElement(method)); + if (debug) { + logger.debug("Found destroy method on class [" + clazz.getName() + "]: " + method); + } + } + } + } + }); + + initMethods.addAll(0, currInitMethods); + destroyMethods.addAll(currDestroyMethods); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + return new LifecycleMetadata(clazz, initMethods, destroyMethods); +} +``` + + + - 3.3.5.2.1.2) invokeInitMethods + +``` +public void invokeInitMethods(Object target, String beanName) throws Throwable { + Collection<LifecycleElement> initMethodsToIterate = + (this.checkedInitMethods != null ? this.checkedInitMethods : this.initMethods); + if (!initMethodsToIterate.isEmpty()) { + boolean debug = logger.isDebugEnabled(); + for (LifecycleElement element : initMethodsToIterate) { + if (debug) { + logger.debug("Invoking init method on bean '" + beanName + "': " + element.getMethod()); + } + element.invoke(target); + } + } +} +``` + + +##### 3.3.5.3) invokeInitMethods(激活自定义的init方法) +- 客户定制的初始化方法除了使用配置init-method外,还可以使自定义的bean实现InitializingBean接口,并在afterPropertiesSet中实现自己的初始化业务逻辑。 +- init-method和afterPropertiesSet都是在初始化bean时执行,执行顺序是afterPropertiesSet先执行,init-method后执行。 +- 该方法中实现了这两个步骤的初始化方法调用。 + +``` +protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + // 首先检查是否是InitializingBean,如果是的话需要调用afterPropertiesSet方法 + boolean isInitializingBean = (bean instanceof InitializingBean); + if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); + } + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { + @Override + public Object run() throws Exception { + ((InitializingBean) bean).afterPropertiesSet(); + return null; + } + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + ((InitializingBean) bean).afterPropertiesSet(); + } + } + + if (mbd != null) { + String initMethodName = mbd.getInitMethodName(); + if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.isExternallyManagedInitMethod(initMethodName)) { + invokeCustomInitMethod(beanName, bean, mbd); + } + } +} +``` + +- + + - 3.3.5.3.1) invokeCustomInitMethod + +``` +protected void invokeCustomInitMethod(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + + String initMethodName = mbd.getInitMethodName(); + final Method initMethod = (mbd.isNonPublicAccessAllowed() ? + BeanUtils.findMethod(bean.getClass(), initMethodName) : + ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + if (initMethod == null) { + if (mbd.isEnforceInitMethod()) { + throw new BeanDefinitionValidationException("Couldn't find an init method named '" + + initMethodName + "' on bean with name '" + beanName + "'"); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No default init method named '" + initMethodName + + "' found on bean with name '" + beanName + "'"); + } + // Ignore non-existent default lifecycle methods. + return; + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); + } + + if (System.getSecurityManager() != null) { + AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { + @Override + public Object run() throws Exception { + ReflectionUtils.makeAccessible(initMethod); + return null; + } + }); + try { + AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { + @Override + public Object run() throws Exception { + initMethod.invoke(bean); + return null; + } + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + InvocationTargetException ex = (InvocationTargetException) pae.getException(); + throw ex.getTargetException(); + } + } + else { + try { + ReflectionUtils.makeAccessible(initMethod); + initMethod.invoke(bean); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +} +``` + + +- + + - 3.3.6) getSingleton(beanName,allowEarlyReference) +- protected Object getSingleton(String beanName, boolean allowEarlyReference) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return (singletonObject != NULL_OBJECT ? singletonObject : null); +} +- + + - 3.3.7) registerDisposableBeanIfNecessary(注册DisposableBean) +- 对于销毁方法的扩展,除了配置属性destroy-method,用户还可以注册后处理器DestructionAwareBeanPostProcessor来统一处理bean的销毁方法。 +- protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) { + AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null); + if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) { + if (mbd.isSingleton()) { +- // 单例模式下,注册需要销毁的bean,此方法中会处理实现DisposableBean的bean,并且对所有的bean使用DestructionAwareBeanPostProcessors处理 + // Register a DisposableBean implementation that performs all destruction + // work for the given bean: DestructionAwareBeanPostProcessors, + // DisposableBean interface, custom destroy method. + registerDisposableBean(beanName, + new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); + } + else { +- // 自定义scope的处理 + // A bean with a custom scope... + Scope scope = this.scopes.get(mbd.getScope()); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'"); + } + scope.registerDestructionCallback(beanName, + new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc)); + } + } +} +- + + - 4) getObjectForBeanInstance(从bean 的实例中获取对象) +- 无论是从缓存中获取到的bean还是通过不同的scope策略加载的bean都只是最原始的bean状态,并不一定是我们最终想要的bean。 +- 比如,我们需要对FactoryBean进行处理,那么这里得到的其实是FactoryBean的初始状态,但是我们真正需要的是FactoryBean中定义的factory-method(getObject方法)方法中返回的bean,而getObjectForBeanInstance就是完成这个工作的。 + +- 下面这个方法完成了以下任务: + - 1)对FactoryBean正确性的验证 + - 2)对非FactoryBean不做任何处理 + - 3)对bean进行转换 + - 4)将从Factory解析bean的工作委托给getObjectFromFactoryBean。 +- protected Object getObjectForBeanInstance( + Object beanInstance, String name, String beanName, RootBeanDefinition mbd) { + + // Don't let calling code try to dereference the factory if the bean isn't a factory. + if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) { + throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); + } + + // Now we have the bean instance, which may be a normal bean or a FactoryBean. + // If it's a FactoryBean, we use it to create a bean instance, unless the + // caller actually wants a reference to the factory. + if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { + return beanInstance; + } + + Object object = null; + if (mbd == null) { + object = getCachedObjectForFactoryBean(beanName); + } + if (object == null) { + // Return bean instance from factory. + FactoryBean<?> factory = (FactoryBean<?>) beanInstance; + // Caches object obtained from FactoryBean if it is a singleton. + if (mbd == null && containsBeanDefinition(beanName)) { + mbd = getMergedLocalBeanDefinition(beanName); + } + boolean synthetic = (mbd != null && mbd.isSynthetic()); + object = getObjectFromFactoryBean(factory, beanName, !synthetic); + } + return object; +} + - 4.1) getObjectFromFactoryBean(从FactoryBean中解析bean) +- 返回的bean如果是单例的,那就必须要保证全局唯一,同时,也因为是单例的,所以不必重复创建,可以使用缓存来提高性能。 +- protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) { + if (factory.isSingleton() && containsSingleton(beanName)) { + synchronized (getSingletonMutex()) { + Object object = this.factoryBeanObjectCache.get(beanName); + if (object == null) { + object = doGetObjectFromFactoryBean(factory, beanName); + // Only post-process and store if not put there already during getObject() call above + // (e.g. because of circular reference processing triggered by custom getBean calls) + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + if (object != null && shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + } + this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT)); + } + } + return (object != NULL_OBJECT ? object : null); + } + } + else { + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (object != null && shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); + } + } + return object; + } +} + - 4.1.1) doGetObjectFromFactoryBean + +``` +private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName) + throws BeanCreationException { + + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + object = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() { + @Override + public Object run() throws Exception { + return factory.getObject(); + } + }, acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + object = factory.getObject(); + } + } + catch (FactoryBeanNotInitializedException ex) { + throw new BeanCurrentlyInCreationException(beanName, ex.toString()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); + } + + // Do not accept a null value for a FactoryBean that's not fully + // initialized yet: Many FactoryBeans just return null then. + if (object == null && isSingletonCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException( + beanName, "FactoryBean which is currently in creation returned null from getObject"); + } + return object; +} +``` + + +- + +- 循环依赖 + +- Spring容器循环依赖包括构造器循环依赖和setter循环依赖。 +- 核心就是singletonObjects、singletonFactories、earlySingletonObjects和singletonsCurrentlyInCreation四个集合。 + - 1)singletonObjects是beanName与beanInstance的Map,是真正的缓存,beanInstance是构造完毕的,凡是正常地构造完毕的单例bean都会放入缓存中。 + - 2)earlySingletonObjects也是beanName与beanInstance的Map,beanInstance是已经调用了createBean方法,但是没有清除加载状态和加入至缓存的bean。仅在当前bean创建时存在,用于检测代理bean循环依赖。 + - 3)singleFactories是beanName与ObjectFactory的Map,仅在当前bean创建时存在,是尚未调用createBean的bean。用于setter循环依赖时实现注入。 + - 4)singletonsCurrentlyInCreation是beanName的集合,用于检测构造器循环依赖。 + +- getBean在循环依赖时所执行的步骤是这样的: + - 1)检测当前bean是否在singletonObjects中,在则直接返回缓存好的bean;不在则检测是否在singletonFactories中,在,则调用其getObject方法,返回,并从singletonFactories中移除,加入到earlySingletonObjects中。 + + - 2)正常创建,beforeSingletonCreation:检测当前bean是否在singletonsCurrentlyInCreation,如果存在,抛出异常。表示存在构造器循环依赖。如果不存在,则将当前bean加入。 + + - 3)bean初始化,分为构造方法初始化、工厂方法初始化和简单初始化。如果是构造方法初始化,那么递归地获取参数bean。其他情况不会递归获取bean。 + + - 4)addSingletonFactory:如果当前bean不在singletonObjects中,则将当前bean加入到singletonFactories中(getObject方法是getEarlyBeanReference),并从earlySingletonObjects中移除。 + + - 5)填充属性,简单初始化的话会递归创建所依赖的bean。 + - 6)调用用户初始化方法,比如BeanPostProcesser、InitializingBean、init-method,有可能返回代理后的bean。 + - 6) 检测循环依赖,如果当前bean在singletonObjects中,则判断当前bean(current bean)与singletonObjects中的bean(cached bean)是否是同一个,如果不是,那么说明当前bean是被代理过的,由于依赖当前bean的bean持有的是对cached bean的引用,这是不被允许的,所以会抛出BeanCurrentlyInCreationException异常。 + + - 7)afterSingletonCreation:将当前bean从singletonsCurrentlyInCreation中删除 + - 8)addSingleton:将当前bean加入到singletonObjects,然后从singletonFactories, earlySingletonObjects中移除,结束 +- 构造器循环依赖 +- 表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。 +- 1、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有,于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,没有,则将A放入。构造方法初始化时需要B实例(A尚未放入到singletonFactories中),于是调用了getBean(B)方法、 +- 2、Spring容器创建单例“B” Bean,首先检测singletonFactories是否包含B,发现没有,于是正常创建,然后检测B是否包含在singletonsCurrentlyInCreation中,没有,则将B放入。构造方法初始化时需要C实例(B尚未放入到singletonFactories中),于是调用了getBean(C)方法、 +- 3、Spring容器创建单例“C” Bean,首先检测singletonFactories是否包含C,发现没有,于是正常创建,然后检测C是否包含在singletonsCurrentlyInCreation中,没有,则将C放入。构造方法初始化时需要A实例(C尚未放入到singletonFactories中),于是调用了getBean(A)方法、 +- 4、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,有,抛出BeanCurrentlyInCreationException异常。 +- setter循环依赖 +- 表示通过setter注入方式构成的循环依赖。 +- 对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。 +- 1、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现没有,于是正常创建,然后检测A是否包含在singletonsCurrentlyInCreation中,没有,则将A放入。注入属性时需要B实例,于是调用了getBean(B)方法、 +- 2、Spring容器创建单例“B” Bean,首先检测singletonFactories是否包含B,发现没有,于是正常创建,然后检测B是否包含在singletonsCurrentlyInCreation中,没有,则将B放入。注入属性时需要C实例,于是调用了getBean(C)方法、 +- 3、Spring容器创建单例“C” Bean,首先检测singletonFactories是否包含C,发现没有,于是正常创建,然后检测C是否包含在singletonsCurrentlyInCreation中,没有,则将C放入。注入属性时需要A实例,于是调用了getBean(A)方法、 +- 4、Spring容器创建单例“A” Bean,首先检测singletonFactories是否包含A,发现有,于是返回缓存了的bean,并将A从singletonFactories删除,返回A实例。 +- 5、C得到A实例。set进来,B、A也是这样。结束。 + +- 对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。 + +- + +- 实例 + +``` +public class Main { + public static void main(String[] args) { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); + UserService userService = (UserService) applicationContext.getBean("userService"); + userService.login(); + } +} +``` + +# 实例——循环依赖 +## 注解注入 + +``` +public class A { + @Autowired + private B b; +} +``` + + + +``` +public class B { + @Autowired + private C c; +} +``` + + + +``` +public class C { + @Autowired + private A a; +} +``` + + + + - 1)创建A时调用beforeSingletonCreation + + - 2)调用doCreateBean时earlySingletonExposure为true,调用了addSingletonFactory +- 注意,并没有递归去初始化B,返回它所依赖的bean时得到的是null。 + + + - 3)然后调用populateBean,在postProcesser处理时调用了 + - AutowiredAnnotationBeanPostProcesser#postProcessPropertyValues,最终调用了3.3.4.2.1.1.1) findAutowireCandidates,它调用了A所依赖的B的getBean方法。 + - 4)B的getBean时调用了beforeSingletonCreation, + + - 5)调用doCreateBean时earlySingletonExposure为true,调用了addSingletonFactory +- 注意,并没有递归去初始化C,返回它所依赖的bean时得到的是null。 + + - 6)然后调用populateBean,在postProcesser处理时调用了 + - AutowiredAnnotationBeanPostProcesser#postProcessPropertyValues,最终调用了3.3.4.2.1.1.1) findAutowireCandidates,它调用了B所依赖的C的getBean方法。 + - 7)B的getBean时调用了beforeSingletonCreation, + + - 8)调用doCreateBean时earlySingletonExposure为true,调用了addSingletonFactory +- 注意,并没有递归去初始化A,返回它所依赖的bean时得到的是null。 + + - 9)然后调用populateBean,在postProcesser处理时调用了 + - AutowiredAnnotationBeanPostProcesser#postProcessPropertyValues,最终调用了3.3.4.2.1.1.1) findAutowireCandidates,它调用了C所依赖的A的getBean方法。 +- 10)A在调用getSingleton时发现singletonFactories存在CircleA,然后调用其getObject方法(调用了getEarlyReference),之后将CircleA放入earlySingletonObjects,然后从singletonFactories中移除。 + + + - 11)从getBean(CircleA)中返回,回到CircleC的findAutowireCandidates,然后带着CircleA实例回到了inject方法,将CircleA实例设置给了CircleC。 + - 12)CircleC去检测循环依赖,没有循环依赖,返回bean。 + - 13)在调用getSingleton时发现singletonFactories存在CircleA,然后调用其getObject方法(调用了getEarlyReference),之后将CircleA放入earlySingletonObjects,然后从singletonFactories中移除。 + - 13)从getBean(CircleC)中返回,回到CircleB的findAutowireCandidates,然后带着CircleC实例回到了inject方法,将CircleC实例设置给了CircleB。 + - 14)CircleB去检测循环依赖,没有循环依赖,返回bean。 + - 15)从getBean(CircleB)中返回,回到CircleA的findAutowireCandidates,然后带着CircleB实例回到了inject方法,将CircleB实例设置给了CircleA。 + - 16)CircleA去检测循环依赖,没有循环依赖,返回bean。结束。 + +## setter注入 + +``` +public class A { + private B b; + + @Autowired + public void setB(B b) { + this.b = b; + } +} +``` + + + +``` +public class B { + private C c; + @Autowired + public C getC() { + return c; + } +} +``` + + + +``` +public class C { + private A a; + + @Autowired + public void setA(A a) { + this.a = a; + } +} +``` + + +- 调用栈与注解注入一致 + +## 构造器注入(抛出BeanCurrentlyInCreationException异常) + +``` +public class A { + private B b; + @Autowired + public A(B b) { + this.b = b; + } +} +``` + + +- 步骤类似,只有两个地方不同。 + - 1)调用doCreateBean时,会采用构造方法初始化的方式,此时会递归地初始化构造方法参数bean。因为是构造方法初始化,所以递归获取参数bean是在将自己放入singletonFactories之前。 + - 2)正因为没有将自己放入singletonFactories,所以不会在getBean从singletonFactories返回已经创建过的bean。 + +## 存在代理时的循环依赖(抛出BeanCurrentlyInCreationException异常) + +``` +@Component("CircleA") +public class A { + @Autowired + private B b; +} +``` + + + +``` +@Component("CircleB") +public class B { + @Autowired + private A a; +} +``` + + + +``` +@Component +public class ABeanPostProcessor implements BeanPostProcessor { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if(beanName.equals("CircleA")) { + System.out.println("proxy-A"); + return new Object(); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} +``` + + +- <context:component-scan base-package="proxycircle" ></context:component-scan> + +- Exception in thread "main" +- org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'CircleA': Bean with name 'CircleA' has been injected into other beans [CircleB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. + +- 此时就与earlySIngletonObjects有关系了。 + - 1)getBean(A)时,将A加入了singletonFactories,注入属性时setter注入B,调用getBean(B) + - 2)调用getBean(B)时,将B加入了singletonFactories,注入属性时setter注入A,调用getBean(A) + - 3)因为A已经存在在singletonFactories,于是取出,调用getObject,然后将A加入到earlySIngletonObjects,返回A。 + - 4)B注入属性A完毕后,B构造完毕,将B加入singletonObjects,从 earlySIngletonObjects和singletonFactories中移除B + - 5)A注入属性B完毕后,执行BeanPostProcessor,此时A变为了Object(CurrentA)。检测代理bean循环依赖,发现singletonObjects中存在Cached A,于是取出,将CachedA 与 CurrentA比较,发现不同,然后发现有B依赖着Cached A,数据发生不一致,抛出异常。 + +- https://www.iflym.com/index.php/code/201208280003.html +- Spring AOP(AspectJ) +- AOP术语 +- 1、切面(aspect/advisor) +- 类是对物体特征的抽象,切面就是对横切关注点的抽象。组合了Pointcut与Advice,在Spring中有时候也称为Advisor。 +- 2、连接点(join point) +- 被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。 +- 3、切入点(pointcut) +- 描述的一组符合某个条件的join point,通常使用pointcut表达式来限定join point。 +- 4、通知(advice) +- 所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、返回、环绕通知五类。 +- 5、目标对象 +- 代理的目标对象 +- 6、织入(weave) +- 将Advice织入join point的这个过程 +- 7、引介(introduction) +- 在不修改代码的前提下,引介可以在运行期为类动态地添加一些方法或字段 +- Advisor +- 通知Advice是Spring提供的一种切面(Aspect)。但其功能过于简单,只能将切面织入到目标类的所有目标方法中,无法完成将切面织入到指定目标方法中。 +- 顾问Advisor是Spring提供的另一种切面。其可以完成更为复杂的切面织入功能。PointcutAdvisor是顾问的一种,可以指定具体的切入点。顾问对通知进行了包装,会根据不同的通知类型,在不同的时间点,将切面织入到不同的切入点。 +- Advisor组合了Pointcut与Advice。 +- 除了引介Advisor外,几乎所有的advisor都是PointcutAdvisor。 + +``` +public interface Advisor { + Advice getAdvice(); + /** + * @return whether this advice is associated with a particular target instance + */ + boolean isPerInstance(); +} +``` + + + + +``` +public interface PointcutAdvisor extends Advisor { + + /** + * Get the Pointcut that drives this advisor. + */ + Pointcut getPointcut(); + +} +``` + +- Advice + +``` +public interface Advice { + +} +``` + + +- 增强(advice)主要包括如下五种类型 +- 1. 前置增强(BeforeAdvice):在目标方法执行前实施增强 +- 2. 后置增强(AfterAdvice):在目标方法执行后(无论是否抛出遗产)实施增强 +- 3. 环绕增强(MethodInterceptor):在目标方法执行前后实施增强 +- 4. 异常抛出增强(ThrowsAdvice):在目标方法抛出异常后实施增强 +- 5. 返回增强(AfterReturningAdvice):在目标方法正常返回后实施增强 +- 6. 引介增强(IntroductionIntercrptor):在目标类中添加一些新的方法和属性 +- BeanPostProcessor + +``` +public interface BeanPostProcessor { + Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; + Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; +} +``` + + +- JDK动态代理与CGLIB代理 +- JDK动态代理: +- 其代理对象必须是某个接口的实现,它是通过在运行时创建一个接口的实现类来完成对目标对象的代理 +- CGLIB代理:在运行时生成的代理对象是针对目标类扩展的子类。 +- CGLIB是高效的代码生产包,底层是依靠ASM操作字节码实现的,性能比JDK强。 +- 相关标签 +- <aop:aspectj-autoproxy proxy-target-class=”true”/> +- true表示使用CGLIB代理。 + +- + +- 解析AOP标签 +- <aop:aspectj-autoproxy /> +- 解析配置文件时,一旦遇到aspectj-autoproxy注解时就会使用解析器 +- AspectJAutoProxyBeanDefinitionParser进行解析。 +- 解析结果是注册了一个bean:AnnotationAwareAspectJAutoProxyCreator。 + +- 与IOC的衔接 +- 处理自定义标签 + +``` +public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) { + String namespaceUri = getNamespaceURI(ele); +``` + +- // AopNamespaceHandler + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +- NamespaceHandler#parse +- NamespaceHandler是一个接口,它有一个实现是NamespaceHandlerSupport,实现了它的parse方法,而AopNamespaceHandler直接继承了parse方法。 + + +- 它继承自NamespaceHandlerSupport,实现了NamespaceHandler接口的parse方法,而AopNamespaceHandler直接继承该parse方法。 +- NamespaceHandlerSupport#parse + +``` +public BeanDefinition parse(Element element, ParserContext parserContext) { + return findParserForElement(element, parserContext).parse(element, parserContext); +} +``` + + + +``` +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + String localName = parserContext.getDelegate().getLocalName(element); + BeanDefinitionParser parser = this.parsers.get(localName); + if (parser == null) { + parserContext.getReaderContext().fatal( + "Cannot locate BeanDefinitionParser for element [" + localName + "]", element); + } + return parser; +} +``` + +- 这里返回的parser即为AspectJAutoProxyBeanDefinitionParser。 +- AspectJAutoProxyBeanDefinitionParser#parse + +``` +public BeanDefinition parse(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + extendBeanDefinition(element, parserContext); + return null; +} +``` + + +- AopNamespaceUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary +- 注册这个creator + +``` +public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + // 注册或升级AutoProxyCreator定义beanName为internalAutoProxyCreator的BeanDefinition + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); +``` + + +- // 对于proxy-target-class以及expose-proxy属性的增强 + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); +- // 注册组件并通知,便于监听器做进一步处理 +- // 其中beanDefinition的className为AnnotationAwareAspectJAutoProxyCreator + registerComponentIfNecessary(beanDefinition, parserContext); +} + - 1) AopConfigUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary + +``` +public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) { + return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); +} +``` + +- registerOrEscalateApcAsRequired + + +``` +private static BeanDefinition registerOrEscalateApcAsRequired(Class<?> cls, BeanDefinitionRegistry registry, Object source) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); +``` + +- // 如果已经存在自动代理创建器,且存在的在自动代理创建器与现在的不一致,那么需要根据优先级来判断到底需要使用哪个 + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { +- // 改变bean最重要的是改变bean所对应的className属性 + apcDefinition.setBeanClassName(cls.getName()); + } + } +- // 如果已经存在自动代理创建器,且与将要创建的一致,那么无需再次创建 + return null; + } + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); + return beanDefinition; +} + + +``` +public static final String AUTO_PROXY_CREATOR_BEAN_NAME = + "org.springframework.aop.config.internalAutoProxyCreator"; +``` + + +- 这里的registery.registerBeanDefinition即为 +- DefaultListableBeanFactory.registerBeanDefinition。 + - 2) useClassProxyingIfNecessary +- 处理proxy-target-class属性和expose-proxy属性 + +``` +private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Element sourceElement) { + if (sourceElement != null) { + boolean proxyTargetClass = Boolean.valueOf(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + boolean exposeProxy = Boolean.valueOf(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE)); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } +} +``` + + + +``` +public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } +} +``` + + + +``` +public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } +} +``` + + +- 创建AOP代理 + +- 上文是通过自定义配置完成了读AnnotationAwareAspectJAutoProxyCreator类的自动注册。 + +- 可见这个类实现了BeanPostProcessor接口,而实现该接口后,当Spring加载这个Bean时会在实例化前 调用其postProcessAfterInitialization方法。 +- 与IOC的衔接 +- beanPostProcessor在两个地方被调用,一个是 + +- 另一个是: + + +- 前一个地方是针对于实现了InstantiationAwareBeanPostProcessor接口的BeanPostProcessor,前一个地方创建代理成功后会直接返回。其他的BeanPostProcessor会在第二个地方被调用。 + +``` +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + +- 在解析AOP标签中注册的AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcessor接口,所以在这里会被调用其postProcessAfterInstantiation方法。 + +- + +- AbstractAutoProxyCreator#postProcessAfterInitialization + + + + + +``` +public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean != null) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + + +- wrapIfNecessary +- 逻辑: + - 1)获取可以应用到该bean的所有advisor + - 2)创建代理 +- protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { +- // 如果已经处理过 + if (beanName != null && this.targetSourcedBeans.contains(beanName)) { + return bean; + } +- // 无需增强 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } +- // 给定的bean类是否代表一个基础设施类,基础设施类不应代理,或者配置了指定bean不需要自动代理 + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + // 获取适合应用到该bean的所有advisor + // Create proxy if we have advice. + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); +- // 如果获取到了增强则需要增强创建代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); +- // 创建代理 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; +} + +- 创建代理主要包含了两个步骤: + - 1)获取增强方法或者增强器 + - 2)根据获取的增强进行代理 + + - 1) getAdvicesAndAdvisorsForBean(获取可以应用到该bean的所有advisor) +- AbstractAdvisorAutoProxyCreator.getAdvicesAndAdvisorsForBean +- protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { + List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} + +- findEligibleAdvisors(合格的) +- 对于指定bean的增强方法的获取包含两个步骤: + - 1)获取所有的增强 + - 2)寻找所有的增强中适用于bean的增强并应用 +- protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { + List<Advisor> candidateAdvisors = findCandidateAdvisors(); + List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} + - 1.1) findCandidateAdvisors(获取所有的增强) +- AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors +- protected List<Advisor> findCandidateAdvisors() { + // Add all the Spring advisors found according to superclass rules. +- // 当使用注解方式配置AOP的方式的时候,并不是丢弃了对XML配置的支持 +- // 在这里调用父类方法加载配置文件中的AOP声明 + List<Advisor> advisors = super.findCandidateAdvisors(); + // Build Advisors for all AspectJ aspects in the bean factory. + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + return advisors; +} + - 1.1.1) AbstractAdvisorAutoProxyCreator#findCandidateAdvisors(获取配置文件中的增强) +- protected List<Advisor> findCandidateAdvisors() { + return this.advisorRetrievalHelper.findAdvisorBeans(); +} + - 1.1.1.1) BeanFactoryAdvisorRetrievalHelper#findAdvisorBeans + +``` +public List<Advisor> findAdvisorBeans() { + // Determine list of advisor bean names, if not cached already. + String[] advisorNames = null; + synchronized (this) { + advisorNames = this.cachedAdvisorBeanNames; + if (advisorNames == null) { + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the auto-proxy creator apply to them! +``` + +- // 从BeanFactory中获取所有对应Advisor的类 +- advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Advisor.class, true, false); + this.cachedAdvisorBeanNames = advisorNames; + } + } + if (advisorNames.length == 0) { + return new LinkedList<Advisor>(); + } + + List<Advisor> advisors = new LinkedList<Advisor>(); + for (String name : advisorNames) { + if (isEligibleBean(name)) { + if (this.beanFactory.isCurrentlyInCreation(name)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping currently created advisor '" + name + "'"); + } + } + else { + try { +- // getBean方法可以获取Advisor +- advisors.add(this.beanFactory.getBean(name, Advisor.class)); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException) { + BeanCreationException bce = (BeanCreationException) rootCause; + if (this.beanFactory.isCurrentlyInCreation(bce.getBeanName())) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping advisor '" + name + + "' with dependency on currently created bean: " + ex.getMessage()); + } + // Ignore: indicates a reference back to the bean we're trying to advise. + // We want to find advisors other than the currently created bean itself. + continue; + } + } + throw ex; + } + } + } + } + return advisors; +} +- advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( +- this.beanFactory, Advisor.class, true, false); + - 1.1.1.1.1) BeanFactoryUtils.beanNamesForTypeIncludingAncestors + +``` +public static String[] beanNamesForTypeIncludingAncestors( + ListableBeanFactory lbf, Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); +``` + +- //获取Class为Advisor的所有bean的名字 + String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + List<String> resultList = new ArrayList<String>(); + resultList.addAll(Arrays.asList(result)); + for (String beanName : parentResult) { + if (!resultList.contains(beanName) && !hbf.containsLocalBean(beanName)) { + resultList.add(beanName); + } + } + result = StringUtils.toStringArray(resultList); + } + } + return result; +} + + + - 1.1.2) BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors(获取标记@Aspect注解的类中的增强) +- 逻辑: + - 1)遍历所有beanName,所有在beanFactory中注册的bean都会被提取出来 + - 2)遍历所有beanName,找出声明@Aspect注解的类,进行进一步的处理 + - 3)对标记为AspectJ注解的类进行增强的提取 + - 4)将提取结果加入缓存 + +``` +public List<Advisor> buildAspectJAdvisors() { + List<String> aspectNames = this.aspectBeanNames; + + if (aspectNames == null) { + synchronized (this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List<Advisor> advisors = new LinkedList<Advisor>(); + aspectNames = new LinkedList<String>(); +``` + +- // 获取所有的beanName + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Object.class, true, false); +- // 遍历所有的beanName找出对应的增强方法 + for (String beanName : beanNames) { +- // 不合法的bean则略过 + if (!isEligibleBean(beanName)) { + continue; + } + // We must be careful not to instantiate beans eagerly as in this case they + // would be cached by the Spring container but would not have been weaved. +- // 获取对应的bean的类型 + Class<?> beanType = this.beanFactory.getType(beanName); + if (beanType == null) { + continue; + } +- // 如果存在@Aspect注解 + if (this.advisorFactory.isAspect(beanType)) { + aspectNames.add(beanName); + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); +- // 解析标记@Aspect中的增强方法 + List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); + } + else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); + this.aspectFactoryCache.put(beanName, factory); advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + } + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } + // 记录在缓存中 +- List<Advisor> advisors = new LinkedList<Advisor>(); + for (String aspectName : aspectNames) { + List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } + else { + MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + return advisors; +} +- 获取 +- this.advisorFactory.getAdvisors(factory) + - 1.1.2.1) getAdvisors(增强器的获取) +- ReflectiveAspectJAdvisorFactory.getAdvisors +- 逻辑: + - 1)对增强器的获取 + - 2)加入同步实例化增强器以保证增强使用前的实例化 + - 3)对DeclareParents注解的获取 + +``` +public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { +``` + +- // 获取标记为@Aspect的类 + Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); +- // 验证 + validate(aspectClass); + + // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator + // so that it will only instantiate once. + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = + new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + + List<Advisor> advisors = new LinkedList<Advisor>(); + for (Method method : getAdvisorMethods(aspectClass)) { +- // 获取普通的advisor + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + // If it's a per target aspect, emit the dummy instantiating aspect. + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { +- // 如果寻找的增强器不为空,而且又配置了增强延迟初始化,那么需要在advisors开头加入同步实例化增强器 + Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + // 获取DeclareParents注解(引介增强IntroductionAdvisor) + // Find introduction fields. + for (Field field : aspectClass.getDeclaredFields()) { + Advisor advisor = getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; +} + + - 1.1.2.1.1) getAdvisor(普通增强器的获取) +- 逻辑: + - 1)切点信息的获取 + - 2)根据切点信息生成增强 + +``` +public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + int declarationOrderInAspect, String aspectName) { + + validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); + //切点信息的获取 + AspectJExpressionPointcut expressionPointcut = getPointcut( + candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); + if (expressionPointcut == null) { + return null; + } + // 根据切点信息生成增强器 + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); +} +``` + +- + + - 1.1.2.1.1.1) getPointcut(方法上切点信息的获取) + +``` + private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) { +``` + +- // 获取方法上的注解 + AspectJAnnotation<?> aspectJAnnotation = +AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + // 使用AspectJExpressionPointcut实例封装获取的信息 +- AspectJExpressionPointcut ajexp = + new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]); +- // 提取得到的注解中的表达式,如@Pointcut(“execution(* *....)”) + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + ajexp.setBeanFactory(this.beanFactory); + return ajexp; +} + + - 1.1.2.1.1.1.1) findAspectJAnnotationOnMethod +- protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) { +- // 寻找特定的注解类 + Class<?>[] classesToLookFor = new Class<?>[] { + Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class}; + for (Class<?> c : classesToLookFor) { + AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) c); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + return null; +} + +- 获取指定方法上的注解并使用AspectJAnnotation封装 + +``` +private static <A extends Annotation> AspectJAnnotation<A> findAnnotation(Method method, Class<A> toLookFor) { + A result = AnnotationUtils.findAnnotation(method, toLookFor); + if (result != null) { + return new AspectJAnnotation<A>(result); + } + else { + return null; + } +} +``` + + +- + + - 1.1.2.1.1.2) InstantiationModelAwarePointcutAdvisorImpl(根据切点信息生成增强器) + +- 所有的增强都由Advisor的实现类InstantiationModelAwarePointcutAdvisorImpl统一封装的。 +- 在实例初始化的过程中还完成了对于增强器的初始化,根据注解中的信息初始化对应的增强器是在instantiateAdvice函数中实现的。 + +``` +public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, + Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + this.declaredPointcut = declaredPointcut; + this.declaringClass = aspectJAdviceMethod.getDeclaringClass(); + this.methodName = aspectJAdviceMethod.getName(); + this.parameterTypes = aspectJAdviceMethod.getParameterTypes(); + this.aspectJAdviceMethod = aspectJAdviceMethod; + this.aspectJAdvisorFactory = aspectJAdvisorFactory; + this.aspectInstanceFactory = aspectInstanceFactory; + this.declarationOrder = declarationOrder; + this.aspectName = aspectName; + + if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + // Static part of the pointcut is a lazy type. + Pointcut preInstantiationPointcut = Pointcuts.union( + aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); + + // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. + // If it's not a dynamic pointcut, it may be optimized out + // by the Spring AOP infrastructure after the first evaluation. + this.pointcut = new PerTargetInstantiationModelPointcut( + this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory); + this.lazy = true; + } + else { + // A singleton aspect. + this.pointcut = this.declaredPointcut; + this.lazy = false; + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + } +} +``` + + + +``` +private Advice instantiateAdvice(AspectJExpressionPointcut pcut) { + return this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pcut, + this.aspectInstanceFactory, this.declarationOrder, this.aspectName); +} +``` + + + +``` +public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + validate(candidateAspectClass); + + AspectJAnnotation<?> aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + // If we get here, we know we have an AspectJ method. + // Check that it's an AspectJ-annotated class + if (!isAspect(candidateAspectClass)) { + throw new AopConfigException("Advice must be declared inside an aspect type: " + + "Offending method '" + candidateAdviceMethod + "' in class [" + + candidateAspectClass.getName() + "]"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found AspectJ method: " + candidateAdviceMethod); + } + + AbstractAspectJAdvice springAdvice; + // 根据不同的注解类型封装不同的增强器 + switch (aspectJAnnotation.getAnnotationType()) { + case AtBefore: + springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfter: + springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfterReturning: + springAdvice = new AspectJAfterReturningAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterReturningAnnotation.returning())) { + springAdvice.setReturningName(afterReturningAnnotation.returning()); + } + break; + case AtAfterThrowing: + springAdvice = new AspectJAfterThrowingAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { + springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); + } + break; + case AtAround: + springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtPointcut: + if (logger.isDebugEnabled()) { + logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); + } + return null; + default: + throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); + } + + // Now to configure the advice... + springAdvice.setAspectName(aspectName); + springAdvice.setDeclarationOrder(declarationOrder); + String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + if (argNames != null) { + springAdvice.setArgumentNamesFromStringArray(argNames); + } + springAdvice.calculateArgumentBindings(); + return springAdvice; +} +``` + + + +- 以AspectJMethodBeforeAdvice 为例,这个类是被MethodBeforeAdviceInterceptor持有的。 +- MethodBeforeAdviceInterceptor会在createProxy中被织入到代理对象中。 + +``` +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { + + private MethodBeforeAdvice advice; + + + /** + * Create a new MethodBeforeAdviceInterceptor for the given advice. + * @param advice the MethodBeforeAdvice to wrap + */ + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis() ); + return mi.proceed(); + } + +} +``` + + +- AspectJMethodBeforeAdvice + +``` +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + + public AspectJMethodBeforeAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public void before(Method method, Object[] args, Object target) throws Throwable { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + + @Override + public boolean isBeforeAdvice() { + return true; + } + + @Override + public boolean isAfterAdvice() { + return false; + } + +} +``` + + +- invokeAdviceMethod +- protected Object invokeAdviceMethod(JoinPointMatch jpMatch, Object returnValue, Throwable ex) throws Throwable { + return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); +} +- +- invokeAdviceMethodWithGivenArgs +- protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterTypes().length == 0) { + actualArgs = null; + } + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + // TODO AopUtils.invokeJoinpointUsingReflection +- // 激活增强方法 method.invoke() + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + + this.aspectJAdviceMethod + "]; pointcut expression [" + + this.pointcut.getPointcutExpression() + "]", ex); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } +} +- + + - 1.1.2.1.2) SyntheticInstantiationAdvisor(同步实例化增强器) +- Advisor instantiationAdvisor = new +- SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); +advisors.add(0, instantiationAdvisor); + + +``` +protected static class SyntheticInstantiationAdvisor extends DefaultPointcutAdvisor { + + public SyntheticInstantiationAdvisor(final MetadataAwareAspectInstanceFactory aif) { + super(aif.getAspectMetadata().getPerClausePointcut(), new MethodBeforeAdvice() { +``` + + +``` +// 目标方法前调用,类似@Before + @Override + public void before(Method method, Object[] args, Object target) { + // Simply instantiate the aspect +``` + +- // 简单初始化aspect + aif.getAspectInstance(); + } + }); + } +} + + - 1.1.2.1.3) getDeclareParentsAdvisor(获取DeclareParents) +- DeclareParents主要用于引介增强的注解形式的实现,而其实现方式和普通增强很类似,只不过只用DeclareParentsAdvisor对功能进行封装。 + +``` +private Advisor getDeclareParentsAdvisor(Field introductionField) { + DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class); + if (declareParents == null) { + // Not an introduction field + return null; + } + + if (DeclareParents.class == declareParents.defaultImpl()) { + throw new IllegalStateException("'defaultImpl' attribute must be set on DeclareParents"); + } + + return new DeclareParentsAdvisor( + introductionField.getType(), declareParents.value(), declareParents.defaultImpl()); +} +``` + + +- + + - 1.2) findAdvisorsThatCanApply(获取匹配的增强并应用) +- protected List<Advisor> findAdvisorsThatCanApply( + List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) { + + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + try { +- // 过滤已经得到的advisor + return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } + finally { + ProxyCreationContext.setCurrentProxiedBeanName(null); + } +} + +- AopUtils.findAdvisorsThatCanApply + +``` +public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List<Advisor> eligibleAdvisors = new LinkedList<Advisor>(); + for (Advisor candidate : candidateAdvisors) { +``` + +- // 首先处理引介增强 + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } +- // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} + - 1.2.1) canApply(真正的匹配) + +``` +public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { +``` + +- // 处理引入增强 + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } +- // 处理PointcutAdvisor,是指有切入点的Advisor + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { +- // 没有切入点的始终匹配 + // It doesn't have a pointcut so we assume it applies. + return true; + } +} + + +``` +public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + // 获取bean目标类和所有接口,放到集合中 + Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + classes.add(targetClass); +``` + +- // 遍历集合,获取每个类/接口的所有方法,并对方法进行逐个匹配 + for (Class<?> clazz : classes) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + if ((introductionAwareMethodMatcher != null && + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) || + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + return false; +} + +- + + - 2) createProxy(创建代理) +- AbstractAutoProxyCreator#createProxy + +- 在获取了所有对应的bean的增强后,便可以进行代理的创建了。 +- 逻辑: + - 1)获取当前类中的属性 + - 2)添加代理接口 + - 3)封装Advisor并加入到ProxyFactory中 + - 4)设置要代理的类 + - 5)子类可以在此函数中进行对ProxyFactory的进一步封装 + - 6)进行获取代理操作 + +- specificInterceptors就是增强们(advisors) +- targetSource 是new SingletonTargetSource(bean) +- protected Object createProxy( + Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) { + + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); +- // 获取当前类中的相关属性 + proxyFactory.copyFrom(this); + + if (!proxyFactory.isProxyTargetClass()) { + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { +- // 添加代理接口 + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + for (Advisor advisor : advisors) { +- // 加入增强 + proxyFactory.addAdvisor(advisor); + } + // 设置要代理的类 + proxyFactory.setTargetSource(targetSource); +- // 定制代理 + customizeProxyFactory(proxyFactory); + // 用来控制proxyFactory被配置后,是否还允许修改增强,缺省值为false + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + return proxyFactory.getProxy(getProxyClassLoader()); +} + + - 2.1) buildAdvisors(封装拦截器为Advisor) +- protected Advisor[] buildAdvisors(String beanName, Object[] specificInterceptors) { + // Handle prototypes correctly... + Advisor[] commonInterceptors = resolveInterceptorNames(); + + List<Object> allInterceptors = new ArrayList<Object>(); + if (specificInterceptors != null) { + allInterceptors.addAll(Arrays.asList(specificInterceptors)); + if (commonInterceptors.length > 0) { + if (this.applyCommonInterceptorsFirst) { + allInterceptors.addAll(0, Arrays.asList(commonInterceptors)); + } + else { + allInterceptors.addAll(Arrays.asList(commonInterceptors)); + } + } + } + if (logger.isDebugEnabled()) { + int nrOfCommonInterceptors = commonInterceptors.length; + int nrOfSpecificInterceptors = (specificInterceptors != null ? specificInterceptors.length : 0); + logger.debug("Creating implicit proxy for bean '" + beanName + "' with " + nrOfCommonInterceptors + + " common interceptors and " + nrOfSpecificInterceptors + " specific interceptors"); + } + + Advisor[] advisors = new Advisor[allInterceptors.size()]; + for (int i = 0; i < allInterceptors.size(); i++) { +- // 拦截器进行封装转化为advisor + advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i)); + } + return advisors; +} + - 2.1.1) DefaultAdvisorAdapterRegistery#wrap + +``` +public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { +``` + +- // 如果要封装的对象本身就是Advisor类型的,那么直接返回 + if (adviceObject instanceof Advisor) { + return (Advisor) adviceObject; + } +- // 因为此封装方法只对Advisor和Advice有效,如果不是则不能封装 + if (!(adviceObject instanceof Advice)) { + throw new UnknownAdviceTypeException(adviceObject); + } + Advice advice = (Advice) adviceObject; + if (advice instanceof MethodInterceptor) { + // So well-known it doesn't even need an adapter. +- // 如果是MethodInterceptor类型则使用DefaultPointcutAdvisor封装 + return new DefaultPointcutAdvisor(advice); + } +- // 如果存在Advisor的Adapter,那么也同样需要进行封装 + for (AdvisorAdapter adapter : this.adapters) { + // Check that it is supported. + if (adapter.supportsAdvice(advice)) { + return new DefaultPointcutAdvisor(advice); + } + } + throw new UnknownAdviceTypeException(advice); +} + +- + + - 2.2) getProxy + +``` +public Object getProxy(ClassLoader classLoader) { + return createAopProxy().getProxy(classLoader); +} +``` + + + - 2.2.1) createAopProxy(创建代理) +- protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + activate(); + } + return getAopProxyFactory().createAopProxy(this); +} + + - 2.2.1.1) DefaultAopProxyFactory#createAopProxy +- 逻辑: +- 如果目标对象实现了接口,默认情况下会使用JDK的动态代理 +- 如果目标对象实现了接口,可以强制使用CGLIB(proxy-target-class=false) +- 如果目标对象没有实现接口,必须采用CGLIB + +``` +public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class<?> targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } +} +``` + + - 2.2.2) getProxy(获取代理) +- 1>JdkDynamicAopProxy#getProxy +- 该AopProxy实现了InvocationHandler接口,重写了invoke方法。 + +``` +public Object getProxy(ClassLoader classLoader) { + if (logger.isDebugEnabled()) { + logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource()); + } + Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); +} +``` + + +- invoke方法: + +``` +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + MethodInvocation invocation; + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Class<?> targetClass = null; + Object target = null; + + try { +``` + +- // 处理equals + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } +- // 处理hashcode + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + // The target does not implement the hashCode() method itself. + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + // There is only getDecoratedClass() declared -> dispatch to proxy config. + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + // Service invocations on ProxyConfig with the proxy config... + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + // 有时候目标对象内部的自我调用将无法实施切面中的增强,则需要通过此属性暴露代理 + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + // May be null. Get as late as possible to minimize the time we "own" the target, + // in case it comes from a pool. + target = targetSource.getTarget(); + if (target != null) { + targetClass = target.getClass(); + } + // 获取当前方法的拦截器链 + // Get the interception chain for this method. + List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + // Check whether we have any advice. If we don't, we can fallback on direct + // reflective invocation of the target, and avoid creating a MethodInvocation. + if (chain.isEmpty()) { + // We can skip creating a MethodInvocation: just invoke the target directly + // Note that the final invoker must be an InvokerInterceptor so we know it does + // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); +- // 如果没有发现任何拦截器,那么直接调用切点方法(method.invoke()) + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { +- // 将拦截器封装在ReflectiveMethodInvocation + // We need to create a method invocation... + invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. +- // proceed中实现了拦截器方法的逐一调用 + retVal = invocation.proceed(); + } + + // Massage return value if necessary. + Class<?> returnType = method.getReturnType(); +- // 返回结果 + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this" and the return type of the method + // is type-compatible. Note that we can't help if the target sets + // a reference to itself in another returned object. + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } +} + - 2.2.2.1) ReflectiveMethodInvocation#proceed(执行拦截器链的方法) + +``` +public Object proceed() throws Throwable { +``` + + - // 执行完所有增强后,执行切点方法(method.invoke()) + // We start with an index of -1 and increment early. + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + // 获取下一个要执行的拦截器 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { +- // 动态匹配 + // Evaluate dynamic method matcher here: static part will already have + // been evaluated and found to match. + InterceptorAndDynamicMethodMatcher dm = + (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { + return dm.interceptor.invoke(this); + } + else { +- // 不匹配则不执行拦截器,递归调用自己,执行下一个拦截器 + // Dynamic matching failed. + // Skip this interceptor and invoke the next in the chain. + return proceed(); + } + } + else { +- // 若为普通拦截器则直接调用拦截器 + // It's an interceptor, so we just invoke it: The pointcut will have + // been evaluated statically before this object was constructed. + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} + + - 2.2.2.1.1) invokeJoinpoint(执行切点方法) +- protected Object invokeJoinpoint() throws Throwable { + return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); +} + + +``` +public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(method); + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + // Invoked method threw a checked exception. + // We must rethrow it. The client won't see the interceptor. + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + + method + "] on target [" + target + "]", ex); + } + catch (IllegalAccessException ex) { + throw new AopInvocationException("Could not access method [" + method + "]", ex); + } +} +``` + + - 2.2.2.1.2) invoke (执行拦截器方法) + +``` +public interface MethodInterceptor extends Interceptor { + Object invoke(MethodInvocation invocation) throws Throwable; +} +``` + + + +- + +- 2>CglibAopProxy#getProxy +- 虽然返回的Proxy是ObjenesisCglibAopProxy,但它继承了CglibAopProxy 的getProxy方法。 +- 实现了Enhancer的创建及接口封装。 + +``` +public Object getProxy(ClassLoader classLoader) { + if (logger.isDebugEnabled()) { + logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource()); + } + + try { + Class<?> rootClass = this.advised.getTargetClass(); + Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); + + Class<?> proxySuperClass = rootClass; + if (ClassUtils.isCglibProxyClass(rootClass)) { + proxySuperClass = rootClass.getSuperclass(); + Class<?>[] additionalInterfaces = rootClass.getInterfaces(); + for (Class<?> additionalInterface : additionalInterfaces) { + this.advised.addInterface(additionalInterface); + } + } + // 验证class + // Validate the class, writing log messages as necessary. + validateClassIfNecessary(proxySuperClass, classLoader); + // 创建及配置Enhancer + // Configure CGLIB Enhancer... + Enhancer enhancer = createEnhancer(); + if (classLoader != null) { + enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader && + ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { + enhancer.setUseCache(false); + } + } + enhancer.setSuperclass(proxySuperClass); + enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader)); + // 设置拦截器 + Callback[] callbacks = getCallbacks(rootClass); + Class<?>[] types = new Class<?>[callbacks.length]; + for (int x = 0; x < types.length; x++) { + types[x] = callbacks[x].getClass(); + } + // fixedInterceptorMap only populated at this point, after getCallbacks call above + enhancer.setCallbackFilter(new ProxyCallbackFilter( + this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); + enhancer.setCallbackTypes(types); + // 生成代理类以及创建代理 + // Generate the proxy class and create a proxy instance. + return createProxyClassAndInstance(enhancer, callbacks); + } + catch (CodeGenerationException ex) { + throw new AopConfigException("Could not generate CGLIB subclass of class [" + + this.advised.getTargetClass() + "]: " + + "Common causes of this problem include using a final class or a non-visible class", + ex); + } + catch (IllegalArgumentException ex) { + throw new AopConfigException("Could not generate CGLIB subclass of class [" + + this.advised.getTargetClass() + "]: " + + "Common causes of this problem include using a final class or a non-visible class", + ex); + } + catch (Throwable ex) { + // TargetSource.getTarget() failed + throw new AopConfigException("Unexpected AOP exception", ex); + } +} +``` + +- + + - 2.2.2.1) getCallbacks + +``` +private Callback[] getCallbacks(Class<?> rootClass) throws Exception { + // Parameters used for optimisation choices... + boolean exposeProxy = this.advised.isExposeProxy(); + boolean isFrozen = this.advised.isFrozen(); + boolean isStatic = this.advised.getTargetSource().isStatic(); + + // Choose an "aop" interceptor (used for AOP calls). +``` + +- // 将拦截器封装在DynamicAdvisedInterceptor中 + Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised); + + // Choose a "straight to target" interceptor. (used for calls that are + // unadvised but can return this). May be required to expose the proxy. + Callback targetInterceptor; + if (exposeProxy) { + targetInterceptor = isStatic ? + new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) : + new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()); + } + else { + targetInterceptor = isStatic ? + new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) : + new DynamicUnadvisedInterceptor(this.advised.getTargetSource()); + } + + // Choose a "direct to target" dispatcher (used for + // unadvised calls to static targets that cannot return this). + Callback targetDispatcher = isStatic ? + new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp(); + + Callback[] mainCallbacks = new Callback[] { +- // 将拦截器链加入到Callback中 + aopInterceptor, // for normal advice + targetInterceptor, // invoke target without considering advice, if optimized + new SerializableNoOp(), // no override for methods mapped to this + targetDispatcher, this.advisedDispatcher, + new EqualsInterceptor(this.advised), + new HashCodeInterceptor(this.advised) + }; + + Callback[] callbacks; + + // If the target is a static one and the advice chain is frozen, + // then we can make some optimisations by sending the AOP calls + // direct to the target using the fixed chain for that method. + if (isStatic && isFrozen) { + Method[] methods = rootClass.getMethods(); + Callback[] fixedCallbacks = new Callback[methods.length]; + this.fixedInterceptorMap = new HashMap<String, Integer>(methods.length); + + // TODO: small memory optimisation here (can skip creation for methods with no advice) + for (int x = 0; x < methods.length; x++) { + List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(methods[x], rootClass); + fixedCallbacks[x] = new FixedChainStaticTargetInterceptor( + chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); + this.fixedInterceptorMap.put(methods[x].toString(), x); + } + + // Now copy both the callbacks from mainCallbacks + // and fixedCallbacks into the callbacks array. + callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; + System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); + System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); + this.fixedInterceptorOffset = mainCallbacks.length; + } + else { + callbacks = mainCallbacks; + } + return callbacks; +} +- + +- CGLIB对于方法的拦截是通过将自定义的拦截器(实现了MethodInterceptor接口)加入Callback中并在调用代理时直接激活拦截器的intercept方法实现的,那么在getCallback中实现了这样一个目的:DynamicAdvisedInterceptor继承自MethodInterceptor,加入到Callback中后,在再次调用代理时会直接调用DynamicAdvisedInterceptor中的intercept方法。 +- CGLIB方式实现的代理,其核心逻辑在DynamicAdvisedInterceptor中的intercept方法中(JDK动态代理的核心逻辑是在invoke方法中)。 + +``` +private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable { + + private final AdvisedSupport advised; + + public DynamicAdvisedInterceptor(AdvisedSupport advised) { + this.advised = advised; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + Class<?> targetClass = null; + Object target = null; + try { + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + // May be null. Get as late as possible to minimize the time we + // "own" the target, in case it comes from a pool... + target = getTarget(); + if (target != null) { + targetClass = target.getClass(); + } +``` + +- // 获取拦截器链 + List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + Object retVal; + // Check whether we only have one InvokerInterceptor: that is, + // no real advice, but just reflective invocation of the target. + if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { + // We can skip creating a MethodInvocation: just invoke the target directly. + // Note that the final invoker must be an InvokerInterceptor, so we know + // it does nothing but a reflective operation on the target, and no hot + // swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); +- // 如果拦截器为空,则直接激活原方法 + retVal = methodProxy.invoke(target, argsToUse); + } + else { + +``` +// 进入拦截器链 + // We need to create a method invocation... + retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); + } + retVal = processReturnType(proxy, target, method, retVal); + return retVal; + } + finally { + if (target != null) { + releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + @Override + public boolean equals(Object other) { + return (this == other || + (other instanceof DynamicAdvisedInterceptor && + this.advised.equals(((DynamicAdvisedInterceptor) other).advised))); + } + + /** + * CGLIB uses this to drive proxy creation. + */ + @Override + public int hashCode() { + return this.advised.hashCode(); + } + + protected Object getTarget() throws Exception { + return this.advised.getTargetSource().getTarget(); + } + + protected void releaseTarget(Object target) throws Exception { + this.advised.getTargetSource().releaseTarget(target); + } +} +``` + + + - 2.2.2.2) createProxyClassAndInstance +- protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { + Class<?> proxyClass = enhancer.createClass(); + Object proxyInstance = null; + + if (objenesis.isWorthTrying()) { + try { + proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()); + } + catch (Throwable ex) { + logger.debug("Unable to instantiate proxy using Objenesis, " + + "falling back to regular proxy construction", ex); + } + } + + if (proxyInstance == null) { + // Regular instantiation via default constructor... + try { + proxyInstance = (this.constructorArgs != null ? + proxyClass.getConstructor(this.constructorArgTypes).newInstance(this.constructorArgs) : + proxyClass.newInstance()); + } + catch (Throwable ex) { + throw new AopConfigException("Unable to instantiate proxy using Objenesis, " + + "and regular proxy instantiation via default constructor fails as well", ex); + } + } + + ((Factory) proxyInstance).setCallbacks(callbacks); + return proxyInstance; +} + +- + +- 实例 + +``` +public class LoggingAspect { + @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") + public void declareJoinPointExpression() { + } + + @Before("declareJoinPointExpression()") + public void beforeMethod(JoinPoint joinPoint) {// 连接点 + Object[] args = joinPoint.getArgs();// 取得方法参数 + log.info("The method [ {} ] begins with Parameters: {}", joinPoint.getSignature(), Arrays.toString(args)); + } + + @AfterReturning(value = "declareJoinPointExpression()", returning = "result") + public void afterMethodReturn(JoinPoint joinPoint, Object result) { + log.info("The method [ {} ] ends with Result: {}", joinPoint.getSignature(), result); + } + + @AfterThrowing(value = "declareJoinPointExpression()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + log.error("Error happened in method: [ {} ]", joinPoint.getSignature()); + log.error("Parameters: {}", Arrays.toString(joinPoint.getArgs())); + log.error("Exception StackTrace: {}", e); + } +} +``` + + + +``` +@Controller +public class HelloController { + + @RequestMapping("/hello") + public ModelAndView hello(ModelAndView modelAndView){ + modelAndView.addObject("user",new RegisterDTO("admin")); + modelAndView.setViewName("hello"); + return modelAndView; + } +} +``` + + +- 来看一下这个HelloController是怎么被AOP代理的。 +- 从AbstractAutoProxyCreator.postProcessAfterInitialization开始看起。 + +- 在findCandidateAdvisors中 +- super.findCandidateAdvisors()返回了三个Advisor + +- 在buildAdvisors中,又添加了LoggingAspect对应的三个Advisor,类型是InstantiationModelAwarePointcutAdvisorImpl。 + +- 它getPointcut返回的是AspectJExpressionPointcut + +- 然后下面会找到所有Controller,得到Controller的所有方法,查看是否匹配。 +- 最后返回的interceptors有5个, + +- 之后调用createProxy,最后调用了DefaultAopProxyFactory.createAopProxy方法,返回了CglibAopProxy。 +- 它再调用getProxy方法,将5个advisor封装为Callback。 + +- 最后生成cglib代理实例。 + +- Spring Transaction(声明式事务) + +- + +- 事务介绍(两类事务) +- 编程式事务:所谓编程式事务指的是通过编码方式实现事务,即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate。 +- 声明式事务:管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。 +- 显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。 +- 声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。 +- 事务传播行为 +- PROPAGATION_REQUIRED +- 如果当前存在一个事务,则加入当前事务;如果不存在任何事务,则创建一个新的事务。总之,要至少保证在一个事务中运行。PROPAGATION_REQUIRED通常作为默认的事务传播行为。 + +- PROPAGATION_SUPPORTS +- 如果当前存在一个事务,则加入当前事务;如果当前不存在事务,则直接执行。 对于一些查询方法来说,PROPAGATION_SUPPORTS通常是比较合适的传播行为选择。 如果当前方法直接执行,那么不需要事务的支持;如果当前方法被其他方法调用,而其他方法启动了一个事务的时候,使用PROPAGATION_SUPPORTS可以保证当前方法能够加入当前事务并洞察当前事务对数据资源所做的更新。 比如说,A.service()会首先更新数据库,然后调用B.service()进行查询,那么,B.service()如果是PROPAGATION_SUPPORTS的传播行为, 就可以读取A.service()之前所做的最新更新结果,而如果使用稍后所提到的PROPAGATION_NOT_SUPPORTED,则B.service()将无法读取最新的更新结果,因为A.service()的事务在这个时候还没有提交(除非隔离级别是read uncommitted)。 +- PROPAGATION_MANDATORY +- PROPAGATION_MANDATORY强制要求当前存在一个事务,如果不存在,则抛出异常。 如果某个方法需要事务支持,但自身又不管理事务提交或者回滚的时候,比较适合使用 +- PROPAGATION_MANDATORY。 +- PROPAGATION_REQUIRES_NEW +- 不管当前是否存在事务,都会创建新的事务。如果当前存在事务的话,会将当前的事务挂起(suspend)。 如果某个业务对象所做的事情不想影响到外层事务的话,PROPAGATION_REQUIRES_NEW应该是合适的选择,比如,假设当前的业务方法需要向数据库中更新某些日志信息, 但即使这些日志信息更新失败,我们也不想因为该业务方法的事务回滚而影响到外层事务的成功提交,因为这种情况下,当前业务方法的事务成功与否对外层事务来说是无关紧要的。 + +- PROPAGATION_NOT_SUPPORTED +- 不支持当前事务,而是在没有事务的情况下执行。如果当前存在事务的话,当前事务原则上将被挂起(suspend),但要依赖于对应的PlatformTransactionManager实现类是否支持事务的挂起(suspend),更多情况请参照TransactionDefinition的javadoc文档。 PROPAGATION_NOT_SUPPORTED与PROPAGATION_SUPPORTS之间的区别,可以参照PROPAGATION_SUPPORTS部分的实例内容。 +- PROPAGATION_NEVER +- 永远不需要当前存在事务,如果存在当前事务,则抛出异常。 + +- PROPAGATION_NESTED +- 如果存在当前事务,则在当前事务的一个嵌套事务中执行,否则与PROPAGATION_REQUIRED的行为类似,即创建新的事务,在新创建的事务中执行。 PROPAGATION_NESTED粗看起来好像与PROPAGATION_REQUIRES_NEW的行为类似,实际上二者是有差别的。 PROPAGATION_REQUIRES_NEW创建的新事务与外层事务属于同一个“档次”,即二者的地位是相同的,当新创建的事务运行的时候,外层事务将被暂时挂起(suspend); 而PROPAGATION_NESTED创建的嵌套事务则不然,它是寄生于当前外层事务的,它的地位比当前外层事务的地位要小一号,当内部嵌套事务运行的时候,外层事务也是处于active状态。是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交,外部事务回滚,它也会被回滚。 +- 解析事务标签 +- <tx:annotation-driven /> +- 同AOP标签,需要一个对应的BeanDefinitionParser。 +- AnnotationDrivenBeanDefinitionParser#parse + +``` +public BeanDefinition parse(Element element, ParserContext parserContext) { + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + } + else { + // mode="proxy" + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + +- AopAutoProxyConfigurer#configureAutoProxyCreator +- 逻辑: +- 注册了一个creator和三个bean。这三个bean支撑起了整个的事务功能。 +- 其中的两个bean(TransactionInterceptor和AnnotationTransactionAttributeSource)被注入到了一个名为BeanFactoryTransactionAttributeSourceAdvisor这个bean中。 + +``` +public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { +``` + +- // 注册InfrastructureAdvisorAutoProxyCreator这个类 + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + + String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; + if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { + Object eleSource = parserContext.extractSource(element); + // Create the TransactionAttributeSource definition. +- //创建TransactionAttributeSource的bean +- RootBeanDefinition sourceDef = new RootBeanDefinition( + "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); +- // 注册bean,并使用Spring中的定义规则生成beanName + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + // 创建TransactionInterceptor的bean + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(element, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); +- // 注册bean,并使用Spring中的定义规则生成beanName + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + // 创建TransactionAttributeSourceAdvisor的bean +- RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); +- // 将sourceName的bean注入advisorDef的transactionAttributeSource + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); +- // 将interceptorName的bean注入advisorDef的adviceBeanName + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); + parserContext.registerComponent(compositeDef); + } +} +- AopNamespaceUtils#registerAutoProxyCreatorIfNecessary + +``` +public static void registerAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); +} +``` + +- AopConfigUtils#registerAutoProxyCreatorIfNecessary + +``` +public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) { + return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); +} +``` + + +- 这里注册了InfrastructureAdvisorAutoProxyCreator(基础设施)这个类。 +- + +- BeanFactoryTransactionAttributeSourceAdvisor(用于对事务方法进行增强) + +- 在AOP的BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans中获取了所有类型为Advisor的bean,包括BeanFactoryTransactionAttributeSourceAdvisor这个类,并随着其他的Advisor一起在后续的步骤中被织入代理。 + +``` +public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + private TransactionAttributeSource transactionAttributeSource; + + private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } + }; + + + /** + * Set the transaction attribute source which is used to find transaction + * attributes. This should usually be identical to the source reference + * set on the transaction interceptor itself. + * @see TransactionInterceptor#setTransactionAttributeSource + */ + public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) { + this.transactionAttributeSource = transactionAttributeSource; + } + + /** + * Set the {@link ClassFilter} to use for this pointcut. + * Default is {@link ClassFilter#TRUE}. + */ + public void setClassFilter(ClassFilter classFilter) { + this.pointcut.setClassFilter(classFilter); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} +``` + +- + +- 与IOC的衔接 +- InfrastructureAdvisorAutoProxyCreator作为一个AbstractAutoProxyCreator,会在getBean时调用其postProcessAfterInstantiation方法,该方法会创建事务代理。 +- InfrastructureAdvisorAutoProxyCreator#postProcessAfterInstantiation + +``` +public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { + Object cacheKey = getCacheKey(beanClass, beanName); + + if (beanName == null || !this.targetSourcedBeans.contains(beanName)) { + if (this.advisedBeans.containsKey(cacheKey)) { + return null; + } + if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return null; + } + } + + // Create proxy here if we have a custom TargetSource. + // Suppresses unnecessary default instantiation of the target bean: + // The TargetSource will handle target instances in a custom fashion. + if (beanName != null) { + TargetSource targetSource = getCustomTargetSource(beanClass, beanName); + if (targetSource != null) { + this.targetSourcedBeans.add(beanName); + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); + Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + } + + return null; +} +``` + + +- getAdvicesAndAdvisorsForBean会寻找所有实现Advisor接口的类。 +- 我们之前注册了BeanFactoryTransactionAttributeSourceAdvisor这个类,这个类实现了Advisor接口。BeanFactoryTransactionAttributeSourceAdvisor作为一个advisor,用于对事务方法进行增强。只要类或方法实现了@Transactional接口,该Advisor一定会被加到拦截器链中,对原方法进行事务增强。 +- 返回的Advisor类型是BeanFactoryTransactionAttributeSourceAdvisor,而其beanName是TransactionInterceptor。 + +- 之后调用了findAdvisorsThatCanApply 方法,又调用canApply方法。 + - 1) canApply(判断bean是否需要添加事务增强) +- canApply(BeanFactoryTransactionAttributeSourceAdvisor,targetClass) +- 关键在于是否从指定的类或类中的方法找到对应的事务属性 + +``` +public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { +``` + +- // 处理引入增强 + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } +- // 处理PointcutAdvisor,是指有切入点的Advisor + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { +- // 没有切入点的始终匹配 + // It doesn't have a pointcut so we assume it applies. + return true; + } +} + +- pca.getPointcut()对于BeanFactoryTransactionAttributeSourceAdvisor而言,是TransactionAttributeSourcePointcut。 + +``` +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + + +- pc.getMethodMatcher()对于TransactionAttributeSourcePointcut而言,就是this。 + +``` +public final MethodMatcher getMethodMatcher() { + return this; +} +``` + + + + +``` +public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + // 获取bean目标类和所有接口,放到集合中 + Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + classes.add(targetClass); +``` + +- // 遍历集合,获取每个类/接口的所有方法,并对方法进行逐个匹配 + for (Class<?> clazz : classes) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + if ((introductionAwareMethodMatcher != null && + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) || +- // 实际的匹配方法 + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + return false; +} + +- methodMatcher.matches(method, targetClass)会使用 +- TransactionAttributeSourcePointcut类的matches方法。 + - 1.1) matches(匹配方法) +- 这里的tas就是TransactionAttributeSource。 +- 如果该bean的该方法中存在事务属性,那么该类/方法需要继续事务增强。 + +``` +public boolean matches(Method method, Class<?> targetClass) { + if (TransactionalProxy.class.isAssignableFrom(targetClass)) { + return false; + } + TransactionAttributeSource tas = getTransactionAttributeSource(); + return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); +} +``` + + - 1.1.1) AnnotationTransactionAttributeSource#getTransactionAttribute(获取事务属性,封装了@Transactional中的配置信息) +- 先尝试从缓存加载,如果对应信息没有被缓存的话,工作又委托给了computeTransactionAttribute方法。 + +``` +public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) { + if (method.getDeclaringClass() == Object.class) { + return null; + } + + // First, see if we have a cached value. + Object cacheKey = getCacheKey(method, targetClass); + Object cached = this.attributeCache.get(cacheKey); + if (cached != null) { + // Value will either be canonical value indicating there is no transaction attribute, + // or an actual transaction attribute. + if (cached == NULL_TRANSACTION_ATTRIBUTE) { + return null; + } + else { + return (TransactionAttribute) cached; + } + } + else { + // We need to work it out. + TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); + // Put it in the cache. + if (txAttr == null) { + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } + else { + String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + if (txAttr instanceof DefaultTransactionAttribute) { + ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); + } + if (logger.isDebugEnabled()) { + logger.debug("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); + } + this.attributeCache.put(cacheKey, txAttr); + } + return txAttr; + } +} +``` + + - 1.1.1.1) computeTransactionAttribute(提取事务注解) +- 逻辑:如果方法中存在事务属性,则使用方法上的属性,否则使用方法所在的类上的属性。如果方法所在类的属性上还是没有搜寻到对应的事务属性,那么再搜寻接口中的方法,再没有的话,最后尝试搜寻接口的类上面的声明。 + +``` +protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // Ignore CGLIB subclasses - introspect the actual user class. + Class<?> userClass = ClassUtils.getUserClass(targetClass); + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. +``` + +- // method 代表接口中的方法,specificMethod代表实现类中的方法 + Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass); + // If we are dealing with method with generic parameters, find the original method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 查看方法中是否存在事务声明 + // First try is the method in the target class. + TransactionAttribute txAttr = findTransactionAttribute(specificMethod); + if (txAttr != null) { + return txAttr; + } + // 查看方法所在类中是否存在事务声明 + // Second try is the transaction attribute on the target class. + txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + // 如果存在接口,则到接口中寻找 + if (specificMethod != method) { + // Fallback is to look at the original method. +- // 查看接口方法 + txAttr = findTransactionAttribute(method); + if (txAttr != null) { + return txAttr; + } + // Last fallback is the class of the original method. +- // 到接口中的类中去寻找 + txAttr = findTransactionAttribute(method.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + } + + return null; +} + +- protected TransactionAttribute findTransactionAttribute(Method method) { + return determineTransactionAttribute(method); +} + +- protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) { + if (ae.getAnnotations().length > 0) { + for (TransactionAnnotationParser annotationParser : this.annotationParsers) { + TransactionAttribute attr = annotationParser.parseTransactionAnnotation(ae); + if (attr != null) { + return attr; + } + } + } + return null; +} + + - 1.1.1.1.1) TransactionAnnotationParser#parseTransactionAnnotation(解析注解) +- 以SpringTransactionAnnotationParser为例: + +``` +public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { +``` + +- //寻找@Transactional注解,有则解析该注解 + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, Transactional.class); + if (attributes != null) { + return parseTransactionAnnotation(attributes); + } + else { + return null; + } +} + +- 解析@Transactional中的各个属性,并封装到TransactionAttribute中返回。 +- protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + Propagation propagation = attributes.getEnum("propagation"); + rbta.setPropagationBehavior(propagation.value()); + Isolation isolation = attributes.getEnum("isolation"); + rbta.setIsolationLevel(isolation.value()); + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + rbta.setReadOnly(attributes.getBoolean("readOnly")); + rbta.setQualifier(attributes.getString("value")); + ArrayList<RollbackRuleAttribute> rollBackRules = new ArrayList<RollbackRuleAttribute>(); + Class<?>[] rbf = attributes.getClassArray("rollbackFor"); + for (Class<?> rbRule : rbf) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + String[] rbfc = attributes.getStringArray("rollbackForClassName"); + for (String rbRule : rbfc) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + Class<?>[] nrbf = attributes.getClassArray("noRollbackFor"); + for (Class<?> rbRule : nrbf) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + String[] nrbfc = attributes.getStringArray("noRollbackForClassName"); + for (String rbRule : nrbfc) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + rbta.getRollbackRules().addAll(rollBackRules); + return rbta; +} +- + +- 创建事务代理 +- 重要类/接口介绍 +- PlatformTransactionManager + +``` +public interface PlatformTransactionManager { + TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; + void commit(TransactionStatus status) throws TransactionException; + void rollback(TransactionStatus status) throws TransactionException; + +} +``` + +- getTransaction():返回一个已经激活的事务或创建一个新的事务(根据给定的TransactionDefinition类型参数定义的事务属性),返回的是TransactionStatus对象代表了当前事务的状态,其中该方法抛出TransactionException(未检查异常)表示事务由于某种原因失败。 +- commit():用于提交TransactionStatus参数代表的事务 +- rollback():用于回滚TransactionStatus参数代表的事务 + +- + +- TransactionSynchronizationManager(持有一系列事务相关的ThreadLocal对象) + +``` +public abstract class TransactionSynchronizationManager { + + private static final ThreadLocal<Map<Object, Object>> resources = + new NamedThreadLocal<Map<Object, Object>>("Transactional resources"); + + private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = + new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations"); + + private static final ThreadLocal<String> currentTransactionName = + new NamedThreadLocal<String>("Current transaction name"); + + private static final ThreadLocal<Boolean> currentTransactionReadOnly = + new NamedThreadLocal<Boolean>("Current transaction read-only status"); + + private static final ThreadLocal<Integer> currentTransactionIsolationLevel = + new NamedThreadLocal<Integer>("Current transaction isolation level"); + + private static final ThreadLocal<Boolean> actualTransactionActive = + new NamedThreadLocal<Boolean>("Actual transaction active"); +``` + +- } + + +- getResource(获取当前线程绑定的连接) + +``` +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} +``` + + + +``` +private static Object doGetResource(Object actualKey) { + Map<Object, Object> map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + + +- bindResouce(将新连接绑定到当前线程) + +``` +public static void bindResource(Object key, Object value) throws IllegalStateException { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Assert.notNull(value, "Value must not be null"); + Map<Object, Object> map = resources.get(); + // set ThreadLocal Map if none found + if (map == null) { + map = new HashMap<Object, Object>(); + resources.set(map); + } + Object oldValue = map.put(actualKey, value); + // Transparently suppress a ResourceHolder that was marked as void... + if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { + oldValue = null; + } + if (oldValue != null) { + throw new IllegalStateException("Already value [" + oldValue + "] for key [" + + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); + } + if (logger.isTraceEnabled()) { + logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + + Thread.currentThread().getName() + "]"); + } +} +``` + +- unbindResource(释放当前线程绑定的连接) + +``` +public static Object unbindResource(Object key) throws IllegalStateException { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doUnbindResource(actualKey); + if (value == null) { + throw new IllegalStateException( + "No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); + } + return value; +} +``` + + +- setActualTransactionActive(设置当前线程是否存在事务) + +``` +public static void setActualTransactionActive(boolean active) { + actualTransactionActive.set(active ? Boolean.TRUE : null); +} +``` + + +- setCurrentTransactionIsolationLevel(设置当前线程对应事务的隔离级别) + +``` +public static void setCurrentTransactionIsolationLevel(Integer isolationLevel) { + currentTransactionIsolationLevel.set(isolationLevel); +} +``` + +- isSynchronizationActive(当前线程对应的事务synchronization不为空) + +``` +public static boolean isSynchronizationActive() { + return (synchronizations.get() != null); +} +``` + +- initSynchronization(初始化当前线程的synchronization) + +``` +public static void initSynchronization() throws IllegalStateException { + if (isSynchronizationActive()) { + throw new IllegalStateException("Cannot activate transaction synchronization - already active"); + } + logger.trace("Initializing transaction synchronization"); + synchronizations.set(new LinkedHashSet<TransactionSynchronization>()); +} +``` + +- clearSynchronization(清空当前线程的synchronization) + +``` +public static void clearSynchronization() throws IllegalStateException { + if (!isSynchronizationActive()) { + throw new IllegalStateException("Cannot deactivate transaction synchronization - not active"); + } + logger.trace("Clearing transaction synchronization"); + synchronizations.remove(); +} +``` + + +- + +- TransactionSynchronization(自定义触发器) +- TransactionStatus(事务状态) + +``` +public interface TransactionStatus extends SavepointManager, Flushable { + + boolean isNewTransaction(); + + boolean hasSavepoint(); + + void setRollbackOnly(); + + boolean isRollbackOnly(); + + @Override + void flush(); + boolean isCompleted(); + +} +``` + +- DefaultTransactionStatus是其实现类 +- isNewTransaction(是否是新事务) + +``` +public boolean isNewTransaction() { + return (hasTransaction() && this.newTransaction); +} +``` + +- hasTransaction(是否有事务) + +``` +public boolean hasTransaction() { + return (this.transaction != null); +} +``` + +- isReadOnly(是否是只读事务) + +``` +public boolean isReadOnly() { + return this.readOnly; +} +``` + + + +- isGlobalRollbackOnly(是否是rollback-only) + +``` +public boolean isGlobalRollbackOnly() { + return ((this.transaction instanceof SmartTransactionObject) && + ((SmartTransactionObject) this.transaction).isRollbackOnly()); +} +``` + + +- + +- 与AOP的衔接 +- ReflectiveMethodInvocation#proceed(执行拦截器链的方法) +- 其中调用了MethodInterceptor.invoke()拦截器方法。 +- 对于标记了@Transactional的方法而言,会被代理,增强事务功能。 +- 这些方法的Advisor增强中包括了TransactionInterceptor +- (BeanFactoryTransactionAttributeSourceAdvisor对应的bean)。 +- TransactionInterceptor支撑着整个事务功能的架构,它继承了MethodInterceptor。 + + +``` +public Object proceed() throws Throwable { +``` + + - // 执行完所有增强后,执行切点方法(method.invoke()) + // We start with an index of -1 and increment early. + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + // 获取下一个要执行的拦截器 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { +- // 动态匹配 + // Evaluate dynamic method matcher here: static part will already have + // been evaluated and found to match. + InterceptorAndDynamicMethodMatcher dm = + (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { + return dm.interceptor.invoke(this); + } + else { +- // 不匹配则不执行拦截器,递归调用自己,执行下一个拦截器 + // Dynamic matching failed. + // Skip this interceptor and invoke the next in the chain. + return proceed(); + } + } + else { +- // 若为普通拦截器则直接调用拦截器 + // It's an interceptor, so we just invoke it: The pointcut will have + // been evaluated statically before this object was constructed. + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} + +- + +- TransactionInterceptor#invoke(事务增强) + +``` +public Object invoke(final MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() { + @Override + public Object proceedWithInvocation() throws Throwable { + return invocation.proceed(); + } + }); +} +``` + + +- TransactionAspectSupport#invokeWithinTransaction +- 逻辑: + - 1)获取事务属性 TransactionAttribute + - 2)加载配置中的TransactionManager + - 3)不同的事务处理方式使用不同的逻辑,就声明式事务而言,会获取方法信息并创建事务信息TransactionInfo(此时已经创建了事务) +- 事务信息(TransactionInfo)与事务属性(TransactionAttribute)并不相同。 +- 前者包含了后者,且包含了其他事务信息,比如PlatformTransactionManager以及TransactionStatus相关信息。 + - 4)try:执行原始方法 + - 5)catch:异常,回滚事务,再次抛出异常,7)及以后的不会执行 + - 6)finally:清除事务信息 + - 7)提交事务 + - 8)返回原始方法的返回值 +- protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) + throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. +- // 获取对应的事务属性 + final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); +- // 获取BeanFactory中的transactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); +- // 获取方法唯一标识(类、方法) + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + // 声明式事务处理 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // Standard transaction demarcation with getTransaction and commit/rollback calls. +- // 创建TransactionInfo(创建事务) + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + Object retVal = null; + try { + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. +- // 执行原始方法 + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // target invocation exception +- // 异常回滚 + completeTransactionAfterThrowing(txInfo, ex); +- // 回滚后又将异常抛了出来 + throw ex; + } + finally { +- // 清除消息 + cleanupTransactionInfo(txInfo); + } +- // 提交事务 + commitTransactionAfterReturning(txInfo); + return retVal; + } + + else { + +``` +// 编程式事务,略过 + // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. + try { + Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, + new TransactionCallback<Object>() { + @Override + public Object doInTransaction(TransactionStatus status) { + TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); + try { + return invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + return new ThrowableHolder(ex); + } + } + finally { + cleanupTransactionInfo(txInfo); + } + } + }); + + // Check result: It might indicate a Throwable to rethrow. + if (result instanceof ThrowableHolder) { + throw ((ThrowableHolder) result).getThrowable(); + } + else { + return result; + } + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + } +} +``` + + - 1)createTransactionIfNecessary(创建事务) +- 逻辑: + - 1)使用DelegatingTransactionAttribute封装传入的TransactionAttribute。 +- TransactionAttribute在这里的实际类型是RuleBasedTransactionAttribute,是由获取事务属性时生成,主要用于数据承载,使用DelegatingTransactionAttribute承载可以提供更多的功能。 + - 2)获取事务 + - 3)构建事务信息 +- protected TransactionInfo createTransactionIfNecessary( + PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + +``` +// 如果没有指定名称,则使用方法唯一标识,并使用DelegatingTransactionAttribute封装TransactionAttribute + if (txAttr != null && txAttr.getName() == null) { + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { +``` + +- // 创建事务,返回TransactionStatus + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } +- // 根据指定的属性与status准备一个TransactionInfo + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} + - 1.1)getTransaction(开启事务,返回TransactionStatus) +- 逻辑: + - 1)获取事务,创建对应的事务实例 + - 2)如果当前线程存在事务,那么根据传播行为进行相应处理 + - 3)事务超时的验证 + - 4)事务传播行为的验证 + - 5)构建DefaultTransactionStatus,创建当前事务的状态 + - 6)完善transaction,包括设置ConnectionHolder、隔离级别、timeout,如果是新连接,则绑定到当前线程 + - 7)将事务信息记录在当前线程中。 + +``` +public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + Object transaction = doGetTransaction(); + + // Cache debug flag to avoid repeated checks. + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + // Use defaults if no transaction definition given. + definition = new DefaultTransactionDefinition(); + } + // 判断当前线程是否存在事务,依据(DataSourceTransactionManager)是当前线程记录的连接connectionHolder不为空,且connectionHolder中的transactionActive属性为true + if (isExistingTransaction(transaction)) { + // Existing transaction found -> check propagation behavior to find out how to behave. +``` + +- // 当前线程已存在事务,根据传播行为进行相应的处理,直接返回 + return handleExistingTransaction(definition, transaction, debugEnabled); + } +- // 当前线程不存在事务 + // 事务超时的验证 + // Check definition settings for new transaction. + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // No existing transaction found -> check propagation behavior to find out how to proceed. +- // 事务传播行为的验证 +- // 当前线程不存在事务,但是传播行为却被声明为PROPAGATION_MANDATORY(支持当前事务,如果当前没有事务,就抛出异常),则抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); + } + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { +- // 当前线程没有事务,且传播行为是以上传播行为,那么空挂起 +- // 考虑到如果有注册的Synchronization的话,需要暂时将这些与将要开启的新事务无关的Synchronization先放一边。 +- // 剩下的其他情况,则返回不包含任何transaction object的TransactionStatus并返回 +- // 这种情况下虽然是空的事务,但有可能需要处理在事务过程中相关的Synchronization。 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } +- + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); +- // 创建当前事务的状态 + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + +- // 完善事务,包括设置ConnectionHolder、隔离级别、timeout +- // 另外如果是新连接,绑定到当前线程 + doBegin(transaction, definition); + +- // 将事务信息记录在当前线程中 + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException ex) { + resume(null, suspendedResources); + throw ex; + } + catch (Error err) { + resume(null, suspendedResources); + throw err; + } + } + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { + logger.warn("Custom isolation level specified but no actual transaction initiated; " + + "isolation level will effectively be ignored: " + definition); + } + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} + + - 1.1.1) doGetTransaction(创建事务实例) +- DataSourceTransactionManager.doGetTransaction +- protected Object doGetTransaction() { + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); +- // 如果当前线程已经记录数据库连接,则使用原有连接 + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource); +- txObject.setConnectionHolder(conHolder, false); + return txObject; +} + - 1.1.1.1) DataSourceTransactionObject.setConnectionHolder + +``` +public void setConnectionHolder(ConnectionHolder connectionHolder, boolean newConnectionHolder) { + super.setConnectionHolder(connectionHolder); + this.newConnectionHolder = newConnectionHolder; +} +``` + + + - 1.1.2) handleExistingTransaction (处理已存在的事务) +- 值得注意的有两点: + - 1)REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动。而如果有一个事务正在运行的话,则在这个方法运行期间被挂起(suspend)。 + - 2)NESTED表示如果当前正在有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚。如果封装事务不存在,行为就像REQUIRES_NEW。 +- Spring主要有两种处理NESTED的方式: +- 首选设置保存点的方式作为异常处理的回滚 +- JTA无法使用保存点,那么处理方式和REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。 + +``` +private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { + + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } + + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus( + definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } + + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } + + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + if (useSavepointForNestedTransaction()) { + // Create savepoint within existing Spring-managed transaction, + // through the SavepointManager API implemented by TransactionStatus. + // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. + DefaultTransactionStatus status = + prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + status.createAndHoldSavepoint(); + return status; + } + else { + // Nested transaction through nested begin and commit/rollback calls. + // Usually only for JTA: Spring synchronization might get activated here + // in case of a pre-existing JTA transaction. + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + // 处理SUPPORTS和REQUIRED + // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. + if (debugEnabled) { + logger.debug("Participating in existing transaction"); + } + if (isValidateExistingTransaction()) { + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) { + Constants isoConstants = DefaultTransactionDefinition.constants; + throw new IllegalTransactionStateException("Participating transaction with definition [" + + definition + "] specifies isolation level which is incompatible with existing transaction: " + + (currentIsolationLevel != null ? + isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) : + "(unknown)")); + } + } + if (!definition.isReadOnly()) { + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + throw new IllegalTransactionStateException("Participating transaction with definition [" + + definition + "] is not marked as read-only but existing transaction is"); + } + } + } + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +} +``` + + + - 1.1.3) suspend(null->doNothing) +- 如果当前线程不存在事务,并且事务传播行为是Required、Required_New、Nested,那么执行suspend(null)。 +- 考虑到如果有注册的synchronization的话,需要暂时将这些与将要开启的新事务无关的synchronization先放一边。 + +- 正常的suspend是记录原有事务的状态,以便后续操作对事务的恢复 +- (TransactionSynchronizationManager.unBindResource) +- protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + suspendedResources = doSuspend(transaction); + } + String name = TransactionSynchronizationManager.getCurrentTransactionName(); + TransactionSynchronizationManager.setCurrentTransactionName(null); + boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); + Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); + boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); + TransactionSynchronizationManager.setActualTransactionActive(false); + return new SuspendedResourcesHolder( + suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); + } + catch (RuntimeException ex) { + // doSuspend failed - original transaction is still active... + doResumeSynchronization(suspendedSynchronizations); + throw ex; + } + catch (Error err) { + // doSuspend failed - original transaction is still active... + doResumeSynchronization(suspendedSynchronizations); + throw err; + } + } + else if (transaction != null) { + // Transaction active but no synchronization active. + Object suspendedResources = doSuspend(transaction); + return new SuspendedResourcesHolder(suspendedResources); + } + else { + // Neither transaction nor synchronization active. + return null; + } +} +- + - 1.1.3.1) doSuspendSynchronization + +``` +private List<TransactionSynchronization> doSuspendSynchronization() { + List<TransactionSynchronization> suspendedSynchronizations = + TransactionSynchronizationManager.getSynchronizations(); + for (TransactionSynchronization synchronization : suspendedSynchronizations) { + synchronization.suspend(); + } +``` + +- // 清空synchronization + TransactionSynchronizationManager.clearSynchronization(); + return suspendedSynchronizations; +} +- + + - 1.1.4) newTransactionStatus (创建当前事务的状态) +- boolean newSynchronization = (getTransactionSynchronization() != +- SYNCHRONIZATION_NEVER); +- DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null); +- 新事务则传入的newTransaction为true,否则为false +- definition.isReadOnly()返回的是@Transactional中的属性,默认为false。 + +- protected DefaultTransactionStatus newTransactionStatus( + TransactionDefinition definition, Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, Object suspendedResources) { + + boolean actualNewSynchronization = newSynchronization && + !TransactionSynchronizationManager.isSynchronizationActive(); + return new DefaultTransactionStatus( + transaction, newTransaction, actualNewSynchronization, + definition.isReadOnly(), debug, suspendedResources); +} + +- 理解newTransaction +- newTransaction标识该切面方法是否新建了事务,后续切面方法执行完毕时,通过该字段判断是否 需要提交事务或者回滚事务。 + +- + + - 1.1.5) doBegin(完善事务实例) +- 逻辑: + - 1)尝试获取连接。如果当前线程中的connectionHolder已经存在,则没有必要再次获取;对于事务同步设置为true的需要重新获取连接 + - 2)设置隔离级别以及只读标识 + - 3)更改默认的提交设置,将提交操作委托给Spring来处理 + - 4)设置标志位,标识当前连接已经被事务激活 + - 5)设置超时时间 + - 6)如果是新连接,则将connectionHolder绑定到当前线程 +- protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + if (txObject.getConnectionHolder() == null || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + Connection newCon = this.dataSource.getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } +- // 设置ConnectionHolder + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); + +- // 设置隔离级别 + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); +- txObject.setPreviousIsolationLevel(previousIsolationLevel); + + // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, + // so we don't want to do it unnecessarily (for example if we've explicitly + // configured the connection pool to set it already). +- // 更改自动提交设置,由Spring控制提交 + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } + + prepareTransactionalConnection(con, definition); +- // 设置判断当前线程是否存在事务的依据,即transactionActive + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { +- //设置timeout属性 + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // Bind the connection holder to the thread. +- // 如果是新的连接,则将当前获取到的连接绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); + } + } + + catch (Throwable ex) { + if (txObject.isNewConnectionHolder()) { + DataSourceUtils.releaseConnection(con, this.dataSource); + txObject.setConnectionHolder(null, false); + } + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } +} + - 1.1.6) prepareSynchronization(记录事务信息至当前线程) +- protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction()); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? + definition.getIsolationLevel() : null); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + TransactionSynchronizationManager.setCurrentTransactionName(definition.getName()); + TransactionSynchronizationManager.initSynchronization(); + } +} + + - 1.2) prepareTransactionInfo(构建事务信息) +- 当已经建立事务连接并完成了事务信息的提取后,需要将所有的事务信息统一记录在TransactionInfo实例中,这个实例包含了目标方法开始前的所有状态信息。一旦事务执行失败,Spring会通过TransactionInfo实例来进行回滚等后续工作。 +- protected TransactionInfo prepareTransactionInfo(PlatformTransactionManager tm, + TransactionAttribute txAttr, String joinpointIdentification, TransactionStatus status) { + + TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification); + if (txAttr != null) { + // We need a transaction for this method... + if (logger.isTraceEnabled()) { + logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]"); + } + // The transaction manager will flag an error if an incompatible tx already exists. +- // 记录事务状态 + txInfo.newTransactionStatus(status); + } + else { + // The TransactionInfo.hasTransaction() method will return false. We created it only + // to preserve the integrity of the ThreadLocal stack maintained in this class. + if (logger.isTraceEnabled()) + logger.trace("Don't need to create transaction for [" + joinpointIdentification + + "]: This method isn't transactional."); + } + + // We always bind the TransactionInfo to the thread, even if we didn't create + // a new transaction here. This guarantees that the TransactionInfo stack + // will be managed correctly even if no transaction was created by this aspect. + txInfo.bindToThread(); + return txInfo; +} + + - 2)completeTransactionAfterThrowing (回滚事务) +- protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) { +- // 当抛出异常时首先判断当前是否存在事务,这是基础依据 + if (txInfo != null && txInfo.hasTransaction()) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + + "] after exception: " + ex); + } +- // 判断是否回滚 默认的依据是 抛出的异常是否是RuntimeException或者是Error的类型 + if (txInfo.transactionAttribute.rollbackOn(ex)) { + try { + // 根据TransactionStatus信息进行回滚处理 + - txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + catch (Error err) { + logger.error("Application exception overridden by rollback error", ex); + throw err; + } + } + else { + - // 如果不满足回滚条件,即使抛出异常也会提交 + // We don't roll back on this exception. + // Will still roll back if TransactionStatus.isRollbackOnly() is true. + try { + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by commit exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException ex2) { + logger.error("Application exception overridden by commit exception", ex); + throw ex2; + } + catch (Error err) { + logger.error("Application exception overridden by commit error", ex); + throw err; + } + } + } +} + - 2.1) TransactionAttribute.rollbackOn(判断是否需要回滚) +- DefaultTransactionAttribute的实现是 + +``` +public boolean rollbackOn(Throwable ex) { + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + + +- RuleBasedTransactionAttribute的实现是 + +``` +public boolean rollbackOn(Throwable ex) { + if (logger.isTraceEnabled()) { + logger.trace("Applying rules to determine whether transaction should rollback on " + ex); + } + + RollbackRuleAttribute winner = null; + int deepest = Integer.MAX_VALUE; + + if (this.rollbackRules != null) { + for (RollbackRuleAttribute rule : this.rollbackRules) { + int depth = rule.getDepth(ex); + if (depth >= 0 && depth < deepest) { + deepest = depth; + winner = rule; + } + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Winning rollback rule is: " + winner); + } + + // User superclass behavior (rollback on unchecked) if no rule matches. + if (winner == null) { + logger.trace("No relevant rollback rule found: applying default rules"); + return super.rollbackOn(ex); + } + + return !(winner instanceof NoRollbackRuleAttribute); +} +``` + + - 2.2) rollback(回滚) + +``` +public final void rollback(TransactionStatus status) throws TransactionException { + if (status.isCompleted()) { + throw new IllegalTransactionStateException( + "Transaction is already completed - do not call commit or rollback more than once per transaction"); + } + + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + processRollback(defStatus); +} +``` + + +- processRollback + - 1)首先是自定义触发器的调用,包括在回滚前、完成回滚后的调用,当然完成回滚包括正常回滚与回滚过程中出现异常,自定义的触发器会根据这些信息做进一步处理,而对于触发器的注册,常见是在回调过程过程中提供TransactionSynchronizationManager类中的静态方法直接注册。 + +``` +public static void registerSynchronization(TransactionSynchronization synchronization) +``` + + - 2)除了触发监听函数外,就是真正的回滚逻辑处理了。有保存点则回滚到保存点,是新事务则回滚整个事务;存在事务又不是新事务,则做回滚标记。 + - 3)回滚后进行信息清除 + +``` +private void processRollback(DefaultTransactionStatus status) { + try { + try { +``` + +- // 激活所有TransactionSynchronization中对应的beforeCompletion方法 + triggerBeforeCompletion(status); + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } +- // 如果有保存点,也就是当前事务为单独的线程,则会退到保存点 + status.rollbackToHeldSavepoint(); + } + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } +- // 如果当前事务为独立的新事务,则直接回滚 + doRollback(status); + } + else if (status.hasTransaction()) { + if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { + if (status.isDebug()) { + logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); + } +- // 如果当前事务不是独立的事务,那么只能标记状态,等到事务链执行完毕后统一回滚 + doSetRollbackOnly(status); + } + else { + if (status.isDebug()) { + logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); + } + } + } + else { + logger.debug("Should roll back transaction but cannot - no transaction available"); + } + } + catch (RuntimeException ex) { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + throw ex; + } + catch (Error err) { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + throw err; + } +- // 激活所有TransactionSynchronization中对应的方法 + triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); + } + finally { +- // 清空记录的资源并将挂起的资源恢复 + cleanupAfterCompletion(status); + } +} + + - 2.2.1) triggerBeforeCompletion(调用触发器) +- protected final void triggerBeforeCompletion(DefaultTransactionStatus status) { + if (status.isNewSynchronization()) { + if (status.isDebug()) { + logger.trace("Triggering beforeCompletion synchronization"); + } + TransactionSynchronizationUtils.triggerBeforeCompletion(); + } +} + +- TransactionSynchronizationUtils.triggerBeforeCompletion() + +``` +public static void triggerBeforeCompletion() { + for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) { + try { + synchronization.beforeCompletion(); + } + catch (Throwable tsex) { + logger.error("TransactionSynchronization.beforeCompletion threw exception", tsex); + } + } +} +``` + + + - 2.2.2) rollbackToHeldSavepoint(回滚至保存点) + +``` +public void rollbackToHeldSavepoint() throws TransactionException { + if (!hasSavepoint()) { + throw new TransactionUsageException( + "Cannot roll back to savepoint - no savepoint associated with current transaction"); + } + getSavepointManager().rollbackToSavepoint(getSavepoint()); + getSavepointManager().releaseSavepoint(getSavepoint()); + setSavepoint(null); +} +``` + + +- JdbcTransactionObjectSupport.rollbackToSavepoint + +``` +public void rollbackToSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().rollback((Savepoint) savepoint); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } +} +``` + + + - 2.2.3) doRollback(回滚整个事务) +- protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not roll back JDBC transaction", ex); + } +} + + - 2.2.4) doSetRollbackOnly(设置回滚标记) +- protected void doSetRollbackOnly(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + txObject.setRollbackOnly(); +} + + +``` +public void setRollbackOnly() { + this.rollbackOnly = true; +} +``` + + + - 2.2.5) triggerAfterCompletion(调用触发器) + +``` +private void triggerAfterCompletion(DefaultTransactionStatus status, int completionStatus) { + if (status.isNewSynchronization()) { + List<TransactionSynchronization> synchronizations = TransactionSynchronizationManager.getSynchronizations(); + TransactionSynchronizationManager.clearSynchronization(); + if (!status.hasTransaction() || status.isNewTransaction()) { + if (status.isDebug()) { + logger.trace("Triggering afterCompletion synchronization"); + } + // No transaction or new transaction for the current scope -> + // invoke the afterCompletion callbacks immediately + invokeAfterCompletion(synchronizations, completionStatus); + } + else if (!synchronizations.isEmpty()) { + // Existing transaction that we participate in, controlled outside + // of the scope of this Spring transaction manager -> try to register + // an afterCompletion callback with the existing (JTA) transaction. + registerAfterCompletionWithExistingTransaction(status.getTransaction(), synchronizations); + } + } +} +``` + + - 2.2.6) cleanupAfterCompletion(回滚后清除信息) +- 逻辑: + - 1)设置状态是对事务信息做完成标识以避免重复调用 + - 2)如果当前事务是新的同步状态,需要将绑定到当前线程的事务信息清除 + - 3)如果是新事务需要做些清除资源的工作 + +``` +private void cleanupAfterCompletion(DefaultTransactionStatus status) { +``` + +- // 设置完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.clear(); + } + if (status.isNewTransaction()) { +- // 清除资源 + doCleanupAfterCompletion(status.getTransaction()); + } + if (status.getSuspendedResources() != null) { + if (status.isDebug()) { + logger.debug("Resuming suspended transaction after completion of inner transaction"); + } +- // 结束之前事务的挂起状态 + resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} + - 2.2.6.1) doCleanupAfterCompletion(新事务则释放资源) +- protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + +- // 将连接从当前线程中解除绑定 + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + } + + // Reset connection. + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + con.setAutoCommit(true); + } +- // 重置数据库连接 + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } +- // 如果当前事务是独立的新创建的事务,则在事务完成时释放数据库连接 + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); +} + - 2.2.6.2) resume(将挂起事务恢复) +- 如果在事务执行前有事务挂起,那么当前事务执行结束后需要将挂起事务恢复 +- protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) + throws TransactionException { + + if (resourcesHolder != null) { + Object suspendedResources = resourcesHolder.suspendedResources; + if (suspendedResources != null) { + doResume(transaction, suspendedResources); + } + List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); + TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); + doResumeSynchronization(suspendedSynchronizations); + } + } +} + +- + + - 3)commitTransactionAfterReturning(提交事务) +- protected void commitTransactionAfterReturning(TransactionInfo txInfo) { + if (txInfo != null && txInfo.hasTransaction()) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); + } + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} + +- commit +- 某个事务是另一个事务的嵌入事务,但是这些事务又不在Spring的管理范围之内,或者无法设置保存点,那么Spring会通过设置回滚标识的方式来禁止提交。首先当某个嵌入事务发生回滚的时候会设置回滚标识,而等到外部事务提交时,一旦判断出当前事务流被设置了回滚标识,则由外部事务来统一进行整体事务的回滚。 + +``` +public final void commit(TransactionStatus status) throws TransactionException { + if (status.isCompleted()) { + throw new IllegalTransactionStateException( + "Transaction is already completed - do not call commit or rollback more than once per transaction"); + } + + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; +``` + +- // 如果在事务链中已经被标记回滚,那么不会尝试提交事务,直接回滚 + if (defStatus.isLocalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Transactional code has requested rollback"); + } + processRollback(defStatus); + return; + } + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + processRollback(defStatus); + // Throw UnexpectedRollbackException only at outermost transaction boundary + // or if explicitly asked to. + if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { + throw new UnexpectedRollbackException( + "Transaction rolled back because it has been marked as rollback-only"); + } + return; + } + // 处理事务提交 + processCommit(defStatus); +} + +- processCommit +- 在提交过程中也不是直接提交的,而是考虑了诸多方面。 +- 符合提交的条件如下: +- 当事务状态中有保存点信息的话便不会提交事务; +- 当事务不是新事务的时候也不会提交事务 + +- 原因是: +- 对于内嵌事务,在Spring中会将其在开始之前设置保存点,一旦内嵌事务出现异常便根据保存点信息进行回滚,但是如果没有出现异常,内嵌事务并不会单独提交,而是根据事务流由最外层事务负责提交,所以如果当前存在保存点信息便不是最外层事务,不做提交操作。 + + +``` +private void processCommit(DefaultTransactionStatus status) throws TransactionException { + try { + boolean beforeCompletionInvoked = false; + try { +``` + +- // 预留 + prepareForCommit(status); +- // 添加的TransactionSynchronization中对应方法的调用 + triggerBeforeCommit(status); +- // 添加的TransactionSynchronization中对应方法的调用 + triggerBeforeCompletion(status); + beforeCompletionInvoked = true; + boolean globalRollbackOnly = false; + if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { + globalRollbackOnly = status.isGlobalRollbackOnly(); + } + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Releasing transaction savepoint"); + } +- // 如果存在保存点,则清除保存点信息 + status.releaseHeldSavepoint(); + } + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction commit"); + } +- // 如果是新事务,则提交 + doCommit(status); + } + // Throw UnexpectedRollbackException if we have a global rollback-only + // marker but still didn't get a corresponding exception from commit. + if (globalRollbackOnly) { + throw new UnexpectedRollbackException( + "Transaction silently rolled back because it has been marked as rollback-only"); + } + } + catch (UnexpectedRollbackException ex) { + // can only be caused by doCommit + triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); + throw ex; + } + catch (TransactionException ex) { + // can only be caused by doCommit + if (isRollbackOnCommitFailure()) { + doRollbackOnCommitException(status, ex); + } + else { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + } + throw ex; + } + catch (RuntimeException ex) { + if (!beforeCompletionInvoked) { +- // 添加的TransactionSynchronization中对应方法的调用 + triggerBeforeCompletion(status); + } +- // 提交过程中出现异常则回滚 + doRollbackOnCommitException(status, ex); + throw ex; + } + catch (Error err) { + if (!beforeCompletionInvoked) { + triggerBeforeCompletion(status); + } + doRollbackOnCommitException(status, err); + throw err; + } + + // Trigger afterCommit callbacks, with an exception thrown there + // propagated to callers but the transaction still considered as committed. + try { +- // 添加的TransactionSynchronization中对应方法的调用 + triggerAfterCommit(status); + } + finally { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); + } + + } + finally { + cleanupAfterCompletion(status); + } +} + +- + +- 实例 + +``` +public interface LoginService { + void login(RegisterDTO dto); +} +``` + + + +``` +@Service +public class LoginServiceImpl implements LoginService{ + @Override + @Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.NESTED) + public void login(RegisterDTO dto) { + System.out.println("login..."); + throw new RuntimeException("exception in loginservice"); + } +} +``` + + + +``` +public interface UserService { + void addUser(RegisterDTO dto); +} +``` + + + +``` +@Service +public class UserServiceImpl implements UserService{ + + @Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.NESTED) + @Override + public void addUser(RegisterDTO dto) { + System.out.println("addUser..."); + } +} +``` + + + +``` +public interface RegisterService { + void register(RegisterDTO dto); +} +``` + + + +``` +@Service +public class RegisterServiceImpl implements RegisterService{ + @Autowired + private LoginService loginService; + @Autowired + private UserService userService; + + @Transactional(rollbackFor = RuntimeException.class,propagation = Propagation.NESTED) + @Override + public void register(RegisterDTO dto) { + System.out.println("registering..."); + loginService.login(dto); + System.out.println("invoke other methods..."); + userService.addUser(dto); + } +} +``` + + + +``` +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath*:spring/spring-*.xml") +@WebAppConfiguration +public class TransactionTest { + @Autowired + private RegisterService registerService; + + @Test + public void test(){ + registerService.register(new RegisterDTO()); + } +} +``` + + +- REQUIRED(内层事务newTransaction为false,内层事务回滚时仅设置回滚标记,外层事务进行外层回滚) +- 如果几个不同的service都是共享同一个事务(也就是service对象嵌套传播机制为Propagation.REQUIRED),那么它们会一起提交,一起回滚。 +- 同一个事务,如果一个service已经提交了,在另外service中rollback自然对第一个service提交的代码回滚不了的。所以spring处理嵌套事务,就是在TransactionInterceptor方法中,根据一系列开关(事务传播行为),来处理事务是同一个还是重新获取,如果是同一个事务,不同service的commit与rollback的时机。 + +- 这里有一个外层切面register,使用了事务,里面调用了两个service,它们也是要求使用事务的。 +- 因为传播行为是REQUIRED,所以共用同一个事务。 +- 当调用register时,在getTransaction时会将TransactionStatus中的newTransaction设置为true,并且将连接绑定到当前线程,设置当前线程存在事务。 + +- 当后续调用login和addUser时,它们的TransactionStatus中的newTransaction设置为false。 + +- 如果register方法中抛出运行时异常,那么直接rollback整个事务,因为它是一个新事务。 +- 如果login方法中抛出运行时异常,只能将rollbackOnly设置为true。因为login中没有catch该异常(回滚后又抛出该异常),所以异常被register捕获(跳过了addUser),所以register又要执行rollback方法,整个事务进行回滚。 +- REQUIRES_NEW(内外层事务平级,内层事务newTransaction为true,suspend外层事务,抛出异常后内层事务进行内层回滚,resume外层事务,外层事务捕获到内层抛出的异常后进行外层回滚) +- register创建一个事务,newTransaction为true。 + +- 调用login方法时检测到已存在事务,则将已存在事务suspend,并且创建的新事务,newTransaction为true。 + +- 当login方法抛出异常时,因为newTransaction为true,则回滚该事务。异常被抛出到外层register,register捕获该异常,并回滚。 +- NESTED(内外层事务嵌套,内层事务newTransaction为false,并创建还原点,抛出异常后rollback至还原点,外层事务捕获到内层抛出的异常后进行外层回滚) +- 如果PlatformTransactionManager支持还原点,便如上执行;如果不支持,那么行为与REQUIRES_NEW相同。 +- 外层事务register的newTransaction为true,进入内层事务login。 + +- 内层事务login的newTransaction为false,并在获取Transaction时创建了一个还原点。(JTA不支持还原点,此时行为与REQUIRES_NEW相同)。抛出异常后在rollback时直接rollback至之前创建的还原点,并删除了该还原点。 + + +- 外层事务捕获到该异常,进入rollback,因为是新事务,执行外层事务的回滚。 + +- Spring MVC +- 配置文件示例 +- web.xml +- <context-param> + <param-name>contextConfigLocation</param-name> + <param-value>classpath*:spring/spring-*.xml</param-value> +</context-param> + +<listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> +</listener> + +<servlet> + <servlet-name>springDispatcherServlet</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> + <init-param> + <param-name>contextConfigLocation</param-name> + <param-value>classpath:spring/spring-web.xml</param-value> + </init-param> +</servlet> + +<servlet-mapping> + <servlet-name>springDispatcherServlet</servlet-name> + <url-pattern>/</url-pattern> +</servlet-mapping> + +- 在这里配置了contextConfigLocation,指定了配置文件的位置; +- 并配置了DispatcherServlet,Spring使用该类拦截Web请求并进行转发; +- 还配置了ContextLoaderListener,用于初始化Spring。 +- 在Spring配置文件中,需要配置viewResolver。 + +- + +- 运行流程 + +- DispatcherServlet: SpringMVC总的拦截器 +- HandlerMapping:请求和处理器之间的映射,用于获取HandlerExecutionChain +- HandlerExecutionChain:持有一组Interceptor和实际请求处理器HandlerAdapter,负责执行Interceptor的各个方法和处理方法。 +- HandlerAdapter:实际的请求处理器,处理后返回ModelAndView +- HandlerExceptionResolver:异常处理器,当拦截器的postHandle方法调用后检查异常。 +- ViewResolver:视图解析器,解析视图名,得到View,由逻辑视图变为物理视图。 +- View :有render方法,渲染视图 +- 渲染完毕后调用转发 +- + +- 初始化ApplicationContext +- ContextLoaderListener(入口) +- ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。 +- 因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法,使用ServletContextListener接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。 +- 在ServletContextListener中的核心逻辑是初始化WebApplicationContext实例并存放在ServletContext中。 + +``` +public void contextInitialized(ServletContextEvent event) { + initWebApplicationContext(event.getServletContext()); +} +``` + +- ContextLoader#initWebApplicationContext + +``` +public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { + if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { + throw new IllegalStateException( + "Cannot initialize context because there is already a root application context present - " + + "check whether you have multiple ContextLoader* definitions in your web.xml!"); + } + + Log logger = LogFactory.getLog(ContextLoader.class); + servletContext.log("Initializing Spring root WebApplicationContext"); + if (logger.isInfoEnabled()) { + logger.info("Root WebApplicationContext: initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + // Store context in local instance variable, to guarantee that + // it is available on ServletContext shutdown. + if (this.context == null) { +``` + +- // 创建WebApplicationContext + this.context = createWebApplicationContext(servletContext); + } + if (this.context instanceof ConfigurableWebApplicationContext) { + ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; + if (!cwac.isActive()) { + // The context has not yet been refreshed -> provide services such as + // setting the parent context, setting the application context id, etc + if (cwac.getParent() == null) { + // The context instance was injected without an explicit parent -> + // determine parent for root web application context, if any. + ApplicationContext parent = loadParentContext(servletContext); + cwac.setParent(parent); + } + configureAndRefreshWebApplicationContext(cwac, servletContext); + } + } + // 记录在ServletContext中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + if (ccl == ContextLoader.class.getClassLoader()) { + currentContext = this.context; + } + else if (ccl != null) { + currentContextPerThread.put(ccl, this.context); + } + + if (logger.isDebugEnabled()) { + logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); + } + if (logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); + } + + return this.context; + } + catch (RuntimeException ex) { + logger.error("Context initialization failed", ex); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); + throw ex; + } + catch (Error err) { + logger.error("Context initialization failed", err); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); + throw err; + } +} +- ContextLoader#createWebApplicationContext(创建WebApplicationContext实例) +- protected WebApplicationContext createWebApplicationContext(ServletContext sc) { + Class<?> contextClass = determineContextClass(sc); + if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { + throw new ApplicationContextException("Custom context class [" + contextClass.getName() + + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); + } + return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); +} + +- determineContextClass +- protected Class<?> determineContextClass(ServletContext servletContext) { + String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); +- // 如果在web.xml中配置了contextClass,则直接加载这个类 + if (contextClassName != null) { + try { + return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new ApplicationContextException( + "Failed to load custom context class [" + contextClassName + "]", ex); + } + } + else { +- // 否则使用默认值 + contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); + try { + return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new ApplicationContextException( + "Failed to load default context class [" + contextClassName + "]", ex); + } + } +} + +- 看defaultStrategies是怎么初始化的 +- static { + // Load default strategy implementations from properties file. + // This is currently strictly internal and not meant to be customized + // by application developers. + try { + ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class); + defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage()); + } +} + + +``` +private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties"; +``` + +- 在ContextLoader目录下有一个配置文件ContextLoader.properties +- 内容为: +- org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext + +- 在初始化过程中,程序首先会读取ContextLoader类的同目录下的配置文件ContextLoader.properties,并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。 + +- + +- 初始化DispatcherServlet +- 第一次访问网站时,会初始化访问到的servlet。 +- 初始化阶段会调用servlet的init方法,在DispatcherServlet中是由其父类HttpServletBean实现的。 +- 逻辑: + - 1)封装及验证初始化参数 解析init-param并封装到PropertyValues中 + - 2)将DispatcherServlet转化为BeanWrapper实例 + - 3)注册相对于Resource的属性编辑器 + - 4)PropertyValues属性注入 + - 5)servletBean的初始化(initServletBean) + + +``` +public final void init() throws ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Initializing servlet '" + getServletName() + "'"); + } + + // Set bean properties from init parameters. + try { +``` + +- // 解析init-param并封装到PropertyValues中 + PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); +- // 把DispatcherServlet转化为一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注入 + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); + ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); +- // 设置自定义属性编辑器,如果遇到Resource类型的属性将会使用ResourceEditor进行解析 + bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); +- // 空实现,留给子类覆盖 + initBeanWrapper(bw); +- // 属性注入 + bw.setPropertyValues(pvs, true); + } + catch (BeansException ex) { + if (logger.isErrorEnabled()) { + logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); + } + throw ex; + } + + // Let subclasses do whatever initialization they like. +- // 留给子类扩展 + initServletBean(); + + if (logger.isDebugEnabled()) { + logger.debug("Servlet '" + getServletName() + "' configured successfully"); + } +} + - 1)ServletConfigPropertyValues(封装init-param) + +``` +private static class ServletConfigPropertyValues extends MutablePropertyValues { + + /** + * Create new ServletConfigPropertyValues. + * @param config ServletConfig we'll use to take PropertyValues from + * @param requiredProperties set of property names we need, where + * we can't accept default values + * @throws ServletException if any required properties are missing + */ + public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) + throws ServletException { + + Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty() ? + new HashSet<String>(requiredProperties) : null); + + Enumeration<String> paramNames = config.getInitParameterNames(); + while (paramNames.hasMoreElements()) { + String property = paramNames.nextElement(); + Object value = config.getInitParameter(property); + addPropertyValue(new PropertyValue(property, value)); + if (missingProps != null) { + missingProps.remove(property); + } + } + + // Fail if we are still missing properties. + if (!CollectionUtils.isEmpty(missingProps)) { + throw new ServletException( + "Initialization from ServletConfig for servlet '" + config.getServletName() + + "' failed; the following required properties were missing: " + + StringUtils.collectionToDelimitedString(missingProps, ", ")); + } + } +} +``` + + + - 2)FrameworkServlet#initServletBean(对WebApplicationContext实例补充初始化) +- protected final void initServletBean() throws ServletException { + getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); + if (this.logger.isInfoEnabled()) { + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + this.webApplicationContext = initWebApplicationContext(); +- // 留给子类覆盖 + initFrameworkServlet(); + } + catch (ServletException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + catch (RuntimeException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + + if (this.logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + + elapsedTime + " ms"); + } +} + - 2.1)FrameworkServlet#initWebApplicationContext +- 创建或刷新WebApplicationContext实例,并对servlet功能所使用的变量进行初始化 + +- 逻辑: + - 1)寻找或创建对应的WebApplicationContext实例 +- 通过构造函数的注入进行初始化 +- 通过contextAttribute进行初始化 +- 通过在web.xml中配置的servlet参数contextAttribute来查找ServletContext中对应的属性,默认为WebApplicationContext.class.getName() + “.ROOT”,也就是在ContextLoaderListener加载时会创建WebApplicationContext实例,并将实例以 +- WebApplicationContext.class.getName() + “.ROOT”为key放入ServletContext中。 +- 重新创建WebApplicationContext实例 + - 2)对已经创建的WebApplicationContext实例进行配置和刷新 + - 3)刷新Spring在Web功能实现中必须使用的全局变量 +- protected WebApplicationContext initWebApplicationContext() { + WebApplicationContext rootContext = + WebApplicationContextUtils.getWebApplicationContext(getServletContext()); + WebApplicationContext wac = null; + + if (this.webApplicationContext != null) { +- // applicationContext实例在构造函数中被注入 + // A context instance was injected at construction time -> use it + wac = this.webApplicationContext; + if (wac instanceof ConfigurableWebApplicationContext) { + ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; + if (!cwac.isActive()) { + // The context has not yet been refreshed -> provide services such as + // setting the parent context, setting the application context id, etc + if (cwac.getParent() == null) { + // The context instance was injected without an explicit parent -> set + // the root application context (if any; may be null) as the parent + cwac.setParent(rootContext); + } +- // 刷新上下文环境 + configureAndRefreshWebApplicationContext(cwac); + } + } + } + if (wac == null) { + // No context instance was injected at construction time -> see if one + // has been registered in the servlet context. If one exists, it is assumed + // that the parent context (if any) has already been set and that the + // user has performed any initialization such as setting the context id +- // 根据contextAttribute属性加载WebApplicationContext + wac = findWebApplicationContext(); + } + if (wac == null) { + // No context instance is defined for this servlet -> create a local one +- // 重新创建WebApplicationContext + wac = createWebApplicationContext(rootContext); + } + + if (!this.refreshEventReceived) { + // Either the context is not a ConfigurableApplicationContext with refresh + // support or the context injected at construction time had already been + // refreshed -> trigger initial onRefresh manually here. +- // 刷新Spring在web功能实现中必须使用的全局变量 + onRefresh(wac); + } + + if (this.publishContext) { + // Publish the context as a servlet context attribute. + String attrName = getServletContextAttributeName(); + getServletContext().setAttribute(attrName, wac); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() + + "' as ServletContext attribute with name [" + attrName + "]"); + } + } + + return wac; +} + - 2.1.1) (与IOC衔接)configureAndRefreshWebApplicationContext +- protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { + if (ObjectUtils.identityToString(wac).equals(wac.getId())) { + // The application context id is still set to its original default value + // -> assign a more useful id based on available information + if (this.contextId != null) { + wac.setId(this.contextId); + } + else { + // Generate default id... + wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + + ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName()); + } + } + + wac.setServletContext(getServletContext()); + wac.setServletConfig(getServletConfig()); + wac.setNamespace(getNamespace()); + wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); + + // The wac environment's #initPropertySources will be called in any case when the context + // is refreshed; do it eagerly here to ensure servlet property sources are in place for + // use in any post-processing or initialization that occurs below prior to #refresh + ConfigurableEnvironment env = wac.getEnvironment(); + if (env instanceof ConfigurableWebEnvironment) { + ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); + } + + postProcessWebApplicationContext(wac); + applyInitializers(wac); +- // 加载配置文件以及整合parent到wac(就是ApplicationContext中的refresh方法) + wac.refresh(); +} + + - 2.1.2) findWebApplicationContext +- protected WebApplicationContext findWebApplicationContext() { + String attrName = getContextAttribute(); + if (attrName == null) { + return null; + } + WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName); + if (wac == null) { + throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); + } + return wac; +} + + - 2.1.3) FrameworkServlet#createWebApplicationContext +- protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) { +- // 获取servlet的初始化参数contextClass,如果没有配置默认为XMLWebApplicationContext.class + Class<?> contextClass = getContextClass(); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Servlet with name '" + getServletName() + + "' will try to create custom WebApplicationContext context of class '" + + contextClass.getName() + "'" + ", using parent context [" + parent + "]"); + } + if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { + throw new ApplicationContextException( + "Fatal initialization error in servlet with name '" + getServletName() + + "': custom WebApplicationContext class [" + contextClass.getName() + + "] is not of type ConfigurableWebApplicationContext"); + } +- // 通过反射实例化contextClass + ConfigurableWebApplicationContext wac = + (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); + + wac.setEnvironment(getEnvironment()); +- // parent为在ContextLoaderListener中创建的实例 + wac.setParent(parent); + - // 获取contextConfigLocation属性,配置在servlet初始化参数中 + wac.setConfigLocation(getContextConfigLocation()); + // 初始化Spring环境包括加载配置文件等(即为2.1.1) + configureAndRefreshWebApplicationContext(wac); + + return wac; +} + + - 2.1.4) DispatcherServlet#onRefresh +- protected void onRefresh(ApplicationContext context) { + initStrategies(context); +} + +- HandlerMapping:请求和处理器之间的映射,用于获取HandlerExecutionChain +- HandlerAdapter:实际的请求处理器,处理后返回ModelAndView +- HandlerExceptionResolver:异常处理器,当拦截器的postHandle方法调用后检查异常。 +- ViewResolver:视图解析器,解析视图名,得到View,由逻辑视图变为物理视图。 + +- protected void initStrategies(ApplicationContext context) { +- // 初始化文件上传模块 + initMultipartResolver(context); +- // 初始化国际化模块 + initLocaleResolver(context); +- // 初始化主题模块 + initThemeResolver(context); +- // 初始化HandlerMappings + initHandlerMappings(context); +- // 初始化HandlerAdapters + initHandlerAdapters(context); +- // 初始化HandlerExceptionResolvers + initHandlerExceptionResolvers(context); + initRequestToViewNameTranslator(context); +- // 初始化ViewResolvers + initViewResolvers(context); + initFlashMapManager(context); +} + + - 2.1.4.1) initHandlerMappings +- 当客户端发出Request时DispatcherServlet会将Request提交给HandlerMapping,然后HandlerMapping根据WebApplicationContext的配置,回传给DispatcherServlet相应的Controller。 +- 在基于SpringMVC的web应用程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级在前的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,则使用当前的Handler进行Web请求的处理,而不再继续询问其他的HandlerMapping。否则,将继续按照各个HandlerMapping的优先级询问,直到获取一个可用的Handler为止。 + +- 默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的bean。如果只期望SpringMVC加载指定的handlerMapping时,可以修改web.xml中的DispatcherServlet的初始参数,将detectAllHandlerMappings的值设置为false。 +- 此时,SpingMVC将查找名为handlerMapping的bean,并作为当前系统中唯一的handlerMapping。如果没有定义handlerMapping的话,则SpringMVC将按照DispatcherServlet所在目录下的DispatcherServlet.properties中所定义的内容来加载默认的handlerMapping。 + +``` +private void initHandlerMappings(ApplicationContext context) { + this.handlerMappings = null; + + if (this.detectAllHandlerMappings) { + // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. + Map<String, HandlerMapping> matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values()); + // We keep HandlerMappings in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerMappings); + } + } + else { + try { + HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); + this.handlerMappings = Collections.singletonList(hm); + } + catch (NoSuchBeanDefinitionException ex) { +``` + +- // Ignore, we'll add a default HandlerMapping later. + } + } + + // Ensure we have at least one HandlerMapping, by registering + // a default HandlerMapping if no other mappings are found. + if (this.handlerMappings == null) { + this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); + } + } +} + +- 默认的handlerMappings有 +- org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping, org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping + +- getBean时会调用afterPropertiesSet + +``` +public void afterPropertiesSet() { + initHandlerMethods(); +} +``` + + +- + + - 2.1.4.1.1) AbstractHandlerMethodMapping#initHandlerMethods(怎么把Controller里的各个方法封装为HandlerMethod的) +- protected void initHandlerMethods() { + if (logger.isDebugEnabled()) { + logger.debug("Looking for request mappings in application context: " + getApplicationContext()); + } + String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : + getApplicationContext().getBeanNamesForType(Object.class)); + + for (String beanName : beanNames) { + if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { + Class<?> beanType = null; + try { + beanType = getApplicationContext().getType(beanName); + } + catch (Throwable ex) { + // An unresolvable bean type, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); + } + } + if (beanType != null && isHandler(beanType)) { + detectHandlerMethods(beanName); + } + } + } + handlerMethodsInitialized(getHandlerMethods()); +} +- 重点!凡是有Controller或者RequestMapping注解的Class类都被视为HandlerMethod,之后会遍历该类的Method检查是否符合要求。 +- protected boolean isHandler(Class<?> beanType) { + return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || + AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); +} + - 2.1.4.1.1.1) detectHandlerMethods + +``` +protected void detectHandlerMethods(final Object handler) { + Class<?> handlerType = (handler instanceof String ? + getApplicationContext().getType((String) handler) : handler.getClass()); + final Class<?> userType = ClassUtils.getUserClass(handlerType); + + Map<Method, T> methods = MethodIntrospector.selectMethods(userType, + new MethodIntrospector.MetadataLookup<T>() { + @Override + public T inspect(Method method) { + try { + return getMappingForMethod(method, userType); + } + catch (Throwable ex) { + throw new IllegalStateException("Invalid mapping on handler class [" + + userType.getName() + "]: " + method, ex); + } + } + }); + + if (logger.isDebugEnabled()) { + logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); + } + for (Map.Entry<Method, T> entry : methods.entrySet()) { + Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); + T mapping = entry.getValue(); + registerHandlerMethod(handler, invocableMethod, mapping); + } +} +``` + + - 2.1.4.1.1.1.1) MethodIntrospector#selectMethods + +``` +public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) { + final Map<Method, T> methodMap = new LinkedHashMap<Method, T>(); + Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>(); + Class<?> specificHandlerType = null; + + if (!Proxy.isProxyClass(targetType)) { + handlerTypes.add(targetType); + specificHandlerType = targetType; + } + handlerTypes.addAll(Arrays.asList(targetType.getInterfaces())); + + for (Class<?> currentHandlerType : handlerTypes) { + final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType); + + ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) { + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + T result = metadataLookup.inspect(specificMethod); + if (result != null) { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) { + methodMap.put(specificMethod, result); + } + } + } + }, ReflectionUtils.USER_DECLARED_METHODS); + } + + return methodMap; +} +``` + + + + - 2.1.4.1.1.1.1.1) doWithMethods + +``` +public static void doWithMethods(Class<?> clazz, MethodCallback mc, MethodFilter mf) { + // Keep backing up the inheritance hierarchy. + Method[] methods = getDeclaredMethods(clazz); + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class<?> superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } +} +``` + + - 2.1.4.1.1.1.1.1.1) metadataLookup#inspect(根据Method找到RequestMapping注解信息) + +``` +public T inspect(Method method) { + try { + return getMappingForMethod(method, userType); + } + catch (Throwable ex) { + throw new IllegalStateException("Invalid mapping on handler class [" + + userType.getName() + "]: " + method, ex); + } +} +``` + + +- protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { + RequestMappingInfo info = createRequestMappingInfo(method); + if (info != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); + if (typeInfo != null) { + info = typeInfo.combine(info); + } + } + return info; +} + + +``` +private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); + RequestCondition<?> condition = (element instanceof Class ? + getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); + return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); +} +``` + + +- protected RequestMappingInfo createRequestMappingInfo( + RequestMapping requestMapping, RequestCondition<?> customCondition) { + + return RequestMappingInfo + .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) + .methods(requestMapping.method()) + .params(requestMapping.params()) + .headers(requestMapping.headers()) + .consumes(requestMapping.consumes()) + .produces(requestMapping.produces()) + .mappingName(requestMapping.name()) + .customCondition(customCondition) + .options(this.config) + .build(); +} + + + - 2.1.4.1.1.1.2) registerHandlerMethod +- protected void registerHandlerMethod(Object handler, Method method, T mapping) { + this.mappingRegistry.register(mapping, handler, method); +} + + - 2.1.4.1.1.1.2.1) MappingRegistry#register + +``` +public void register(T mapping, Object handler, Method method) { + this.readWriteLock.writeLock().lock(); + try { + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + assertUniqueMethodMapping(handlerMethod, mapping); + + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + this.mappingLookup.put(mapping, handlerMethod); + + List<String> directUrls = getDirectUrls(mapping); + for (String url : directUrls) { + this.urlLookup.add(url, mapping); + } + + String name = null; + if (getNamingStrategy() != null) { + name = getNamingStrategy().getName(handlerMethod, mapping); + addMappingName(name, handlerMethod); + } + + CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); + if (corsConfig != null) { + this.corsLookup.put(handlerMethod, corsConfig); + } + + this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name)); + } + finally { + this.readWriteLock.writeLock().unlock(); + } +} +``` + + - 2.1.4.1.1.1.2.1.1) AbstractHandlerMethodMapping#createHandlerMethod +- protected HandlerMethod createHandlerMethod(Object handler, Method method) { + HandlerMethod handlerMethod; + if (handler instanceof String) { + String beanName = (String) handler; + handlerMethod = new HandlerMethod(beanName, + getApplicationContext().getAutowireCapableBeanFactory(), method); + } + else { + handlerMethod = new HandlerMethod(handler, method); + } + return handlerMethod; +} + + +``` +public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) { + Assert.hasText(beanName, "Bean name is required"); + Assert.notNull(beanFactory, "BeanFactory is required"); + Assert.notNull(method, "Method is required"); + this.bean = beanName; + this.beanFactory = beanFactory; + this.beanType = ClassUtils.getUserClass(beanFactory.getType(beanName)); + this.method = method; + this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); +``` + +- // 初始化方法参数 + this.parameters = initMethodParameters(); + this.resolvedFromHandlerMethod = null; +} + + +``` +private MethodParameter[] initMethodParameters() { + int count = this.bridgedMethod.getParameterTypes().length; + MethodParameter[] result = new MethodParameter[count]; + for (int i = 0; i < count; i++) { + HandlerMethodParameter parameter = new HandlerMethodParameter(i); + GenericTypeResolver.resolveParameterType(parameter, this.beanType); + result[i] = parameter; + } + return result; +} +``` + + + +- + + - 2.1.4.2) initHandlerAdapters +- detectAllHandlerAdapters这个变量和detectAllHandlerMappings作用差不多。 + + +``` +private void initHandlerAdapters(ApplicationContext context) { + this.handlerAdapters = null; + + if (this.detectAllHandlerAdapters) { + // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. + Map<String, HandlerAdapter> matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values()); + // We keep HandlerAdapters in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerAdapters); + } + } + else { + try { + HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); + this.handlerAdapters = Collections.singletonList(ha); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerAdapter later. + } + } + + // Ensure we have at least some HandlerAdapters, by registering + // default HandlerAdapters if no other adapters are found. + if (this.handlerAdapters == null) { + this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default"); + } + } +} +``` + + +- getDefaultStrategies(context, HandlerAdapter.class) +- defaultStrategies是从配置文件DispatcherServlet.properties中加载得到的。 +- 默认有以下HandlerAdapter: +- org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter, org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter, org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + +- protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { + String key = strategyInterface.getName(); + String value = defaultStrategies.getProperty(key); + if (value != null) { + String[] classNames = StringUtils.commaDelimitedListToStringArray(value); + List<T> strategies = new ArrayList<T>(classNames.length); + for (String className : classNames) { + try { + Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); + Object strategy = createDefaultStrategy(context, clazz); + strategies.add((T) strategy); + } + catch (ClassNotFoundException ex) { + throw new BeanInitializationException( + "Could not find DispatcherServlet's default strategy class [" + className + + "] for interface [" + key + "]", ex); + } + catch (LinkageError err) { + throw new BeanInitializationException( + "Error loading DispatcherServlet's default strategy class [" + className + + "] for interface [" + key + "]: problem with class file or dependent class", err); + } + } + return strategies; + } + else { + return new LinkedList<T>(); + } +} + +- HttpRequestHandlerAdapter(HTTP请求处理器适配器) +- 仅仅支持对HTTP请求处理器的适配,它简单地将HTTP请求和响应对象传递给HTTP请求处理器的实现,并不需要返回值,主要应用在基于HTTP的远程调用的实现上。 + +- SimpleControllerHandlerAdapter(简单控制器处理器适配器) +- 这个实现类将HTTP请求适配到一个Controller的实现进行处理。这里控制器的实现是一个简单的Controller接口的实现。SimpleControllerHandlerAdapter 被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在Controller接口的实现类中实现的。 +- AnnotationMethodHandlerAdapter(注解方法处理器适配器) +- 这个类的实现是基于注解的实现,它需要结合注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前的HTTP请求。 +- 在处理的过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并且映射返回值到模型和控制器对象,最后返回模型和控制器对象给DispatcherServlet。 + +- RequestMappingHandlerAdapter(请求映射处理器适配器) +- 初始化该Adapter时初始化一些解析器: + +``` +public void afterPropertiesSet() { + // Do this first, it may add ResponseBody advice beans + initControllerAdviceCache(); + + if (this.argumentResolvers == null) { + List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.initBinderArgumentResolvers == null) { + List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers(); + this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.returnValueHandlers == null) { + List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); + this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); + } +} +``` + + + +``` +private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { + List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); + + // Annotation-based argument resolution + resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); + resolvers.add(new RequestParamMapMethodArgumentResolver()); + resolvers.add(new PathVariableMethodArgumentResolver()); + resolvers.add(new PathVariableMapMethodArgumentResolver()); + resolvers.add(new MatrixVariableMethodArgumentResolver()); + resolvers.add(new MatrixVariableMapMethodArgumentResolver()); + resolvers.add(new ServletModelAttributeMethodProcessor(false)); + resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); + resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); + resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); + resolvers.add(new RequestHeaderMapMethodArgumentResolver()); + resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); + + // Type-based argument resolution + resolvers.add(new ServletRequestMethodArgumentResolver()); + resolvers.add(new ServletResponseMethodArgumentResolver()); + resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); + resolvers.add(new RedirectAttributesMethodArgumentResolver()); + resolvers.add(new ModelMethodProcessor()); + resolvers.add(new MapMethodProcessor()); + resolvers.add(new ErrorsMethodArgumentResolver()); + resolvers.add(new SessionStatusMethodArgumentResolver()); + resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); + + // Custom arguments + if (getCustomArgumentResolvers() != null) { + resolvers.addAll(getCustomArgumentResolvers()); + } + + // Catch-all + resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); + resolvers.add(new ServletModelAttributeMethodProcessor(true)); + + return resolvers; +} +``` + + +- Spring中所使用的Handler并没有任何特殊的联系,为了统一处理,Spring提供了不同情况下的适配器。 + - 2.1.4.3) initHandlerExceptionResolvers +- HanlderExceptionResolver是可以被用户定制的,只要实现该接口,实现resolveException方法并定义为一个bean即可。 +- resolveException方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的ModelAndView对象,如果该方法返回null,则Spring会继续寻找其他实现了HanlderExceptionResolver接口的bean。 + + +``` +private void initHandlerExceptionResolvers(ApplicationContext context) { + this.handlerExceptionResolvers = null; + + if (this.detectAllHandlerExceptionResolvers) { + // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts. + Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils + .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values()); + // We keep HandlerExceptionResolvers in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); + } + } + else { + try { + HandlerExceptionResolver her = + context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); + this.handlerExceptionResolvers = Collections.singletonList(her); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, no HandlerExceptionResolver is fine too. + } + } + + // Ensure we have at least some HandlerExceptionResolvers, by registering + // default HandlerExceptionResolvers if no other resolvers are found. + if (this.handlerExceptionResolvers == null) { + this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default"); + } + } +} +``` + + + - 2.1.4.4) initRequestToViewNameTranslator +- 当Controller方法没有返回一个View或者逻辑视图名称,并且在该方法中没有直接往response输出流里面写数据的时候,Spring就会按照约定好的方式提供一个逻辑视图名称(最简答的情况就是加prefix和suffix)。 +- 这个逻辑视图名称是通过Spring定义的RequestToViewNameTranslator接口的getViewName方法来实现的,用户也可以自定义自己的RequestToViewNameTranslator。 +- Spring为我们提供了一个默认的实现DefaultRequestToViewNameTranslator。 + +``` +public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; +``` + + + +``` +private void initRequestToViewNameTranslator(ApplicationContext context) { + try { + this.viewNameTranslator = + context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class); + if (logger.isDebugEnabled()) { + logger.debug("Using RequestToViewNameTranslator [" + this.viewNameTranslator + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate RequestToViewNameTranslator with name '" + + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + "': using default [" + this.viewNameTranslator + + "]"); + } + } +} +``` + + + - 2.1.4.5) initViewResolvers +- 当Controller将请求处理结果放到ModelAndView中以后,DispatcherServlet会根据ModelAndView选择合适的视图进行渲染。ViewResolver接口定义了resolveViewName方法,根据viewName创建合适类型的View实现。 +- 可以在Spring的配置文件中定义viewResolver。 + +``` +private void initViewResolvers(ApplicationContext context) { + this.viewResolvers = null; + + if (this.detectAllViewResolvers) { + // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. + Map<String, ViewResolver> matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values()); + // We keep ViewResolvers in sorted order. + AnnotationAwareOrderComparator.sort(this.viewResolvers); + } + } + else { + try { + ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); + this.viewResolvers = Collections.singletonList(vr); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default ViewResolver later. + } + } + + // Ensure we have at least one ViewResolver, by registering + // a default ViewResolver if no other resolvers are found. + if (this.viewResolvers == null) { + this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default"); + } + } +} +``` + + + - 2.1.4.6) initFlashMapManager +- flash attributes提供了一个请求存储属性,可供其他请求使用。在使用重定向的时候非常必要。 +- Spring MVC 有两个主要的抽象来支持 flash attributes。 FlashMap 用于保持 flash attributes 而 FlashMapManager用于存储,检索,管理FlashMap 实例。 +- Flash attribute 支持默认开启,并不需要显式启用,它永远不会导致HTTP Session的创建。 每一个请求都有一个 “input”FlashMap 具有从上一个请求(如果有的话)传过来的属性和一个 “output” FlashMap 具有将要在后续请求中保存的属性。 这两个 FlashMap 实例都可以通过静态方法RequestContextUtils从Spring MVC的任何位置访问。 + +``` +private void initFlashMapManager(ApplicationContext context) { + try { + this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class); + if (logger.isDebugEnabled()) { + logger.debug("Using FlashMapManager [" + this.flashMapManager + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate FlashMapManager with name '" + + FLASH_MAP_MANAGER_BEAN_NAME + "': using default [" + this.flashMapManager + "]"); + } + } +} +``` + +- + +- 处理请求 +- FrameworkServlet#service(入口) +- DispatcherServlet(FrameworkServlet的子类)无论是doGet、doPost等方法都会调用processRequest方法处理请求。 +- FrameworkServlet#processRequest +- 逻辑: + - 1)为了保证当前线程的LocaleContext和RequestAttributes可以在当前请求处理完毕后还能恢复,提取当前线程的两个属性 + - 2)根据当前request创建对应的LocaleContext和RequestAttributes,并绑定到当前线程 + - 3)委托给doService方法进一步处理 + - 4)请求处理结束后恢复线程到原始状态 + - 5)请求处理结束后无论成功与否都会发布事件通知 + +- protected final void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + long startTime = System.currentTimeMillis(); + Throwable failureCause = null; + + LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); + LocaleContext localeContext = buildLocaleContext(request); + + RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); + ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); + // 将localeContext和requestAttributes绑定到当前线程 + initContextHolders(request, localeContext, requestAttributes); + + try { + doService(request, response); + } + catch (ServletException ex) { + failureCause = ex; + throw ex; + } + catch (IOException ex) { + failureCause = ex; + throw ex; + } + catch (Throwable ex) { + failureCause = ex; + throw new NestedServletException("Request processing failed", ex); + } + + finally { +- // 恢复线程到原始状态 + resetContextHolders(request, previousLocaleContext, previousAttributes); + if (requestAttributes != null) { + requestAttributes.requestCompleted(); + } + + if (logger.isDebugEnabled()) { + if (failureCause != null) { + this.logger.debug("Could not complete request", failureCause); + } + else { + if (asyncManager.isConcurrentHandlingStarted()) { + logger.debug("Leaving response open for concurrent processing"); + } + else { + this.logger.debug("Successfully completed request"); + } + } + } + // 发布事件通知 + publishRequestHandledEvent(request, response, startTime, failureCause); + } +} + +- DispatcherServlet#doService +- 将已经初始化的功能辅助工具变量设置在request属性中。 +- 主要业务逻辑是在doDispatch方法中处理。 +- protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (logger.isDebugEnabled()) { + String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : ""; + logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed + + " processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]"); + } + + // Keep a snapshot of the request attributes in case of an include, + // to be able to restore the original attributes after the include. + Map<String, Object> attributesSnapshot = null; + if (WebUtils.isIncludeRequest(request)) { + attributesSnapshot = new HashMap<String, Object>(); + Enumeration<?> attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) { + attributesSnapshot.put(attrName, request.getAttribute(attrName)); + } + } + } + + // Make framework objects available to handlers and view objects. + request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); + request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); + request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); + request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); + + FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); + if (inputFlashMap != null) { + request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); + } + request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); + request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); + + try { + doDispatch(request, response); + } + finally { + if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { + // Restore the original attribute snapshot, in case of an include. + if (attributesSnapshot != null) { + restoreAttributesAfterInclude(request, attributesSnapshot); + } + } + } +} +- + +- doDispatch(主体) +- protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + ModelAndView mv = null; + Exception dispatchException = null; + + try { +- // 如果request是MultiPartContent类型的,则将其转为MultiPartHttpServletRequest + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + // 根据request信息寻找对应的Handler + mappedHandler = getHandler(processedRequest); + if (mappedHandler == null || mappedHandler.getHandler() == null) { +- // 没有找到对应的handler,则通过response反馈错误信息 + noHandlerFound(processedRequest, response); + return; + } + // 根据当前的handler寻找对应的HandlerAdapter + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + // 如果当前handler支持Last-Modified请求头,则对其进行处理 + // Process last-modified header, if supported by the handler. + String method = request.getMethod(); + boolean isGet = "GET".equals(method); + if (isGet || "HEAD".equals(method)) { + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if (logger.isDebugEnabled()) { + logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); + } + if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { + return; + } + } + // 调用拦截器的preHandle方法 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + // 调用handler并返回视图 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + if (asyncManager.isConcurrentHandlingStarted()) { + return; + } + // 转换视图名称(加prefix和suffix) + applyDefaultViewName(processedRequest, mv); +- // 调用拦截器的postHandle方法 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } + catch (Exception ex) { + dispatchException = ex; + } + catch (Throwable err) { + // As of 4.3, we're processing Errors thrown from handler methods as well, + // making them available for @ExceptionHandler methods and other scenarios. + dispatchException = new NestedServletException("Handler dispatch failed", err); + } + // 处理handle的结果 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + catch (Exception ex) { + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } + catch (Throwable err) { + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); + } + finally { + if (asyncManager.isConcurrentHandlingStarted()) { + // Instead of postHandle and afterCompletion + if (mappedHandler != null) { + mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); + } + } + else { + // Clean up any resources used by a multipart request. + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } + } + } +} +- + + - 1) checkMultiPart(处理文件上传请求) +- protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { + logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " + + "this typically results from an additional MultipartFilter in web.xml"); + } + else if (hasMultipartException(request) ) { + logger.debug("Multipart resolution failed for current request before - " + + "skipping re-resolution for undisturbed error rendering"); + } + else { + try { + return this.multipartResolver.resolveMultipart(request); + } + catch (MultipartException ex) { + if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { + logger.debug("Multipart resolution failed for error dispatch", ex); + // Keep processing error dispatch with regular request handle below + } + else { + throw ex; + } + } + } + } + // If not returned before: return original request. + return request; +} +- + +- MultipartResolver.resolveMultipart +- MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException; + + - 2) getHandler(获取HandlerExecutionChain) +- protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + for (HandlerMapping hm : this.handlerMappings) { + if (logger.isTraceEnabled()) { + logger.trace( + "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); + } + HandlerExecutionChain handler = hm.getHandler(request); + if (handler != null) { + return handler; + } + } + return null; +} +- HandlerMapping.getHandler +- HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; + - 2.1) AbstractHandlerMapping#getHandler + +``` +public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { +``` + +- // 根据request获取对应的handler + Object handler = getHandlerInternal(request); + if (handler == null) { +- // 如果没有对应的request的handler则使用默认的handler + handler = getDefaultHandler(); + } +- // 没有默认的handler则无法继续处理 + if (handler == null) { + return null; + } +- // 当查找的Controller为String,就意味着返回的是配置的bean名称,需要根据bean名称查找对应的bean + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + + HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); + if (CorsUtils.isCorsRequest(request)) { + CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request); + CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); + CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); + executionChain = getCorsHandlerExecutionChain(request, executionChain, config); + } + return executionChain; +} + +- 2.1.1-a) AbstractUrlHandlerMapping#getHandlerInternal +- 以AbstractUrlHandlerMapping为例 +- protected Object getHandlerInternal(HttpServletRequest request) throws Exception { +- // 截取用于匹配url的有效路径 + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); +- // 根据路径寻找handler + Object handler = lookupHandler(lookupPath, request); + if (handler == null) { + // We need to care for the default handler directly, since we need to + // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. + Object rawHandler = null; + if ("/".equals(lookupPath)) { +- // 如果请求路径是/,那么使用RootHandler进行处理 + rawHandler = getRootHandler(); + } + if (rawHandler == null) { +- // 如果无法找到handler,则使用默认handler + rawHandler = getDefaultHandler(); + } + if (rawHandler != null) { + // Bean name or resolved handler? + if (rawHandler instanceof String) { +- // 根据beanName寻找对应的beanName + String handlerName = (String) rawHandler; + rawHandler = getApplicationContext().getBean(handlerName); + } + validateHandler(rawHandler, request); + // 模板方法 +- handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); + } + } + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Mapping [" + lookupPath + "] to " + handler); + } + else if (handler == null && logger.isTraceEnabled()) { + logger.trace("No handler mapping found for [" + lookupPath + "]"); + } + return handler; +} + - 2.1.1.1) lookupHandler +- protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { + // Direct match? + - // 直接匹配 + Object handler = this.handlerMap.get(urlPath); + if (handler != null) { + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + return buildPathExposingHandler(handler, urlPath, urlPath, null); + } + // 通配符匹配 + // Pattern match? + List<String> matchingPatterns = new ArrayList<String>(); + for (String registeredPattern : this.handlerMap.keySet()) { + if (getPathMatcher().match(registeredPattern, urlPath)) { + matchingPatterns.add(registeredPattern); + } + else if (useTrailingSlashMatch()) { + if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { + matchingPatterns.add(registeredPattern +"/"); + } + } + } + + String bestMatch = null; + Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath); + if (!matchingPatterns.isEmpty()) { + Collections.sort(matchingPatterns, patternComparator); + if (logger.isDebugEnabled()) { + logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns); + } + bestMatch = matchingPatterns.get(0); + } + if (bestMatch != null) { + handler = this.handlerMap.get(bestMatch); + if (handler == null) { + if (bestMatch.endsWith("/")) { + handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); + } + if (handler == null) { + throw new IllegalStateException( + "Could not find handler for best pattern match [" + bestMatch + "]"); + } + } + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + validateHandler(handler, request); + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); + + // There might be multiple 'best patterns', let's make sure we have the correct URI template variables + // for all of them + Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>(); + for (String matchingPattern : matchingPatterns) { + if (patternComparator.compare(bestMatch, matchingPattern) == 0) { + Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); + Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars); + uriTemplateVariables.putAll(decodedVars); + } + } + if (logger.isDebugEnabled()) { + logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables); + } + return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables); + } + + // No handler found... + return null; +} + - 2.1.1.1.1) buildPathExposingHandler(将handler封装为HandlerExecutionChain,并添加两个拦截器) +- protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern, + String pathWithinMapping, Map<String, String> uriTemplateVariables) { + + HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); + chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping)); + if (!CollectionUtils.isEmpty(uriTemplateVariables)) { + chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); + } + return chain; +} +- 2.1.1-b) AbstractHandlerMethodMapping#getHandlerInternal(加入拦截器) +- protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); + if (logger.isDebugEnabled()) { + logger.debug("Looking up handler method for path " + lookupPath); + } + this.mappingRegistry.acquireReadLock(); + try { + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + if (logger.isDebugEnabled()) { + if (handlerMethod != null) { + logger.debug("Returning handler method [" + handlerMethod + "]"); + } + else { + logger.debug("Did not find handler method for [" + lookupPath + "]"); + } + } + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); + } + finally { + this.mappingRegistry.releaseReadLock(); + } +} + - 2.1.1.1) lookupHandlerMethod +- protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + List<Match> matches = new ArrayList<Match>(); +- // 直接匹配 + List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); + if (directPathMatches != null) { + addMatchingMappings(directPathMatches, matches, request); + } + if (matches.isEmpty()) { + // No choice but to go through all mappings... +- // 加入所有的映射 + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); + } + + if (!matches.isEmpty()) { + Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); +- // 按匹配程度排序 + Collections.sort(matches, comparator); + if (logger.isTraceEnabled()) { + logger.trace("Found " + matches.size() + " matching mapping(s) for [" + + lookupPath + "] : " + matches); + } + - // 得到最符合的匹配结果 + Match bestMatch = matches.get(0); + if (matches.size() > 1) { + if (CorsUtils.isPreFlightRequest(request)) { + return PREFLIGHT_AMBIGUOUS_MATCH; + } + Match secondBestMatch = matches.get(1); + if (comparator.compare(bestMatch, secondBestMatch) == 0) { + Method m1 = bestMatch.handlerMethod.getMethod(); + Method m2 = secondBestMatch.handlerMethod.getMethod(); + throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); + } + } + handleMatch(bestMatch.mapping, lookupPath, request); + return bestMatch.handlerMethod; + } + else { + return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); + } +} + + - 2.1.2) getHandlerExecutionChain(加入拦截器) +- 将handler实例和所有匹配的拦截器封装到HandlerExecutionChain中。 +- protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { + HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? + (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); + + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + for (HandlerInterceptor interceptor : this.adaptedInterceptors) { + if (interceptor instanceof MappedInterceptor) { + MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; + if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { + chain.addInterceptor(mappedInterceptor.getInterceptor()); + } + } + else { + chain.addInterceptor(interceptor); + } + } + return chain; +} + + - 3) noHandlerFound(没有找到HandlerExecutionChain) +- protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (pageNotFoundLogger.isWarnEnabled()) { + pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) + + "] in DispatcherServlet with name '" + getServletName() + "'"); + } +- // 默认为false + if (this.throwExceptionIfNoHandlerFound) { + throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), + new ServletServerHttpRequest(request).getHeaders()); + } + else { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } +} + +- + + - 4) getHandlerAdapter(根据HandlerExecutionChain获取HandlerAdapter) +- protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + for (HandlerAdapter ha : this.handlerAdapters) { + if (logger.isTraceEnabled()) { + logger.trace("Testing handler adapter [" + ha + "]"); + } + if (ha.supports(handler)) { + return ha; + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); +} + +- 4.1-a) SimpleControllerHandlerAdapter#supports + +``` +public boolean supports(Object handler) { + return (handler instanceof Controller); +} +``` + +- 4.1-b) AbstractHandlerMethodAdapter#supports + +``` +public final boolean supports(Object handler) { + return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)); +} +``` + + + - 5) HandlerExecutionChain#applyPreHandle(拦截器preHandle) +- boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + HandlerInterceptor[] interceptors = getInterceptors(); + if (!ObjectUtils.isEmpty(interceptors)) { + for (int i = 0; i < interceptors.length; i++) { + HandlerInterceptor interceptor = interceptors[i]; + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + } + return true; +} + + - 6) HandlerAdapter#handle(处理请求) +- ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; + +- 6.1-a) SimpleControllerHandlerAdapter.handle + +``` +public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return ((Controller) handler).handleRequest(request, response); +} +``` + + + - 6.1.1) AbstractController#handleRequest + +``` +public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + response.setHeader("Allow", getAllowHeader()); + return null; + } + + // Delegate to WebContentGenerator for checking and preparing. + checkRequest(request); + prepareResponse(response); + // 如果需要session内的同步执行 + // Execute handleRequestInternal in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { +``` + +- // 调用用户的逻辑 + return handleRequestInternal(request, response); + } + } + } + // 调用用户的逻辑 + return handleRequestInternal(request, response); +} +- 6.1-b) AbstractHandlerMethodAdapter#handle + +``` +public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return handleInternal(request, response, (HandlerMethod) handler); +} +``` + + + - 6.1.1) RequestMappingHandlerAdapter#handleInternal +- protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ModelAndView mav; + checkRequest(request); + + // Execute invokeHandlerMethod in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { + mav = invokeHandlerMethod(request, response, handlerMethod); + } + } + else { + // No HttpSession available -> no mutex necessary + mav = invokeHandlerMethod(request, response, handlerMethod); + } + } + else { + // No synchronization on session demanded at all... + mav = invokeHandlerMethod(request, response, handlerMethod); + } + + if (!response.containsHeader(HEADER_CACHE_CONTROL)) { + if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { + applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); + } + else { + prepareResponse(response); + } + } + + return mav; +} + + - 6.1.1.1) invokeHandlerMethod +- protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ServletWebRequest webRequest = new ServletWebRequest(request, response); + try { +- WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); + ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); + + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + invocableMethod.setDataBinderFactory(binderFactory); + invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); + modelFactory.initModel(webRequest, mavContainer, invocableMethod); + mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); + + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.setTaskExecutor(this.taskExecutor); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerCallableInterceptors(this.callableInterceptors); + asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); + + if (asyncManager.hasConcurrentResult()) { + Object result = asyncManager.getConcurrentResult(); + mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; + asyncManager.clearConcurrentResult(); + if (logger.isDebugEnabled()) { + logger.debug("Found concurrent result value [" + result + "]"); + } + invocableMethod = invocableMethod.wrapConcurrentResult(result); + } + + invocableMethod.invokeAndHandle(webRequest, mavContainer); + if (asyncManager.isConcurrentHandlingStarted()) { + return null; + } + + return getModelAndView(mavContainer, modelFactory, webRequest); + } + finally { + webRequest.requestCompleted(); + } +} + + +- protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) { + return new ServletInvocableHandlerMethod(handlerMethod); +} + + +``` +private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, + ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { + + modelFactory.updateModel(webRequest, mavContainer); + if (mavContainer.isRequestHandled()) { + return null; + } +``` + +- // 将返回值ModelMap转为ModelAndView + ModelMap model = mavContainer.getModel(); + ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); + if (!mavContainer.isViewReference()) { + mav.setView((View) mavContainer.getView()); + } + if (model instanceof RedirectAttributes) { + Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); + } + return mav; +} + + - 6.1.1.1.1) ServletInvocableHandlerMethod#invokeAndHandle + +``` +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + setResponseStatus(webRequest); + + if (returnValue == null) { + if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) { + mavContainer.setRequestHandled(true); + return; + } + } + else if (StringUtils.hasText(this.responseReason)) { + mavContainer.setRequestHandled(true); + return; + } + + mavContainer.setRequestHandled(false); + try { + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + } + catch (Exception ex) { + if (logger.isTraceEnabled()) { + logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); + } + throw ex; + } +} +``` + +- + + - 6.1.1.1.1.1) InvocableHandlerMethod#invokeForRequest + +``` +public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) + + "' with arguments " + Arrays.toString(args)); + } + Object returnValue = doInvoke(args); + if (logger.isTraceEnabled()) { + logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) + + "] returned [" + returnValue + "]"); + } + return returnValue; +} +``` + + + - 6.1.1.1.1.1.1) (处理参数)getMethodArgumentValues(方法参数注入,返回值即为方法参数) + +``` +private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + // 遍历请求参数,然后委托给HandlerMethodArgumentResolver#resolveArgument进行参数解析 + MethodParameter[] parameters = getMethodParameters(); + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + args[i] = resolveProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + if (this.argumentResolvers.supportsParameter(parameter)) { + try { + args[i] = this.argumentResolvers.resolveArgument( + parameter, mavContainer, request, this.dataBinderFactory); + continue; + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex); + } + throw ex; + } + } + if (args[i] == null) { + throw new IllegalStateException("Could not resolve method parameter at index " + + parameter.getParameterIndex() + " in " + parameter.getMethod().toGenericString() + + ": " + getArgumentResolutionErrorMessage("No suitable resolver for", i)); + } + } + return args; +} +``` + + - 6.1.1.1.1.1.1.1) HandlerMethodArgumentResolverComposite#resolveArgument(将形参解析为实参) + +``` +public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + if (resolver == null) { + throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]"); + } + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + + - 6.1.1.1.1.1.1.1.1) getArgumentResolver + +``` +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) { + if (logger.isTraceEnabled()) { + logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" + + parameter.getGenericParameterType() + "]"); + } + if (methodArgumentResolver.supportsParameter(parameter)) { + result = methodArgumentResolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + + - 6.1.1.1.1.1.1.1.2) (例子)RequestParamMethodArgumentResolver + + +``` +public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); + return StringUtils.hasText(paramName); + } + else { + return true; + } + } + else { + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return false; + } + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; + } + else if (this.useDefaultResolution) { + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); + } + else { + return false; + } + } +} +``` + + +- AbstractNamedValueMethodArgumentResolver#resolveArgument + +``` +public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveStringValue(namedValueInfo.name); + if (resolvedName == null) { + throw new IllegalArgumentException( + "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); + } + + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + if (arg == null) { + if (namedValueInfo.defaultValue != null) { + arg = resolveStringValue(namedValueInfo.defaultValue); + } + else if (namedValueInfo.required && !nestedParameter.isOptional()) { + handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); + } + arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); + } + else if ("".equals(arg) && namedValueInfo.defaultValue != null) { + arg = resolveStringValue(namedValueInfo.defaultValue); + } + + if (binderFactory != null) { + WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); + try { +``` + +- // WebDataBinder进行数据转换 + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); + } + catch (ConversionNotSupportedException ex) { + throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), + namedValueInfo.name, parameter, ex.getCause()); + } + catch (TypeMismatchException ex) { + throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), + namedValueInfo.name, parameter, ex.getCause()); + + } + } + + handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); + + return arg; +} + + - 6.1.1.1.1.1.1.1.2.1) resolveName +- protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); + + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; + } + + Object arg = null; + if (multipartRequest != null) { + List<MultipartFile> files = multipartRequest.getFiles(name); + if (!files.isEmpty()) { + arg = (files.size() == 1 ? files.get(0) : files); + } + } + if (arg == null) { + String[] paramValues = request.getParameterValues(name); + if (paramValues != null) { + arg = (paramValues.length == 1 ? paramValues[0] : paramValues); + } + } + return arg; +} + + - 6.1.1.1.1.1.1.1.2.2) WebDataBinder#convertIfNecessary + +``` +public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam) + throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); +} +``` + + + + - 6.1.1.1.1.1.2)(处理调用) InvocableHandlerMethod#doInvoke +- protected Object doInvoke(Object... args) throws Exception { + ReflectionUtils.makeAccessible(getBridgedMethod()); + try { +- // Controller中的方法,method.invoke + return getBridgedMethod().invoke(getBean(), args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(getBridgedMethod(), getBean(), args); + String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument"); + throw new IllegalStateException(getInvocationErrorMessage(text, args), ex); + } + catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else if (targetException instanceof Error) { + throw (Error) targetException; + } + else if (targetException instanceof Exception) { + throw (Exception) targetException; + } + else { + String text = getInvocationErrorMessage("Failed to invoke handler method", args); + throw new IllegalStateException(text, targetException); + } + } +} + + - 6.1.1.1.1.2) (处理返回值)HandlerMethodReturnValueHandler#handleReturnValue +- 以HandlerMethodReturnValueHandlerComposite为例: + +``` +public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); + } + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` + + + - 6.1.1.1.1.2.1) selectHandler + +``` +private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) { + boolean isAsyncValue = isAsyncReturnValue(value, returnType); + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { + continue; + } + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +- 如果返回值是ModelAndView,那么handler是ModelAndViewMethodReturnValueHandler。 +- 如果返回值是普通的对象(@ResponseBody),那么handler是 +- RequestResponseBodyMethodProcessor。 + + - 6.1.1.1.1.2.2) handleReturnValue +- 以RequestResponseBodyMethodProcessor为例: + +``` +public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + mavContainer.setRequestHandled(true); + ServletServerHttpRequest inputMessage = createInputMessage(webRequest); + ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); + + // Try even with null return value. ResponseBodyAdvice could get involved. + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); +} +``` + + +- 以ModelAndViewMethodReturnValueHandler为例: + +``` +public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + ModelAndView mav = (ModelAndView) returnValue; + if (mav.isReference()) { + String viewName = mav.getViewName(); + mavContainer.setViewName(viewName); + if (viewName != null && isRedirectViewName(viewName)) { + mavContainer.setRedirectModelScenario(true); + } + } + else { + View view = mav.getView(); + mavContainer.setView(view); + if (view instanceof SmartView) { + if (((SmartView) view).isRedirectView()) { + mavContainer.setRedirectModelScenario(true); + } + } + } + mavContainer.setStatus(mav.getStatus()); + mavContainer.addAllAttributes(mav.getModel()); +} +``` + + +- + + - 7) applyDefaultViewName(转换视图名称) + +``` +private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception { + if (mv != null && !mv.hasView()) { + mv.setViewName(getDefaultViewName(request)); + } +} +``` + + +- protected String getDefaultViewName(HttpServletRequest request) throws Exception { + return this.viewNameTranslator.getViewName(request); +} + +- DefaultRequestToViewNameTransaltor.getDefaultViewName + +``` +public String getViewName(HttpServletRequest request) { + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + return (this.prefix + transformPath(lookupPath) + this.suffix); +} +``` + + + - 8) HandlerExecutionChain#applyPostHandle(拦截器postHandle) +- void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception { + HandlerInterceptor[] interceptors = getInterceptors(); + if (!ObjectUtils.isEmpty(interceptors)) { + for (int i = interceptors.length - 1; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + interceptor.postHandle(request, response, this.handler, mv); + } + } +} + + - 9) processDispatchResult(处理ModelAndView请求结果) +- 如果返回的是纯数据(@ResponseBody),mv就是null,该方法基本上是空方法。 + +``` +private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { + + boolean errorView = false; + + if (exception != null) { + if (exception instanceof ModelAndViewDefiningException) { + logger.debug("ModelAndViewDefiningException encountered", exception); + mv = ((ModelAndViewDefiningException) exception).getModelAndView(); + } + else { + Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); +``` + +- // 处理异常 + mv = processHandlerException(request, response, handler, exception); + errorView = (mv != null); + } + } + + // Did the handler return a view to render? +- // 如果handler处理结果中返回了view,那么需要对页面进行渲染 + if (mv != null && !mv.wasCleared()) { + render(mv, request, response); + if (errorView) { + WebUtils.clearErrorRequestAttributes(request); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() + + "': assuming HandlerAdapter completed request handling"); + } + } + + if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { + // Concurrent handling started during a forward + return; + } + + if (mappedHandler != null) { + mappedHandler.triggerAfterCompletion(request, response, null); + } +} + + - 9.1) processHandlerException(处理异常) +- protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + + // Check registered HandlerExceptionResolvers... + ModelAndView exMv = null; + for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { +- // 使用handlerExceptionResolver来处理异常 + exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); + if (exMv != null) { + break; + } + } + if (exMv != null) { + if (exMv.isEmpty()) { + request.setAttribute(EXCEPTION_ATTRIBUTE, ex); + return null; + } + // We might still need view name translation for a plain error model... + if (!exMv.hasView()) { + exMv.setViewName(getDefaultViewName(request)); + } + if (logger.isDebugEnabled()) { + logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex); + } + WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); + return exMv; + } + + throw ex; +} + - 9.2) render(渲染视图) +- protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { + // Determine locale for request and apply it to the response. + Locale locale = this.localeResolver.resolveLocale(request); + response.setLocale(locale); + + View view; + if (mv.isReference()) { + // We need to resolve the view name. + view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + else { + // No need to lookup: the ModelAndView object contains the actual View object. + view = mv.getView(); + if (view == null) { + throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + + "View object in servlet with name '" + getServletName() + "'"); + } + } + + // Delegate to the View object for rendering. + if (logger.isDebugEnabled()) { + logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); + } + try { + if (mv.getStatus() != null) { + response.setStatus(mv.getStatus().value()); + } + view.render(mv.getModelInternal(), request, response); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + + getServletName() + "'", ex); + } + throw ex; + } +} + - 9.2.1) resolveViewName(创建视图) +- protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, + HttpServletRequest request) throws Exception { + + for (ViewResolver viewResolver : this.viewResolvers) { +- // 使用viewResolver解析视图名称 + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + return view; + } + } + return null; +} + - 9.2.1.1) AbstractCachingViewResolver#resolveViewName + + +``` +public View resolveViewName(String viewName, Locale locale) throws Exception { + if (!isCache()) { +``` + +- // 不存在缓存的情况下直接创建视图 + return createView(viewName, locale); + } + else { +- // 直接从缓存中获取 + Object cacheKey = getCacheKey(viewName, locale); + View view = this.viewAccessCache.get(cacheKey); + if (view == null) { + synchronized (this.viewCreationCache) { + view = this.viewCreationCache.get(cacheKey); + if (view == null) { + // Ask the subclass to create the View object. + view = createView(viewName, locale); + if (view == null && this.cacheUnresolved) { + view = UNRESOLVED_VIEW; + } + if (view != null) { + this.viewAccessCache.put(cacheKey, view); + this.viewCreationCache.put(cacheKey, view); + if (logger.isTraceEnabled()) { + logger.trace("Cached view [" + cacheKey + "]"); + } + } + } + } + } + return (view != UNRESOLVED_VIEW ? view : null); + } +} + + - 9.2.1.1.1) UrlBasedViewResolver#createView +- protected View createView(String viewName, Locale locale) throws Exception { + // If this resolver is not supposed to handle the given view, + // return null to pass on to the next resolver in the chain. +- // 如果当前解析器不支持当前解析器,如viewName为空等情况 + if (!canHandle(viewName, locale)) { + return null; + } +- // 处理redirect:xx的情况 + // Check for special "redirect:" prefix. + if (viewName.startsWith(REDIRECT_URL_PREFIX)) { + String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); + RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); + view.setHosts(getRedirectHosts()); + return applyLifecycleMethods(viewName, view); + } +- // 处理forward:xx的情况 + // Check for special "forward:" prefix. + if (viewName.startsWith(FORWARD_URL_PREFIX)) { + String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); + return new InternalResourceView(forwardUrl); + } + // Else fall back to superclass implementation: calling loadView. + return super.createView(viewName, locale); +} + +- AbstractCachingViewResolver.createView +- protected View createView(String viewName, Locale locale) throws Exception { + return loadView(viewName, locale); +} + +- UrlBasedViewResolver.loadView +- protected View loadView(String viewName, Locale locale) throws Exception { + AbstractUrlBasedView view = buildView(viewName); + View result = applyLifecycleMethods(viewName, view); + return (view.checkResource(locale) ? result : null); +} + +- UrlBasedViewResolver.buildView +- protected AbstractUrlBasedView buildView(String viewName) throws Exception { + AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); +- // 添加前缀和后缀 + view.setUrl(getPrefix() + viewName + getSuffix()); + + String contentType = getContentType(); + if (contentType != null) { +- // 设置ContentType + view.setContentType(contentType); + } + + view.setRequestContextAttribute(getRequestContextAttribute()); + view.setAttributesMap(getAttributesMap()); + + Boolean exposePathVariables = getExposePathVariables(); + if (exposePathVariables != null) { + view.setExposePathVariables(exposePathVariables); + } + Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); + if (exposeContextBeansAsAttributes != null) { + view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); + } + String[] exposedContextBeanNames = getExposedContextBeanNames(); + if (exposedContextBeanNames != null) { + view.setExposedContextBeanNames(exposedContextBeanNames); + } + + return view; +} + + - 9.2.2) AbstractView.render(页面跳转) + +``` +public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { + if (logger.isTraceEnabled()) { + logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + + " and static attributes " + this.staticAttributes); + } + // 处理Model + Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); + prepareResponse(request, response); +``` + +- // 处理页面跳转 + renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); +} + + - 9.2.2.1) AbstractView#createMergedOutputModel(处理Model) +- protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, + HttpServletResponse response) { + + @SuppressWarnings("unchecked") + Map<String, Object> pathVars = (this.exposePathVariables ? + (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null); + + // Consolidate static and dynamic model attributes. + int size = this.staticAttributes.size(); + size += (model != null ? model.size() : 0); + size += (pathVars != null ? pathVars.size() : 0); + + Map<String, Object> mergedModel = new LinkedHashMap<String, Object>(size); + mergedModel.putAll(this.staticAttributes); + if (pathVars != null) { + mergedModel.putAll(pathVars); + } + if (model != null) { + mergedModel.putAll(model); + } + + // Expose RequestContext? + if (this.requestContextAttribute != null) { + mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); + } + + return mergedModel; +} + + - 9.2.2.2) renderMergedOutputModel#renderMergedOutputModel(处理页面跳转) +- protected void renderMergedOutputModel( + Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { + // 将model中的数据以属性方式设置到request中 + // Expose the model object as request attributes. + exposeModelAsRequestAttributes(model, request); + + // Expose helpers as request attributes, if any. + exposeHelpers(request); + + // Determine the path for the request dispatcher. + String dispatcherPath = prepareForRendering(request, response); + + // Obtain a RequestDispatcher for the target resource (typically a JSP). + RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); + if (rd == null) { + throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + + "]: Check that the corresponding file exists within your web application archive!"); + } + + // If already included or response already committed, perform include, else forward. + if (useInclude(request, response)) { + response.setContentType(getContentType()); + if (logger.isDebugEnabled()) { + logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); + } + rd.include(request, response); + } + + else { + // Note: The forwarded resource is supposed to determine the content type itself. + if (logger.isDebugEnabled()) { + logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); + } + rd.forward(request, response); + } +} +- + +- 实例-MVC + +``` +@Controller +public class IndexController { + + @RequestMapping("/hello") + public ModelAndView index(ModelAndView modelAndView){ + modelAndView.addObject("user",new RegisterDTO("admin")); + modelAndView.setViewName("hello"); + return modelAndView; + } +} +``` + +- hello.jsp +- <body> + <h1>Hello World!</h1> + ${requestScope.user.username} +</body> + +- 执行路径: +- getHandler中handlerMappings为 + +- hm是0号,它调用getHandler。 +- HandlerExecutionChain handler = hm.getHandler(request); +- 该方法中的getHandlerInternal方法这里用到的类是AbstractHandlerMethodMapping。 +- 最终返回的是该handler,类型是HandlerMethod。 + + +- getHandlerExecutionChain执行完后多了三个interceptor。 + +- getHandlerAdapter中的handlerAdapters是 + +- 遍历时先用到的是RequestMappingHandlerAdapter,调用的supports是AbstractHandlerMethodAdapter。匹配后,返回的adapter是RequestMappingHandlerAdapter类型的 + +- + +- 然后调用该adapter的handle方法。 +- handle方法最终会调用Controller的对应方法,然后获取ModelAndView类型返回值的ModelAndViewMethodReturnValueHandler,对modelAndView进行处理。 +- 然后调用applyDefaultViewName方法. +- 在调用processDispatchResult之前,modelAndView是这样的。 + +- 然后去调用render方法。 +- render +- ->InternalResourceViewResolver.resolveViewName +- ->UriBasedViewResolver.createView +- ->UriBasedViewResolver.loadView +- ->InternalResourceViewResolver.buildView +- 此时得到的view如下: + +- 之后又调用了view的render方法,最终调用了requestDispatcher.forward方法结束整个过程。 +- + +- 实例-REST + +``` +@RequestMapping("/users/{name}") +@ResponseBody +public RegisterDTO findUserByName(@PathVariable("name") String name){ + return new RegisterDTO(name); +} +``` + +- 前面都一样,但是处理请求结果时使用的handler不一样,它使用的是RequestResponseBodyMethodProcessor,将返回值写入输出流。 +- From e3c10b5f47d3de4ae294d8feded456f347f8720c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:53:10 +0800 Subject: [PATCH 91/97] =?UTF-8?q?Create=20=E4=BA=8C=E5=8D=81=E4=B8=80?= =?UTF-8?q?=E3=80=81Tomcat=E6=BA=90=E7=A0=81=E8=A7=A3=E6=9E=90.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\347\240\201\350\247\243\346\236\220.md" | 7717 +++++++++++++++++ 1 file changed, 7717 insertions(+) create mode 100644 "docs/\344\272\214\345\215\201\344\270\200\343\200\201Tomcat\346\272\220\347\240\201\350\247\243\346\236\220.md" diff --git "a/docs/\344\272\214\345\215\201\344\270\200\343\200\201Tomcat\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/docs/\344\272\214\345\215\201\344\270\200\343\200\201Tomcat\346\272\220\347\240\201\350\247\243\346\236\220.md" new file mode 100644 index 00000000..22ee7294 --- /dev/null +++ "b/docs/\344\272\214\345\215\201\344\270\200\343\200\201Tomcat\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -0,0 +1,7717 @@ +# Tomcat +# Tomcat 参数调优 +- 默认值: +- <Connector port="8080" protocol="HTTP/1.1" +- connectionTimeout="20000" +- redirectPort="8443" /> + +- 修改配置: +- <Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" +- connectionTimeout="20000" +- redirectPort="8443" +- executor="TomcatThreadPool" +- enableLookups="false" +- acceptCount="100" +- maxPostSize="10485760" +- compression="on" +- disableUploadTimeout="true" +- compressionMinSize="2048" +- noCompressionUserAgents="gozilla, traviata" +- acceptorThreadCount="2" +- compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript" +- URIEncoding="utf-8"/> +- + +# Connector +- Tomcat有一个acceptor线程来accept socket连接,然后有工作线程来进行业务处理。对于client端的一个请求进来,流程是这样的:tcp的三次握手建立连接,建立连接的过程中,OS维护了半连接队列(syn队列)以及完全连接队列(accept队列),在第三次握手之后,server收到了client的ack,则进入establish的状态,然后该连接由syn队列移动到accept队列。 + +- Tomcat的acceptor线程则负责从accept队列中取出该connection,接受该connection,然后交给工作线程去处理(读取请求参数、处理逻辑、返回响应等等;如果该连接不是keep alived的话,则关闭该连接,然后该工作线程释放回线程池,如果是keep alived的话,则等待下一个数据包的到来直到keepAliveTimeout,然后关闭该连接释放回线程池), +- 然后自己接着去accept队列取connection(当当前socket连接超过maxConnections的时候,acceptor线程自己会阻塞等待,等连接降下去之后,才去处理accept队列的下一个连接)。acceptCount指的就是这个accept队列的大小。 + +# protocol(IO方式) +- Tomcat 8 设置 nio2 更好:org.apache.coyote.http11.Http11Nio2Protocol(如果这个用不了,就用下面那个) +- Tomcat 6、7 设置 nio 更好:org.apache.coyote.http11.Http11NioProtocol +- apr:调用httpd核心链接库来读取或文件传输,从而提高tomat对静态文件的处理性能。Tomcat APR模式也是Tomcat在高并发下的首选运行模式 +# URIEncoding(URL编码) +- URIEncoding=”UTF-8” + +- 使得Tomcat可以解析含有中文名的文件的url +# Executor(启用Worker线程池) +- <Executor name="TomcatThreadPool" namePrefix="catalina-exec-" +- maxThreads="150" minSpareThreads="100" +- prestartminSpareThreads="true" maxQueueSize="100"/> +## minSpareThreads(初始化时创建的线程数,类似于corePoolSize) +- 最小备用线程数,Tomcat启动时的初始化的线程数。 +## maxThreads(最大并发数,类似于maxPoolSize) +- maxThreads Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数,即最大并发数。 +- 默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断。 +- 虽然client的socket连接上了,但是可能都在Tomcat的task queue里头,等待worker线程处理返回响应。 +## maxQueueSize(Task队列大小) +- 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认设置 100。 +# connectionTimeout(超时时间) +- connectionTimeout为网络连接超时时间毫秒数。 +# enableLookups(是否允许DNS查询) +- enableLookups="false" 为了消除DNS查询对性能的影响我们可以关闭DNS查询,方式是修改 server.xml文件中的enableLookups参数值。 +# maxConnections(接收的最大连接数) +- 这个值表示最多可以有多少个socket连接到Tomcat上。NIO模式下默认是10000. +- 当连接数达到最大值后,系统会继续接收连接但不会超过acceptCount的值。 +# acceptCount(accept队列大小) +- 当accept队列满了之后,即使client继续向server发送ACK的包,也会不被响应,此时,server通过/proc/sys/net/ipv4/tcp_abort_on_overflow来决定如何返回,0表示直接丢丢弃该ACK,1表示发送RST通知client;相应的,client则会分别返回read timeout 或者 connection reset by peer。 + +- acceptCount在源码里对应的是backlog参数。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。Linux自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。 + +- client端的socket等待队列: +- 当第一次握手,建立半连接状态:client 通过 connect 向 server 发出 SYN 包时,client 会维护一个 socket 队列,如果 socket 等待队列满了,而 client 也会由此返回 connection time out,只要是 client 没有收到 第二次握手SYN+ACK,3s 之后,client 会再次发送,如果依然没有收到,9s 之后会继续发送。 + +- server端的半连接队列(syn队列): +- 此时server 会维护一个 SYN 队列,半连接 syn 队列的长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) ,在机器的tcp_max_syn_backlog值在/proc/sys/net/ipv4/tcp_max_syn_backlog下配置,当 server 收到 client 的 SYN 包后,会进行第二次握手发送SYN+ACK 的包加以确认,client 的 TCP 协议栈会唤醒 socket 等待队列,发出 connect 调用。 + +- server端的完全连接队列(accpet队列): +- 当第三次握手时,当server接收到ACK 报之后, 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则应该是由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以有我们的应用程序去定义的。 + +# acceptorThreadCount(用于接收请求的线程数) +- 用于接收连接的线程的数量,默认值是1。一般这个指需要改动的时候是因为该服务器是一个多核CPU,如果是多核 CPU 一般配置为 2。 +# HTTP压缩 +compression="on" compressionMinSize="2048"              +compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain" +- HTTP 压缩可以大大提高浏览网站的速度,它的原理是,在客户端请求网页后,从服务器端将网页文件压缩,再下载到客户端,由客户端的浏览器负责解压缩并浏览。相对于普通的浏览过程HTML,CSS,Javascript , Text ,它可以节省40%左右的流量。更为重要的是,它可以对动态生成的,包括CGI、PHP , JSP , ASP , Servlet,SHTML等输出的网页也能进行压缩,压缩效率惊人。 + - 1)compression="on" 打开压缩功能 + - 2)compressionMinSize="2048" 启用压缩的输出内容大小,这里面默认为2KB + - 3)noCompressionUserAgents="gozilla, traviata" 对于以下的浏览器,不启用压缩 + - 4)compressableMimeType="text/html,text/xml" 压缩类型 +- + +# 组件与框架 + + + +- Bootstrap:作为 Tomcat 对外界的启动类,在 $CATALINA_BASE/bin 目录下,它通过反射创建 Catalina 的实例并对其进行初始化及启动。 +- Catalina:解析 $CATALINA_BASE/conf/server.xml 文件并创建 StandardServer、StandardService、StandardEngine、StandardHost 等 +- Server:代表整个Catalina Servlet容器,可以包含一个或多个Service +- Service:包含一个或多个 Connector,和一个 Engine,Connector 和 Engine 都是在解析 conf/server.xml 文件时创建的,Engine 在 Tomcat 的标准实现是 StandardEngine +- Connector:实现某一协议的连接器,用来处理客户端发送来的协议,如默认的实现协议有HTTP、HTTPS、AJP。 +- 主要作用有: +- 根据不同的协议解析客户端的请求 +- 将解析完的请求转发给Connector关联的Engine容器处理 + +1.- Mapper 维护了 URL 到容器的映射关系。当请求到来时会根据 Mapper 中的映射信息决定将请求映射到哪一个 Host、Context、Wrapper。 +2.- Http11NioProtocol 用于处理 HTTP/1.1 的请求 +3.- NioEndpoint 是连接的端点,在请求处理流程中该类是核心类,会重点介绍。 +4.- CoyoteAdapter 用于将请求从 Connctor 交给 Container 处理,使 Connctor 和 Container 解耦。 +- MapperListener 实现了 LifecycleListener 和 ContainerListener 接口用于监听容器事件和生命周期事件。该监听器实例监听所有的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,当容器有变动时,注册容器到 Mapper。 +- Engine:代表的是Servlet引擎,接收来自不同Connector请求,处理后将结果返回给Connector。Engine是一个逻辑容器,包含一个或多个Host。默认实现是StandardEngine,主要有以下模块: +- Cluster:实现Tomcat管理 +- Realm:实现用户权限管理模块 +- Pipeline和Valve(阀门):处理Pipeline上的各个Valve,是一种责任链模式。只是简单的将Connector传过来的变量传给Host容器 + +- Host:虚拟主机,即域名或网络名,用于部署该虚拟主机上的应用程序。通常包含多个 Context (Context 在 Tomcat 中代表应用程序)。Context 在 Tomcat 中的标准实现是 StandardContext。 +- Context:部署的具体Web应用,每个请求都在是相应的上下文里处理,如一个war包。默认实现是StandardContext,通常包含多个 Wrapper主要有以下模块: +- Realm:实现用户权限管理模块 +- Pipeline和Valve:处理Pipeline上的各个Valve,是一种责任链模式 +- Manager: 它主要是应用的session管理模块 +- Resources: 它是每个web app对应的部署结构的封装 +- Loader:它是对每个web app的自有的classloader的封装 +- Mapper:它封装了请求资源URI与每个相对应的处理wrapper容器的映射关系 +- Wrapper:对应定义的Servlet,一一对应。默认实现是StandardWrapper,主要有以下模块: +- Pipeline和Valve:处理Pipeline上的各个Valve,是一种责任链模式 +- Servlet和Servlet Stack:保存Wrapper包装的Servlet +- StandardPipeline 组件代表一个流水线,与 Valve(阀)结合,用于处理请求。 StandardPipeline 中含有多个 Valve, 当需要处理请求时,会逐一调用 Valve 的 invoke 方法对 Request 和 Response 进行处理。特别的,其中有一个特殊的 Valve 叫 basicValve,每一个标准容器都有一个指定的 BasicValve,他们做的是最核心的工作。 +- StandardEngine 的是 StandardEngineValve,他用来将 Request 映射到指定的 Host; +- StandardHost 的是 StandardHostValve, 他用来将 Request 映射到指定的 Context; +- StandardContext 的是 StandardContextValve,它用来将 Request 映射到指定的 Wrapper; +- StandardWrapper 的是 StandardWrapperValve,他用来加载 Rquest 所指定的 Servlet,并调用 Servlet 的 Service 方法。 + +- 由上可知,Catalina中有两个主要的模块:连接器(Connector)和容器(Container)、 + +- 以 Tomcat 为例,它的主线流程大致可以分为 3 个:启动、部署、请求处理。入口点就是 Bootstrap 类和 接受请求的 Acceptor 类! + +# 生命周期 +- 在Tomcat启动时,会读取server.xml文件创建Server, Service, Connector, Engine, Host, Context, Wrapper等组件。 +# Lifestyle +- Tomcat中的所有组件都继承了Lifecycle接口,Lifecycle接口定义了一整套生命周期管理的函数,从组件的新建、初始化完成、启动、停止、失败到销毁,都遵守同样的规则,Lifecycle组件的状态转换图如下。 + +- 正常的调用顺序是init()->start()->destroy(),父组件的init()和start()会触发子组件的init()和start(),所以Tomcat中只需调用Server组件的init()和start()即可。 +每个实现组件都继承自LifecycleBase,LifecycleBase实现了Lifecycle接口,当容器状态发生变化时,都会调用fireLifecycleEvent方法,生成LifecycleEvent,并且交由此容器的事件监听器处理。 +# 启动 + +- tomcat/bin/startup.sh脚本是启动了org.apache.catalina.startup.Bootstra类的main方法,并传入start参数。 + +- + +- 主要步骤如下: +1.- 新建Bootstrap对象daemon,并调用其init()方法 +2.- 初始化Tomcat的类加载器(init) +3.- 用反射实例化org.apache.catalina.startup.Catalina对象catalinaDaemon(init) +4.- 调用daemon的load方法,实质上调用了catalinaDaemon的load方法(load) +5.- 加载和解析server.xml配置文件(load) +6.- 调用daemon的start方法,实质上调用了catalinaDaemon的start方法 (start) +7.- 启动Server组件,Server的启动会带动其他组件的启动,如Service, Container, Connector(start) +8.- 调用catalinaDaemon的await方法循环等待接收Tomcat的shutdown命令 +- BootStrap#main + +``` +public static void main(String args[]) { + + if (daemon == null) { + // Don't set daemon until init() has completed + Bootstrap bootstrap = new Bootstrap(); + try { + bootstrap.init(); + } catch (Throwable t) { + handleThrowable(t); + t.printStackTrace(); + return; + } + daemon = bootstrap; + } else { + // When running as a service the call to stop will be on a new + // thread so make sure the correct class loader is used to prevent + // a range of class not found exceptions. + Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); + } + + try { + String command = "start"; + if (args.length > 0) { + command = args[args.length - 1]; + } + + if (command.equals("startd")) { + args[args.length - 1] = "start"; + daemon.load(args); + daemon.start(); + } else if (command.equals("stopd")) { + args[args.length - 1] = "stop"; + daemon.stop(); + } else if (command.equals("start")) { +``` + + - // 设置 Catalina 的 await 属性为 true。在 Start 阶段尾部,若该属性为 true,Tomcat 会在 main 线程中监听 SHUTDOWN 命令,默认端口是 8005.当收到该命令后执行 Catalina 的 stop() 方法关闭 Tomcat 服务器。 + daemon.setAwait(true); + daemon.load(args); + daemon.start(); + } else if (command.equals("stop")) { + daemon.stopServer(args); + } else if (command.equals("configtest")) { + daemon.load(args); + if (null==daemon.getServer()) { + System.exit(1); + } + System.exit(0); + } else { + log.warn("Bootstrap: command \"" + command + "\" does not exist."); + } + } catch (Throwable t) { + // Unwrap the Exception for clearer error reporting + if (t instanceof InvocationTargetException && + t.getCause() != null) { + t = t.getCause(); + } + handleThrowable(t); + t.printStackTrace(); + System.exit(1); + } + +} + - 1) BootStrap#init +- 必须使用反射去实例化Catalina对象,此时可以使用Tomcat自己的Classloader。否则会使用Java的Classloader去加载Catalina对象。 + +``` +public void init() throws Exception { +``` + +- // 初始化commonLoader、catalinaLoader和sharedLoader; + initClassLoaders(); + // 将catalinaLoader设置为Tomcat主线程的线程上下文类加载器; + Thread.currentThread().setContextClassLoader(catalinaLoader); + + SecurityClassLoad.securityClassLoad(catalinaLoader); + + // Load our startup class and call its process() method + if (log.isDebugEnabled()) + log.debug("Loading startup class"); + Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); + Object startupInstance = startupClass.getConstructor().newInstance(); + + // Set the shared extensions class loader + if (log.isDebugEnabled()) + log.debug("Setting startup class properties"); + String methodName = "setParentClassLoader"; + Class<?> paramTypes[] = new Class[1]; + paramTypes[0] = Class.forName("java.lang.ClassLoader"); + Object paramValues[] = new Object[1]; + paramValues[0] = sharedLoader; + Method method = + startupInstance.getClass().getMethod(methodName, paramTypes); + method.invoke(startupInstance, paramValues); + + catalinaDaemon = startupInstance; + +} + - 1.1) BootStrap#initClassLoaders() +- commonLoader、catalinaLoader和sharedLoader是在Tomcat容器初始化的的过程刚刚开始(即调用Bootstrap的init方法时)创建的。catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身的class。 + +``` + +private void initClassLoaders() { + try { + commonLoader = createClassLoader("common", null); + if( commonLoader == null ) { + // no config file, default to this loader - we might be in a 'single' env. + commonLoader=this.getClass().getClassLoader(); + } + catalinaLoader = createClassLoader("server", commonLoader); + sharedLoader = createClassLoader("shared", commonLoader); + } catch (Throwable t) { + handleThrowable(t); + log.error("Class loader creation threw exception", t); + System.exit(1); + } +} +``` + +#### 1.1.1) createClassLoader +- createClassLoader的处理步骤如下: + +- 定位资源路径与资源类型; +- 使用ClassLoaderFactory创建类加载器org.apache.catalina.loader.StandardClassLoader,这个StandardClassLoader仅仅继承了URLClassLoader而没有其他更多改动。 +- Tomcat默认只会指定commonLoader,catalinaLoader和sharedLoader实际也是commonLoader。(在catalina.properties配置文件中,我们可以看到common属性默认值为{catalina.base}/lib/.jar,{catalina.home}/lib/.jar,如下配置所示,属性catalina.home默认为Tomcat的根目录。) + + +``` +common.loader=${catalina.home}/lib,${catalina.home}/lib/*.jar +``` + + +``` +private ClassLoader createClassLoader(String name, ClassLoader parent) + throws Exception { + + String value = CatalinaProperties.getProperty(name + ".loader"); + if ((value == null) || (value.equals(""))) + return parent; + + value = replace(value); + + List<Repository> repositories = new ArrayList<>(); + + String[] repositoryPaths = getPaths(value); + + for (String repository : repositoryPaths) { + // Check for a JAR URL repository + try { + @SuppressWarnings("unused") + URL url = new URL(repository); + repositories.add( + new Repository(repository, RepositoryType.URL)); + continue; + } catch (MalformedURLException e) { + // Ignore + } + + // Local repository + if (repository.endsWith("*.jar")) { + repository = repository.substring + (0, repository.length() - "*.jar".length()); + repositories.add( + new Repository(repository, RepositoryType.GLOB)); + } else if (repository.endsWith(".jar")) { + repositories.add( + new Repository(repository, RepositoryType.JAR)); + } else { + repositories.add( + new Repository(repository, RepositoryType.DIR)); + } + } + + return ClassLoaderFactory.createClassLoader(repositories, parent); +} +``` + + - 1.1.1.1) ClassLoaderFactory#createClassLoader + +``` +public static ClassLoader createClassLoader(List<Repository> repositories, + final ClassLoader parent) + throws Exception { + + if (log.isDebugEnabled()) + log.debug("Creating new class loader"); + + // Construct the "class path" for this class loader + Set<URL> set = new LinkedHashSet<>(); + + if (repositories != null) { + for (Repository repository : repositories) { + if (repository.getType() == RepositoryType.URL) { + URL url = buildClassLoaderUrl(repository.getLocation()); + if (log.isDebugEnabled()) + log.debug(" Including URL " + url); + set.add(url); + } else if (repository.getType() == RepositoryType.DIR) { + File directory = new File(repository.getLocation()); + directory = directory.getCanonicalFile(); + if (!validateFile(directory, RepositoryType.DIR)) { + continue; + } + URL url = buildClassLoaderUrl(directory); + if (log.isDebugEnabled()) + log.debug(" Including directory " + url); + set.add(url); + } else if (repository.getType() == RepositoryType.JAR) { + File file=new File(repository.getLocation()); + file = file.getCanonicalFile(); + if (!validateFile(file, RepositoryType.JAR)) { + continue; + } + URL url = buildClassLoaderUrl(file); + if (log.isDebugEnabled()) + log.debug(" Including jar file " + url); + set.add(url); + } else if (repository.getType() == RepositoryType.GLOB) { + File directory=new File(repository.getLocation()); + directory = directory.getCanonicalFile(); + if (!validateFile(directory, RepositoryType.GLOB)) { + continue; + } + if (log.isDebugEnabled()) + log.debug(" Including directory glob " + + directory.getAbsolutePath()); + String filenames[] = directory.list(); + if (filenames == null) { + continue; + } + for (int j = 0; j < filenames.length; j++) { + String filename = filenames[j].toLowerCase(Locale.ENGLISH); + if (!filename.endsWith(".jar")) + continue; + File file = new File(directory, filenames[j]); + file = file.getCanonicalFile(); + if (!validateFile(file, RepositoryType.JAR)) { + continue; + } + if (log.isDebugEnabled()) + log.debug(" Including glob jar file " + + file.getAbsolutePath()); + URL url = buildClassLoaderUrl(file); + set.add(url); + } + } + } + } + + // Construct the class loader itself + final URL[] array = set.toArray(new URL[set.size()]); + if (log.isDebugEnabled()) + for (int i = 0; i < array.length; i++) { + log.debug(" location " + i + " is " + array[i]); + } + + return AccessController.doPrivileged( + new PrivilegedAction<URLClassLoader>() { + @Override + public URLClassLoader run() { + if (parent == null) + return new URLClassLoader(array); + else + return new URLClassLoader(array, parent); + } + }); +} +``` + + + - 2) BootStrap#load + +``` +private void load(String[] arguments) + throws Exception { + + // Call the load() method + String methodName = "load"; + Object param[]; + Class<?> paramTypes[]; + if (arguments==null || arguments.length==0) { + paramTypes = null; + param = null; + } else { + paramTypes = new Class[1]; + paramTypes[0] = arguments.getClass(); + param = new Object[1]; + param[0] = arguments; + } + Method method = + catalinaDaemon.getClass().getMethod(methodName, paramTypes); + if (log.isDebugEnabled()) + log.debug("Calling startup class " + method); + method.invoke(catalinaDaemon, param); + +} +``` + + - 2.1) Catalina#load + +``` +public void load() { + + if (loaded) { + return; + } + loaded = true; + + long t1 = System.nanoTime(); + + initDirs(); + + // Before digester - it may be needed + initNaming(); + + // Create and execute our Digester + Digester digester = createStartDigester(); + + InputSource inputSource = null; + InputStream inputStream = null; + File file = null; + try { + try { + file = configFile(); + inputStream = new FileInputStream(file); + inputSource = new InputSource(file.toURI().toURL().toString()); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("catalina.configFail", file), e); + } + } + if (inputStream == null) { + try { + inputStream = getClass().getClassLoader() + .getResourceAsStream(getConfigFile()); + inputSource = new InputSource + (getClass().getClassLoader() + .getResource(getConfigFile()).toString()); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("catalina.configFail", + getConfigFile()), e); + } + } + } + + // This should be included in catalina.jar + // Alternative: don't bother with xml, just create it manually. + if (inputStream == null) { + try { + inputStream = getClass().getClassLoader() + .getResourceAsStream("server-embed.xml"); + inputSource = new InputSource + (getClass().getClassLoader() + .getResource("server-embed.xml").toString()); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("catalina.configFail", + "server-embed.xml"), e); + } + } + } + + + if (inputStream == null || inputSource == null) { + if (file == null) { + log.warn(sm.getString("catalina.configFail", + getConfigFile() + "] or [server-embed.xml]")); + } else { + log.warn(sm.getString("catalina.configFail", + file.getAbsolutePath())); + if (file.exists() && !file.canRead()) { + log.warn("Permissions incorrect, read permission is not allowed on the file."); + } + } + return; + } + + try { + inputSource.setByteStream(inputStream); + digester.push(this); + digester.parse(inputSource); + } catch (SAXParseException spe) { + log.warn("Catalina.start using " + getConfigFile() + ": " + + spe.getMessage()); + return; + } catch (Exception e) { + log.warn("Catalina.start using " + getConfigFile() + ": " , e); + return; + } + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // Ignore + } + } + } + + getServer().setCatalina(this); + getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile()); + getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); + + // Stream redirection + initStreams(); + + // Start the new server + try { + getServer().init(); + } catch (LifecycleException e) { + if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) { + throw new java.lang.Error(e); + } else { + log.error("Catalina.start", e); + } + } + + long t2 = System.nanoTime(); + if(log.isInfoEnabled()) { + log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms"); + } +} +``` + + - 2.1.1) Digester#parse(配置文件解析,创建子容器) + +``` +public Object parse(InputSource input) throws IOException, SAXException { + configure(); + getXMLReader().parse(input); + return root; +} +``` + +- org.apache.commons.digester +- 该包提供了基于规则的,可任意处理XML文档的类 + +- org.apache.commons.digester.Digester是Digester类库的主类,该类可用于解析XML文档。 +- 解析过程分为两步: +- 定义好模式(定义要匹配的标签) +- 将模式与规则(定义匹配到标签后的行为的对象)相关联 + +- 解析过程中会调用startElement方法,会按照既定的一些规则,在读取的同时去创建对象。 +- 比如: +- digester.addRule("Server/Service/Connector", + new ConnectorCreateRule()); + + - 2.1.1.1) ConnectorCreateRule#begin + +``` +public void begin(String namespace, String name, Attributes attributes) + throws Exception { + Service svc = (Service)digester.peek(); + Executor ex = null; + if ( attributes.getValue("executor")!=null ) { + ex = svc.getExecutor(attributes.getValue("executor")); + } + Connector con = new Connector(attributes.getValue("protocol")); + if (ex != null) { + setExecutor(con, ex); + } + String sslImplementationName = attributes.getValue("sslImplementationName"); + if (sslImplementationName != null) { + setSSLImplementationName(con, sslImplementationName); + } + digester.push(con); +} +``` + + + + - 2.1.1.1.1) Connector#constructor(从Connector开始的初始化) + + +``` +public Connector(String protocol) { + boolean aprConnector = AprLifecycleListener.isAprAvailable() && + AprLifecycleListener.getUseAprConnector(); + + if ("HTTP/1.1".equals(protocol) || protocol == null) { + if (aprConnector) { + protocolHandlerClassName = "org.apache.coyote.http11.Http11AprProtocol"; + } else { + protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol"; + } + } else if ("AJP/1.3".equals(protocol)) { + if (aprConnector) { + protocolHandlerClassName = "org.apache.coyote.ajp.AjpAprProtocol"; + } else { + protocolHandlerClassName = "org.apache.coyote.ajp.AjpNioProtocol"; + } + } else { + protocolHandlerClassName = protocol; + } + + // Instantiate protocol handler + ProtocolHandler p = null; + try { +``` + +- // 反射创建Http11NioProtocol + Class<?> clazz = Class.forName(protocolHandlerClassName); + p = (ProtocolHandler) clazz.getConstructor().newInstance(); + } catch (Exception e) { + log.error(sm.getString( + "coyoteConnector.protocolHandlerInstantiationFailed"), e); + } finally { + this.protocolHandler = p; + } + + // Default for Connector depends on this system property + setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")); +} + - 2.1.1.1.1.1) Http11NioProtocol#constructor + +``` +public Http11NioProtocol() { + super(new NioEndpoint()); +} +``` + + + +``` +public AbstractHttp11JsseProtocol(AbstractJsseEndpoint<S,?> endpoint) { + super(endpoint); +} +``` + + + +``` +public AbstractHttp11Protocol(AbstractEndpoint<S,?> endpoint) { + super(endpoint); + setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT); + ConnectionHandler<S> cHandler = new ConnectionHandler<>(this); + setHandler(cHandler); + getEndpoint().setHandler(cHandler); +} +``` + + + +``` +public AbstractProtocol(AbstractEndpoint<S,?> endpoint) { + this.endpoint = endpoint; + setConnectionLinger(Constants.DEFAULT_CONNECTION_LINGER); + setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY); +} +``` + + + - 2.1.1.1.1.1.1) ConnectionHandler#constructor + +``` +public ConnectionHandler(AbstractProtocol<S> proto) { + this.proto = proto; +} +``` + + +### + - 2.1.2) StandardServer#init +- 模板方法模式,调用的是自己重写的initInternal。 +- protected void initInternal() throws LifecycleException { + + super.initInternal(); + + // Register global String cache + // Note although the cache is global, if there are multiple Servers + // present in the JVM (may happen when embedding) then the same cache + // will be registered under multiple names + onameStringCache = register(new StringCache(), "type=StringCache"); + + // Register the MBeanFactory + MBeanFactory factory = new MBeanFactory(); + factory.setContainer(this); + onameMBeanFactory = register(factory, "type=MBeanFactory"); + + // Register the naming resources + globalNamingResources.init(); + + // Populate the extension validator with JARs from common and shared + // class loaders + if (getCatalina() != null) { + ClassLoader cl = getCatalina().getParentClassLoader(); + // Walk the class loader hierarchy. Stop at the system class loader. + // This will add the shared (if present) and common class loaders + while (cl != null && cl != ClassLoader.getSystemClassLoader()) { + if (cl instanceof URLClassLoader) { + URL[] urls = ((URLClassLoader) cl).getURLs(); + for (URL url : urls) { + if (url.getProtocol().equals("file")) { + try { + File f = new File (url.toURI()); + if (f.isFile() && + f.getName().endsWith(".jar")) { + ExtensionValidator.addSystemResource(f); + } + } catch (URISyntaxException e) { + // Ignore + } catch (IOException e) { + // Ignore + } + } + } + } + cl = cl.getParent(); + } + } + // Initialize our defined Services + for (int i = 0; i < services.length; i++) { + services[i].init(); + } +} + +- 初始化StandardService + - 2.1.2.1) StandardService#init +- protected void initInternal() throws LifecycleException { + + super.initInternal(); + + if (engine != null) { + engine.init(); + } + + // Initialize any Executors + for (Executor executor : findExecutors()) { + if (executor instanceof JmxEnabled) { + ((JmxEnabled) executor).setDomain(getDomain()); + } + executor.init(); + } + + // Initialize mapper listener + mapperListener.init(); + + // Initialize our defined Connectors + synchronized (connectorsLock) { + for (Connector connector : connectors) { + connector.init(); + } + } +} + - 2.1.2.1.1) Connector#init +- protected void initInternal() throws LifecycleException { + + super.initInternal(); + + if (protocolHandler == null) { + throw new LifecycleException( + sm.getString("coyoteConnector.protocolHandlerInstantiationFailed")); + } + + // Initialize adapter + adapter = new CoyoteAdapter(this); +- // protocolHandler 即 Http11NioProtocol + protocolHandler.setAdapter(adapter); + + // Make sure parseBodyMethodsSet has a default + if (null == parseBodyMethodsSet) { + setParseBodyMethods(getParseBodyMethods()); + } + + if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) { + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr", + getProtocolHandlerClassName())); + } + if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() && + protocolHandler instanceof AbstractHttp11JsseProtocol) { + AbstractHttp11JsseProtocol<?> jsseProtocolHandler = + (AbstractHttp11JsseProtocol<?>) protocolHandler; + if (jsseProtocolHandler.isSSLEnabled() && + jsseProtocolHandler.getSslImplementationName() == null) { + // OpenSSL is compatible with the JSSE configuration, so use it if APR is available + jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName()); + } + } + + try { + protocolHandler.init(); + } catch (Exception e) { + throw new LifecycleException( + sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); + } +} + - 2.1.2.1.1.1) AbstractHttp11Protocol#init + + +``` +public void init() throws Exception { + // Upgrade protocols have to be configured first since the endpoint + // init (triggered via super.init() below) uses this list to configure + // the list of ALPN protocols to advertise + for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { + configureUpgradeProtocol(upgradeProtocol); + } + + super.init(); +} +``` + + +- AbstractProtocol#init + +``` +public void init() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.init", getName())); + } + + if (oname == null) { + // Component not pre-registered so register it + oname = createObjectName(); + if (oname != null) { + Registry.getRegistry(null, null).registerComponent(this, oname, null); + } + } + + if (this.domain != null) { + rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName()); + Registry.getRegistry(null, null).registerComponent( + getHandler().getGlobal(), rgOname, null); + } + + String endpointName = getName(); + endpoint.setName(endpointName.substring(1, endpointName.length()-1)); + endpoint.setDomain(domain); + + endpoint.init(); +} +``` + + - 2.1.2.1.1.1.1) AbstractEndPoint#init + +``` +public final void init() throws Exception { + if (bindOnInit) { + bind(); + bindState = BindState.BOUND_ON_INIT; + } + if (this.domain != null) { + // Register endpoint (as ThreadPool - historical name) + oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\""); + Registry.getRegistry(null, null).registerComponent(this, oname, null); + + for (SSLHostConfig sslHostConfig : findSslHostConfigs()) { + registerJmx(sslHostConfig); + } + } +} +``` + + - 2.1.2.1.1.1.1.1) NioEndpoint#init + +``` +public void bind() throws Exception { + initServerSocket(); + + // Initialize thread count defaults for acceptor, poller + if (acceptorThreadCount == 0) { + // FIXME: Doesn't seem to work that well with multiple accept threads + acceptorThreadCount = 1; + } + if (pollerThreadCount <= 0) { + //minimum one poller thread + pollerThreadCount = 1; + } + setStopLatch(new CountDownLatch(pollerThreadCount)); + + // Initialize SSL if needed + initialiseSsl(); + + selectorPool.open(); +} +``` + + + - 2.1.2.1.1.1.1.1.1) NioEndpoint#initServerSocket(创建阻塞的ServerSocket) +- protected void initServerSocket() throws Exception { + serverSock = ServerSocketChannel.open(); + socketProperties.setProperties(serverSock.socket()); + InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort())); + serverSock.socket().bind(addr,getAcceptCount()); + serverSock.configureBlocking(true); //mimic APR behavior +} +- 打开一个 ServerSocket,默认绑定到 8080 端口,默认的连接等待队列长度是 100, 当超过 100 个时会拒绝服务。我们可以通过配置 conf/server.xml 中 Connector 的 acceptCount 属性对其进行定制。 + - 2.1.2.1.1.1.1.1.2) NioSelectorPool#open(辅助selector) +- protected static final boolean SHARED = + Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.net.NioSelectorShared", "true")); + + +``` +public void open() throws IOException { + enabled = true; + getSharedSelector(); + if (SHARED) { + blockingSelector = new NioBlockingSelector(); + blockingSelector.open(getSharedSelector()); + } +} +``` + +### + - 2.1.2.1.1.1.1.1.2.1) NioSelectorPool#getSharedSelector(开启selector) + +- protected Selector getSharedSelector() throws IOException { + if (SHARED && SHARED_SELECTOR == null) { + synchronized ( NioSelectorPool.class ) { + if ( SHARED_SELECTOR == null ) { + SHARED_SELECTOR = Selector.open(); + log.info("Using a shared selector for servlet write/read"); + } + } + } + return SHARED_SELECTOR; +} + + - 2.1.2.1.1.1.1.1.2.2) NioBlockingSelector#open(启动blockPoller线程) + +``` +public void open(Selector selector) { + sharedSelector = selector; + poller = new BlockPoller(); + poller.selector = sharedSelector; + poller.setDaemon(true); + poller.setName("NioBlockingSelector.BlockPoller-"+(++threadCounter)); + poller.start(); +} +``` + + + - 3) BootStrap#start + +``` +public void start() + throws Exception { + if( catalinaDaemon==null ) init(); + + Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); + method.invoke(catalinaDaemon, (Object [])null); + +} +``` + + - 3.1) Catalina#start + +``` +public void start() { + + if (getServer() == null) { + load(); + } + + if (getServer() == null) { + log.fatal("Cannot start server. Server instance is not configured."); + return; + } + + long t1 = System.nanoTime(); + + // Start the new server + try { + getServer().start(); + } catch (LifecycleException e) { + log.fatal(sm.getString("catalina.serverStartFail"), e); + try { + getServer().destroy(); + } catch (LifecycleException e1) { + log.debug("destroy() failed for failed Server ", e1); + } + return; + } + + long t2 = System.nanoTime(); + if(log.isInfoEnabled()) { + log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms"); + } + + // Register shutdown hook + if (useShutdownHook) { + if (shutdownHook == null) { + shutdownHook = new CatalinaShutdownHook(); + } + Runtime.getRuntime().addShutdownHook(shutdownHook); + + // If JULI is being used, disable JULI's shutdown hook since + // shutdown hooks run in parallel and log messages may be lost + // if JULI's hook completes before the CatalinaShutdownHook() + LogManager logManager = LogManager.getLogManager(); + if (logManager instanceof ClassLoaderLogManager) { + ((ClassLoaderLogManager) logManager).setUseShutdownHook( + false); + } + } + + if (await) { + await(); + stop(); + } +} +``` + + - 3.1.1) StandardServer#start +- start同样也是模板方法模式。 +- protected void startInternal() throws LifecycleException { + + fireLifecycleEvent(CONFIGURE_START_EVENT, null); + setState(LifecycleState.STARTING); + + globalNamingResources.start(); + + // Start our defined Services + synchronized (servicesLock) { + for (int i = 0; i < services.length; i++) { + services[i].start(); + } + } +} + - 3.1.1.1) StandardService#start +- protected void startInternal() throws LifecycleException { + + if(log.isInfoEnabled()) + log.info(sm.getString("standardService.start.name", this.name)); + setState(LifecycleState.STARTING); + + // Start our defined Container first + if (engine != null) { + synchronized (engine) { + engine.start(); + } + } + + synchronized (executors) { + for (Executor executor: executors) { + executor.start(); + } + } + + mapperListener.start(); + + // Start our defined Connectors second + synchronized (connectorsLock) { + for (Connector connector: connectors) { + // If it has already failed, don't try and start it + if (connector.getState() != LifecycleState.FAILED) { + connector.start(); + } + } + } +} + - 3.1.1.1.1) StandardEngine#start + + +- protected synchronized void startInternal() throws LifecycleException { + + // Log our server identification information + if(log.isInfoEnabled()) + log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo()); + + // Standard container startup + super.startInternal(); +} + + + - 3.1.1.1.1.1) ContainerBase#startInternal +- protected synchronized void startInternal() throws LifecycleException { + + // Start our subordinate components, if any + logger = null; + getLogger(); + Cluster cluster = getClusterInternal(); + if (cluster instanceof Lifecycle) { + ((Lifecycle) cluster).start(); + } + Realm realm = getRealmInternal(); + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).start(); + } + + // Start our child containers, if any + Container children[] = findChildren(); + List<Future<Void>> results = new ArrayList<>(); + for (int i = 0; i < children.length; i++) { + results.add(startStopExecutor.submit(new StartChild(children[i]))); + } + + boolean fail = false; + for (Future<Void> result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("containerBase.threadedStartFailed"), e); + fail = true; + } + + } + if (fail) { + throw new LifecycleException( + sm.getString("containerBase.threadedStartFailed")); + } + + // Start the Valves in our pipeline (including the basic), if any + if (pipeline instanceof Lifecycle) + ((Lifecycle) pipeline).start(); + + + setState(LifecycleState.STARTING); + + // Start our thread + threadStart(); + +} + - 3.1.1.1.1.1.1) StandardHost#start +- protected synchronized void startInternal() throws LifecycleException { + + // Set error report valve + String errorValve = getErrorReportValveClass(); + if ((errorValve != null) && (!errorValve.equals(""))) { + try { + boolean found = false; + Valve[] valves = getPipeline().getValves(); + for (Valve valve : valves) { + if (errorValve.equals(valve.getClass().getName())) { + found = true; + break; + } + } + if(!found) { + Valve valve = + (Valve) Class.forName(errorValve).getConstructor().newInstance(); + getPipeline().addValve(valve); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString( + "standardHost.invalidErrorReportValveClass", + errorValve), t); + } + } + super.startInternal(); +} + + - 3.1.1.1.1.1.1.1) StandardContext#start(会初始化loadOnStartup的servlet) + - protected synchronized void startInternal() throws LifecycleException { + + if(log.isDebugEnabled()) + log.debug("Starting " + getBaseName()); + + // Send j2ee.state.starting notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.starting", + this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + setConfigured(false); + boolean ok = true; + + // Currently this is effectively a NO-OP but needs to be called to + // ensure the NamingResources follows the correct lifecycle + if (namingResources != null) { + namingResources.start(); + } + + // Post work directory + postWorkDirectory(); + + // Add missing components as necessary + if (getResources() == null) { // (1) Required by Loader + if (log.isDebugEnabled()) + log.debug("Configuring default Resources"); + + try { + setResources(new StandardRoot(this)); + } catch (IllegalArgumentException e) { + log.error(sm.getString("standardContext.resourcesInit"), e); + ok = false; + } + } + if (ok) { + resourcesStart(); + } + // 初始化WebappLoader + if (getLoader() == null) { + WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); + webappLoader.setDelegate(getDelegate()); + setLoader(webappLoader); + } + + // An explicit cookie processor hasn't been specified; use the default + if (cookieProcessor == null) { + cookieProcessor = new Rfc6265CookieProcessor(); + } + + // Initialize character set mapper + getCharsetMapper(); + + // Validate required extensions + boolean dependencyCheck = true; + try { + dependencyCheck = ExtensionValidator.validateApplication + (getResources(), this); + } catch (IOException ioe) { + log.error(sm.getString("standardContext.extensionValidationError"), ioe); + dependencyCheck = false; + } + + if (!dependencyCheck) { + // do not make application available if dependency check fails + ok = false; + } + + // Reading the "catalina.useNaming" environment variable + String useNamingProperty = System.getProperty("catalina.useNaming"); + if ((useNamingProperty != null) + && (useNamingProperty.equals("false"))) { + useNaming = false; + } + + if (ok && isUseNaming()) { + if (getNamingContextListener() == null) { + NamingContextListener ncl = new NamingContextListener(); + ncl.setName(getNamingContextName()); + ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite()); + addLifecycleListener(ncl); + setNamingContextListener(ncl); + } + } + + // Standard container startup + if (log.isDebugEnabled()) + log.debug("Processing standard container startup"); + + + // Binding thread + ClassLoader oldCCL = bindThread(); + + try { + if (ok) { + // Start our subordinate components, if any + Loader loader = getLoader(); + if (loader instanceof Lifecycle) { + +``` +// 启动WebappClassLoader + ((Lifecycle) loader).start(); + } + + // since the loader just started, the webapp classloader is now + // created. + setClassLoaderProperty("clearReferencesRmiTargets", + getClearReferencesRmiTargets()); + setClassLoaderProperty("clearReferencesStopThreads", + getClearReferencesStopThreads()); + setClassLoaderProperty("clearReferencesStopTimerThreads", + getClearReferencesStopTimerThreads()); + setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread", + getClearReferencesHttpClientKeepAliveThread()); + + // By calling unbindThread and bindThread in a row, we setup the + // current Thread CCL to be the webapp classloader + unbindThread(oldCCL); + oldCCL = bindThread(); + + // Initialize logger again. Other components might have used it + // too early, so it should be reset. + logger = null; + getLogger(); + + Realm realm = getRealmInternal(); + if(null != realm) { + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).start(); + } + + // Place the CredentialHandler into the ServletContext so + // applications can have access to it. Wrap it in a "safe" + // handler so application's can't modify it. + CredentialHandler safeHandler = new CredentialHandler() { + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials); + } + + @Override + public String mutate(String inputCredentials) { + return getRealmInternal().getCredentialHandler().mutate(inputCredentials); + } + }; + context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler); + } + + // Notify our interested LifecycleListeners + fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null); + + // Start our child containers, if not already started + for (Container child : findChildren()) { + if (!child.getState().isAvailable()) { + child.start(); + } + } + + // Start the Valves in our pipeline (including the basic), + // if any + if (pipeline instanceof Lifecycle) { + ((Lifecycle) pipeline).start(); + } + + // Acquire clustered manager + Manager contextManager = null; + Manager manager = getManager(); + if (manager == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.cluster.noManager", + Boolean.valueOf((getCluster() != null)), + Boolean.valueOf(distributable))); + } + if ( (getCluster() != null) && distributable) { + try { + contextManager = getCluster().createManager(getName()); + } catch (Exception ex) { + log.error("standardContext.clusterFail", ex); + ok = false; + } + } else { + contextManager = new StandardManager(); + } + } + + // Configure default manager if none was specified + if (contextManager != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.manager", + contextManager.getClass().getName())); + } + setManager(contextManager); + } + + if (manager!=null && (getCluster() != null) && distributable) { + //let the cluster know that there is a context that is distributable + //and that it has its own manager + getCluster().registerManager(manager); + } + } + + if (!getConfigured()) { + log.error(sm.getString("standardContext.configurationFail")); + ok = false; + } + + // We put the resources into the servlet context + if (ok) + getServletContext().setAttribute + (Globals.RESOURCES_ATTR, getResources()); + + if (ok ) { + if (getInstanceManager() == null) { + javax.naming.Context context = null; + if (isUseNaming() && getNamingContextListener() != null) { + context = getNamingContextListener().getEnvContext(); + } + Map<String, Map<String, String>> injectionMap = buildInjectionMap( + getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources()); + setInstanceManager(new DefaultInstanceManager(context, + injectionMap, this, this.getClass().getClassLoader())); + } + getServletContext().setAttribute( + InstanceManager.class.getName(), getInstanceManager()); + InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager()); + } + + // Create context attributes that will be required + if (ok) { + getServletContext().setAttribute( + JarScanner.class.getName(), getJarScanner()); + } + + // Set up the context init params + mergeParameters(); + + // Call ServletContainerInitializers + for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : + initializers.entrySet()) { + try { + entry.getKey().onStartup(entry.getValue(), + getServletContext()); + } catch (ServletException e) { + log.error(sm.getString("standardContext.sciFail"), e); + ok = false; + break; + } + } + + // Configure and call application event listeners + if (ok) { + if (!listenerStart()) { + log.error(sm.getString("standardContext.listenerFail")); + ok = false; + } + } + + // Check constraints for uncovered HTTP methods + // Needs to be after SCIs and listeners as they may programmatically + // change constraints + if (ok) { + checkConstraintsForUncoveredMethods(findConstraints()); + } + + try { + // Start manager + Manager manager = getManager(); + if (manager instanceof Lifecycle) { + ((Lifecycle) manager).start(); + } + } catch(Exception e) { + log.error(sm.getString("standardContext.managerFail"), e); + ok = false; + } + + // Configure and call application filters + if (ok) { + if (!filterStart()) { + log.error(sm.getString("standardContext.filterFail")); + ok = false; + } + } + + // Load and initialize all "load on startup" servlets + if (ok) { + if (!loadOnStartup(findChildren())){ + log.error(sm.getString("standardContext.servletFail")); + ok = false; + } + } + + // Start ContainerBackgroundProcessor thread + super.threadStart(); + } finally { + // Unbinding thread + unbindThread(oldCCL); + } + + // Set available status depending upon startup success + if (ok) { + if (log.isDebugEnabled()) + log.debug("Starting completed"); + } else { + log.error(sm.getString("standardContext.startFailed", getName())); + } + + startTime=System.currentTimeMillis(); + + // Send j2ee.state.running notification + if (ok && (this.getObjectName() != null)) { + Notification notification = + new Notification("j2ee.state.running", this.getObjectName(), + sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + // The WebResources implementation caches references to JAR files. On + // some platforms these references may lock the JAR files. Since web + // application start is likely to have read from lots of JARs, trigger + // a clean-up now. + getResources().gc(); + + // Reinitializing if something went wrong + if (!ok) { + setState(LifecycleState.FAILED); + } else { + setState(LifecycleState.STARTING); + } +} +``` + + + - 3.1.1.1.1.1.1.1.1) StandardWrapper#start +- protected synchronized void startInternal() throws LifecycleException { + + // Send j2ee.state.starting notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.starting", + this.getObjectName(), + sequenceNumber++); + broadcaster.sendNotification(notification); + } + + // Start up this component + super.startInternal(); + + setAvailable(0L); + + // Send j2ee.state.running notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.state.running", this.getObjectName(), + sequenceNumber++); + broadcaster.sendNotification(notification); + } + +} + + - 3.1.1.1.1.1.1.1.2) WebappLoader#constructor +- if (getLoader() == null) { + WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); + webappLoader.setDelegate(getDelegate()); + setLoader(webappLoader); + } + + +``` +/** + * Construct a new WebappLoader with the specified class loader + * to be defined as the parent of the ClassLoader we ultimately create. + * + * @param parent The parent class loader + */ +public WebappLoader(ClassLoader parent) { + super(); + this.parentClassLoader = parent; +} +``` + + + + +``` +@Override +public void setLoader(Loader loader) { + + Lock writeLock = loaderLock.writeLock(); + writeLock.lock(); + Loader oldLoader = null; + try { + // Change components if necessary + oldLoader = this.loader; + if (oldLoader == loader) + return; + this.loader = loader; + + // Stop the old component if necessary + if (getState().isAvailable() && (oldLoader != null) && + (oldLoader instanceof Lifecycle)) { + try { + ((Lifecycle) oldLoader).stop(); + } catch (LifecycleException e) { + log.error("StandardContext.setLoader: stop: ", e); + } + } + + // Start the new component if necessary + if (loader != null) + loader.setContext(this); + if (getState().isAvailable() && (loader != null) && + (loader instanceof Lifecycle)) { + try { + ((Lifecycle) loader).start(); + } catch (LifecycleException e) { + log.error("StandardContext.setLoader: start: ", e); + } + } + } finally { + writeLock.unlock(); + } + + // Report this property change to interested listeners + support.firePropertyChange("loader", oldLoader, loader); +} +``` + + + - 3.1.1.1.1.1.1.1.3) WebappLoader#start +- protected void startInternal() throws LifecycleException { + + if (log.isDebugEnabled()) + log.debug(sm.getString("webappLoader.starting")); + + if (context.getResources() == null) { + log.info("No resources for " + context); + setState(LifecycleState.STARTING); + return; + } + + // Construct a class loader based on our current repositories list + try { + + classLoader = createClassLoader(); + classLoader.setResources(context.getResources()); + classLoader.setDelegate(this.delegate); + + // Configure our repositories + setClassPath(); + + setPermissions(); + + classLoader.start(); + + String contextName = context.getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + ObjectName cloname = new ObjectName(context.getDomain() + ":type=" + + classLoader.getClass().getSimpleName() + ",host=" + + context.getParent().getName() + ",context=" + contextName); + Registry.getRegistry(null, null) + .registerComponent(classLoader, cloname, null); + + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.error( "LifecycleException ", t ); + throw new LifecycleException("start: ", t); + } + + setState(LifecycleState.STARTING); +} + - 3.1.1.1.1.1.1.1.3.1) WebappLoader#createClassLoader + +``` +private WebappClassLoaderBase createClassLoader() + throws Exception { + + Class<?> clazz = Class.forName(loaderClass); + WebappClassLoaderBase classLoader = null; + // parentClassLoader实际就是sharedLoader,即org.apache.catalina.loader.StandardClassLoader + if (parentClassLoader == null) { + parentClassLoader = context.getParentClassLoader(); + } + Class<?>[] argTypes = { ClassLoader.class }; + Object[] args = { parentClassLoader }; + Constructor<?> constr = clazz.getConstructor(argTypes); + classLoader = (WebappClassLoaderBase) constr.newInstance(args); + + return classLoader; +} +``` + + + + - 3.1.1.1.1.1.2) StandardPipeline#start +- protected synchronized void startInternal() throws LifecycleException { + + // Start the Valves in our pipeline (including the basic), if any + Valve current = first; + if (current == null) { + current = basic; + } + while (current != null) { + if (current instanceof Lifecycle) + ((Lifecycle) current).start(); + current = current.getNext(); + } + + setState(LifecycleState.STARTING); +} + + - 3.1.1.1.1.1.2) ContainerBase#threadStart(启动后台线程,检查session过期) + +``` +/** + * Start the background thread that will periodically check for + * session timeouts. + */ +protected void threadStart() { + + if (thread != null) + return; + if (backgroundProcessorDelay <= 0) + return; + + threadDone = false; + String threadName = "ContainerBackgroundProcessor[" + toString() + "]"; + thread = new Thread(new ContainerBackgroundProcessor(), threadName); + thread.setDaemon(true); + thread.start(); + +} +``` + + + - 3.1.1.1.2) Connector#start +- protected void startInternal() throws LifecycleException { + + // Validate settings before starting + if (getPort() < 0) { + throw new LifecycleException(sm.getString( + "coyoteConnector.invalidPort", Integer.valueOf(getPort()))); + } + + setState(LifecycleState.STARTING); + + try { + protocolHandler.start(); + } catch (Exception e) { + throw new LifecycleException( + sm.getString("coyoteConnector.protocolHandlerStartFailed"), e); + } +} + + - 3.1.1.1.2.1) AbstractProtocol#start + +``` +public void start() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.start", getName())); + } + + endpoint.start(); + + // Start async timeout thread + asyncTimeout = new AsyncTimeout(); + Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout"); + int priority = endpoint.getThreadPriority(); + if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) { + priority = Thread.NORM_PRIORITY; + } + timeoutThread.setPriority(priority); + timeoutThread.setDaemon(true); + timeoutThread.start(); +} +``` + + - 3.1.1.1.2.1.1) NioEndpoint#start + +``` +public void startInternal() throws Exception { + + if (!running) { + running = true; + paused = false; + + processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getProcessorCache()); + eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getEventCache()); + nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getBufferPool()); + + // Create worker collection + if ( getExecutor() == null ) { + createExecutor(); + } + + initializeConnectionLatch(); + + // Start poller threads + pollers = new Poller[getPollerThreadCount()]; + for (int i=0; i<pollers.length; i++) { + pollers[i] = new Poller(); + Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i); + pollerThread.setPriority(threadPriority); + pollerThread.setDaemon(true); + pollerThread.start(); + } + + startAcceptorThreads(); + } +} +``` + + - 3.1.1.1.2.1.1.1) NioEndpoint#createExecutor(创建Worker线程池) +- 用于创建 Worker 线程池。默认会启动 10 个 Worker 线程,Tomcat 处理请求过程中,Woker 最多不超过 200 个。我们可以通过配置 conf/server.xml 中 Connector 的 minSpareThreads 和 maxThreads 对这两个属性进行定制。 + +``` +public void createExecutor() { + internalExecutor = true; + TaskQueue taskqueue = new TaskQueue(); + TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); + executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf); + taskqueue.setParent( (ThreadPoolExecutor) executor); +} +``` + + + - 3.1.1.1.2.1.1.1.1) ThreadPoolExecutor#constructor(启动Worker) + +``` +public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectHandler()); + prestartAllCoreThreads(); +} +``` + + + +``` +public int prestartAllCoreThreads() { + int n = 0; + while (addWorker(null, true)) + ++n; + return n; +} +``` + + + +``` +private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + int wc = workerCountOf(c); + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + if (compareAndIncrementWorkerCount(c)) + break retry; + c = ctl.get(); // Re-read ctl + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + + boolean workerStarted = false; + boolean workerAdded = false; + Worker w = null; + try { + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // Recheck while holding lock. + // Back out on ThreadFactory failure or if + // shut down before lock acquired. + int rs = runStateOf(ctl.get()); + + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + workerAdded = true; + } + } finally { + mainLock.unlock(); + } + if (workerAdded) { + t.start(); + workerStarted = true; + } + } + } finally { + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; +} +``` + + +- 每个Worker线程启动是一个后台线程完成的。 + + - 3.1.1.1.1.1.1.2) Poller(Runnable)#run(启动Poller) +- 以守护线程的方式运行。 +- 用于检测已就绪的 Socket。 默认最多不超过 2 个, +- Math.min(2,Runtime.getRuntime().availableProcessors());。我们可以通过配置 pollerThreadCount 来定制。 + - 3.1.1.1.1.1.1.3) NioEndpoint#startAcceptorThreads(启动Acceptors) +- Acceptors以后台线程方式运行 +- 用于接受新连接。默认是 1 个。我们可以通过配置 acceptorThreadCount 对其进行定制。 +- protected final void startAcceptorThreads() { + int count = getAcceptorThreadCount(); + acceptors = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + Acceptor<U> acceptor = new Acceptor<>(this); + String threadName = getName() + "-Acceptor-" + i; + acceptor.setThreadName(threadName); + acceptors.add(acceptor); + Thread t = new Thread(acceptor, threadName); + t.setPriority(getAcceptorThreadPriority()); + t.setDaemon(getDaemon()); + t.start(); + } +} + + - 3.1.2) StandardServer#await + +``` +public void await() { + // Negative values - don't wait on port - tomcat is embedded or we just don't like ports + if( port == -2 ) { + // undocumented yet - for embedding apps that are around, alive. + return; + } + if( port==-1 ) { + try { + awaitThread = Thread.currentThread(); + while(!stopAwait) { + try { + Thread.sleep( 10000 ); + } catch( InterruptedException ex ) { + // continue and check the flag + } + } + } finally { + awaitThread = null; + } + return; + } + + // Set up a server socket to wait on + try { + awaitSocket = new ServerSocket(port, 1, + InetAddress.getByName(address)); + } catch (IOException e) { + log.error("StandardServer.await: create[" + address + + ":" + port + + "]: ", e); + return; + } + + try { + awaitThread = Thread.currentThread(); + + // Loop waiting for a connection and a valid command + while (!stopAwait) { + ServerSocket serverSocket = awaitSocket; + if (serverSocket == null) { + break; + } + + // Wait for the next connection + Socket socket = null; + StringBuilder command = new StringBuilder(); + try { + InputStream stream; + long acceptStartTime = System.currentTimeMillis(); + try { + socket = serverSocket.accept(); + socket.setSoTimeout(10 * 1000); // Ten seconds + stream = socket.getInputStream(); + } catch (SocketTimeoutException ste) { + // This should never happen but bug 56684 suggests that + // it does. + log.warn(sm.getString("standardServer.accept.timeout", + Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste); + continue; + } catch (AccessControlException ace) { + log.warn("StandardServer.accept security exception: " + + ace.getMessage(), ace); + continue; + } catch (IOException e) { + if (stopAwait) { + // Wait was aborted with socket.close() + break; + } + log.error("StandardServer.await: accept: ", e); + break; + } + + // Read a set of characters from the socket + int expected = 1024; // Cut off to avoid DoS attack + while (expected < shutdown.length()) { + if (random == null) + random = new Random(); + expected += (random.nextInt() % 1024); + } + while (expected > 0) { + int ch = -1; + try { + ch = stream.read(); + } catch (IOException e) { + log.warn("StandardServer.await: read: ", e); + ch = -1; + } + // Control character or EOF (-1) terminates loop + if (ch < 32 || ch == 127) { + break; + } + command.append((char) ch); + expected--; + } + } finally { + // Close the socket now that we are done with it + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + // Ignore + } + } + + // Match against our command string + boolean match = command.toString().equals(shutdown); + if (match) { + log.info(sm.getString("standardServer.shutdownViaPort")); + break; + } else + log.warn("StandardServer.await: Invalid command '" + + command.toString() + "' received"); + } + } finally { + ServerSocket serverSocket = awaitSocket; + awaitThread = null; + awaitSocket = null; + + // Close the server socket and return + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + // Ignore + } + } + } +} +``` + +# 停止 +- catalinaDaemon调用await等待停止命令,我们一般是通过执行tomcat/bin/shutdown.sh来关闭Tomcat,等价于执行org.apache.catalina.startup.Bootstra类的main方法,并传入stop参数。 +- 逻辑: +1.- 新建Bootstrap对象daemon,并调用其init()方法 +2.- 初始化Tomcat的类加载器 +3.- 用反射实例化org.apache.catalina.startup.Catalina对象catalinaDaemon +4.- 调用daemon的stopServer方法,实质上调用了catalinaDaemon的stopServer方法 +5.- 解析server.xml文件,构造出Server容器 +6.- 获取Server的socket监听端口和地址,创建Socket对象连接启动Tomcat时创建的ServerSocket,最后向ServerSocket发送SHUTDOWN命令 +7.- 运行中的Server调用stop方法停止 + +- BootStrap#stopServer + +``` +public void stopServer(String[] arguments) + throws Exception { + + Object param[]; + Class<?> paramTypes[]; + if (arguments==null || arguments.length==0) { + paramTypes = null; + param = null; + } else { + paramTypes = new Class[1]; + paramTypes[0] = arguments.getClass(); + param = new Object[1]; + param[0] = arguments; + } + Method method = + catalinaDaemon.getClass().getMethod("stopServer", paramTypes); + method.invoke(catalinaDaemon, param); + +} +``` + + - 1) Catalina#stopServer + +``` +public void stopServer() { + stopServer(null); +} +``` + + + +``` +public void stopServer(String[] arguments) { + + if (arguments != null) { + arguments(arguments); + } + + Server s = getServer(); + if (s == null) { + // Create and execute our Digester + Digester digester = createStopDigester(); + File file = configFile(); + try (FileInputStream fis = new FileInputStream(file)) { + InputSource is = + new InputSource(file.toURI().toURL().toString()); + is.setByteStream(fis); + digester.push(this); + digester.parse(is); + } catch (Exception e) { + log.error("Catalina.stop: ", e); + System.exit(1); + } + } else { + // Server object already present. Must be running as a service + try { + s.stop(); + } catch (LifecycleException e) { + log.error("Catalina.stop: ", e); + } + return; + } + + // Stop the existing server + s = getServer(); + if (s.getPort()>0) { + try (Socket socket = new Socket(s.getAddress(), s.getPort()); + OutputStream stream = socket.getOutputStream()) { + String shutdown = s.getShutdown(); + for (int i = 0; i < shutdown.length(); i++) { + stream.write(shutdown.charAt(i)); + } + stream.flush(); + } catch (ConnectException ce) { + log.error(sm.getString("catalina.stopServer.connectException", + s.getAddress(), + String.valueOf(s.getPort()))); + log.error("Catalina.stop: ", ce); + System.exit(1); + } catch (IOException e) { + log.error("Catalina.stop: ", e); + System.exit(1); + } + } else { + log.error(sm.getString("catalina.stopServer")); + System.exit(1); + } +} +``` + + + +- + +# 请求处理 +# Connector +- 在Tomcat9中,Connector支持的协议是HTTP和AJP,协议处理类分别对应org.apache.coyote.http11.Http11NioProtocol和 +- org.apache.coyote.http11.Http11AprProtocol(已经取消BIO模式)。 +- Connector主要包含三个模块:Http11NioProtocol, Mapper, CoyoteAdapter,http请求在Connector中的流程如下: + +1.- Acceptor为监听线程,调用serverSocketAccept()阻塞,本质上调用ServerSocketChannel.accept() +2.- Acceptor将接收到的Socket添加到Poller池中的一个Poller +3.- Poller通过worker线程把socket包装成SocketProcessor +4.- SocketProcessor调用getHandler()获取对应的ConnectionHandler +5.- ConnectionHandler把socket交由Http11Processor处理,解析http的Header和Body +6.- Http11Processor调用service()把包装好的request和response传给CoyoteAdapter +7.- CoyoteAdapter会通过Mapper把请求对应的session、servlet等关联好,准备传给Container + + +# Container +- 有4个Container,采用了责任链的设计模式。 +- Pipeline就像是每个容器的逻辑总线,在Pipeline上按照配置的顺序,加载各个Valve。通过Pipeline完成各个Valve之间的调用,各个Valve实现具体的应用逻辑。 +- 每个请求在pipeline上流动,经过每个Container(对应着一个或多个Valve阀门),各个Container按顺序处理请求,最终在Wrapper结束。 +- +- Connector中的CoyoteAdapter会调用invoke()把request和response传给Container,Container中依次调用各个Valve,每个Valve的作用如下: +1.- StandardEngineValve:StandardEngine中的唯一阀门,主要用于从request中选择其host映射的Host容器StandardHost +2.- AccessLogValve:StandardHost中的第一个阀门,主要用于管道执行结束之后记录日志信息 +3.- ErrorReportValve:StandardHost中紧跟AccessLogValve的阀门,主要用于管道执行结束后,从request对象中获取异常信息,并封装到response中以便将问题展现给访问者 +4.- StandardHostValve: StandardHost中最后的阀门,主要用于从request中选择其context映射的Context容器StandardContext以及访问request中的Session以更新会话的最后访问时间 +5.- StandardContextValve:StandardContext中的唯一阀门,主要作用是禁止任何对WEB-INF或META-INF目录下资源的重定向访问,对应用程序热部署功能的实现,从request中获得StandardWrapper +6.- StandardWrapperValve:StandardWrapper中的唯一阀门,主要作用包括调用StandardWrapper的loadServlet方法生成Servlet实例和调用ApplicationFilterFactory生成Filter链 + +- 最终将Response返回给Connector完成一次http的请求。 + +- + +# NioEndPoint职责 +- 在Tomcat中Endpoint主要用来接收网络请求,处理则由ConnectionHandler来执行。 + +- 包含了三个组件: +- Acceptor:后台线程,负责监听请求,将接收到的Socket请求放到Poller队列中 +- Poller:后台线程,当Socket就绪时,将Poller队列中的Socket交给Worker线程池处理 +- SocketProcessor(Worker):处理socket,本质上委托ConnectionHandler处理 + +- Connector 启动以后会启动一组线程用于不同阶段的请求处理过程。 +- Acceptor 线程组。用于接受新连接,并将新连接封装一下,选择一个 Poller 将新连接添加到 Poller 的事件队列中。 +- Poller 线程组。用于监听 Socket 事件,当 Socket 可读或可写等等时,将 Socket 封装一下添加到 worker 线程池的任务队列中。 +- worker 线程组。用于对请求进行处理,包括分析请求报文并创建 Request 对象,调用容器的 pipeline 进行处理。 + +- Acceptor、Poller、worker 所在的 ThreadPoolExecutor 都维护在 NioEndpoint 中。 + +- 这种模式类似于Reactor的主从多线程方式。 +- + + - 1) Acceptor#run(BIO,阻塞接收Socket连接,mainReactor) + +1.- Acceptor 在启动后会阻塞在 ServerSocketChannel.accept(); 方法处,当有新连接到达时,该方法返回一个 SocketChannel。 +2. - 配置完 Socket 以后将 Socket 封装到 NioChannel 中,并注册到 Poller,值的一提的是,我们一开始就启动了多个 Poller 线程,注册的时候,连接是公平的分配到每个 Poller 的。NioEndpoint 维护了一个 Poller 数组,当一个连接分配给 pollers[index] 时,下一个连接就会分配给 pollers[(index+1)%pollers.length]. +3.- addEvent() 方法会将 Socket 添加到该 Poller 的 PollerEvent 队列中。到此 Acceptor 的任务就完成了。 + +- 持有Endpoint + +``` +private final AbstractEndpoint<?,U> endpoint; +``` + +- 在启动后会阻塞在 ServerSocketChannel.accept(); 方法处,当有新连接到达时,该方法返回一个 SocketChannel。 + +``` +public void run() { + + int errorDelay = 0; + + // Loop until we receive a shutdown command + while (endpoint.isRunning()) { + + // Loop if endpoint is paused + while (endpoint.isPaused() && endpoint.isRunning()) { + state = AcceptorState.PAUSED; + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore + } + } + + if (!endpoint.isRunning()) { + break; + } + state = AcceptorState.RUNNING; + + try { + //if we have reached max connections, wait + endpoint.countUpOrAwaitConnection(); + + // Endpoint might have been paused while waiting for latch + // If that is the case, don't accept new connections + if (endpoint.isPaused()) { + continue; + } + + U socket = null; + try { + // Accept the next incoming connection from the server + // socket + socket = endpoint.serverSocketAccept(); + } catch (Exception ioe) { + // We didn't get a socket + endpoint.countDownConnection(); + if (endpoint.isRunning()) { + // Introduce delay if necessary + errorDelay = handleExceptionWithDelay(errorDelay); + // re-throw + throw ioe; + } else { + break; + } + } + // Successful accept, reset the error delay + errorDelay = 0; + + // Configure the socket + if (endpoint.isRunning() && !endpoint.isPaused()) { + // setSocketOptions() will hand the socket off to + // an appropriate processor if successful + if (!endpoint.setSocketOptions(socket)) { + endpoint.closeSocket(socket); + } + } else { + endpoint.destroySocket(socket); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("endpoint.accept.fail"); + // APR specific. + // Could push this down but not sure it is worth the trouble. + if (t instanceof Error) { + Error e = (Error) t; + if (e.getError() == 233) { + // Not an error on HP-UX so log as a warning + // so it can be filtered out on that platform + // See bug 50273 + log.warn(msg, t); + } else { + log.error(msg, t); + } + } else { + log.error(msg, t); + } + } + } + state = AcceptorState.ENDED; +} +``` + + + - 1.1) NioEndpoint#setSocketOptions(处理Socket) + - 配置完 Socket 以后将 Socket 封装到 NioChannel 中,并注册到 Poller,值的一提的是,我们一开始就启动了多个 Poller 线程,注册的时候,连接是公平的分配到每个 Poller 的。NioEndpoint 维护了一个 Poller 数组,当一个连接分配给 pollers[index] 时,下一个连接就会分配给 pollers[(index+1)%pollers.length]. +- protected boolean setSocketOptions(SocketChannel socket) { + // Process the connection + try { + //disable blocking, APR style, we are gonna be polling it + socket.configureBlocking(false); + Socket sock = socket.socket(); + socketProperties.setProperties(sock); + + NioChannel channel = nioChannels.pop(); + if (channel == null) { + SocketBufferHandler bufhandler = new SocketBufferHandler( + socketProperties.getAppReadBufSize(), + socketProperties.getAppWriteBufSize(), + socketProperties.getDirectBuffer()); + if (isSSLEnabled()) { + channel = new SecureNioChannel(socket, bufhandler, selectorPool, this); + } else { + channel = new NioChannel(socket, bufhandler); + } + } else { + channel.setIOChannel(socket); + channel.reset(); + } + getPoller0().register(channel); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + log.error("",t); + } catch (Throwable tt) { + ExceptionUtils.handleThrowable(tt); + } + // Tell to close the socket + return false; + } + return true; +} + + +``` +public Poller getPoller0() { + int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length; + return pollers[idx]; +} +``` + + + - 1.1.1) Poller#register(将Socket放入Poller队列) +- addEvent() 方法会将 Socket 添加到该 Poller 的 PollerEvent 队列中。到此 Acceptor 的任务就完成了。 + +``` +public void register(final NioChannel socket) { + socket.setPoller(this); + NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this); + socket.setSocketWrapper(ka); + ka.setPoller(this); + ka.setReadTimeout(getConnectionTimeout()); + ka.setWriteTimeout(getConnectionTimeout()); + ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests()); + ka.setSecure(isSSLEnabled()); + PollerEvent r = eventCache.pop(); + ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into. + if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER); + else r.reset(socket,ka,OP_REGISTER); + addEvent(r); +} +``` + + - 1.1.1.1) NioSocketWrapper#constructor(持有NioEndpoint的SelectorPool) + +``` +public NioSocketWrapper(NioChannel channel, NioEndpoint endpoint) { + super(channel, endpoint); + pool = endpoint.getSelectorPool(); + socketBufferHandler = channel.getBufHandler(); +} +``` + + + +``` +public SocketWrapperBase(E socket, AbstractEndpoint<E> endpoint) { + this.socket = socket; + this.endpoint = endpoint; + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + this.blockingStatusReadLock = lock.readLock(); + this.blockingStatusWriteLock = lock.writeLock(); +} +``` + + + - 1.1.1.2) Poller#addEvent + +``` +private void addEvent(PollerEvent event) { + events.offer(event); + if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup(); +} +``` + + + +``` +private final SynchronizedQueue<PollerEvent> events = + new SynchronizedQueue<>(); +``` + +- + + - 2) Poller#run(NIO,把队列中的就绪的Socket封装为SocketProcessor交给Worker线程池,subReactor) + +1.- selector.select(1000)。当 Poller 启动后因为 selector 中并没有已注册的 Channel,所以当执行到该方法时只能阻塞。所有的 Poller 共用一个 Selector,其实现类是 sun.nio.ch.EPollSelectorImpl +2.- events() 方法会将通过 addEvent() 方法添加到事件队列中的 Socket 注册到 EPollSelectorImpl,当 Socket 可读时,Poller 才对其进行处理 +3.- createSocketProcessor() 方法将 Socket 封装到 SocketProcessor 中,SocketProcessor 实现了 Runnable 接口。worker 线程通过调用其 run() 方法来对 Socket 进行处理。 +4.- execute(SocketProcessor) 方法将 SocketProcessor 提交到线程池,放入线程池的 workQueue 中。workQueue 是 BlockingQueue 的实例。到此 Poller 的任务就完成了。 + +- 调用selector的select()函数,监听就绪事件 +- 根据向selector中注册的key遍历channel中已经就绪的keys,并处理key +- 处理key对应的channel,调用NioEndPoint的processSocket() +- 从SocketProcessor池中取出空闲的SocketProcessor,关联socketWrapper,提交运行SocketProcessor + +``` +public Poller() throws IOException { + this.selector = Selector.open(); +} +``` + + +- 它的selector是初始化时开启的,每个Poller对应着自己的Selector,监听该Poller对应的SocketChannel的Read事件。当Poller队列中加入新的Socket时,会将Socket注册在selector上,这样selector就可以监测socket就绪事件了。 + + +``` +public void run() { + // Loop until destroy() is called + while (true) { + + boolean hasEvents = false; + + try { + if (!close) { + hasEvents = events(); + if (wakeupCounter.getAndSet(-1) > 0) { + //if we are here, means we have other stuff to do + //do a non blocking select + keyCount = selector.selectNow(); + } else { + keyCount = selector.select(selectorTimeout); + } + wakeupCounter.set(0); + } + if (close) { + events(); + timeout(0, false); + try { + selector.close(); + } catch (IOException ioe) { + log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe); + } + break; + } + } catch (Throwable x) { + ExceptionUtils.handleThrowable(x); + log.error("",x); + continue; + } + //either we timed out or we woke up, process events first + if ( keyCount == 0 ) hasEvents = (hasEvents | events()); + + Iterator<SelectionKey> iterator = + keyCount > 0 ? selector.selectedKeys().iterator() : null; + // Walk through the collection of ready keys and dispatch + // any active event. + while (iterator != null && iterator.hasNext()) { + SelectionKey sk = iterator.next(); + NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment(); + // Attachment may be null if another thread has called + // cancelledKey() + if (attachment == null) { + iterator.remove(); + } else { + // 有Socket出现读事件 + iterator.remove(); + processKey(sk, attachment); + } + }//while + + //process timeouts + timeout(keyCount,hasEvents); + }//while + + getStopLatch().countDown(); +} +``` + + + - 2.1) Poller#events(将队列中的Socket注册到Selector) +- events() 方法会将通过 addEvent() 方法添加到事件队列中的 Socket 注册到 EPollSelectorImpl,当 Socket 可读时,Poller 才对其进行处理。 + +``` +public boolean events() { + boolean result = false; + + PollerEvent pe = null; + for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { + result = true; + try { + pe.run(); + pe.reset(); + if (running && !paused) { + eventCache.push(pe); + } + } catch ( Throwable x ) { + log.error("",x); + } + } + + return result; +} +``` + + + - 2.1.1) PollerEvent#run(注册到Selector) + + +``` +public void run() { + if (interestOps == OP_REGISTER) { + try { + socket.getIOChannel().register( + socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper); + } catch (Exception x) { + log.error(sm.getString("endpoint.nio.registerFail"), x); + } + } else { + final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); + try { + if (key == null) { + // The key was cancelled (e.g. due to socket closure) + // and removed from the selector while it was being + // processed. Count down the connections at this point + // since it won't have been counted down when the socket + // closed. + socket.socketWrapper.getEndpoint().countDownConnection(); + } else { + final NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment(); + if (socketWrapper != null) { + //we are registering the key to start with, reset the fairness counter. + int ops = key.interestOps() | interestOps; + socketWrapper.interestOps(ops); + key.interestOps(ops); + } else { + socket.getPoller().cancelledKey(key); + } + } + } catch (CancelledKeyException ckx) { + try { + socket.getPoller().cancelledKey(key); + } catch (Exception ignore) {} + } + } +} +``` + + + - 2.2) Poller#processKey(将就绪的Socket交给线程池) +- protected void processKey(SelectionKey sk, NioSocketWrapper attachment) { + try { + if ( close ) { + cancelledKey(sk); + } else if ( sk.isValid() && attachment != null ) { + if (sk.isReadable() || sk.isWritable() ) { + if ( attachment.getSendfileData() != null ) { + processSendfile(sk,attachment, false); + } else { + unreg(sk, attachment, sk.readyOps()); + boolean closeSocket = false; + // Read goes before write + if (sk.isReadable()) { + if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) { + closeSocket = true; + } + } + if (!closeSocket && sk.isWritable()) { + if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) { + closeSocket = true; + } + } + if (closeSocket) { + cancelledKey(sk); + } + } + } + } else { + //invalid key + cancelledKey(sk); + } + } catch ( CancelledKeyException ckx ) { + cancelledKey(sk); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error("",t); + } +} + + - 2.2.1) AbstractEndpoint#processSocket +- createSocketProcessor() 方法将 Socket 封装到 SocketProcessor 中,SocketProcessor 实现了 Runnable 接口。worker 线程通过调用其 run() 方法来对 Socket 进行处理。 + +``` +public boolean processSocket(SocketWrapperBase<S> socketWrapper, + SocketEvent event, boolean dispatch) { + try { + if (socketWrapper == null) { + return false; + } + SocketProcessorBase<S> sc = processorCache.pop(); + if (sc == null) { + sc = createSocketProcessor(socketWrapper, event); + } else { + sc.reset(socketWrapper, event); + } + Executor executor = getExecutor(); + if (dispatch && executor != null) { + executor.execute(sc); + } else { + sc.run(); + } + } catch (RejectedExecutionException ree) { + getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree); + return false; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // This means we got an OOM or similar creating a thread, or that + // the pool and its queue are full + getLog().error(sm.getString("endpoint.process.fail"), t); + return false; + } + return true; +} +``` + + - 2.2.1.1) NioEndpoint#createSocketProcessor +- protected SocketProcessorBase<NioChannel> createSocketProcessor( + SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { + return new SocketProcessor(socketWrapper, event); +} + + +``` +public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { + super(socketWrapper, event); +} +``` + + + +``` +public SocketProcessorBase(SocketWrapperBase<S> socketWrapper, SocketEvent event) { + reset(socketWrapper, event); +} +``` + + + +``` +public void reset(SocketWrapperBase<S> socketWrapper, SocketEvent event) { + Objects.requireNonNull(event); + this.socketWrapper = socketWrapper; + this.event = event; +} +``` + +- + + - 3) Worker#run(将SocketProcessor封装为Request,IO Handler) + +1.- worker 线程被创建以后就执行 ThreadPoolExecutor 的 runWorker() 方法,试图从 workQueue 中取待处理任务,但是一开始 workQueue 是空的,所以 worker 线程会阻塞在 workQueue.take() 方法。 +2.- 当新任务添加到 workQueue后,workQueue.take() 方法会返回一个 Runnable,通常是 SocketProcessor,然后 worker 线程调用 SocketProcessor 的 run() 方法对 Socket 进行处理。 +3.- createProcessor() 会创建一个 Http11Processor, 它用来解析 Socket,将 Socket 中的内容封装到 Request 中。注意这个 Request 是临时使用的一个类,它的全类名是 org.apache.coyote.Request, +4.- postParseRequest() 方法封装一下 Request,并处理一下映射关系(从 URL 映射到相应的 Host、Context、Wrapper)。 +5.- CoyoteAdapter 将 Rquest 提交给 Container 处理之前,并将 org.apache.coyote.Request 封装到 org.apache.catalina.connector.Request,传递给 Container 处理的 Request 是 org.apache.catalina.connector.Request。 +6.- connector.getService().getMapper().map(),用来在 Mapper 中查询 URL 的映射关系。映射关系会保留到 org.apache.catalina.connector.Request 中,Container 处理阶段 request.getHost() 是使用的就是这个阶段查询到的映射主机,以此类推 request.getContext()、request.getWrapper() 都是。 +7.- connector.getService().getContainer().getPipeline().getFirst().invoke() 会将请求传递到 Container 处理,当然了 Container 处理也是在 Worker 线程中执行的,但是这是一个相对独立的模块,所以单独分出来一节。 + + +``` +/** Delegates main run loop to outer runWorker */ +public void run() { + runWorker(this); +} +``` + + +- final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + Runnable task = w.firstTask; + w.firstTask = null; + w.unlock(); // allow interrupts + boolean completedAbruptly = true; + try { + while (task != null || (task = getTask()) != null) { + w.lock(); + // If pool is stopping, ensure thread is interrupted; + // if not, ensure thread is not interrupted. This + // requires a recheck in second case to deal with + // shutdownNow race while clearing interrupt + if ((runStateAtLeast(ctl.get(), STOP) || + (Thread.interrupted() && + runStateAtLeast(ctl.get(), STOP))) && + !wt.isInterrupted()) + wt.interrupt(); + try { + beforeExecute(wt, task); + Throwable thrown = null; + try { + task.run(); + } catch (RuntimeException x) { + thrown = x; throw x; + } catch (Error x) { + thrown = x; throw x; + } catch (Throwable x) { + thrown = x; throw new Error(x); + } finally { + afterExecute(task, thrown); + } + } finally { + task = null; + w.completedTasks++; + w.unlock(); + } + } + completedAbruptly = false; + } finally { + processWorkerExit(w, completedAbruptly); + } +} + +- task是SocketProcessor类型 + - 3.1) SocketProcessor#run + +``` +public final void run() { + synchronized (socketWrapper) { + // It is possible that processing may be triggered for read and + // write at the same time. The sync above makes sure that processing + // does not occur in parallel. The test below ensures that if the + // first event to be processed results in the socket being closed, + // the subsequent events are not processed. + if (socketWrapper.isClosed()) { + return; + } + doRun(); + } +} +``` + + +- protected void doRun() { + NioChannel socket = socketWrapper.getSocket(); + SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); + + try { + int handshake = -1; + + try { + if (key != null) { + if (socket.isHandshakeComplete()) { + // No TLS handshaking required. Let the handler + // process this socket / event combination. + handshake = 0; + } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT || + event == SocketEvent.ERROR) { + // Unable to complete the TLS handshake. Treat it as + // if the handshake failed. + handshake = -1; + } else { + handshake = socket.handshake(key.isReadable(), key.isWritable()); + // The handshake process reads/writes from/to the + // socket. status may therefore be OPEN_WRITE once + // the handshake completes. However, the handshake + // happens when the socket is opened so the status + // must always be OPEN_READ after it completes. It + // is OK to always set this as it is only used if + // the handshake completes. + event = SocketEvent.OPEN_READ; + } + } + } catch (IOException x) { + handshake = -1; + if (log.isDebugEnabled()) log.debug("Error during SSL handshake",x); + } catch (CancelledKeyException ckx) { + handshake = -1; + } + if (handshake == 0) { + SocketState state = SocketState.OPEN; + // Process the request from this socket + if (event == null) { + state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ); + } else { + state = getHandler().process(socketWrapper, event); + } + if (state == SocketState.CLOSED) { + close(socket, key); + } + } else if (handshake == -1 ) { + close(socket, key); + } else if (handshake == SelectionKey.OP_READ){ + socketWrapper.registerReadInterest(); + } else if (handshake == SelectionKey.OP_WRITE){ + socketWrapper.registerWriteInterest(); + } + } catch (CancelledKeyException cx) { + socket.getPoller().cancelledKey(key); + } catch (VirtualMachineError vme) { + ExceptionUtils.handleThrowable(vme); + } catch (Throwable t) { + log.error("", t); + socket.getPoller().cancelledKey(key); + } finally { + socketWrapper = null; + event = null; + //return to cache +- // keep-alive的实现 + if (running && !paused) { + processorCache.push(this); + } + } +} + + - 3.1.1) AbstractProtocol#process +- 先试图从connections中获取当前Socket对应的Processor,如果没有找到的话从recycledProcessors中获取,也就是已经处理过连接但是没有被销毁的Processor,这样做的好处是避免频繁地创建和销毁对象。processor还是为空的话,那就使用createProcessor创建。 + +``` +public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractConnectionHandler.process", + wrapper.getSocket(), status)); + } + if (wrapper == null) { + // Nothing to do. Socket has been closed. + return SocketState.CLOSED; + } + + S socket = wrapper.getSocket(); + + Processor processor = connections.get(socket); + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractConnectionHandler.connectionsGet", + processor, socket)); + } + + if (processor != null) { + // Make sure an async timeout doesn't fire + getProtocol().removeWaitingProcessor(processor); + } else if (status == SocketEvent.DISCONNECT || status == SocketEvent.ERROR) { + // Nothing to do. Endpoint requested a close and there is no + // longer a processor associated with this socket. + return SocketState.CLOSED; + } + + ContainerThreadMarker.set(); + + try { + if (processor == null) { + String negotiatedProtocol = wrapper.getNegotiatedProtocol(); + if (negotiatedProtocol != null) { + UpgradeProtocol upgradeProtocol = + getProtocol().getNegotiatedProtocol(negotiatedProtocol); + if (upgradeProtocol != null) { + processor = upgradeProtocol.getProcessor( + wrapper, getProtocol().getAdapter()); + } else if (negotiatedProtocol.equals("http/1.1")) { + // Explicitly negotiated the default protocol. + // Obtain a processor below. + } else { + // TODO: + // OpenSSL 1.0.2's ALPN callback doesn't support + // failing the handshake with an error if no + // protocol can be negotiated. Therefore, we need to + // fail the connection here. Once this is fixed, + // replace the code below with the commented out + // block. + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString( + "abstractConnectionHandler.negotiatedProcessor.fail", + negotiatedProtocol)); + } + return SocketState.CLOSED; + /* + * To replace the code above once OpenSSL 1.1.0 is + * used. + // Failed to create processor. This is a bug. + throw new IllegalStateException(sm.getString( + "abstractConnectionHandler.negotiatedProcessor.fail", + negotiatedProtocol)); + */ + } + } + } + if (processor == null) { + processor = recycledProcessors.pop(); + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractConnectionHandler.processorPop", + processor)); + } + } + if (processor == null) { + processor = getProtocol().createProcessor(); + register(processor); + } + + processor.setSslSupport( + wrapper.getSslSupport(getProtocol().getClientCertProvider())); + + // Associate the processor with the connection + connections.put(socket, processor); + + SocketState state = SocketState.CLOSED; + do { + state = processor.process(wrapper, status); + + if (state == SocketState.UPGRADING) { + // Get the HTTP upgrade handler + UpgradeToken upgradeToken = processor.getUpgradeToken(); + // Retrieve leftover input + ByteBuffer leftOverInput = processor.getLeftoverInput(); + if (upgradeToken == null) { + // Assume direct HTTP/2 connection + UpgradeProtocol upgradeProtocol = getProtocol().getUpgradeProtocol("h2c"); + if (upgradeProtocol != null) { + processor = upgradeProtocol.getProcessor( + wrapper, getProtocol().getAdapter()); + wrapper.unRead(leftOverInput); + // Associate with the processor with the connection + connections.put(socket, processor); + } else { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString( + "abstractConnectionHandler.negotiatedProcessor.fail", + "h2c")); + } + return SocketState.CLOSED; + } + } else { + HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); + // Release the Http11 processor to be re-used + release(processor); + // Create the upgrade processor + processor = getProtocol().createUpgradeProcessor(wrapper, upgradeToken); + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractConnectionHandler.upgradeCreate", + processor, wrapper)); + } + wrapper.unRead(leftOverInput); + // Mark the connection as upgraded + wrapper.setUpgraded(true); + // Associate with the processor with the connection + connections.put(socket, processor); + // Initialise the upgrade handler (which may trigger + // some IO using the new protocol which is why the lines + // above are necessary) + // This cast should be safe. If it fails the error + // handling for the surrounding try/catch will deal with + // it. + if (upgradeToken.getInstanceManager() == null) { + httpUpgradeHandler.init((WebConnection) processor); + } else { + ClassLoader oldCL = upgradeToken.getContextBind().bind(false, null); + try { + httpUpgradeHandler.init((WebConnection) processor); + } finally { + upgradeToken.getContextBind().unbind(false, oldCL); + } + } + } + } + } while ( state == SocketState.UPGRADING); + + if (state == SocketState.LONG) { + // In the middle of processing a request/response. Keep the + // socket associated with the processor. Exact requirements + // depend on type of long poll + longPoll(wrapper, processor); + if (processor.isAsync()) { + getProtocol().addWaitingProcessor(processor); + } + } else if (state == SocketState.OPEN) { + // In keep-alive but between requests. OK to recycle + // processor. Continue to poll for the next request. + connections.remove(socket); + release(processor); + wrapper.registerReadInterest(); + } else if (state == SocketState.SENDFILE) { + // Sendfile in progress. If it fails, the socket will be + // closed. If it works, the socket either be added to the + // poller (or equivalent) to await more data or processed + // if there are any pipe-lined requests remaining. + } else if (state == SocketState.UPGRADED) { + // Don't add sockets back to the poller if this was a + // non-blocking write otherwise the poller may trigger + // multiple read events which may lead to thread starvation + // in the connector. The write() method will add this socket + // to the poller if necessary. + if (status != SocketEvent.OPEN_WRITE) { + longPoll(wrapper, processor); + } + } else if (state == SocketState.SUSPENDED) { + // Don't add sockets back to the poller. + // The resumeProcessing() method will add this socket + // to the poller. + } else { + // Connection closed. OK to recycle the processor. Upgrade + // processors are not recycled. + connections.remove(socket); + if (processor.isUpgrade()) { + UpgradeToken upgradeToken = processor.getUpgradeToken(); + HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); + InstanceManager instanceManager = upgradeToken.getInstanceManager(); + if (instanceManager == null) { + httpUpgradeHandler.destroy(); + } else { + ClassLoader oldCL = upgradeToken.getContextBind().bind(false, null); + try { + httpUpgradeHandler.destroy(); + } finally { + try { + instanceManager.destroyInstance(httpUpgradeHandler); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + getLog().error(sm.getString("abstractConnectionHandler.error"), e); + } + upgradeToken.getContextBind().unbind(false, oldCL); + } + } + } else { + release(processor); + } + } + return state; + } catch(java.net.SocketException e) { + // SocketExceptions are normal + getLog().debug(sm.getString( + "abstractConnectionHandler.socketexception.debug"), e); + } catch (java.io.IOException e) { + // IOExceptions are normal + getLog().debug(sm.getString( + "abstractConnectionHandler.ioexception.debug"), e); + } catch (ProtocolException e) { + // Protocol exceptions normally mean the client sent invalid or + // incomplete data. + getLog().debug(sm.getString( + "abstractConnectionHandler.protocolexception.debug"), e); + } + // Future developers: if you discover any other + // rare-but-nonfatal exceptions, catch them here, and log as + // above. + catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + // any other exception or error is odd. Here we log it + // with "ERROR" level, so it will show up even on + // less-than-verbose logs. + getLog().error(sm.getString("abstractConnectionHandler.error"), e); + } finally { + ContainerThreadMarker.clear(); + } + + // Make sure socket/processor is removed from the list of current + // connections + connections.remove(socket); + release(processor); + return SocketState.CLOSED; +} +``` + + + - 3.1.1.1) AbstractHttp11Protocol#createProcessor +- createProcessor() 会创建一个 Http11Processor, 它用来解析 Socket,将 Socket 中的内容封装到 Request 中。注意这个 Request 是临时使用的一个类,它的全类名是 org.apache.coyote.Request。 +- protected Processor createProcessor() { + Http11Processor processor = new Http11Processor(this, adapter); + return processor; +} + - 3.1.1.1.1) Http11Processor#constructor(创建req和resp缓冲区) + +``` +public Http11Processor(AbstractHttp11Protocol<?> protocol, Adapter adapter) { + super(adapter); + this.protocol = protocol; + + userDataHelper = new UserDataHelper(log); + + inputBuffer = new Http11InputBuffer(request, protocol.getMaxHttpHeaderSize(), + protocol.getRejectIllegalHeaderName()); + request.setInputBuffer(inputBuffer); + + outputBuffer = new Http11OutputBuffer(response, protocol.getMaxHttpHeaderSize()); + response.setOutputBuffer(outputBuffer); + + // Create and add the identity filters. + inputBuffer.addFilter(new IdentityInputFilter(protocol.getMaxSwallowSize())); + outputBuffer.addFilter(new IdentityOutputFilter()); + + // Create and add the chunked filters. + inputBuffer.addFilter(new ChunkedInputFilter(protocol.getMaxTrailerSize(), + protocol.getAllowedTrailerHeadersInternal(), protocol.getMaxExtensionSize(), + protocol.getMaxSwallowSize())); + outputBuffer.addFilter(new ChunkedOutputFilter()); + + // Create and add the void filters. + inputBuffer.addFilter(new VoidInputFilter()); + outputBuffer.addFilter(new VoidOutputFilter()); + + // Create and add buffered input filter + inputBuffer.addFilter(new BufferedInputFilter()); + + // Create and add the chunked filters. + //inputBuffer.addFilter(new GzipInputFilter()); + outputBuffer.addFilter(new GzipOutputFilter()); + + pluggableFilterIndex = inputBuffer.getFilters().length; +} +``` + + + +``` +public AbstractProcessor(Adapter adapter) { + this(adapter, new Request(), new Response()); +} +``` + + +- protected AbstractProcessor(Adapter adapter, Request coyoteRequest, Response coyoteResponse) { + this.adapter = adapter; + asyncStateMachine = new AsyncStateMachine(this); + request = coyoteRequest; + response = coyoteResponse; + response.setHook(this); + request.setResponse(response); + request.setHook(this); +} + - 3.1.1.1.1.1) Http11InputBuffer#constructor(存放解析后的Request信息) + +``` +public Http11InputBuffer(Request request, int headerBufferSize, + boolean rejectIllegalHeaderName) { + + this.request = request; + headers = request.getMimeHeaders(); + + this.headerBufferSize = headerBufferSize; + this.rejectIllegalHeaderName = rejectIllegalHeaderName; + + filterLibrary = new InputFilter[0]; + activeFilters = new InputFilter[0]; + lastActiveFilter = -1; + + parsingHeader = true; + parsingRequestLine = true; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + parsingRequestLineQPos = -1; + headerParsePos = HeaderParsePosition.HEADER_START; + swallowInput = true; + + inputStreamInputBuffer = new SocketInputBuffer(); +} +``` + + + - 3.1.1.2) ConnectionHandler#register(注册Http11Processor) +- protected void register(Processor processor) { + if (getProtocol().getDomain() != null) { + synchronized (this) { + try { + long count = registerCount.incrementAndGet(); + RequestInfo rp = + processor.getRequest().getRequestProcessor(); + rp.setGlobalProcessor(global); + ObjectName rpName = new ObjectName( + getProtocol().getDomain() + + ":type=RequestProcessor,worker=" + + getProtocol().getName() + + ",name=" + getProtocol().getProtocolName() + + "Request" + count); + if (getLog().isDebugEnabled()) { + getLog().debug("Register " + rpName); + } + Registry.getRegistry(null, null).registerComponent(rp, + rpName, null); + rp.setRpName(rpName); + } catch (Exception e) { + getLog().warn("Error registering request"); + } + } + } +} + + - 3.1.1.3) AbstractProcessorLight#process(Http11Processor进行处理) + +``` +public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status) + throws IOException { + + SocketState state = SocketState.CLOSED; + Iterator<DispatchType> dispatches = null; + do { + if (dispatches != null) { + DispatchType nextDispatch = dispatches.next(); + state = dispatch(nextDispatch.getSocketStatus()); + } else if (status == SocketEvent.DISCONNECT) { + // Do nothing here, just wait for it to get recycled + } else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) { + state = dispatch(status); + if (state == SocketState.OPEN) { + // There may be pipe-lined data to read. If the data isn't + // processed now, execution will exit this loop and call + // release() which will recycle the processor (and input + // buffer) deleting any pipe-lined data. To avoid this, + // process it now. + state = service(socketWrapper); + } + } else if (status == SocketEvent.OPEN_WRITE) { + // Extra write event likely after async, ignore + state = SocketState.LONG; + } else if (status == SocketEvent.OPEN_READ){ + state = service(socketWrapper); + } else { + // Default to closing the socket if the SocketEvent passed in + // is not consistent with the current state of the Processor + state = SocketState.CLOSED; + } + + if (state != SocketState.CLOSED && isAsync()) { + state = asyncPostProcess(); + } + + if (getLog().isDebugEnabled()) { + getLog().debug("Socket: [" + socketWrapper + + "], Status in: [" + status + + "], State out: [" + state + "]"); + } + + if (dispatches == null || !dispatches.hasNext()) { + // Only returns non-null iterator if there are + // dispatches to process. + dispatches = getIteratorAndClearDispatches(); + } + } while (state == SocketState.ASYNC_END || + dispatches != null && state != SocketState.CLOSED); + + return state; +} +``` + + + - 3.1.1.3.1) (service骨架)Http11Processor#service(包含servlet后续处理,keep-alive的实现) +- 1,org.apache.coyote.Request 是tomcat内部使用用于存放关于request消息的数据结构 +- 2,org.apache.tomcat.util.buf.MessageBytes 用于存放消息,在org.apache.coyote.Request中大量用于存放解析后的byte字符 +- 3,org.apache.tomcat.util.buf.ByteChunk 真正用于存放数据的数据结构,存放的是byte[],org.apache.tomcat.util.buf.MessageBytes使用它。 + +- Request存放着解析后的Request信息,其数据来自于InputBuffer。 +- http消息通过inputBuffer解析后放到Request中,Request把它放到相应的MessageBytes,最后MessageBytes把它存到ByteChunk里。 + + +``` +public SocketState service(SocketWrapperBase<?> socketWrapper) + throws IOException { + RequestInfo rp = request.getRequestProcessor(); + rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); + + // Setting up the I/O + setSocketWrapper(socketWrapper); + inputBuffer.init(socketWrapper); + outputBuffer.init(socketWrapper); + + // Flags + keepAlive = true; + openSocket = false; + readComplete = true; + boolean keptAlive = false; + SendfileState sendfileState = SendfileState.DONE; + + while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null && + sendfileState == SendfileState.DONE && !protocol.isPaused()) { + + // Parsing the request header + try { + if (!inputBuffer.parseRequestLine(keptAlive, protocol.getConnectionTimeout(), + protocol.getKeepAliveTimeout())) { + if (inputBuffer.getParsingRequestLinePhase() == -1) { + return SocketState.UPGRADING; + } else if (handleIncompleteRequestLineRead()) { + break; + } + } + + if (protocol.isPaused()) { + // 503 - Service unavailable + response.setStatus(503); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } else { + keptAlive = true; + // Set this every time in case limit has been changed via JMX + request.getMimeHeaders().setLimit(protocol.getMaxHeaderCount()); + if (!inputBuffer.parseHeaders()) { + // We've read part of the request, don't recycle it + // instead associate it with the socket + openSocket = true; + readComplete = false; + break; + } + if (!protocol.getDisableUploadTimeout()) { + socketWrapper.setReadTimeout(protocol.getConnectionUploadTimeout()); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.header.parse"), e); + } + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + break; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + UserDataHelper.Mode logMode = userDataHelper.getNextMode(); + if (logMode != null) { + String message = sm.getString("http11processor.header.parse"); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("http11processor.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message, t); + break; + case DEBUG: + log.debug(message, t); + } + } + // 400 - Bad Request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, t); + getAdapter().log(request, response, 0); + } + + // Has an upgrade been requested? + Enumeration<String> connectionValues = request.getMimeHeaders().values("Connection"); + boolean foundUpgrade = false; + while (connectionValues.hasMoreElements() && !foundUpgrade) { + foundUpgrade = connectionValues.nextElement().toLowerCase( + Locale.ENGLISH).contains("upgrade"); + } + + if (foundUpgrade) { + // Check the protocol + String requestedProtocol = request.getHeader("Upgrade"); + + UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol); + if (upgradeProtocol != null) { + if (upgradeProtocol.accept(request)) { + // TODO Figure out how to handle request bodies at this + // point. + response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + response.setHeader("Connection", "Upgrade"); + response.setHeader("Upgrade", requestedProtocol); + action(ActionCode.CLOSE, null); + getAdapter().log(request, response, 0); + + InternalHttpUpgradeHandler upgradeHandler = + upgradeProtocol.getInternalUpgradeHandler( + socketWrapper, getAdapter(), cloneRequest(request)); + UpgradeToken upgradeToken = new UpgradeToken(upgradeHandler, null, null); + action(ActionCode.UPGRADE, upgradeToken); + return SocketState.UPGRADING; + } + } + } + + if (!getErrorState().isError()) { + // Setting up filters, and parse some request headers + rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); + try { + prepareRequest(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare"), t); + } + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + getAdapter().log(request, response, 0); + } + } + + int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests(); + if (maxKeepAliveRequests == 1) { + keepAlive = false; + } else if (maxKeepAliveRequests > 0 && + socketWrapper.decrementKeepAlive() <= 0) { + keepAlive = false; + } + + // Process the request in the adapter + if (!getErrorState().isError()) { + try { + rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); +getAdapter().service(request, response); + // Handle when the response was committed before a serious + // error occurred. Throwing a ServletException should both + // set the status to 500 and set the errorException. + // If we fail here, then the response is likely already + // committed, so we can't try and set headers. + if(keepAlive && !getErrorState().isError() && !isAsync() && + statusDropsConnection(response.getStatus())) { + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } catch (InterruptedIOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (HeadersTooLargeException e) { + log.error(sm.getString("http11processor.request.process"), e); + // The response should not have been committed but check it + // anyway to be safe + if (response.isCommitted()) { + setErrorState(ErrorState.CLOSE_NOW, e); + } else { + response.reset(); + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, e); + response.setHeader("Connection", "close"); // TODO: Remove + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("http11processor.request.process"), t); + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + getAdapter().log(request, response, 0); + } + } + + // Finish the handling of the request + rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT); + if (!isAsync()) { + // If this is an async request then the request ends when it has + // been completed. The AsyncContext is responsible for calling + // endRequest() in that case. + endRequest(); + } + rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT); + + // If there was an error, make sure the request is counted as + // and error, and update the statistics counter + if (getErrorState().isError()) { + response.setStatus(500); + } + + if (!isAsync() || getErrorState().isError()) { + request.updateCounters(); + if (getErrorState().isIoAllowed()) { + inputBuffer.nextRequest(); + outputBuffer.nextRequest(); + } + } + + if (!protocol.getDisableUploadTimeout()) { + int connectionTimeout = protocol.getConnectionTimeout(); + if(connectionTimeout > 0) { + socketWrapper.setReadTimeout(connectionTimeout); + } else { + socketWrapper.setReadTimeout(0); + } + } + + rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); + + sendfileState = processSendfile(socketWrapper); + } + + rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); + + if (getErrorState().isError() || protocol.isPaused()) { + return SocketState.CLOSED; + } else if (isAsync()) { + return SocketState.LONG; + } else if (isUpgrade()) { + return SocketState.UPGRADING; + } else { + if (sendfileState == SendfileState.PENDING) { + return SocketState.SENDFILE; + } else { + if (openSocket) { + if (readComplete) { + return SocketState.OPEN; + } else { + return SocketState.LONG; + } + } else { + return SocketState.CLOSED; + } + } + } +} +``` + + - 3.1.1.3.1.1) Http11inputBuffer#init(初始化InputBuffer) +- void init(SocketWrapperBase<?> socketWrapper) { + + wrapper = socketWrapper; + wrapper.setAppReadBufHandler(this); + + int bufLength = headerBufferSize + + wrapper.getSocketBufferHandler().getReadBuffer().capacity(); + if (byteBuffer == null || byteBuffer.capacity() < bufLength) { + byteBuffer = ByteBuffer.allocate(bufLength); + byteBuffer.position(0).limit(0); + } +} + + - 3.1.1.3.1.2) Http11inputBuffer#parseRequestLine(解析请求行) +- 将SocketBufferHandler中的readBuffer的部分数据填充到byteBuffer中,读取byteBuffer,解析,将结果存入Request + - boolean parseRequestLine(boolean keptAlive) throws IOException { + + // check state + if (!parsingRequestLine) { + return true; + } + // + // Skipping blank lines + // + if (parsingRequestLinePhase < 2) { + byte chr = 0; + do { + + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (keptAlive) { + // Haven't read any request data yet so use the keep-alive + // timeout. + wrapper.setReadTimeout(wrapper.getEndpoint().getKeepAliveTimeout()); + } + if (!fill(false)) { + // A read is pending, so no longer in initial state + parsingRequestLinePhase = 1; + return false; + } + // At least one byte of the request has been received. + // Switch to the socket timeout. + wrapper.setReadTimeout(wrapper.getEndpoint().getSoTimeout()); + } + if (!keptAlive && byteBuffer.position() == 0 && byteBuffer.limit() >= CLIENT_PREFACE_START.length - 1) { + boolean prefaceMatch = true; + for (int i = 0; i < CLIENT_PREFACE_START.length && prefaceMatch; i++) { + if (CLIENT_PREFACE_START[i] != byteBuffer.get(i)) { + prefaceMatch = false; + } + } + if (prefaceMatch) { + // HTTP/2 preface matched + parsingRequestLinePhase = -1; + return false; + } + } + // Set the start time once we start reading data (even if it is + // just skipping blank lines) + if (request.getStartTime() < 0) { + request.setStartTime(System.currentTimeMillis()); + } + chr = byteBuffer.get(); + } while ((chr == Constants.CR) || (chr == Constants.LF)); + byteBuffer.position(byteBuffer.position() - 1); + + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 2; + if (log.isDebugEnabled()) { + log.debug("Received [" + + new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining(), StandardCharsets.ISO_8859_1) + "]"); + } + } + if (parsingRequestLinePhase == 2) { + // + // Reading the method name + // Method name is a token + // + boolean space = false; + while (!space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) // request line parsing + return false; + } + // Spec says method name is a token followed by a single SP but + // also be tolerant of multiple SP and/or HT. + int pos = byteBuffer.position(); + byte chr = byteBuffer.get(); + if (chr == Constants.SP || chr == Constants.HT) { + space = true; + request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, + pos - parsingRequestLineStart); + } else if (!HttpParser.isToken(chr)) { + byteBuffer.position(byteBuffer.position() - 1); + throw new IllegalArgumentException(sm.getString("iib.invalidmethod")); + } + } + parsingRequestLinePhase = 3; + } + if (parsingRequestLinePhase == 3) { + // Spec says single SP but also be tolerant of multiple SP and/or HT + boolean space = true; + while (space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) // request line parsing + return false; + } + byte chr = byteBuffer.get(); + if (!(chr == Constants.SP || chr == Constants.HT)) { + space = false; + byteBuffer.position(byteBuffer.position() - 1); + } + } + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 4; + } + if (parsingRequestLinePhase == 4) { + // Mark the current buffer position + + int end = 0; + // + // Reading the URI + // + boolean space = false; + while (!space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) // request line parsing + return false; + } + int pos = byteBuffer.position(); + byte chr = byteBuffer.get(); + if (chr == Constants.SP || chr == Constants.HT) { + space = true; + end = pos; + } else if (chr == Constants.CR || chr == Constants.LF) { + // HTTP/0.9 style request + parsingRequestLineEol = true; + space = true; + end = pos; + } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) { + parsingRequestLineQPos = pos; + } else if (HttpParser.isNotRequestTarget(chr)) { + throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget")); + } + } + if (parsingRequestLineQPos >= 0) { + request.queryString().setBytes(byteBuffer.array(), parsingRequestLineQPos + 1, + end - parsingRequestLineQPos - 1); + request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, + parsingRequestLineQPos - parsingRequestLineStart); + } else { + request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, + end - parsingRequestLineStart); + } + parsingRequestLinePhase = 5; + } + if (parsingRequestLinePhase == 5) { + // Spec says single SP but also be tolerant of multiple and/or HT + boolean space = true; + while (space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) // request line parsing + return false; + } + byte chr = byteBuffer.get(); + if (!(chr == Constants.SP || chr == Constants.HT)) { + space = false; + byteBuffer.position(byteBuffer.position() - 1); + } + } + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 6; + + // Mark the current buffer position + end = 0; + } + if (parsingRequestLinePhase == 6) { + // + // Reading the protocol + // Protocol is always "HTTP/" DIGIT "." DIGIT + // + while (!parsingRequestLineEol) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) // request line parsing + return false; + } + + int pos = byteBuffer.position(); + byte chr = byteBuffer.get(); + if (chr == Constants.CR) { + end = pos; + } else if (chr == Constants.LF) { + if (end == 0) { + end = pos; + } + parsingRequestLineEol = true; + } else if (!HttpParser.isHttpProtocol(chr)) { + throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol")); + } + } + + if ((end - parsingRequestLineStart) > 0) { + request.protocol().setBytes(byteBuffer.array(), parsingRequestLineStart, + end - parsingRequestLineStart); + } else { + request.protocol().setString(""); + } + parsingRequestLine = false; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + return true; + } + throw new IllegalStateException( + "Invalid request line parse phase:" + parsingRequestLinePhase); +} + - 3.1.1.3.1.2.1) Http11InputBuffer#fill() + +``` + +/** + * Attempts to read some data into the input buffer. + * + * @return <code>true</code> if more data was added to the input buffer + * otherwise <code>false</code> + */ +private boolean fill(boolean block) throws IOException { + + if (parsingHeader) { + if (byteBuffer.limit() >= headerBufferSize) { + throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error")); + } + } else { + byteBuffer.limit(end).position(end); + } + + byteBuffer.mark(); + if (byteBuffer.position() < byteBuffer.limit()) { + byteBuffer.position(byteBuffer.limit()); + } + byteBuffer.limit(byteBuffer.capacity()); + int nRead = wrapper.read(block, byteBuffer); + byteBuffer.limit(byteBuffer.position()).reset(); + if (nRead > 0) { + return true; + } else if (nRead == -1) { + throw new EOFException(sm.getString("iib.eof.error")); + } else { + return false; + } + +} +``` + + + - 3.1.1.3.1.2.1.1) NioEndpoint#read + +``` +public int read(boolean block, ByteBuffer to) throws IOException { + int nRead = populateReadBuffer(to); + if (nRead > 0) { + return nRead; + /* + * Since more bytes may have arrived since the buffer was last + * filled, it is an option at this point to perform a + * non-blocking read. However correctly handling the case if + * that read returns end of stream adds complexity. Therefore, + * at the moment, the preference is for simplicity. + */ + } + + // The socket read buffer capacity is socket.appReadBufSize + int limit = socketBufferHandler.getReadBuffer().capacity(); +``` + +- // 如果to的剩余可用比read buffer还要大。那么直接从socketchannel读到to + if (to.remaining() >= limit) { + to.limit(to.position() + limit); + nRead = fillReadBuffer(block, to); + updateLastRead(); + } else { + // Fill the read buffer as best we can. + nRead = fillReadBuffer(block); + updateLastRead(); + + // Fill as much of the remaining byte array as possible with the + // data that was just read + if (nRead > 0) { + nRead = populateReadBuffer(to); + } + } + return nRead; +} + - 3.1.1.3.1.2.1.1.1) SocketWrapperBase#populateReadBuffer(将SocketBufferHandler中的ByteBuffer拷贝到Http11InputBuffer中的ByteBuffer) +- protected int populateReadBuffer(ByteBuffer to) { + // Is there enough data in the read buffer to satisfy this request? + // Copy what data there is in the read buffer to the byte array + socketBufferHandler.configureReadBufferForRead(); + int nRead = transfer(socketBufferHandler.getReadBuffer(), to); + + if (log.isDebugEnabled()) { + log.debug("Socket: [" + this + "], Read from buffer: [" + nRead + "]"); + } + return nRead; +} + + +- protected static int transfer(ByteBuffer from, ByteBuffer to) { + int max = Math.min(from.remaining(), to.remaining()); + if (max > 0) { + int fromLimit = from.limit(); + from.limit(from.position() + max); + to.put(from); + from.limit(fromLimit); + } + return max; +} + + - 3.1.1.3.1.2.1.1.2) NioEndpoint#fillReadBuffer(从channel或者selectorPool中读到ByteBuffer中) + +``` +private int fillReadBuffer(boolean block, ByteBuffer to) throws IOException { + int nRead; + NioChannel channel = getSocket(); + if (block) { + Selector selector = null; + try { + selector = pool.get(); + } catch (IOException x) { + // Ignore + } + try { + NioEndpoint.NioSocketWrapper att = (NioEndpoint.NioSocketWrapper) channel + .getAttachment(); + if (att == null) { + throw new IOException("Key must be cancelled."); + } + nRead = pool.read(to, channel, selector, att.getReadTimeout()); + } finally { + if (selector != null) { + pool.put(selector); + } + } + } else { + nRead = channel.read(to); + if (nRead == -1) { + throw new EOFException(); + } + } + return nRead; +} +``` + + + - 3.1.1.3.1.3) Http11inputBuffer#parseHeaders(解析请求头) +- 读取byteBuffer,解析,将结果存入Request + + - 3.1.1.3.1.4) prepareRequest(封装InputFilter) + +``` +private void prepareRequest() { + + http11 = true; + http09 = false; + contentDelimitation = false; + + if (protocol.isSSLEnabled()) { + request.scheme().setString("https"); + } + MessageBytes protocolMB = request.protocol(); + if (protocolMB.equals(Constants.HTTP_11)) { + http11 = true; + protocolMB.setString(Constants.HTTP_11); + } else if (protocolMB.equals(Constants.HTTP_10)) { + http11 = false; + keepAlive = false; + protocolMB.setString(Constants.HTTP_10); + } else if (protocolMB.equals("")) { + // HTTP/0.9 + http09 = true; + http11 = false; + keepAlive = false; + } else { + // Unsupported protocol + http11 = false; + // Send 505; Unsupported HTTP version + response.setStatus(505); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare")+ + " Unsupported HTTP version \""+protocolMB+"\""); + } + } + + MimeHeaders headers = request.getMimeHeaders(); + + // Check connection header + MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION); + if (connectionValueMB != null) { + ByteChunk connectionValueBC = connectionValueMB.getByteChunk(); + if (findBytes(connectionValueBC, Constants.CLOSE_BYTES) != -1) { + keepAlive = false; + } else if (findBytes(connectionValueBC, + Constants.KEEPALIVE_BYTES) != -1) { + keepAlive = true; + } + } + + if (http11) { + MessageBytes expectMB = headers.getValue("expect"); + if (expectMB != null) { + if (expectMB.indexOfIgnoreCase("100-continue", 0) != -1) { + inputBuffer.setSwallowInput(false); + request.setExpectation(true); + } else { + response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } + } + + // Check user-agent header + Pattern restrictedUserAgents = protocol.getRestrictedUserAgentsPattern(); + if (restrictedUserAgents != null && (http11 || keepAlive)) { + MessageBytes userAgentValueMB = headers.getValue("user-agent"); + // Check in the restricted list, and adjust the http11 + // and keepAlive flags accordingly + if(userAgentValueMB != null) { + String userAgentValue = userAgentValueMB.toString(); + if (restrictedUserAgents.matcher(userAgentValue).matches()) { + http11 = false; + keepAlive = false; + } + } + } + + + // Check host header + MessageBytes hostValueMB = null; + try { + hostValueMB = headers.getUniqueValue("host"); + } catch (IllegalArgumentException iae) { + // Multiple Host headers are not permitted + // 400 - Bad request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.multipleHosts")); + } + } + if (http11 && hostValueMB == null) { + // 400 - Bad request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare")+ + " host header missing"); + } + } + + // Check for a full URI (including protocol://host:port/) + ByteChunk uriBC = request.requestURI().getByteChunk(); + if (uriBC.startsWithIgnoreCase("http", 0)) { + + int pos = uriBC.indexOf("://", 0, 3, 4); + int uriBCStart = uriBC.getStart(); + int slashPos = -1; + if (pos != -1) { + pos += 3; + byte[] uriB = uriBC.getBytes(); + slashPos = uriBC.indexOf('/', pos); + int atPos = uriBC.indexOf('@', pos); + if (slashPos == -1) { + slashPos = uriBC.getLength(); + // Set URI as "/" + request.requestURI().setBytes + (uriB, uriBCStart + pos - 2, 1); + } else { + request.requestURI().setBytes + (uriB, uriBCStart + slashPos, + uriBC.getLength() - slashPos); + } + // Skip any user info + if (atPos != -1) { + pos = atPos + 1; + } + if (http11) { + // Missing host header is illegal but handled above + if (hostValueMB != null) { + // Any host in the request line must be consistent with + // the Host header + if (!hostValueMB.getByteChunk().equals( + uriB, uriBCStart + pos, slashPos - pos)) { + if (protocol.getAllowHostHeaderMismatch()) { + // The requirements of RFC 2616 are being + // applied. If the host header and the request + // line do not agree, the request line takes + // precedence + hostValueMB = headers.setValue("host"); + hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos); + } else { + // The requirements of RFC 7230 are being + // applied. If the host header and the request + // line do not agree, trigger a 400 response. + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.inconsistentHosts")); + } + } + } + } + } else { + // Not HTTP/1.1 - no Host header so generate one since + // Tomcat internals assume it is set + hostValueMB = headers.setValue("host"); + hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos); + } + } + } + + // Input filter setup + InputFilter[] inputFilters = inputBuffer.getFilters(); + + // Parse transfer-encoding header + if (http11) { + MessageBytes transferEncodingValueMB = headers.getValue("transfer-encoding"); + if (transferEncodingValueMB != null) { + String transferEncodingValue = transferEncodingValueMB.toString(); + // Parse the comma separated list. "identity" codings are ignored + int startPos = 0; + int commaPos = transferEncodingValue.indexOf(','); + String encodingName = null; + while (commaPos != -1) { + encodingName = transferEncodingValue.substring(startPos, commaPos); + addInputFilter(inputFilters, encodingName); + startPos = commaPos + 1; + commaPos = transferEncodingValue.indexOf(',', startPos); + } + encodingName = transferEncodingValue.substring(startPos); + addInputFilter(inputFilters, encodingName); + } + } + + // Parse content-length header + long contentLength = request.getContentLengthLong(); + if (contentLength >= 0) { + if (contentDelimitation) { + // contentDelimitation being true at this point indicates that + // chunked encoding is being used but chunked encoding should + // not be used with a content length. RFC 2616, section 4.4, + // bullet 3 states Content-Length must be ignored in this case - + // so remove it. + headers.removeHeader("content-length"); + request.setContentLength(-1); + } else { + inputBuffer.addActiveFilter + (inputFilters[Constants.IDENTITY_FILTER]); + contentDelimitation = true; + } + } + + parseHost(hostValueMB); + + if (!contentDelimitation) { + // If there's no content length + // (broken HTTP/1.0 or HTTP/1.1), assume + // the client is not broken and didn't send a body + inputBuffer.addActiveFilter + (inputFilters[Constants.VOID_FILTER]); + contentDelimitation = true; + } + + if (getErrorState().isError()) { + getAdapter().log(request, response, 0); + } +} +``` + + + - 3.1.1.3.1.4)(service骨架) CoyoteAdapter#service(将coyote的req和resp转为catalina的req和resp) + +``` +public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) + throws Exception { + + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + + if (request == null) { + // Create objects + request = connector.createRequest(); + request.setCoyoteRequest(req); + response = connector.createResponse(); + response.setCoyoteResponse(res); + + // Link objects + request.setResponse(response); + response.setRequest(request); + + // Set as notes + req.setNote(ADAPTER_NOTES, request); + res.setNote(ADAPTER_NOTES, response); + + // Set query string encoding + req.getParameters().setQueryStringCharset(connector.getURICharset()); + } + + if (connector.getXpoweredBy()) { + response.addHeader("X-Powered-By", POWERED_BY); + } + + boolean async = false; + boolean postParseSuccess = false; + + req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get()); + + try { + // Parse and set Catalina and configuration specific + // request parameters + postParseSuccess = postParseRequest(req, request, res, response); + if (postParseSuccess) { + //check valves if we support async + request.setAsyncSupported( + connector.getService().getContainer().getPipeline().isAsyncSupported()); + // Calling the container + // 加入到pipeline中进行调用 connector.getService().getContainer().getPipeline().getFirst().invoke( + request, response); + } + if (request.isAsync()) { + async = true; + ReadListener readListener = req.getReadListener(); + if (readListener != null && request.isFinished()) { + // Possible the all data may have been read during service() + // method so this needs to be checked here + ClassLoader oldCL = null; + try { + oldCL = request.getContext().bind(false, null); + if (req.sendAllDataReadEvent()) { + req.getReadListener().onAllDataRead(); + } + } finally { + request.getContext().unbind(false, oldCL); + } + } + + Throwable throwable = + (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // If an async request was started, is not going to end once + // this container thread finishes and an error occurred, trigger + // the async error process + if (!request.isAsyncCompleting() && throwable != null) { + request.getAsyncContextInternal().setErrorState(throwable, true); + } + } else { + request.finishRequest(); + response.finishResponse(); + } + + } catch (IOException e) { + // Ignore + } finally { + AtomicBoolean error = new AtomicBoolean(false); + res.action(ActionCode.IS_ERROR, error); + + if (request.isAsyncCompleting() && error.get()) { + // Connection will be forcibly closed which will prevent + // completion happening at the usual point. Need to trigger + // call to onComplete() here. + res.action(ActionCode.ASYNC_POST_PROCESS, null); + async = false; + } + + // Access log + if (!async && postParseSuccess) { + // Log only if processing was invoked. + // If postParseRequest() failed, it has already logged it. + Context context = request.getContext(); + // If the context is null, it is likely that the endpoint was + // shutdown, this connection closed and the request recycled in + // a different thread. That thread will have updated the access + // log so it is OK not to update the access log here in that + // case. + if (context != null) { + context.logAccess(request, response, + System.currentTimeMillis() - req.getStartTime(), false); + } + } + + req.getRequestProcessor().setWorkerThreadName(null); + + // Recycle the wrapper request and response + if (!async) { + request.recycle(); + response.recycle(); + } + } +} +``` + + + - 3.1.1.3.1.4.1)(Mapper#map) CoyoteAdapter#postParseRequest(req和resp的转换) + +- postParseRequest() 方法封装一下 Request,并处理一下映射关系(从 URL 映射到相应的 Host、Context、Wrapper)。 +- CoyoteAdapter 将 Rquest 提交给 Container 处理之前,并将 org.apache.coyote.Request 封装到 org.apache.catalina.connector.Request,传递给 Container 处理的 Request 是 org.apache.catalina.connector.Request。 +- connector.getService().getMapper().map(),用来在 Mapper 中查询 URL 的映射关系。映射关系会保留到 org.apache.catalina.connector.Request 中,Container 处理阶段 request.getHost() 是使用的就是这个阶段查询到的映射主机,以此类推 request.getContext()、request.getWrapper() 都是。 + +``` +protected boolean postParseRequest(org.apache.coyote.Request req, Request request, + org.apache.coyote.Response res, Response response) throws IOException, ServletException { + + // If the processor has set the scheme (AJP does this, HTTP does this if + // SSL is enabled) use this to set the secure flag as well. If the + // processor hasn't set it, use the settings from the connector + if (req.scheme().isNull()) { + // Use connector scheme and secure configuration, (defaults to + // "http" and false respectively) + req.scheme().setString(connector.getScheme()); + request.setSecure(connector.getSecure()); + } else { + // Use processor specified scheme to determine secure state + request.setSecure(req.scheme().equals("https")); + } + + // At this point the Host header has been processed. + // Override if the proxyPort/proxyHost are set + String proxyName = connector.getProxyName(); + int proxyPort = connector.getProxyPort(); + if (proxyPort != 0) { + req.setServerPort(proxyPort); + } else if (req.getServerPort() == -1) { + // Not explicitly set. Use default ports based on the scheme + if (req.scheme().equals("https")) { + req.setServerPort(443); + } else { + req.setServerPort(80); + } + } + if (proxyName != null) { + req.serverName().setString(proxyName); + } + + MessageBytes undecodedURI = req.requestURI(); + + // Check for ping OPTIONS * request + if (undecodedURI.equals("*")) { + if (req.method().equalsIgnoreCase("OPTIONS")) { + StringBuilder allow = new StringBuilder(); + allow.append("GET, HEAD, POST, PUT, DELETE, OPTIONS"); + // Trace if allowed + if (connector.getAllowTrace()) { + allow.append(", TRACE"); + } + // Always allow options + res.setHeader("Allow", allow.toString()); + // Access log entry as processing won't reach AccessLogValve + connector.getService().getContainer().logAccess(request, response, 0, true); + return false; + } else { + response.sendError(400, "Invalid URI"); + } + } + + MessageBytes decodedURI = req.decodedURI(); + + if (undecodedURI.getType() == MessageBytes.T_BYTES) { + // Copy the raw URI to the decodedURI + decodedURI.duplicate(undecodedURI); + + // Parse the path parameters. This will: + // - strip out the path parameters + // - convert the decodedURI to bytes + parsePathParameters(req, request); + + // URI decoding + // %xx decoding of the URL + try { + req.getURLDecoder().convert(decodedURI, false); + } catch (IOException ioe) { + response.sendError(400, "Invalid URI: " + ioe.getMessage()); + } + // Normalization + if (!normalize(req.decodedURI())) { + response.sendError(400, "Invalid URI"); + } + // Character decoding + convertURI(decodedURI, request); + // Check that the URI is still normalized + if (!checkNormalize(req.decodedURI())) { + response.sendError(400, "Invalid URI"); + } + } else { + /* The URI is chars or String, and has been sent using an in-memory + * protocol handler. The following assumptions are made: + * - req.requestURI() has been set to the 'original' non-decoded, + * non-normalized URI + * - req.decodedURI() has been set to the decoded, normalized form + * of req.requestURI() + */ + decodedURI.toChars(); + // Remove all path parameters; any needed path parameter should be set + // using the request object rather than passing it in the URL + CharChunk uriCC = decodedURI.getCharChunk(); + int semicolon = uriCC.indexOf(';'); + if (semicolon > 0) { + decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon); + } + } + + // Request mapping. + MessageBytes serverName; + if (connector.getUseIPVHosts()) { + serverName = req.localName(); + if (serverName.isNull()) { + // well, they did ask for it + res.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, null); + } + } else { + serverName = req.serverName(); + } + + // Version for the second mapping loop and + // Context that we expect to get for that version + String version = null; + Context versionContext = null; + boolean mapRequired = true; + + if (response.isError()) { + // An error this early means the URI is invalid. Ensure invalid data + // is not passed to the mapper. Note we still want the mapper to + // find the correct host. + decodedURI.recycle(); + } + + while (mapRequired) { +``` + + - // 使用Mapper将当前request映射到Host、Context、Wrapper + // This will map the the latest version by default + connector.getService().getMapper().map(serverName, decodedURI, + version, request.getMappingData()); + + // If there is no context at this point, either this is a 404 + // because no ROOT context has been deployed or the URI was invalid + // so no context could be mapped. + if (request.getContext() == null) { + // Don't overwrite an existing error + if (!response.isError()) { + response.sendError(404, "Not found"); + } + // Allow processing to continue. + // If present, the error reporting valve will provide a response + // body. + return true; + } + + // Now we have the context, we can parse the session ID from the URL + // (if any). Need to do this before we redirect in case we need to + // include the session id in the redirect + String sessionID; + if (request.getServletContext().getEffectiveSessionTrackingModes() + .contains(SessionTrackingMode.URL)) { + + // Get the session ID if there was one + sessionID = request.getPathParameter( + SessionConfig.getSessionUriParamName( + request.getContext())); + if (sessionID != null) { + request.setRequestedSessionId(sessionID); + request.setRequestedSessionURL(true); + } + } + + // Look for session ID in cookies and SSL session + parseSessionCookiesId(request); + parseSessionSslId(request); + + sessionID = request.getRequestedSessionId(); + + mapRequired = false; + if (version != null && request.getContext() == versionContext) { + // We got the version that we asked for. That is it. + } else { + version = null; + versionContext = null; + + Context[] contexts = request.getMappingData().contexts; + // Single contextVersion means no need to remap + // No session ID means no possibility of remap + if (contexts != null && sessionID != null) { + // Find the context associated with the session + for (int i = contexts.length; i > 0; i--) { + Context ctxt = contexts[i - 1]; + if (ctxt.getManager().findSession(sessionID) != null) { + // We found a context. Is it the one that has + // already been mapped? + if (!ctxt.equals(request.getMappingData().context)) { + // Set version so second time through mapping + // the correct context is found + version = ctxt.getWebappVersion(); + versionContext = ctxt; + // Reset mapping + request.getMappingData().recycle(); + mapRequired = true; + // Recycle cookies and session info in case the + // correct context is configured with different + // settings + request.recycleSessionInfo(); + request.recycleCookieInfo(true); + } + break; + } + } + } + } + + if (!mapRequired && request.getContext().getPaused()) { + // Found a matching context but it is paused. Mapping data will + // be wrong since some Wrappers may not be registered at this + // point. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Should never happen + } + // Reset mapping + request.getMappingData().recycle(); + mapRequired = true; + } + } + + // Possible redirect + MessageBytes redirectPathMB = request.getMappingData().redirectPath; + if (!redirectPathMB.isNull()) { + String redirectPath = URLEncoder.DEFAULT.encode( + redirectPathMB.toString(), StandardCharsets.UTF_8); + String query = request.getQueryString(); + if (request.isRequestedSessionIdFromURL()) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + ";" + + SessionConfig.getSessionUriParamName( + request.getContext()) + + "=" + request.getRequestedSessionId(); + } + if (query != null) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + "?" + query; + } + response.sendRedirect(redirectPath); + request.getContext().logAccess(request, response, 0, true); + return false; + } + + // Filter trace method + if (!connector.getAllowTrace() + && req.method().equalsIgnoreCase("TRACE")) { + Wrapper wrapper = request.getWrapper(); + String header = null; + if (wrapper != null) { + String[] methods = wrapper.getServletMethods(); + if (methods != null) { + for (int i=0; i<methods.length; i++) { + if ("TRACE".equals(methods[i])) { + continue; + } + if (header == null) { + header = methods[i]; + } else { + header += ", " + methods[i]; + } + } + } + } + res.addHeader("Allow", header); + response.sendError(405, "TRACE method is not allowed"); + // Safe to skip the remainder of this method. + return true; + } + + doConnectorAuthenticationAuthorization(req, request); + + return true; +} + + - 3.1.1.3.1.4.2) (->4))Valve#invoke + +``` +public void invoke(Request request, Response response) + throws IOException, ServletException; +``` + + +- connector.getService().getContainer().getPipeline().getFirst().invoke() 会将请求传递到 Container 处理,当然了 Container 处理也是在 Worker 线程中执行的,但是这是一个相对独立的模块,所以单独分出来一节。 + +- 第一个Container#Valve是StandardEngineValve。 +- 按照这样的顺序:engine->host->context->wrapper。 + - 3.1.1.3.1.4.3) Request#finishRequest(非异步Servlet被调用) + +``` +public void finishRequest() throws IOException { + if (response.getStatus() == HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE) { + checkSwallowInput(); + } +} +``` + + - 3.1.1.3.1.4.4) Response#finishResponse(非异步Servlet被调用) + +``` +public void finishResponse() throws IOException { + // Writing leftover bytes + outputBuffer.close(); +} +``` + + - 3.1.1.3.1.4.5) Request#recycle(非异步Servlet被调用,释放资源,待被复用) + + +``` +/** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ +public void recycle() { + + internalDispatcherType = null; + requestDispatcherPath = null; + + authType = null; + inputBuffer.recycle(); + usingInputStream = false; + usingReader = false; + userPrincipal = null; + subject = null; + parametersParsed = false; + if (parts != null) { + for (Part part: parts) { + try { + part.delete(); + } catch (IOException ignored) { + // ApplicationPart.delete() never throws an IOEx + } + } + parts = null; + } + partsParseException = null; + locales.clear(); + localesParsed = false; + secure = false; + remoteAddr = null; + remoteHost = null; + remotePort = -1; + localPort = -1; + localAddr = null; + localName = null; + + attributes.clear(); + sslAttributesParsed = false; + notes.clear(); + + recycleSessionInfo(); + recycleCookieInfo(false); + + if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) { + parameterMap = new ParameterMap<>(); + } else { + parameterMap.setLocked(false); + parameterMap.clear(); + } + + mappingData.recycle(); + applicationMapping.recycle(); + + applicationRequest = null; + if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) { + if (facade != null) { + facade.clear(); + facade = null; + } + if (inputStream != null) { + inputStream.clear(); + inputStream = null; + } + if (reader != null) { + reader.clear(); + reader = null; + } + } + + asyncSupported = null; + if (asyncContext!=null) { + asyncContext.recycle(); + } + asyncContext = null; +} +``` + + + - 3.1.1.3.1.4.6) Response#recycle(非异步Servlet被调用,释放资源,待被复用) + +``` +/** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ +public void recycle() { + + cookies.clear(); + outputBuffer.recycle(); + usingOutputStream = false; + usingWriter = false; + appCommitted = false; + included = false; + isCharacterEncodingSet = false; + + applicationResponse = null; + if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) { + if (facade != null) { + facade.clear(); + facade = null; + } + if (outputStream != null) { + outputStream.clear(); + outputStream = null; + } + if (writer != null) { + writer.clear(); + writer = null; + } + } else if (writer != null) { + writer.recycle(); + } + +} +``` + + + - 3.1.1.3.1.5) endRequest(非异步Servlet被调用) + +``` +/* + * No more input will be passed to the application. Remaining input will be + * swallowed or the connection dropped depending on the error and + * expectation status. + */ +private void endRequest() { + if (getErrorState().isError()) { + // If we know we are closing the connection, don't drain + // input. This way uploading a 100GB file doesn't tie up the + // thread if the servlet has rejected it. + inputBuffer.setSwallowInput(false); + } else { + // Need to check this again here in case the response was + // committed before the error that requires the connection + // to be closed occurred. + checkExpectationAndResponseStatus(); + } + + // Finish the handling of the request + if (getErrorState().isIoAllowed()) { + try { + inputBuffer.endRequest(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // 500 - Internal Server Error + // Can't add a 500 to the access log since that has already been + // written in the Adapter.service method. + response.setStatus(500); + setErrorState(ErrorState.CLOSE_NOW, t); + log.error(sm.getString("http11processor.request.finish"), t); + } + } + if (getErrorState().isIoAllowed()) { + try { + action(ActionCode.COMMIT, null); + outputBuffer.end(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + setErrorState(ErrorState.CLOSE_NOW, t); + log.error(sm.getString("http11processor.response.finish"), t); + } + } +} +``` + + + - 3.1.1.3.1.5.1) Http11InputBuffer#endRequest + - void endRequest() throws IOException { + + if (swallowInput && (lastActiveFilter != -1)) { + int extraBytes = (int) activeFilters[lastActiveFilter].end(); + byteBuffer.position(byteBuffer.position() - extraBytes); + } +} + + - 3.1.1.3.1.5.2) AbstractProcessor#action(COMMIT) +- case COMMIT: { + if (!response.isCommitted()) { + try { + // Validate and write response headers + prepareResponse(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } + } + break; +} + +- Http11Processor#prepareResponse + - protected final void prepareResponse() throws IOException { + + boolean entityBody = true; + contentDelimitation = false; + + OutputFilter[] outputFilters = outputBuffer.getFilters(); + + if (http09 == true) { + // HTTP/0.9 + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + outputBuffer.commit(); + return; + } + + int statusCode = response.getStatus(); + if (statusCode < 200 || statusCode == 204 || statusCode == 205 || + statusCode == 304) { + // No entity body + outputBuffer.addActiveFilter + (outputFilters[Constants.VOID_FILTER]); + entityBody = false; + contentDelimitation = true; + if (statusCode == 205) { + // RFC 7231 requires the server to explicitly signal an empty + // response in this case + response.setContentLength(0); + } else { + response.setContentLength(-1); + } + } + + MessageBytes methodMB = request.method(); + if (methodMB.equals("HEAD")) { + // No entity body + outputBuffer.addActiveFilter + (outputFilters[Constants.VOID_FILTER]); + contentDelimitation = true; + } + + // Sendfile support + if (protocol.getUseSendfile()) { + prepareSendfile(outputFilters); + } + + // Check for compression + + boolean useCompression = false; + if (entityBody && sendfileData == null) { + useCompression = protocol.useCompression(request, response); + } + + MimeHeaders headers = response.getMimeHeaders(); + // A SC_NO_CONTENT response may include entity headers + if (entityBody || statusCode == HttpServletResponse.SC_NO_CONTENT) { + String contentType = response.getContentType(); + if (contentType != null) { + headers.setValue("Content-Type").setString(contentType); + } + String contentLanguage = response.getContentLanguage(); + if (contentLanguage != null) { + headers.setValue("Content-Language") + .setString(contentLanguage); + } + } + + long contentLength = response.getContentLengthLong(); + boolean connectionClosePresent = false; + if (http11 && response.getTrailerFields() != null) { + // If trailer fields are set, always use chunking + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); + } else if (contentLength != -1) { + headers.setValue("Content-Length").setLong(contentLength); + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + contentDelimitation = true; + } else { + // If the response code supports an entity body and we're on + // HTTP 1.1 then we chunk unless we have a Connection: close header + connectionClosePresent = isConnectionClose(headers); + if (http11 && entityBody && !connectionClosePresent) { + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); + } else { + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + } + } + + if (useCompression) { + outputBuffer.addActiveFilter(outputFilters[Constants.GZIP_FILTER]); + } + + // Add date header unless application has already set one (e.g. in a + // Caching Filter) + if (headers.getValue("Date") == null) { + headers.addValue("Date").setString( + FastHttpDateFormat.getCurrentDate()); + } + + // FIXME: Add transfer encoding header + + if ((entityBody) && (!contentDelimitation)) { + // Mark as close the connection after the request, and add the + // connection: close header + keepAlive = false; + } + + // This may disabled keep-alive to check before working out the + // Connection header. + checkExpectationAndResponseStatus(); + + // If we know that the request is bad this early, add the + // Connection: close header. + if (keepAlive && statusDropsConnection(statusCode)) { + keepAlive = false; + } + if (!keepAlive) { + // Avoid adding the close header twice + if (!connectionClosePresent) { + headers.addValue(Constants.CONNECTION).setString( + Constants.CLOSE); + } + } else if (!http11 && !getErrorState().isError()) { + headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE); + } + + // Add server header + String server = protocol.getServer(); + if (server == null) { + if (protocol.getServerRemoveAppProvidedValues()) { + headers.removeHeader("server"); + } + } else { + // server always overrides anything the app might set + headers.setValue("Server").setString(server); + } + + // Build the response header + try { + outputBuffer.sendStatus(); + + int size = headers.size(); + for (int i = 0; i < size; i++) { + outputBuffer.sendHeader(headers.getName(i), headers.getValue(i)); + } + outputBuffer.endHeaders(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // If something goes wrong, reset the header buffer so the error + // response can be written instead. + outputBuffer.resetHeaderBuffer(); + throw t; + } + + outputBuffer.commit(); +} + +- Http11OutputBuffer#commit +- protected void commit() throws IOException { + response.setCommitted(true); + + if (headerBuffer.position() > 0) { + // Sending the response header buffer + headerBuffer.flip(); + try { + socketWrapper.write(isBlocking(), headerBuffer); + } finally { + headerBuffer.position(0).limit(headerBuffer.capacity()); + } + } +} + + + - 3.1,1,3,1,5,3) Http11OutputBuffer#end + +``` +public void end() throws IOException { + if (responseFinished) { + return; + } + + if (lastActiveFilter == -1) { + outputStreamOutputBuffer.end(); + } else { + activeFilters[lastActiveFilter].end(); + } + + responseFinished = true; +} +``` + + + +##### 3.1.1.3.2) asyncPostProcess(异步Servlet) + +``` +public SocketState asyncPostProcess() { + return asyncStateMachine.asyncPostProcess(); +} +``` + + + + - 3.1.1.3.2.1) AsyncStateMachine#asyncPostProcess +- synchronized SocketState asyncPostProcess() { + if (state == AsyncState.COMPLETE_PENDING) { + doComplete(); + return SocketState.ASYNC_END; + } else if (state == AsyncState.DISPATCH_PENDING) { + doDispatch(); + return SocketState.ASYNC_END; + } else if (state == AsyncState.STARTING || state == AsyncState.READ_WRITE_OP) { + state = AsyncState.STARTED; + return SocketState.LONG; + } else if (state == AsyncState.MUST_COMPLETE || state == AsyncState.COMPLETING) { + asyncCtxt.fireOnComplete(); + state = AsyncState.DISPATCHED; + return SocketState.ASYNC_END; + } else if (state == AsyncState.MUST_DISPATCH) { + state = AsyncState.DISPATCHING; + return SocketState.ASYNC_END; + } else if (state == AsyncState.DISPATCHING) { + state = AsyncState.DISPATCHED; + return SocketState.ASYNC_END; + } else if (state == AsyncState.STARTED) { + // This can occur if an async listener does a dispatch to an async + // servlet during onTimeout + return SocketState.LONG; + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", + "asyncPostProcess()", state)); + } +} + +- + + - 4) Container#Valve#invoke(在Worker线程池中执行) + + + +1.- 需要注意的是,基本上每一个容器的 StandardPipeline 上都会有多个已注册的 Valve,我们只关注每个容器的 Basic Valve。其他 Valve 都是在 Basic Valve 前执行。 +2.- request.getHost().getPipeline().getFirst().invoke() 先获取对应的 StandardHost,并执行其 pipeline。 +3.- request.getContext().getPipeline().getFirst().invoke() 先获取对应的 StandardContext,并执行其 pipeline。 +4.- request.getWrapper().getPipeline().getFirst().invoke() 先获取对应的 StandardWrapper,并执行其 pipeline。 +5.- 最值得说的就是 StandardWrapper 的 Basic Valve,StandardWrapperValve +6.- allocate() 用来加载并初始化 Servlet,值的一提的是 Servlet 并不都是单例的,当 Servlet 实现了 SingleThreadModel 接口后,StandardWrapper 会维护一组 Servlet 实例,这是享元模式。当然了 SingleThreadModel 在 Servlet 2.4 以后就弃用了。 +7.- createFilterChain() 方法会从 StandardContext 中获取到所有的过滤器,然后将匹配 Request URL 的所有过滤器挑选出来添加到 filterChain 中。 +8.- doFilter() 执行过滤链,当所有的过滤器都执行完毕后调用 Servlet 的 service() 方法。 + +- 第一个Container#Valve是StandardEngineValve。 +- 按照这样的顺序:engine->host->context->wrapper。 +- 这四个容器都继承自ContainerBase。 +## ContainerBase + +``` +public abstract class ContainerBase extends LifecycleMBeanBase + implements Container { +``` + + +``` +/** + * The Pipeline object with which this Container is associated. + */ +protected final Pipeline pipeline = new StandardPipeline(this); +``` + +- } +- 持有一个StandardPipeline对象。 + +## Pipeline(一个pipeline只能与一个Container关联,多对一) + + +- StandardPipeline 组件代表一个流水线,与 Valve(阀)结合,用于处理请求。 StandardPipeline 中含有多个 Valve, 当需要处理请求时,会逐一调用 Valve 的 invoke 方法对 Request 和 Response 进行处理。特别的,其中有一个特殊的 Valve 叫 basicValve,每一个标准容器都有一个指定的 BasicValve,他们做的是最核心的工作。 + +``` +public class StandardPipeline extends LifecycleBase implements Pipeline { + + private static final Log log = LogFactory.getLog(StandardPipeline.class); + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new StandardPipeline instance with no associated Container. + */ + public StandardPipeline() { + + this(null); + + } + + + /** + * Construct a new StandardPipeline instance that is associated with the + * specified Container. + * + * @param container The container we should be associated with + */ + public StandardPipeline(Container container) { + + super(); + setContainer(container); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The basic Valve (if any) associated with this Pipeline. + */ + protected Valve basic = null; + + + /** + * The Container with which this Pipeline is associated. + */ + protected Container container = null; + + + /** + * The first valve associated with this Pipeline. + */ + protected Valve first = null; +``` + +- } +## Valve(一个pipeline对应着多个Valve,一对多,链表结构) +- Valve是一个接口,其基本实现的BaseValve类。 + + +``` +public abstract class ValveBase extends LifecycleMBeanBase implements Contained, Valve { + + protected static final StringManager sm = StringManager.getManager(ValveBase.class); + + + //------------------------------------------------------ Constructor + + public ValveBase() { + this(false); + } + + + public ValveBase(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + + //------------------------------------------------------ Instance Variables + + /** + * Does this valve support Servlet 3+ async requests? + */ + protected boolean asyncSupported; + + + /** + * The Container whose pipeline this Valve is a component of. + */ + protected Container container = null; + + + /** + * Container log + */ + protected Log containerLog = null; + + + /** + * The next Valve in the pipeline this Valve is a component of. + */ + protected Valve next = null; +``` + +- } + + - 4.1) StandardEngineValve#invoke + +``` +public final void invoke(Request request, Response response) + throws IOException, ServletException { + + // Select the Host to be used for this Request + Host host = request.getHost(); + if (host == null) { + response.sendError + (HttpServletResponse.SC_BAD_REQUEST, + sm.getString("standardEngine.noHost", + request.getServerName())); + return; + } + if (request.isAsyncSupported()) { + request.setAsyncSupported(host.getPipeline().isAsyncSupported()); + } + + // Ask this Host to process this request + host.getPipeline().getFirst().invoke(request, response); + +} +``` + + + - 4.1.1) StandardHostValve#invoke + +``` +public final void invoke(Request request, Response response) + throws IOException, ServletException { + + // Select the Context to be used for this Request + Context context = request.getContext(); + if (context == null) { + return; + } + + if (request.isAsyncSupported()) { + request.setAsyncSupported(context.getPipeline().isAsyncSupported()); + } + + boolean asyncAtStart = request.isAsync(); + + try { + context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + + if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) { + // Don't fire listeners during async processing (the listener + // fired for the request that called startAsync()). + // If a request init listener throws an exception, the request + // is aborted. + return; + } + + // Ask this Context to process this request. Requests that are in + // async mode and are not being dispatched to this resource must be + // in error and have been routed here to check for application + // defined error pages. + try { + if (!response.isErrorReportRequired()) { + context.getPipeline().getFirst().invoke(request, response); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + container.getLogger().error("Exception Processing " + request.getRequestURI(), t); + // If a new error occurred while trying to report a previous + // error allow the original error to be reported. + if (!response.isErrorReportRequired()) { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + throwable(request, response, t); + } + } + + // Now that the request/response pair is back under container + // control lift the suspension so that the error handling can + // complete and/or the container can flush any remaining data + response.setSuspended(false); + + Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // Protect against NPEs if the context was destroyed during a + // long running request. + if (!context.getState().isAvailable()) { + return; + } + + // Look for (and render if found) an application level error page + if (response.isErrorReportRequired()) { + if (t != null) { + throwable(request, response, t); + } else { + status(request, response); + } + } + + if (!request.isAsync() && !asyncAtStart) { + context.fireRequestDestroyEvent(request.getRequest()); + } + } finally { + // Access a session (if present) to update last accessed time, based + // on a strict interpretation of the specification + if (ACCESS_SESSION) { + request.getSession(false); + } + + context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + } +} +``` + + + - 4.1.1.1) StandardContextValve#invoke + +``` +public final void invoke(Request request, Response response) + throws IOException, ServletException { + + // Disallow any direct access to resources under WEB-INF or META-INF + MessageBytes requestPathMB = request.getRequestPathMB(); + if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0)) + || (requestPathMB.equalsIgnoreCase("/META-INF")) + || (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0)) + || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Select the Wrapper to be used for this Request + Wrapper wrapper = request.getWrapper(); + if (wrapper == null || wrapper.isUnavailable()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Acknowledge the request + try { + response.sendAcknowledgement(); + } catch (IOException ioe) { + container.getLogger().error(sm.getString( + "standardContextValve.acknowledgeException"), ioe); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + if (request.isAsyncSupported()) { + request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported()); + } + wrapper.getPipeline().getFirst().invoke(request, response); +} +``` + + - 4.1.1.1.1) (调用Servlet)StandardWrapperValve#invoke +- StandardWrapperValve +1.- allocate() 用来加载并初始化 Servlet,值的一提的是 Servlet 并不都是单例的,当 Servlet 实现了 SingleThreadModel 接口后,StandardWrapper 会维护一组 Servlet 实例,这是享元模式。当然了 SingleThreadModel 在 Servlet 2.4 以后就弃用了。 +2.- createFilterChain() 方法会从 StandardContext 中获取到所有的过滤器,然后将匹配 Request URL 的所有过滤器挑选出来添加到 filterChain 中。 +3.- doFilter() 执行过滤链,当所有的过滤器都执行完毕后调用 Servlet 的 service() 方法。 + + +``` +public final void invoke(Request request, Response response) + throws IOException, ServletException { + + // Initialize local variables we may need + boolean unavailable = false; + Throwable throwable = null; + // This should be a Request attribute... + long t1=System.currentTimeMillis(); + requestCount.incrementAndGet(); + StandardWrapper wrapper = (StandardWrapper) getContainer(); + Servlet servlet = null; + Context context = (Context) wrapper.getParent(); + + // Check for the application being marked unavailable + if (!context.getState().isAvailable()) { + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardContext.isUnavailable")); + unavailable = true; + } + + // Check for the servlet being marked unavailable + if (!unavailable && wrapper.isUnavailable()) { + container.getLogger().info(sm.getString("standardWrapper.isUnavailable", + wrapper.getName())); + long available = wrapper.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) { + response.setDateHeader("Retry-After", available); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardWrapper.isUnavailable", + wrapper.getName())); + } else if (available == Long.MAX_VALUE) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("standardWrapper.notFound", + wrapper.getName())); + } + unavailable = true; + } + + // Allocate a servlet instance to process this request + try { + if (!unavailable) { + servlet = wrapper.allocate(); + } + } catch (UnavailableException e) { + container.getLogger().error( + sm.getString("standardWrapper.allocateException", + wrapper.getName()), e); + long available = wrapper.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) { + response.setDateHeader("Retry-After", available); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardWrapper.isUnavailable", + wrapper.getName())); + } else if (available == Long.MAX_VALUE) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("standardWrapper.notFound", + wrapper.getName())); + } + } catch (ServletException e) { + container.getLogger().error(sm.getString("standardWrapper.allocateException", + wrapper.getName()), StandardWrapper.getRootCause(e)); + throwable = e; + exception(request, response, e); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.allocateException", + wrapper.getName()), e); + throwable = e; + exception(request, response, e); + servlet = null; + } + + MessageBytes requestPathMB = request.getRequestPathMB(); + DispatcherType dispatcherType = DispatcherType.REQUEST; + if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC; + request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType); + request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, + requestPathMB); + // Create the filter chain for this request + ApplicationFilterChain filterChain = + ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); + + // Call the filter chain for this request + // NOTE: This also calls the servlet's service() method + try { + if ((servlet != null) && (filterChain != null)) { + // Swallow output if needed + if (context.getSwallowOutput()) { + try { + SystemLogHandler.startCapture(); + if (request.isAsyncDispatching()) { + request.getAsyncContextInternal().doInternalDispatch(); + } else { + filterChain.doFilter(request.getRequest(), + response.getResponse()); + } + } finally { + String log = SystemLogHandler.stopCapture(); + if (log != null && log.length() > 0) { + context.getLogger().info(log); + } + } + } else { + if (request.isAsyncDispatching()) { + request.getAsyncContextInternal().doInternalDispatch(); + } else { + filterChain.doFilter + (request.getRequest(), response.getResponse()); + } + } + + } + } catch (ClientAbortException e) { + throwable = e; + exception(request, response, e); + } catch (IOException e) { + container.getLogger().error(sm.getString( + "standardWrapper.serviceException", wrapper.getName(), + context.getName()), e); + throwable = e; + exception(request, response, e); + } catch (UnavailableException e) { + container.getLogger().error(sm.getString( + "standardWrapper.serviceException", wrapper.getName(), + context.getName()), e); + // throwable = e; + // exception(request, response, e); + wrapper.unavailable(e); + long available = wrapper.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) { + response.setDateHeader("Retry-After", available); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardWrapper.isUnavailable", + wrapper.getName())); + } else if (available == Long.MAX_VALUE) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("standardWrapper.notFound", + wrapper.getName())); + } + // Do not save exception in 'throwable', because we + // do not want to do exception(request, response, e) processing + } catch (ServletException e) { + Throwable rootCause = StandardWrapper.getRootCause(e); + if (!(rootCause instanceof ClientAbortException)) { + container.getLogger().error(sm.getString( + "standardWrapper.serviceExceptionRoot", + wrapper.getName(), context.getName(), e.getMessage()), + rootCause); + } + throwable = e; + exception(request, response, e); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString( + "standardWrapper.serviceException", wrapper.getName(), + context.getName()), e); + throwable = e; + exception(request, response, e); + } + + // Release the filter chain (if any) for this request + if (filterChain != null) { + filterChain.release(); + } + + // Deallocate the allocated servlet instance + try { + if (servlet != null) { + wrapper.deallocate(servlet); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.deallocateException", + wrapper.getName()), e); + if (throwable == null) { + throwable = e; + exception(request, response, e); + } + } + + // If this servlet has been marked permanently unavailable, + // unload it and release this instance + try { + if ((servlet != null) && + (wrapper.getAvailable() == Long.MAX_VALUE)) { + wrapper.unload(); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.unloadException", + wrapper.getName()), e); + if (throwable == null) { + throwable = e; + exception(request, response, e); + } + } + long t2=System.currentTimeMillis(); + + long time=t2-t1; + processingTime += time; + if( time > maxTime) maxTime=time; + if( time < minTime) minTime=time; + +} +``` + + + - 4.1.1.1.1.1) StandardWrapper#allocate(创建servlet实例) + +``` +public Servlet allocate() throws ServletException { + + // If we are currently unloading this servlet, throw an exception + if (unloading) { + throw new ServletException(sm.getString("standardWrapper.unloading", getName())); + } + + boolean newInstance = false; + + // If not SingleThreadedModel, return the same instance every time + if (!singleThreadModel) { + // Load and initialize our instance if necessary + if (instance == null || !instanceInitialized) { + synchronized (this) { + if (instance == null) { + try { + if (log.isDebugEnabled()) { + log.debug("Allocating non-STM instance"); + } + + // Note: We don't know if the Servlet implements + // SingleThreadModel until we have loaded it. + instance = loadServlet(); + newInstance = true; + if (!singleThreadModel) { + // For non-STM, increment here to prevent a race + // condition with unload. Bug 43683, test case + // #3 + countAllocated.incrementAndGet(); + } + } catch (ServletException e) { + throw e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("standardWrapper.allocate"), e); + } + } + if (!instanceInitialized) { + initServlet(instance); + } + } + } + + if (singleThreadModel) { + if (newInstance) { + // Have to do this outside of the sync above to prevent a + // possible deadlock + synchronized (instancePool) { + instancePool.push(instance); + nInstances++; + } + } + } else { + if (log.isTraceEnabled()) { + log.trace(" Returning non-STM instance"); + } + // For new instances, count will have been incremented at the + // time of creation + if (!newInstance) { + countAllocated.incrementAndGet(); + } + return instance; + } + } + + synchronized (instancePool) { + while (countAllocated.get() >= nInstances) { + // Allocate a new instance if possible, or else wait + if (nInstances < maxInstances) { + try { + instancePool.push(loadServlet()); + nInstances++; + } catch (ServletException e) { + throw e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("standardWrapper.allocate"), e); + } + } else { + try { + instancePool.wait(); + } catch (InterruptedException e) { + // Ignore + } + } + } + if (log.isTraceEnabled()) { + log.trace(" Returning allocated STM instance"); + } + countAllocated.incrementAndGet(); + return instancePool.pop(); + } +} +``` + + + - 4.1.1.1.1.1.1) StandardWrapper#loadServlet + +``` +public synchronized Servlet loadServlet() throws ServletException { + + // Nothing to do if we already have an instance or an instance pool + if (!singleThreadModel && (instance != null)) + return instance; + + PrintStream out = System.out; + if (swallowOutput) { + SystemLogHandler.startCapture(); + } + + Servlet servlet; + try { + long t1=System.currentTimeMillis(); + // Complain if no servlet class has been specified + if (servletClass == null) { + unavailable(null); + throw new ServletException + (sm.getString("standardWrapper.notClass", getName())); + } + + InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager(); + try { + servlet = (Servlet) instanceManager.newInstance(servletClass); + } catch (ClassCastException e) { + unavailable(null); + // Restore the context ClassLoader + throw new ServletException + (sm.getString("standardWrapper.notServlet", servletClass), e); + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + unavailable(null); + + // Added extra log statement for Bugzilla 36630: + // http://bz.apache.org/bugzilla/show_bug.cgi?id=36630 + if(log.isDebugEnabled()) { + log.debug(sm.getString("standardWrapper.instantiate", servletClass), e); + } + + // Restore the context ClassLoader + throw new ServletException + (sm.getString("standardWrapper.instantiate", servletClass), e); + } + + if (multipartConfigElement == null) { + MultipartConfig annotation = + servlet.getClass().getAnnotation(MultipartConfig.class); + if (annotation != null) { + multipartConfigElement = + new MultipartConfigElement(annotation); + } + } + + // Special handling for ContainerServlet instances + // Note: The InstanceManager checks if the application is permitted + // to load ContainerServlets + if (servlet instanceof ContainerServlet) { + ((ContainerServlet) servlet).setWrapper(this); + } + + classLoadTime=(int) (System.currentTimeMillis() -t1); + + if (servlet instanceof SingleThreadModel) { + if (instancePool == null) { + instancePool = new Stack<>(); + } + singleThreadModel = true; + } + + initServlet(servlet); + + fireContainerEvent("load", this); + + loadTime=System.currentTimeMillis() -t1; + } finally { + if (swallowOutput) { + String log = SystemLogHandler.stopCapture(); + if (log != null && log.length() > 0) { + if (getServletContext() != null) { + getServletContext().log(log); + } else { + out.println(log); + } + } + } + } + return servlet; + +} +``` + + + - 4.1.1.1.1.1.1.1) StandardWrapper#initServlet + +``` +private synchronized void initServlet(Servlet servlet) + throws ServletException { + + if (instanceInitialized && !singleThreadModel) return; + + // Call the initialization method of this servlet + try { + if( Globals.IS_SECURITY_ENABLED) { + boolean success = false; + try { + Object[] args = new Object[] { facade }; + SecurityUtil.doAsPrivilege("init", + servlet, + classType, + args); + success = true; + } finally { + if (!success) { + // destroy() will not be called, thus clear the reference now + SecurityUtil.remove(servlet); + } + } + } else { + servlet.init(facade); + } + + instanceInitialized = true; + } catch (UnavailableException f) { + unavailable(f); + throw f; + } catch (ServletException f) { + // If the servlet wanted to be unavailable it would have + // said so, so do not call unavailable(null). + throw f; + } catch (Throwable f) { + ExceptionUtils.handleThrowable(f); + getServletContext().log("StandardWrapper.Throwable", f ); + // If the servlet wanted to be unavailable it would have + // said so, so do not call unavailable(null). + throw new ServletException + (sm.getString("standardWrapper.initException", getName()), f); + } +} +``` + + + - 4.1.1.1.1.2) ApplicationFilterFactory#createFilterChain + +``` +public static ApplicationFilterChain createFilterChain(ServletRequest request, + Wrapper wrapper, Servlet servlet) { + + // If there is no servlet to execute, return null + if (servlet == null) + return null; + + // Create and initialize a filter chain object + ApplicationFilterChain filterChain = null; + if (request instanceof Request) { + Request req = (Request) request; + if (Globals.IS_SECURITY_ENABLED) { + // Security: Do not recycle + filterChain = new ApplicationFilterChain(); + } else { + filterChain = (ApplicationFilterChain) req.getFilterChain(); + if (filterChain == null) { + filterChain = new ApplicationFilterChain(); + req.setFilterChain(filterChain); + } + } + } else { + // Request dispatcher in use + filterChain = new ApplicationFilterChain(); + } + + filterChain.setServlet(servlet); + filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); + + // Acquire the filter mappings for this Context + StandardContext context = (StandardContext) wrapper.getParent(); + FilterMap filterMaps[] = context.findFilterMaps(); + + // If there are no filter mappings, we are done + if ((filterMaps == null) || (filterMaps.length == 0)) + return filterChain; + + // Acquire the information we will need to match filter mappings + DispatcherType dispatcher = + (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); + + String requestPath = null; + Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR); + if (attribute != null){ + requestPath = attribute.toString(); + } + + String servletName = wrapper.getName(); + + // Add the relevant path-mapped filters to this filter chain + for (int i = 0; i < filterMaps.length; i++) { + if (!matchDispatcher(filterMaps[i] ,dispatcher)) { + continue; + } + if (!matchFiltersURL(filterMaps[i], requestPath)) + continue; + ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) + context.findFilterConfig(filterMaps[i].getFilterName()); + if (filterConfig == null) { + // FIXME - log configuration problem + continue; + } + filterChain.addFilter(filterConfig); + } + + // Add filters that match on servlet name second + for (int i = 0; i < filterMaps.length; i++) { + if (!matchDispatcher(filterMaps[i] ,dispatcher)) { + continue; + } + if (!matchFiltersServlet(filterMaps[i], servletName)) + continue; + ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) + context.findFilterConfig(filterMaps[i].getFilterName()); + if (filterConfig == null) { + // FIXME - log configuration problem + continue; + } + filterChain.addFilter(filterConfig); + } + + // Return the completed filter chain + return filterChain; +} +``` + + + - 4.1.1.1.1.3) ApplicationFilterChain#doFilter + +``` +public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + + if( Globals.IS_SECURITY_ENABLED ) { + final ServletRequest req = request; + final ServletResponse res = response; + try { + java.security.AccessController.doPrivileged( + new java.security.PrivilegedExceptionAction<Void>() { + @Override + public Void run() + throws ServletException, IOException { + internalDoFilter(req,res); + return null; + } + } + ); + } catch( PrivilegedActionException pe) { + Exception e = pe.getException(); + if (e instanceof ServletException) + throw (ServletException) e; + else if (e instanceof IOException) + throw (IOException) e; + else if (e instanceof RuntimeException) + throw (RuntimeException) e; + else + throw new ServletException(e.getMessage(), e); + } + } else { + internalDoFilter(request,response); + } +} +``` + + + - 4.1.1.1.1.3.1) ApplicationFilterChain#internalDoFilter(这里是起个头,后续doFilter是在用户Filter中调用的) + +``` +private void internalDoFilter(ServletRequest request, + ServletResponse response) + throws IOException, ServletException { + + // Call the next filter if there is one + if (pos < n) { + ApplicationFilterConfig filterConfig = filters[pos++]; + try { + Filter filter = filterConfig.getFilter(); + + if (request.isAsyncSupported() && "false".equalsIgnoreCase( + filterConfig.getFilterDef().getAsyncSupported())) { + request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); + } + if( Globals.IS_SECURITY_ENABLED ) { + final ServletRequest req = request; + final ServletResponse res = response; + Principal principal = + ((HttpServletRequest) req).getUserPrincipal(); + + Object[] args = new Object[]{req, res, this}; + SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); + } else { + filter.doFilter(request, response, this); + } + } catch (IOException | ServletException | RuntimeException e) { + throw e; + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("filterChain.filter"), e); + } + return; + } + + // We fell off the end of the chain -- call the servlet instance + try { + if (ApplicationDispatcher.WRAP_SAME_OBJECT) { + lastServicedRequest.set(request); + lastServicedResponse.set(response); + } + + if (request.isAsyncSupported() && !servletSupportsAsync) { + request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, + Boolean.FALSE); + } + // Use potentially wrapped request from this point + if ((request instanceof HttpServletRequest) && + (response instanceof HttpServletResponse) && + Globals.IS_SECURITY_ENABLED ) { + final ServletRequest req = request; + final ServletResponse res = response; + Principal principal = + ((HttpServletRequest) req).getUserPrincipal(); + Object[] args = new Object[]{req, res}; + SecurityUtil.doAsPrivilege("service", + servlet, + classTypeUsedInService, + args, + principal); + } else { + servlet.service(request, response); + } + } catch (IOException | ServletException | RuntimeException e) { + throw e; + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("filterChain.servlet"), e); + } finally { + if (ApplicationDispatcher.WRAP_SAME_OBJECT) { + lastServicedRequest.set(null); + lastServicedResponse.set(null); + } + } +} +``` + + + - 4.1.1.1.1.3.1.1) DefaultServlet#service(处理静态资源,如果任何servlet都无法匹配,则转向该servlet) + +- protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (req.getDispatcherType() == DispatcherType.ERROR) { + doGet(req, resp); + } else { + super.service(req, resp); + } +} + +- protected void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + // Serve the requested resource, including the data content + serveResource(request, response, true, fileEncoding); + +} + + - 4.1.1.1.1.3.1.1.1) DefaultServlet#serveResource +- 首先会判断要请求的资源是否存在,文件是否可读,之后,根据资源的类型,设置响应头的content-type,判断文件的时间,设置超时时间等,最终是流的读写。 + +``` +/** + * Serve the specified resource, optionally including the data content. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param content Should the content be included? + * @param encoding The encoding to use if it is necessary to access the + * source as characters rather than as bytes + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ +protected void serveResource(HttpServletRequest request, + HttpServletResponse response, + boolean content, + String encoding) + throws IOException, ServletException { + + boolean serveContent = content; + + // Identify the requested resource path + String path = getRelativePath(request, true); + + if (debug > 0) { + if (serveContent) + log("DefaultServlet.serveResource: Serving resource '" + + path + "' headers and data"); + else + log("DefaultServlet.serveResource: Serving resource '" + + path + "' headers only"); + } + + if (path.length() == 0) { + // Context root redirect + doDirectoryRedirect(request, response); + return; + } + + WebResource resource = resources.getResource(path); + boolean isError = DispatcherType.ERROR == request.getDispatcherType(); + + if (!resource.exists()) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_REQUEST_URI); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } else { + // We're included + // SRV.9.3 says we must throw a FNFE + throw new FileNotFoundException(sm.getString( + "defaultServlet.missingResource", requestUri)); + } + + if (isError) { + response.sendError(((Integer) request.getAttribute( + RequestDispatcher.ERROR_STATUS_CODE)).intValue()); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri); + } + return; + } + + if (!resource.canRead()) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_REQUEST_URI); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } else { + // We're included + // Spec doesn't say what to do in this case but a FNFE seems + // reasonable + throw new FileNotFoundException(sm.getString( + "defaultServlet.missingResource", requestUri)); + } + + if (isError) { + response.sendError(((Integer) request.getAttribute( + RequestDispatcher.ERROR_STATUS_CODE)).intValue()); + } else { + response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri); + } + return; + } + + // If the resource is not a collection, and the resource path + // ends with "/" or "\", return NOT FOUND + if (resource.isFile() && (path.endsWith("/") || path.endsWith("\\"))) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_REQUEST_URI); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } + response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri); + return; + } + + boolean included = false; + // Check if the conditions specified in the optional If headers are + // satisfied. + if (resource.isFile()) { + // Checking If headers + included = (request.getAttribute( + RequestDispatcher.INCLUDE_CONTEXT_PATH) != null); + if (!included && !isError && !checkIfHeaders(request, response, resource)) { + return; + } + } + + // Find content type. + String contentType = resource.getMimeType(); + if (contentType == null) { + contentType = getServletContext().getMimeType(resource.getName()); + resource.setMimeType(contentType); + } + + // These need to reflect the original resource, not the potentially + // precompressed version of the resource so get them now if they are going to + // be needed later + String eTag = null; + String lastModifiedHttp = null; + if (resource.isFile() && !isError) { + eTag = resource.getETag(); + lastModifiedHttp = resource.getLastModifiedHttp(); + } + + + // Serve a precompressed version of the file if present + boolean usingPrecompressedVersion = false; + if (compressionFormats.length > 0 && !included && resource.isFile() && + !pathEndsWithCompressedExtension(path)) { + List<PrecompressedResource> precompressedResources = + getAvailablePrecompressedResources(path); + if (!precompressedResources.isEmpty()) { + Collection<String> varyHeaders = response.getHeaders("Vary"); + boolean addRequired = true; + for (String varyHeader : varyHeaders) { + if ("*".equals(varyHeader) || + "accept-encoding".equalsIgnoreCase(varyHeader)) { + addRequired = false; + break; + } + } + if (addRequired) { + response.addHeader("Vary", "accept-encoding"); + } + PrecompressedResource bestResource = + getBestPrecompressedResource(request, precompressedResources); + if (bestResource != null) { + response.addHeader("Content-Encoding", bestResource.format.encoding); + resource = bestResource.resource; + usingPrecompressedVersion = true; + } + } + } + + ArrayList<Range> ranges = null; + long contentLength = -1L; + + if (resource.isDirectory()) { + if (!path.endsWith("/")) { + doDirectoryRedirect(request, response); + return; + } + + // Skip directory listings if we have been configured to + // suppress them + if (!listings) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + request.getRequestURI()); + return; + } + contentType = "text/html;charset=UTF-8"; + } else { + if (!isError) { + if (useAcceptRanges) { + // Accept ranges header + response.setHeader("Accept-Ranges", "bytes"); + } + + // Parse range specifier + ranges = parseRange(request, response, resource); + + // ETag header + response.setHeader("ETag", eTag); + + // Last-Modified header + response.setHeader("Last-Modified", lastModifiedHttp); + } + + // Get content length + contentLength = resource.getContentLength(); + // Special case for zero length files, which would cause a + // (silent) ISE when setting the output buffer size + if (contentLength == 0L) { + serveContent = false; + } + } + + ServletOutputStream ostream = null; + PrintWriter writer = null; + + if (serveContent) { + // Trying to retrieve the servlet output stream + try { + ostream = response.getOutputStream(); + } catch (IllegalStateException e) { + // If it fails, we try to get a Writer instead if we're + // trying to serve a text file + if (!usingPrecompressedVersion && + ((contentType == null) || + (contentType.startsWith("text")) || + (contentType.endsWith("xml")) || + (contentType.contains("/javascript"))) + ) { + writer = response.getWriter(); + // Cannot reliably serve partial content with a Writer + ranges = FULL; + } else { + throw e; + } + } + } + + // Check to see if a Filter, Valve of wrapper has written some content. + // If it has, disable range requests and setting of a content length + // since neither can be done reliably. + ServletResponse r = response; + long contentWritten = 0; + while (r instanceof ServletResponseWrapper) { + r = ((ServletResponseWrapper) r).getResponse(); + } + if (r instanceof ResponseFacade) { + contentWritten = ((ResponseFacade) r).getContentWritten(); + } + if (contentWritten > 0) { + ranges = FULL; + } + + if (resource.isDirectory() || + isError || + ( (ranges == null || ranges.isEmpty()) + && request.getHeader("Range") == null ) || + ranges == FULL ) { + + // Set the appropriate output headers + if (contentType != null) { + if (debug > 0) + log("DefaultServlet.serveFile: contentType='" + + contentType + "'"); + response.setContentType(contentType); + } + if (resource.isFile() && contentLength >= 0 && + (!serveContent || ostream != null)) { + if (debug > 0) + log("DefaultServlet.serveFile: contentLength=" + + contentLength); + // Don't set a content length if something else has already + // written to the response. + if (contentWritten == 0) { + response.setContentLengthLong(contentLength); + } + } + + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + InputStream renderResult = null; + if (ostream == null) { + // Output via a writer so can't use sendfile or write + // content directly. + if (resource.isDirectory()) { + renderResult = render(getPathPrefix(request), resource, encoding); + } else { + renderResult = resource.getInputStream(); + } + copy(resource, renderResult, writer, encoding); + } else { + // Output is via an InputStream + if (resource.isDirectory()) { + renderResult = render(getPathPrefix(request), resource, encoding); + } else { + // Output is content of resource + if (!checkSendfile(request, response, resource, + contentLength, null)) { + // sendfile not possible so check if resource + // content is available directly + byte[] resourceBody = resource.getContent(); + if (resourceBody == null) { + // Resource content not available, use + // inputstream + renderResult = resource.getInputStream(); + } else { + // Use the resource content directly + ostream.write(resourceBody); + } + } + } + // If a stream was configured, it needs to be copied to + // the output (this method closes the stream) + if (renderResult != null) { + copy(resource, renderResult, ostream); + } + } + } + + } else { + + if ((ranges == null) || (ranges.isEmpty())) + return; + + // Partial content response. + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + + if (ranges.size() == 1) { + + Range range = ranges.get(0); + response.addHeader("Content-Range", "bytes " + + range.start + + "-" + range.end + "/" + + range.length); + long length = range.end - range.start + 1; + response.setContentLengthLong(length); + + if (contentType != null) { + if (debug > 0) + log("DefaultServlet.serveFile: contentType='" + + contentType + "'"); + response.setContentType(contentType); + } + + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + if (!checkSendfile(request, response, resource, + range.end - range.start + 1, range)) + copy(resource, ostream, range); + } else { + // we should not get here + throw new IllegalStateException(); + } + } + } else { + response.setContentType("multipart/byteranges; boundary=" + + mimeSeparation); + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + copy(resource, ostream, ranges.iterator(), contentType); + } else { + // we should not get here + throw new IllegalStateException(); + } + } + } + } +} +``` + + +- + + +# Mapper +- 在Tomcat中,当一个请求到达时,该请求最终由哪个Servlet来处理呢?这个任务是由Mapper路由映射器完成的。Mapper是由Service管理。 +# 存储结构 + +# MapElement(基类) + +``` +protected abstract static class MapElement<T> { + + public final String name; + public final T object; + + public MapElement(String name, T object) { + this.name = name; + this.object = object; + } +} +``` + +# MappedHost + +``` +protected static final class MappedHost extends MapElement<Host> { + + public volatile ContextList contextList; + + /** + * Link to the "real" MappedHost, shared by all aliases. + */ + private final MappedHost realHost; + + /** + * Links to all registered aliases, for easy enumeration. This field + * is available only in the "real" MappedHost. In an alias this field + * is <code>null</code>. + */ + private final List<MappedHost> aliases; + + /** + * Constructor used for the primary Host + * + * @param name The name of the virtual host + * @param host The host + */ + public MappedHost(String name, Host host) { + super(name, host); + realHost = this; + contextList = new ContextList(); + aliases = new CopyOnWriteArrayList<>(); + } +``` + +- } + +# MappedContext + +``` +protected static final class MappedContext extends MapElement<Void> { + public volatile ContextVersion[] versions; + + public MappedContext(String name, ContextVersion firstVersion) { + super(name, null); + this.versions = new ContextVersion[] { firstVersion }; + } +} +``` + + +- 其中ContextVersion包含了Context下的所有Servlet,有多种映射方式,如精确的map,通配符的map,扩展名的map,如下: + + +``` +protected static final class ContextVersion extends MapElement<Context> { + public final String path; + public final int slashCount; + public final WebResourceRoot resources; + public String[] welcomeResources; + public MappedWrapper defaultWrapper = null; +``` + + +``` +// 精确匹配 + public MappedWrapper[] exactWrappers = new MappedWrapper[0]; +``` + + +``` +// 通配符匹配 + public MappedWrapper[] wildcardWrappers = new MappedWrapper[0]; +``` + + +``` +// 基于扩展名的匹配 + public MappedWrapper[] extensionWrappers = new MappedWrapper[0]; + public int nesting = 0; + private volatile boolean paused; + + public ContextVersion(String version, String path, int slashCount, + Context context, WebResourceRoot resources, + String[] welcomeResources) { + super(version, context); + this.path = path; + this.slashCount = slashCount; + this.resources = resources; + this.welcomeResources = welcomeResources; + } +``` + +- } + +# MappedWrapper + +``` +protected static class MappedWrapper extends MapElement<Wrapper> { + + public final boolean jspWildCard; + public final boolean resourceOnly; + + public MappedWrapper(String name, Wrapper wrapper, boolean jspWildCard, + boolean resourceOnly) { + super(name, wrapper); + this.jspWildCard = jspWildCard; + this.resourceOnly = resourceOnly; + } +} +``` + +# Mapper +- 简单地说,Mapper中以数组的形式保存了host, context, wrapper, 且他们在数组中有序的,Mapper可以通过请求的url,通过二分法查找定位到wrapper。 + +``` +public final class Mapper { + + + private static final Log log = LogFactory.getLog(Mapper.class); + + private static final StringManager sm = StringManager.getManager(Mapper.class); + + // ----------------------------------------------------- Instance Variables + + + /** + * Array containing the virtual hosts definitions. + */ + // Package private to facilitate testing +``` + + +``` +// host数组,host里面又包括了context和wrapper数组 + volatile MappedHost[] hosts = new MappedHost[0]; + + + /** + * Default host name. + */ + private String defaultHostName = null; + private volatile MappedHost defaultHost = null; + + + /** + * Mapping from Context object to Context version to support + * RequestDispatcher mappings. + */ + private final Map<Context, ContextVersion> contextObjectToContextVersionMap = + new ConcurrentHashMap<>(); +``` + +- } +- Mapper#addHost + +``` +public synchronized void addHost(String name, String[] aliases, + Host host) { + name = renameWildcardHost(name); + MappedHost[] newHosts = new MappedHost[hosts.length + 1]; + MappedHost newHost = new MappedHost(name, host); + if (insertMap(hosts, newHosts, newHost)) { + hosts = newHosts; + if (newHost.name.equals(defaultHostName)) { + defaultHost = newHost; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHost.success", name)); + } + } else { + MappedHost duplicate = hosts[find(hosts, name)]; + if (duplicate.object == host) { + // The host is already registered in the mapper. + // E.g. it might have been added by addContextVersion() + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHost.sameHost", name)); + } + newHost = duplicate; + } else { + log.error(sm.getString("mapper.duplicateHost", name, + duplicate.getRealHostName())); + // Do not add aliases, as removeHost(hostName) won't be able to + // remove them + return; + } + } + List<MappedHost> newAliases = new ArrayList<>(aliases.length); + for (String alias : aliases) { + alias = renameWildcardHost(alias); + MappedHost newAlias = new MappedHost(alias, newHost); + if (addHostAliasImpl(newAlias)) { + newAliases.add(newAlias); + } + } + newHost.addAliases(newAliases); +} +``` + + +- Mapper#addContextVersion + +``` +public void addContextVersion(String hostName, Host host, String path, + String version, Context context, String[] welcomeResources, + WebResourceRoot resources, Collection<WrapperMappingInfo> wrappers) { + + hostName = renameWildcardHost(hostName); + + MappedHost mappedHost = exactFind(hosts, hostName); + if (mappedHost == null) { + addHost(hostName, new String[0], host); + mappedHost = exactFind(hosts, hostName); + if (mappedHost == null) { + log.error("No host found: " + hostName); + return; + } + } + if (mappedHost.isAlias()) { + log.error("No host found: " + hostName); + return; + } + int slashCount = slashCount(path); + synchronized (mappedHost) { + ContextVersion newContextVersion = new ContextVersion(version, + path, slashCount, context, resources, welcomeResources); + if (wrappers != null) { + addWrappers(newContextVersion, wrappers); + } + + ContextList contextList = mappedHost.contextList; + MappedContext mappedContext = exactFind(contextList.contexts, path); + if (mappedContext == null) { + mappedContext = new MappedContext(path, newContextVersion); + ContextList newContextList = contextList.addContext( + mappedContext, slashCount); + if (newContextList != null) { + updateContextList(mappedHost, newContextList); + contextObjectToContextVersionMap.put(context, newContextVersion); + } + } else { + ContextVersion[] contextVersions = mappedContext.versions; + ContextVersion[] newContextVersions = new ContextVersion[contextVersions.length + 1]; + if (insertMap(contextVersions, newContextVersions, + newContextVersion)) { + mappedContext.versions = newContextVersions; + contextObjectToContextVersionMap.put(context, newContextVersion); + } else { + // Re-registration after Context.reload() + // Replace ContextVersion with the new one + int pos = find(contextVersions, version); + if (pos >= 0 && contextVersions[pos].name.equals(version)) { + contextVersions[pos] = newContextVersion; + contextObjectToContextVersionMap.put(context, newContextVersion); + } + } + } + } + +} +``` + + +- Mapper#addWrapper + +``` +public void addWrapper(String hostName, String contextPath, String version, + String path, Wrapper wrapper, boolean jspWildCard, + boolean resourceOnly) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, + contextPath, version, false); + if (contextVersion == null) { + return; + } + addWrapper(contextVersion, path, wrapper, jspWildCard, resourceOnly); +} +``` + + + +``` +protected void addWrapper(ContextVersion context, String path, + Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) { + + synchronized (context) { + if (path.endsWith("/*")) { + // Wildcard wrapper + String name = path.substring(0, path.length() - 2); + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, + jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.wildcardWrappers; + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.wildcardWrappers = newWrappers; + int slashCount = slashCount(newWrapper.name); + if (slashCount > context.nesting) { + context.nesting = slashCount; + } + } + } else if (path.startsWith("*.")) { + // Extension wrapper + String name = path.substring(2); + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, + jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.extensionWrappers; + MappedWrapper[] newWrappers = + new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.extensionWrappers = newWrappers; + } + } else if (path.equals("/")) { + // Default wrapper + MappedWrapper newWrapper = new MappedWrapper("", wrapper, + jspWildCard, resourceOnly); + context.defaultWrapper = newWrapper; + } else { + // Exact wrapper + final String name; + if (path.length() == 0) { + // Special case for the Context Root mapping which is + // treated as an exact match + name = "/"; + } else { + name = path; + } + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, + jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.exactWrappers; + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.exactWrappers = newWrappers; + } + } + } +} +``` + + +- Mapper#find(查找MapElement) +- // 根据name,查找一个MapElement(host, context, 或者wrapper) + +``` +/** + * Find a map element given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ +private static final <T> int find(MapElement<T>[] map, CharChunk name) { + return find(map, name, name.getStart(), name.getEnd()); +} +``` + + + +``` +/** + * Find a map element given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ +private static final <T> int find(MapElement<T>[] map, CharChunk name, + int start, int end) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, start, end, map[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, start, end, map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, start, end, map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } +} +``` + + + +``` +/** + * Compare given char chunk with String. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ +private static final int compare(CharChunk name, int start, int end, + String compareTo) { + int result = 0; + char[] c = name.getBuffer(); + int len = compareTo.length(); + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (c[i + start] > compareTo.charAt(i)) { + result = 1; + } else if (c[i + start] < compareTo.charAt(i)) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length() > (end - start)) { + result = -1; + } else if (compareTo.length() < (end - start)) { + result = 1; + } + } + return result; +} +``` + +- Mapper#exactFind(精确查找MapElement) + +``` +private static final <T, E extends MapElement<T>> E exactFind(E[] map, + String name) { + int pos = find(map, name); + if (pos >= 0) { + E result = map[pos]; + if (name.equals(result.name)) { + return result; + } + } + return null; +} +``` + + +- Mapper#map + +``` +public void map(MessageBytes host, MessageBytes uri, String version, + MappingData mappingData) throws IOException { + + if (host.isNull()) { + host.getCharChunk().append(defaultHostName); + } + host.toChars(); + uri.toChars(); + internalMap(host.getCharChunk(), uri.getCharChunk(), version, + mappingData); +} +``` + + +- MappingData是Request中的域 +## internalMap(查找host和context) + +``` +private final void internalMap(CharChunk host, CharChunk uri, + String version, MappingData mappingData) throws IOException { + + if (mappingData.host != null) { + // The legacy code (dating down at least to Tomcat 4.1) just + // skipped all mapping work in this case. That behaviour has a risk + // of returning an inconsistent result. + // I do not see a valid use case for it. + throw new AssertionError(); + } + + // Virtual host mapping + MappedHost[] hosts = this.hosts; + MappedHost mappedHost = exactFindIgnoreCase(hosts, host); + if (mappedHost == null) { + // Note: Internally, the Mapper does not use the leading * on a + // wildcard host. This is to allow this shortcut. + int firstDot = host.indexOf('.'); + if (firstDot > -1) { + int offset = host.getOffset(); + try { + host.setOffset(firstDot + offset); + mappedHost = exactFindIgnoreCase(hosts, host); + } finally { + // Make absolutely sure this gets reset + host.setOffset(offset); + } + } + if (mappedHost == null) { + mappedHost = defaultHost; + if (mappedHost == null) { + return; + } + } + } +``` + + - // 设置host + mappingData.host = mappedHost.object; + + if (uri.isNull()) { + // Can't map context or wrapper without a uri + return; + } + + uri.setLimit(-1); + + // Context mapping + ContextList contextList = mappedHost.contextList; + MappedContext[] contexts = contextList.contexts; + int pos = find(contexts, uri); + if (pos == -1) { + return; + } + + int lastSlash = -1; + int uriEnd = uri.getEnd(); + int length = -1; + boolean found = false; + MappedContext context = null; + while (pos >= 0) { + context = contexts[pos]; + if (uri.startsWith(context.name)) { + length = context.name.length(); + if (uri.getLength() == length) { + found = true; + break; + } else if (uri.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = nthSlash(uri, contextList.nesting + 1); + } else { + lastSlash = lastSlash(uri); + } + uri.setEnd(lastSlash); + pos = find(contexts, uri); + } + uri.setEnd(uriEnd); + + if (!found) { + if (contexts[0].name.equals("")) { + context = contexts[0]; + } else { + context = null; + } + } + if (context == null) { + return; + } + + mappingData.contextPath.setString(context.name); + + ContextVersion contextVersion = null; + ContextVersion[] contextVersions = context.versions; + final int versionCount = contextVersions.length; + if (versionCount > 1) { + Context[] contextObjects = new Context[contextVersions.length]; + for (int i = 0; i < contextObjects.length; i++) { + contextObjects[i] = contextVersions[i].object; + } + mappingData.contexts = contextObjects; + if (version != null) { + contextVersion = exactFind(contextVersions, version); + } + } + if (contextVersion == null) { + // Return the latest version + // The versions array is known to contain at least one element + contextVersion = contextVersions[versionCount - 1]; + } + mappingData.context = contextVersion.object; + mappingData.contextSlashCount = contextVersion.slashCount; + + // Wrapper mapping + if (!contextVersion.isPaused()) { + internalMapWrapper(contextVersion, uri, mappingData); + } + +} + +### internalMapWrapper(查找Wrapper) + +``` +private final void internalMapWrapper(ContextVersion contextVersion, + CharChunk path, + MappingData mappingData) throws IOException { + + int pathOffset = path.getOffset(); + int pathEnd = path.getEnd(); + boolean noServletPath = false; + + int length = contextVersion.path.length(); + if (length == (pathEnd - pathOffset)) { + noServletPath = true; + } + int servletPath = pathOffset + length; + path.setOffset(servletPath); + + // Rule 1 -- Exact Match + MappedWrapper[] exactWrappers = contextVersion.exactWrappers; + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 2 -- Prefix Match + boolean checkJspWelcomeFiles = false; + MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers; + if (mappingData.wrapper == null) { + internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, + path, mappingData); + if (mappingData.wrapper != null && mappingData.jspWildCard) { + char[] buf = path.getBuffer(); + if (buf[pathEnd - 1] == '/') { + /* + * Path ending in '/' was mapped to JSP servlet based on + * wildcard match (e.g., as specified in url-pattern of a + * jsp-property-group. + * Force the context's welcome files, which are interpreted + * as JSP files (since they match the url-pattern), to be + * considered. See Bugzilla 27664. + */ + mappingData.wrapper = null; + checkJspWelcomeFiles = true; + } else { + // See Bugzilla 27704 + mappingData.wrapperPath.setChars(buf, path.getStart(), + path.getLength()); + mappingData.pathInfo.recycle(); + } + } + } + + if(mappingData.wrapper == null && noServletPath && + contextVersion.object.getMapperContextRootRedirectEnabled()) { + // The path is empty, redirect to "/" + path.append('/'); + pathEnd = path.getEnd(); + mappingData.redirectPath.setChars + (path.getBuffer(), pathOffset, pathEnd - pathOffset); + path.setEnd(pathEnd - 1); + return; + } + + // Rule 3 -- Extension Match + MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers; + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + internalMapExtensionWrapper(extensionWrappers, path, mappingData, + true); + } + + // Rule 4 -- Welcome resources processing for servlets + if (mappingData.wrapper == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + char[] buf = path.getBuffer(); + checkWelcomeFiles = (buf[pathEnd - 1] == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < contextVersion.welcomeResources.length) + && (mappingData.wrapper == null); i++) { + path.setOffset(pathOffset); + path.setEnd(pathEnd); + path.append(contextVersion.welcomeResources[i], 0, + contextVersion.welcomeResources[i].length()); + path.setOffset(servletPath); + + // Rule 4a -- Welcome resources processing for exact macth + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 4b -- Welcome resources processing for prefix match + if (mappingData.wrapper == null) { + internalMapWildcardWrapper + (wildcardWrappers, contextVersion.nesting, + path, mappingData); + } + + // Rule 4c -- Welcome resources processing + // for physical folder + if (mappingData.wrapper == null + && contextVersion.resources != null) { + String pathStr = path.toString(); + WebResource file = + contextVersion.resources.getResource(pathStr); + if (file != null && file.isFile()) { + internalMapExtensionWrapper(extensionWrappers, path, + mappingData, true); + if (mappingData.wrapper == null + && contextVersion.defaultWrapper != null) { + mappingData.wrapper = + contextVersion.defaultWrapper.object; + mappingData.requestPath.setChars + (path.getBuffer(), path.getStart(), + path.getLength()); + mappingData.wrapperPath.setChars + (path.getBuffer(), path.getStart(), + path.getLength()); + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + } + + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + + } + + /* welcome file processing - take 2 + * Now that we have looked for welcome files with a physical + * backing, now look for an extension mapping listed + * but may not have a physical backing to it. This is for + * the case of index.jsf, index.do, etc. + * A watered down version of rule 4 + */ + if (mappingData.wrapper == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + char[] buf = path.getBuffer(); + checkWelcomeFiles = (buf[pathEnd - 1] == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < contextVersion.welcomeResources.length) + && (mappingData.wrapper == null); i++) { + path.setOffset(pathOffset); + path.setEnd(pathEnd); + path.append(contextVersion.welcomeResources[i], 0, + contextVersion.welcomeResources[i].length()); + path.setOffset(servletPath); + internalMapExtensionWrapper(extensionWrappers, path, + mappingData, false); + } + + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + } + + + // Rule 7 -- Default servlet + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + if (contextVersion.defaultWrapper != null) { + mappingData.wrapper = contextVersion.defaultWrapper.object; + mappingData.requestPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + mappingData.wrapperPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + mappingData.matchType = MappingMatch.DEFAULT; + } + // Redirection to a folder + char[] buf = path.getBuffer(); + if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') { + String pathStr = path.toString(); + WebResource file; + // Handle context root + if (pathStr.length() == 0) { + file = contextVersion.resources.getResource("/"); + } else { + file = contextVersion.resources.getResource(pathStr); + } + if (file != null && file.isDirectory() && + contextVersion.object.getMapperDirectoryRedirectEnabled()) { + // Note: this mutates the path: do not do any processing + // after this (since we set the redirectPath, there + // shouldn't be any) + path.setOffset(pathOffset); + path.append('/'); + mappingData.redirectPath.setChars + (path.getBuffer(), path.getStart(), path.getLength()); + } else { + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + + path.setOffset(pathOffset); + path.setEnd(pathEnd); +} +``` + +#### internalMapExactWrapper(URL精确匹配) + +``` +private final void internalMapExactWrapper + (MappedWrapper[] wrappers, CharChunk path, MappingData mappingData) { + MappedWrapper wrapper = exactFind(wrappers, path); + if (wrapper != null) { + mappingData.requestPath.setString(wrapper.name); + mappingData.wrapper = wrapper.object; + if (path.equals("/")) { + // Special handling for Context Root mapped servlet + mappingData.pathInfo.setString("/"); + mappingData.wrapperPath.setString(""); + // This seems wrong but it is what the spec says... + mappingData.contextPath.setString(""); + mappingData.matchType = MappingMatch.CONTEXT_ROOT; + } else { + mappingData.wrapperPath.setString(wrapper.name); + mappingData.matchType = MappingMatch.EXACT; + } + } +} +``` + + + +``` +private static final <T, E extends MapElement<T>> E exactFind(E[] map, + CharChunk name) { + int pos = find(map, name); + if (pos >= 0) { + E result = map[pos]; + if (name.equals(result.name)) { + return result; + } + } + return null; +} +``` + + +- + +# Tomcat类加载器 +- Tomcat不能直接使用系统的类加载器,必须要实现自定义的类加载器。servlet应该只允许加载WEB-INF/classes目录及其子目录下的类,和从部署的库到WEB-INF/lib目录加载类,实现不同的应用之间的隔离。另一个要实现自定义类加载器的原因是,为了提供热加载的功能。如果WEB-INF/classes或WEB-INF/lib目录下的类发生变化时,Tomcat应该会重新加载这些类。在Tomcat的类加载中,类加载使用一个额外的线程,不断检查servlet类和其他类的文件的时间戳。Tomcat所有类加载器必须实现Loader接口,支持热加载的还需要实现Reloader接口。 + +- Tomcat类加载器 + +- commonLoader、catalinaLoader和sharedLoader是在Tomcat容器初始化时创建的。catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身的class。 + +- 它们三个都是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties配置文件中配置 +- (common.loader,server.loader,shared.loader). +# 应用隔离 +- 对于每个webapp应用,都会对应唯一的StandContext,在StandContext中会引用WebappLoader,该类又会引用WebappClassLoader,WebappClassLoader就是真正加载webapp的classloader。 + +- WebappClassLoader加载class的步骤如下: +1.- 先检查webappclassloader的缓存是否有该类 +2.- 为防止webapp覆盖java se类,尝试用application classloader(应用类加载器)加载 +3.- 尝试WebappClassLoader自己加载class +4.- 最后无条件地委托给父加载器 common classloader,加载CATALINA_HOME/lib下的类 +5.- 如果都没有加载成功,则抛出ClassNotFoundException异常 + +- WebappClassLoader#loadClass +- 不同的StandardContext有不同的WebappClassLoader,那么不同的webapp的类加载器就是不一致的。加载器的不一致带来了名称空间不一致,所以webapp之间是相互隔离的。 + +``` +public Class<?> loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); +} +``` + + + +``` +public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + + synchronized (getClassLoadingLock(name)) { + if (log.isDebugEnabled()) + log.debug("loadClass(" + name + ", " + resolve + ")"); + Class<?> clazz = null; + + // Log access to stopped class loader + checkStateForClassLoading(name); + + // (0) Check our previously loaded local class cache + clazz = findLoadedClass0(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Returning class from cache"); + if (resolve) + resolveClass(clazz); + return clazz; + } + + // (0.1) Check our previously loaded class cache + clazz = findLoadedClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Returning class from cache"); + if (resolve) + resolveClass(clazz); + return clazz; + } + + // (0.2) Try loading the class with the system class loader, to prevent + // the webapp from overriding Java SE classes. This implements + // SRV.10.7.2 + String resourceName = binaryNameToPath(name, false); + + ClassLoader javaseLoader = getJavaseClassLoader(); + boolean tryLoadingFromJavaseLoader; + try { + // Use getResource as it won't trigger an expensive + // ClassNotFoundException if the resource is not available from + // the Java SE class loader. However (see + // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for + // details) when running under a security manager in rare cases + // this call may trigger a ClassCircularityError. + // See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for + // details of how this may trigger a StackOverflowError + // Given these reported errors, catch Throwable to ensure any + // other edge cases are also caught + tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null); + } catch (Throwable t) { + // Swallow all exceptions apart from those that must be re-thrown + ExceptionUtils.handleThrowable(t); + // The getResource() trick won't work for this class. We have to + // try loading it directly and accept that we might get a + // ClassNotFoundException. + tryLoadingFromJavaseLoader = true; + } + // 使用System ClassLoader加载J2SE的类 + if (tryLoadingFromJavaseLoader) { + try { + clazz = javaseLoader.loadClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + // (0.5) Permission to access this class when using a SecurityManager + if (securityManager != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + try { + securityManager.checkPackageAccess(name.substring(0,i)); + } catch (SecurityException se) { + String error = "Security Violation, attempt to use " + + "Restricted Class: " + name; + log.info(error, se); + throw new ClassNotFoundException(error, se); + } + } + } + + boolean delegateLoad = delegate || filter(name, true); + + // (1) Delegate to our parent if requested + if (delegateLoad) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader1 " + parent); + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from parent"); + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + // (2) Search local repositories + if (log.isDebugEnabled()) + log.debug(" Searching local repositories"); + try { + clazz = findClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from local repository"); + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // (3) Delegate to parent unconditionally + if (!delegateLoad) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader at end: " + parent); + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from parent"); + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + } + + throw new ClassNotFoundException(name); +} +``` + + +# 热部署 +- 后台的定期检查,该定期检查是StandardContext的一个后台线程,会做reload的check,过期session清理等等,这里的modified实际上调用了WebappClassLoader中的方法以判断这个class是不是已经修改。注意到它调用了StandardContext的reload方法。 +- StandardContext#backgroundProcess + +``` +public void backgroundProcess() { + + if (!getState().isAvailable()) + return; + + Loader loader = getLoader(); + if (loader != null) { + try { + loader.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString( + "standardContext.backgroundProcess.loader", loader), e); + } + } + Manager manager = getManager(); + if (manager != null) { + try { + manager.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString( + "standardContext.backgroundProcess.manager", manager), + e); + } + } + WebResourceRoot resources = getResources(); + if (resources != null) { + try { + resources.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString( + "standardContext.backgroundProcess.resources", + resources), e); + } + } + InstanceManager instanceManager = getInstanceManager(); + if (instanceManager != null) { + try { + instanceManager.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString( + "standardContext.backgroundProcess.instanceManager", + resources), e); + } + } + super.backgroundProcess(); +} +``` + + +- WebappLoader#backgroundProcess + +``` +public void backgroundProcess() { + if (reloadable && modified()) { + try { + Thread.currentThread().setContextClassLoader + (WebappLoader.class.getClassLoader()); + if (context != null) { + context.reload(); + } + } finally { + if (context != null && context.getLoader() != null) { + Thread.currentThread().setContextClassLoader + (context.getLoader().getClassLoader()); + } + } + } +} +``` + + +- StandardContext#reload +- Tomcat lifecycle中标准的启停方法stop和start,别忘了,start方法会重新造一个WebappClassLoader并且重复loadOnStartup的过程,从而重新加载了webapp中的类,注意到一般应用很大时,热部署通常会报outofmemory: permgen space not enough之类的,这是由于之前加载进来的class还没有清除而方法区内存又不够的原因 + + +``` +public synchronized void reload() { + + // Validate our current component state + if (!getState().isAvailable()) + throw new IllegalStateException + (sm.getString("standardContext.notStarted", getName())); + + if(log.isInfoEnabled()) + log.info(sm.getString("standardContext.reloadingStarted", + getName())); + + // Stop accepting requests temporarily. + setPaused(true); + + try { + stop(); + } catch (LifecycleException e) { + log.error( + sm.getString("standardContext.stoppingContext", getName()), e); + } + + try { + start(); + } catch (LifecycleException e) { + log.error( + sm.getString("standardContext.startingContext", getName()), e); + } + + setPaused(false); + + if(log.isInfoEnabled()) + log.info(sm.getString("standardContext.reloadingCompleted", + getName())); + +} +``` + +# 异步Servlet +- 入口点是Request#startAsync +- Request#startAsync(开启异步上下文,之后Tomct回收Worker线程) + +``` +public AsyncContext startAsync() { + return startAsync(getRequest(),response.getResponse()); +} +``` + + + + +``` +public AsyncContext startAsync(ServletRequest request, + ServletResponse response) { + if (!isAsyncSupported()) { + IllegalStateException ise = + new IllegalStateException(sm.getString("request.asyncNotSupported")); + log.warn(sm.getString("coyoteRequest.noAsync", + StringUtils.join(getNonAsyncClassNames())), ise); + throw ise; + } + + if (asyncContext == null) { + asyncContext = new AsyncContextImpl(this); + } + + asyncContext.setStarted(getContext(), request, response, + request==getRequest() && response==getResponse().getResponse()); + asyncContext.setTimeout(getConnector().getAsyncTimeout()); + + return asyncContext; +} +``` + + + - 1) AsyncContextImpl#construactor +- 成员变量 +- Tomcat工作线程在Request#startAsync之后,把该异步servlet的后续代码执行完毕后,Tomcat工作线程直接就结束了,也就是返回线程池中了,相当于线程根本不会保存记录信息。 + +``` +public class AsyncContextImpl implements AsyncContext, AsyncContextCallback { + + private static final Log log = LogFactory.getLog(AsyncContextImpl.class); + + protected static final StringManager sm = + StringManager.getManager(Constants.Package); + + /* When a request uses a sequence of multiple start(); dispatch() with + * non-container threads it is possible for a previous dispatch() to + * interfere with a following start(). This lock prevents that from + * happening. It is a dedicated object as user code may lock on the + * AsyncContext so if container code also locks on that object deadlocks may + * occur. + */ + private final Object asyncContextLock = new Object(); + + private volatile ServletRequest servletRequest = null; + private volatile ServletResponse servletResponse = null; + private final List<AsyncListenerWrapper> listeners = new ArrayList<>(); + private boolean hasOriginalRequestAndResponse = true; + private volatile Runnable dispatch = null; + private Context context = null; + // Default of 30000 (30s) is set by the connector + private long timeout = -1; + private AsyncEvent event = null; + private volatile Request request; + private volatile InstanceManager instanceManager; +``` + +- } + + + +``` +public AsyncContextImpl(Request request) { + if (log.isDebugEnabled()) { + logDebug("Constructor"); + } + this.request = request; +} +``` + + + + - 2) AsyncContextImpl#setStarted + + +``` +public void setStarted(Context context, ServletRequest request, + ServletResponse response, boolean originalRequestResponse) { + + synchronized (asyncContextLock) { + this.request.getCoyoteRequest().action( + ActionCode.ASYNC_START, this); + + this.context = context; + this.servletRequest = request; + this.servletResponse = response; + this.hasOriginalRequestAndResponse = originalRequestResponse; + this.event = new AsyncEvent(this, request, response); + + List<AsyncListenerWrapper> listenersCopy = new ArrayList<>(); + listenersCopy.addAll(listeners); + listeners.clear(); + for (AsyncListenerWrapper listener : listenersCopy) { + try { + listener.fireOnStartAsync(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn("onStartAsync() failed for listener of type [" + + listener.getClass().getName() + "]", t); + } + } + } +} +``` + + +- AbstractProcessor#action +- case ASYNC_START: { + asyncStateMachine.asyncStart((AsyncContextCallback) param); + break; +} + +- AsyncStateMachine#asyncStart +- synchronized void asyncStart(AsyncContextCallback asyncCtxt) { + if (state == AsyncState.DISPATCHED) { + state = AsyncState.STARTING; + this.asyncCtxt = asyncCtxt; + lastAsyncStart = System.currentTimeMillis(); + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", + "asyncStart()", state)); + } +} + + - 3) AsyncContextImpl#setTimeout + +``` +public void setTimeout(long timeout) { + check(); + this.timeout = timeout; + request.getCoyoteRequest().action(ActionCode.ASYNC_SETTIMEOUT, + Long.valueOf(timeout)); +} +``` + + +- AsyncContext#complete(结束) + +``` +public void complete() { + if (log.isDebugEnabled()) { + logDebug("complete "); + } + check(); + request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null); +} +``` + + +- case ASYNC_COMPLETE: { + clearDispatches(); + if (asyncStateMachine.asyncComplete()) { + processSocketEvent(SocketEvent.OPEN_READ, true); + } + break; +} + + +- protected void processSocketEvent(SocketEvent event, boolean dispatch) { + SocketWrapperBase<?> socketWrapper = getSocketWrapper(); + if (socketWrapper != null) { + socketWrapper.processSocket(event, dispatch); + } +} + + +``` +public void processSocket(SocketEvent socketStatus, boolean dispatch) { + endpoint.processSocket(this, socketStatus, dispatch); +} +``` + + + - 见2.2.1) AbstractEndpoint#processSocket +- 相当于重新开启一个工作线程,这个工作线程带着SocketWrapper,又来一遍容器的流程,而这一遍的流程,因为Servlet已经处理过,所以会略过servlet的执行直接将后续的处理走完,包括最后response的收尾,对象的清空等等。 +- 但是异步Servlet此时不会重新跑一次Servlet,直接跳到response收尾。 +- AsyncContext#dispatch(转发) From 65eb2ba50976e513fde2de728eea398aed716915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:56:11 +0800 Subject: [PATCH 92/97] Update README.md --- README.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d9f5178e..c8c7e3de 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,8 @@ -# Java-Interview -本仓库会持续更新,欢迎Star给一个鼓励! -Java 面试必会 直通BAT +- 本仓库会持续更新,欢迎Star给一个鼓励! +- 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 -最近上班有点忙,会抽时间持续更新本仓库。 - -java面试需要的各个方向和面试题会持续更新,可以先关注一波 - -我的公众号 **程序员乔戈里** - -整理了这个仓库java的面试题和答案面试题和答案的pdf的可以在我的公众号后台回复 面经 自己耗时一个月整理的面试题和答案,后续也会持续更新, - - ->觉得文章不错的欢迎关注我的WX公众号:**程序员乔戈里** -我是**百度**后台开发工程师,哈工大计算机本硕,专注分享技术干货/编程资源/求职面试/成长感悟等,关注送5000G编程资源和自己整理的一份帮助不少人拿下java的offer的面经附答案,免费下载CSDN资源。 +>github必须md格式才能看得舒服些,花了很多时间找word转md的工具,找了几款不太好用,于是自己手动把word改成md格式,后来发现有些重复性工作可以写个程序处理,就写了个程序,把word中的标题、代码都变成md格式,虽然能处理不少,但是还是需要人工校对,还有图片需要上传,真的超级费事,要搞吐了。。。各位也别抱怨我的github格式不好了,毕竟也还没完全处理完,体谅一下~ ![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) From 69b8e3283ed721c2362bebd0289fcc49ae24be48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:56:35 +0800 Subject: [PATCH 93/97] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8c7e3de..afade8b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ - 本仓库会持续更新,欢迎Star给一个鼓励! - 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 -- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 +- 所以可以先关注一下我的公众号,在我的公众号[程序员乔戈里]后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 >github必须md格式才能看得舒服些,花了很多时间找word转md的工具,找了几款不太好用,于是自己手动把word改成md格式,后来发现有些重复性工作可以写个程序处理,就写了个程序,把word中的标题、代码都变成md格式,虽然能处理不少,但是还是需要人工校对,还有图片需要上传,真的超级费事,要搞吐了。。。各位也别抱怨我的github格式不好了,毕竟也还没完全处理完,体谅一下~ From 6cf0d9fdacb25526c03cced4d0647e7f6921fe2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:57:12 +0800 Subject: [PATCH 94/97] Create README.md --- docs/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ + From fa1e1ee3e1059db67107ddbbccbf3b512f7e54e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Sat, 5 Oct 2019 21:57:34 +0800 Subject: [PATCH 95/97] Update README.md --- docs/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/README.md b/docs/README.md index 8b137891..685fceb5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,7 @@ +- 本github最初的版本是一份word文档,目前只是把word刚刚搬上来了,但是有些图片、排版还没来得急整理,看起来可能还是有点困难 +- 所以可以先关注一下我的公众号,在我的公众号后台回复 **888** 获取这个github仓库的PDF版本,左侧有导航栏,方便大家阅读。 + +>github必须md格式才能看得舒服些,花了很多时间找word转md的工具,找了几款不太好用,于是自己手动把word改成md格式,后来发现有些重复性工作可以写个程序处理,就写了个程序,把word中的标题、代码都变成md格式,虽然能处理不少,但是还是需要人工校对,还有图片需要上传,真的超级费事,要搞吐了。。。各位也别抱怨我的github格式不好了,毕竟也还没完全处理完,体谅一下~ + +![](http://ww1.sinaimg.cn/large/007s8HJUly1g0fkgcpy8cj30760760t7.jpg) From bf3e799975165240dacbea00716faa7f3ad4a49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 6 Apr 2020 19:44:26 +0800 Subject: [PATCH 96/97] =?UTF-8?q?Create=20Java=E5=9F=BA=E7=A1=80=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...7\272\347\241\200\345\255\246\344\271\240" | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 "docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" diff --git "a/docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" "b/docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" new file mode 100644 index 00000000..43b7f2a1 --- /dev/null +++ "b/docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" @@ -0,0 +1,24 @@ +# Java + +Oracle JDK有部分源码是闭源的,如果确实需要可以查看OpenJDK的源码,可以在该网站获取。 + +http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/ + +http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/73d5bcd0585d/src +上面这个还可以查看native方法。 + +### JDK&JRE&JVM + +JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具(编译、开发工具)和Java核心类库。 +Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。 +JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。 + +![123.png](http://ww1.sinaimg.cn/large/007s8HJUly1gdka822vwdj30dw099414.jpg) + +JDK包含JRE和Java编译、开发工具; +JRE包含JVM和Java核心类库; +运行Java仅需要JRE;而开发Java需要JDK。 + +### 跨平台 + +字节码是在虚拟机上运行的,而不是编译器。换而言之,是因为JVM能跨平台安装,所以相应JAVA字节码便可以跟着在任何平台上运行。只要JVM自身的代码能在相应平台上运行,即JVM可行,则JAVA的程序员就可以不用考虑所写的程序要在哪里运行,反正都是在虚拟机上运行,然后变成相应平台的机器语言,而这个转变并不是程序员应该关心的。 From e6b8ff8e54066d07934c166f176833e37b0ca6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7=EF=BC=9A?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E4=B9=94=E6=88=88=E9=87=8C?= <990878733@qq.com> Date: Mon, 6 Apr 2020 19:45:36 +0800 Subject: [PATCH 97/97] =?UTF-8?q?Rename=20Java=E5=9F=BA=E7=A1=80=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0=20to=20Java=E5=9F=BA=E7=A1=80=E5=AD=A6=E4=B9=A0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Java\345\237\272\347\241\200\345\255\246\344\271\240.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" => "docs/Java\345\237\272\347\241\200\345\255\246\344\271\240.md" (100%) diff --git "a/docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" "b/docs/Java\345\237\272\347\241\200\345\255\246\344\271\240.md" similarity index 100% rename from "docs/Java\345\237\272\347\241\200\345\255\246\344\271\240" rename to "docs/Java\345\237\272\347\241\200\345\255\246\344\271\240.md"