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
 * &quot;hand-over-hand&quot; or &quot;chain locking&quot;: 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关键字实现同步访问控制。
但是,原则上是极其不建议这种混搭的同步访问控制方式的,不仅代码难以阅读,而且容易出错。

 

posted @ 2021-08-23 17:03  姚春辉  阅读(171)  评论(0编辑  收藏  举报