Silent Majority


  • 首页

  • 关于

  • 归档

数据库面试相关问题汇总

发表于 2018-04-14

ACID

  • Atomicity 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • Isolation 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

隔离级别

  • 未提交读 Read Commited:可能发生脏读(一个事务读取到了另一个事务未提交的数据修改)
  • 提交读 Read Commited:可能发生不可重复读(一个事务内因为另一个事务的修改两次读取的数据不同)
  • 可重复读 Repeated Read:可能发生幻读(一个事务发现了另一个事务的插入数据)
  • 串行化 Serializable
隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 不可重复读:update&delete
  • 幻读:insert

锁

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
  • 提交读:数据读取不加锁,数据的写入、修改和删除是需要加锁。事务对操作的数据(写入、修改和删除)加行锁,另一个事务无法拿到行锁直至超时
  • 可重复读:事务第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据。但无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现之前没有的数据,这就是幻读。
  • 乐观锁:基于数据版本记录机制实现。在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
  • 悲观锁:为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
  • MVCC:数据添加事务的版本号,操作时检查版本号

引擎

  • MyISAM
    • 不支持事务
    • 只支持表锁
    • 保存表的总行数
    • 大量SELECT性能更佳
  • InnoDB
    • 支持事务、外键
    • 支持表锁、行锁(Where主键有效)
    • 不保存表的总行数
    • 大量INSERT或者UPDATE性能更佳

索引

索引增加了数据库存储空间,插入修改时花费更多时间修改索引

  • 聚集索引:索引的逻辑顺序与数据库物理顺序相同,聚集索引查找到需要的数据
  • 非聚集索引:索引的逻辑顺序与数据库物理顺序不相同,非聚集索引查找到记录对应的主键
  • B树或者B+树实现
    • 查找次数少
    • 利用磁盘读取特性

HashMap及ConcurrentHashMap源码分析

发表于 2018-04-12

HashMap

继承 接口

1
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

实现了Map的接口

常数

1
2
3
4
5
6
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
  • default initial capacity必须是2的幂次
  • bin中元素数量默认超过8时从链表转换成红黑树

内部类

1
2
3
4
5
6
7
8
9
10
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
......
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
}

hash和key由final修饰;hashCode值由key和value的hashCode值异或获得。

1
2
3
final class KeySet extends AbstractSet<K> { ... }
final class Values extends AbstractCollection<V> { ... }
final class EntrySet extends AbstractSet<Map.Entry<K,V>> { ... }
1
2
3
4
5
6
7
8
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
....
}

补充红黑树:自平衡二叉查找树,O(logn)时间内查找插入和删除

性质

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

hash

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

计算key的hash值时,16位右位移异或混合即混合高位与低位,加大随机性

Fields

1
2
3
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;

table被allocate时,长度总是2的幂次

构造函数

1
2
3
public HashMap(int initialCapacity, float loadFactor) { ...}
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR;}

构造函数可传入参数有初始大小和负载因子,默认值分别为16和0.75。

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

table不为null,长度大于0且Node映射所在bin不为null时进行操作:

get(key)返回null值并不一定代表map不包含key的映射,也可能有key对于null的映射

使用(n-1)&hash计算到key映射的bin的下标(n-1为全1,与后即可对应到bin的位置),first指向bin中第一结点并比较key是否相等;不相等则依次比较next结点直到链表尾部,比较时需先判断时红黑树结点,相等即返回指向结点;比较都不相等则返回null

put

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
37
38
39
40
41
42
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

table为null或者table长度为0时先扩容resize

Node映射所在bin为null时,直接新建赋值新的Node;否则则bin的第一个结点存在

当第一个结点存在且key相同时,将value替换为新Node的value,并返回旧的value;当第一个结点存在且key不同且结点为红黑树的结点时,put为红黑树结点;当第一个结点存在且key相同且不为红黑树结点时,遍历链表上结点并比较key,同时记录链表上结点数量;当链表中某一结点key相同时,记录下该结点并跳出循环替换value;当遍历至链表尾部,以新的结点插入;如果结点数量超过树化阈值时,转化为红黑树

resize

table为null时,初始化为DEFAULT_INITIAL_CAPACITY大小;否则,扩容为2倍大小,每个bin中的元素要么仍然位于原来的位置,要么在新的table中以2的幂次位移进行移动

ConcurrentHashMap

继承 接口

1
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable

常数

基本同HashMap

内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
...
}
static final class TreeBin<K,V> extends Node<K,V> { ... }

hash和key由final修饰,val和next由volatile修饰

Spread

1
2
3
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}

访问table

1
2
3
4
5
6
7
8
9
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static 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);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

volatile访问table[i]

CAS更新table[i]的首结点

volatile设置table[i]

构造函数

1
2
3
4
5
public ConcurrentHashMap() { }
public ConcurrentHashMap(int initialCapacity) { ... }
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
} else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}

使用spread计算key的hash值

当table不为null,长度大于0且元素所在table处不为null时继续操作:

当第一个结点key相同时,返回value;否则遍历链表,key相同时返回value;直至遍历至链表尾部,返回null

put

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
} else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

ConcurrentHashMap不允许key或者value为null

table为null或者table长度为0时初始化table

通过volatile访问到元素所在table的bin处若为null,则CAS更新table处bin为新的Node,bin为空时不需要加锁;

元素所在bin不为null时,对第一个元素进行synchronized操作,并再次volatile访问确认第一个结点是否发生了变化。遍历链表,如果key相同则替换value值并跳出遍历;直至遍历至链表尾部,插入新的Node;如果第一个结点是红黑树结点,则以红黑树结点插入。遍历链表时记录结点个数,如果大于TREEIFY_THRESHOLD则转换成红黑树。

计算机网络面试相关问题汇总

发表于 2018-04-11

七层OSI

  1. 物理层
  2. 数据链路层(ARP)
  3. 网络层(IP ICMP)
  4. 传输层(TCP UDP)
  5. 会话层 (SSL TLS)
  6. 表示层
  7. 应用层(HTTP FTP SMTP POP3 DNS)

三次握手&四次挥手

三次握手

client -> server : SYN=1, seq=x (client : SYN_SENT)

server -> client : ACK=1, ack=x+1, seq=y (server : SYN_REV)

client->server : ACK=1, ack=y+1 (client : ESTABLISHED)

四次挥手

client -> server : FIN, seq=x (client : FIN_WAIT_1)

server -> client : ACK, ack=x+1 (server: CLOSE_WAIT, client : FIN_WAIT_2)

server -> client : FIN, seq=y (server : LAST_ACK, client : TIME_WAIT)

client -> server : ACK, ack=y+1 (client : CLOSED)

TCP vs UDP

都位于传输层

TCP

  • 基于连接,可靠
  • 系统资源要求多
  • 流量控制、堵塞控制、重传

UDP

  • 无连接,不可靠
  • 系统资源要求少

session vs cookie

HTTP是无状态的协议,服务端需要记录用户的状态,采用某种机制来标记识别用户,使用session跟踪会话。

session

  • 服务器端
  • 保存在集群、数据库、文件中

cookie

  • 客户端
  • 保存用户名与密码

第一次创建Session时,服务端会在HTTP协议中告诉客户端需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器。

HTTPS

https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%AE%89%E5%85%A8%E5%8D%8F%E8%AE%AE

经由HTTP进行通信,但利用SSL/TLS来加密数据包

主要目的:提供对网站服务器的身份认证,保护数据的隐私与完整性

  • HTTP:默认端口80
  • HTTPS:默认端口443

《图解HTTP》

HTTP+加密+认证+完整性保护=HTTPS

HTTPS采用混合加密机制

  1. 使用公开密钥加密方式安全地交换在稍后的共享密钥加密中要使用的密钥
  2. 确保交换的密钥是安全的前提下,使用共享密钥加密方式进行通信
  • 共享密钥加密(对称密钥加密):加密和解密通用一个密钥
  • 公开密钥加密(非对称密钥加密):使用一堆非对称的密钥(私有密钥、公开密钥),发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密

HTTPS的安全通信机制

  1. 客户端发送报文开始SSL通信,报文中包含客户端支持的SSL指定版本,加密组件(加密算法及密钥长度等)
  2. 服务器端以报文作为应答,报文包含SSL版本以及加密组件;器发送证书报文,包含公开密钥证书;发送结束报文,SSL握手协商部分结束
  3. 客户端以报文作为回应,包括随机密码串,已用证书中公开密钥加密;发送报文提示服务器之后通信采用密钥加密;发送结束报文,包含连接至今全部报文的整体校验值
  4. 服务器端发送报文提示采用密钥加密;发送结束报文
  5. SSL连接建立完成

服务器与客户端协商加密组件 -> 服务器发送公开密钥证书 -> 客户端确认证书有效性,取出公开密钥 -> 客户端生成随机数,使用公开密钥对随机数进行加密处理,发送已加密的随机数 ->服务器用私有密钥解密,生成随机数

HTTP状态码

  • 2XX 成功
    • 200 OK、
    • 204 No Content
  • 3XX 重定向
  • 4XX 客户端错误
    • 400 Bad Request
    • 401 Unauthorized
    • 403 Forbidden
    • 404 Not found
  • 5XX 服务器错误
    • 500 Internal Server Error
    • 503 Service Unavailable

HTTP协议

HTTP是一种不保存状态,即无状态协议

HTTP请求报文

  • 请求行:请求方法 请求URL 协议版本
  • 请求首部字段(可选)
  • 内容实体

HTTP响应报文

  • 响应行:协议版本 状态码 状态码的原因短语
  • 响应首部字段(可选)
  • 实体主体

HTTP方法

  • GET:获取资源
  • POST:传输实体主体
  • PUT:传输文件
  • HEAD:获得报文首部
  • DELETE:删除文件
  • OPTION:询问支持的方法
  • TRACE:追踪路径
  • CONNECT:要求用隧道协议连接代理

HTTP/2.0

改善用户在使用Web时的速度体验

  • 多路复用允许同时通过难过单一的HTTP/2.0连接发起多重的请求响应消息

操作系统面试相关问题汇总

发表于 2018-04-11

进程VS线程

进程

  • 一个程序至少有一个进程
  • 私有地址空间
  • 进程间切换更耗时
  • 操作系统进行资源分配的最小单元

线程

  • 一个进程至少有一个线程,线程是轻量级的进程
  • 共享地址空间
  • 线程间切换不如进程间切换耗时
  • CPU调度的基本单位

进程间通信

https://zh.wikipedia.org/wiki/%E8%A1%8C%E7%A8%8B%E9%96%93%E9%80%9A%E8%A8%8A

https://www.ibm.com/developerworks/cn/linux/l-ipc/

  • 管道:具有亲缘关系进程间通信
  • 有名管道:允许无亲缘间关系进程间通信
  • 信号
  • 消息队列
  • 共享内存
  • 信号量
  • 套接字

进程同步

  • 信号量
  • 自旋锁
  • 栅栏

线程同步

  • 互斥量
  • 信号量
  • 条件变量
  • 读写锁

死锁

《现代操作系统》 P246

四个条件

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它地进程显式地释放
  • 环路等待:死锁发生时,系统中一定有由两个或两个以上地进程组成地一条环路,该环路中地每个进程都在等待着下一个进程所占有的资源

生产者消费者

使用Object的wait()和notify()

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 ProducerConsumer {
private Queue<Integer> queue = new LinkedList<Integer>();
private final static int SIZE = 3;
class Producer implements Runnable {
@Override
public void run() {
synchronized (queue) {
while (queue.size() == SIZE) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(new Random().nextInt(100));
queue.notifyAll();
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
queue.notifyAll();
}
}
}
}

使用Lock的Condition

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
37
38
39
40
41
42
public class ProducerConsumer {
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
private Queue<Integer> queue = new LinkedList<Integer>();
private final static int SIZE = 3;
class Producer implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (queue.size() == SIZE) {
notFull.await();
}
queue.add(new Random().nextInt(100));
notEmpty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
queue.poll();
notFull.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

缓存

程序需要第k+1层某个数据对象d时:

  • 缓存命中:d刚好缓存在第k层中
  • 缓存不命中:第k层中没有缓存数据对象d

计算机系统每个存储器地址有m位,形成M=2^m个不同的地址。组织成S=2^s个告诉缓存组的数组,每个组包含E个高速缓存行,每个行是由一个B=2^b字节的数据块组成的,一个有效位指明这个行是否包含有意义的 信息,还有t=m-(b+s)个标记位唯一地标识存储在这个高速缓存行中的块。

地址:t位(标记)+ s位(组索引)+ b位(块偏移)

  • write through:立即将w的高速缓存块写回到紧接着的低一层中
  • write back:只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层

虚拟内存

页表

页表将虚拟页映射到物理页,每次地址翻译时硬件将一个虚拟地址转换成物理地址时,都会读取页表。

页表就是一个页表条目(PTE)的数组

地址翻译

虚拟地址:虚拟页号(VPN)+ 虚拟页偏移量(VPO)

VPN作为到页表中的索引,获得物理页号;虚拟页偏移量 -> 物理页偏移量

物理地址:物理页号(PPN)+ 物理页偏移量(PPO)

  1. 处理器生成一个虚拟地址,并把它传送给MMU(Memory Management Unit)
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它
  3. 高速缓存/主存向MMU返回PTE
  4. MMU构造物理地址,并把它传送给高速缓存/主存
  5. 高速缓存/主存返回所请求的数据字给处理器

TLB加速地址翻译

  1. CPU产生一个虚拟地址
    1. MMU从TLB中取出相应的PTE
    2. MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
    3. 高速缓存/主存返回所请求的数据字给CPU

Kafka权威指南 笔记

发表于 2018-03-18

实习的这两个多月基于Kafka开发系统,主要学习参考了Kafka权威指南这本书籍。正好也是第一次看原版的技术书籍,针对最重要的几个章节记录一下笔记。

Chapter 3 Kafka Producers

Overview

通过创建一个ProducerRecord向Kafka发布消息,必须包括topic和value,可选指明key和/或者partition。

  1. 发布者会先序列化key和value对象成ByteArrays以便在网络上传递。
  2. 数据被传递给partitioner。如果指定了partition,partitioner不会有任何操作并返回指定的partition。如果没有指定,partition会基于key选择一个partition。一旦选择了partition后,producer将会知道record发往的topic和partition,然后将在一批发往同一topic和partition的records中加入record。一个独立的线程负责把这些成批的records发往合适的Kafka集群。
  3. 集群收到消息,将会返回一个响应。如果消息成功写入Kafka,它会返回一个包含topic,partition和offset的RecordMetadata对象。如果消息写入失败,它会返回一个error。当producer收到一个error,它可能会在放弃返回error前重试几次发送消息。

Constructing a Kafka Producer

  • bootstrap.servers
  • key.serializer
  • value.serializer
1
2
3
4
5
private Properties kafkaProps = new Properties();
kafkaProps.put("bootstrap.servers", "broker1:9092,broker2:9092");
kafkaProps.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<String, String>(kafkaProps);

创建了producer后,开始发送消息。主要有三种发送消息的方法

  • Fire-and-forget

    发送消息到服务器并不在意是否成功到达,一些消息可能丢失

  • Synchronous send

    send()方法返回一个Future对象,使用get()方法等待future查看send是否成功

  • Asynchronous send

    与send()一起使用callback函数,当收到Kafka集群响应后被出发

Sending a Message to Kafka

1
2
3
4
5
6
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "Precision Products", "France");
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}

Sending a Message Synchronously

1
2
3
4
5
6
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "Precision Products", "France");
try {
producer.send(record).get();
} catch (Exception e) {
e.printStackTrace();
}

Sending a Message Asynchronously

同步发送消息可能导致延迟,同时我们需要知道是否成功

1
2
3
4
5
6
7
8
9
10
private class DemoProducerCallback implements Callback {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
e.printStackTrace();
}
}
}
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "Biomedical Materials", "USA");
producer.send(record, new DemoProducerCallback());

Configuring Producers

  • acks

    • ack=0:producer不会等待broker的响应,高吞吐率但不能得知消息是否丢失
    • ack=1:producer在leader replica收到消息时得到来自broker的成功响应。如果消息不能被写入leader,producer会收到error响应并重试发送消息避免消息丢失。
    • ack=all:producer在所有in-sync replica收到消息时得到来自broker的成功响应,延迟高
  • buffer.memory

    设置producer用于缓存等待发往broker消息的缓存大小

  • compression.type

    默认消息未被压缩发送

  • retries

    放弃并通知问题前重试发送消息的次数

  • batch.size

    当多个records被发往同一partition,producer将会分批发送。控制用于每一批消息内存大小的bytes数量。批次满时,批次内消息会被发送,但这未必意味着producer会等待批次满再发送。

  • linger.ms

    发送当前批次消息前等待额外消息的时间。KafkaProducer在当前批次消息满或者到达linger.ms限制时发送一批消息。默认在一有可用发送消息线程时就发送消息。

  • max.request.size

    producer发送请求的大小——最大可发送消息的大小和一次请求中可以发送消息的数量

Partitions

只使用topic和value创建ProducerRecord时,key被默认设置未null。Key有两个作用:与消息一起存储的额外信息并用于决定信息被写往的topic partition。所有相同key的消息将到相同的partition中,意味着如果一个进程只读取topic partitions的一个子集时,所有单个key的记录将被同个进程读取。

1
ProducerRecord<Integer, String> record = new ProducerRecord<>("CustomerCountry", "Laboratory Equipment", "USA");

当key为null,将会使用默认的partitioner,记录会被随机发送可用的partition之一。轮询算法被用于partitions之间消息的平衡。当key存在且默认的partitioner被使用时,Kafka使用自己的hash算法对key进行hash,使用结果映射消息到特定的partition。因为一个key总是映射到同一partition,使用topic中所有partition计算映射,而不是可用的partitions。只有当topic的partition数量不变时,keys到partitions的映射是不变的。当topic中加入新的partition,新的记录会被写往不同的partition中。

Chapter 4 Kafka Consumers

Kafka Consumer Concepts

Consumers and Consumer Groups

Kafka consumers是consumer group的一部分。当同一consumer group中的多个consumers订阅了一个topic时,组内的每一个consumer将会收到topic不同partition子集的消息。

  • Partition Num > Consumer Num (1)

    consumer会收到所有partition的消息

  • Partition Num > Consumer Num (> 1)

    每一个consumer收到不同的partition子集的消息

  • Partition Num = Consumer Num

    每一个consumer收到一个parition的消息

  • Partition Num < Consumer Num

    部分consume收不到消息

  • consumer group num > 1

    每一个组会单独收到partition所有的消息,组内负载均衡

Consumer Groups and Partition Rebalance

组内添加一个新的consumer或者一个consumer关闭或者崩溃时,会开始消费其他consumer之前消费的partition。从一个consumer移动partition的归属到另一个consumer叫做再平衡。再平衡时整个consumer group会暂时不可用。Consumer通过发送心跳到Kafka称为group coordinator的broker保持在一个consumer group的成员归属以及partition的归属。只要consumer以规律的时间间隔发送心跳,它就被认为是存活的并从partition处理消息。Consumer poll时发送消息,已经消费了就commit record。一旦consumer足够长时间内停止发送心跳,session就会超时,group coordinator就会认为它死亡并出发再平衡。新版本中有独立的心跳线程再poll之间发送心跳,允许分离心跳频率和poll的频率。

Creating a Kafka Consumer

  • bootstrap.servers
  • key.deserializer
  • value.deserializer
  • group.id 非必需
1
2
3
4
5
6
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
props.put("group.id", "CountryCounter");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);

Subscribing to Topics

1
2
consumer.subscribe(Collections.singletonList("customerCountries"));
consumer.subscribe("test.*");

The Poll Loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
log.debug("topic = %s, partition = %s, offset = %d,customer = %s, country = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
int updatedCount = 1;
if (custCountryMap.countainsValue(record.value())) {
updatedCount = custCountryMap.get(record.value()) + 1;
}
custCountryMap.put(record.value(), updatedCount)
JSONObject json = new JSONObject(custCountryMap);
System.out.println(json.toString(4))
}
}
} finally {
consumer.close();
}

第一次使用新的consumer调用poll()时,负责找到GroupCoordinator,加入consumer group,收到partition的分配。

Thread safe

One consumer per thread is the rule.

Configuring Consumers

  • fetch.min.bytes

    获取records时希望从broker获取的数据的最小量。如果新的records比min.fetch.bytes的byte数少时,broker会再发送records返回给consumer前等待更多可用的信息

  • fetch.max.wait.ms

    默认Kafka等待500ms。

  • max.partition.fetch.bytes

    服务器每个partition返回的最大bytes数量

  • session.timeout.ms

  • auto.offset.reset

  • enable.auto.commit

  • partition.assignment.strategy 随机/轮询

  • max.poll.records

    单次调用poll()返回的最大records数量

  • receive.buffer.bytes and send.buffer.bytes

Commits and Offsets

我们称一个partition中更新当前位置的动作为commit。消费者生产一条带着每个partition的committed offset消息至Kafka__consumer_offsets的topic。如果一个消费者崩溃或者新的消费者加入了消费者组,将会触发rebalance。rebalance后,每个消费者可能被分配到新的一组partition,为了知道从哪里开始工作,消费者会从那里读取最新的每个parition的committed offset。

如果committed offset比起消费者处理的最后一条消息offset小,committed offset和最后处理的offset间的消息会被处理两次;

如果committed offset比起消费者实际处理的最后一条消息offset大,committed offset和最后处理的offset间的消息会被消费者组丢失。

Automatic Commit

如果配置了enable.auto.commit=true,每5秒钟消费者将会commit从poll()中收到的最大的offset。5秒钟的间隔是默认值并由auto.commit.interval.ms设置。自动commit由poll循环驱动。无论何时poll时,消费者检查是否是时间commit,如果是则commit上一次poll时返回的offset。

可以通过配置commit间隔使得更加频繁地commit并减少records重复的窗口,但不可能消除。通过使能自动commit,一次poll的调用总是返回上一次poll的最后一个offset,所以再次调用poll之前处理poll返回的所有event十分重要。

Commit Current Offset

设置auto.commit.offset=false,offsets只有在应用显式commit时才会被commit。最简单可靠的API是commitSync(),这个API会commit由poll()返回的最新的offset,一旦offset被commit就返回,如果commit由于某些原因失败了就抛出异常。

1
2
3
4
5
6
7
8
9
10
11
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
try {
consumer.commitSync();
} catch (CommitFailedException e) {
log.error("commit failed", e)
}
}

一旦处理完当前批次的records,在poll更多消息前调用commitSync()commit批次中最后一个offset。

Asynchronous Commit

1
2
3
4
5
6
7
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
consumer.commitAsync();
}

commitAsync()将会重试commit直到要么成功要么遇到不可重试的失败。原因是commitAsync()收到来自服务器的响应时,可能有更晚一些的commit已经成功。

1
2
3
4
5
6
7
8
9
10
11
12
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
consumer.commitAsync(new OffsetCommitCallback() {
public void onComplete(Map<TopicPartition,OffsetAndMetadata> offsets, Exception exception) {
if (e != null)
log.error("Commit failed for offsets {}", offsets, e);
}
});
}

保证异步重试commit顺序正确的简单策略时使用递增序列数字。每次commit时增加序列号。当准备发送重试消息时,检查commit序列号是否等于实例变量。如果相等,没有更新的commit,重试是安全的。如果实例序列号更大则不要重试,因为更新的commit已经被发送。

Combining Synchronous and Asynchronous Commits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
consumer.commitAsync();
}
} catch (Exception e) {
log.error("Unexpected error", e);
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}

Commit Specified Offset

消费者API允许通过传递partition和希望commit的offset的映射调用commitSync()和commitAsync()。

1
2
3
4
5
6
7
8
9
10
11
12
13
private Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
int count = 0;
....
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d, customer = %s, country = %s\n", record.topic(), record.partition(), record.offset(), record.key(), record.value());
currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new
OffsetAndMetadata(record.offset()+1, "no metadata"));
if (count % 1000 == 0) consumer.commitAsync(currentOffsets, null);
count++;
}
}

读取每个record后,通过我们希望处理的下一个offset更新offset的映射,这是下次开始时开始读取的地方。

Consuming Records with Specific Offsets

如果想从partition的开始开始读取所有消息或者跳过所有消息到partition的尾开始只消费新的消息,使用seekToBeginning(TopicPartition tp)和seekToEnd(TopicPartition tp)。

或者使用seek()指定哪里开始读取。

实战Java虚拟机 JVM故障诊断与性能优化 笔记

发表于 2017-10-08

第4章 垃圾回收概念与算法

认识垃圾回收

GC中的垃圾特指存在于内存中的、不会在被使用的对象。

常用的垃圾回收算法

引用计数法

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能在被使用。

问题

  • 无法处理循环引用的情况
  • 每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

标记清除法 Mark-Sweep

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段

  • 标记阶段:首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。
  • 清除阶段:清除所有为被标记的对象

可能产生的最大问题是空间碎片,回收后的空间不不连续的

复制算法

将原有的空间内存分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存块的角色,完成垃圾回收。

可确保回收后的内存空间是没有碎片的,代价是将系统内存折半。

在Java新生代串行垃圾回收器重,使用了复制算法的思想。新生代分为eden空间,from空间和to空间三个部分。其中from和to空间可以视为用于复制的两块大小相同、地位相等、且可进行角色互换的空间块。from和to空间也成为survivor空间,用于存放未被回收的对象。

在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间(假设是to)中,正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中。(大对象,或者老年对象会直接进入老年代,如果to空间已满,则对象也会直接进入老年代)。此时eden空间和from空间中的剩余对象就是垃圾对象,可以直接清空。to空间则存放此次回收后的存活对象。

标记压缩法(标记整理法)

复制算法的高效是建立在存活对象少,垃圾对象多的前提下的。但在老年代,更常见的情况是大部分对象都是存活对象。

标记压缩算法是一种老年代的回收算法,它在标记清除算法的基础上做了一些优化。首先需要从根节点开始,对所有可达对象做一次标记,将所有的存活对象压缩至内存的一端。之后清除边界外的所有空间。这种方法避免了碎片的产生,又不需要两块相同的内存空间。

分代算法

新生代比较适合使用复制算法。当一个对象经过几次回收后仍然存活,对象就会被放入老年代的内存空间。对老年带的回收使用标记压缩算法或者标记清除算法。

判断可触及性

引用和可触及性的强度

强引用

程序中一般使用的引用类型

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常也不会回收强引用所指向对象
  • 强引用可能导致内存泄漏

软引用

一个对象只持有软引用,那么当堆空间不足时,就会被回收。

弱引用

在系统GC时,只要发现弱引用,不管系统对空间使用情况如何,都会将对象进行回收。

虚引用

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

第5章 垃圾收集器和内存分配

串行回收器

串行回收期是指使用单线程进行垃圾回收的回收器。

新生代串行回收器

  • 仅仅使用单线程进行垃圾回收
  • 独占式的垃圾回收

Stop the world:在串行回收器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成。

老年代串行回收器

串行的独占式的垃圾回收器

并行回收器

使用多个线程同时进行垃圾回收

新生代ParNew回收器

简单地将串行回收器多线程化

新生代ParallelGC回收器

关注系统的吞吐量

老年代ParallelOldGC回收器

多线程并发的收集器

CMS回收器

主要关注系统停顿时间,并发标记清除

主要工作步骤

  • 初始标记:STW标记根对象
  • 并发标记:标记所有对象
  • 预清理:清理前准备以及控制停顿时间(可关闭)
  • 重新标记:STW修正并发标记数据
  • 并发清除:清理垃圾
  • 并发重置

由于CMS回收器不是独占式的回收器,在CMS回收过程中,应用程序仍然在不停地工作。在应用程序工作过程中,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。同时由于应用程序没有中断,所以在CMS回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值(默认是68)时会执行一次CMS回收。

CMS是一个基于标记清除算法的回收器,会造成大量内存碎片。

G1回收器

G1回收器是在JDK1.7中正式使用的全新的垃圾回收器。从分代上来看会区分年轻代和老年代,依然有eden区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续,它使用了分区算法。

分区算法

分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。为了更好地控制GC产生地停顿时间,将一块大地内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

G1的内存划分和主要收集过程

G1收集器将堆进行分区,划分为一个个的区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的一次停顿时间。

收集过程

  • 新生代GC
  • 并发标记周期
  • 混合收集
  • 如果需要,可能会进行Full GC

G1的新生代GC

主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。

回收后,所有的eden区都应该被清空,而survivor区会被收集一部分数据,但是应该至少仍然存在一个survivor区。老年代的区域增多,因为部分survivor区或者eden区的对象可能会晋升到老年代。

G1的并发标记周期

  • 初始标记
  • 根区域扫描
  • 并发标记
  • 重新标记
  • 独占清除
  • 并发清除

混合回收

G1优先回收垃圾比例较高的区域。在这个阶段,既会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收。

必要时Full GC

不能完全避免在特别繁忙的场合会出现回收过程中内存不充足的情况,当遇到这种情况时,G1也会转入一个Full GC进行回收。

有关对象内存分配和回收的一些细节问题

禁用System.gc()

1
-XX:-+DisableExplicitGC

对象何时进入老年代

  • 初创的对象在eden区

  • 老年对象进入老年代

    对象的年龄是由对象经历的GC次数决定的。在新生代中的对象每经历一次GC,如果它没有被回收,它的年龄就加1。默认情况下,在新生代的对象最多经历15次GC,就可以晋升到老年代。

  • 大对象进入老年代

在TLAB上分配对象

线程本地分配缓存。TLAB是一个线程专用的内存分配区域,为了加速对象分配而生,本身占用了eden区的空间。在TLAB启用的情况下,虚拟机会为每一个Java线程分配一块TLAB空间。

第6章 性能监控工具

JDK性能监控工具

  • jps:查看Java进程
  • jstat:查看虚拟机运行时信息
  • jinfo:查看虚拟机参数
  • jmap:导出堆到文件
  • jhat:JDK自带的堆分析工具
  • jstatck:查看线程堆栈
  • jstatd:远程主机信息收集
  • jcmd:多功能命令行
  • hprof:性能统计工具

第8章 锁与并发

锁的基本概念和实现

对象头和锁

在Java虚拟机的实现中每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个称为Mark Word的部分,它是实现锁的关键。一个对象是否占用锁,占有哪个锁,就记录在这个Mark Word中。

锁在Java虚拟机中的实现和优化

偏向锁

若某一锁被线程获取后,便进入偏向模式。当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省了操作时间。如果在此之间有其他线程进行了锁请求,则锁退出偏向模式。

轻量级锁

如果偏向锁失败,Java虚拟机会让线程申请轻量级锁。当需要判断某一线程是否持有该对象锁时,只需简单地判断对象头的指针是否在当前线程的栈地址范围内即可。

锁膨胀

当轻量级锁失败,虚拟机就会使用重量级锁。

自旋锁

在锁膨胀后,虚拟机会使用自旋锁。自旋锁可以使线程在没有获得锁时不被挂起,而转而去执行一个空循环。在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。

锁消除

Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不能存在共享资源竞争的锁。

锁在应用层的优化思路

  • 减少锁持有时间
  • 减小锁粒度
  • 锁分离
  • 锁粗化

无锁

CAS

包含三个参数CAS(V, E, N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N。如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。

理解Java内存模型

  • 原子性
  • 有序性
  • 可见性
  • Happens-Before原则

第10章 Class装载系统

Class文件的装载流程

  • 加载
  • 连接
    • 验证
    • 准备
    • 解析
  • 初始化

装载类的条件

一个类或接口在初次使用(主动使用)前,必须要进行初始化。

  • 创建一个类的实例时,使用new关键字,或者通过反射、克隆、反序列化
  • 调用类的静态方法时
  • 使用类或接口的静态字段时(final常量除外)
  • 使用java.lang.reflect包的方法反射类的方法时
  • 初始化子类,要求先初始化父类
  • 作为启动虚拟机,含有main()方法的那个类

加载类

  • 通过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构
  • 创建java.lang.Class类的实例,表示该类型

验证类

保证加载的字节码时合法、合理并符合规范的

  • 格式检查
  • 语义检查
  • 字节码验证
  • 符号引用验证

准备

虚拟机为这个类分配相应的内存空间,并设置初始值。如果类存在常量字段,那么常量字段也会在准备阶段被附上正确的值。

解析类

将类、接口、字段和方法的符号引用转为直接引用。

初始化

执行类的初始化方法,它是由编译器自动生成的,有类静态成员的赋值语句以及static语句块合并产生的。

ClassLoader

类加载

ClassLoader的分类

ClassLoader的层次自定往下

  • 启动类加载器:完全由C代码实现
  • 拓展类加载器
  • 应用类加载器(系统类加载器)
  • 自定义类加载器

当系统需要使用一个类时,在判断类是否已经被加载时,会先从当前底层类加载进行判断。当系统需要加载一个类时,会从顶层类开始加载,以此向下尝试,直至成功。

双亲委托模式

类加载时,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载。在尝试加载时,会先请双亲处理,如果双亲请求失败,则会自己加载。

实战Java高并发程序设计 笔记

发表于 2017-10-06

第1章 走入并行世界

概念

同步和异步

  • 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为;
  • 异步:一旦方法,方法调用就会立即返回,调用者就可以继续后续的操作。如果异步调用需要返回结果,那么当这个异步调用真是完成时,则会通知调用者。

临界区

表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它。一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待。

阻塞和非阻塞

  • 阻塞:一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起。
  • 非阻塞:没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断前向执行。

JMM

原子性

原子性是指一个操作是不可中断的。多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

有序性

程序实行时,可能会进行指令重排。

Happen-Before规则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写先发生于读
  • 锁规则:unlock必然发生在随后的lock前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结
  • 线程的interrupt()先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法

第2章 Java并行程序基础

初始线程:线程的基本操作

新建线程

1
2
3
4
5
6
7
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("Hello");
}
};
t1.start();

注意:调用start()方法,不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

1
2
Thread t1 = new Thread();
t1.start();

考虑Java是单继承的,使用Runnable接口来实现同样的操作。

1
2
3
4
5
6
7
8
9
10
public class MyThread implements Runnable{
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread());
t1.start();
}
@Override
public void run() {
System.out.println("Runnbale");
}
}

终止线程

stop()方法不推荐,可能引起数据不一致的问题。Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。

可以使用一个标记变量用于指示线程是否需要退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
volatile boolean flag = false;
public void stop() {
flag = true;
}
@Override
public void run() {
while (true) {
if (flag) {
break;
}
……
}
}

线程中断

线程中断并不会使线程立即推出,而是给线程发送一个通知,告诉目标线程希望退出。目标线程接到通知后如何入理,完全由目标线程自行决定。

Thread.interrupt()方法是一个实例方法,通知目标线程中断,也就是设置中断标志位。

Thread.isInterrupted()方法也是实例方法,判断当前线程是否由被中断(通过检查中断标志位)

Thread.interrupted也是用来判断当前线程的中断状态,同时会清除当前线程的中断标志位状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread() {
@Override
public void run() {
while(true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted");
break;
}
Thread.yield();
}
}
};
t1.start();
t1.sleep(2000);
t1.interrupt();
}

wait和notify

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。

如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程并将其唤醒,这个选择是不公平的,并不是先等待的线程会被优先选择,这个选择完全是随机的。Object对象还有一个类似的notifyAll()方法,它会唤醒在这个等待队列中所有的等待的线程,而不是随机选择一个。

无论是wait()或者notify()都需要首先获得目标对象的一个监视器。

join和yield

线程需要等待依赖线程执行完毕,join()方法会一直阻塞当前线程,直到目标线程执行完毕。

yield()方法会使当前线程让出CPU,还会进行CPU资源的争夺。

守护线程

如果用户线程全部结束,守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。

线程安全与synchronized

关键字synchronized的作用是实现线程间的同步,它的工作是对同步的代码加锁,是的每一次只能有一个线程进入同步块,从而保证线程间的安全性

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
  • 直接作用与静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁

除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。

第3章 JDK并发包

多线程的同步协作:同步控制

synchronized的功能拓展:重入锁

重入锁可以完全替代synchronized关键字,使用java.util.concurrent.locks.ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException{
ReenterLock rl = new ReenterLock();
Thread t1 = new Thread(rl);
Thread t2 = new Thread(rl);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}

使用重入锁保护临界区资源i,确保多线程对i操作的安全性。开发人员必须手动指定何时加锁,何时释放锁,对逻辑控制的灵活性要远远好于synchronized。

“重入”:如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。

除了使用上的灵活性外,重入锁还提供了一些高级功能。

  • 中断响应:对于synchronized来说如果一个线程在等待锁,那么要么它获得这把锁继续执行,要么它就保持等待。使用重入锁,线程可以被中断,也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。

    1
    lock.lockInterruptibly();
  • 锁申请等待超时

    1
    lock.tryLock(5, TimeUnit.SECONDS)

    tryLock()方法接受两个参数,一个表示等待时长,另外一个表示记时单位,表示线程在这个锁请求中最多等待时间,如果超过时间还没有得到锁就会返回false,如果成功获得锁则返回true。

    tryLock()方法也可以不带参数直接运行,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true,如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁。

  • 公平锁

    公平锁按照时间的先后顺序,保证先到者先得,后到者后得。公平锁不会产生饥饿现象。

    构造函数,参数fair为true时表示锁的公平的。

    1
    public ReentrantLock(boolean fair)

    实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下。因此默认情况下,锁是非公平的。根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。

ReentrantLock的几个重要方法

  • lock():获取锁,如果所已经被占用,则等待
  • lockInterruptibly():获得锁,但优先相应中断
  • tryLock():尝试获得锁,如果成功返回true,失败返回false;该方法不等待,立即返回
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁
  • unlock():释放锁

Condition条件

wait()和notify()方法是和synchronized关键字合作使用的,而Condition是与重入锁相关联的。通过Lock接口的new Condition()方法可以生成一个与当前重入锁绑定的Condition实例。

  • await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行
  • signal()方法用于唤醒一个在等待中的线程
  • signalAll()方法会唤醒所有在等待中的线程

信号量

信号量可以指定多个线程同时访问某一个资源。

构造函数

1
2
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
  • acquire()方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断
  • tryAcquire()尝试获得一个许可,如果成功返回true,失败返回false,不会进行等待,立即返回
  • release()用户在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问

ReadWriteLock读写锁

读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁允许多个线程同时读。

  • 读-读不互斥
  • 读-写互斥
  • 写-写互斥

如果在系统中,读操作次数远远大于写操作,则读写锁可以发挥最大的功效

1
2
3
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();

倒计时器 CountDownLatch

CountDownLatch构造函数接受一个整数作为参数,即当前这个计时器的计数个数

1
public CountDownLatch(int count)
1
2
3
CountDownLatch latch = new CountDownLatch(10);
latch.countDown();
latch.await();
  • countDown()方法通知CountDownLatch一个线程已经完成了任务,倒计时器可以减1
  • await()方法要求主线程等待所有任务全部完成后,主线程才能继续执行

循环栅栏 CyclicBarrier

计数器可以反复使用。构造函数中parties表示计数总数,也就是参与的线程总数。barrierAction就是当计数器一次计数完成后,系统会执行的动作

1
public CyclicBarrier(int parties, Runnable barrierAction)

线程复用:线程池

线程的创建与关闭需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的情况,反而会得不偿失。

其次,线程本身也是要占用内存空间的,大量的线程会抢占宝贵的内存资源。

线程池

让创建的线程进行复用。使用线程池后,创建线程变成了从线程池获取空闲线程,关闭线程变成了向线程池归还线程。

JDK对线程池的支持

JDK提供了一套Executor框架,其本质就是一个线程池。ThreadPoolExecutor表示一个线程池。Executors类则扮演线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。

  • newFixedThreadPool()方法:返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,带有线程空闲时,便处理在任务队列中的任务。
  • newSingleThreadExecutor()方法:返回一个只有一个线程的线程池。若多于一个任务被提交到线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
  • newCachedThreadPool()方法:返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • newScheduledThreadPool()方法:返回一个ScheduledExecutorService对象,可以指定线程数量。拓展了在给定时间执行某任务的功能。

核心线程池的内部实现

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
  • corePoolSize:指定了线程池中的线程数量
  • maximumPoolSize:指定了线程池中最大线程数量
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。即超过corePoolSize的空闲线程在多长时间内会被销毁
  • unit:keepAliveTime的单位
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:线程工厂,用于创建线程
  • handler:拒绝策略。当任务太多来不及处理如何拒绝任务

第4章 锁的优化及注意事项

有助于提高锁性能的几点建议

  • 减小锁持有时间
  • 减小锁粒度
  • 读写分离锁来替换独占锁
  • 锁分离
  • 锁粗化

Java虚拟机对锁优化所做的努力

锁偏向

锁偏向是一种针对加锁操作的优化手段。它的核心思想是如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须在做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。因此对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次多次极有可能是同一个线程请求相同的锁。

轻量级锁

如果偏向锁失败,虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。轻量级锁简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁就会膨胀为重量级锁。

自旋锁

锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机会使用自旋锁。系统假设在不久的将来线程可以得到这把锁,因此虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真实地将线程在操作系统层面挂起。

锁消除

Java虚拟机在JIT编译时,通过对运行上下文地扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。

ThreadLocal

ThreadLocal是一个线程的局部变量,也就是说只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。

  • set()方法:首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。
  • get()方法:首先取得当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

无锁

对于并发控制而言,锁是一种悲观的策略。它总是假设每一次临界区操作会发生冲突。如果有多个线程需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。

无所是一种乐观的策略,它会假设对资源的访问是没有冲突的。如果遇到冲突,无锁的策略使用CAS比较交换的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

CAS 比较交换

包含三个参数(V, E, N):V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N;如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。

CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作同一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行适当的处理。

第5章 并行模式与算法

单例模式

单例模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。

1
2
3
4
5
6
7
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
  • 把构造函数设置为private,保证系统中不会有人意外构建多余的实例
  • instance对象必须是private并且static的
  • 不足:实例在什么时候创建是不受控制的。对于静态成员instance,它会在类第一次初始化的时候被创建

一种支持延迟加载的策略

1
2
3
4
5
6
7
8
9
public class LazySingleton {
private LazySingleton() {}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if (instance == null)
instance = new LazySingleton();
return instance;
}
}

补充:双重检查模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null)
instance = new DCLSingleton();
}
}
return instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
public class StaticSingleton {
private StaticSingleton() {}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
  • getInstance()方法中没有锁,使得高并发环境下性能优越
  • 只有在getInstance()方法被第一次调用时,StaticSingleton的实例才会被创建

不变模式

一个对象一旦被创建,则它的内部状态将永远不会发生变化。所以没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。

生产者-消费者模式

生产者线程负责提交用户请求,消费者线程负责具体处理生产者提交的任务。生产者和消费者之间通过共享内存缓冲区进行通信。

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
class Producer implements Runnable {
BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
queue.put(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue){
this.queue = queue;
}
@Override
public void run() {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

深度探索C++对象模型 笔记

发表于 2017-09-18

第1章 关于对象

C++在布局以及存取时间上的主要的额外负担是由virtual引起的,包括

  • virtual function机制:用以支持一个有效率的执行期绑定
  • virtual base class:用以实现多次出现在继承体系中的base class有一个单一而被共享的实体

C++对象模式

C++对象模型

  • nonstatic data members被配置于每一个class object之内
  • static data members则被存放在所有的class object之外
  • static和nonstatic function members被放在所有的class object之外
  • virtual functions以两个步骤支持之
    • 每一个class产生出一堆指向virtual functions的指针,放在表格之中,这个表格被称为virtual table(vtbl)
    • 每一个class object被添加了一个指针(vptr),指向相关的virtual table

加上继承

C++支持单一继承,也支持多重继承;继承关系也可以指定为虚拟(virtual共享)

虚拟继承的情况下,base class不管在继承串链中被派生多少次,永远只会存在一个实体。

对象的差异

C++以下列方法支持多态

  • 经由一组隐含的转化操作,例如把一个derived class指针转化为一个指向其public base type的指针

    1
    shape *ps = new circle();
  • 经由virtual function机制

    1
    ps->rotate();
  • 经由dynamic_cast和typeid运算符

    多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base class中。共享接口是以virtual function机制引发的,它可以在执行期间根据object的真正类型解析出到底是哪一个函数实体被调用。

需要多少内存才能够表示一个class object?

  • 其nonstatic data members的总和大小
  • 加上任何由于alignment的需求而填补上去的空间
  • 加上为了支持virtual而由内部产生的任何额外overhead

一个指针,不管它指向哪一种数据类型,指针本身所需的内存大小是固定的。

一个派生类指针和一个基类指针(指向同一个对象)有什么不同?

它们每个都指向对象的第一个Byte。其间的差别是,派生类指针所涵盖的地址包含整个对象,而基类指针所涵盖的地址只包含对象中的子对象。除了子对象中出现的成员,不能够使用基类指针直接处理派生类的任何成员,唯一例外是virtual机制。

第2章 构造函数语意学

成员的初始化队伍

编译器会一一操作initialization list,以适当次序在constructor之内安插初始化操作,并且在任何也explicit user code之前。

list中的项目次序是由class中的members声明次序决定,不是由initialization list中的排列次序决定。

第3章 Data语意学

1
2
3
4
class X {};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};

一个空的class事实上并不是空的,由一个隐晦的1byte,被编译器安插进去的一个char,这使得这个class的两个objects得以在内存中配置独一无二的地址

Y和Z的大小收到三个因素的影响

  • 语言本身所造成的overhead

    在derived class中,这个overhead反应在某种形式的指针身上,它或者指向virtual base class subobject,或者指向一个相关表格;表格中存放的若不是virtual base class subobject的地址,就是其偏移量。

  • 编译器对于特殊情况所提供的优化处理

  • alignment的限制

static data members被放置在程序的一个global data segment中,不会影响个别的class object的大小。在程序之中,不管该class被产生出多少个objects,static data members永远只存在一份实体。

Data Member的布局

nonstatic data members在class object中的排列顺序将和其被声明的顺序一样,任何中间介入的static data members都不会被放进对象布局中。

Static Data Members

每一个static data member只有一个实体,存放在程序的data segment之中。

Nonstatic Data Members

nonstatic data members直接存放在每一个class object之中。

继承与Data Member

继承和多态

把vptr放在class object的前端,对于“在多重继承之下,通过指向class members的指针条用virutal function”会带来一些帮助。

Java多线程编程核心技术 笔记

发表于 2017-08-23

Java多线程编程核心技术

第1章 Java多线程技能

使用多线程

继承Thread类

局限:不支持多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread {
@Override
public void run() {
……
}
}
public class Main {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
}
}

Thread.java类中的start()方法通知线程规划器此线程已经准备就绪,等待调用线程对象的run()方法,让系统安排时间调用Thread中的run()方法,使线程得到运行。启动线程,具有异步执行的效果。

如果调用代码thread.run()就不是异步执行,而是同步,由main主线程来调用run()方法,必须等run()方法中的代码执行完毕后才可以执行后面的代码。

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
public class MyRunnable implements Runnable {
public void run() {
……
}
}
public class Main {
public static void main(String[] args) {
Runnable r = new MyRunnable();
Thread thread = new Thread(r);
thread.start();
}
}

守护线程

当进程中不存在非守护线程,则守护线程自动销毁。典型的守护进程就是垃圾回收进程。

第2章 对象及变量的并发访问

synchronized同步方法

方法内的变量为线程安全

方法前加关键字synchronized

多个对象多个锁

关键字synchronized取得的锁都是对象锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,其他线程只能呈现等待状态,前提是多个线程访问的是同一个对象。

锁重入

一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

出现异常,锁自动释放

当一个县城执行的代码出现异常时,其所持有的锁会自动释放。

同步不具有继承性

还要在子类的方法中添加synchronized关键字

synchronized同步语句块

当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对于同一个object中所有其他synchronized(this)同步代码块的访问将堵塞。

synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁

volatile关键字

通过使用volatile关键字,强制地从公共内存中读取变量的值。

使用volatile关键字增加了实例变量在多个线程之间的可见性,但最致命的缺点是不支持原子性

synchronized VS volatile

  • 关键字volatile是线程同步的轻量级实现,性能更好;

    volatile只能修饰与变量,synchronized可以修饰方法以及代码块。

    随着JDK新版本发布,synchronized关键字在执行效率上得到很大提升

  • 多线程访问volatile不会发生堵塞,synchronized会发生堵塞

  • volatile能保证数据的可见性,但不能保持原子性;

    synchronized可以保证原子性,间接保证可见性

  • 关键字volatile解决的变量在多个线程之间的可见性;

    关键字synchronized解决的是多个线程之间访问资源的同步性;

非原子性

如果修改实例变量中的数据,比如i++,并不是一个原子操作,也就是非线程安全的

  1. 从内存中取出i的值
  2. 计算i的值
  3. 将i的值写到内存中

使用原子类进行i++操作

AtomicInteger原子类进行实现

第3章 线程间通信

通知等待机制

关键字synchronized可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了wait()和notify()方法,它们必须用在被synchronized同步的Object的临界区内。通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁。而notify()操作可以唤醒一个因调用了wait()操作而处于堵塞状态中的线程,使其进入就绪状态。

调用方法notify()一次只随机通知一个线程进行唤醒;

为了唤醒全部线程,可以使用notifyAll()方法。

类ThreadLocal的使用

类ThreadLocal主要解决的就是每个线程绑定自己的值。

第4章 Lock的使用

使用ReentrantLock类

类ReentrantLock需要借助于Condition对象可以实现等待通知模式。

公平锁与非公平锁

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序;

非公平锁就是一种获取锁的抢占机制,是随机获得锁的。

使用ReentrantReadWriteLock类

类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,虽然保证了实例变量的线程安全性,但效率却是非常底下的。

读写锁表示也有两个锁,一个是读操作相关的锁,也成为共享锁;另一个是写操作相关的锁,也叫排他锁。多个读锁之间不互斥,写锁与读锁互斥,写锁与写锁互斥。多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

第6章 单例模式与多线程

立刻加载

1
2
3
4
5
6
7
public class MyObject {
private static MyObject myObject = new MyObject();
private MyObject() {}
public static MyObject getInstance() {
return myObject;
}
}

延迟加载

1
2
3
4
5
6
7
8
9
10
public class MyObject {
private static MyObject myObject;
private MyObject() {}
public static MyObject getInstance() {
if (myObject == null) {
myObject = new MyObject();
}
return myObject;
}
}

不能保证多线程安全

延迟加载的解决方案

synchronized关键字

1
2
3
4
5
6
7
8
9
10
public class MyObject {
private static MyObject myObject;
private MyObject() {}
synchronized public static MyObject getInstance() {
if (myObject == null) {
myObject = new MyObject();
}
return myObject;
}
}

效率低

双检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyObject {
private volatile static MyObject myObject;
private MyObject() {}
public static MyObject getInstance() {
if (myObject == null) {
synchronized(MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
return myObject;
}
}

第7章 其他

线程的状态

六种状态

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

补充:五种状态

  • 新建
  • 就绪(RUNNABLE )
  • 运行(RUNNABLE )
  • 堵塞(BLOCKED, WAITING,TIMED_WAITING)
  • 死亡

MySQL技术内幕:InnoDB存储引擎 笔记

发表于 2017-08-21

MySQL技术内幕:InnoDB存储引擎

第1章 MySQL体系结构和存储引擎

MySQL被设计为一个单进程多线程架构的数据库。MySQL数据库实例在系统上的表现就是一个进程。

第4章 表

约束

  • Primary Key
  • Unique Key
  • Foreign Key
  • Default
  • NOT NULL

创建了一个唯一索引,就创建了一个唯一的约束。约束更是一个逻辑的概念,用来保证数据的完整性;而索引是一个数据结构,有逻辑上的概念,在数据库中更是一个物理存储的方式。

触发器

1
2
3
CREATE TRIGGER trigger_name
BEFORE|AFTER INSERT|UPDATE|DELETE on tbl_name
FOR EACH ROW trigger_stmt

外键

1
2
FOREIGN KEY (index_col_name)
REFERENCES tbl_name(index_col_name)

视图

视图是一个命名的虚表,它由一个查询来定义,可以当做表使用。视图中的数据没有物理表现形式。主要用途之一是被用做一个抽象装置。

1
CREATE VIEW view_name AS select_statement

第5章 索引与算法

InnoDB存储索引支持两种常见的索引,一种是B+树索引,另一种是哈希索引。

B+树

在B+树中,所有记录节点都是按照键值的大小顺序存放在同一层的叶节点中,各叶结点指针进行连接。

B+树索引

B+索引在数据库中有一个特点就是其高扇出性,因此在数据库中,B+树的高度一般在2~3层,也就是对于查找某一键值的行记录,最多只需要2到3次IO。减少IO次数

B+树索引可以分为聚集索引和辅助聚集索引(非聚集索引)。不同点是叶节点存放的是否是一整行的信息。

聚集索引

聚集索引就是按照每张表的主键构造一颗B+树,并且叶节点中存放着整张表的行记录数据,因此也让聚集索引的叶节点成为数据页。

由于实际的数据页只能按照一颗B+树进行排序,因此每张表只能拥有一个聚集索引。
好处

  • 主键的排序查找
  • 范围查询

辅助索引

叶级别不包含行的全部数据。叶节点除了包含键值以外,每个叶级别中的索引行中还包含了一个书签,用来告诉存储引擎哪里可以找到与索引相对应的行数据。
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。通过辅助索引来寻找数据时,InnoDB存储引擎遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。

索引的创建

1
2
CREATE INDEX index_name
ON tbl_name(index_col_name……)

B+树索引的使用

访问表中很少一部分行时,使用B+树索引才有意义。
如果某个字段的取值范围很广,几乎没有重复性,即高选择性,使用B+树索引是最适合的,但是取出的行数占表中大部分的数据时,MySQL数据库不会使用B+树索引。

联合索引

联合索引是指对表上的多个列做索引。
通过叶节点可以逻辑上顺序地读出所有数据,可以对第二个键值进行排序。

哈希算法

InnoDB存储引擎中自适应哈希索引使用的是散列表的数据结构。

第6章 锁

InnoDB存储引擎中的锁

索的类型

  • 共享锁:允许事务读一行数据
  • 排他锁:允许事务删除或者更新一行数据

第7章 事务

ACID

  • 原子性atomicity

    整个数据库事务是不可分割的单位。

  • 一致性consistency

    数据库从与一种状态转变为下一种一致的状态。事物开始之前和事务结束以后,数据库的完整性约束没有被破坏。

  • 隔离性isolation

    一个事务的影响在该事务提交前对其他事务都不可见,通过锁实现。

  • 持久性durability

    事务一旦提交,其结果就是永久性的。

事务控制语句

1
2
START TRANSACTION
COMMIT

事务的隔离级别

  • READ UNCOMMITED
  • READ COMMITED
  • REPEATABLE READ
  • SERIALIZABLE
12
Edward

Edward

SE @ SJTU

18 日志
3 标签
© 2018 Edward
由 Hexo 强力驱动
主题 - NexT.Muse