Java多线程6:ReentrantLock同步锁

x33g5p2x  于2021-09-10 转载在 Java  
字(7.0k)|赞(0)|评价(0)|浏览(361)

**前言:**Java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析一下ReentrantLock。


一、Lock接口简介

1.1、Lock接口概述

在JUC包下面有一个java.util.concurrent.locks包,这个包提供了一系列基础的锁工具,对传统的synchronizd、wait和notify等同步机制进行补充和增强。下面先来介绍下这个Lock接口。

Lock接口可以视为synchronized的增强版,提供了更灵活的功能。相对于synchronized关键字,Lock接口还提供了限时锁等待、锁中断和锁尝试等功能。

该接口的定义如下

public interface Lock {
// 尝试去获得锁
// 如果锁不可用,当前线程会变得不可用,直到获得锁为止。(中途会忽略中断)    
void lock();

// 尝试去获取锁,如果锁获取不到,线程将不可用
// 直到获取锁,或者被其他线程中断
// 线程在获取锁操作中,被其他线程中断,则会抛出InterruptedException异常,并且将中断标识清除。
void lockInterruptibly() throws InterruptedException;

// 锁空闲时返回true,锁不空闲是返回false
// 该方法不会引起当前线程阻塞
boolean tryLock();

// 在unit时间内成功获取锁,返回true
// 在unit时间内未成功获取锁,返回false
// 如果当前线程在获取锁操作中,被其他线程中断,则会抛出InterruptedException异常,并且将中断标识清除。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 释放锁
void unlock();

// 获取一个绑定到当前Lock对象的Condition对象
// 获取Condition对象的前提是当前线程持有Lock对象
Condition newCondition();
}

关于上面的lock()和lockInterruptibly()方法,有如下区别:

lock()方法类似于使用synchronized关键字加锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
lockInterruptibly()方法顾名思义,就是如果锁不可用,那么当前正在等待的线程是可以被中断的,这比synchronized关键字更加灵活。

1.2、Lock接口的经典用法

Lock lock = new ReentrantLock();
//尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
//该方法不会引起当前线程阻塞
if (lock.tryLock()) {
    try {
        // manipulate protected state
    } finally {
        lock.unlock();
    }
} else {
    // perform alternative actions
}

或者是

lock.lock()
try {
        // manipulate protected state
    } finally {
        lock.unlock();
    }

这边不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。


二、ReentrantLock实现类

ReentrantLock是Lock接口的实现类。ReentrantLock字面意思为可重入的锁,顾名思义ReentrantLock类是一个可重入的独占锁。除了具有和synchronized一样的功能外,还具有限时锁等待、锁中断和公平锁等功能。ReentrantLock底层是通过继承AQS来实现独占锁功能的,AOS是并发编程的基石,后面我会单独抽一篇文章讲。

刚入行Java开发或者说在JDK1.5之前,遇到并发问题时,第一印象想到的都是synchronized同步锁。我们前面已经讲过,关于synchronized同步锁的使用,会存在以下几个问题:

synchronized是不可中断锁,需要线程执行完才能释放锁。
*
synchronized是非公平锁。
*
在Synchronized优化以前,synchronized的性能非常不乐观,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,性能有所提升。
*
synchronized的细粒度和灵活度不够好。
*
....

于是,Doug Lea大师就搞了个ReentrantLock,ReentrantLock诞生于JDK1.5,位于java.util.concurrent(简称JUC)包目录下。

2.1、ReentrantLock类组成

进入java.util.concurrent.locks.ReentrantLock中,发现ReentrantLock有三个内部类:

对应UML类图结构如下:

看到了一个熟悉的身影java.util.concurrent.locks.AbstractQueuedSynchronizer(江湖人简称为AQS),这就是传说中的AQS(同步队列器)。

我们都知道锁分为公平锁和非公平锁。你那么上面类图中NonfairSync为非公平锁,FairSync为公平锁。

2.2、ReentrantLock对比synchronized

ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized在发生异常的时候,会自动释放线程占有的锁,而ReentrantLock在发生异常时,如果没有通过unlock去释放锁,很有可能造成死锁,因此需要在finally块中释放锁

(3)ReentrantLock支持非阻塞的方式获取锁,能够响应中断,而synchronized不可响应中断,一个线程获取不到锁就一直等着。

  (4)ReentrantLock可以是公平锁或者非公平锁,而synchronized只能是非公平锁

2.3、公平锁和非公平锁

关于ReentrantLock,有两个很重要的概念需要学习:公平锁和非公平锁

查看ReentrantLock的源代码,我们会看到两个构造函数,分为对应构造公平锁和非公平锁。

//默认构造非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//true构造公平锁,false构造非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁:是指线程在抢占锁失败后会进入一个等待队列,先进入队列的线程会先获得锁。公平性体现在先来先得。
非公平锁:是指线程抢占锁失败后会进入一个等待队列,但是这些等待线程谁能先获得锁不是按照先来先得的规则,而是随机的。不公平性体现在后来的线程可能先得到锁。

如果有很多线程竞争一把公平锁,系统的总体吞吐量(即速度很慢,常常极其慢)比较低,因为此时在线程调度上面的开销比较大。

原因是采用公平策略时,当一个线程释放锁时,需要先将等待队列中的线程唤醒。这个唤醒的调度过程是比较耗费时间的。如果使用非公平锁的话,当一个线程释放锁之后,可用的线程能立马获得锁,效率较高。

2.4、ReentrantLock代码实现

a. 非公平锁代码

static final class NonfairSync extends Sync {

    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        //如果没有线程占据锁,则占据锁,也就是将state从0设置为1
        //这种抢占方式不要排队,有人释放了锁,你可以直接插到第一位
        //去抢,只要你能抢到
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
        //否则尝试抢占锁
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

AQS中抢占锁的时候会调用 tryAcquire 方法。非公平锁的这个方法直接调用了父类中的nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //锁已经被释放,则直接占据锁
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //否则判断锁是不是之前被自己占用过,并设置重入次数
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

b. 公平锁代码

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //没人在队列中排队,并且锁已经被释放才能抢占到锁,否则去队列中排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //设置重入次数
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

三、ReentrantLock使用场景

规范的话,lock.lock()应该放在try之前。

lock.lock();
 try {
      doSomething();
 } finally {
       lock.unlock();
 }

如果按照下边写法。当lock.lock()报错时也会进入finally释放锁,根据Lock.unlock()文档,当非锁持有线程调用该方法时会抛出unchecked异常或者IllegalMonitorStateException,这样会丢失掉导致加锁失败的异常(虽然结果都是加锁失败,但异常信息却会不一致)。

try {
      lock.lock();
      doSomething();
 } finally {
       lock.unlock();
 }

下面场景图片来源于网上,我就不改了太麻烦.......

场景1:如果发现该操作已经在执行中则不再执行(有状态执行)

a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。 
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

 场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)

这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。 但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

ReentrantLock默认情况下为不公平锁

不公平锁与公平锁的区别: 
公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队) 
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。 
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

这里写图片描述

场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作

synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)

这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

这里写图片描述

下面是ReentrantLock的一个代码示例

//: concurrency/AttemptLocking.java  
// 以下是ReentrantLock中断机制的一个代码实现、如果换成synchronized就会出现死锁  
import java.util.concurrent.*;  
import java.util.concurrent.locks.*;  
 
public class AttemptLocking {  
    private ReentrantLock lock = new ReentrantLock();  
 
    public void untimed() {  
        boolean captured = lock.tryLock();  
        try {  
            System.out.println("tryLock(): " + captured);  
        } finally {  
            if (captured)  
                lock.unlock();  
        }  
    }  
 
    public void timed() {  
        boolean captured = false;  
        try {  
            captured = lock.tryLock(2, TimeUnit.SECONDS);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        try {  
            System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);  
        } finally {  
            if (captured)  
                lock.unlock();  
        }  
    }  
 
    public static void main(String[] args) throws InterruptedException {  
        final AttemptLocking al = new AttemptLocking();  
        al.untimed(); // True -- 可以成功获得锁  
        al.timed(); // True --可以成功获得锁  
        //新创建一个线程获得锁并且不释放  
        new Thread() {  
            {  
                setDaemon(true);  
            }  
 
            public void run() {  
                al.lock.lock();  
                System.out.println("acquired");  
            }  
        }.start();  
        Thread.sleep(100);// 保证新线程能够先执行  
        al.untimed(); // False -- 马上中断放弃  
        al.timed(); // False -- 等两秒超时后中断放弃  
    }  
}  

参考链接:

同步锁——ReentrantLock

并发编程的基石——AQS类

ReentrantLock实现原理

深入剖析ReentrantLock实现原理

相关文章