目录
思维导图
1 性能的思考
使用线程最主要原因是增加性能。
改进性能意味着更少的资源做更多的事。
1.1 性能“遭遇”可伸缩性
可伸缩性:当增加计算资源(cpu、内存、贷款等),吞吐量和生产力更高相应的改善。
性能的两个方面——有多快和有多少是完全分离的。为了实现更好的可伸缩性,我们通常停止增加任务的工作量,而是把任务分解到多个子任务。
1.2 对性能的权衡进行评估
避免不成熟的优化。首先使程序正确,然后再加快。
2 Amdahl定律
Amdahl定律:描述在一个系统中,基于可并行化和串行化组件所占的比重,程序通过额外的资源,理论能够加速多少。
S
p
e
e
d
U
p
≤
1
(
F
+
(
1
−
F
)
N
)
SpeedUp \le \frac{1} {(F+\frac{(1-F)}{N})}
SpeedUp≤(F+N(1−F))1
F:串行化所占比重,N:处理器个数
以F:0.1,N:100为例,则最多达到9.2%使用率。
下面demo-1是一个串行任务例子:
private final BlockingQueue<Runnable> taskQueue;
public WorkThread(BlockingQueue<Runnable> queue) {
taskQueue = queue;
}
/**
* 串行从任务队列拿取任务
*/
@Override
public void run() {
while (true) {
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
break;
}
}
}
一个线程从队列取任务,其它线程想要取就必须等待——任务处理中串行部分。
所有的并发程序都有一些串行源。
2.1 示例:框架中隐藏的串行化
比如LinkedList加同步锁和ConcurrentLinkedQueue。两者的串行化不同,
LInkedList通过
Collections.synchronizedList()
对整个更新操作都会进行加锁,从而变为串行化。
ConcurrentLinkedQueue通过原子化来更新操作。
3 线程引入的开销
对于性能改进的线程来说,并行带来的性能优势必须超过并发带来的开销。
3.1 切换上下文
切换上下文是要付出代价的:线程的调度需要操纵OS和JVM中的共享数据结构。
比如如果一个线程因为竞争锁陷入阻塞,JVM通常会挂起这个锁,运行它被换出。如果线程频繁发生阻塞,将导致频繁的上下文切换,增加调度的开销。
3.2 内存同步
synchronized和volatile提供的可见性保证要求需要使用一个名为存储关卡的指令,来刷新缓存,使缓存无效。
现在的JVM已经可以通过优化解除不存在竞争的锁,减少同步,如demo-2:
//锁优化
private int a ;
public void meaninglessSynchronized() {
//无意义的同步,jvm会进行优化去掉锁
synchronized (new Object()) {
a++;
}
}
更加成熟的JVM可以分析逸出,如果本地对象引用没有暴露,是线程本地的,则可以安全访问。如下的demo-3:
/**
*
* 没有逸出,不需要进行加锁,会直接优化掉锁
* @return
*/
public String escape() {
List<String> scoope = new Vector<>();
scoope.add("shanghai");
scoope.add("beijing");
scoope.add("xian");
return scoope.toString();
}
上述中Vector的加锁操作会被取消,如果没有逸出分析,编译器会进行锁的粗化,会将三次add操作的锁合并。
3.3 阻塞
当竞争锁失败的线程通常采取下面两种方式:
- 自旋等待:适合短期的等待。
- 操作系统中挂起:适合长时间等待。
具体取决于上下文切换的开销。
4 减少锁的竞争
减少锁的竞争可以改进性能和可伸缩性。
并发程序中,对可伸缩性的首要威胁就是独占的锁资源。
通常有两个因素影响锁的竞争性:
- 锁被请求的频率。
- 锁持有的时间。
减少锁的竞争:
- 减少持锁时间。
- 减少请求锁频率。
- 使用协调机制取代独占锁。
4.1 缩小锁的范围
如下demo-4:
private final Map<String, String> attributesMap = new HashMap<>();
/**
* 持有锁超过必要的时间,其实只有get操作需要加锁,其余不需要进行同步
* @param name
* @param regexp
* @return
*/
public synchronized boolean userLocationMather(String name, String regexp) {
String key = "user." + name + ".location";
String value = attributesMap.get(key);
if (value == null) {
return false;
}
return Pattern.matches(regexp, value);
}
demo-4锁是加在方法上,锁持有时间超过了必要的时间,因为方法中许多操作不需要加锁。
改进如demo-5:
/**
* 进行优化,减少持有锁的时间
* @param name
* @param regexp
* @return
*/
public boolean betterUserLocationMather(String name, String regexp) {
String key = "user." + name + ".location";
String value;
synchronized (this) {
value = attributesMap.get(key);
}
if (value == null) {
return false;
} else {
return Pattern.matches(regexp, value);
}
}
demo-5也就是缩小加锁范围。
4.2 减小锁的粒度
减小锁的粒度可以通过分拆锁或者分离锁实现。
如下demo-6:
/**
* 应当拆分锁
*/
public class ServerStatus {
private final Set<String> users = new HashSet<>();
private final Set<String> queries = new HashSet<>();
public synchronized void addUser(String user) {
users.add(user);
}
public synchronized void removeUser(String user) {
users.remove(user);
}
public synchronized void addQuery(String query) {
queries.add(query);
}
public synchronized void removeQuery(String query) {
queries.remove(query);
}
}
demo-6中users和queries关联不大,不需要使用同一个锁进行操作,完全可以分离。
改进为demo-7:
/**
* 通过锁拆分重构ServerStatus
*/
public class BetterServerStatus {
private final Set<String> users = new HashSet<>();
private final Set<String> queries = new HashSet<>();
public void addUser(String user) {
synchronized (users) {
users.add(user);
}
}
public void removeUser(String user) {
synchronized (users) {
users.remove(user);
}
}
public void addQuery(String query) {
synchronized (queries) {
queries.add(query);
}
}
public void removeQuery(String query) {
synchronized (queries) {
queries.remove(query);
}
}
}
通过users和queries两个锁改善了竞争情况
4.3 分离锁
分拆锁有时候可以扩展为加锁块的集合,这就是分离锁。比如ConcurrentHashMap就是使用了16个锁的Array。
如下demo-8是对基于hash的map的演示:
/**
* 基于哈希map使用分离锁
*/
public class StripedMap {
private static final int N_LOCKS = 16;
private final Object[] locks;
private final Node[] map ;
public StripedMap(int size) {
map = new Node[size];
locks = new Object[N_LOCKS];
for (int i = 0; i < N_LOCKS; i++) {
locks[i] = new Object();
}
}
public int hash(Object key) {
return Math.abs(key.hashCode() % map.length);
}
public Object get(Object key) {
int hash = hash(key);
synchronized (locks[hash % N_LOCKS]) {
for (Node node = map[hash]; node != null; node = node.next) {
if (node.key.equals(key)) {
return node.value;
}
}
}
return null;
}
public void clear() {
for (int i = 0; i < map.length; i++) {
synchronized (locks[i%N_LOCKS]) {
map[i] = null;
}
}
}
private static class Node {
public Node next;
public Object key;
public Object value;
}
}
通过多个分离锁分别处理map不同部分。
负面:对容器加锁,进行独占的访问更加困难和昂贵。
4.4 避免热点域
比如容器中的计数器size,每个更改操作都要访问size的话,将导致可伸缩性问题。这个情况下,size被称为热点域。
总结
伸缩性通常可以通过下面方式进行提升:减少获取锁的时间、减小锁的粒度、减少锁占有时间或者使用非独占非阻塞锁代替独占锁。