您的位置:首页 > 编程语言 > Java开发

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

2018-03-24 17:29 1146 查看
[超级链接: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.
*
* <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关键字实现同步访问控制。

但是,原则上是极其不建议这种混搭的同步访问控制方式的,不仅代码难以阅读,而且容易出错。

上面的9条总结就是我对JDK源码注释的理解,其实还有很多可以学习的,例如:可重入锁、死锁检测等等。由于时间和认知有限,就不再多说了,如果有需要,请自行学习。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息