Java并发18:Lock系列-Lock接口与synchronized关键字的比较
1.JDK源码注释
通过前面章节的学习,我们都知道Lock接口与synchronized关键字都是Java提供的用于对对象进行加锁和解锁的技术,那这两种方式有什么区别和联系呢?先看JDK源码中的注释:
/** * {@code Lock} implementations provide more extensive locking * operations than can be obtained using {@code synchronized} methods * and statements. They allow more flexible structuring, may have * quite different properties, and may support multiple associated * {@link Condition} objects. * * <p>A lock is a tool for controlling access to a shared resource by * multiple threads. Commonly, a lock provides exclusive access to a * shared resource: only one thread at a time can acquire the lock and * all access to the shared resource requires that the lock be * acquired first. However, some locks may allow concurrent access to * a shared resource, such as the read lock of a {@link ReadWriteLock}. * * <p>The use of {@code synchronized} methods or statements provides * access to the implicit monitor lock associated with every object, but * forces all lock acquisition and release to occur in a block-structured way: * when multiple locks are acquired they must be released in the opposite * order, and all locks must be released in the same lexical scope in which * they were acquired. * * <p>While the scoping mechanism for {@code synchronized} methods * and statements makes it much easier to program with monitor locks, * and helps avoid many common programming errors involving locks, * there are occasions where you need to work with locks in a more * flexible way. For example, some algorithms for traversing * concurrently accessed data structures require the use of * "hand-over-hand" or "chain locking": you * acquire the lock of node A, then node B, then release A and acquire * C, then release B and acquire D and so on. Implementations of the * {@code Lock} interface enable the use of such techniques by * allowing a lock to be acquired and released in different scopes, * and allowing multiple locks to be acquired and released in any * order. * * <p>With this increased flexibility comes additional * responsibility. The absence of block-structured locking removes the * automatic release of locks that occurs with {@code synchronized} * methods and statements. In most cases, the following idiom * should be used: * * <pre> {@code * Lock l = ...; * l.lock(); * try { * // access the resource protected by this lock * } finally { * l.unlock(); * }}</pre> * * When locking and unlocking occur in different scopes, care must be * taken to ensure that all code that is executed while the lock is * held is protected by try-finally or try-catch to ensure that the * lock is released when necessary. * * <p>{@code Lock} implementations provide additional functionality * over the use of {@code synchronized} methods and statements by * providing a non-blocking attempt to acquire a lock ({@link * #tryLock()}), an attempt to acquire the lock that can be * interrupted ({@link #lockInterruptibly}, and an attempt to acquire * the lock that can timeout ({@link #tryLock(long, TimeUnit)}). * * <p>A {@code Lock} class can also provide behavior and semantics * that is quite different from that of the implicit monitor lock, * such as guaranteed ordering, non-reentrant usage, or deadlock * detection. If an implementation provides such specialized semantics * then the implementation must document those semantics. * * <p>Note that {@code Lock} instances are just normal objects and can * themselves be used as the target in a {@code synchronized} statement. * Acquiring the * monitor lock of a {@code Lock} instance has no specified relationship * with invoking any of the {@link #lock} methods of that instance. * It is recommended that to avoid confusion you never use {@code Lock} * instances in this way, except within their own implementation. * * <p>Except where noted, passing a {@code null} value for any * parameter will result in a {@link NullPointerException} being * thrown. * * <h3>Memory Synchronization</h3> * * <p>All {@code Lock} implementations <em>must</em> enforce the same * memory synchronization semantics as provided by the built-in monitor * lock, as described in * <a href="https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4"> * The Java Language Specification (17.4 Memory Model)</a>: * <ul> * <li>A successful {@code lock} operation has the same memory * synchronization effects as a successful <em>Lock</em> action. * <li>A successful {@code unlock} operation has the same * memory synchronization effects as a successful <em>Unlock</em> action. * </ul> * * Unsuccessful locking and unlocking operations, and reentrant * locking/unlocking operations, do not require any memory * synchronization effects. * * @see ReentrantLock * @see Condition * @see ReadWriteLock * * @since 1.5 * @author Doug Lea */ public interface Lock {//...}
上面的注释翻译如下:
{Lock接口的实现}提供了比{synchronized关键字}更加广泛的锁定操作。{Lock接口的实现}允许更加灵活的结构、更多的属性,而且支持与{Condition接口}对象的关联使用。
{Lock接口的实现}是一种用来控制多线程对共享资源的访问权限的工具。通常情况下,{Lock接口}提供对共享资源的独占访问:一次只能有一个线程可以获取锁;所有对共享资源的访问都需要首先获取锁。然而,有些{Lock接口的实现}提供了对共享资源的并发访问,例如:{ReadWriteLock接口}提供的读锁。
通过使用{synchronized关键字}定义的同步方法/代码块提供对每个对象的隐式监视器锁的访问权限,但是强制要求所有对锁的获取和释放以{块结构}的方式进行:当某个线程获取了多个对象锁时,它必须以相反的顺序释放这些锁;并且所有的锁必须在获取它们的语法范围内释放。
虽然同步方法/代码块的作用域机制使得对监视器锁的编程更简易,而且帮助我们避免很多常见的涉及锁操作的编程错误,但是我们有时候需要更加灵活的方式使用锁。
例如:有些并发的穿插访问数据的算法需要使用{手牵手}或者{链锁}方式:你获取了节点A的锁,接着获取了节点B的锁,然后释放了节点A的锁并获取了节点C的锁,然后又释放了节点B的锁并且获得节点D的锁等等。{Lock接口的实现}提供了上述的技术使用:通过允许在不同作用域内获取和释放锁,并且允许以任意的次序获取和释放多个锁。
{Lock接口的实现}提供了更多的灵活性,随之带来的是更多的编程限制。由于没有块结构锁定机制,所以{Lock接口的实现}不需要同步方法/代码块的自动释放锁机制。
在大多数情况下,使用如下的方式进行{Lock接口}的加锁和解锁:
Lock l = ...; l.lock();//加锁 try { // access the resource protected by this lock } finally { l.unlock();//解锁 }}
当加锁和解锁发生在不同的作用域时,我们必须注意确保所有持有锁的代码被{try-finally}或{try-catch}保护起来,以确保必要时能够释放锁。
相较于{synchronized关键字},{Lock接口的实现}提供了更多的功能:通过{tryLock()方法}提供了一种非阻塞的获取锁的操作、通过{lockInterruptibly()方法}提供了一种可以被中断的锁、通过{tryLock(long, TimeUnit)方法}提供了一种可以超时的锁。
{Lock接口的实现}还可以提供与{隐式监视器锁}完全不同的行为和语义,如:次序保证、不可重入和死锁检测等。
需注意,{Lock接口的实现}的实例只是普通的对象,并且可以将它们作为目标使用在{synchronized关键字}定义的语句中。通过{synchronized关键字}获取{Lock接口的实现}的实例对象的{监视器锁}和调用{Lock接口的实现}的实例对象的加锁方法没有任何关系。当然了,虽然{Lock接口的实现}和{synchronized关键字}互不相干,但是不建议使用这种混搭的加锁方式。
所有的{Lock接口的实现}都必须提供与内置的监视器锁(Java内存模型中定义的)相同的内存同步语义:
- 一个成功的{lock.lock()}操作都必须与Lock操作拥有一致的内存同步效果。
- 一个成功的{lock.unlock()}操作都必须与Unlock操作拥有一致的内存同步效果。
- 不成功的加锁或解锁操作以及可重入的加锁和解锁操作,不需要保证任何内存同步效果。
2.Lock接口与synchronized关键字的区别与联系
上面的注释说了一大堆,有些朋友可能看了也没多大体会,下面我用更加容易理解的方式进行叙述:
1.JDK版本不同
synchronized关键字产生于JKD1.5之前,是低版本保证共享资源同步访问的主要技术。
Lock接口产生于JDK1.5版本,位于著名的java.util.concurrent并发包中,是Java提供的一种比synchronized关键字更加灵活与丰富的共享资源同步访问技术。
2.读写锁
synchronized关键字只提供了一种锁,即独占锁。
Lock接口不仅提供了与前者类似的独占锁,而且还通过ReadWriteLock接口提供了读锁和写锁。
读写锁最大的优势在于读锁与读锁并不独占,提高了共享资源的使用效率。
3.块锁与链锁
synchronized关键字以代码块或者说是作用域机制实现了加锁与解锁,我简称为块锁。synchronized关键字的作用域机制导致同步块必须包含在同一方法中,且多个锁的加锁与解锁顺序正好相反,即:{{{}}}结构。
Lock接口并不限制锁的作用域和加解锁次序,可以提供类似于链表样式的锁,所以我简称为链锁。Lock接口并不需要把加锁和解锁方法放在同一方法中,且加锁和解锁顺序完全随意,即:{{}{}}结构。
4.解锁方式
synchronized关键字:随着同步块/方法执行完毕,自动解锁。
Lock接口:需要手动通过lock.unlock()方法解锁,一般此操作位于finally{}中。
5.阻塞锁与非阻塞锁
synchronized关键字提供的锁是阻塞的,它会一直尝试通过轮询去获取对象的监视锁。
Lock接口通过lock.tryLock()方法提供了一种非阻塞的锁,它会尝试去获取锁,如果没有获取锁,则不再尝试。
6.可中断锁
synchronized关键字提供的锁是不可中断的,它会一直尝试去获取锁,我们无法手动的中断它。
Lock接口通过lock.lockInterruptibly()提供了一种可中断的锁,我们可以主动的去中断这个锁,避免死锁的发生。
7.可超时锁
synchronized关键字提供的锁是不可超时的,它会一直尝试去获取锁,直至获取锁。
Lock接口通过{tryLock(long, TimeUnit)方法}方法提供了一种可超时的锁,它会在一段时间内尝试去获取锁,如果限定时间超时,则不再尝试去获取锁,避免死锁的发生。
8.公平锁(线程次序保证)
我们都知道,如果高并发环境下多个线程尝试去访问同一共享资源,同一时刻只有一个线程拥有访问这个共享资源的锁,其他的线程都在等待。
synchronized关键字提供的锁是非公平锁,如果持有锁的线程释放了锁,则新进入的线程与早就等待的线程拥有同样的机会获取这个锁,简单来说就是不讲究:先来后到,反而讲究:来得早不如来得巧。非公平锁可能导致某些线程永远都不会获取锁。
Lock接口默认也是非公平锁,但是他还可以通过fair参数指定为公平锁。在公平锁机制下,等待的线程都会被标记等待次数,等待次数越多的锁获取锁的优先级越高,也就是常说的:先到先得。
9.互不干扰、可以共用
synchronized关键字是通过关键字实现对对象的加锁与解锁的。
Lock接口是通过Lock接口的实现类的实例对象的lock()和unlock()方法实现加锁与解锁的。
我们也可以通过synchronized关键字对Lock接口的实现类的实例对象进行监视器锁的加锁与解锁。而且对监视器锁的加锁与解锁与Lock接口的实现类的实例对象的lock()和unlock()方法并不冲突。
也就是说我们可以同时使用Lock接口和synchronized关键字实现同步访问控制。
但是,原则上是极其不建议这种混搭的同步访问控制方式的,不仅代码难以阅读,而且容易出错。