线程的主要目的是提高程序的运行性能。
提高资源利用率,系统响应性。
多线程开销:线程之间的协调(加锁、触发信号、内存同步),上下文的切换,线程创建和销毁,线程的调度。
可伸缩性:当增加计算资源(CPU、内存、存储容量、IO),程序的吞吐朗或者处理能力相应增加。
避免不成熟的优化,首先使程序正确,然后提高运行速度。
计算加速比 <= 1 / (F + (1-F) / N)
F为串行的比例,N为处理器个数。
在所有并发程序中都包含一些串行部分。
如果可运行的线程数大于CPU的数量,那么操作系统就会存在线程调度。
一般调度器会为每个可运行的线程分配一个最小执行时间,即使有许多其他的线程正在等待执行:它将上下文切换的开销分摊到更多不会中断的执行时间上,从而提高整体的吞吐量(损失响应性为代价)。
内存栅栏:可以刷新缓存,使缓存无效,刷新硬件的写缓冲,以及停止执行管道。间接会影响性能,因为会抑制编译器的优化操作,内存栅栏大多数是不能被重排序。
volatile通常是无竞争的,synchronized对无竞争的同步进行了优化。例如一个锁只能有由当前线程访问;JVM逸出分析不会发布到堆的本地对象引用(线程本地),可以去掉锁;锁粒度粗化,合并单个锁。
同步会增加共享内存总线上的通信量,可能会影其它线程。
非竞争的同步可以在JVM中处理,竞争的同步可能需要操作系统的介入。
JVM实现阻塞行为时,可以采用自旋等待或者通过操作系统挂起被阻塞的线程。如果等待时间较短,适合自旋等待;但大多数JVM在等待锁时只是将线程挂起。
在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。
锁的请求频率,每次持有该锁的时间。如果乘积很小,那么大多数获取锁的操作都不会发生竞争。
降低锁的竞争程度:
另外,如果把一个同步代码块分解为多个同步代码块,反而会有负面影响。(有可能会被锁粒度粗化)
降低线程请求锁的频率,可以通过锁分解、锁分段。这些技术中采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况。能减小锁操作的粒度,实现更高的可伸缩性。然而使用的锁越多,死锁的风险越高。
锁分解:例如一个锁分为两个锁。
一组独立对象上的锁进行分解。
ConcurrentHashMap的实现使用了一个包含16个锁的数组,假设三列函数具有合理的分布性,能把锁的请求减少到原来的1/16,这样可以支持多达16个并发的写入器。劣势在于有时候需要加锁整个容器,这样开销就大了。
锁分解和锁分段技术都能提高可伸缩性,使不同的线程在不同的数据(或者同一数据的不同部分)上操作,不会相互干扰。
如果采用锁分段,那么一定要表现出在锁上的竞争频率高于在锁保护的数据上发生竞争的频率。
性能与伸缩性之间平衡,可以引入热点域,缓存一些计算结果。例如HashMap的size,但是会重新导致对独占锁存在时的可伸缩性问题。ConcurrentHashMap中size为将每个分段中的数量相加,而不是维护一个全局计数,每个分段独立计数。
原子变量提供了在整数或者对象引用上的细粒度原子操作(可伸缩性更高),并使用现代处理器中提供的底层并发原语(例如CAS)。
原子变量能降低热点域的更新开销,并不能完全消除。
对象池中,对象能被循环利用。
当线程分配新对象时,通常使用线程本地的内存块,而从对象池中请求,需要同步协调对对象池数据结构的访问,可能导致线程阻塞。这个阻塞开销是内存分配的数百倍,成为瓶颈。
通常,对象分配操作的开销比同步的开销更低。
ConcurrentHashMap在实现中假设,大多数操作都是获取某个已存在的值,因此对各种get操作进行优化。大多数读不加锁,写和少部分读使用分段锁。
日志
中断会干扰人们的工作并降低效率,阻塞和上下文切换同样会干扰线程的正常运行。