研究生阶段一直使用C++,到工作时,才接触到Java。写了这么多年的多线程程序,觉得对于互斥(注意,不是同步哦)的各种锁有必要做个总结。
这里我想将Windows,Linux和Java JVM三种环境中使用锁的环境及虽然将Windows,Linux和Java JVM放在一起比较是有些不合适的,但是对基于Windows操作系统C++,Linux操作系统C++及Java程序而言,在应用层面上来说,这种横向比较对于加深对于多线程互斥中锁应用上的理解,是有意义的。如果有不正确的地方,欢迎指正。
Windows
有InterLock系列函数,CRITICAL_SECTION临界区,SRWLock读写锁,Mutex互斥量
Linux
有atomic_t,Mutex互斥量,,rwlock读写锁
Java JVM
有Atomic原子操作,synchronized同步关键字,ReentrantLock,ReentrantReadWriteLock读写锁
互斥量 |
环境 |
睡眠锁 / 自旋锁 |
可重入 / 不可重入 |
用户态 / 内核态 |
可被中断 / 不可被中断 |
备注 |
CRITICAL_SECTION 临界区 |
Windows |
先自旋锁,后睡眠锁 |
可重入 |
用户态 |
|
|
SRWLock 读写锁 |
Windows |
读写锁 |
不可重入 |
用户态 |
|
|
Mutex 互斥量 |
Windows |
睡眠锁 |
不可重入 |
内核态 |
|
|
|
|
|
|
|
|
|
spin_lock |
Linux |
自旋锁 |
|
内核 |
|
|
Mutex 互斥量 |
Linux |
睡眠锁 |
|
pthread |
|
|
rwlock |
Linux |
读写锁 |
|
pthread |
|
|
|
|
|
|
|
|
|
synchronized |
JVM |
类自旋锁 |
可重入 |
JVM 内部锁 |
|
|
ReentrantLock |
JVM |
类睡眠锁 |
可重入 |
Java 类库的锁 |
|
|
ReentrantReadWriteLock |
JVM |
读写锁 |
可重入 |
Java类库的锁 |
|
|
|
|
|
|
|
|
|
这里着重说一下,何时使用自旋锁,何时使用读写锁,何时使用睡眠锁。
在临界区范围较小,竞争不强,并且其中没有读写文件之类的内核态操作时,最好使用自旋锁;这是因为,自旋锁是以一种“忙等”的方式抢占锁,如果临界区范围较大,或竞争很强,或者有内核态操作,导致多个线程同时等待一个自旋锁资源,服务器的CPU使用率将会非常高,上下文切换会比较频繁,性能不佳。对于Windows的临界区,微软特别做了优化,它会先自旋一段时间,然后,如果长时间进入不了临界区,则会使用互斥量进入睡眠状态。
在读写数量上差距很大时,(读多写少或者写多读少),使用读写锁;读写锁,大多数情况下都是自旋锁的一个变种。
在临界区范围较大或者临界区其中有内核态操作时,最好使用睡眠锁;睡眠锁在抢占不到锁的情况下,会进入内核态,使自身所在的线程被挂起。在临界区较小时,进入内核态的时间长度很可能大于在临界区中的时间长度,导致性能不佳,但是如果临界区很大,其中操作时间长度大于睡眠锁进入内核态的时间长度,尤其是在竞争很强的情况下,性能相对自旋锁要好些。
对于JVM,为什么我会写类自旋锁和类睡眠锁,这是因为,它们的性能情况和在C++下是不一样的,在Java5.0中,synchronized的性能和ReentrantLock的性能的对比与自旋锁和睡眠锁的性能对比类似,但是在Java6.0中则不然,synchronized在竞态的性能有了大幅的提高,使得在竞态情况下,synchronized的性能和ReentrantLock的性能基本一致。
最后,也是最重要的是,以上说法都不准确,唯一准确的是在你自己的代码中测试性能的结果。