Java并发18:Lock系列-Lock接口与synchronized关键字的比较

[超级链接:Java并发学习系列-绪论]

Lock接口在之前的章节中多次提及:

  • Java并发02:Java并发Concurrent技术发展简史(各版本JDK中的并发技术)
  • Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例
  • Java并发13:并发三特性-原子性定义、原子性问题与原子性保证技术
  • Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术
  • Java并发15:并发三特性-有序性定义、有序性问题与有序性保证技术

本章主要通过解读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.
 *
 * 

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}. * *

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. * *

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. * *

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: * *

 {@code
 * Lock l = ...;
 * l.lock();
 * try {
 *   // access the resource protected by this lock
 * } finally {
 *   l.unlock();
 * }}
* * 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. * *

{@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)}). * *

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. * *

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. * *

Except where noted, passing a {@code null} value for any * parameter will result in a {@link NullPointerException} being * thrown. * *

Memory Synchronization

* *

All {@code Lock} implementations must enforce the same * memory synchronization semantics as provided by the built-in monitor * lock, as described in * * The Java Language Specification (17.4 Memory Model): *

    *
  • A successful {@code lock} operation has the same memory * synchronization effects as a successful Lock action. *
  • A successful {@code unlock} operation has the same * memory synchronization effects as a successful Unlock action. *
* * 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关键字实现同步访问控制。
  • 但是,原则上是极其不建议这种混搭的同步访问控制方式的,不仅代码难以阅读,而且容易出错。

上面的9条总结就是我对JDK源码注释的理解,其实还有很多可以学习的,例如:可重入锁、死锁检测等等。由于时间和认知有限,就不再多说了,如果有需要,请自行学习。

你可能感兴趣的:(Java并发,Java并发学习实例)