欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java Lock详解

程序员文章站 2022-10-03 18:35:05
Lock锁机制详解Lock锁方法详解Lock与synchronized关键字的区别...

Lock锁机制详解

java.util.concurrent.locks包下常用的类与接口

Java Lock详解

  1. Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
    Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
    ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
  2. Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

Lock的理解

java.util.concurrent.locks public interface Lock
Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated 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 ReadWriteLock.
The use of 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 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 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 synchronized methods and statements. In most cases, the following idiom should be used:
   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.
Lock implementations provide additional functionality over the use of synchronized methods and statements by providing a non-blocking attempt to acquire a lock (tryLock()), an attempt to acquire the lock that can be interrupted (lockInterruptibly, and an attempt to acquire the lock that can timeout (tryLock(long, TimeUnit)).
A 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 Lock instances are just normal objects and can themselves be used as the target in a synchronized statement. Acquiring the monitor lock of a Lock instance has no specified relationship with invoking any of the lock methods of that instance. It is recommended that to avoid confusion you never use Lock instances in this way, except within their own implementation.
Except where noted, passing a null value for any parameter will result in a NullPointerException being thrown.
Memory Synchronization
All 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 lock operation has the same memory synchronization effects as a successful Lock action.
A successful 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.
Implementation Considerations
The three forms of lock acquisition (interruptible, non-interruptible, and timed) may differ in their performance characteristics, ordering guarantees, or other implementation qualities. Further, the ability to interrupt the ongoing acquisition of a lock may not be available in a given Lock class. Consequently, an implementation is not required to define exactly the same guarantees or semantics for all three forms of lock acquisition, nor is it required to support interruption of an ongoing lock acquisition. An implementation is required to clearly document the semantics and guarantees provided by each of the locking methods. It must also obey the interruption semantics as defined in this interface, to the extent that interruption of lock acquisition is supported: which is either totally, or only on method entry.
As interruption generally implies cancellation, and checks for interruption are often infrequent, an implementation can favor responding to an interrupt over normal method return. This is true even if it can be shown that the interrupt occurred after another action may have unblocked the thread. An implementation should document this behavior.
Since:
1.5
See Also:
ReentrantLock, Condition, ReadWriteLock
  < 1.8 >

与使用同步方法和语句相比,Lock实现提供了更广泛的锁操作。它们允许更灵活的结构,可以具有完全不同的属性,并且可以支持多个关联的Condition对象。
Lock是一种用于控制多个线程对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取该锁,对共享资源的所有访问都需要首先获取该锁。但是,某些锁可能允许并发访问共享资源,例如ReadWriteLock的读取锁。
使用同步方法或语句可访问与每个对象关联的隐式监视器锁,但强制所有锁的获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序释放,并且所有锁必须在获得它们的相同词汇范围内释放。
尽管用于同步方法和语句的作用域机制使使用监视器锁的编程变得容易得多,并且有助于避免许多常见的涉及锁的编程错误,但在某些情况下,您需要以更灵活的方式使用锁。例如,某些用于遍历并发访问的数据结构的算法需要使用“hand-over-hand”或“chain olcking”:您获取节点A的锁,然后获取节点B的锁,然后释放A并获取C,然后释放B并获得D等。 (但是synchronized难以实现)。锁接口的实现通过允许在不同的作用域中获取和释放锁,并允许以任何顺序获取和释放多个锁,从而允许使用此类技术。

随着灵活性的增加,来了更多的责任。缺少块结构锁定移除了synchronized方法或语句块会自动
获得和释放锁的能力。在大多数情况下,应使用一下习语:

Lock l = ...;
  l.lock();
  try {
    // access the resource protected by this lock
  } finally {
    l.unlock();
  }

当锁定和解锁发生在不同的范围内时,必须小心,确保在锁定时执行的所有代码都在try-catch
-finally受保护的
,以确保在必要时释放锁。锁实现通过提供在synchronized方法及语句块
之上的非阻塞方法tryLock()方法来获得锁,试图获取锁可以被中断,并且可能超时tryLock(long,
TimeUnit)
Lock类还可以提供与隐式监视器锁完全不同的行为和语义。(如保证排序,不可重入用法或死锁检测)
如果实现提供了此专用语义,那么实现必须记录这些语义。
请注意,锁实例只是普通对象,并且可以作为同步语句块中的目标使用。获取锁实例的监视器与调用
该实例的任何lock()方法没有指定的关系。建议避免混乱,你永远不会这样使用锁实例,除非在它们
的实现中。
还有,为任何参数传递null将导致空指针异常。

内存同步
所有的锁实现必须强制执行与内置监视器锁提供的相同内存同步语义。

  • 一个成功的{lock.lock()}操作都必须与Lock操作拥有一致的内存同步效果。
  • 一个成功的{lock.unlock()}操作都必须与Unlock操作拥有一致的内存同步效果。

不成功的加锁或解锁操作以及可重入的加锁和解锁操作,不需要保证任何内存同步效果。

将要实现的原则
三种形式的锁获取(可中断、不可中断和定时)可能不同于其性能特点,顺序保证及其他实现质量。此外,在给定的锁类中可能无法使用中断正在进行的锁获取的能力,因此,不需要实现对所有三种形式的锁获取完全相同的保证或语义,也不需要支持正在进行的锁获取中断。需要实现以清楚地记录每个锁定方法提供的语义和保证。它还必须遵循此接口中定义的中断语义, 优劣支持锁获取中断: 这要么完全, 或仅在方法项上。
由于中断通常意味着取消,并且通常不频繁地检查中断,因此实现可以支持通过寻常方法返回对
一个中断的相应。当中断发生在另一个操作阻塞线程也成立。

Lock锁方法详解

    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();

lock()方法

通过lock()方法获取锁
如果锁已被其他线程获取,则进行等待。如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用:

Lock l = ...;
  l.lock(); // 加锁
  try {
    // access the resource protected by this lock
  } finally {
    l.unlock(); // 释放锁
  }

lockInterruptibly()方法

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者。因此,lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

tryLock()&&tryLock(long time, TimeUnit unit)方法

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
一般情况下,通过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

unlock()方法

释放锁
在finally块中执行。

Lock的实现类 ReentrantLock

ReentrantLock,即可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
构造方法(不带参数 和带参数 true: 公平锁; false: 非公平锁):

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReadWriteLock锁

ReadWriteLock 接口只有两个方法:

    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个reader 线程同时保持,而写入锁是独占的。

package com.learn.thread.rwlock;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void readMethod() {
        rwLock.readLock().lock();
        try {
            for (int i = 0; i < 3; ++i) {
                try {
                    Thread.sleep((long) (100 * Math.random()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在读文件。。。");
            }
        } finally {
            System.out.println("释放读锁。。。");
            rwLock.readLock().unlock();
        }
    }

    public void writeMethod() {
        rwLock.writeLock().lock();
        try {
            for (int i = 0; i < 3; ++i) {
                try {
                    Thread.sleep((long) (100 * Math.random()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在写文件。。。");
            }
        } finally {
            System.out.println("释放写锁。。。");
            rwLock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockTest rwt = new ReadWriteLockTest();

        for (int i = 1; i < 4; ++i) {
            new Thread(() -> { rwt.readMethod(); }, "读线程" + i).start();
        }

        for (int i = 1; i < 3; ++i) {
            new Thread(() -> { rwt.writeMethod(); }, "写线程" + i).start();
        }
    }
}

Java Lock详解

  1. Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
  2. ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字。
  3. ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
  4. ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

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

本文地址:https://blog.csdn.net/wz122330/article/details/107493354

相关标签: 多线程