跨尾是什么;高配置笔记本电脑脑要什么配置

让笔记本不再单调 十大跨界产品排行榜|跨界|产品|笔记本_笔记本_科技时代_新浪网
让笔记本不再单调 十大跨界产品排行榜
  “跨界”早已经不是什么新鲜词汇了,在设计圈中,跨界嫣然成为了一大主流理念。它的英文“crossover”愿意指交换、转型,现在多用于描述跨越两个不同领域、不同文化形式等范畴而融合而成一个新行业、新模式、新风格等的行为。
  车有跨界、服装有跨界、音乐、表演有跨界,电子产品特别是笔记本电脑领域,跨界之风也日渐盛行,一款八面玲珑的全能笔记本才不单调,才够好玩。下面,笔者就为大家盘点几款目前市面上主流的跨界笔记本/超极本产品。
&&& 本本与电影的约定--东芝U800W
  东芝U800W超极本将两种完全不同风格的材质进行了完美的融合,整体采用金属拉丝工艺打造,顶盖又使用了手感细腻的仿皮革材质打造,银色金属与黑色皮革的搭配很有个性。独特的21:9超宽屏,拥有的分辨率。
点击图片查看东芝U800W详细资料
点击图片查看东芝U800W详细资料
东芝 U800W-T01S
Windows 7 Home Premium 64bit(64位家庭高级版)
Intel 酷睿i5 3317U
32GB+500GB
混合硬盘(SSD+5400转HDD)
屏幕分辨率
Intel GMA HD 4000
4芯锂电池,5400毫安
笔记本重量
  产品点评:
  东芝U800W是一款专为影音娱乐打造的个性化超极本产品,独有的21:9超宽屏能带来全无黑边的超爽视觉体验,整机性能主流实用。降价后相当超值。
  东芝 U800W-T01S
  [优惠促销价]&5999元
东芝 U800 笔记本相关文章
值班电话:010-&&|&&&&|&&
请用微博账号,推荐效果更好!
看过本文的人还看过戴尔笔记本电脑型号
戴尔笔记本电脑型号
(C)2017 列表网&琼ICP备号-12&增值电信业务经营许可证B2-&快照读(读): select语句当前读(写): 增删改语句;特殊读(select * from table where ? lock in share mode).
快照读: 使用乐观锁(MVCC);当前读: 使用间隙锁(其实是行锁+间隙锁=&nextKey锁)。
规范中的可重复读: 有幻读问题;Mysql中的可重复读: 快照读没有幻读问题.
之前一直对mysql中间隙锁有点迷糊, 不知道为什么有了MVCC这么好的东西,还需要间隙锁。今天看了美团的技术博客才恍然大悟。
MVCC其实就是乐观锁:优点: 并发性能好;缺点: 实效性差,本质上读到的是快照(历史数据).
对于实效性要求比较高的写操作,MVCC是不可用的,比如:1update class_teacher set class_name='xxx' where teacherid=30;
这种操作如果要达到没有幻读,就要使用间隙锁.间隙锁顾名思义,就是把teacherid=30的左边的间隙和右边的间隙都锁上,不能插入新的teacherid=30的。
如果现有teacherid总共有10,30,50三种,会对(10,50)都上锁,其中30的是行锁,[10,30),(30,50]的是间隙锁.这个时候,不但30的插入不了,20的也插入不了了,因为正好在区间内。
特殊情况: 如果teacherid没有索引,那么也没什么间隙可以锁了,于是mysql就只好锁全表了.
不符合规范的优化:mysql加了范围锁以后,会把不符合条件的行锁提前释放.而这种操作是不符合两段锁规范的,两段锁协议要求流程要是一旦开始解锁就不能再加锁了。(所有解锁操作都在加锁后面.)
ConcurrentLinkedQueue目标: 线程安全的队列,并发性能好。实现: 基于链表的队列,ConcurrentLinkedQueue,使用volatile,CAS和HOPS变量(LockFree),性能较好,而且可以通过HOPS变量调节性能.
初始时:123456private transient volatile Node&E&tail =private transient volatile Node&E&public ConcurrentLinkedQueue() {
head = tail = new Node&E&(null);
simple解法(假想方案)一种simple的实现方案是这样的:
tail定义:1将tail定义成永远是队列最后一个节点:
1234567891011121314public &E& boolean offer(E e) {
if (e == null) {
throw new NullPointerException();
Node&E& n = new Node(e);
for (; ; ) {
Node&E& t =
if (t.casNext(null, n) && casTail(t, n)) {
return true;
}}
回想我们的目标:
安全: 由于tail是volatile的,因此可以通过CAS操作保证并发写的安全;
性能: 每次入列至少需要:12341次 volatile读;(tail)2次 CAS操作;2次 volatie写.(tail.next和tail)// tail.next实际上也是volatile变量。
jdk注释直接阅读源码有可能误解了作者的意思,而且源码确实比较trick,看不太懂,注释里声明的几个基本点:123456789101. 仅有一个节点的next为null,它是队列的最后一个节点. 想访问最后一个节点的话,可以通过tail节点往后找,可以通过O(1)时间找到(最多4次); // 注意tail不一定指向最后一个节点2. 队列没有null的实际数据(item = null),也就是容器不接受空元素。// dumpHead头节点的item为 移除节点的时候把item置为null,所以null是一个很重要的标志量,所以不允许实际数据有null造成冲突;3. 为了满足上一条,更为了gc,过期的节点的next指向自己;//
所以很多节点遇到p==p.next的时候,会跳转到head重新进入有效节点范围,或者进行过期逻辑处理;4. 把item置为null相当于移除了该数据,迭代器iterator会跳过item为null的节点.
实际解法这个思路最重要的有两点:一点就是updateHead函数,离群节点的next置为自己;另一点就是tail不一定是最后一个节点.由于volatile写的开销远远高于volatile读,jdk中的解法是:1231. 假设队列中最后一个节点为:realT2. tail 不一定等于 realT3. 仅当tail与realTail距离大于等于HOPS时,才更新tail=realTail.
这样设计以后,可以通过增加volatile读减少volatile写。
1234567final void updateHead(Node&E& h, Node&E& p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
offer方法123456789101112131415161718192021222324public boolean offer(E e) {
checkNotNull(e);
final Node&E& newNode = new Node&E&(e);
for (Node&E& t = tail, p =;) {
Node&E& q = p.
if (q == null) {
if (p.casNext(null, newNode)) {
if (p != t)
casTail(t, newNode);
return true;
else if (p == q)
p = (t != (t = tail)) ? t :
p = (p != t && t != (t = tail)) ? t :
}}
其中有一种trick:123456789t != (t = tail)if(t!=tail){t=}else{}
因此p = (t != (t = tail)) ? t :的实际含义就是:1234567891011if(t!=tail){
p=t;}else{
p=}1. 如果t!=tail,说明t过时了,tail可能还可用,因此尝试从tail重连;(t=tail,p=t);2. 如果t==tail,说明t和tail都过时了,尝试从head重连。(p=head)
使用这个trick的目的不得而知,个人猜测是因为效率更高,代码更精炼。毕竟非阻塞并发存在的意义就是性能,而不是可读性,如果想要可读性,就去用阻塞队列了。可读性下降了.但如果用得熟练的话,肉眼parse功力上涨,也可能对于大牛来说比if else阅读起来更快。
节点定义1234567891011121314151617181920212223242526272829303132333435363738private static class Node&E& {
volatile E
volatile Node&E&
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
void lazySetNext(Node&E& val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
boolean casNext(Node&E& cmp, Node&E& val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
private static final sun.misc.Unsafe UNSAFE;
private static final long itemO
private static final long nextO
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class&?& k = Node.
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
poll方法1234567891011121314151617181920212223public E poll() {
restartFromHead:
for (;;) {
for (Node&E& h = head, p = h,;) {
E item = p.
if (item != null && p.casItem(item, null)) {
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
else if (p == q)
continue restartFromH
}}
succ方法12345final Node&E& succ(Node&E& p) {
Node&E& next = p.
return (p == next) ? head :}
remove方法1234567891011121314151617181920212223public boolean remove(Object o) {
if (o != null) {
Node&E& next, pred = null;
for (Node&E& p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.
if (item != null) {
if (!o.equals(item)) {
next = succ(p);
removed = p.casItem(item, null);
next = succ(p);
if (pred != null && next != null)
pred.casNext(p, next);
if (removed)
return true;
return false;
2-3树与红黑树
参考资料:2-3树:红黑树:
背景2-3树和红黑树都是一种平衡查找树。其中红黑树是在2-3树的基础上,对于实现进行了简化,代价是逻辑上(表现图)增加了直接理解的难度。换句话说就是:
2-3树理解起来简单,实现起来代码比较麻烦(其实也可以实现);
红黑树理解起来稍微曲折,实现起来代码非常简单/简洁,如果用递归实现还非常优雅。
因此我们应该先学2-3树,理解以后,带着简单理解去学红黑树的实现。上述参考资料里的两个链接对于两者的介绍非常值得一看。
2-3树定义2-3树意思就是树里有两种节点:2-node: 保存1个key,左右2个子节点;3-node: 保存2个key,左中右3个子节点。
和二叉查找树类似,如果中序遍历一个2-3树,可以得到一个排好序的序列。在一个完全平衡的2-3树中,根节点到每一个叶节点距离相同.
查找和B树类似,按key跳转子节点查找就好了.
插入往2-node节点插入
插入K,把2-node变成3-node。
往3-node节点插入1. 只有一个3-node节点插入S,把3-node变成4-node,把4-node分裂成3个2-node,中间key拉升做根节点.(无法向上传导,根分裂,高度+1)
2. 父节点是2-node,节点是3-node插入Z:
把3-node变成4-
把中间key往上扔,传递给父节点;
父节点从2-node=&3-node。
3. 父节点是3-node,节点是3-node思路和之前相似,把中间元素往上传导,直到遇到父节点是2-node的。如果连根节点也是3-node,就进行根节点分裂,高度+1。
性能最坏情况,所有节点都是2-node,因此最坏时间复杂度为O(logN).最好情况,所有节点都是3-node,因此最好时间复杂度为O(log3N)=0.631logN.
红黑树2-3树中有3-node,红黑树则是将其全部转化成二叉树来实现。参考资料2中同时出现红色节点与红色连线,容易引起混淆. 这里统一定义一下, 红色本质上是指节点,红黑信息实现上存放在红子节点中. 之所以有红色连线,主要是为了在图里画,可以更清晰得看出有两个红子节点的父节点.
图中: 以红色连线表示子节点是红节点;
实现中: 红色信息存放在子节点中. (并不会存指向父节点的指针)
具体实现:1234567891011class Node{
public Node Left { }
public Node Right { }
public TKey Key { }
public TValue Value { }
public int Number { }
public bool Color { }
定义:1231. 所有节点分为红节点和黑节点;2. 表现图中一个红节点的画法: 把它和父节点之间的连线画成红色。(看图的时候,一条红线:子节点是红节点)
规则:12341. 所有节点,只有左子节点可以是红节点, 右子节点只能是黑;(通过旋转保证)2. 红色节点不能相连; // 父节点红色,子节点不能也红色3. 不能左右都是红节点. // 左右子节点不能都是红色(2,3两个规则,图上看就是一个节点不能有两个红色连线)
如上图所示,红黑树其实是2-3树的另一种表现形式。将红黑树的红色连线水平绘制,就可以把水平的两个节点整体看作一个3-node.从实现上看,也就是把节点和它的红子节点看作一个3-node。从这个思路来看,也就能理解上述的3条规则了, 如果违反了任意一条,都会导致节点与其红子节点合并成一个整体来看待的时候, 变成4-node甚至5-node更多.只有满足所有规则, 才能保证合并来看的时候最多是一个3-node。
查找由于本质上还是二叉查找树,查找的代码与普通二叉查找树一样.只不过由于比较平衡,性能有保障。(如果把红黑树看作2-3树的话,查找过程的差别就在于遇到3-node的时候,先找的是最左的key,然后通过红子节点查找了中间key。)
平衡化为了维持上文中的规则,也为了红黑树保持平衡,保障性能,需要进行一些平衡操作。
旋转左旋: 输入父节点;右旋: 输入父节点.
左旋实现:123456789101112private Node RotateLeft(Node h){
Node x = h.R
h.Right = x.L
x.Color = h.C
h.Color = RED;
return}
注意: 子节点继承父节点的颜色.父节点变为红色。
右旋实现:1234567891011private Node RotateRight(Node h){
Node x = h.L
h.Left = x.R
x.Color = h.C
h.Color = RED;
return}
注意: 子节点继承父节点的颜色.父节点变为红色。
颜色反转如果违反规则2,也就是两个子节点都是红色节点,也需要调整/平衡化.如下图所示的话,其实就是把父节点变成红色,两个子节点变成黑色. 很简单。
插入往2-node底部插入规则:1新插入的节点标记为红色.
左图不需要额外操作;右图需要一次左旋.
往3-node底部插入
往只有两个节点的树中插入: (注意形状一定是左偏的)规则:1新插入的节点标记为红色.
左图需要1次颜色反转;中图需要1次右旋,1次颜色反转;右图需要1次左旋,1次右旋,1次颜色反转.
简单总结一下平衡化的技巧如下:(按顺序检查,每次调整后回到1从头重复检查)
如果左右都是红,用颜色反转;
如果右子是红,左旋;
如果连续两个左子是红,(父子都是红节点),右旋,然后颜色反转.
更多案例:注意图14中到红色传递到根节点M后,由于根节点没有父节点,它的红黑属性不再重要,为了维持不打破上述的规则,定义根节点始终为红. 如果通过颜色反转或者其他平衡化操作,把根节点变红以后,可以视作根节点还是黑.(强制根节点黑)
Put实现代码12345678910111213141516171819202122232425262728293031public override void Put(TKey key, TValue value){
root = Put(root, key, value);
root.Color = BLACK; }private Node Put(Node h, TKey key, TValue value){
if (h == null) return new Node(key, value, 1, RED);
int cmp = key.CompareTo(h.Key);
if (cmp & 0) h.Left = Put(h.Left, key, value);
else if (cmp & 0) h.Right = Put(h.Right, key, value);
else h.Value =
if (IsRed(h.Right) && !IsRed(h.Left)) h = RotateLeft(h);
if (IsRed(h.left) && IsRed(h.Left.Left)) h = RotateRight(h);
if (IsRed(h.Left) && IsRed(h.Right)) h = FlipColor(h);
h.Number = Size(h.Left) + Size(h.Right) + 1;
return}private int Size(Node node){
if (node == null) return 0;
return node.N}
注意到插入操作是自顶向下(dfs递归调用)进行的,由于dfs深度优先,可以先从子节点检查平衡/调整.因此不平衡从子节点传递到父节点,可以逐步调整传递到根节点,平衡操作也贯穿这个层层返回的过程,从平衡树的底层一直进行到根节点为止.
性能满足上述规则后, 红黑树能保障所有操作最坏复杂度为O(logN)。
最坏情况:除了最左侧路径,其他全部是3-node组成. 红黑相间的路径长度是全黑路径的两倍.
特性:红黑树从根节点到叶子节点的路径中,全黑路径是平衡的.
应用主要是各种map/符号表:1234Java: TreeMap,TreeSet,HashMap,ConcurrentHashMapC++: STL: map,multimap,multiset.Net: SortedDictionary,SortedSetLinux: epoll,防火墙规则表
不能两个左右子节点都是红节点; (颜色反转)
父子节点不能都是红节点;(右旋)
右节点不能是红节点;(左旋)
新插入的节点是红节点;(新红)
根节点强制是黑节点.(根黑)
/etc/resolv.conf配置文件的4个关键字:123456789101112// 1. 定义dns服务器的地址,按序查找nameserver 8.8.8.8nameserver 114.114.114.114// 2. 本地域名.
domain google.com// 3. 搜索域名.search google.com cn.bing.com// 4. 返回域名的排序sortlist
主要用到的其实是nameserver和search.
其中search的用例如下:1ping new
由于new这个域名找不到尝试搜索new.google.com,(search域的第一项)如果还是搜不到,就尝试搜索new.cn.bing.com.也就说search会尝试补齐域名。
至于domain,相当于配置search的默认值。
第六章 java并发容器和框架主要内容是concurrent包中并发性能较好的几个容器,逻辑上是:
map: concurrentHashMap;
队列: concurrentLinkedQueue;
阻塞队列.此外还有Fork/Join工作窃取框架(java8 stream api的底层)。
6.1 ConcurrentHashMap6.1.1 背景HashTable缺点:并发性能低,直接锁整个table。HashMap缺点:非线程安全,并发时产生死循环。多线程put操作可能导致环形链表。
ConcurrentHashMap:分段加锁,并发效率高。
6.1.2 具体实现concurrentHashMap在不同jdk版本中实现不同:
jdk1.6中的实现
基本思路将哈希表分成几个段,然后分段加锁。把原来的一维数组,变成二维数组。(类似于二级索引,先寻址到第一级,获得锁以后再访问第二级具体数据)
如类图所示:ConcurrentHashMap-&Segment-&HashEntry
第一维是Segment数组,每个Segment里存一个HashEntry数组,最后拉链表。HashEntry源码:
12345static final class HashEntry&K,V& {
volatile V
volatile HashEntry&K,V&
特点1.实际并发度为2的n次方。(离散取值,而不是连续)=&原因:Segment数组的长度需要为2的n次方=&原因:寻址的时候使用移位来提高性能,需要数组长度为2的n次方:hash值的高位确定落在哪个Segment里,低位确定落在Segment中的位置。
123hash &&& segmentShift) & segmentMaskint index = hash & (tab.length - 1);
并发度(Concurrency Level)实际就是由多少锁,也就是Segment数组的长度。如果设定并发度为14,实际上会创建长度为16的Segment数组,也就是实际并发度为16.
2.弱一致性写入的数据并不一定能马上读到,或者不一定能读到最新数据。// get,containsKey,clear方法和迭代器都是弱一致性的原因:并发的时候使用了volatile代替锁提高性能,牺牲了一致性换取性能。12345static final class HashEntry&K,V& {
volatile V
volatile HashEntry&K,V&
由于遍历期间其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据。如在get执行UNSAFE.getObjectVolatile(segments, u)之后,其他线程若执行了clear操作,则get将读到失效的数据。
由于clear没有使用全局的锁,当清除完一个segment之后,开始清理下一个segment的时候,已经清理segments可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此,clear方法是弱一致的。
如果加锁的话,就能保证强一致性了。
jdk1.8中的实现
基本思路//把链表改成红黑树,提高效率。第一维:数组第二维:链表(&8)/红黑树用CAS操作代替大范围加锁。锁的粒度缩小到Node级别。(linux中很多并发相关的数据结构都是红黑树,在如epoll,防火墙中应用)
最小粒度是Node,成员变量如下:12345static class Node&K,V& implements Map.Entry&K,V& {
volatile V
volatile Node&K,V&
可以看出与原来的HashEntry基本一致,多了一个继承。
其他的容器成员:1234567891011121314151617181920212223242526272829303132333435363738transient volatile Node&K,V&[]private transient volatile Node&K,V&[] nextTprivate transient volatile long baseCprivate transient volatile int sizeCprivate transient volatile int transferIprivate transient volatile int cellsBprivate transient volatile CounterCell[] counterCprivate transient KeySetView&K,V& keySprivate transient ValuesView&K,V&private transient EntrySetView&K,V& entryS
修饰符复习:transient: 序列化的时候忽略。(一般会自定义相关的序列化操作)一般用于一些运行时的状态变量。
sizeCtl表初始化和扩容时的控制状态量。
-1: 正在初始化;
-2: 正在扩容,有1个活跃的扩容线程数;
-3: 正在扩容,有2个活跃的扩容线程数;
-N: 正在扩容,有N-1个活跃的扩容线程数;以此类推…
0: 还没初始化,未指定容量。
table=null: 还没初始化,目标初始化容量。
table!=null: sizeCtrl=0.75*n(扩容阈值,抵达这个值就要进行扩容了)
Node节点会从链表节点转化为红黑树节点TreeNode:
123456789static final class TreeNode&K,V& extends Node&K,V& {
TreeNode&K,V&
TreeNode&K,V&
TreeNode&K,V&
TreeNode&K,V&
TreeBin作为红黑树的根节点,封装相关的操作,并且有读写锁:
12345678910111213141516171819202122232425262728293031static final class TreeBin&K,V& extends Node&K,V& {
TreeNode&K,V&
volatile TreeNode&K,V&
volatile T
volatile int lockS
static final int WRITER = 1;
static final int WAITER = 2;
static final int READER = 4;
private final void lockRoot() {
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
contendedLock();
private final void unlockRoot() {
lockState = 0;
null if added
final TreeNode&K,V& putTreeVal(int h, K k, V v) {
tabAt方法,获取i位置上的Node:
123static final &K,V& Node&K,V& tabAt(Node&K,V&[] tab, int i) {
return (Node&K,V&)U.getObjectVolatile(tab, ((long)i && ASHIFT) + ABASE);}
casTabAt方法,CAS设定i位置上的Node:
1234static final &K,V& boolean casTabAt(Node&K,V&[] tab, int i,Node&K,V& c, Node&K,V& v) {return U.compareAndSwapObject(tab, ((long)i && ASHIFT) + ABASE, c, v);}
setTabAt方法,直接设定i位置上的Node(已经占据锁的时候使用),利用volatile:
123static final &K,V& void setTabAt(Node&K,V&[] tab, int i, Node&K,V& v) {U.putObjectVolatile(tab, ((long)i && ASHIFT) + ABASE, v);}
简单总结:get方法: 利用volatile,不加锁,并发访问;(弱一致性)put方法: 利用CAS方法,轮询,大部分时候不加锁.如果是红黑树,调用它的putTreeVal方法。
前情提要:12(1)中写的主要是Lock接口,Condition接口的api;然后是自定义Lock时,需要使用的AQS的api以及大致实现、示例、基础数据结构。
这部分主要介绍案例:jdk库中提供的重入锁、读写锁以及LockSupport。
java并发编程的艺术-第五章(2)5.3 重入锁重入锁: 一个线程能否重复获得同一个锁。该特性需要解决两个问题:
线程再次获取锁。识别获取锁的线程是否为当前占据锁的线程。
锁的最终释放。重复获取n次,则也需要释放n次。
重入锁示例:
synchronized临界区,同一个线程能够重复进入;
ReentrantLock锁,能够重复使用lock.lock().
不可重入锁示例:前文(1)部分中自定义的锁Mutex。
公平锁与非公平锁公平锁: FIFO,竞争锁时需要判断先来后到;非公平锁: 效率优先,可能有饥饿。同一个线程可能连续获得锁。
实现上:ReentrantLock的公平锁的tryAcquire方法判断条件比非公平的多了一个hasQueuedPredecessors方法,以确保FIFO。
回顾之前同步队列的节点数据结构,是一个双向链表,因此可以判断前驱节点是否存在,即使是因为中断被唤醒节点也可以正确判断自己的位置。 而等待队列是一个单向链表,因此如果节点需要进入到等待队列时,本质上都是非公平的。
5.4 读写锁ReentrantReadWriteLock前文提到的所有Lock的实现,依赖于一个状态变量volatile int state。本质上都是排他锁。只不过有些实现上通过设定state的合法状态范围(TwinsLock),设定了资源的最大数量,让同一个时间能有多个线程同时获取到锁。(需要考虑与可重入特性是否冲突)
读写锁通过对state状态变量进行前16位和后16位分割,当作两个状态变量来使用(需要考虑数据类型溢出),从而同时保存了读写状态。
同一时刻可以允许多个读线程访问;
写线程访问时:其他读写线程都不能访问;
读线程访问时:读线程可以访问,写线程不能访问。
回顾(1)部分中的独占式和共享式api的区别,可以明白读写锁的实现需要同时实现tryAcquire(独占式)和tryAcquireShared(共享式)。
读写锁能提供比简单写锁更好的性能。(并发性和吞吐量)
读写锁ReentrantReadWriteLock的特性:
支持公平或非公平锁;
支持重进入;
支持锁降级。
锁降级锁降级指的是从写锁降级为读锁。具体流程:
释放读锁。
那么为什么需要锁降级这个特性呢?因为需要提高性能。锁降级的基本思想就尽量减少写锁的持续时间,同时保持这个线程操作的语义不变。
例如:假如一个线程A需要做的事:
读a,然后计算b=a+1(结果b=2)。
上述过程中其实只有步骤1需要写锁,从步骤2开始只需要读锁就好了。但如果直接在步骤1后释放写锁,从1到2的时间间隙中,可能被别的线程获取到写锁,然后修改了a的值。这样就改变了线程A操作的原子性。
为了保证线程A操作的原子性,有两种方案:
步骤1和2整个过程都占据写锁;
步骤1结束后,进行锁降级。由于线程A占据读锁后,所有线程无法获取写锁,达到了性能与语义兼顾。
使用锁降级的话,整个过程中所有别的线程都无法获取写锁,但别的线程在后半程能够获取读锁。因此提高了读性能。
5.4.1 读写锁的接口与示例接口: ReadWriteLockjdk实现: ReentrantReadWriteLock
ReadWriteLock的api:
readLock()
writeLock()
ReentrantReadWriteLock的api:
int getReadLockCount()
读锁被获取的次数(pv). 不去重。
int getReadHoldCount()
当前线程获取读锁次数(pv).不去重
boolean isWriteLocked()
写锁是否被获取
int getWriteHoldCount()
写锁被获取的次数(pv)
案例之CacheReentrantReadWriteLock的使用示例,实现一个cache:123456789101112131415161718192021222324252627282930313233343536373839import java.util.HashMimport java.util.Mimport java.util.concurrent.locks.Limport java.util.concurrent.locks.ReentrantReadWriteLpublic class Cache {
static Map&String, Object& map = new HashMap&&();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
public static final Object get(String key) {
try {
return map.get(key);
} finally {
r.unlock();
public static final Object put(String key, Object value) {
try {
return map.put(key, value);
} finally {
w.unlock();
public static final void clear() {
try {
map.clear();
} finally {
w.unlock();
}}
通过ReentrantReadWriteLock生成的readLock和writeLock,把非线程安全的HashMap操作包装成线程安全的,并且尽量保持了并发性能。使用上还是比较简单的,只需要每次加上finally unlock即可。
5.4.2 读写锁的实现分析
读写状态的设计由于需要使用AQS来实现读写锁,而AQS成员变量里状态变量只有一个,因此将state变量复用为两个变量。state本来是一个int,把高16位作为读的状态量,低16位作为写的状态量。由此可以看出读线程最大并发数是2^16-1,写线程重入的嵌套深度是2^16-1。
读状态: S&&&16 (无符号右移)写状态:S & 0x0000FFFF
写锁的获取与释放:写锁获取: S=0(c=0),没有人获取写锁,也没人获取读锁。由exclusiveCount函数获取写状态。
123456789101112131415161718192021222324252627282930313233@ReservedStackAccessprotected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) & MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;}@ReservedStackAccessprotected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() -
boolean free = exclusiveCount(nextc) == 0;
setExclusiveOwnerThread(null);
setState(nextc);
return}
读锁的获取与释放读锁获取: 没有人占据写锁。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162@ReservedStackAccessprotected 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 =
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldC
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
return fullTryAcquireShared(current);}@ReservedStackAccessprotected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldC
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
int count = rh.
if (count &= 1) {
readHolds.remove();
if (count &= 0)
throw unmatchedUnlockException();
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}}
4. 案例之锁降级锁降级指的是从写锁降级为读锁。具体流程:
释放读锁。12345678910111213141516171819202122232425
public void processData() {
if (!update) {
r.unlock();
try {
if (!update) {
update = true;
} finally {
w.unlock();
try {
} finally {
r.unlock();
值得注意的是,案例中使用了双检,因此update变量应该是volatile。
5.5 LockSupport工具回顾前文中的实现层次,自顶向下:1231. Lock/Condition接口2. AQS3. volatile/CAS/LockSupport
其中AQS中除了使用volatile变量与CAS操作以外,还调用了LockSupport以完成等待操作。例如线程在同步队列中进行自旋等待时,调用的方法:1234private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();}
唤醒下一个节点时调用的方法:1234567891011121314private void unparkSuccessor(Node node) {
int ws = node.waitS
if (ws & 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.
if (s == null || s.waitStatus & 0) {
for (Node p = p != node && p != null; p = p.prev)
if (p.waitStatus &= 0)
if (s != null)
LockSupport.unpark(s.thread);
LockSupport提供的api:
void park(Object blocker)
阻塞当前线程. 类似wait。使用时可以park(this),也可以park其他对象。
void parkNanos(long t)
加上超时返回。
void parkUntil(long deadline)
最迟deadline时返回。
void unpark(Thread thread)
唤醒特定线程。
Object getBlocker(Thread t)
获取某线程调试对象,如果未阻塞则为null。
上述api也可以不带blocker参数。blocker参数仅仅是用于调试和系统监控。
LockSupport提供的park/unpark类似于wait/notify,都是等待/通知的模式。主要存在以下几点不同:
park还可能在没有被唤醒的时候返回,因此必须在循环中重新检查返回条件。这种设计是一种忙碌等待的优化,效率介于快速自旋与wait之间,灵敏度介于快速自旋与wait之间。示例用法(检查返回条件):
123456789101112131415161718public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
unpark可以先于park调用,而notify不能先于wait。unpark相当于赋予线程一个许可,最多缓存一个,等待下一次park时可以直接通过。
unpark可以精确唤醒某个线程,而notify只能随机唤醒一个,或者唤醒全部。
LockSupport实现浅析总结:12park对象用于实现;blocker对象用于调试。
park与unpark的实现都是委托给了一个Unsafe对象U实现的:12345private static final Unsafe U = Unsafe.getUnsafe();public static void park() {
U.park(false, 0L);
park方法面向的主体是Thread,每个线程内有一个Parker对象以承载相应的阻塞操作;wait方法则依赖的是每个对象的内置锁实现。因此两者是正交的。
可以查阅hotpot的源代码进一步深入其Parker的实现。
Blocker参数带Blocker参数的park方法:12345678910public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}private static void setBlocker(Thread t, Object arg) {
U.putObject(t, PARKBLOCKER, arg);}
可以看出区别是多了setBlocker函数的调用,而且Blocker参数在阻塞结束后会被清空。此外,Blocker与Parker类似,都是每个线程有一个。设定的逻辑是在该线程t的PARKBLOCKER偏移量中填入对象blocker的引用。
使用blocker的话,在线程阻塞时进行线程dump,可以获得blocker的信息,方便调试和监控。类比,如果线程因为synchronized(this)而阻塞,线程dump的时候是可以获得this的信息的。
实验Blocker:
12345678910111213141516171819202122232425262728293031323334353637383940public class ParkWithBlockerTest {
public static class UnparkerAndReciever extends Thread {
UnparkerAndReciever(Thread main) {
this.main =
private void sleep(int secends){
try {
TimeUnit.SECONDS.sleep(secends);
} catch (InterruptedException e) {
e.printStackTrace();
public void run() {
LockSupport.unpark(main);
Object blocker = LockSupport.getBlocker(main);
if (blocker != null && blocker instanceof String) {
System.out.println(blocker);
else{
System.out.println("blocker=null or not a String");
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
UnparkerAndReciever unparkerAndReciever = new UnparkerAndReciever(mainThread);
unparkerAndReciever.start();
LockSupport.park("string in park");
}}
第五章 java中的锁这章主要写java并发包中与锁相关的api.先是使用,然后是实现。
5.1 Lock接口Lock接口由Java SE 5新增,对飙的是旧java中的synchronized内置锁。主要新增了三个api:
非阻塞获取锁(tryLock);
能被中断地获取锁(lockInterruptbly);
超时获取锁(tryLock(long time,TimeUnit unit)).此外还增加了Condition接口,增加了等待队列的数量及相关特性。(Condition使用await(),对飙的是原来的obj.wait())
示例代码:12345678910Lock lock=new ReentrantLock();lock.lock();try{
}finally{
lock.unlock();
阻塞式地获取锁。只有在获取到锁后才处理interrupt信息。
lockInterruptibly()
获取锁。(可中断)
非阻塞地获取锁。不论成败立即返回。
tryLock(long time,TimeUnit unit)
超时获取锁,接受中断
Condition newCondition()
获取等待通知组件。
回顾之前的线程状态,lock方法会让线程进入Blocked状态。线程状态与可中断的关系:
Runnable: 不可中断(除非主动检查)
Terminated: 不可中断;
Blocked: 不可中断; (在synchronized时阻塞住不会响应中断)
Waiting: 可中断;(sleep方法,wait方法都会抛异常)
Time_waiting: 可中断;
简单得说,就只有Waiting和Time_waiting状态可以中断,或者自己在代码里手动检查中断状态。
本章的内容有点乱,不太通顺,下面的内容按照个人理解重排。
5.6 Condition接口Condition接口对飙的是原来的wait/notify机制,新推出的是await/signal机制。wait/notify依赖synchronized获得锁,而Condition依赖Lock获得锁。原来的wait/notify:12345678synchronize(obj){
obj.wait();}synchronize(obj){
obj.notify();}
使用Condition的await/signal:12345678910111213141516171819Lock lock=new ReentrantLock();Condition condition=lock.newCondition();lock.lock();try{
condition.await();}finally{
lock.unlock();}lock.lock();try{
condition.signal();}finally{
lock.unlock();}
功能上:两种实现都是使用一个对象进行同步操作(加锁解锁)。消费者在获得锁后,在该对象上进行等待(释放锁等待);生产者获得锁后,通知在该对象上等待的其他线程。区别在于synchronized不需要关心锁的释放,而Lock接口需要在finally中手动确保锁的释放。wait只有一个等待队列,而由于一个Lock可以生成多个Condition,因此await可以有多个等待队列。
案例之有界队列12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152import java.util.concurrent.locks.Cimport java.util.concurrent.locks.Limport java.util.concurrent.locks.ReentrantLpublic class BoundedQueue&T& {
private Object[]
private int addIndex, removeIndex,
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
items[addIndex] =
if (++addIndex == items.length) {
addIndex = 0;
notEmpty.signal();
} finally {
lock.unlock();
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
Object x = items[removeIndex];
if (++removeIndex == items.length) {
removeIndex = 0;
notFull.signal();
return (T)
} finally {
lock.unlock();
}}
上述案例中使用一个锁(Lock对象),而有两个等待队列,分别等待notEmpty和notFull条件。
实现逻辑上:(逻辑上的工作流程)
线程尝试获取锁,如果成功则占据锁,失败则进入同步队列;
如果成功获取锁,且进行等待(wait或await),则进入等待队列,释放锁,通知同步队列中的后继节点;
如果成功获取锁,且进行通知(notify或signal),则等待队列的首节点挪入同步队列,释放同步锁,通知同步队列中的后继节点。
两个后面会反复用到的概念:
同步队列: 获取同步锁失败后的线程进入同步队列;
等待队列: 获取同步锁成功后,主动等待的线程进入等待队列。
5.2 队列同步器前文中锁的实现中,一个很重要的组件是:队列同步器:AQS(AbstractQueuedSynchronizer)。此外,其他同步组件的基础框架也是使用AQS。(如ReentrantLock,CountDownLatch)回顾55页3.5.4节中的架构层次:1234// 自顶向下:1.
Lock,同步器,阻塞队列,Executor,并发容器2.
AQS,非阻塞数据结构,原子变量类3.
volatile读写,CAS操作
AQS是一个抽象类。AQS中自底向上的3类方法:
基础方法。(固定)
可重写的方法(调用1的方法);
模版方法;(固定,调用1,2的方法)
AQS在设计上是基于模版方法模式的抽象类。也就是说,我们需要新增一个子类继承AQS,然后重写上述第2类方法。而第1类和第3类方法,要么是private的无法继承,要么是final的无法重写。而第二类方法,如果没有重写,默认实现只有一行throw new UnsupportedOperationException();,调用的时候就会直接抛异常了。
getState(): 获取当前同步状态;
setState(): 设置当前同步状态;
compareAndSetState(int expect,int update): 使用CAS设置当前状态,该方法保证设置的原子性。
可重写的方法
可重写的方法
boolean tryAcquire(int arg)
独占式获取同步状态。(调CAS)
boolean tryRelease(int arg)
独占式释放同步状态。
int tryAcquireShared(int arg)
共享式获取同步状态。返回值&=0则成功。
boolean tryReleaseShared(int arg)
共享式释放同步状态。
boolean isHeldExclusively(int arg)
判断是否被当前线程独占。
void acquire(int arg)
独占式获取同步状态(调用上面的tryAcquire),失败则进入同步队列。
void acquireShared(int arg)
共享式获取同步状态,失败则进入同步队列。
boolean release(int arg)
独占式释放同步状态
boolean releaseShared(int arg)
共享式释放同步状态
Condition& Thread&getQueuedThreads()
获取同步队列线程集合
其他响应中断/超时返回的版本的方法
案例之-独占锁Mutex的实现:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879import java.util.concurrent.TimeUimport java.util.concurrent.locks.AbstractQueuedSimport java.util.concurrent.locks.Cimport java.util.concurrent.locks.Lpublic class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
protected boolean isHeldExclusively() {
return getState() == 1;
public boolean tryAcquire(int acquireds) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
return false;
protected boolean tryRelease(int releases) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
Condition newCondition() {
return new ConditionObject();
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
public boolean tryLock() {
return sync.tryAcquire(1);
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
public void unlock() {
sync.release(1);
public Condition newCondition() {
return sync.newCondition();
public boolean isLocked() {
return sync.isHeldExclusively();
public boolean hasQueuedThread() {
return sync.hasQueuedThreads();
}}
上述sync只实现了独占操作,因此调用共享操作会抛异常。因此mutex中只调用了sync的独占模版方法。因此mutex最多是一个独占锁。
案例之-TwinsLock用AQS实现一个最多能被两个线程同时占据的锁。TwinsLock实现:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172public class TwinsLock implements Lock {
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count &= 0) {
throw new IllegalArgumentException("count must larger than 0");
setState(count);
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceC
if (newCount & 0 || compareAndSetState(current, newCount)) {
return newC
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnC
if (compareAndSetState(current, newCount)) {
return true;
Condition newCondition() {
return new ConditionObject();
private final Sync sync = new Sync(2);
public void lock() {
sync.acquireShared(1);
public void unlock() {
sync.releaseShared(1);
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
public boolean tryLock() {
return sync.tryAcquireShared(1) &= 0;
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
public Condition newCondition() {
return sync.newCondition();
}}
与Mutex不同的是,TwinsLock中的同步器主要重写了AQS的共享方法,Lock接口中也调用的是共享方法。也就是按照共享式访问编写,然后用count控制同步资源数。
5.2.2 AQS的实现分析上面讲了Lock,Condition的使用,AQS的使用(用来自定义Lock),下面讲AQS的实现。
同步队列与等待队列AQS即队列同步器,很重要的一个概念就是同步,要控制多个线程对于一个锁的访问获取释放。获取锁失败的线程都会进入同步队列,而一个lock上可以有多个condition,获取锁成功后还需要等待某个condition的线程进入等待队列,然后释放锁。
同步队列是一个FIFO队列,实现上使用一个双向链表。等待队列也是一个FIFO队列,实现上使用一个单向链表。
同步队列的基本结构:
加上等待队列后的结构:
因为节点可以在同步队列和等待队列之间转化。(同步队列中节点获得锁后,可能发现需要等待condition,进入等待队列尾部;等待队列中节点唤醒后进入同步队列尾部)JDK中将两个链表的节点的数据结构杂糅在了一起,大致如下:
Thread thread
线程引用。
同步队列使用。前驱同步节点。
同步队列使用。后继同步节点。
Node nextWaiter
等待队列使用。后继等待节点。
int waitStatus
等待状态。
其中的waitStatus取值包括:Cancelled:1. 同步队列中等待超时或被中断;Signal:-1. 当前节点释放了同步状态或者被取消。Condition: -2. 当前节点等待某个Condition,进入等待队列。Propagate: -3. 下一次共享式同步状态获取将会无条件地被传播下去。(没看懂.TODO)Initial: 0. 初始状态。
同步队列遵循FIFO,AQS中保存了head和tail。每次唤醒时,唤醒head;(由于每次由已经获取锁的线程完成,只有一个线程,没有并发,因此不需要CAS)每次新增线程时,用CAS新增更改tail。(各种链表指针操作)
自旋:同步队列中的线程被唤醒的两种可能:
被离开队列的首节点唤醒。每次被唤醒都会进行一次自旋。自旋说白了就是一个死循环,首先检查前驱是否是首节点(判断是否是第二种情况),如果是就试图获取锁,获取失败就接着睡等待下次唤醒(等待下次自旋)。因此并不是只有第二个节点会自旋, 同步队列的所有节点都会自旋,毕竟存在被中断唤醒的可能。
同步状态的获取与释放同步状态获取释放相关的方法主要有两类:独占式和共享式。
模版方法:(AQS中写好的,可以翻看源码研读)
12345public final void acquirepublic final boolean releaseprivate Node addWaiterprivate Node enqfinal boolean acquiredQueued`
需要自己重写的:
12protected boolean tryAcquireprotected boolean tryRelease
独占式的特点就是:tyrAcquire,tryRelease的返回值是boolean。因为同一时刻只能有一个线程占据,因此用boolean就能表达了。相当于资源数只有1,状态只有0,1两种。
共享式的代码逻辑会复杂一些,如上图所示:
临界区里有共享锁时,只有共享请求能进入;
临界区里有独占锁时,谁都不能进。
共享式的方法:
模版方法:
123public final void acquiredSharedprivate void doAcquiredSharedpublic final boolean releaseShared
自己重写的方法:
12protected int tryAcquireSharedprotected int tryReleaseShared
共享式的特点就是:tryAcquireShared,tryReleaseShared返回值是int。因为共享式的资源数可能不为1,状态较多。doAcquireShared方法中判断是否还有资源,用tryAcquireShared(arg)&=0来判断,可知自己实现的时候,要注意让tryAcquireShared方法返回剩下的资源数。
考虑优化的层面
服务器层(优化器,缓存)
schema设计(索引/表)
存储引擎 (详见第二章笔记)
尽量使用innodb,以便使用它的行级锁;Myisam是表级锁,锁竞争更频繁。
根据场景选择:场景:全文索引: InnoDB + Sphinx在线热备份: InnoDB记录日志: MyISAM,Archive10TB以下: InnoDB10TB以上: InfoBright.大部分情况优先考虑: Innodb
schema设计表设计(详见第四章笔记)
数据类型:
1234(1) 更小的通常更好,int 好于 char (如可以用int存储ip);date/time/datetime 好于 char.(2) 尽量Not N // 因为NULL导致索引需要额外字节.// 不过这个影响不大
考虑使用缓存表,汇总表,计数器表;
表设计原则:
1231. 避免太多的列; 服务器层需要缓冲存储引擎层的行,变长的行会引入转换开销.2. 避免太多关联;3. 恰当使用NULL. 大多数时候避免NULL列,有时候可以使用.(当异常值不好定义时)
适当程度使用范式,不迷信三范式.特定需求可以单独定制相应的缓存表和汇总表,而不改变原有的设计结构.
索引设计(详见第五章笔记)
不同类型索引适用场景:12341. 哈希索引:(全键值查询,Memory引擎)数据仓库的查找表。(精确匹配O(1)复杂度,星形,多表连接)2. B-Tree索引:大部分场景。
三星系统索引与查询匹配程度的评价:三星系统:
一星: 将查询相关记录放在了一起;(顺序IO,范围查询能利用到索引)
二星: 数据顺序与查找顺序一致;(避免排序,让排序也能利用到索引)
三星: 索引列包含查询的所有列。(避免二次访问磁盘)
上述评价的对象是(索引,某种查询). 因此并没有一个索引能够完美适应所有查询.只能说尽量适应当前场景下的高频查询.
索引适用场景:
小表: 直接扫全表即可;
中表: 索引;
大表: 分区。索引开销太大。
作为过滤条件的列需要考察的3个属性,按重要性如下:
查询频率;
相关查询是否是范围查询,或者列的取值是否可枚举。
设计索引的原则:
查询频率高,则应该加入索引;
如果是范围查询,则如果作为联合索引的一列,应该放在后面;(因为最左前缀无法利用范围查询后的列)如果是可枚举列,则查询语句中可以使用In (‘male’,’female’)绕过列顺序限制,让Mysql始终能够应用相关索引;
满足前两个条件后,应该把选择性高的列放在联合索引的前面。
查询优化(详见第六章)优化思路
尽量减少数据量(提前使用limit);
检查响应时间组成;
索引相关优化;
重写分解复杂查询;
如果是日志系统,开启异步写入数据的参数;
检查关联表的顺序;
相关命令123explain extended &sql&;show warnings;
检查是内排还是外排(max_length_for_sort_data控制);
排序优化:关联查询排序时候,尽量把Order by的列提到第一张表中。( 如果Order by的所有列都来自关联的第1个表,Mysql在关联处理第一个表的时候就会进行排序。)
网络传输;(1)缓冲区大小(SQL_BUFFER_RESULT);(2)先释放表锁,再发送结果:SQL_BUFFER_RESULT。
范围查询转化成枚举,充分利用索引;如索引为(dt,city),查询为:
1234567select dt,city from twhere dt&='' and dt&=''and city='beijing'select dt,city from twhere dt in ('','')and city='beijing'
延迟关联+覆盖索引;
检查explain的type:
12345678910111213const(system): 根据PRI或Unique key,只取出确定的(一行)数据,常量优化. eq_ref: JOIN条件包括了所有索引,并且索引是Unique key. ref: JOIN条件包括部分索引或不是Unique key.ref_or_null: WHERE col=exp or col is nullindex_merge: Where id=xx or userid=xxx. 索引合并优化.unique_subquery: where col in( select id from xxx). 这里id唯一.index_subquery: 同上,只是id不唯一.range: 索引在某个范围内.all: 扫全表
检查索引字段是否正确转换了类型:
1dt = date_format(date_sub(current_date, interval 1 day), '%Y-%m-%d')
查询中锁的使用
尽量使用读锁,避免使用写锁;
降低写锁使用时间(先抢注后处理+定时清理占用标志),消除select ... for update的使用;(避免显式加锁,优先考虑隐式加锁)
Innodb不同索引对应的锁:(1) 主键索引: 排他锁(写锁);(2) 二级索引: 共享锁(读锁)。
前情提要:前面讲了一些场景下针对性的优化。
6.7.5 优化Limit分页
场景偏移量非常大,数据量非常的时候。
例如Limit 10000,20.
工作原理:Mysql查询10020条记录,然后返回最后20条。前面1W条都被丢弃。
解决方案:
限制分页的数量;(不让查这么大偏移量)
使用覆盖索引优化性能。(延迟关联)
记录上次分页的id。(利用索引)
方案2(延迟关联)其中方案2,使用覆盖索引优化的原理:
使用覆盖索引,返回目标行的id;
再使用关联查询,返回目标行的所有列。
方案2在偏移量很大的时候,性能优化明显。
方案3(记录上次分页id)这个方案在多次顺序分页的时候性能很好。比如上次分页查询到了id为10240的地方,记录这个值(可以在应用层),然后下次的查询就可以是:123select * from twhere id&10240limit 20
6.7.6 SQL_CALC_FOUND_ROWS相关使用limit的时候,如果加上hint:1SQL_CALC_FOUND_ROWS
就可以获得limit之前结果集的行数。
工作原理:Mysql扫描整个结果集,获得行数。如果不用这个hint,则Mysql只扫描limit的行数返回即可,因此这个hint的代价可能非常大。
使用这个hint的可能原因及相关优化:
想知道是否应该显示“下一页”的按钮;解决方案:如果原来是limit 20,则改成limit 21即可,这样就知道需不需要“下一页”了。
应用层进行缓存;
使用Explain结果的行数估算。
6.7.7 优化UNION查询
UNION去重后合并
UNION ALL不去重,直接合并
Mysql执行UNION的原理:创建临时表,UNION的过程逐步填充行。
优化方法:将WHERE,limit,ORDER BY子句置入内部UNION的子句,以提前过滤。
6.7.9 用户自定义变量
示例:12345678set @one :=1;set @min_actor := (select min(id) from actor); set @last_week := current_date-interval 1 week;select * from twhere col &= @last_
使用自定义变量后,无法使用查询缓存;
表名,列名,Limit中无法使用;
仅在当前连接中有用;(如果持久化连接、连接池,则会发生数据竞态,出BUG)
不能定义变量的类型;
优化器可能将变量优化掉,导致结果不可预期;
赋值的顺序和时机不固定,导致结果不可预期;
使用未定义的变量不会产生任何语法错误。(编译期无法判断错误)出错几率很大。
优化排名语句实现行号功能:1234567set @rownum:=0;select id,@rownum := @rownum+1 as rownumfrom actor limit 3;id: id1 id2 id3rownum: 1 2 3
查询并且更新数据原查询:(2条语句)123update t set lastUpdated=now() where id=1;select lastUpdated from t where id=1;
改写后:(2条语句)1234update t set lastUpdated=now() where id=1 and @now:= now();select @now;
统计更新和插入的数量
示例:123456789insert into t (c1,c2) values(4,4),(2,1),(3,1)on duplicate key updatec1=values(c1)+( 0* (@x:= @x+1));select @x;
第一句的返回值能知道更改了n1行;第二句通过@x统计到更新了n2行;因此:更新的行数=n2;插入的行数=n1-n2。
6.8 案例学习6.8.1 使用MYSQL构建一个队列表
tips:无论什么时候,避免Select .. For Update这种用法。原因:会造成事务阻塞。总是有更好的方法替代这个用法。此外,对于索引列,它产生排他行锁;对于非索引列,它产生排他表锁。
相关函数:12select connection_id();
案例:表unsent_emails:
status (未发送,已发送,正在发送)
owner (正在处理的连接id,默认是0)
错误示例:1234567891011Begin;select id from unsent emailswhere owner=0 and status='未发送'limit 10 For Update;update unset_emailsset status='正在发送',owner=Connection_id()where id in (1,2,3);Commit;
select和update之间的间隙时间内,所有相同的查询会阻塞。(所有线程全都在竞争1个排他锁)
改进后:1234567891011121314set autocommit=1;commit;update unset_emailsset status='正在发送',owner=connection_id()where owner=0 and status='unsent'limit 10;set autocommit=0;select id from unsent_emailswhere owner=connect_id() and status='正在发送';
改进后,不必使用select ... for update,避免了事务阻塞。唯一需要注意的时,需要定期检查是否有线程挂了,需要把它原来抢注的任务重置为无人拥有。
此外,消息队列还是别用Mysql实现了,用MQ,redis之类的吧。XD
6.8.2 计算两点之间的距离
Tip:复杂地理信息计算建议使用PostgreSql.
案例:某个点附近所有可以出租的房子。(一定半径内的所有点)(或社交网站中匹配附近的用户)
表Locations:
A点和B点之间的距离计算公式CAL:1234Acos(
cos(latA)*cos(latB)*cos(lonA-lonB)
+sin(latA)*sin(latB))* 地球的半径
原查询:12select * from locationswhere 地球的半径* CAL &= 100;
上述查询无法使用索引,且非常消耗CPU。
优化方法:降低精度;修改上述的椭圆计算公式,近似成立方体公式。
- 优化后:123select * from locationswhere lat between 38.03-degrees(0.0253) and 38.03 + degree(0.0253)and lon between -78.48 - degrees(0.0253) and -78.48 + degrees(0.0253);
上述查询可以利用索引(lat)或(lon),但由于都是范围查询,无法利用联合索引。
为了进一步优化范围查询,可以使用枚举优化(使用IN列表)。由于lat和lon列其实不是离散值,可以手动创建离散列:新增两列存储坐标的近似值Floor(),然后在查询中使用IN将所有点的整数值放在列表中:
Lat_floor: floor(lat)
Lon_floor: floor(lon)
Key(Lon_floor,Lat_floor)
- 进一步优化:123456select * from locationswhere lat between 38.03-degrees(0.0253) and 38.03 + degree(0.0253)and lon between -78.48 - degrees(0.0253) and -78.48 + degrees(0.0253)and lat_floor in (36,37,38,39,40)and lon_floor in (-80,-79,-78,-77);
上述优化可以利用索引(lat_floor,lon_floor),因此比前面更快。
精度恢复:上述优化只是滤掉不适合的点,可以进一步使用原来的公式进行过滤。由于此时剩下的点不多,因此即使原来的公式很复杂,性能也能够接受。
6.8.3 用户自定义函数(UDF)书里说的比较含糊,总之是写一个C/C++程序作为后台程序,接受某种网络通信协议。(TODO,进一步搜索别人的实际案例。)
6.9 总结查询优化的思路:
检查执行的时间消耗;
检查Schema设计;
检查索引设计;(三星系统)
检查Explain计划(是否利用到索引,范围索引转化为枚举)
其他的各种优化:(1) limit优化;(2) 覆盖索引+延迟关联;(3) 复杂查询分解;(4) 执行计划搜索提前终止;(5) 排序优化(1次传输,2次传输),Order By的列提取到第一张表;(6) UNION优化:手动下推谓词;(7) Count优化:反向过滤, 利用MyISam的统计信息;(8) Delayed优化:日志系统异步写入;(9) 排他锁优化:避免使用Select ... for update,尽早释放锁(先抢注,释放锁,后处理)。
前情提要:SQL-hash(SQL)查询缓存-&SQL-语法解析器--&解析树-预处理--&解析树(合法,有权限)-优化器--&最优执行计划-&接下来就要查询执行引擎了:
6.4.4 查询执行引擎
工作原理查询执行引擎根据执行计划,访问存储引擎的相关API。(Handler Api)查询中每张表由一个Handler实例表示,抽象了底层存储引擎的具体实现。
6.4.5 返回结果给客户端采用TCP传输结果给客户端,为了避免服务器存储太多的结果,每生成一部分结果,就可以向客户端逐步返回结果集。相关参数:12SQL_BUFFER_RESULT-- 结果集的缓冲区
此外,查询的结果如果可以缓存,也会缓存下来。
6.5 优化器的局限性6.5.1 关联子查询
案例123456Select * from filmwhere id in (
select film_id
from film_actor
where actor_id=1);
优化器改写后:123456select * from filmwhere exists(
select * from film_actor
where actor_id=1
and film_id=film.id);
通过explain命令,可以看到执行流程是:
对film进行全表扫描,得到所有id;
使用id逐个排查film_actor表。
缺点对于film表很大的时候,性能很差。
12345select film.*from filmjoin film_actoron id=film_idwhere actor_id=1;
这个查询优化后,会使用较小的表作为驱动表,因此可以回避上述缺点。
解决方案2123456select * from filmwhere id in( select group_concat(film_id)from film_actorwhere actor_id=1);
这个查询优化后,会先对film_actor进行处理,生成IN列表后,会先排序,然后二分查找。仅当film很大,而film_actor表很小的时候效率比原来的方案高。(换言之,不一定更高)
6.5.2 UNION的限制外层的limit20无法提前传入内层,因此可以自己手动在内层先Limit一下,减少数据量。12345678(select * from t1limit 20)UNION ALL(select * from t2limit 20)limit 20
以下是几个不支持的特性:
6.5.5 Mysql无法并行执行Mysql无法利用多核。
6.5.6 哈希关联Mysql的关联都是嵌套关联,不支持哈希关联。不过可以通过建立哈希索引来曲线实现哈希关联。Memory存储的索引都是哈希索引。
6.5.7 松散索引扫描例如当索引为(a,b),而查询条件中只有b的时候,由于不是最左前缀索引,因此无法利用索引,Mysql只能进行全表扫描。
如果有松散索引扫描:先扫描a列第一个值对应的b列的范围;再扫描a列第二个值对应的b列的范围;以此类推,就能少扫描很多数据行。(类似一个对a列值进行枚举的过程)
可惜Mysql还不支持松散索引扫描。
6.5.8 最大值和最小值优化
案例12select min(id) from actorwhere first_name=&PENELOPE&
其中first_name列没有索引,而id是主键列。
因此Mysql的处理方法是:
进行全表扫描获取符合where的数据行;
将上述数据中的id取min.
优化方案由于数据是按主键顺序排列的,因此全表扫描的时候,顺序IO,顺序扫描,遇到的主键也是递增的。因此扫描时遇到的第一个符合条件的数据行,它的主键值一定是最小的。
优化后的SQL
123select id from actoruse index(primary)where first_name=&PENELOPE& limit 1;
优点:快。
缺点:可读性差。依赖于底层数据的物理组织。没人能看懂这里是要取最小值。
6.5.9 同一张表同时查询和更新Mysql无法运行的SQL:12345update t AS out_tset cnt= (
select count(1) from t as in_t
where in_t.type=out_t.type);
上述SQL的语意是将表中相似行(type相同)的数量,记录到所有行的cnt列中。
改写后可以运行:1234567update t join
(select type,count(1) as cnt
group by type
) AS tmp using (type)set t.cnt=tmp.cnt
上述SQL使用生成表(临时表)的形式绕过了同时进行查询和更新的限制,查询的是临时表,更改的是原表。
上述是Mysql不支持的特性。
6.6 查询优化器的提示(hint)这些提示随版本可能会变化。目前可以使用的一些提示如下:
High_priority/Low_priority(作用不大,只对使用表锁的引擎有效)多个语句同时访问某一个表时,哪些语句的优先级高。原理:等待某个表锁的语句组成等待队列。这个提示可以控制等待队列中的排列顺序。
Delayed(有用)对于Insert和Replace有效。相应处理流程:
立即回复客户端;
将插入的行数据放入缓冲区;
表空闲时将数据批量写入。
适用场景: 日志系统;缺点:
并不是所有存储引擎都支持;
导致Last_insert_id()无法正常工作;
不知道什么时候数据才真正落地。(有效性不好保证)
Straight_join手动指定表的关联顺序。
SQL_SMALL_RESULT/SQL_BIG_RESULT告诉优化器对于Group by或Distinct查询如何使用临时表及排序。
SQL_SMALL_RESULT:结果集会很小,将结果集放在内存中的索引临时表,以避免排序操作。
SQL_BIG_RESULT:结果集会很大,建议使用磁盘临时表进行排序。(外排)
SQL_BUFFER_RESULT使用这个hint:将查询结果放入一个临时表中,然后尽可能快地释放表锁。
不使用这个hint:查询结果全部发送给客户端后,才释放表锁。
SQL_Cache/Sql_no_cache是否缓存SQL结果。(详见第七章)
SQL_CALC_FOUND_ROWS(不该使用)返回结果中加上结果集的大小。(Limit之前)
For update/Lock in share mode(尽可能避免使用)只对实现了行锁的存储引擎有效。
Use index/ignore index/force index(有用)指定优化器使用或不使用哪些索引。
optimizer_search_depth(有用)优化器穷举执行计划的限度。如果查询长时间处于statistics状态,可以考虑调低此参数。
optimizer_prune_level默认开启。是否根据扫描的行数决定跳过某些执行计划。
上述hint即使有用,也不应滥用,不应假设自己比优化器的选择更优,可以实验验证。应注意到随着版本升级,有些hint可能过时。(优化器变得更聪明)
6.7 优化特定类型的查询6.7.1 count查询优化count(1)等效于count(*)。
利用Myisam特性优化
MyIsam特性:统计信息中记录数据行数量,因此全表count极快。
案例:123select count(1)from twhere id&5;
其中id上有索引。
优化后:1234select
(select count(1) from t )
- count(1)FROM twhere id&=5;
优化前扫描的为&5的所有行;(可能很多)优化后扫描的为&=5的所有行(最多5行)。
应当注意到上述优化只对MyISam引擎有用。
使用近似值使用explain中预估的行数。
缓存、汇总表
6.7.2 优化关联查询
关联键上有索引;
Group by,order by只涉及一张表的列;
6.7.4 优化Group by和DistinctGroup by和Distinct会互相转化。
无法使用索引时,Groupby的两种策略:
用临时表进行分组;(内存排序)
用排序进行分组。(外排)
可以通过hint干预优化器的选择。
关联查询+GroupBy
案例1234select first_name,last_name,count(1)from film_actorjoin actor using (actor_id)group by first_name,last_name
优化后:1234select first_name,last_name,count(1)from film_actorjoin actor using(actor_id)group by actor_id
上述查询group by和select列不同.
相关参数12SQL_MODE=ONLY_FULL_GROUP_BY这种取值时,这类查询会直接返回错误。
上述优化之所以能更优,是因为:
ID唯一决定了姓名,而且姓名唯一;
ID列分组效率高,而且所在的表小。
取消GROUP BY排序:使用ORDER BY NULL,否则结果会按分组的字段进行排序。

我要回帖

更多关于 笔记本电脑吃鸡配置 的文章

 

随机推荐