可伸缩性
当增加计算资源时(如CPU、内存、存储容量或I/O带宽),程序的吞吐量或处理能力能相应的增加
Amdahl定理
F为必须被串行执行的部分,在N个处理器的机器中,在增加计算资源所能达到的最高加速比是
N趋于无穷大时,最大加速比趋于1/F,故串行部分是限制吞吐率的重要因素
线程引入的开销
上下文切换
切换过程中将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文
内存同步
synchronized和volatile可能使用内存栅栏,其会刷新缓存,并可能抑制一些编译器优化操作
同步分为有竞争同步和无竞争同步,JVM会优化一些不会发生竞争的锁,如下
synchroized(new Object()){
}
当对象不会发布到堆,只会在栈中操作时,可以通过锁消除优化去掉加锁操作,如下Vector只会在栈上使用,可以消除add()加锁操作
public void testMethod() {
Vector<String> vector = new Vector<>();
vector.add("A");
vector.add("B");
}
此外还可以进行锁粒度粗化,将临近的同步代码块用同一个锁合并起来,如上两个add()方法合并为单个锁的调用
阻塞
在锁上竞争时,失败的线程会阻塞,主要有两种阻塞方式
- 自旋等待,循环不断获取锁直到成功
- 通过操作系统挂起被阻塞的线程
减少锁的竞争
有两个因素影响锁上发生竞争的可能性
- 锁的请求频率
- 持有锁的时间
降低锁的竞争程度的方式有
- 降低锁的请求频率
- 减少锁的持有时间
- 使用带有协调机制的独占锁
缩小锁的范围
可以减少持有锁时需要执行的指令数量
public class A {
public synchronized void test() {
}
public void test2() {
synchronized (this) {
}
}
}
锁分解
用多个独立的锁保护独立的变量
public class A {
private final Set<String> man = new HashSet<>();
private final Set<String> woman = new HashSet<>();
public synchronized void addMan(String s) {
man.add(s);
}
public synchronized void addWoman(String s) {
woman.add(s);
}
public void addMan2(String s) {
synchronized (man) {
man.add(s);
}
}
public void addWoman2(String s) {
synchronized (woman) {
woman.add(s);
}
}
}
锁分段
对一组独立对象上的锁进行分解。如ConcurrentHashMap使用一个包含16个锁的数组,每个锁保护散列桶的1/16,第N个散列桶由第N/16个锁来保护,这样能把锁的请求减少到原来的1/16
避免热点域
在集合的put和remove方法新增计数器记录集合的修改,在size方法返回时可以避免遍历集合,但会导致该计数器成为热点域,每个导致元素变化的操作都需要访问它,在同步时也会影响性能
为了避免上述问题,ConcurrentHashMap为每个分段都维护一个独立的计数器,并用分段的锁来维护,当调用size时将每个计数器相加
代替独占锁
- ReadWriteLock:若读取操作不会修改共享资源则可以同时访问,但写入时必须以独占方式获取锁
- 原子变量:提供了在整数或对象引用上的细粒度原子操作