高性能Mysql笔记-第六章(1)

第六章 查询性能优化

  • 前面章节的内容:
  1. 最优库表结构;
  2. 最优索引。
  • 本章内容:
    设计合理的查询。

6.1 查询任务的组成

  • 查询的生命周期
  1. 客户端到服务器;
  2. 服务器解析,生成执行计划;
  3. 执行;
  4. 返回结果给客户端。

其中第三步执行可以分解为:

  • 执行:
  1. 检索数据到存储引擎;
  2. 数据处理。(排序,分组)

查询时间消耗包括:

  • 网络;
  • CPU计算;
  • 生成统计信息和执行计划;
  • 锁等待(互斥等待);
  • 其他。

6.2 慢查询基础:优化数据访问

6.2.1 减少数据量

慢查询的基本原因之一是数据量太大了,针对这个方面,方法有两个:

  1. 减少访问的行数;(使用limit)
  2. 减少访问的列数。(利用覆盖索引)

此外,对于重复查询,在应用层进行缓存也是一个比较常见的方法。

6.2.2 检查是否扫描了额外的记录

衡量查询开销的三个指标:

  1. 响应时间;
  2. 扫描和返回的行数;
  3. 扫描的行数和访问类型。

响应时间

响应时间可分为:

  1. 服务时间: 真正处理的时间;
  2. 排队时间: 等待资源时的时间。

扫描和返回的行数

理想情况:
扫描行数=返回行数。

较短的行代价较小。

扫描的行数和访问类型

访问类型:
Explain语句的type字段可以得到。包括:

  1. ALL,全表扫描;(差)
  2. Index,索引扫描;
  3. Range,范围扫描;
  4. 唯一索引查询;
  5. 常数引用。(优)
    等等…

访问类型越优,扫描的成本越小。

预估的扫描行数
查询优化器预估的扫描行数,可以通过Explain语句的rows字段得到。

Where的三种应用方式
三种方式由好到坏依次为:

  1. 索引中使用。在存储引擎层完成;(Extra列为空)
  2. 使用索引覆盖扫描(Extra列出现Using index)。扫描后,在服务器层过滤,但不用回表。
  3. 不使用索引。从存储引擎返回行,在服务器层过滤。(Extra列为Using Where)。

相关的查询优化技巧:

  1. 覆盖索引;
  2. 改变库表结构,使用单独的汇总表;
  3. 重写复杂查询。(后续讨论)

6.3 重构查询的方式

6.3.1 复杂查询分解成多个简单查询

由于现代mysql的连接更轻量,网速也更快,因此分解复杂大查询成简单小查询也是很有必要的。
(也要适度权衡,不能过度分解)

6.3.2 切分查询

一个简单查询,但锁住大量行,可以分解成几个锁住少量行的查询,分时间段执行,减轻服务器瞬发压力。
比如删除100万行数据,可以分成删除100个1万行数据。

6.3.3 分解关联查询

分解关联查询后,在应用层做关联。

  • 原查询

    1
    2
    3
    4
    5
    select *
    from tag
    join tag_post on tagid=id
    join post on post.tagid=id
    where tag='mysql'
  • 分解后:

    1
    2
    3
    select * from tag where tag='mysql';
    select * from tag_post where tagid=1234;
    select * from post where id in (1,2,3,4)

上述分解的好处:

  1. 缓存的效率更高;
  2. 分解后,锁竞争更少;(锁粒度更细)
  3. 数据库拆分更容易;(不需要跨库查询)
  4. 关联可能会更高效;
  5. 减少记录的重复查询;
  6. 相当于应用中实现了哈希关联(效率高)。

6.4 查询执行的基础

查询流程:

  1. 客户端:发送查询到服务器;
  2. 服务器:检查缓存;
  3. 服务器:SQL解析,预处理,优化器生成执行计划;
  4. 调用存储引擎的API执行查询;
  5. 返回结果给客户端。

6.4.1 Mysql客户端/服务器通信协议

特点:

  1. 半双工

优点:

  • 简单快捷;

缺点:

  • 无法进行流量控制,一旦一端开始发送消息,另一端要完整接收后才能响应它。
  1. 服务器端:
    参数max_allowed_packet的作用:如果查询太大,服务端会拒绝接收更多的数据。

  2. 客户端:
    如果服务器发送的数据特别多,客户端必须完整地接收完整个结果。粗暴地断开连接不是个好主意,应该使用Limit进行限制。
    服务器端发送完全部数据,才会释放相应资源。

查询状态
相关命令:

1
SHOW FULL processlist;

相关状态值含义:

  1. Sleep: 等待客户端发送请求;
  2. Query:正在执行或发送结果;
  3. Locked:(MyIsam有,Innodb没有) 服务器层,线程等待表锁;(行锁等存储引擎级别的锁不会体现在这里。)
  4. Analyzing and statistics: 线程正在收集存储引擎的统计信息,并声称查询的执行计划。
  5. Copying to tmp table[on disk]: 线程正在执行查询,并且将其结果集都复制到一个临时表中。可能的操作包括: (1) GROUP BY;(2) 文件排序;(3) UNION操作。
  6. Sorting result: 对结果集进行排序;
  7. Sending data: (1)多个状态之间传送数据;
    (2)生成结果集中;
    (3)向客户端返回数据。

6.4.2 查询缓存(详见第7章)

缓存匹配原理:

  • Key: SQL的哈希值;
  • Value: 缓存(查询结果数据)。

其中对于函数Hash(SQL)是大小写敏感的,因此即使轻微的改动,也无法利用缓存。(Perconan版本的Mysql能够忽略注释。)

6.4.3 查询优化处理

SQL计划的生成:

  1. 解析SQL;
  2. 预处理;
  3. 优化SQL执行计划。

语法解析器和预处理

  • 语法解析器:
    SQL-语法解析器->解析树

  • 预处理:
    检查解析树是否合法,验证权限。
    例如检查数据表和数据列是否存在,检查名字和别名是否有歧义。

查询优化器

(上述语法树合法后)

  • 优化器:
    语法树-查询优化器->执行计划

一条查询可以有很多种执行方式,优化器的作用就是找到这其中最好的执行计划。

  • Mysql的优化器实现:

基于成本。
尝试预测一个查询使用某种执行计划的成本,选择其中成本最小的一个。

  • 成本计算示例
  1. 执行一次Where条件比较的成本,可以通过查询当前会话的Last_query_cost的值来得知Mysql计算的当前查询的成本。
    相关命令:
    1
    Show status like 'Last_query_cost'
    上述结果的Value列如果为1040,则意思是优化器认为大概需要1040个数据页的随机查找才能完成上面的查询。

优化器评估成本的时候并不考虑任何层面的缓存,假设读取任何数据都需要一次磁盘IO。

  • 优化器出错:
    选择的执行计划不是最优。(结果还是对的)

  • Mysql优化器出错的原因:

  1. 统计信息不准确;(存储引擎)
  2. 优化器不考虑磁盘数据页分布,成本估算出错;(可能因为顺序IO和随机IO情况不清楚)
  3. 优化器不考虑是否有并发执行;
  4. 优化器使用的目标函数并不以时间为目标,而是以估算的操作成本(有多少个数据页被读取,数据量)。
  5. 有些规则架凌于目标函数之上。例如:如果存在全文搜索的MATCH()子句,则在存在全文索引的时候就使用全文索引。(即使使用别的索引可能更快)。
  6. 优化器不考虑一些操作的成本。例如,存储过程的执行成本,UDF的执行成本;
  7. 优化器不会穷举所有可能的执行计划,可能错过最优解。

优化策略

  1. 静态优化;(类似编译时优化)
  2. 动态优化。(运行时优化)
  • 静态优化:
    直接对解析树进行分析,优化。例如通过代数变换把Where条件转换成另一种形式(但不会进行类型转换)。

  • 动态优化:
    与查询的上下文有关。例如Where条件中的具体取值,索引中条目对应的数据行数,等等。

优化类型

  1. 重定义关联表的顺序;
  2. 外连接转换成内连接;(等价时)
  3. 使用等价变换规则;(简化表达式)
  4. 优化Count,Min,Max:当Explain中看到Select tables optimized away时,进行了此种优化,把表的聚合计算用一个常数进行了替换;
  5. 常数替换:用常数替换某个表达式,或者某个唯一查询。(例如用主键或者唯一键查找的)(const)
  6. 覆盖索引扫描。(无需回表)
  7. 子查询优化;(?没说清,TODO)
  8. 提前终止查询:发现已经满足查询需求的时候,Mysql总能立即终止查询。(例如使用Limit时、发现不成立条件、空结果时)
  9. 等值传播:如果两列有等式关联,优化器能把其中一列的Where条件传递到另一列上。
  10. 列表In的比较:Mysql处理In列表的流程(O(NlogN)):(1)先对In列表进行排序;(2)二分查找确定值是否在列表中。其他数据库是转换成N个OR条件,因此是O(N)。IN列表很长时,Mysql速度更快。
  11. 其他。

Mysql如何执行关联查询

  • 关联
    每一次查询都是一次关联。

  • 执行关联的策略
    先在一个表中循环取出单条数据,再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。

上述策略无法应用于Full Join,因此Mysql没有Full Join。

  • 多表关联:
    Mysql选取某张表作为驱动表,然后按照上述逻辑进行循环,逐个关联其他表。

  • 缺点
    可并发度低。理想情况下,应该以平衡树形式进行关联。例如4张表关联,应该两两关联。而Mysql的做法就像使用一棵左侧深度优先的树,可并发度较低。

执行计划

SQL->指令树->存储引擎执行完成这棵指令树并返回结果。

  • 相关命令
    1
    2
    3
    explain extended <sql>;
    show warnings;
    -- 可以得到重构出的查询。

### 关联查询优化器
-目标
优化多个表关联时的顺序。

  • 案例SQL
    1
    2
    3
    4
    5
    6
    select *
    from film AS a
    join film_actor AS b
    ON a.id=b.film_id
    join actor AS c
    ON b.actor_id=c.id

一种可能的优化下,关联顺序是倒序,也就是使用actor作为驱动表。原因:预估的扫描数据页数量较少,通过存储引擎的索引统计信息,预估actor表较小,数据量减少得比较快。(其实就是小表优先)

n个表的关联,顺序的可能有n!种,所以搜索空间非常大,优化器不可能全部都预估一遍。相关参数optimizer_search_depth指定相应的阈值大小。超过阈值以后,优化器使用贪婪模式进行搜索。

排序优化

  • 排序的方法:
  1. 使用索引生成排序结果;
  2. 内存排序:数据量小于排序缓冲区;
  3. 外排。(需要使用磁盘)

Mysql将上述第二种和第三种统称为filesort

  • Mysql使用的排序算法
  1. 两次传输;(旧版本)
    数据 = (行指针,需要排序的字段);
    对上述数据排序,然后根据结果行指针,回表获取全部数据。
  • 优点:
    排序数据量小(只用了排序字段),可以更多使用内存排序;

  • 缺点:
    回表时,随机IO很多。而且需要两次传输。

  1. 单次传输;(新版本)
    数据 = (所有需要返回的列)
    对上述数据排序,然后直接返回结果。
    算法开始时一次顺序IO读取所有数据,
  • 优点
    随机IO少,适用于IO密集型应用。

  • 缺点
    如果返回列很多,单行很长,需要外排。

上述两种算法相关参数:
max_length_for_sort_data
如果所有列的总长度不超过这个参数,就使用单次传输,反之使用两次传输。

  • 关联查询+排序:
    (1) 如果Order by的所有列都来自关联的第1个表,Mysql在关联处理第一个表的时候就会进行排序。
    此时Explain结果的Extra字段会有Using filesort
    (2) 其他情况下,Mysql会等整个关联结果出来后,再进行排序。
    此时Explain结果的Extra字段会有Using temporary; Using filesort
    如果有Limit话,Limit也会在排序之后应用。
  • 启发
    关联查询排序时候,尽量把Order by的列提到第一张表中。

高性能Mysql笔记-第五章(2)

5.3.7 使用索引扫描来做排序

Mysql生成有序结果的两种方法: (详见第7章)

  1. 排序操作.
  2. 按索引顺序扫描.

// explain结果中的type,如果为index,则使用第二种方法,按索引顺序扫描.
按索引顺序扫描工作流程:(Innodb二级索引)

  1. 顺序扫描索引,获取主键指针;
  2. 回表查询相应主键的数据行.
    如果索引是覆盖索引,则可以避免上述第二步,减少一次磁盘IO,并且避免大范围扫描时的大量随机IO.

正确示例:
索引为(a,b,c)
示例1:

1
2
select a,b,c from t
where a='xxx' ORDER BY b,c

组合where条件和order by条件,而且都是顺序(ASC). 此外如果组合后是索引的最左前缀,也是可以利用索引进行排序的.

错误示例
示例2:

1
2
select a,b,c from t
where a='xxx' order by b DESC,c ASC.

由于顺序和建立索引的不同,因此不能利用到索引进行排序.
如果需要按不同方向排序, 可以通过存储这列的相反数或者反转值, 满足这种需求.

示例3:

1
2
select a,b,c from t
where a>'xxx' order by b,c

由于a是范围查询,因此无法利用索引的其他列(b和c).

5.3.8 压缩(前缀压缩)索引

1
仅对于MyISAM引擎.

MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中.

  • 压缩对象:

    1
    2
    1. 字符串(默认设置)
    2. 整数(用参数设置)
  • 压缩方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    1. 完全保存索引块的第一个值;
    2. 从第二个值开始,保存:(相同前缀字节数,剩余部分)

    例如原始数据为:
    1. perform
    2. performance

    压缩后:
    1. perform (完整保存)
    2. 7,ance (相同前缀字节数,剩余部分)
  • 压缩优点:
    使用更少空间;

  • 压缩缺点:
    某些操作变慢: 因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法二分查找. 正序扫描不受影响,倒序扫描性能变慢.
    块中查找某一行的平均耗时: 扫描半个索引块.

  • 压缩索引使用场景:
    IO密集型查询.(减少传输量,可能只需要1/10的磁盘大小)

  • 压缩索引不适用的场景:
    CPU密集型查询.(因为扫描需要随机查找)

5.3.9 冗余和重复索引

  • 背景:
    MySQL允许在列上创建多个索引.

  • 重复索引:
    相同列,顺序相同,类型相同的索引.
    (例: 同一列上建立主键和唯一索引,事实上唯一限制和主键限制都是通过索引实现的,事实上建立了重复索引)

  • 冗余索引:
    前缀索引与索引冗余.
    (例: 索引(A,B)与索引(A)冗余.)

处理:

  1. 重复索引应该移除;
  2. 冗余索引应该考虑移除:
    (1) 如果原来的索引已经太长到影响性能,可以保留冗余索引
    (2) 如果(1)不成立,应该尽量移除冗余索引.

索引太多的缺点:

  1. 写入速度变慢;(可以通过禁用索引,然后延迟build索引优化)
  2. 查询时mysql需要选择索引,索引多的话需要比较的索引也变多,影响查询计划的
    生成速度.
  3. 存储索引成本增加.

5.3.10 未使用的索引

检查各个索引的使用频率:

  1. 打开userstates参数;(默认是关闭的)
  2. mysql服务器正常运行一段时间;(可以考虑使用benchmark)
  3. 查询Information_schema.Index_statistics,查到每个索引的使用频率.

5.3.11 索引和锁

1
以下讨论Innodb引擎

索引可以让查询锁定更少的行:

  1. 减少内存使用;
  2. 减少锁争用,提高并发度.
  • 早期Mysql:
    事务提交后才释放锁;

  • 新版本Mysql:
    服务器端过滤掉行后,释放没用到的行锁.

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Set autoCommit=0;-- 默认是每句都是一个事务
    -- 这里改成显式提交。
    BEGIN
    Explain
    select actor_id from t
    where actor_id<5 and actor_id!=1
    FOR Update; -- 加上排他锁

    Extra列:
    Using Where;Using index

    从Extra列可以看出,虽然使用了索引,但依然有Using Where,意思是使用索引从存储引擎获取到一些数据行,传送给服务器后,依然要使用Where条件进行过滤。因此这个查询锁住的并不只是id为2-4的行。
    (TODO: 验证锁住的范围,以及索引类型是否为主键的影响)

Innodb不同索引对应的锁:

  1. 主键索引: 排他锁(写锁);
  2. 二级索引: 共享锁(读锁)。

5.4 索引案例

5.4.1 支持多种过滤条件

也就是支持Where条件。
作为过滤条件的列需要考察的3个属性,按重要性如下:

  1. 查询频率;
  2. 相关查询是否是范围查询,或者列的取值是否可枚举。
  3. 选择性。

设计索引的原则:

  1. 查询频率高,则应该加入索引;
  2. 如果是范围查询,则如果作为联合索引的一列,应该放在后面;(因为最左前缀无法利用范围查询后的列)
    如果是可枚举列,则查询语句中可以使用In (‘male’,’female’)绕过列顺序限制,让Mysql始终能够应用相关索引;
  3. 满足前两个条件后,应该把选择性高的列放在联合索引的前面。

案例:
查询频率高的列,组成联合索引:
(sex,country,age)

由于sex的枚举值少,可以放在最前面。
由于age值一般是范围查询,所以应该放在最后面。

  • 注意事项:
  1. 避免使用多个范围查询;
  2. 通过枚举列(IN条件),让Mysql尽量使用到索引。

5.4.3 优化排序

  • 示例1:

    1
    2
    3
    select * from profiles
    where sex='M'
    order by rating limit 10

    可以考虑建立索引(sex,rating)来优化查询。

  • 示例2:

    1
    2
    3
    select * from profiles
    where sex='M'
    order by rating limit 10000,10

    这个查询的性能难点在于偏移量太大。
    Mysql需要花费大量的时间扫描需要丢弃的数据。

解决方案:

  1. 预先计算+缓存;
  2. 覆盖索引+延迟关联。

方案2的代码如下:

1
2
3
4
5
6
7
select * from profiles AS a 
join
(select id from profiles
where sex='M'
order by rating limit 10000,10
) as b
on a.id=b.id

5.5 维护索引和表

相关内容包括:

  1. 找到并修复损坏的表;
  2. 维护准确的索引信息;
  3. 减少碎片。

5.5.1 找到并修复损坏的表

  • 相关命令:
    1
    2
    3
    4
    5
    6
    7
    -- 检查:
    Check Table xxx
    -- 修复方法1:
    Repair Table xxx
    -- 修复方法2:(重建表)
    Alter table innodb_tbl engine=innodb;
    -- 注意此处引擎类型应该与原先一致。

5.5.2 维护索引信息

  • 相关命令:
    1
    2
    -- 重新生成统计信息:
    Analyze Table xxx

Innodb引擎则不在磁盘存储索引统计信息,而是通过随机的索引抽样访问获取信息存储在内存。
相关参数:
innodb_stats_sample_pages
设置样本页的数量。

5.5.3 减少索引和数据的碎片

1
以下针对B-Tree索引。

碎片的类型:

  1. 行碎片;数据行被分散存储到多个地方。
  2. 行间碎片: 顺序的行被存储到多个地方;
  3. 剩余空间碎片: 数据页中有大量空余空间。

相关命令:

1
2
Optimize table xxx
-- 或者可以重建表/索引,也可以消除碎片。

高性能Mysql笔记-第5章

第五章 创建高性能的索引

索引,key,index。
mysql中,key相当于index
索引的存储=>存储引擎层。

5.1.1 索引的类型

B-Tree索引

实际上大多使用的是B+树。
相关区别:https://www.cnblogs.com/oldhorse/archive/2009/11/16/1604009.html
B树:二叉树
B-树:多叉树,数据存所有的各个节点上;
B+树:数据存叶节点上,叶节点连在一起。
B*树:比B+树多一个非叶节点之间的指针。(类似跳表)

虽然不同存储引擎的具体索引实现不同,但Mysql中对于这一类索引使用的关键字是B-Tree索引。

1
2
3
NDB引擎: T-Tree结构
InnoDB: B+Tree
MyIsam: 前缀压缩,索引更小

B-Tree索引(B+Tree)的适用查询:

1
2
3
4
1. 全键值: Key中所有列是某个值;
2. 键值范围: Key中所有列落在某个范围;
3. 键前缀(最左前缀):Key中前几列或前缀是某个值或某个范围。
4. 上述条件的order by。

哈希索引

哈希索引基于哈希表实现:
每一个Key计算一个哈希值,哈希表里的value存数据行的指针。Memory引擎对于相同哈希值的行用拉链存储。(非唯一哈希索引)

支持哈希索引的引擎: 只有Memory引擎。

Memory引擎: 默认哈希索引,也支持B-Tree索引。

哈希索引的适用查询

1
1. 全键值。

哈希索引的局限性

  1. 只存了哈希值,因此无法进行范围查询;
  2. 无法用于排序;
  3. 整个Key做哈希,因此无法部分匹配查询;
  4. 不支持前缀匹配;
  5. 要求哈希函数冲突少。

哈希索引的适用场景

  • 数据仓库的查找表。(精确匹配O(1)复杂度,星形,多表连接)

个人理解: 哈希索引既然是Memory引擎里用的,可能实现上就是在内存里load表数据,做了一张HashMap.(TODO)

自适应哈希索引
Innodb引擎的自适应哈希索引
当注意到某些索引值被使用得非常频繁时,会在内存中基于B-Tree索引再创建一个哈希索引。
(可以选择开启)

创建自定义哈希索引
假如有一个列url特别长,使用B-Tree索引的话存储内容就很大。如果查询时大多是精确匹配某个url,就可以考虑使用哈希索引。
方法:

  1. 新增一列url_crc,专门存储url的哈希值;
  2. 用触发器维护这一列。(增改)

哈希函数选择:

  1. 不使用MD5或SHA1,开销太大,结果太长;
  2. 使用CRC32或自定义。

难点: 平衡长度和冲突。

空间数据索引(R-Tree)(不推荐)

多维度索引数据(不只是前缀索引)。
需要使用Mysql的GIS相关函数MBRContains()
推荐使用PostgreSql的PostGIS,Mysql还没做好。

全文索引

见另一篇文章中的讨论。
http://xiaoyue26.github.io/2018/01/14/2018-01/%E9%AB%98%E6%80%A7%E8%83%BDMysql%E7%AC%94%E8%AE%B0-%E5%85%A8%E6%96%87%E7%B4%A2%E5%BC%95/

其他索引类型

第三方存储引擎中的索引类型:
TokuDB: 分形树索引。
ScaleDB: Patricia tries
等等。

5.2 索引的优点

  1. 减少扫描数据量;
  2. 避免排序和临时表;
  3. 将随机IO转化为顺序IO.

索引与查询匹配程度的评价:三星系统:

  1. 一星: 将查询相关记录放在了一起;(顺序IO,范围查询能利用到索引)
  2. 二星: 数据顺序与查找顺序一致;(避免排序,让排序也能利用到索引)
  3. 三星: 索引列包含查询的所有列。(避免二次访问磁盘)

上述评价的对象是(索引,某种查询). 因此并没有一个索引能够完美适应所有查询.只能说尽量适应当前场景下的高频查询.

索引适用场景:

  1. 小表: 直接扫全表即可;
  2. 中表: 索引;
  3. 大表: 分区。索引开销太大。

5.3 高性能的索引策略

索引策略分为两个方面:

  1. 建立索引的策略;
  2. 使用索引的策略。

5.3.1 独立列(使用索引的策略)

独立列: 索引列不能是表达式的一部分,也不能是函数的参数。
此外,如果数据类型不匹配,相当于隐式调用了类型转换函数,也是无法使用到索引的。

1
2
3
4
5
6
7
8
9
10
11
12
错误示例:(表达式)
select * from xxx where id+1=5;
正确示例:(移到等号另一边)
select * from xxx where id=5-1;

错误示例:(类型不匹配)
select * from xxx
where dt=date_sub(current_date, interval 1 day);

正确示例:
select * from xxx
where dt = date_format(date_sub(current_date, interval 1 day), '%Y-%m-%d')

可以使用explain语句检查查询是否能够利用索引。(type不是ALL

5.3.2 前缀索引和索引选择性(建立索引策略)

索引的选择性= count(distinct key)/count(1)
因此Unique key的选择性是1.

可用: 选择性达到0.031以上。
当提升前缀的长度已经无法显著提高选择性,就可以停下来考虑这个长度了。

前缀索引的优点:

  1. 性能好;
  2. 空间小。

前缀索引的缺点:

  1. Order By和Group By无法使用;
  2. 覆盖扫描无法使用。

后缀索引:
可以存储反转以后的列来达到这个目的。

5.3.3 多列索引

index merge(索引合并)
利用两个单列索引来优化总查询。
explain计划里的索引type是index_merge.
三个变种:

  1. OR=转化=>分治以后Union;
  2. And=转化=>分治以后取交集;
  3. 上述两种情况的组合。

OR

1
2
3
4
5
select * from xxx where actorid=1 or filmid=2
转化为:
select * from xxx where actorid=1
UNION
select * from xxx where filmid=2

索引合并并不一定比扫全表快,可以考虑用参数optimizer_switch来关闭,或者IGNORE index
另一种思路就是直接建立多列索引,而不是单列索引。

5.3.4 索引顺序

本节针对B-Tree索引。

索引的顺序设置得好的话,可以受益的查询:

  1. order by与group by
  2. where条件部分匹配,范围匹配
  3. distinct
  • 经验法则:
    选择性高的列放前面=>优化Where
    (不一定能优化order by和group by)

5.3.5 聚簇索引

形容的是Innodb存储B-tree索引的数据存储方式。
聚簇索引: 数据行存储在叶子节点。
聚簇: 数据行与相邻的键值紧凑存储。

InnoDB的主键列是聚簇索引;其他key列则不是(叶子节点存的不是数据行,而是主键值)。

Innodb没有主键时:

  1. 找Not Null的Unique key,建立聚簇索引;
  2. 没有的话,隐式定义一个主键。

聚簇索引优点:

  1. 符合查询的话,磁盘IO次数少;
  2. 顺序IO多;
  3. 覆盖索引扫描可以使用页节点中的主键值。

缺点:

  1. 数据不在磁盘(例如在内存),顺序IO就没优势了;
  2. 插入速度依赖于插入顺序;
  3. 更新聚簇索引列代价高(大量移动);
  4. 扫全表稍微慢(页分裂导致数据存储不连续);
  5. 页分裂导致存储空间大于实际数据大小;
  6. 二级索引需要两次索引查找(第一次只找到主键)。(可以使用自适应哈希索引,优化重复的查询)

Innodb和Myisam的数据分布对比
MyIsam: 主键索引和非主键索引都是非聚簇索引,存储上没有差异,叶子节点都指向物理地址。
Innodb: 主键索引是聚簇索引,叶子节点指向物理地址;非主键索引是二级索引,非聚簇索引,叶子节点指向主键值(需要二次查找)。

Innodb二级索引:
存储主键值,所以主键里的页分裂或移动的时候,不需要更新二级索引。

Innodb中按主键顺序插入行

使用自增id可以让主键顺序,保证插入效率,也保证聚簇索引的存储效率。

不适用场景:

  1. id随机;
  2. 不连续id,id分布范围过大。

(避免使用uuid作为innodb的主键)

顺序主键的缺点:

  1. 并发插入时间隙锁成为热点;
  2. 自增锁成为热点,可以通过更改Innodb_autoinc _lock_mode尽量避免这一点,但又会引进自增id存在空洞的问题.

Innodb_autoinc _lock_mode有三个取值:

1
2
3
0: 传统模式. 每个sesson都会竞争自增锁;(默认)
1: 连续模式. 只有bulk insert使用自增锁.
2: 交叉模式. 不用自增锁.性能最好.空洞最多.

自增id空洞产生的原因:

  1. 事务回滚,导致原先分配的id浪费了;
  2. 使用连续模式时有并发插入;
  3. 使用交叉模式.

详见https://www.jianshu.com/p/cca59b515e20.

5.3.6 覆盖索引

覆盖索引: 索引已经覆盖到了需要查询的所有数据。(叶子节点)
(这种情况下Select *性能会低于Select xxx)

覆盖索引的优点:

  1. 减少IO次数;
  2. 减少IO数量,提高缓存效率;
  3. 对于Innodb,如果二级索引覆盖了数据,就可以避免二次查询(二次查询主键索引)。

由于覆盖索引需要存储列的值,因此哈希索引、空间索引、全文索引都不能成为覆盖索引。
只有B-Tree索引才能成为覆盖索引。

explain时,如果Extra列的值为Using index,说明利用了覆盖索引。

回顾一下三星评价系统:
1星: 常用查询的范围查询能利用到索引(顺序IO);
2星: 常用查询的Order BY能利用到索引;
3星: 常用查询的输出只包含索引. (避免二次查找,减少磁盘IO, 覆盖索引)
因此如果一个覆盖索引,同时又能满足1星和2星,那就是3星级的索引了.

覆盖索引相关优化:

  1. 索引为: (actor,title)
    1
    2
    3
    select * from products
    where actor='SEAN CARREY'
    AND title like '%APOLO%'
    上述查询explain以后,Extra列为Using Where,说明没有用到覆盖索引.
    原因是,select *包含的是所有列,而索引只有两列,不符合覆盖索引的定义.
    用到的索引是actor前缀索引,title无法应用到,因为用的Like模糊查询.

查询流程大致为:
(1) 使用actor前缀索引,取出表中符合条件的主键地址;// 第1次磁盘IO
(2) 使用(1)中的主键地址,回表(读磁盘)取出相应的数据行. // 第2次磁盘IO

  1. 索引为: (actor,title,pro_id)
    1
    2
    3
    4
    5
    6
    7
    8
    select *
    FROM product
    JOIN (
    select prod_id
    FROM product
    WHERE actor='SEAN CARREY' AND title like '%APOLO%'
    )AS t1
    ON t1.prod_id=t2.prod_id
    上述查询explain以后,Extra列为Using where;Using index.说明用到了覆盖索引.
    原因是,子查询中输出的列是prod_id,包含在索引中,因此省去了此处的二次查询.
    随后进行了延迟关联,找出product表中的其他列.
    查询流程大致为:
    (1)使用actor前缀索引,取出表中符合条件的索引与主键地址;//第1次磁盘IO
    (2)使用(1)中的进行join,取出表中其他数据列.//第2次磁盘IO

由于都要使用两次磁盘IO,
1和2查询的效率取决于where条件中匹配返回的行数,假设这个product表有100W行:
(1) 返回行数很多:
actor过滤后剩下3W行,title过滤后剩下2W行;
1和2查询效率相同:
大部分时间在发送和读取数据上;
(2) 返回行数很少:
actor过滤后剩下3W行,title过滤后剩下40行;
2的查询效率高很多:
2读取3W个id,40个整行;1读取3w个整行.
(3) 总数据量少,返回行数也少:
actor过滤后剩下50行,title过滤后剩下40行;
1和2查询效率相近:
2读取50个id,40个整行;1读取50个整行.

上述查询的执行框架:

  1. 使用索引actor从存储引擎获取数据行;
  2. 将数据行读入服务器层,用title进行过滤.

因此上述优化针对的是减少存储引擎读取到服务器层的数据行.
上述优化只针对Mysql5.6版本之前. 5.6之后,有Index condition pushdown (索引条件推送), 把查询条件推送到存储引擎, 减少读取的数据量.

– TO BE CONTINUE…

case class浅析

结论

case class又称样例类,可用于DTO(序列化特性),模式匹配(unapply特性).
作为一种语法糖,在定义之后会隐式获得很多经典的方法.

1. case class与case object

无参数时,使用case object;
有参数时,使用case class.
case object比参数为()case classapplyunapply方法.

2. 类参数

类参数默认作为private final的类成员.
同时自动生成一个同名public方法以获取它的值.

3. 伴生对象

  • apply方法和unapply方法:
    case class会自动生成一个伴生对象(object),作为当前作用域的一个单例,同时在里面实现了apply方法,可以使用apply方法或者括号函数()创建对象.(也可以依旧使用new关键字)
    由于有unapply方法实现,因此可以用于模式匹配.

4. 序列化特性

详情

1. 用IDE查看class文件

假如写这么一个scala文件:

1
2
package learn
case class TestCaseClass()

编译后生成的class文件会有两个:

1
2
TestCaseClass$.class
TestCaseClass.class

用IDE打开TestCaseClass.class是:

1
2
3
package learn
case class TestCaseClass() extends scala.AnyRef with scala.Product with scala.Serializable {
}

可见定义case class后,自动会继承AnyRef,加上ProductSerializabletrait.
另一个内部类文件TestCaseClass$.class虽然用ide打开为空,但看文件大小1180,可见并不是空文件.

2. 反编译

使用javap -p -c -s TestCaseClass.class查看,过滤掉看不懂的部分和常量池里的各种符号引用,大致能发现如下内容:

  • 类签名

    1
    public class learn.TestCaseClass implements scala.Product,scala.Serializable

    可见本质上是实现Product和Serializable的接口.(加上默认实现)

  • 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static boolean unapply(learn.TestCaseClass);
    public static learn.TestCaseClass apply();
    public learn.TestCaseClass copy();
    public java.lang.String productPrefix();
    public int productArity();
    public java.lang.Object productElement(int);
    public scala.collection.Iterator<java.lang.Object> productIterator();
    public boolean canEqual(java.lang.Object);
    public int hashCode();
    public java.lang.String toString();
    public boolean equals(java.lang.Object);
    public learn.TestCaseClass();

    注意到unapplyapply方法是static的,说明会自动生成一个伴生对象,并添加这两个方法.(static方法在object里)

反编译内部类:

  • 类签名:
    1
    public final class learn.TestCaseClass$ extends scala.runtime.AbstractFunction0<learn.TestCaseClass> implements scala.Serializable
    可见case class其实也是作为一个Function存在的,如果把源码改动一下加上参数:
    1
    2
    package learn
    case class TestCaseClass(xyz:String)

则这里的类签名变为:

1
public final class learn.TestCaseClass$ extends scala.runtime.AbstractFunction1<java.lang.String, learn.TestCaseClass> implements scala.Serializable

此外由于类参数没有加限定,默认变成了val,因此类参数是private final的:

1
private final java.lang.String xyz

其中AbstractFuction是一个抽象类,源码为:

1
abstract class AbstractFunction0[@specialized(Specializable.Primitives) +R] extends Function0[R] {}
  • 方法签名:
    1
    2
    3
    4
    5
    public static {};// 3: invokespecial #15  // Method "<init>":()V
    public final java.lang.String toString();
    public learn.TestCaseClass apply();
    public boolean unapply(learn.TestCaseClass);
    public java.lang.Object apply();

reduce100%卡死故障排除

  • 摘要

    集群所有MR job/Spark job奇慢无比,经过排查发现两个DataNode故障.
    措施:
    下线故障的DataNode,所有job运行重新变得飞快顺滑.
    原因:
    reduce结束后落盘的时候,pipeline中其中一个DN故障,导致返回太迟.
    如果pipeline中恰好不包括故障DN,就能顺利完成.

详情

早上起来发现集群的任务只完成了平时的约1/8.

STEP1

首先怀疑是调度不合理,job之间资源占用卡死.
进入管理页:http://f04:8088/cluster/scheduler
找到卡死的job,选出资源占用最大的job将其kill.

表现1:
资源空闲情况下,即使是小job,也会在reduce的最终阶段卡死不动.最显著的特征是在reduce100%的状态下,依然卡死不动.
查看典型job后发现有如下log:

1
2
3
4
5
6
7
WARN [ResponseProcessor for block BP-133847077-ip0-1413174984773:blk_3659724856_2615805444]
org.apache.hadoop.hdfs.DFSClient: Slow ReadProcessor read fields took 54565ms (threshold=30000ms);
ack: seqno: 144299 reply: SUCCESS reply: SUCCESS reply: SUCCESS downstreamAckTimeNanos: 61280175650
flag: 0 flag: 0 flag: 0, targets:
[DatanodeInfoWithStorage[ip1:50010,DS-7048c932-5153-4b82-8f6e-e8a920f61947,DISK]
, DatanodeInfoWithStorage[ip2:50010,DS-f11ae138-54eb-4fda-aeda-43e3a9eddeae,DISK]
, DatanodeInfoWithStorage[ip3:50010,DS-82512645-0f03-434f-8693-4f1c8876c437,DISK]]

虽然只是一个Warning,但其实关键信息是reduce最后落盘的时候落不下来,写数据块的时候pipeline里的三个DN,至少有一个迟迟不接Block.

进一步登录上述三个DN(ip1,ip2,ip3),查看DN的log,发现大致有如下log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
INFO datanode.DataNode: Exception for BP-133847077-ip0-1413174984773:blk_3659234296_2615314111
java.io.InterruptedIOException: Interrupted while waiting for IO on channel java.nio.channels.SocketChannel[connected
local=/ip4:24281 remote=/ip1:50010]. 485000 millis timeout left.
at org.apache.hadoop.net.SocketIOWithTimeout$SelectorPool.select(SocketIOWithTimeout.java:342)
at org.apache.hadoop.net.SocketIOWithTimeout.doIO(SocketIOWithTimeout.java:157)
at org.apache.hadoop.net.SocketOutputStream.write(SocketOutputStream.java:159)
at org.apache.hadoop.net.SocketOutputStream.write(SocketOutputStream.java:117)
at java.io.BufferedOutputStream.write(BufferedOutputStream.java:122)
at java.io.DataOutputStream.write(DataOutputStream.java:107)
at org.apache.hadoop.hdfs.protocol.datatransfer.PacketReceiver.mirrorPacketTo(PacketReceiver.java:200)
at org.apache.hadoop.hdfs.server.datanode.BlockReceiver.receivePacket(BlockReceiver.java:556)
at org.apache.hadoop.hdfs.server.datanode.BlockReceiver.receiveBlock(BlockReceiver.java:895)
at org.apache.hadoop.hdfs.server.datanode.DataXceiver.writeBlock(DataXceiver.java:801)
at org.apache.hadoop.hdfs.protocol.datatransfer.Receiver.opWriteBlock(Receiver.java:137)
at org.apache.hadoop.hdfs.protocol.datatransfer.Receiver.processOp(Receiver.java:74)
at org.apache.hadoop.hdfs.server.datanode.DataXceiver.run(DataXceiver.java:253)
at java.lang.Thread.run(Thread.java:745)

查看DN的连接数,发现异乎寻常得高:

1
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

STEP2

经过收集几个卡住的job的log,统计pipeline中出现的DN的ip,可以找出出现频率最高的两个DN.
将上述DN kill,卡住的job就能够继续运行了.

pipeline中某个DN挂了的话,会提前返回. 三备份会在以后找机会补上.

下线(退役)DN,其上的Block会自动备份到其他DN.
集群故障排除.具体DN故障的原因待查.// TODO

java并发编程的艺术-第四章

第四章 JAVA并发编程基础

4.1 线程简介

线程(轻量级进程): 现代操作系统调度的最小单位.
线程共享的存储: 堆
线程独占的存储: 栈(局部变量,方法参数),PC,堆的ThreadLocal

JAVA程序天生多线程: 执行main方法的是一个名字为main的线程.
天生的线程:

  1. Signal Dispatcher: 分发处理发送给JVM信号的线程;
  2. Finalizer: 调用对象finallize方法的线程;
  3. Reference Handler: 清除Reference的线程;
  4. main: main线程,用户程序入口.

要查看上述线程,可以用JMX打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo ti : threadInfos) {
System.out.println(ti.getThreadId()+":"+ti.getThreadName());
}
/*
5:Monitor Ctrl-Break
4:Signal Dispatcher
3:Finalizer
2:Reference Handler
1:main
*/

如果懒得写上述代码,也可以使用IDE的debug功能,例如在Intellij idea中打个断点,就直接可以在debug标签页看到1-4号线程的栈帧了.(5号看不见,原因未知.TODO)

4.1.3 线程优先级(很可能被操作系统忽略)

线程优先级: 整型变量priority
优先级范围: 1~10
默认优先级: 5
设定策略:

  1. cpu密集线程=>设定较低优先级;(需要cpu时间长,防止它独占太久)
  2. io密集线程=>设定较高优先级(相当于cpu占用时间不长的线程,反而可以优先给它,一种短作业优先的逻辑).
    // 短作业优先可能导致长作业饿死,因此上述策略下,如果IO密集线程特别多,就不好了.

4.1.4 线程的状态

  • Runnable
    Java把操作系统中的就绪运行中统称为Runnable.

线程状态转化大致如下:

  1. New=>Runnable=>Terminated // 理想状态
  2. Runnable => Blocked/Waiting/Time_Waited=>Runnable // 可能误入的歧途
状态 说明
New 创建线程后,调用start()前.
Runnable 就绪和运行中.
Terminated 终止. 执行完毕
Blocked 阻塞. 阻塞于锁. (synchronized)
Waiting 等待. 等待其他线程的中断或者通知.(Lock类)
Time_Waiting 超时等待. 比Waiting多一个超时返回功能.(Lock类)

查看某个java程序目前各线程状态:

  1. 先用jps查看该进程的进程id;
  2. 运行命令jstack 即可.

或者用kill -3 <id>命令让进程把threadDump信息输出到标准输出.(可以之前让进程把标准输出重定向到日志文件中)
或者用IDE的threadDump按钮也可以.

Runnable与其他状态的转化

1.Waiting
Runnable=>Waiting: // 主动等待某个对象

1
2
3
obj.wait()
obj.join()
LockSupport.park()

Waiting=>Runnable: // 被别人中断或通知

1
2
3
obj.notify() // 必须在wait之后调用才有效
obj.notifyAll()
LockSupport.unpark(Thread) // 在park之前调用也有效.(会累计1个,但不会累计2个)

2.Time_Waiting
Runnable=>Time_Waiting: // 基本就是比Waiting多个时长

1
2
3
4
5
obj.wait(long)
Thread.join(long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(long)
Thread.sleep(long)

Time_Waiting=>Runnable: // 与Waiting完全一样

1
2
3
obj.notify()
obj.notifyAll()
LockSupport.unpark(Thread)

3.Blocked
Runnable=>Blocked:

1
synchronized(xx)// 没获取到锁

Runnable=>Blocked:

1
synchronized(xx)// 获取到了锁

4.1.4 Daemon线程

守护线程,用作后台调度以及支持性工作.
换句话说,是为普通线程服务的,如果普通线程不存在了(运行结束了),Daemon线程也就没有存在的意义了,因此会被立即终止.

立即终止发生得非常突然,以至于Daemon线程的finally方法都可能来不及执行.

设定线程为Daemon的方法:

1
thread.setDaemon(true);

4.2 启动和终止线程

4.2.1 构造线程

线程的构造内容包括:

  1. 父线程; (创建它的线程) // 下面的属性默认值均与父线程一致:
  2. 线程组;
  3. 是否守护线程;
  4. 名字;
  5. ThreadLocal内容. (复制一份父线程的可继承部分)

构造完成后,在堆内存中等待运行.

4.2.2 启动线程

  • start方法的含义:
    当前线程(父线程)同步通知JVM虚拟机,在线程规划期空闲时,启动线程.

4.2.3 中断

每个线程的中断标识位:

  • true: 被中断. (收到了中断信号)
  • false(初始值): 没中断,或已经运行结束.

容易混淆的几个方法:

1
2
3
obj.interrupt();// 中断某线程.把它的中断标志改为`true`.
obj.isInterrupted(); // 查询是否中断
Thread.interrupted();// 把当前线程的中断标志重置为`false`.

除了Thread.interrupted(),还有一些方法抛出interruptedException前也会清除中断标志(置为false),以表示自己已经处理了这个中断.(如sleep方法.)

4.2.4 废弃方法: suspend(),resume(),stop()

  • suspend(): 挂起(暂停), 不释放锁.
  • resume(): 恢复(继续)
  • stop(): 停止,太突然,可能没释放资源.

4.2.5 安全的终止/暂停的方法

使用中断.
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestCancel2 {
class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;

PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}

public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted()) {// 检查是否中断.
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e) {
e.printStackTrace();
/*
* 一般处理策略:
* 1. 捕获;
* 2. do 自定义存盘工作;
* 3. 接着往外抛,提醒调用者.
* ( Thread.currentThread().interrupt();
* )
*
* 本代码中由于不需要提醒调用者,因此没有接着往外抛.
* */
}

}

public void cancel() {
interrupt();
}
}

}

这部分在<并发编程实战>第七章有详细讨论.
根据具体情况的不同,有多种解决方案.

  1. 简单情况: 直接轮询标志位;
  2. while中操作可能阻塞: (1)检查while中每一行;(2)while中只提交任务,起另外的线程执行任务;
  3. 能中断但不能取消的任务: 保持中断状态,直到收到继续信号;
  4. 其他….
    详见:
    https://github.com/xiaoyue26/scala-gradle-demo/tree/master/src/main/java/practice/chapter7

4.3 线程间通信

4.3.1 使用volatile和synchronized

首先,本质上是使用共享内存进行通信,同步则是使用volatile附带的内存屏障和sychronized带来的内置锁。(排他锁)
下面分别介绍volatilesynchronized

volatile

volatile主要作用就是让写入能够尽快从cpu缓存刷新到内存;
而读则尽量读内存。(最新数据)

应用场景:
一个线程写,其他线程只读的场景。

出错场景:(这种场景应改用AtomicInteger等原子类)
多个线程写:

  1. 线程A进行自增操作,从1增加到2;
  2. 线程B进行自增操作,从1增加到2;
  3. A,B分别先读后写,最后都写入2,因此出错。(还有其他次序及结果)

底层内存屏障:

  1. volatile写:
    1
    2
    3
    StoreStore屏障
    volatile写
    StoreLoad屏障
  2. volatile读:
    1
    2
    3
    volatile读
    LoadLoad屏障
    LoadStore屏障

volatile模拟锁,辅助线程同步(通信):

1
2
3
4
5
6
7
8
9
10
11
12
13
volatile boolean flag=false;
//线程A:
a=10;
flag=true;

//线程B:
int i;
while(true){
if(flag){
i=a;// 保证获取到了A里的10
break;
}
}

还有其他库里的同步类,原子类也是在volatile的基础上,加上CAS操作实现的。

Synchronized

synchronized用于线程同步时,使用的是对象的内置锁。

  1. Java代码层面: synchronized
  2. class字节码层面: monitorenter,monitorexit,ACC_SYNCHRONIZED指令
  3. 执行层面:多个线程竞争某个对象的内置锁,这个内置锁是排他的,一次只有一个线程能够成功获得内置锁。
    线程获取内置锁有成功失败两种情况:
    (1) Thread==Monitor enter=>失败=>进入同步队列(Blocked状态);
    (2)Thread==Monitor enter=>成功=>结束后释放锁,唤醒同步队列的线程.

相关字节码实验:

  1. 源代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SynchronizedTest{
    public static void main(String[]args){
    synchronized(SynchronizedTest.class){
    // do something
    }
    }

    public static synchronized void m(){
    // do something
    }
    }
  2. 反编译class文件

    1
    javap -v <xxx.class>

    结果大致如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 省略几行
    Constant pool:
    // 省略此处的#1~#27常量.(包括符号引用)
    {
    // 省略一些
    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC // 访问修饰符
    Code:
    stack=2, locals=3, args_size=1
    0: ldc #2// class practice/art/chapter4/SynchronizedTest
    2: dup
    3: astore_1
    4: monitorenter // 获取锁
    5: aload_1
    6: monitorexit // 释放锁
    7: goto 15
    10: astore_2
    11: aload_1
    12: monitorexit
    13: aload_2
    14: athrow
    15: invokestatic #3 // Method m:()V
    18: return
    // 省略很多
    public static synchronized void m();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    //注意这里的ACC_SYNCHRONIZED
    Code:
    stack=0, locals=0, args_size=0
    0: return
    LineNumberTable:
    line 16: 0
    }
    SourceFile: "SynchronizedTest.java"

4.3.2 等待/通知机制

相关Java方法:

方法 描述
obj.wait() 在某个对象上等待
obj.notify() 通知1个在该对象上等待的线程,使其从wait()方法返回。
obj.notifyAll() 通知所有在该对象上等待的线程。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
static Object obj=new Object();
// A:
synchronized(obj){
obj.wait();
}
// B:
synchronized(obj){
obj.notify();
}
// 注意A得先启动,不然B发送的通知A可能收不到,就永远没人唤醒A了。
// 使用park,unpark可以避免这种情况。

4.3.4 管道输入/输出流

依然是使用共享内存进行通信的一种方法。具体实现有2种:

  1. 面向字节:PipedOutputStream/PipedInputStream
  2. 面向字符:PipedReader/PipedWriter

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
PipedWriter out=new PipedWriter();
PipedReader in=new PipedReader();
out.connect(in);// 注意这里,需要连接,否则出错
// 写:
out.write(xxx);
// 读:
while(receive=in.read()!=-1){
System.out.print((char)receive);
}
// 事后XD:
finally{
out.close();
}

threadA.join()用于线程同步

假如threadB中调用threadA.join(),意思就是等待threadA线程对象退出。
本质上join是一个sychronized方法,调用了线程对象的wait方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

因此join方法的特性大致与wait方法相同:线程状态变成waiting,能接受中断(IDE会提示受检异常),接受notify,等待时会释放锁。
threadA线程对象退出的时候,会调用notifyAll方法,通知所有等待它退出的线程。

4.4 线程应用实例

这节主要写了一个简单的线程池构造、使用示例,Web服务器示例。
实现中:

  1. 线程通信使用了原子变量AtomicInteger进行数据记录,记录多个线程成功及失败的线程数;
  2. 连接池的线程安全委托给了LinkedList,但是用Collections.synchronizedList包装了一下;另一个地方的实现则是用synchronized对所有相关容器的访问进行保护;
  3. 实验相关的代码,为了加大线程的冲突,用countDownLatch同步了线程的启动和结束;
  4. 使用了wait/notify机制,尽量使用了notify而不是notifyAll,避免唤醒太多;(感觉可以考虑使用unpark)

awk笔记

语法:

1
2
3
awk [参数] '具体命令' var=xxx file(s)
#
awk [参数] -f 具体命令所在文件 var=xxx file(s)

示例1:

1
2
3
4
5
# 每行按空格或\t分隔,输出文本的第1,4列
awk '{print $1,$4}' log.txt

# 比上面多了格式. (左对齐,第一列宽8,第二列宽10)
awk '{printf "%-8s %-10s\n",$1,$4}' log.txt

示例1中使用的是默认分隔符(空格或\t),也可以通过-F参数指定分隔符.

示例2:

1
2
3
4
5
6
7
8
# 使用逗号分隔
awk -F, '{print $1,$2}' log.txt
# 使用空格分隔
awk -F' ' '{print $1,$2}' log.txt
# 使用t字母分隔
awk 'BEGIN{FS="t"} {print $1}' log.txt
# 使用多个分隔符.
awk -F '[ ,]' '{print $1,$2,$5}' log.txt

awk -v #设置变量
示例3:

1
2
3
4
awk -va=1 '{print $1,$1+a}' log.txt
# mac下的话:
awk -v a=1 '{print $1,$1+a}' log.txt
# 非数字+1=1

示例4:(过滤)

1
2
3
4
5
awk '$1>2' log.txt  # 第一列大于2
# 非数字大于2

# 第一列>2且第二列为Are:
awk '$1>2 && $2=="Are" {print $1,$2,$3}' log.txt

示例5:(正则匹配)
~ 表示模式开始, 表达式首尾加上斜杠:
语法:
awk '$2 ~ /正则表达式/ {print $1}' log.txt

1
2
3
4
5
# 第二列符合正则: th[a-z]+ 
awk '$2 ~ /th[a-z]+/ {print $2}' log.txt

# 模式取反:
awk '$2 !~ /th[a-z]+/ {print $2}' log.txt`

awk脚本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/awk -f
#运行前
BEGIN {
math = 0
english = 0
computer = 0
printf "NAME NO. MATH ENGLISH COMPUTER TOTAL\n"
printf "---------------------------------------------\n"
}
#运行中 // 每一行
{
math+=$3
english+=$4
computer+=$5
printf "%-6s %-6s %4d %8d %8d %8d\n", $1, $2, $3,$4,$5, $3+$4+$5
}
#运行后
END {
printf "---------------------------------------------\n"
printf " TOTAL:%10d %8d %8d \n", math, english, computer
printf "AVERAGE:%10.2f %8.2f %8.2f\n", math/NR, english/NR, computer/NR
}

Decimal类型存储实验

  • 为啥要实验:
    <高性能mysql>第四章中提到decimal类型的存储空间时,用decimal(18,9)举例,说前9个数字占4B,后9个数字占4B,小数点占1B,总共9B.
    我觉得小数点也要占1B实在诡异,查了mysql官方文档和其他网上资料也没看到说小数点也占一个字节的说法,于是实验一番.

步骤1: 准备数据

  • 建表语句:
    1
    2
    3
    4
    5
    CREATE TABLE `du_decimal` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `dump_data` varchar(45) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=34 DEFAULT CHARSET=utf8

由于Innodb把数据和索引都存在了一个文件.idb里,为了只关注数据的存储空间占用,这里使用MyIsam引擎,只关注.MYD文件即可.

1
2
3
插入数据17行后容量:
$ ll du_decimal.MYD
-rw-rw---- 1 maintain maintain 340 Jan 29 09:10 du_decimal.MYD

之所以是17行,主要是想选个质数.XD

步骤2: 插入Decimal列

下表为新增不同Decimal列后数据文件的大小:

Decimal列 .MYD大小(B) 列新增大小(B)(每行)
340 0
Decimal(1,0) 408 4
Decimal(2,0) 408 4
Decimal(2,1) 408 4
Decimal(3,0) 408 4
Decimal(4,0) 408 4
Decimal(4,1) 408 4
Decimal(5,0) 408 4
Decimal(6,0) 408 4
Decimal(7,0) 408 4
Decimal(8,0) 408 4
Decimal(9,0) 408 4
Decimal(18,9) 476 8
Decimal(20,6) 544 12

由此可以看出Decimal(18,9)占了8B,而不是书上说的9B.(小数点不需要1B)
但因为操作系统最小分配4B,所以9位数字以内的存储占用都是4B.

Tips

此外,如果没有登录数据库那台机器的权限,可以使用sql进行数据文件大小查询:

1
2
3
4
5
SELECT TABLE_SCHEMA as db
,table_name
,data_length
FROM information_schema.TABLES
where TABLE_NAME='du_decimal'

高性能Mysql笔记-第4章

第四章 Schema与数据类型优化

4.1 数据类型

原则:

  1. 更小的通常更好,
    int 好于 char (如可以用int存储ip);
    date/time/datetime 好于 char.
  2. 尽量Not Null; // 因为NULL导致索引需要额外字节.// 不过这个影响不大

ip转换函数: Inet_AtoN(), Inet_NtoA().

4.1.1 整数

包括:

1
2
3
4
5
TinyInt : 8位 
SmallInt: 16位
MediumInt: 24位
Int: 32位
BigInt: 64位

如果加上unsigned,正数范围提高一倍.
对于计算来说,一般都会转化成BigInt进行计算;
聚合函数则转化成DecimalDouble.

Int(11)中的11没有意义,只是对于GUI或控制台来说显示长度为11.

4.1.2 实数

包括:

1
2
3
Decimal: 类似于字符串. 精确小数,使用mysql实现的计算;
Float : 4字节. 不精确,更快,使用cpu原生计算;
Double: 8字节. 不精确,更快.

Decimal(18,9):
总长18,小数点右边9个数字.(所以左边也是9个数字)
左边9个数字占4个字节,右边9个数字占4个字节.
总共8个字节.

  • Decimal的存储空间计算:
    mysql把每9位数字存成4个字节. (转化成2进制字符串存储)
    如果不足9位数字,存储空间消耗映射如下:

    1-2位=> 1字节
    3-4位=> 2字节
    5-6位=> 3字节
    7-9位=> 4字节

因此对于Decimal(20,6),左边14位数字需要空间=4+3=7B;右边6位需要3B,因此总共需要10B.

Decimal最多65个数字. 如果是Decimal(P,D),则P<=65,D<=30,且D<=P.
FLOAD,Double计算时转化为Double.

如果想要精确计算又想速度快(如财务数据):

使用BigInt,如果精度是1/10000,把所有数据乘以10000即可.

4.1.3 字符串

字符串需要关注的方面:

  1. 字符集
  2. 排序规则(校对规则/collation)
  3. 存储方式: 与存储引擎的具体实现有关.

字符串类型包括:

  1. char: 定长. 填充使用空格. 会截断末尾空格.
  2. varchar: 变长. (但如果使用ROW_FORMAT=FIXED,则会变成定长, 短的会填充补齐)
  3. Binary: 存储二进制字符串. (字节码) 填充时使用’\0’而不是空格.
  4. VarBinary: 变长二进制.
  5. BloB: 很长的二进制.
  6. Text: 很长的字符串.

varchar的存储
长度<=255: 1个字节存储长度信息,其余存储数据;
长度>255: 2个字节存储长度信息,其余存储数据.

latin1字符集:
varchar(10)存储空间= 1+10=11B.
varchar(1000)存储空间= 2+1000=1002B.
过长的varchar: InnoDb将其存储为BLOB.

UTF-8字符集:
varchar(1000)存储空间: 未知,因为UTF-8比较复杂,每个字符空间不定;
最大存储空间: 每个字符最多3B,因此最大空间为2+3000=3002B;

tip
对于数据’hello’,varchar(100)和varchar(5)实际存储空间一致(如果是latin1,6B),
但使用varchar(5)在计算时能减少内存消耗. (尤其是使用内存临时表进行排序时)

  • varchar的优点
    与char相比节省存储空间,因为短的数据会占用更少的空间,不像定长会浪费.

  • varchar的缺点
    变长行在update时可能需要额外工作,如果行变长,MyIsam会将行分拆成不同片段存储;
    InnoDb会分裂页来使行可以放进页内.

使用varchar的场景

  • max(列的长度) 远大于 avg(列的长度). 这样大部分数据只需要很少的空间.
  • 列的更新很少. (碎片不是问题)
  • 使用了像UTF-8这样的复杂字符集,每个字符都使用不同的字节数进行存储.

使用char的场景

  • 所有数据长度很接近. (如密码的MD5值,长度完全一致)
  • 列更新频繁.(没碎片)
  • 非常短的列. (不用存储长度信息)

char列中数据末尾的空格会被截断.还会根据排序需要填充空格.
(服务器层处理,与存储引擎无关)

BLOB和Text
BLOB家族:

  • TinyBlob
  • SmallBlob = Blob
  • MediumBlob
  • LongBlob

Text家族:

  • TinyText

  • SmallText = Text

  • MediumText

  • LongText

  • 存储
    Blob和Text均作为对象存储(行里只存储指针),Innodb会使用专门的外部存储区域来存储实际的值. 指针的存储空间为1~4个字节.

  • 排序
    对每列前缀排序,前缀长度为max_sort_length个字节.
    可以通过配置max_sort_length调整排序性能;
    或者写order by substring(col,length).

  • 索引
    Blob和Text列上建索引时,不能使用列的全部长度.

  • 性能
    由于Memory引擎不支持Blob和Text(以下讨论基于这个假设),
    如果查询需要隐式临时表,将会使用MyIsam的磁盘临时表(内存到磁盘,性能下降).

explain执行计划中的extra列中包含Using temporary,说明查询使用了隐式临时表.

tips

  1. 避免使用Blob和Text类型;(过长varchar会变成text,因此varchar不能过长(>255))
  2. 临时手动转换成字符串; // 在使用Text的地方写substring(col,length);

临时表从内存转化到磁盘的时机:

  • BlobText列;
  • 大小超过max_heap_table_sizetmp_table_size.

Enum(枚举)
如果字符串列的取值固定,可以使用枚举代替字符串. (会影响排序规则)
ENUM: 存储时为整数,比较时为字符串,但又不是原字符串.(= =||)

个人感觉, 这件事不应当在Mysql层做,应该在应用层做,因此此节略过.

4.1.4 日期和时间类型

相关类型包括:(最多只能精确到秒,如果要存储微秒,直接用bigint吧)

  1. DateTime: 1001年到9999年. 精度为秒. 8个字节整数.
  2. Timestamp: 1970年到2038年. 精度为秒. 4个字节整数.(才到2038年,根本不能用)

个人感觉还是BigInt好,干净利落. (8B)

4.1.5 位数据类型(技术上来说是字符串类型)

相关类型包括:
1.Bit. (避免使用,行为诡异,游离在字符串和数字间)
例如Bit(2)可以存储俩布尔值.最大是Bit(64).
MyIsam: 所有Bit列打包起来存储;
InnoDb,Memory: 每个Bit列使用一个恰当大小的整型存放.
服务器层: Bit列作为字符串类型检索,遇到数字上下文又变成数字.

// 也就是存储时是数字,使用时是字符串或数字.

2.SET: 保存多个布尔值.
可以使用Find_in_set()/Field()函数.

  • 缺点:
    改变列定义的代价昂贵. 也无法在Set列上进行索引查找.

  • 小结

    Mysql的位数据类型都不靠谱,还是使用整型吧.
    自己在应用层进行位操作, 简单/清晰一些.

4.1.6 ID列的数据类型选择

通常使用整型. 确定数据范围即可.

可以考虑使用美团的分布式ID生成器.

4.2 数据库设计原则

  1. 避免太多的列; 服务器层需要缓冲存储引擎层的行,变长的行会引入转换开销.
  2. 避免太多关联;
  3. 恰当使用NULL. 大多数时候避免NULL列,有时候可以使用.(当异常值不好定义时)

4.3 范式与反范式

范式优点:

  • 更新简单;
  • 冗余少;
  • 设计更优雅;
  • 简单查询性能高.

范式缺点:

  • 关联多,复杂查询性能低.
  • 复杂查询性能差.

反范式(nosql)优点:

  • 关联少,复杂查询性能好.

反范式缺点:

  • 经常需要distinct,group by操作,影响性能, 代码写起来烦.

  • 更新性能差.

  • 更新逻辑复杂.

  • 小结
    适当程度使用范式,不迷信三范式.
    特定需求可以单独定制相应的缓存表和汇总表,而不改变原有的设计结构.

4.4 缓存表和汇总表

对于某些检索需求,可以定制化得设计相应的缓存表和汇总表,而不用更改原有的设计.
如需要计算24小时内发送的消息数. 可以维护一个表.

4.4.1 物化视图

Mysql并不原生支持,需要插件(FlexViews).

4.4.2 计数器表

100个槽的计数器表:(预先插入100个0)

1
2
3
4
create table hit_counter(
slot tinyint unsigned not null primary key,
cnt int unsigned not null
) engine=innodb;

每次更新计数器可以随机选一个slot进行,这样可以有更高的并发性能.
查询时只需要聚合一下结果就好:

1
select sum(cnt)

4.5 Alter Table操作的优化

Mysql执行Alter操作流程: (大部分情况下)

  1. 用新的结构创建一个空表;
  2. 从旧表中查出所有数据插入新表.
  3. 删除旧表.

Innodb的优化: 通过排序来建立索引.使建索引更快并有一个紧凑的布局.

大部分Alter操作导致Mysql服务中断. // 导致事务强制提交.
相应解决技巧:

  1. 在一个不提供服务的机器上进行Alter,然后和提供服务的主库进行切换;
  2. 影子拷贝. 复制一张新表,通过重命名交换两张表.

不会引起表重建的Alter操作:

  • 更改或删除一个列的默认值.

方法1: // 重建全表// 可以通过Show Status查到.

1
2
Alter table xxx
MODIFY COLUMN duration tinyint(3) not null default 5;

方法2:// 直接更改元数据(.frm文件).

1
2
Alter table xxx
Alter Column duration Set Default 5;

修改列的方法包括: Alter Column,Modify Column,Change Column.

  • ALter Column: 只能修改列的默认值.直接修改元数据,非常快.

    1
    2
    alter table film alter column rental_duration set default 5;  
    alter table film alter column rental_duration drop default;
  • Change Column: 可以修改列的一切. 修改数据(重建表)

  • Modify Column: 比change少一个重命名功能,其他一样.

4.5.1 只修改元数据(.frm文件)

适用场景:

  • 移除一个列的Auto_increment属性;
  • 增加移除更改ENUM和SET常量.

流程:

  1. 创建一张相同结构的空表,进行相应修改;
  2. 关闭正在使用的表,并禁止任何表被打开; (通过执行Flush tables with read lock. )
  3. 交换.frm文件;
  4. 释放第二步的锁.(执行Unlock Tables)

4.5.2 快速创建MyIsam索引

流程:

  1. 禁用索引;// Alter table xxx Disable Keys;
  2. 加载数据;// Load Data
  3. 启用索引.// Alter table xxx Enable Keys.

之所以能更快,是因为不用逐行更改索引了. 批量建立索引可以使用排序构建, 索引树碎片更少,更紧凑.

由于Disable Keys对于唯一索引无效(Unique Key),所以如果涉及到唯一索引,可以这么做:

  1. 删除唯一索引;
  2. 加载数据;
  3. 重建唯一索引.
    // 这种方法下,唯一性需要自行保证. 应用场景: 备份数据, 对数据知根知底的情况下.

还有一种方法是通过交换元数据文件,如上一节中的做法.
然后通过Repair Table来重建索引, 个人觉得有些危险,还是删索引比较简单安全.


前面两章的内容:(需要网上资料配合实验)

第二章 Benchmark

第三章 服务器性能剖析

查看某条查询执行的时间分布:

1
2
3
4
set profiling=1;
-- select * from xxx ;
show profiles;
show profile for query 1;

查看某查询的执行计划:

1
explain select xxx;

sending data中的行为包括:

1
2
关联时搜索匹配的行
...

java并发编程的艺术笔记-第三章

Java内存模型

内容有4个部分:

  1. 基本概念
  2. 顺序一致性
  3. 同步原语的内存语义
  4. 内存模型的设计原理

3.1 基本概念

并发编程的两个关键问题:

  1. 线程之间通信: 何种机制交换数据,包括共享内存和消息传递;
  2. 线程之间同步: 不同线程的操作发生的顺序.

JAVA的线程通信: 共享内存(隐式).

JAVA的内存:

  1. 堆内存(共享): 实例域,静态域,数组元素;
  2. 栈内存(不共享): 局部变量,方法参数,异常参数.

线程A,B通信流程:

  1. 线程A把自己内存中更新过的共享变量刷新到主内存中;(把cpu缓存刷到内存)
  2. 线程B把到主内存中读取A更新后的共享变量. (把内存刷新到cpu缓存)

3.1.3 源代码到指令序列的重排序

3种重排类型:

  1. 编译器优化的重排序: 单线程程序语义不改变;
  2. 指令级并行的重排序: 多线程的指令重排并行执行;
  3. 内存系统的重排序: 指令实际执行的时候,由于缓存/缓冲的存在,加载和存储可能看起来是乱序执行.

2和3属于处理器重排序.

JAVA内存模型约束:

  1. 禁止某些编译器优化;
  2. 插入某些特定类型内存屏障(Memory Fence指令),禁止特定类型处理器重排序.

3.1.4 并发编程模型的分类

写缓冲区: 临时保存向内存写入的数据. (避免cpu等待io)
仅对该cpu可见.

内存屏障指令:
1.LoadLoad Barries

1
2
Load1;LoadLoad;Load2
// 确保Load1的装载先于Load2及后续Load指令

2.StoreStore Barries:

1
2
3
Store1;StoreStore;Store2
// 确保Store1的数据先于Store2及后续Store指令
// 刷新到内存

3.LoadStore Barries:

1
2
Load1;LoadStore;Store2
// 确保Load1的装载先于Store2及后续Store指令

4.StoreLoad Barries:

1
2
3
4
Store1;StoreLoad;Load2
// 确保Store1的装载先于Load2及后续Load指令
// 刷新到内存.
// 且会使它之前的所有内存访问指令(Load和Store)都完成后,才执行之后的.

其中第四个,StoreLoad屏障最严格,会把写缓冲区刷新到内存,开销最大,大部分cpu都支持.

3.1.5 Happens-before规则

  1. 程序顺序规则: 单线程内顺序一致性; // 有数据依赖的指令不重排. 保证结果和顺序执行一致即可.
  2. 锁规则: 解锁先于获得锁;
  3. volidate,原子变量: 写before读;
  4. 线程启动. Thread.start之后才会有run等其他操作发生.
  5. 中断. (1)A线程中断B线程;(2)B检测到中断. 保证(1)在(2)前面.
  6. 终结器. 构造函数在终结器之前执行完成.
  7. 传递性: 上述规则可以传递.

上述规则被JVM翻译成各种约束(具体来说就是在指令里加一些内存屏障),影响了编译器重排序和处理器重排序.

3.2 顺序一致性

顺序一致性

顺序一致性: 所有线程看到的执行顺序一致.
正确使用了同步机制,才能达到顺序一致性.
主要解决的是以下两者的矛盾:
(1) 程序员希望指令重排的约束越多越好, 保证执行结果好理解;
(2) 编译器/cpu希望约束越少越好,执行可以快一些.
顺序一致性: 约束的数量够用,让程序运行的结果与完全约束(顺序执行)一致.

3.2.1 数据依赖性

编译器和处理器:

  1. 单线程: 考虑数据依赖性; (写后读,写后写,读后写)
  2. 不同线程\不同处理器: 不考虑数据依赖性.

3.2.2 as-if-serial语义

编译器和处理器: 单线程语义不变.
// 约束够用,执行结果和串行执行结果一致.


一些约束的具体实现方案:

volatile变量

读: 读取最新的内存值(不一定是CPU缓存值); (有原子性)
写: 写入cpu缓存后,刷新到内存; (有原子性)
自增: 相当于先读后写两个操作, 不具有原子性.

volatile代替锁.(从内存语义上说,由于它与锁有相同内存效果,可以实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
volatile boolean flag=false;
A:
a=10;
flag=true;

B:
int i;
while(true){
if(flag){
i=a;
break;
}
}
  1. 对A来说,由于单线程的语义符合程序顺序规则,flag=true发生在a=10之后;
  2. 对B来说,由于flag是volatile变量,flag=true之后才能取到i. 因此达到了通信的效果,B中能正确取到A中的a的10.

3.4.4 volatile内存语义的实现

内存屏障:(JMM内存模型采用的是保守策略,保证在所有cpu上都能正确)

  1. volatile写前面插入StoreStore屏障;
  2. volatile写后面插入StoreLoad屏障;
  3. volatile读后面插入LoadLoad屏障;
  4. volatile读后面插入LoadStore屏障.

volatile写指令序列:

1
2
3
4
5
普通读写
StoreStore屏障// 先把前面的Store刷新到内存,再执行volatile写.(其实没必要,只是新版本赋予了volatile锁的内存语义)
volatile写
StoreLoad屏障 // 先把volatie写的store刷新到内存,再执行读.(免得读到污染值)
volatile读

volatile读指令序列:

1
2
3
4
volatile读
LoadLoad屏障 // 禁止下面的读越过volatile
LoadStore屏障// 禁止下面的写越过volatile
普通读写

这俩屏障x86里没有.因为x86只有写-读重排序,没必要有.
此外,个人理解这俩屏障也没啥意义,唯一能想到的用处还是让voldatile具有锁的功能. 毕竟是JSR-133之后才加的.

3.5 锁的内存语义

ReentrantLock的实现依赖于:

1
2
3
4
FariSync
NonfaiSync
Sync
AQS(AbstrackQueuedSynchronizer)

其中AQS里头有个volatile变量:

1
private volatile int state;

锁的内存语义基本就靠这个volatile变量和CAS操作.

1.加锁流程:(公平锁)

1
2
3
获取volatile变量state;
CAS操作更改state变量的值.
若更改成功则获取锁成功.

2.解锁流程:(公平锁)

1
2
3
一些操作...
写state变量.
其他线程立刻发现了state变化,可以竞争这个锁了.

CAS操作能保证(读-改-写)操作原子执行.
CAS操作的底层实现:

1
2
3
LOCK总线;
禁止该指令与前后读写指令重排;
将写缓冲区数据刷新到内存.

3.5.4 concurrent包的实现

实现层次如下:

1
2
3
LOCK,同步器,阻塞队列,Executor,并发容器
AQS,非阻塞数据结构,原子变量类
volatile变量的读写,CAS

3.6 final域的内存语义

3.6.1 final域的重排序规则

  1. 在构造函数内对一个final域的写入,与随和把这个对象的引用赋值给一个引用变量,这俩操作不能重排;
  2. 初次读包含final域的对象,初次读这个final域,这俩操作不能重排.

规则1的实现(构造函数中):

1
2
3
final域的写
StoreStore屏障 // 先把上述写(Store)的结果刷新到内存. 再写引用.
构造函数return // 把对象地址赋值给引用

示例代码:

1
2
3
FinalObj obj=inputObj; // 别的线程负责创建的对象
int a=obj.i;// 如果i是final的,那一定能读到初始化以后的值;
int b=obj.j;// 如果j不是final的,那可能读到还没初始化的值.

规则2的实现:

1
2
LoadLoad屏障// 保证先Load到引用. (明明有数据依赖)
final域的读

大多数cpu不需要这个规则也能正确进行final读,但还是有少部分cpu会无视间接依赖,因此需要这个规则.(需要加入这个屏障)
示例代码:

1
2
3
FinalObj obj=inputObj; // 别的线程负责创建的对象
int a=obj.i;// 如果i是final的,那会等obj读到对象引用后再去读i;
int b=obj.j;// 如果j不是final的,那可能没等到读到对象obj,就直接取读j了,读取错误.

最后,由于x86处理器很多重排并不会做,所以其实final域的内存屏障都不需要加,会被省略.

3.8.4 类初始化中的约束

类的初始化是啥:

Class加载=>连接=>初始化=>使用=>卸载;
其中连接分为:
验证=>准备=>解析.

具体来说:

  • 加载
  1. 通过类全名获取二进制字节流;
  2. 将字节流中的静态存储(常量)转换为方法区的运行时数据;
  3. JAVA堆中生成代表类的Class对象,指向方法区的数据.
  • 验证: 格式/元数据/访问验证;

  • 准备: 类变量(非常量)分配到堆,赋予初值;

  • 解析

    4类符号引用的解析: 类/接口,字段,类方法,接口方法. 把这些引用解析到实际的目标对象.
    这个和初始化过程可能混在一起.

  • 初始化

    1. 分配对象的内存空间
    2. 初始化对象
    3. 设置instance指向内存空间
  • 初始化发生的时机:// T是一个类

    1. T的实例被创建;// 如new
    2. T的静态方法被调用;// static
    3. T的静态字段(非常量)被使用/赋值; // static成员被读写.
      // 对于第3点,如果是类常量,加载阶段已经完成了.

还有一个时机:

4.T是一个顶级类,而且一个断言语句嵌套在T内部被执行.// TODO
// 这个没懂.= = ||

小结:

static常量: 加载期完成,存储在方法区,从堆区Class对象指过去;
static变量: 准备期分配到堆,初始化阶段赋值.
成员变量: 初始化阶段完成.

类初始化存在竞态条件
多个线程可能同时对一个类进行初始化,因此需要锁.
JVM对于每一个类或接口都有唯一的初始化锁LC.(放在Class对象中)
初始化流程:

  1. 获取Class对象中的LC锁;
  2. 读取Class对象的state,发现是noInitialization,设置为initializing;// 表示从未初始化改成正在初始化,以便别的线程知道;
  3. 释放LC锁. // 这个时候别的线程可以获取到锁,然后知道有人正在初始化,于是等待.
  4. 初始化类的static变量;
  5. 分配实例对象内存空间,初始化对象,赋值地址给引用(解析);
  6. 设置state=initializated. // 初始化结束. 唤醒等待的线程.

基于上述过程的单例如下:

1
2
3
4
5
6
7
8
9
10
11
public class A{ 
private static class B {
public static C c=New C();
}

public static C getC(){
return B.c; // 导致B类初始化.
}
}
// 使用时:
C c=A.getC(); // 导致A被初始化.

相比于双检,利用了Class对象的初始化锁LC. 达到线程安全的目的.
上述初始化流程的第5,6步顺序不会重排,因此可以不用volatile来保证引用的解析晚于对象的构建;

之所以使用内部类B: 想达到懒汉的目的.
如果不用内部类, 可以这么写:

1
2
3
4
5
6
7
8
9
public class A{ 
private final static C c=new C();
private A(){}
public static C getC(){
return c;
}
}
// 使用:
C c=A.getC(); // 导致A被初始化.

区别在于导致A被初始化的途径多,而上一个方法中导致B被初始化的途径少(只有getC一个途径,毕竟B是private).
对于第一个方法:

A被初始化=>没关系;
调用getC()方法=>B被初始化=> c对象构建.

对于第二个方法:

A被初始化=> c对象构建.

A被初始化的途径很多,(A的静态方法被访问,A的静态变量被访问,A被创建实例,A中有断言执行),如果要让第二个方法达到足够懒,需要额外做的事情:

  1. 只有getC()一个静态方法;
  2. 只有C一个静态变量;
  3. 不让创建对象(private构造函数)
  4. 没有断言执行嵌套在内部.

不过两种方法都不能抵挡序列化.(得用枚举才行)

3.9 JAVA内存模型综述

3.9.1 CPU内存模型

根据重排的剧烈程度,或者说约束的强弱,可以把CPU划分为几种类型:
(性能由低到高,约束逐渐减少)

  1. TSO模型(Total Store Order). 写-读顺序会进行重排;// 因为要使用写缓冲区
  2. PSO模型(Partial Store Order). 写-读,写-写都会进行重排.
  3. RMO模型(Relaxed Memory Order). 写-读,写-写,读-读,读-写都会进行重排.
    (PowerPC内存模型也是)

上述重排都在遵守数据依赖的前提下,不然连单线程程序的正确性都无法保证了.
所有CPU都起码是TSO模型,因为都需要写缓冲区.

3.9.2 各种内存模型之间的关系

JAVA内存模型: JMM. 是语言级内存模型, 把底层的CPU的内存模型进行封装, 通过加入内存屏障, 让底层对于程序员来说变成一致的JMM. (平台无关)