带有场景的举例子:
小A 在linke上建立的新的迭代,同时生成了代码分支 dev
,然后 A、B两个同学都在这个迭代进行开发。小 A 基于 dev
分支创建了 dev_A
分支,小B基于dev分支创建了dev_B
分支。后来小A将他的变更合并到了 dev 分支,小B在开发过程中由于和master分支有冲突,所以他在本地解决冲突合并了一下 master 分支,然后又合并了一下 dev 分支,然后推送到 antcode 进行 MR,发现MR中代入了 master 的代码,和小A已经提交的代码?why?
git diff [<options>] <commit>...<commit> [--] [<path>...] |
既然 "git diff A...B" is equivalent to "git diff $(git merge-base A B) B"
,那我们先来看看 git merge-base A B
,看看这个是个啥?
简单来说就是 找到 A 和 B 的两个的最近的共同祖先,然后在和 B 做比较。
这个时候我们来看下上面那个场景的拓扑图
这个是 小 B 提交了合并到 dev
的 MR,他看到的对比应该是
dev
和 dev_B
的共同祖先是 X
节点,所以展示的diff的区别就是 X
和 devB
最新commit 的区别,所以区别上会带入其他迭代的代码和dev_A
的代码。
不用理会这个告警
]]>一面主要考察了基础知识,同时对于原来公司负责的服务也进行了考察。我统计了一篇基础知识的笔记,在我的公众号做分享。
二面则是基本上都是项目的介绍,面试官让我讲我负责的项目。就这样一个一个的讲,讲得我口干舌燥。
这里我感觉要突出你项目的亮点,你用了什么方式,解决的什么问题,用到了什么技术。由于这次面试主要是项目所以,没有太多可以具体的可以分享。如果能在面试之前过一遍自己的项目,然后准备一些技术难点或是隐藏比较深的问题的解决思路。
还有就是需要有一定的设计能力,当你自己提出了一个问题之后会问你是如何解决的这个问题,然后如果是现在的你会如何解决,有没有更好的方案,目的就是找出你的极限。
三面是主管和HR一起进行面试的,主管应该就是我以后的汇报主管。问的问题主要包含历史绩效、在原来公司的职级,为什么要换工作、希望在工作中负责什么部分、为什么没有考研、蚂蚁这边的加班是否能够接受、有么有遇到什么挫折。我看来主要是看我这个候选人的性格、为人是否符合公司的价值观吧。这个阶段的 HR 的问题基本上都是类似的,比起前面的技术面试更加有套路。
完整版基础知识资料请关注我的个人公众号,回复 “蚂蚁面经“ 获取 HTML 版本。
]]>先看一眼概念也未必是坏事:动态规划 - 维基百科,自由的百科全书
动态规划的所有题目,在不考虑性能的情况下,都可以使用简单的递归来解决。
下面来看一道 LeetCode 经典动态规划题目:(70) Climbing Stairs - LeetCode
You are climbing a stair case. It takes n steps to reach to the top.
Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
Note: Given n will be a positive integer.
Example 1:
Input: 2 |
Example 2:
Input: 3 |
公式如下(这个也是做动态规划必须要列出的公式)
我们来试试用简单的递归来解决这个问题:
/** |
如果我们这时候计算的是 climbStairs(5)
,通过下图可以看到我们一共计算 f(3)
2次、 f(2)
3次、 f(1)
2次。这是在台阶数 n 很小的情况下,如果 n 很大的话,重复计算的模块会变的更多。
这个时候如果我们能够通过一个数组来记录中间结果就好了,当上图中的每一个节点如果已经计算过,就不再重复计算了。
代码如下:
class Solution { |
递归会新建很多局部变量,建立很多栈帧,带来很多不必要的消耗,如果能够去除递归效率会有进一步的提升。
上面我们都是从图中的根结点一步一步向下计算,是一个深度优先搜索的过程,如果我们从根节点向上算就能去掉递归了。
代码如下:
/** |
多刷点题,就啥都明白了。
]]>众所周知,线程阻塞带来的上下文切换的代价是很大的,Java 为了尽量减少上下文的切换从而引入了更多的锁机制。在了解各种锁机制之前,先要学习一些前置知识。对于各种锁的获取和释放、以及锁升级的流程,在文末总结处有一张图,如果赶时间,直接看图吧。
Java 对象头里面有一部分叫做 Mark Word,在32位虚拟机下面占有 32bit,在 64 位虚拟机下面占用 64 bit,本文以 32 位虚拟机为例子。
偏向锁:一个线程反复的去获取/释放一个锁,如果这个锁是轻量级锁或者重量级锁,不断的加解锁显然是没有必要的,造成了资源的浪费。于是引入了偏向锁,偏向锁在获取资源的时候会在资源对象上记录该对象是偏向该线程的,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断改资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
轻量级锁:轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 在轻量级锁中如果发生多线程竞争,未持有锁的线程会自旋等待。
重量级锁:由操作系统的 mutex 来实现,多线程竞争下,未持有锁的线程将被阻塞。
首先确定该类的偏向锁是否可用(决定了在新建对象实例的时候倒数第三 bit 是 0 还是 1);如果不可用,直接使用轻量锁。如果偏向锁可用,新建实例对象 obj,此时 obj 进入到未锁定、未偏向、可偏向的状态。
此时线程 A 想要获取对象 obj 的偏向锁,由于此时 obj 没有偏向任何线程(有可能是刚刚新建,也有可能是由锁定状态重偏向之后导致的未锁定状态),所以利用 CAS 操作将线程 ID 写入到 Mark Word 里面,此时 线程 A 获取了 obj 的偏向锁。obj 处于已偏向、锁定状态。
一旦 obj 第一次偏向了线程 A,A 就可以在没有竞争的情况下,也就是锁不升级的情况下,以极小的代价反复获取 obj 对象的锁。
如果 obj 先被线程 A 锁定,然后释放。然后线程 B 过来是否能重新获取偏向锁呐?在一定条件下,经过了重偏向可以重新获得偏向锁。
通过上面的 mark word 我们可以看出,在偏向锁的时候有一个字段是 epoch,同时在 obj 的类 O 信息里面也有一个 epoch,每次系统到达安全点会对类的 epoch 加 1,变成 epoch_new,然后扫描所有的类 O 的实例,判断该偏向锁是否还被持有,如果被持有则将 epoch_new 复制给对象头的 epoch 字段。
每次去获取偏向锁的时候回去判断对象实例的 epoch 和 类的 epoch 是否相等,如果不等代表对象是未锁定、可偏向、未偏向状态,可以偏向新的线程。
Once biased, that thread can subsequently lock and unlock the object without resorting to expensive atomic instructions. Obviously, an object can be biased toward at most one thread at any given time. (We refer to that thread as the bias holding thread). If another thread tries to acquire a biased object, however, we need to revoke the bias from the original thread.
线程 B 来竞争,此时偏向锁会膨胀位轻量级锁。当 B 线程想利用 CAS 获取偏向锁失败, A 线程被暂停,然后检查 A 线程;
A 线程已经退出了同步代码块,或者是已经不在存活了,如果是上面两种情况之一的,将 obj 的 Mark Word 后 3bit 改为 001(无锁状态),然后再去唤醒 A 线程。
A 线程还在同步代码块中,此时将 A 线程的偏向锁升级为轻量级锁。首先会 copy 一份 obj 对象的 mark word 内容,放到线程 A 当前栈帧的一个叫做 Lock Record 空间的 displace mark word 的位置,
然后通过 CAS 将 obj 中 mark word 的内容替换为指向栈帧中 mark word 的位置,最后将 Lock Record 里面的 owner 指向 obj 对象。并且对象Mark Word的锁标志位设置为“00”,到此完成了锁的升级。
Java 会在类信息里面维护一个计数器记录,记录该类发生偏向锁 revoke 的次数,一旦达到某个阈值,则认为这个类不适合偏向锁,将会禁用这个类的偏向锁功能。这个类新建的对象实例的最后 3bit 将是 001(无锁)。
Objects that are explicitly designed to be shared between multiple threads, such as producer/consumer queues, are not suitable for biased locking. Therefore, biased locking is disabled for a class if revocations for its instances happened frequently in the past. This is called bulk revocation. If the locking code is invoked on an instance of a class for which biased locking was disabled, it performs the standard thin locking. Newly allocated instances of the class are marked as non-biasable.
刚刚在上文中关于撤销偏向(revoke)中已经描述了锁定的流程。在此再总结一遍
线程 A 在去获取轻量级锁的时候,会首先使用 CAS 操作,如果操作失败那么会在此时判断是不是该线程已经持有过该对象的锁了,通过判断对象的 mark word 是不是指向当前线程的栈帧,如果是则会在最新的栈帧处新建一个 displaced mark word 为 null 的 lock record。关于为什么要这么做,其实就是为了记录一下锁的重入,发生重入了需要记录,本来是记录到 mark word 里面最方便,可能是 mark word 没有足够的空间。如果是在 lock record里面记录的话,需要遍历 lock record 才可以获取这个数量。所以最终Hotspot选择每次获得锁都添加一个Lock Record
来表示锁的重入。
类似的递归的解锁只需要将栈帧中的 lock records 删除即可。
线程 A 持有对象 obj 的轻量级锁,线程 B 过来 CAS 失败,开始自旋等待,自旋到达一定次数之后如果还没有获取该轻量级锁,则会将该锁膨胀为重量级锁。会将 mark word 的的锁标志为改为10,同时经指针指向互斥量,然后线程 B 挂起。
JDK1.6中 -XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数。
JDK1.7后,去掉 -XX:PreBlockSpin 参数,由jvm控制。
轻量级锁解锁也是理由 CAS 将 mark word 里面的指针替换为无锁的 mark word 信息。需要判断 mark word 里面是指向该线程的 lock record
释放 mutex,唤醒在等待的线程
新标签页打开图片可看大图
java 中的锁 — 偏向锁、轻量级锁、自旋锁、重量级锁 - zqz_zqz的博客 - CSDN博客
Java 8 并发篇 - 冷静分析 Synchronized(下) - 知乎
Biased Locking in HotSpot | Oracle David Dice’s Blog //rebias
Java中的偏向锁,轻量级锁, 重量级锁解析 - 神评网 //rebias
]]>通常可以把一系列的分布式的操作序列称之为子事务。分布式事务也可以被定义为一种嵌套型事物。分布式系统不可能像单机系统那样可以严格满足 ACID 特性,所以要有不同的取舍,因此有了 CAP 和 BASE 理论。
一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求。
一致性: 在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。数据在发生变化的时候,需要保证发生变化之后,所有的副本都能保持一致。
可用性:系统提供的服务必须一直处于可运送的状态,对于每一个请求总是能够在有限的时间内返回结果。
分区容错性: 分布式系统在遇到任何网络分区故障的时候,仍然需要保证对外提供满足一致性和可用性的服务。
对于 CAP 的取舍:放弃 P 的通用做法是将所有的数据都放到一个节点,这样可以避免网络分区带来的问题,但是值得注意的是,放弃了 P 也就等于放弃了系统的扩展性;放弃 A :当系统发生故障的时候,允许系统短时间内不可用,需要等待系统恢复后方可继续提供服务; 放弃 C:放弃 C 并不是说放弃最终一致性,而是说放弃强一致性,保证系统在某段时间之后能够达到最终一致性。
对于一个分布式系统而言,不同节点之间必然存在网络通讯,因此网络分区问题是不可避免的。同时分布式系统的扩展特性也是无法舍弃的。因此系统的设计和架构一般都考虑如何权衡 A 和 C。
BASE 是 Basically Available(基本可用)、Soft status(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。
基本可用:在系统出现故障的时候,允许损失部分可用性。比如响应时间上的损失和功能上的损失。
软状态: 也称为弱状态,表示允许系统中的数据存在中间状态,即允许不同的数据副本之间数据同步存在延迟。
最终一致性: 经过一段时间的同步之后,系统最终能够达到一个数据一致的状态。
最终一致性的一些变种:
- 因果一致性:进程 A 更新数据后通知了进程 B,进程 B 必须对修改后的数据可见。
- 读己之所写:进程 A 对数据做的更改,自己总是能够获得最新的值。
- 会话一致性:在单个会话中,进程 A 总是能够看到最新的值。
- 单调读一致性:在系统中读出数据项 Y 的值 y 后,后面的读取中不允许返回比 y 更旧的值。
- 单调写一致性:系统需要保证来自同一个进程的写操作被顺序的执行。
2PC 和 3PC 就是用来保证 CAP 中的 C 或者 BASE 里面的 E 的协议。
当一个事务需要跨越多个分布式节点的时候,需要引入一个成为“协调者”(Coordinator)的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为“参与者”(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否真的需要把事务进行真正的提交。由此,衍生出二阶段提交和三阶段提交两种协议。
两阶段提交主要包含投票(Propose)和执行(Commit)两个阶段。
在 Propose 阶段,协调者向所有参与者(voter)发送事务内容,询问是否可以执行事务提交操作,并等待参与者的响应。各个参与者为该事务预留资源,保证在下一个阶段能够提交事务,如果资源可以获取反馈给协调者 agree 响应,反之返回 disagree 响应。
事务提交:如果在 Propose 阶段,所有参与者都返回的是 agree 信号,那么协调者会发送提交事务的请求;所有参与者收到事务提交请求后会正式执行事务提交操作并释放事务执行期间占用的资源,执行完成后将事务执行结果反馈给协调者;协调者收到所有参与者的反馈信息后,事务执行完毕。
事务中断: 如果在 Propose 阶段有参与者返回 disagree 或者有的节点超时未返回,没有投票达成一致,那么协调者会向所有参与者发送回滚请求;参与者收到回滚请求之后会释放掉在 Propose 阶段占用的资源,然后反馈信号给协调者;协调者收到反馈消息之后,完成事务终端。
二阶段提交协议的优点是:原理简单,实现方便。
二阶段提交协议的缺点是:
假设coordinator和voter3都在Commit这个阶段crash了, 而voter1和voter2没有收到commit消息. 这时候voter1和voter2就陷入了一个困境. 因为他们并不能判断现在是两个场景中的哪一种:
- 上轮全票通过然后 voter3 第一个收到了 commit 的消息并在 commit 操作之后 crash 了。
- 上轮 voter3 反对所以干脆没有通过。
三阶段提交是在二阶段提交的基础上进行的改进,将二阶段提交的 Propose Phase 一分为二,形成了 CanCommit、PreCommit 和 doCommit 三个阶段。
CanCommit:协调者向所有参与者询问是否可以执行事务提交操作;各参与者想协调者反馈是否可以提交事务。
PreCommit:
doCommit:
需要注意的是,在 doCommit 阶段,可能出现下面的两种故障:
无论出现上面那种情况,都会造成从黁税额无法及时收到来自协调者的 doCommit 请求或者是 abort 请求,针对这种情况,参与者会在等待超时之后,继续事务的提交操作。
三阶段提交协议的优点是:相比于 2PC 能够在单点故障后继续达成一致。
三阶段提交协议的缺点是:引入了新的问题,如果某个参与者与协调者出现在不同的网络分区,那么该参与者会提交事务,有可能出现数据不一致的情况。
1/2
才对呀。直到我去看了一些分析,写了这篇文章。蒙提霍尔问题,亦称为蒙特霍问题或三门问题(英文:Monty Hall problem),是一个源自博弈论的数学游戏问题,大致出自美国的电视游戏节目Let’s Make a Deal。问题的名字来自该节目的主持人蒙蒂·霍尔。
这个游戏的玩法是:参赛者会看见三扇关闭了的门,其中一扇的后面有一辆汽车或者是奖品,选中后面有车的那扇门就可以赢得该汽车或奖品,而另外两扇门后面则各藏有一只山羊。当参赛者选定了一扇门,但未去开启它的时候,知道门后情形的节目主持人会开启剩下两扇门的其中一扇,露出其中一只山羊。主持人其后会问参赛者要不要换另一扇仍然关上的门。问题是:换另一扇门会否增加参赛者赢得汽车的几率?如果严格按照上述的条件的话,答案是会。换门的话,赢得汽车的几率是2/3。
这条问题亦被叫做蒙提霍尔悖论:虽然该问题的答案在逻辑上并不自相矛盾,但十分违反直觉。这问题曾引起一阵热烈的讨论。
先说一下错误的思维方式:当主持人打开一扇后面有羊的门之后,问题就变成了有两扇门,一扇门里有汽车,一扇门里有羊,选择任何一个门获的汽车的概率必然是相同的,也就是1/2
。
上面这种方式的问题就是,打开一扇门后,并不等价于在两扇门里做选择;而是你是否需要转换。(人的直觉往往是不可信的)
当年的玛丽莲·沃斯·莎凡特用整整4个专栏,数百个新闻故事及在小学生课堂模拟的测验来说服她的读者她是正确的。
我们只有最最简单的列出所有可能的情况就可以证明这个问题。
上图可以看到,转换后获胜的概率是2/3
。
但是有质疑是为什么第一种情况下不可以分为 A 羊和 B 羊,而是算作一种情况呐。我认为这是由于打开门后出现 A 羊还是 B 羊,并不会影响我们计算的概率,影响概率的是主持人选羊这件事。所以 A 羊和 B 羊并不能算作两种情况。
概率问题通过增大样本容量来获取接近概率的值。所以不如写一段代码来看一下结果
🐑🐑 区分 A 羊和 B 羊
public class Main { |
换选获胜次数:666072 |
🐑 不区分两只羊
public class Main2 { |
换选获胜次数:665600 |
如果在使用 Iterator
遍历一个元素的时候,如果同时使用 List.remove()
方法去移除元素,会报出 ConcurrentModificationException
异常。如何避免发生这种异常,以及如何为什么会抛出这个异常。
先上代码:
public static void main(String[] args) { |
异常日志:
Exception in thread "main" java.util.ConcurrentModificationException |
我们看上面的代码貌似没有使用 Iterator
呀,其实上面的 foreach
循环就是其实就是使用了这个对象,甚至如果你使用这样的语句 System.out.print(list)
也会使用的list
对象的迭代器。我们把上面的代码改写一下
public static void main(String[] args) { |
iterator.next()
究竟在哪里抛出了异常,我们来一探究竟。首先 iterator
是 ArrayList.Itr
类型,next 方法源码如下
public E next() { |
final void checkForComodification() { |
到这里我们明白,抛出异常的原因是因为 modCount != expectedModCount
,但是这两个字段是什么含义呐,为什么会不相等?
modCount
是AbstractList
的一个字段,用来表示这个容器实例被修改的次数,如果容器中的元素有增加、移除、替换等操作的都会修改这个值。expectedModCount
是 ArrayList.Itr
类的一个字段。在创建迭代器的时候,会将modCount
赋值给expectedModCount
private class Itr implements Iterator<E> { |
然后我们来结合我们的代码来分析,在循环中我们移除了元素值为 7 的元素 list.remove(next);
在该方法内部调用了 fastRemove
方法
/* |
等到到达下一次 Integer next = iterator.next();
的时候就触发了 ConcurrentModificationException
异常。
以上,对于异常的原因分析就结束了。
改写上面的代码,利用 iterator
来移除元素即可。
public static void main(String[] args) { |
我们来看看 iterator.remove();
发生了什么
public void remove() { |
看到这里你可能会问,如此大费周章的去删除一个元素究竟是为了什么?
通过两种移除元素方法的对比可以发现,使用iterator.remove();
移除元素,仅仅支持移除当前迭代的元素,并且在不进入下一次迭代前iterator.remove();
只能调用一次。而通过 list.remove
的方式可以移除任意的元素。通过使用迭代器移除可以获得一个容器确定的视图。下面我们假装list.remove
不会抛出异常,来举个例子
public static void main(String[] args) { |
我们本来是想拿到一个完整的迭代,但是却缺少了 7 这个元素。这里仅仅是做了打印处理,如果是要利用迭代器计算这个容器所有元素的和,那么这个和必然是不符合预期的。有人会说我自己知道移除了 1 这个元素,并且所以我预期的和就是没有 7 的和。如果真的是这样的话,这样的代码维护起来将是一个噩梦,你要注意在哪里移除了某个元素,以及是否会对后面的逻辑产生影响。当代码逻辑进一步复杂的时候,这样的做法会让代码表现更加的不可预期。
如果是在多线程中使用 ArrayList
,仅针对本文中涉及的元素来看,modCount
的可见性会有问题,每个线程看到的modCount
的大小可能是不一样的,同时modCount++
等改变值得操作也会没有同步性措施而变得失去原子性。
Vector
作为同步版本的 List
的做法是在有关 modCount
操作的地方使用 synchronized
来保证可见性和同步。
由于 list.iterator();
每次都会生成新的迭代器,所以 cursor
和 lastRet
变量是线程封闭的,无需同步。
首先我们来写一个最简单的单例,该单例只能使用在单线程的情况下。
public class UnsafeLazyInitialization { |
我们来分析一下这个单例如果放到多线程模式下会有什么问题,如果线程 A 和 B 同时到达 if (resource == null)
的时候(或者是由于可见性的原因,线程 B 没有看到线程 A 已经对引用赋值了),此时判断逻辑都为 true
于是就会创建 Resource
对象,两个线程创建了两个对象(如果有多个线程,可能会创建更多的对象),不符合单例的要求。相对这个问题而言,这里还隐藏了更危险的问题,由于指令的重排序的原因,可能导致一些线程能够获取这个单例,但是单例对象还在初始化,内部处于一个不确定的状态。
下面我们改进一下这个单例,让其变为一个线程安全的单例。
public class EagerInitialization { // 饿汉模式 |
上面的代码如何做到的线程安全,是由于利用了静态初始化器。
静态初始化器是由 JVM 再类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于 JVM 将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个累已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。
上面的模式展示了如何使用 Java 本身类加载的机制来保证单例的,使用同样的方式也可以构建一种懒汉模式的单例。
public class ResourceFactory { |
在江湖上还传说着一种既高效又线程安全的单例写法:
// 不要这么做 |
上面的 DCL 真正的问题在于在没有同步的情况下读取一个共享对象。可见性问题会造成有些线程可能看到失效的值,这个值可能是一个 null
,更加糟糕的情况下,这个值可能是一个不确定的值,因为该对象还没有初始化完成就发布了。
上面的例子可以改写成以下代码,通过使用 volatile
来保证共享对象的可见性
public class DoubleCheckedLocking { |
然而,DCL 的这种使用方法已经被广泛地废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及 JVM 启动很慢)已经不复存在了,因此它不是一种高效的优化措施。延迟初始化能带来同样的优势,并且更容易理解。
public interface MySingleton { |
effective java 推荐使用枚举来做单例
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
使用枚举来做单例的优势是:写法简单,单例线程安全由 JMM 提供、反序列化得到的对象也能保证是同一个。
单例线程安全:如果反编译单例的代码之后,会发现是一些 static 的属性和代码块。
反序列化的保障:
Java的序列化机制针对枚举类型是特殊处理的。简单来讲,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象。
推荐使用枚举去实现单例模式!
]]>注意:单条 SQL 也是一个事务,也会和其他事务发生死锁。
首先奉上 InnoDB 日志 和 DDL
CREATE TABLE `campaignmockqueue` ( |
------------------------ |
如果看不懂日志,可以参考 这个文章
我们先来看看 事务1 和事务2 分别持有什么锁,又在等待什么锁。由于日志是在事务 1 的角度来打印的,所以我们只能看到事务 2 持有 lock_mode X locks rec but not gap
锁,在等待 lock_mode X locks gap before rec insert intention
锁。
lock_mode X locks rec but not gap
就是写记录锁,只锁了单条记录。
lock_mode X locks gap before rec insert intention
就是一个插入意向锁,目标是在对应的间隙上(不包括记录本身)加锁。
通过事务 2 的锁信息我们可以推测出事务 1 的锁持有信息,因此就有了下面的图。
事务 1 当前拥有的应该是间隙 A 和 记录X 组成的 nexy-key 锁 ,现在正在等待的是间隙 b 的锁。
DELETE FROM `campaignmockqueue` WHERE `campaignid`=52327710 and `addtime` <= 1557992297 |
事务 2 当前拥有的应该是 记录 Y 的 X锁, 间隙 D 的插入意向锁, 间隙 C 的插入意向锁 ,现在正在等待的是间隙 e 的 间隙插入意向锁(也有可能是 记录 Y 的 record lock)。
INSERT INTO `campaignmockqueue`(`campaignid`, `addtime` )VALUES (52327709, 1557992297) , (52327709, 1559383140), (52327709, 1557992296) |
由于 next-key 锁 和 插入意向锁互斥,所以事务 1 在等待事务 2 释放 C, Y, D;事务 2 在等待事务 1 释放 A。 这样看来正好符合 InnoDB 中的 log。
个人认为 由于 next-key 锁和 Gap 锁不是一种锁,因此必然存在时间差,这种时间差在并发量很大的情况下才会凸显出来。
事务1 | 事务2 |
---|---|
INSERT INTO campaignmockqueue 记录 Y | |
DELETE FROM campaignmockqueue 获取了 A 的间隙锁还没获取 next-key 锁 | |
INSERT INTO campaignmockqueue (campaignid , addtime )VALUES (52327709, 1557992297) , (52327709, 1559383140), (52327709, 1557992296) (阻塞) | |
DELETE FROM campaignmockqueue 获取 next-key 锁 (阻塞) |
上面的情况仅仅是推测,如果我们能拿到 INSERT 语句角度的死锁日志就好了。搜寻了一下 InnoDB 日志,得到了下面的日志,通过交叉分析,可以验证了我们的猜想。
------------------------ |
DELETE FROM campaignmockqueue
不使用 idx_campid
这个索引加锁,而使用唯一主键来做操作,和 insert 操作使用不同的索引,来避免这个问题。
本文在 MacOS 下可以通过所有步骤,其他平台需要自行修改相关步骤
推荐使用 Homebrew 安装
brew install aria2 |
将下面的内容复制到 /Users/用户名/.aria2/aria2.conf
里面,用户名是你 Mac 的用户名。
同时建立session文件touch ~/.aria2/aria2.session
文中注释了一些选项含义,所以的含义需要看这个网址获取 aria2c(1) — aria2 1.34.0 documentation
不建议修改下面的除了 用户文件夹之外的内容,这些内容都是经过实践得出的比较优秀的配置,下载速度较快
需要修改下面的用户名 jacob 改为你自己的用户名方可使用。
## '#'开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ## |
Aria2 启动可能会有问题,我们需要调试成功之后才然后设置开机启动,否则无法查看错误信息,
/usr/local/bin/aria2c --conf-path=/Users/jacob/.aria2/aria2.conf |
通过上面的命令可以看到提示,如果没有错误就可以进入下一步了。如果有错误根据错误提示解决问题
aira2 每次都要自己启动,十分麻烦,但是 aria2 资源占用很低,适合在后台常驻,所以可以设置为开机启动。
cd ~/Library/LaunchAgents |
将下面的内容放入 com.aria2c.plist
|
加载配置
launchctl load ~/Library/LaunchAgents/com.aria2c.plist |
此时 通过 ps aux | grep "aria2"
可以看到 aria2 已经启动,如果没有启动可以注销Mac用户后登陆即可看到 aria2 在运行中。以后 aira2 就可以开机启动了。
Aria2 有很多 GUI,这个推荐一个 Chrome 插件用作 GUI, Chrome 的功能我们可以让他看起来像一个独立的应用。
安装 Aria2 for Chrome - Chrome 网上应用店
右键插件图标进入选项,设置 rpc 网址为 http://token:maria.rpc.2018@localhost:6800/jsonrpc
然后单击插件图标即可看到提示 Aria2 已连接的提示
如果这里无法连接,需要在 GUI 内部设置token等信息
下面我们可以让这个插件变成一个应用,如果你不感兴趣可以直接看下一节。
右键点击 插件 图标,点击 打开AriaNG 文字,进入页面
然后将这个页面导出为应用
此时在mac的应用里就会有一个 AriaNG 应用了,图标可以自行定制,如何修改图标自己搜索下吧
效果如下
普通下载不必多讲,按照插件和 GUI 的页面可以很容易的进行下载,这里说说如何下载 磁力链接。Aria2 磁力链接的速度会很慢,甚至可能无法下载,此时我们可以通过修改 配置中的 bt-tracker 选项来获得高速下载,速度不输迅雷。
可以通过复制 https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt 的内容然后修改一下格式,配置给 Aria2,因为 trackers 一直在变化我也写了一个python3 的脚本来辅助更新。内容如下
import urllib.request |
通过运行上面的脚本可以一键更新 bt-tracker 内容,注意配置修改之后必须要重启 Aria2 才可以生效。可以通过 kill 进程来实现,我们的开机启动配置会自动重启一个 Aria 进程。
先看看效果:
上面的插件是没有实现下载完成后通知的,这样其实有些尴尬,每次都要去看看写没写完,可以通过 Aria2 自带的 钩子函数来实现。对应在 conf 里的配置是这些
## 一些脚本 |
这些脚本的内容如下
/Users/jacob/.aria2/aria2_on_bt_download_complete.sh
/usr/local/bin/terminal-notifier -title "Aria2" -subtitle "BT下载完成" -message "" -activate com.google.Chrome.app.Default-hpjnfnejkleckpmhmjppopcbnedoocmg -appIcon /Users/jacob/Applications/Chrome\ Apps.localized/Default\ hpjnfnejkleckpmhmjppopcbnedoocmg.app/Contents/Resources/app.icns |
/Users/jacob/.aria2/aria2_on_download_complete.sh
/usr/local/bin/terminal-notifier -title "Aria2" -subtitle "下载完成" -message $msg -execute "/usr/bin/open $3" -appIcon /Users/jacob/Applications/Chrome\ Apps.localized/Default\ hpjnfnejkleckpmhmjppopcbnedoocmg.app/Contents/Resources/app.icns |
/Users/jacob/.aria2/aria2_on_download_error.sh
var=$3 |
/Users/jacob/.aria2/aria2_on_download_pause.sh
var=$3 |
/Users/jacob/.aria2/aria2_on_download_start.sh
var=$3 |
使用这些脚本需要安装软件 julienXX/terminal-notifier: Send User Notifications on macOS from the command-line.
安装方式 brew install terminal-notifier
appIcon 就是通知时候的图标,可以自定义。这样就实现了下载完成后通知,并且点击通知就能够打开下载的文件。terminal-notifier 一定要使用全路径名,否则无法生效。
安装 Chrome 插件acgotaku/BaiduExporter: Assistant for Baidu to export download links to aria2/aria2-rpc
安装方式看 GitHub 的 readme, 然后配置本地 Aria2
设置在这里:
配置 URL 为 http://token:maria.rpc.2018@localhost:6800/jsonrpc
以后使用 导出下载即可获得满速下载。
]]>public class StaticTest { |
如果你能很简单的得到这段代码的输出结果,也就无需继续看下去了,如果结果和你得出的结果有差异,那不妨看看类加载器是如何来对这些变量做处理的。
答案如下
2 |
看那道题目之前我们先看些简单的吧!
上图是类加载的过程,涉及到类变量和类实例变量的初始化和赋值都在准备和初始化这两个阶段,因此我们只讨论这两部分。
此时是为类变量(静态变量或者静态代码块)分配内存并进行初始化的阶段。这些变量都是分配在方法区的,而不是在堆中。并且这里的初始化指的是数据类型的零值,对于基本数据类型就是0,对于引用类型则是 null
。这里我们举两个例子
public static int value = 123; |
在准备阶段,value
会被初始化为0,person
会被初始化为 null
,将 value 设置为 123 ,是在类加载的初始化阶段中,在<clinit>()
方法中。但是有一种情况除外,如果一个字段是常量,这个字段就会在准备阶段赋值,如下
public static final int x = 999; // x会在准备阶段被赋值为999 |
在准备阶段被初始化为零值的那些变量会在初始化阶段赋值为在代码中定义的值。初始化阶段其实就是执行 <clinit>()
方法。 <clinit>()
方法是做的就是类变量的赋值动作和静态语句块。并且变量的赋值顺序就是在代码源文件中的出现顺序,静态语句块只能访问到定义在它之前的变量,定义在它之后的变量可以赋值,但是不能访问。
public static int value = 123; |
public class Test { |
实例的初始化函数是 <init>()
,它的执行顺序是:
- 父类变量初始化 和 父类语句块 (顺序是源码顺序)
- 父类构造函数
- 子类变量初始化 和 子类语句块 (顺序是源码顺序)
- 子类构造函数
对应的类初始化函数 <client>()
的执行顺序是:
- 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
- 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)
综合上面两个函数可以得到一个包含所有步骤的顺序
- 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
- 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)
- 父类变量初始化 和 父类语句块 (顺序是源码顺序)
- 父类构造函数
- 子类变量初始化 和 子类语句块 (顺序是源码顺序)
- 子类构造函数
注意这里说的是开始顺序,并不是结束顺序,实例的初始化可以在类的初始化之前完成,也就是说,<clinit>()
可能发生在 <init>()
之后,文章开头的例子就是这样。
在本文的例子中,当准备阶段完成后,类变量会被赋值为以下值:
public class StaticTest { |
完成准备阶段后,来到了初始化阶段 <clinit>()
的前半段,会在 <clinit>()
结束前,执行 <init>()
public class StaticTest { |
然后是 <clinit>()
的后半段
public class StaticTest { |
至此,对于变量的初始化应该就在比较明确了。
重点还是记住下面的顺序,同时对 类加载过程有一定的了解。
]]>
- 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
- 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)
- 父类变量初始化 和 父类语句块 (顺序是源码顺序)
- 父类构造函数
- 子类变量初始化 和 子类语句块 (顺序是源码顺序)
- 子类构造函数
|
我们只需要在对应的方法上使用 @Transactional
注解即可让这个方法在事务中执行。这里需要注意的是,如果是在一个类中的两个方法,事务是不会生效的。举例:
import org.springframework.transaction.annotation.Propagation; |
为何不会生效?是因为这样的调用不会经过 Spring 的代理,无法通过 Spring 的 advisor 来拦截数据库操作请求。
首先我们来了解一下 Spring Bean 的生命周期
- 调用InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation(Class<?> beanClass, String beanName)
- bean实例化
- 调用InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation(Object bean, String beanName)
- 调用InstantiationAwareBeanPostProcessor的postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName)
- bean注入properties
- 分别调用BeanNameAware,BeanClassLoaderAware,BeanFactoryAware中的方法
- 调用BeanPostProcessor的postProcessBeforeInitialization(Object bean, String beanName)
- 调用InitializingBean的afterPropertiesSet方法
- 调用自定义初始化方法
- 调用BeanPostProcessor的postProcessAfterInitialization(Object bean, String beanName)
- 调用DisposableBean的destroy()方法
- 调用自定义销毁方法
作者:土豆肉丝盖浇饭
链接:https://www.jianshu.com/p/6d5c58168493
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
通过上面的流程我们可以看到,BeanPostProcessor 中的两个方法 postProcessBeforeInitialization
和postProcessAfterInitialization
分别在 bean 调用 init 方法前后调用。其中对于对象的代理就是在 postProcessAfterInitialization
方法中完成的,用代理的 bean 来替换原来的 bean
默认情况下,BeanPostProcessor
的职能是通过默认实现类 DefaultAdvisorAutoProxyCreator
实现的,类 DefaultAdvisorAutoProxyCreator
继承自AbstractAdvisorAutoProxyCreator
该类的继承关系如下图
DefaultAdvisorAutoProxyCreator
如何代理被@Transactional
注解的方法所属类来看看 AbstractAutoProxyCreator
中发生了什么
/** |
跟进 wrapIfNecessary
方法
/** |
// AbstractAdvisorAutoProxyCreator 类 |
/** |
我们来看看生成的动态类是什么样子的?
其中有一个 advisor 为
adviceBeanName:org.springframework.transaction.interceptor.TransactionInterceptor#0
我们此时来看看 org.springframework.transaction.interceptor.TransactionInterceptor
里究竟是如何执行 SQL 语句的。我们需要关注的方法为 invoke
方法
|
查看 invokeWithinTransaction
方法
// TransactionAspectSupport.java |
至此,我们基本了解了 Spring 声明式事务的工作流程
]]>package web; |
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现。幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
有些操作是天然幂等的,譬如查询操作、删除操作。其他一些插入和更新操作有时候需要引入其他的机制来保证幂等,比如乐观锁、悲观锁等。这里介绍两种常用的来保证幂等的方案:利用数据库唯一索引和通过token机制来实现幂等。
利用数据库的一些特性来实现幂等设计,可以将一些复杂的加锁解锁操作转移到数据库来处理。这样可以大大减少编码的成本。
这里用来创建唯一索引的数据必须保证是唯一的,不可出现业务上允许的重复。
token机制防止页面重复提交。由于网络或者重复点击的问题,可能会有重复的请求发送到服务器。利用token方式可以防止重复提交。
这种方式适合web平台的管理页面,在进入页面后便拿到token,点击提交之后携带改token来请求逻辑处理,如果该token不存在,拒绝处理,如果已经处理完毕,返回处理结果。
以下表为例子
没有提交的事务被其他事务读取到了,这叫做脏读。
事务开始之前两个人各有1000元
事务一执行了转账操作,事务二统计两个人一共有多少钱。事务2的到的结果明显是错误的。
同一个事务对同一条记录读取两遍,两次读出来的结果不一样,称为不可重复读
事务2中两次执行同一条SQL得到的结果却不同。不可重复读与脏读的区别在于,不可重复读读到的是其他事务已经提交的修改,而脏读是读到了其他事务还未提交的修改。
同样的条件,第一次和第二次读出来的记录数不一样,称为幻读
同样的条件,第一次和第二次读出来的记录集合不一样。不可重复读是因为其他事务进行了 UPDATE 操作,幻读是因为其他事务进行了 INSERT 或者 DELETE 操作。
事务 2 的两次查询,第一次查出 2 条记录,第二次却查出 3 条记录,多出来的这条记录,正如 phantom(幽灵,幻影,错觉) 的意思,就像幽灵一样。
两个事务都是写操作,某种情况下有些修改被提交后又被覆盖了,称为丢失更新
上图中事务2的提交覆盖了事务1的提交。
上图中事务1的回滚直接忽视了事务2的 UPDATE 操作
为了有效的保证数据一致性和数据库并发性能,便有了四种不同的数据库隔离级别
针对这四种隔离级别,应该根据具体的业务来取舍,如果某个系统的业务里根本就不会出现重复读的场景,完全可以将数据库的隔离级别设置为 RC,这样可以最大程度的提高数据库的并发性。不同的隔离级别和可能发生的并发现象如下表:
RR级别下会出现提交覆盖吗?答案文末揭晓。
上面所说的都是事务和隔离级别的概念,是 SQL 标准中通用的概念,不同的数据库产品有不同的实现。
传统的隔离级别是基于锁实现的,这种方式叫做 基于锁的并发控制(Lock-Based Concurrent Control,简写 LBCC)
通过对读写操作加不同的锁,以及对释放锁的时机进行不同的控制,就可以实现四种隔离级别。传统的锁有两种:读操作通常加共享锁(Share locks,S锁,又叫读锁),写操作加排它锁(Exclusive locks,X锁,又叫写锁);加了共享锁的记录,其他事务也可以读,但不能写;加了排它锁的记录,其他事务既不能读,也不能写。另外,对于锁的粒度,又分为行锁和表锁,行锁只锁某行记录,对其他行的操作不受影响,表锁会锁住整张表,所有对这个表的操作都受影响。
归纳起来,四种隔离级别的加锁策略如下:
通过对锁的类型(读锁还是写锁),锁的粒度(行锁还是表锁),持有锁的时间(临时锁还是持续锁)合理的进行组合,就可以实现四种不同的隔离级别。这四种不同的加锁策略实际上又称为 封锁协议(Locking Protocol),所谓协议,就是说不论加锁还是释放锁都得按照特定的规则来。读未提交 的加锁策略又称为 一级封锁协议,后面的分别是二级,三级,序列化 的加锁策略又称为 四级封锁协议。
其中三级封锁协议在事务的过程中为写操作加持续 X 锁,为读操作加持续 S 锁,并且在事务结束时才对锁进行释放,像这种加锁和解锁明确的分成两个阶段我们把它称作 两段锁协议(2-phase locking,简称 2PL)。在两段锁协议中规定,加锁阶段只允许加锁,不允许解锁;而解锁阶段只允许解锁,不允许加锁。这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化的(关于串行化是一个非常重要的概念,尤其是在数据恢复和备份的时候)。在两段锁协议中,还有一种特殊的形式,叫 一次封锁,意思是指在事务开始的时候,将事务可能遇到的数据全部一次锁住,再在事务结束时全部一次释放,这种方式可以有效的避免死锁发生。
虽然数据库的四种隔离级别通过 LBCC 技术都可以实现,但是它最大的问题是它只实现了并发的读读,对于并发的读写还是冲突的,写时不能读,读时不能写,当读写操作都很频繁时,数据库的并发性将大大降低,针对这种场景,MVCC 技术应运而生。MVCC 的全称叫做 Multi-Version Concurrent Control(多版本并发控制)。
InnoDb 会为每一行记录增加几个隐含的“辅助字段”,(实际上是 3 个字段:一个隐式的 ID 字段,一个事务 ID,还有一个回滚指针),事务在写一条记录时会将其拷贝一份生成这条记录的一个原始拷贝,写操作同样还是会对原记录加锁,但是读操作会读取未加锁的新记录,这就保证了读写并行。要注意的是,生成的新版本其实就是 undo log,它也是实现事务回滚的关键技术。
在 read uncommit 隔离级别下,每次都是读取最新版本的数据行,所以不能用 MVCC 的多版本,而 serializable 隔离级别每次读取操作都会为记录加上读锁,也和 MVCC 不兼容,所以只有 RC 和 RR 这两个隔离级别才有 MVCC。
RR 和 RC 隔离级别都实现了 MVCC 来满足读写并行,但是读的实现方式是不一样的:RC 总是读取记录的最新版本,如果该记录被锁住,则读取该记录最新的一次快照,而 RR 是读取该记录事务开始时的那个版本。虽然这两种读取方式不一样,但是它们读取的都是快照数据,并不会被写操作阻塞,所以这种读操作称为 快照读(Snapshot Read),有时候也叫做 非阻塞读(Nonlocking Read)
除了 快照读 ,MySQL 还提供了另一种读取方式:当前读(Current Read),有时候又叫做 加锁读(Locking Read) 或者 阻塞读(Blocking Read),这种读操作读的不再是数据的快照版本,而是数据的最新版本,并会对数据加锁,根据加锁的不同,又分成两类:
当前读在 RR 和 RC 两种隔离级别下的实现也是不一样的:RC 只加记录锁,RR 除了加记录锁,还会加间隙锁,用于解决幻读问题。
不同隔离级别下InnoDB的读操作
MySQL 的实现和 ANSI-SQL 标准之间的差异,在标准的传统实现中,RR 隔离级别是使用持续的 X 锁和持续的 S 锁来实现的(参看下面的 “隔离级别的实现” 一节),由于是持续的 S 锁,所以避免了其他事务有写操作,也就不存在提交覆盖问题。但是 MySQL 在 RR 隔离级别下,普通的 SELECT 语句只是快照读,没有任何的加锁,和标准的 RR 是不一样的。如果要让 MySQL 在 RR 隔离级别下不发生提交覆盖,可以使用 SELECT … LOCK IN SHARE MODE 或者 SELECT … FOR UPDATE 。
在 MySQL 中锁的种类有很多,但是最基本的还是表锁和行锁:表锁指的是对一整张表加锁,一般是 DDL 处理时使用,也可以自己在 SQL 中指定;而行锁指的是锁定某一行数据或某几行,或行和行之间的间隙。行锁的加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,通常都是用行锁来处理并发事务。表锁由 MySQL 服务器实现,行锁由存储引擎实现,常见的就是 InnoDb,所以通常我们在讨论行锁时,隐含的一层意义就是数据库的存储引擎为 InnoDb ,而 MyISAM 存储引擎只能使用表锁。
表锁由 MySQL 服务器实现,所以无论你的存储引擎是什么,都可以使用。一般在执行 DDL 语句时,譬如 ALTER TABLE 就会对整个表进行加锁。在执行 SQL 语句时,也可以明确对某个表加锁,譬如下面的例子 lock table products read;
Query OK, 0 rows affected (0.00 sec)
select * from products where id = 100;
unlock tables;
Query OK, 0 rows affected (0.00 sec)
上面的 SQL 首先对 products 表加一个表锁,然后执行查询语句,最后释放表锁。
表锁可以细分成两种:读锁和写锁,如果是加写锁,则是 lock table products write
。
关于表锁,我们要了解它的加锁和解锁原则,要注意的是它使用的是 一次封锁 技术,也就是说,我们会在会话开始的地方使用 lock 命令将后面所有要用到的表加上锁,在锁释放之前,我们只能访问这些加锁的表,不能访问其他的表,最后通过 unlock tables 释放所有表锁。这样的好处是,不会发生死锁!所以我们在 MyISAM 存储引擎中,是不可能看到死锁场景的。对多个表加锁的例子如下:
lock table products read, orders read; |
可以看到由于没有对 users 表加锁,在持有表锁的情况下是不能读取的,另外,由于加的是读锁,所以后面也不能对 orders 表进行更新。MySQL 表锁的加锁规则如下:
锁的释放规则如下:
表锁不仅实现和使用都很简单,而且占用的系统资源少,所以在很多存储引擎中使用,如 MyISAM、MEMORY、MERGE 等,MyISAM 存储引擎几乎完全依赖 MySQL 服务器提供的表锁机制,查询自动加表级读锁,更新自动加表级写锁,以此来解决可能的并发问题。但是表锁的粒度太粗,导致数据库的并发性能降低,为了提高数据库的并发能力,InnoDb 引入了行锁的概念。行锁和表锁对比如下:
行锁和表锁一样,也分成两种类型:读锁和写锁。常见的增删改(INSERT、DELETE、UPDATE)语句会自动对操作的数据行加写锁,查询的时候也可以明确指定锁的类型,SELECT … LOCK IN SHARE MODE 语句加的是读锁,SELECT … FOR UPDATE 语句加的是写锁。
行锁这个名字听起来像是这个锁加在某个数据行上,实际上这里要指出的是:在 MySQL 中,行锁是加在索引上的。
当执行下面的 SQL 时(id 为 students 表的主键),我们要知道,InnoDb 存储引擎会在 id = 49 这个主键索引上加一把 X 锁。 update students set score = 100 where id = 49;
当执行下面的 SQL 时(name 为 students 表的二级索引),InnoDb 存储引擎会在 name = ‘Tom’ 这个索引上加一把 X 锁,同时会通过 name = ‘Tom’ 这个二级索引定位到 id = 49 这个主键索引,并在 id = 49 这个主键索引上加一把 X 锁。 update students set score = 100 where name = 'Tom';
加锁过程如下图所示:
多条记录的加锁流程: update students set level = 3 where score >= 60;
下图展示了当用户执行这条 SQL 时,MySQL Server 和 InnoDb 之间的执行流程:
MySQL 在操作多条记录时 InnoDB 与 MySQL Server 的交互是一条一条进行的,加锁也是一条一条依次进行的,先对一条满足条件的记录加锁,返回给 MySQL Server,做一些 DML 操作,然后在读取下一条加锁,直至读取完毕。
根据锁的粒度可以把锁细分为表锁和行锁,行锁根据场景的不同又可以进一步细分,在 MySQL 的源码里,定义了四种类型的行锁,如下:
/* Precise modes */
MySQL 将锁分成两类:锁类型(lock_type)和锁模式(lock_mode)。锁类型就是上文中介绍的表锁和行锁两种类型,当然行锁还可以细分成记录锁和间隙锁等更细的类型,锁类型描述的锁的粒度,也可以说是把锁具体加在什么地方;而锁模式描述的是到底加的是什么锁,譬如读锁或写锁。锁模式通常是和锁类型结合使用的,锁模式在 MySQL 的源码中定义如下:/* Basic lock modes */
enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table in an exclusive mode*/
...
};
将锁分为读锁和写锁主要是为了提高读的并发,如果不区分读写锁,那么数据库将没办法并发读,并发性将大大降低。而 IS(读意向)、IX(写意向)只会应用在表锁上,方便表锁和行锁之间的冲突检测。LOCK_AUTO_INC 是一种特殊的表锁。下面依次进行介绍。
读锁和写锁都是最基本的锁模式,它们的概念也比较容易理解。读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,所有的事务都可以读取,但是不能修改,并且可同时有多个事务对记录加读锁。写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁之后,只有拥有该锁的事务可以读取和修改,其他事务都不可以读取和修改,并且同一时间只能有一个事务加写锁。(注意:这里说的读都是当前读,快照读是无需加锁的,记录上无论有没有锁,都可以快照读)
表锁锁定了整张表,而行锁是锁定表中的某条记录,它们俩锁定的范围有交集,因此表锁和行锁之间是有冲突的。譬如某个表有 10000 条记录,其中有一条记录加了 X 锁,如果这个时候系统需要对该表加表锁,为了判断是否能加这个表锁,系统需要遍历表中的所有 10000 条记录,看看是不是某条记录被加锁,如果有锁,则不允许加表锁,显然这是很低效的一种方法,为了方便检测表锁和行锁的冲突,从而引入了意向锁。
意向锁为表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加上读锁或写锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
下面是各个表锁之间的兼容矩阵:
AUTO_INC 锁又叫自增锁(一般简写成 AI 锁),它是一种特殊类型的表锁,当插入的表中有自增列(AUTO_INCREMENT)的时候可能会遇到。当插入表中有自增列时,数据库需要自动生成自增值,在生成之前,它会先为该表加 AUTO_INC 表锁,其他事务的插入操作阻塞,这样保证生成的自增值肯定是唯一的。AUTO_INC 锁具有如下特点:
显然,AUTO_INC 表锁会导致并发插入的效率降低,为了提高插入的并发性,MySQL 从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替 AUTO_INC 锁,我们可以通过参数 innodb_autoinc_lock_mode 控制分配自增值时的并发策略。参数 innodb_autoinc_lock_mode 可以取下列值:
innodb_autoinc_lock_mode = 0 (traditional lock mode)
innodb_autoinc_lock_mode = 1 (consecutive lock mode)
Simple inserts
、Bulk inserts
、Mixed-mode inserts
。通过分析 INSERT 语句可以明确知道插入数量的叫做 Simple inserts
,譬如最经常使用的 INSERT INTO table VALUE(1,2) 或 INSERT INTO table VALUES(1,2), (3,4);通过分析 INSERT 语句无法确定插入数量的叫做 Bulk inserts,譬如 INSERT INTO table SELECT 或 LOAD DATA 等;还有一种是不确定是否需要分配自增值的,譬如 INSERT INTO table VALUES(1,’a’), (NULL,’b’), (5, ‘C’), (NULL, ‘d’) 或 INSERT … ON DUPLICATE KEY UPDATE,这种叫做 Mixed-mode inserts
。innodb_autoinc_lock_mode = 2 (interleaved lock mode)
前面在讲行锁时有提到,在 MySQL 的源码中定义了四种类型的行锁,我们这一节将学习这四种锁。
记录锁 是最简单的行锁。 UPDATE accounts SET level = 100 WHERE id = 5;
这条 SQL 语句就会在 id = 5 这条记录上加上记录锁,防止其他事务对 id = 5 这条记录进行修改或删除。记录锁永远都是加在索引上的,就算一个表没有建索引,数据库也会隐式的创建一个索引。如果 WHERE 条件中指定的列是个二级索引,那么记录锁不仅会加在这个二级索引上,还会加在这个二级索引所对应的聚簇索引上(参考上面的加锁流程一节)。
注意,如果 SQL 语句无法使用索引时会走主索引实现全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁。如果一个 WHERE 条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由 MySQL Server 层进行过滤。不过在实际使用过程中,MySQL 做了一些改进,在 MySQL Server 层进行过滤的时候,如果发现不满足,会调用 unlock_row 方法,把不满足条件的记录释放锁(显然这违背了两段锁协议)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见在没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,而且极大的降低了数据库的并发性能,所以说,更新操作一定要记得走索引。
还是看上面的那个例子,如果 id = 5 这条记录不存在,这个 SQL 语句还会加锁吗?
这个 SQL 语句在 RC 隔离级别不会加任何锁,在 RR 隔离级别为了避免幻读会在 id = 5 前后两个索引之间加上间隙锁。
间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入。
Next-key 锁 是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含10、11、13 和 20 这几个值,可能的 Next-key 锁如下:
(-∞, 10] |
通常我们都用这种左开右闭区间来表示 Next-key 锁,前面四个都是 Next-key 锁,最后一个为间隙锁。
和间隙锁一样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。继续拿上面的 SQL 例子来说,如果 id 不是主键,而是二级索引,且不是唯一索引,那么这个 SQL 在 RR 隔离级别下会加什么锁呢?答案就是 Next-key 锁,如下:(a, 5]
(5, b)
其中,a 和 b 是 id = 5 前后两个索引,我们假设 a = 1、b = 10,那么此时如果插入一条 id = 3 的记录将会阻塞住。之所以要把 id = 5 前后的间隙都锁住,仍然是为了解决幻读问题,因为 id 是非唯一索引,所以 id = 5 可能会有多条记录,为了防止再插入一条 id = 5 的记录,必须将下面标记 ^
的位置都锁住,因为这些位置都可能再插入一条 id = 5 的记录:1 ^ 5 ^ 5 ^ 5 ^ 10 11 13 15
可以看出来,Next-key 锁确实可以避免幻读,但是带来的副作用是连插入 id = 3 这样的记录也被阻塞了。
关于 Next-key 锁,有一个比较有意思的问题,比如下面这个 orders 表(id 为主键,order_id 为二级非唯一索引):
+-----+----------+ |
这个时候不仅 order_id = 5 这条记录会加上 X 记录锁,而且这条记录前后的间隙也会加上锁,加锁位置如下:1 2 ^ 5 ^ 5 ^ 9
可以看到 (2, 9) 这个区间都被锁住了,这个时候如果插入 order_id = 4 或者 order_id = 8 这样的记录肯定会被阻塞,这没什么问题,那么现在问题来了,如果插入一条记录 order_id = 2 或者 order_id = 9 会被阻塞吗?答案是可能阻塞,也可能不阻塞,这取决于插入记录主键的值,感兴趣的可以参考这篇博客。
插入意向锁 是一种特殊的间隙锁(所以有的地方把它简写成 II GAP),这个锁表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,但是和上面介绍的表级意向锁是两个完全不同的概念,不要搞混淆了。插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中,id = 1 和 id = 5 之间如果有两个事务要同时分别插入 id = 2 和 id = 3 是没问题的,虽然两个事务都会在 id = 1 和 id = 5 之间加上插入意向锁,但是不会冲突。
插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,那么间隙锁是如何防止幻读的呢?正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
下面我们对这四种行锁做一个总结,它们之间的兼容矩阵如下图所示:
其中,行表示已有的锁,列表示要加的锁。
Shallow Size and Retained Size 的含义都是指的实例对象,不是类本身。
下面将用 sampleClass 表示类Sample的一个实例(instance)
Shallow Size 就是对象本身所占用的大小,不包括其引用的对象。
举个例子:
public class SampleClass { |
可达对象就是说,从 GC roots 开始搜索,能够到达的对象,也就是说下一次GC不会被清理的对象,采用下面的计算方法来得到 Retained Size 对象
Retained Size的含义相对于Shallow Size 不太好理解。依然是上面的例子,在不同的情况下,sampleClass 的Retained Size 并不相同。下面引用StackOverFlow上的一个回答来解释这个问题。
Retained size of an object is its shallow size plus the shallow sizes of the objects that are accessible, directly or indirectly, only from this object. In other words, the retained size represents the amount of memory that will be freed by the garbage collector when this object is collected.
上图应该比较容易理解,看起来每个对象的Retained Size 都符合公式( Retained Size = Shallow Size + 直接子对象的 Retained Size),并没有什么问题,然后我们看另外一种引用关系中,Obj1 的Retained Size 就会发生变化。
第二种图中,你会发现Obj2的Retained Size 不再符合我们刚刚总结出来的公式,这是因为Obj2的直接子对象Obj5还被Obj6所引用,造成的结果就是,如果Obj2被回收,Obj5并不会被回收,所以 Obj2 的Retained Size就不应该包括 Obj5 的Retained Size. 虽然 Obj2 的 Retained Size 发生了变化,但是 Obj1 的 Retained Size 并没有发生变化。
指的是下一次 GC 会被清理的对象,也就是说没有其他对象引用自己的对象。
如果你尝试使用 VisualVM 分析dump文件的时候,你会发现有些对象的 Retained Size 居然是0,这个0就推翻了上面那种分析方法的的结论,觉得无论什么样的对象肯定是会有 Shallow Size 的,不可能为0。
通过分析最后可以这样理解:针对不可达对象,也就是可以完全被清除的对象,Retained Size 都是0
好多人是简单的将保留大小描述为,该对象本身和其直接引用或者间接引用的对象的大小的总和
。这种理解是片面的,就像上面例子中的 Obj2 一样,如果按照这种理解就会得到错误的结论。
原文链接:https://docs.python.org/3.6/howto/curses.html
原文作者: A.M. Kuchling, Eric S. Raymond
版本:Release-2.04
curses 是为文本终端提供一个界面绘图和键盘输入响应的库。这些终端包括 VT100s、Linux终端,和不同程序提供的模拟终端。终端支持使用不同的控制代码来实现相同的操作,类似移动光标、滚动屏幕、擦除屏幕区域等。不同的终端的控制代码大部分都不相同,并且有着自己的特殊操作习惯和技巧。
也许有人会问,在这个图形化显示的时代,为什么还需要这种终端操作库,基于字符的终端显示确实是一个过时的技术,但是在很多有价值的场景下还是能够使用终端显示做出一些十分迷人的产物。其中一个场景就是在并不具备图形显示的便携式和嵌入式 Linux 中,另外还有就是在安装系统或者配置内核的时候,这些操作都不得不在图像界面启动之前操作。
Curses 提供的基本功能,是为程序员提供一个不重复的文本窗口。窗口的内容可以通过不同的方式改变:添加文本、删除文本、修改外观等等。curses 会屏蔽掉底层终端命令的差异性,计算出你需要执行的命令。curses 并不提供类似按钮、复选框,或者对话这种用户界面。如果你需要这些元素可以使用一些类似 Urwid 的用户界面库。
Curses 一开始是为了 BSD Unix写的;后来的 AT&T 的 System V 版本对原有功能做了增强,同时添加了许多新的功能。 BSD curses 就不再维护了,而是被 ncurses 替代了。ncurses 是一个 AT&T 接口的一个开源实现。如果你正在使用一个开源的 Unix,类似Linux或者FreeBSD,你的系统应该已经包含了ncurses。因为现在大多数的商业 Unix 版本都是基于 System V 的代码开发的,这里描述的功能理论上也会存在于上述 Unix 版本中。尽管如此,一些老版本 unix 对 curses 可能并不会有很好的支持。
Windows 版本的 Python 并不包含 curses 模块。而是有一个类似的替代版本 UniCurses。你也可以尝试使用 Fredrik Lundh 写的the Console module,虽然和 curses 的 API 不一样,但是也可以提供基于光标的输出,并且为鼠标和键盘提供全方位支持。
这个 python 模块是针对 C 语言版本 curses 的简单封装,如果你已经熟悉了 C 语言的 curses 编程,在 Python 中应用这些知识也会变得非常简单。最大的不同就是,由于合并了一些C语言中的不同函数 Python 接口会比 C 语言函数更加简单。比如 addstr()
,mvaddstr()
, 和mvwaddstr()
被合并成了一个函数addstr()
。后面你会看到更多这样的例子。
这篇教程是使用 curses 和 Python 编写终端文本程序的介绍,并不尝试成为一个 curses API 的复杂手册。查看 Python curses 手册和 C 语言的 ncurses 手册可以得到更详细的 API 介绍。
在开始之前,Curses 必须先经过初始化。通过调用initscr()
来实现。这个函数会判断终端类型,并发送一些启动需要的指令给终端、创建内部的数据结构。如果执行成功,initscr()
返回一个代表整个屏幕的窗口类;这沿袭 C 语言中的变量名stdscr
.
import Curses |
通常 curses 会关闭屏幕回显,保证只在特定条件下才会读取键盘输入并显示。这需要调用 noecho()
方法。
curses.noecho() |
应用程序通常需要对键盘输入立即做出响应,而不需要特意的按下回车键;这叫做 cbreak 模式,与之对应的是常用的缓冲输入模式。
curses.cbreak() |
终端通常返回特殊键作为多字节转义序列,比如光标键、Home键、Page Up等, curses 可以让你的程序根据转义序列执行相应的代码。让 curses 可以响应这些特殊值,需要开启 keypad 模式。
stdscr.keypad(True) |
结束一个 curses 应用比启动简单多了。只需要执行下面的方法:
curses.nocbreak() |
为了恢复 curses 的终端设置。调用 endwin() 方法重置为原来的操作模式
curses.endwin() |
在你调试你的程序的时候,一个经常出现的问题就是你会把终端搞得一团糟,通常是因为你的代码产生了 bug 并且引发了一个没有捕获的异常。例如:键盘输入不会在回显在屏幕上,这回让终端使用起来很困难。
在 Python 中你可以使用 curses.wrapper()
来避免这种问题,让调试变得简单。
from curses import wrapper |
wrapper()
函数接收一个可调用对象并且执行上文描述的初始化过程。如果支持颜色配置,同时会初始化颜色配置。然后会运行你的代码。一旦代码 return,wrapper()
会重置终端一开始的状态,并且代码会放在try
…except
中执行,如果异常抛出会将终端重置为原始状态然后将异常抛出。因此,在有异常抛出的时候,你的终端不会处在一个可笑的状态,并且能够根据异常信息定位问题。
窗口是 curses 中最基本的元素。一个窗口代表着屏幕中的一块矩形区域,支持展示文本,删除文本,用户输入等等。
initscr()
函数返回的stdscr
对象就是一个覆盖了整个屏幕的窗口对象。对于许多程序来说一个窗口就足够了,但是有时候也需要将屏幕分割为不同的窗口,以便于分别重绘和清除这些窗口。newwin()
函数创建一个给定大小的新窗口,并返回这个窗口类。
begin_x = 20; begin_y = 7 |
注意到 curses 的坐标系统是不同寻常的。坐标通常是以 y,x 的格式,坐标的原点在窗口的左上角。这和通常程序中处理坐标时以 x 开头的方式是不同的。虽然这样令人有些不舒服,但是这是 curses 诞生的时候的设置,现在修改为时已晚。
你可以通过curses.LINES
和curses.COLS
来获取屏幕的大小。从(0,0)
到(curses.LINES - 1, curses.COLS - 1)
就是都是可以使用的坐标。
当你调用函数去展示或者擦除文本,效果并不会立即展现在屏幕上。你必须调用窗口实例的refresh()
方法才能更新屏幕显示。
这是因为 curses 终端连接的带宽比较小,减少屏幕重绘时间变得十分有必要。因此,Curses 会积累修改,当你调用refresh()
方法的时候,以最有效率的方式来重绘窗口。举例说明:如果你在一个窗口添加了一些文本,然后又清除了这个窗口的内容,这样添加文本的操作就变得没有必要了,因为你不会看到被添加的文本。
实际上,显式的通知 curses 来刷新窗口并不会对编程增加很多的复杂性。大部分程序都是在经历一系列的活动之后等待用户的操作,只需要在等待用户输入之前调用stdscr.refresh()
或者refresh()
方法。
Pad 是窗口的特殊形式,它可以比屏幕面积更大,并且可以每次只展示 pad 的一部分。创建 pad 需要制定 pad 的高和宽,刷新需要给定pad的在屏幕上显示部分的坐标。
pad = curses.newpad(100, 100) |
这个refresh()
方法会在屏幕的(5,5)
到(10,75)
的部分显示pad的一部分;显示部分左上角的pad坐标是(0,0)
。除此之外,pad和窗口的使用都是相同的,并且有相同的方法。
如果你需要多个窗口和pad协作的话,有一个更加高效的方式来刷新屏幕并避免每个部分更新时候烦人的闪烁。refresh()
实际上做了两件事:
noutrefresh()
方法更新屏幕显示的底层数据结构,并不会刷新屏幕。doupdate()
方法来将上面的数据结构物理的刷新到屏幕上。所以,你可以在一些窗口调用noutrefresh()
方法更新数据结构,然后调用doupdate()
方法来刷新屏幕。
C 语言程序员也许会觉得 cursers 的方法像迷宫一样错综复杂,方法之间只有着很细微的区别。例如:addstr()
会在当前 stdscr
窗口的光标处展示文字,而mvaddstr()
则是在指定的坐标展示文字。waddstr()
功能和addstr()
类似,但是允许指定窗口而不是默认的stdscr
,mvwaddstr
允许同时指定窗口和坐标。
幸运的是,Python 的接口隐藏了这些细节。stdscr
是一个和其他相同的窗口类,类似addstr()
的方法可以接收不同的参数类型。
通常包含以下四种参数类型。
参数类型 | 描述 |
---|---|
str 或者 ch | 在当前位置展示 str 或者 ch |
str 或者 ch,attr | 在当前位置使用属性 attr 展示str或者ch |
y,x str 或者 ch | 在坐标 y,x 展示 str 或者 ch |
y,x str 或者 ch, attr | 在坐标 y,x 使用attr 属性显示 str 或者 ch |
属性(attributes) 可以突出显示文本,类似粗体、下划线、负片显示,或者为文本着色。这会在下面的部分详细解释。
addstr()
函数在终端显示一个字符串或者字节串。字节串直接发送给终端,字符串通过窗口属性编码为字节发送给终端,默认的系统编码可以通过locale.getpreferredcoding()
获得。
addch()
函数可以接收一个字符,可以是一个长度为1的字符串,或者长度为1的字节串,或者一个整型数。
针对扩展的字符提供了一些常量,这些常量都大于255。例如:ACS_PLMINUS
代表 +/-
符号,ACS_ULCORNER
代表一个 box(处理绘制边框) 的左上角。你还可以使用其他合适 Unicode 字符。
窗口会自动记住上次操作之后光标的位置,如果你不使用坐标,所有的的操作都会开始于上次结束的地方。你也可以通过 move(y,x)
移动光标。因为有些终端光标是默认闪烁的,将光标移动到一些不是那么烦人的位置是很有必要的。
如果你的应用不需要闪烁的光标,你可以调用curs_set(False)
将光标设置为不可见。为了和旧版本的 curses 版本兼容,leaveok(bool)
和curs_set
有着相同的功能。当 bool 是 true 的时候,光标就会变得不可见,你也不必担心光标会在一些奇怪的地方闪烁。
字符可以用不同的方式展示。基于文本的应用程序通常使用负片显示状态。文本编辑器往往需要高亮一些特定的单词。 curses 支持通过属性来为每个字符进行配置以支持上面的描述。
一个属性是一个整数,每一个 bit 都代表着不同的属性。你可以尝试设置不同的bit位来达到不同的效果,不过 curses 不保证所有的组合都是可用的,也不保证不同的组合就一定是不同的显示。这取决于被使用的终端,所以使用大部分终端都会支持的属性是最明智的。列表如下
属性 | 描述 |
---|---|
A_BLINK | 字符闪烁 |
A_BOLD | 高亮或者加粗字符 |
A_DIM | 半高亮字符 |
A_REVERSE | 负片显示字符 |
A_UNDERLINE | 为字符添加下划线 |
所以,想要在屏幕顶端显示负片字符状态,可以使用下面的代码:
stdscr.addstr(0, 0, "Current mode: Typing mode", curses.A_REVERSE) |
curses 同样支持为支持颜色显示的终端文字添加颜色。
如果想使用颜色功能,必须要在执行玩initscr()
之后执行start_color()
函数(curses.wrapper()
会自动执行)来初始化颜色管理,如果终端支持颜色显示,那么调用 has_colors()
函数会返回 TRUE。
curses 维护了有限的颜色搭配,包括前景色(文字颜色)和背景色,可以通过使用color_pair()
函数来设置字体颜色,类似 A_REVERSE, 也是按照bit位的属性设置的,同样的,不保证所有的组合都能够在所有的终端上正确显示。
使用颜色组合1来显示文字的例子:
stdscr.addstr("Pretty text", curses.color_pair(1)) |
像上文说的那样,每个颜色模式分为前景色和背景色。init_pair(n, f, b)
方法会修改颜色模式n的前景色为f,背景色为b。颜色模式0表示黑底白字,不可以修改。
颜色是被编号的,start_color()
方法会初始化八种基本颜色,分别是:0:黑色,1:红色,2:绿色,3:黄色,4:蓝色,5:洋红色,6:青色,7:白色。curses 同样也为这些颜色设置了常量值,curses.COLOR_BLACK, curses.COLOR_RED, curses.COLOR_GREEN, 等等。
来实际应用一下,将1号颜色模式修改为白底红字:
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE) |
当你修改一个颜色模式的时候,所有使用了这个颜色模式的文字都会刷新为新的颜色。
你也可以通过下面的方式来添加文字
stdscr.addstr(0, 0, "RED ALERT!", curs_set.color_pair(1)) |
很多终端支持通过给定的 RGB 值来修改颜色。这可以将颜色模式修改为任何你想要的颜色。不幸的是,Linux 标准终端并不支持,所以我无法演示,或给出例子。你可以通过调用can_change_color()
函数来确定你的终端是否支持这个功能。如果你的终端恰好返回的是 True,表示支持,可以查阅 man 手册来获取更多的信息。
C 语言的 curses 提供一个很简单的输入方式。python 的 curses 增加了一些基本的输入技巧(其他类似 Urwid 的库提供了更多种类的输入技巧)。
在一个窗口上获取输入有两种方式:
getch()
刷新屏幕并等待用户输入一个按键,如果echo()
在这之前被调用,输入的按键会同步显示在屏幕上。你可以指定坐标控制显示的位置。getkey()
和上面的函数做了同样的事情,不同的是将整数转换成了字符串。单字符返回一个单字符串,特殊按键会返回一个长串,类似KEY_UP
或者^G
通过调用nodelay()
函数可以实现不等待用户的输入,如果设置nodelay(True)
,getch()
和 getkey()
函数不会再等待输入。如果没有输入,getch()
会返回 curses.ERR
(值为-1),getkey()
函数则是会抛出一个异常。getch()
还有一个halfdelay()
函数可以设定在指定的时间(单位为十分之一秒)内如果没有得到用户输入才会抛出异常。
getch()
函数返回一个整数。如果在0~255范围内代表的是 ASCII 码,如果大于255,则可能是一些特殊按键类似: Page Up,Home,或者光标按键。你可以通过输入值和一些常量的比较确定输入。常量类似:curses.KEY_PPAGE, curses.KEY_HOME, curses.KEY_LEFT。所以你的代码主循环有可能是这样的:
while True: |
curses.ascii
模块提供了 ASCII 处理函数,参数为一个整数或者单字符。对于书写更加可读的代码很有用。同样提供接收一个整数或者字符的对话函数。例如:curses.ascii.ctrl()
根据参数返回控制字符。
还有一个可以获得整个字符串的函数,就是getstr()
,由于功能限制很少使用。仅有的编辑按键就是 backspace 和 Enter 按键。 getstr()
可以用于获取指定长度的字符串。
curses.echo() # Enable echoing of characters |
curses.textpad
模块提供一个支持类似 Emacs 键盘快捷键的文本框。Textbox
的不同方法支持输入编辑和聚合编辑结果,无论是不是有多余的空格。
下面是例子
import curses |
更多内容参考curses.textpad
的文档。
这篇教程并没有包含一些高级主题,例如读取屏幕的信息,捕获鼠标的动作。但是 Python 的 curses 模块的文档现在已经完成了,下一步你应该阅读他们。
如果你还对 curses 函数的一些行为细节只有怀疑,查询你的 curses 实现的文档吧,无论是 ncurses 或者其他 Unix 实现。手册中会记录各种小技巧,并提供完整的函数列表、属性,还有那些 ACS_* 字符可用。
因为 curses 的 API 非常繁杂,因此有一些函数并没有得到 Python 的支持。这通常不是因为这些函数很难实现,而是因为这些函数已经没人需要了。同样,Python 也不支持 ncurses 的菜单库。欢迎大家是实现这些没有实现的功能,阅读Python Developer’s Guide学习如何为 Python 提交代码。
JDK
自带的命令行工具、和一些可视化工具,如jvisualvm
。 JDK
一些常用的命令行工具,能够让你不安装、不需要在服务器输出文件拷贝到本地的在进行分析,从而能够快速地查看虚拟机参数,了解虚拟机当前的运行状况,十分实用。本问介绍的命令都需要在 JDK8 或以上的环境运行,如果在低于 JDK8 可能会有部分指标乱码。
命令一览
名称 | 主要作用 |
---|---|
jps | JVM Process Status Tool, 显示系统内所有的 HotSpot 进程。 注意:只显示当前用户的 |
jstat | JVM Statistics Monitoring Tool, 用于收集 HotSpot 虚拟机各方面的参数 |
jinfo | Configuration Info for Java, 显示虚拟机配置信息 |
jmap | Memory Map for Java,生成虚拟机的内存转储快照片 |
jhat | JVM Heap Dump Browser,用于分析heapdump文件,他会建立一个 HTTP/HTML 服务器,让用户可以在榴浏览器上查看分析结果 |
jstack | Stack Trace for Java, 显示虚拟机的线程快照 |
全名:Java Virtual Machine Process Status Tool
选项 | 含义 |
---|---|
-q | 忽略主类名称,只输出 LVMID(一般和进程号相同 |
-m | 打印被传入到 main() 方法的参数 |
-l | 打印 main 方法的包名,或者运行的 JAR 包的全路径名 |
-v | 打印传递给 JVM 的参数 |
-V | 打印通过 flags file 传递给 JVM 的参数 |
-Joption | 因为jps也是运行在 JVM 之上的,这就是传递给运行jps的JVM的参数。eg:-J-Xms48m |
全名:Java Virtual Machine statistics monitoring tool
选项 | 作用 |
---|---|
-class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
-gc | 监视Java堆状况,包括Eden区域、两个survivor区、老年代、元空间等的容量、已用空间、GC 时间合计等信息 |
-compiler | 输出 JIT 编译器编译过的方法、耗时等信息 |
-gccapacity | 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间 |
-gcutil | 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间百分比 |
-gccause | 与 -gcutil 功能一样,但是会额外输出导致上一次GC产生的原因 |
-gcnew | 监视新生代 GC 状况 |
-gcnewcapacity | 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间 |
-gcold | 监视老年代 GC 情况 |
-gcoldcapacity | 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 |
-gcpermcapacity | 输出永久代使用到的最大、最小空间(JDK8中已经没有了,并且暂时没有找到查看metaspace的大小的命令,可以直接使用-gc) |
-printcompilation | 输出已经被 JIT 编译过的方法 |
从左到右的含义分别是:
选项 | 描述 |
---|---|
Loaded | 已经加载的类数量 |
Bytes | 已经加载类大小 |
Unloaded | 卸载的类数量 |
Bytes | 卸载的类大小 |
Time | 类加载和卸载操作消耗的时间 |
~ jstat -compiler 50674 |
从左到右的含义分别是:
选项 | 描述 |
---|---|
Compiled | JIT 编译任务的数量 |
Failed | JIT 失败的编译任务 |
Invalid | 无效的 JIT 编译任务 |
Time | 编译需要的时间 |
FailedType | 最后一个编译失败的原因 |
FailedMethod | 最后一个编译失败的类名和方法 |
jacob@JacobMBPlocal~ jstat -gc 50674 |
从左到右的含义分别是:
选项 | 描述 |
---|---|
SOC | S0 区域总大小 |
S1C | S1 区域总大小 |
S0U | S0 区域使用大小 |
S1U | S1 区域使用大小 |
EC | Eden 总大小 |
EU | Eden 区域使用大小 |
OC | 当前 Old Space 总大小 |
OU | Old Space 使用大小 |
PC | 当前 Permanent Space 总大小 |
PU | Permanent Space 使用大小 |
YCG | 新生代 GC 发生次数 |
YGCT | 新生代 GC 时间 |
FGC | full GC 次数q |
FGCT | full GC 时间 |
GCT | 垃圾回收时间总和 |
~ jstat -gccapacity 50674 |
从左到右的含义分别是:
选项 | 描述 |
---|---|
NGCMN | 新生代使用到的最小空间。 Minimum new generation capacity (KB). |
NGCMX | 新生代使用到的最大空间。Maximum new generation capacity (KB). |
NGC | 当前新生代大小。Current new generation capacity (KB). |
S0C | 当前 S0 区域大小。Current survivor space 0 capacity (KB). |
S1C | 当前 S1 区域大小。Current survivor space 1 capacity (KB). |
EC | 当前 Eden 区域大小。Current eden space capacity (KB). |
OGCMN | 老年代使用到的最小空间大小。Minimum old generation capacity (KB). |
OGCMX | 老年代使用到的最大空间大小。Maximum old generation capacity (KB). |
OGC | 当前老年代大小。Current old generation capacity (KB). |
OC | 当前老年代空间大小。Current old space capacity (KB). |
PGCMN | 永久代使用到的最小空间。Minimum permanent generation capacity (KB). |
PGCMX | 永久代使用到的最大空间。Maximum Permanent generation capacity (KB). |
PGC | 当前永久代大小。Current Permanent generation capacity (KB). |
PC | 当前永久空间大小。Current Permanent space capacity (KB). |
VGC | Young GC 次数。 Number of Young generation GC Events. |
FGC | Full GC 次数。Number of Full GC Events. |
如果读者对于
Current old generation capacity
和Current old space capacity
有疑问,可以参考StackOverFlow
上面的这个提问:jstat: difference between OGC & OC, PGC & PC ,简单点描述就是 一个代包含的空间不一定只有一个,只是 HotSpot 恰好只有一个。OGC = sum(all OC)
gcutil
和 gccapacity
展现的数据基本相同,gccapacity
只是一个是精确的数据,gcutil
是更加直观的百分比。
$ jstat -gcutil 17931 |
从左到右的含义分别是:
选项 | 描述 |
---|---|
S0 | S0 区空间占用百分比。Survivor space 0 utilization as a percentage of the space’s current capacity. |
S1 | S1 区空间占用百分比。Survivor space 1 utilization as a percentage of the space’s current capacity. |
E | Eden 区空间占用百分比。Eden space utilization as a percentage of thespace’s current capacity. |
O | 老年代空间占用百分比。Old space utilization as a percentage of the space’s current capacity. |
P | 永久代空间占用百分比。Permanent space utilization as a percentage ofthe space’s current capacity. |
YGC | Young GC 次数。Number of young generation GC events. |
YGCT | Young GC 时间。Young generation garbage collection time. |
FGC | Full GC 次数。Number of Full GC events. |
FGCT | Full GC 时间。Full garbage collection time. |
GCT | 总的 GC 时间。Total garbage collection time. |
gccause
和gccause
基本相同,多出了两个参数,分别表示上一次 GC 的原因,和本次 GC 的原因。
$ jstat -gccause 17931 |
新增的两个参数:
选项 | 描述 |
---|---|
LGCC | 上次 GC 的原因。Cause of last Garbage Collection. |
GCC | 本次 GC 的原因。Cause of current Garbage Collection. |
新生代统计
$ jstat -gcnew 17931 |
选项 | 含义 |
---|---|
SOC | 当前 S0 区域大小。Current survivor space 0 capacity (KB). |
S1C | 当前 S1 区域大小。Current survivor space 1 capacity (KB). |
S0U | S0 占用大小。Survivor space 0 utilization (KB). |
S1U | S1 占用大小。Survivor space 1 utilization (KB). |
TT | Tenuring 阈值。Tenuring threshold. |
MTT | 最大 Tenuring 阈值。Maximum tenuring threshold. |
DSS | 需要的S区域大小。Desired survivor size (KB). |
EC | Current eden space capacity (KB). |
EU | Eden space utilization (KB). |
VGC | Number of young generation GC events. |
VGCT | Young generation garbage collection time. |
Tenuring 阈值是动态变化的,最大 Tenuring 阈值可以通过 JVM 参数设置。具体可以参考我的另外一篇博客 Tenuring Threshold 是动态变化的
DSS
为需要的 S 空间的大小,如果实际空间不足,新生代的对象会提前进入老年代。提前转移实验
$ jstat -gcnewcapacity 17931 |
选项 | 描述 |
---|---|
NGCMN | 新生代最大空间大小。Minimum new generation capacity (KB). |
NGCMX | 新生代最小空间大小。Maximum new generation capacity (KB). |
NGC | 当前新生代空间大小。Current new generation capacity (KB). |
S0CMX | S0 区域最大空间大小。Maximum survivor space 0 capacity (KB). |
S0C | S0 区域当前空间大小。Current survivor space 0 capacity (KB). |
S1CMX | S1 区域最大空间大小。Maximum survivor space 1 capacity (KB). |
S1C | S1 区域当前空间大小。Current survivor space 1 capacity (KB). |
ECMX | Eden 区域最大空间大小。Maximum eden space capacity (KB). |
EC | Eden 区域当前空间大小。Current eden space capacity (KB). |
YGC | Young GC 次数。Number of young generation GC events. |
FGC | Full GC 次数。Number of Full GC Events. |
$ jstat -gcold 17931 |
选项 | 描述 |
---|---|
PC | 当前永久代大小。Current permanent space capacity (KB). |
PU | 当前永久代占用。Permanent space utilization (KB). |
OC | 当前老年代大小。Current old space capacity (KB). |
OU | 当前老年代占用。Old space utilization (KB). |
YGC | Young GC 次数。Number of young generation GC events. |
FGC | Full GC 次数。Number of Full GC events. |
FGCT | Full GC 时间。Full garbage collection time. |
GCT | 所有 GC 时间。Total garbage collection time. |
$ jstat -gcoldcapacity 17931 |
选项 | 描述 |
---|---|
OGCMN | 最小老年代大小。Minimum old generation capacity (KB). |
OGCMV | 最大老年代大小。Maximum old generation capacity (KB). |
OGC | 当前老年代大小。Current old generation capacity (KB). |
OC | 当前老年空间大小。Current old space capacity (KB). |
YGC | Young GC 次数。Number of young generation GC events. |
FGC | Full GC 次数。Number of Full GC events. |
FGCT | Full GC 时间。Full garbage collection time. |
GCT | 所有 GC 时间。Total garbage collection time. |
$ jstat -printcompilation 17931 |
选项 | 描述 |
---|---|
Compiled | Number of compilation tasks performed. |
Size | Number of bytes of bytecode for the method. |
Type | Compilation type. |
Method | Class name and method name identifying the compiled method. Class name uses “/“ instead of “.” as namespace separator. Method name is the method within the given class. The format for these two fields is consistent with the HotSpot -XX:+PrintComplation option. |
上述的命令都可以使用类似下面的参数
jstat -gcutil -t 17931 500 100
其中-t
表示打印时间戳、17931
为 lvmid、500
表示间隔 500ms 来输出信息、 100
表示信息条目显示d的数量。
譬如,下面显示了10行,显示时间间隔为500ms
$ jstat -gcutil 17931 500 10 |
输出 Java 配置信息
下面是 Intellj IDEA 的部分输出信息
jinfo 88747 |
选项 | 描述 |
---|---|
参数为空 | 打印命令行标记和系统属性 |
-flags | 打印命令行标记 |
-sysprops | 打印Java系统属性 |
下面的工具介绍不再提供例子,读者感兴趣可以自己尝试
选 项 | 作 用 |
---|---|
-dump | 生成 Java 堆转储快照。格式为:-dump:[live,]format=b,file=<filename> ,其中 live 子参数说明是否只 dump 出存活的对象 |
-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 想成执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效。在JDK9 Mac 平台失败,JDK8 可以 |
-heap | 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux/Solaris 平台下有效。在JDK9 Mac 平台失败,JDK8可以 |
-histo | 显示堆中对象的统计信息,包括类、实例数量、合计容量 |
permstat | 以ClassLoader为统计口径显示永久代内存状态,只在 Linux/Solaris 平台下有效。 JDK8以上版本已经废弃 |
-F | 当虚拟机进程对 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。只在 Linux/Solaris 平台下有效。 |
虚拟机堆转储快照分析工具,不建议使用, 建议使用 visualVM
等工具进行分析
Java堆栈跟踪工具
“线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。”
摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)”。 iBooks.
选项 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
-m | 如果调用到本地方法的话,可以显示C/C++ 的堆栈.(JDK9失败) |
visualVM 通过可视化的方式来和命令行的命令进行互补,来分析一些内存占用的趋势,一些参数的动态变化比命令行工具有着先天的优势,由于官方文档比较全面,不再赘述。
visualVM 官方提供了中文文档:
]]>Atom
编辑器使用的插件和主题配置,推荐给大家使用,同时也给自己做个备份。 下面是一些废话,可以跳过
说起这个主题,真的是一波三折。Atom 默认的
One Dark
主题用的太久了,有点腻了,然后就寻思着换个主题,看了很多,没有特别有感觉的,直到在某国外网站上看到了这个推荐 10 Best Atom themes of 2017,感觉这个 Native UI 甚是满足我的审美,于是乎赶紧下载了下来,然后发现切换到该主题后 Atom 巨卡无比,本来打算放弃了,可是想了想设置里面应该可以关闭半透明效果应该能够让 Atom 不那么力不从心。机智的我终于看了设置里面的两个选项看了半天,愣是没发现这个User Interface
是半透明效果的开关。甚是蛋疼。
主题推荐:10 Best Atom themes of 2017
我最推荐的主题:Native UI
大概是这个样子:
安装方法:
native-ui
GIF教程: 设置: 设置里面找到 theme ,然后选择 native,再然后点击右侧设置图标,取消勾选 User Interface
用来对代码进行格式化,让杂乱无章的代码瞬间变得有条理,我自己平常用的最多的就是格式化 JSON 字符串了,注意在格式化的时候必须先选定语言。快捷键可以通过 cmd+shift+p
搜索得到。
不太推荐这个插件了,有点丑。
最近发现了一个比较好看的atom-html-preview
,非常奶思。效果如下:
可以在 atom 里面预览HTML,快捷键可以通过 cmd+shift+p
搜索得到。
这个比较好,让atom更加美丽,会在文件前面给一个小图标,针对不同格式的文件有着很好的支持,atom 的文件图标再也不会那么单调了。
不推荐这个插件了,过于臃肿。
这个号称最全markdown插件,可算是集成了所有用得到的和用不到的功能,强大的有些许过分,有时候会造成写markdown的时候卡顿,由于暂时没有找到能够同步滑动的插件,就暂时使用了这个。更多设置可以参考作者的中文文档:https://shd101wyy.github.io/markdown-preview-enhanced/#/zh-cn/
好消息好消息:Markdown Preview Plus (MPP) 也支持同步滑动了。
效果挺好的,简洁好看,推荐使用!
markdown中编辑表格的神器,只需要打一个 |
然后通过 table 和回车键就可以完成表格的编辑,比起自己写效率高到不知道哪里去了。
没啥好说的,让你可以在Atom中直接查看PDF
可以在atom中直接使用终端,是 terminal-plus
的替代品。有些许卡顿,大体可以接受。可以自定义shell。如果配合 iTerm2+Oh my zsh 使用出现乱码,注意在插件设置中设置你在 iTerm 中使用的字体,我的是Meslo LG M DZ for Powerline
这个插件可以将你的atom配置和插件同步到指定的Gist,换了电脑也不需要一个一个的自己重新安装了,它会自动帮你装好,恢复你原来的工作环境。配置和使用教程查看官方说明。
]]>