Java Core 「7」各种不同类型的锁
在谈到并发编程时,我们知道锁是一种常用的机制,来保证多个线程之间对竞态(临界)资源的同步访问。而聊起 Java 中的锁时,我们常常能够听到各种概念,例如可重入锁、乐观锁与悲观锁、自旋锁、公平锁和非公平锁等等。接下来,我们将一探究竟,探究不同类型锁的应用场景。
01-乐观锁与悲观锁
乐观锁与悲观锁并非指某种具体的锁,而是一种广义上的概念,或者说思想:
乐观锁,在访问竞态资源时比较乐观,认为其他线程不会同时访问竞态资源,而仅在其对竞态资源进行修改时才判断下是否有其他线程在其访问期间修改了竞态资源。CAS 是典型的乐观锁。
悲观锁,相反,在访问竞态资源时比较悲观,认为其他显示随时可能会修改竞态资源,所以在访问前先对其加锁,防止其他线程访问并修改竞态资源。synchronized 和 Lock 都是典型的悲观锁。
从上述两种锁对待竞态资源的态度可以看出来,乐观锁更适用于读多、写少的场景;而悲观锁更适合用于写多、读少的场景。
02-共享锁和排他(独享)锁
根据锁是否可由多个线程共享,可以分为共享锁和排他锁:
共享锁,锁对象可由多个线程共享,即某个线程获得共享锁后,不影响其他线程继续获得锁。ReentrantReadWriteLock 中的 ReadLock 是共享锁。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); try { rwLock.readLock().lock(); ... } finally { rwLock.readLock().unlock(); } // 多个线程可同时对 ReadLock 进行加锁,且可以成功
排他锁,一旦被某个线程持有,除非该线程释放锁,否则锁不可被其他线程获得。ReentrantReadWriteLock 中 WriteLock 是排他的。
共享锁适合并行读的场景,可以提高并发度。排他锁适合对竞态资源进行更新或写操作的场景,排他可以保证对竞态资源访问的安全性。
03-公平锁和非公平锁
线程尝试获取排他锁时,如果获取失败,该线程则会被挂起,直至持锁线程释放排他锁,所有因尝试获取该排他锁而挂起的线程会重新尝试获取排他锁。公平和非公平是指:
公平锁,所有挂起进程按照 FIFO 规则,最早挂起的线程获得锁;(“先来先得”)
非公平锁,排他锁被释放的同时,所有挂起的线程谁抢到算谁的;(“来得早不如来得巧”)
采用公平锁还是非公平锁,需要根据业务场景选择。非公平锁有一个缺点,即极端情况下可能导致某额线程长时间无法获取锁。公平锁也有缺点,一个持锁时间过长的线程,可能导致其他所有线程等待时间过长。
ReentrantLock 中同时实现了公平锁和非公平锁,可在创建对象时通过参数指定。
04-可重入锁
可重入锁是指当持锁进程尝试再次获取同一个锁时,仍可以获取成功。可重入锁本质上是允许线程多次执行同一份临界代码。synchronized 和 ReentrantLock 都是可重入锁。
05-自旋锁
在 01.3 节中提到,当线程尝试获取排他锁失败后,该线程会被挂起。其实,除了挂起外,还有其他选项,那就是自旋。在介绍自旋之前,需要了解一些操作系统层面的知识。线程的挂起与唤醒需要操作系统切换 CPU 的状态来完成。这种切换需要消耗 CPU 时间,是一种代价比较高昂的操作。如果被挂起的线程仅需要执行非常简单的操作(即其执行需要耗费的 CPU 时间还不如状态切换所耗费的 CPU 时间长),则不必去挂起线程,而让线程进行一定程度的“自旋”。自旋的本质是重试,即将尝试获取锁的操作放在 while 循环中,如果失败则进行尝试。
自旋不能无限度的执行,如果超过了一定的限度(默认是 10 次,XX:PreBlockSpin 来指定),则认为获取失败,挂起线程。
还有一种高阶的自旋机制,称为自适应自旋,自适应是指并非每次都尝试 10 次,尝试的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
do-while + CAS 就是就是典型的自旋锁。
自旋锁有一个缺点,就是会浪费 CPU 资源,因为自旋时并未让出 CPU。
06-偏向锁
谈到偏向锁,一般指的是 JDK 1.6 后,synchronized 底层实现的一种高效锁机制。在前面的文章《Java Core 「2」synchronized 关键字》中我们学习了 synchronized 关键字的实现机制。这里就不再多说。
[1] Synchronized原理(偏向锁篇)[2] Java并发 - Java中所有的锁
历史文章
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/855dca15c9c1257caf9cd3de5】。文章转载请联系作者。
评论