Java中的锁

本文在参考java并发编程实战后完成,参考内容较多

Java中的锁

锁是用来控制多线程访问共享资源的方式,一个锁能够防止多个线程同事访问共享资源。在Lock接口出现之前,Java程序是通过synchronized来实现锁功能的,在JDK1.5之后,新增的Lock接口可以实现锁功能,他的功能与Synchronized类似,但是需要显式的获取和释放锁,他失去了隐式获取释放锁的便捷性,但是可操作性更强,同时具有可中断获取锁以及超时获取锁的特性。

Lock接口

Lock接口提供了几个synchronized不具备的主要特性:

  • 尝试非阻塞的获取锁
  • 能被中断的获取锁
  • 超时获取锁

Lock是一个接口,定义如下:

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;

/**
 * @see ReentrantLock
 * @see Condition
 * @see ReadWriteLock
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface Lock {

    /**
     * 线程获取锁,如果获取锁失败,线程无法向下执行
     */
    void lock();

    /**
     * 可中端的获取锁,和lock()相比这个方法可以响应中断,就是在获取锁的过程中可以中断当前线程
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试非阻塞的获取锁,调用该方法后立刻返回,获取锁成功返回true,否则返回false
     */
    boolean tryLock();

    /**
     * 超时的获取锁,在发生下面的情况下会返回:
     * 1、在超时时间范围内获取锁成功立刻返回。2、当前线程在超时时间内被中断 3、超时时间结束,返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁
     */
    void unlock();

    /**
     * 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的wait方法,调用后,当前线程将释放锁。
     */
    Condition newCondition();
}
队列同步器
队列同步器AbstractQueuedSynchronizer,是构建锁或者其他同步组件的基础框架,他使用int成员变量表示同步状态(这个同步状态在不同的同步组件中表示的含义会有差异),通过内置的FIFO队列完成线程获取资源的排队工作。

同步器使用的主要方式是通过继承并实现它定义的抽象方法来管理同步状态。队列同步器提供了操作同步状态的方法,可以保证状态的修改是线程安全的,同步器支持独占的获取同步状态,也支持共享式的获取。

同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁是面向使用这的,他定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,他简化了锁的实现方式,屏蔽了同步状态管理、线程排队,等待唤醒等底层操作。

AbstractQueuedSynchronizer的定义:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable

//同步器提供的修改活访问状态的方法

//获取当前同步状态
protected final int getState() {
    return state;
}
//设置当前同步状态
protected final void setState(int newState) {
    state = newState;
}
//使用CAS设置当前状态,可以保证设置状态的原子性
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

同步器的设计是基于模版方法模式的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定的同步组件实现中,并调用同步器提供的模版方法,而这些模版方法将会调用使用者重写的方法

ReentrantLock

ReentrantLock是Java中提供的另一种锁的实现.他通过调用lock()方法获取锁;调用unlock()方法释放锁。
ReentrantLock的实现依赖于Java同步框架AbstractQueuedSynchronizer,AQS使用一个整形的volatile变量(命名为state)来维护同步状态,volatile变量是ReentrantLock内存语义实现的关键。

ReentrantLock分为公平锁和非公平锁。
  • 公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁
  • 每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。

ReentrantLock的类图如下:
图片描述

ReentrantLock实现了Lock接口,内部有三个内部类,Sync、NonfairSync、FairSync,Sync是一个抽象类型,它继承AbstractQueuedSynchronizer,这个AbstractQueuedSynchronizer是一个模板类,它实现了许多和锁相关的功能,并提供了钩子方法供用户实现,比如tryAcquire,tryRelease等。Sync实现了AbstractQueuedSynchronizer的tryRelease方法。NonfairSync和FairSync两个类继承自Sync,实现了lock方法,然后分别公平抢占和非公平抢占针对tryAcquire有不同的实现。

首先分析公平锁:
ReentrantLock lock = new ReentrantLock(true);公平锁声明,如果不为true,或者使用默认,那么是非公平锁。
加锁lock()方法的调用轨迹如下:

  1. ReentrantLock.lock()
  2. sync.lock() [ReentrantLock.FairSync.lock()]
  3. AbstractQueuedSynchronizer.acquire(int arg) [arg = 1]
  4. ReentrantLock.FairSync.tryAcquire(int acquires);

第4步是加锁的关键步骤:

protected final boolean tryAcquire(int acquires) {
    //获取当前尝试获取锁的线程
    final Thread current = Thread.currentThread();
    //获取AQS中的volatile变量state
    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;
}
CAS + volatile 构成了AQS以及原子变量类。
JDK文档对CAS的说明:如果当前状态值与预期值相等,则以原子的方式讲同步状态设置为给定的更新值,此操作具有volatile读和写的内存语义。这意味着编译器不能对CAS与CAS前面和后面的任意内存操作重排序。CAS操作对应的本地方法最终对应的处理器源代码回有一个Atomic::cmpxchg指令。程序回根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果是多处理器上运行,那么添加Lock前缀,如果是单处理器那么就会省略。lock前缀会确保内存的读写改操作原子执行。(但处理器自身会维护)
【补充volatile的内存语义:1、在程序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重拍下这两个操作 2、当第二个操作为volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后 3、当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序,这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前 4、当第一个操作是volatile写,第二个操作是volatile读时,不能重排序】
读写锁

ReentrantLock是支持可重入的排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有和读线程和其他的写线程均被阻塞。读写锁维护了两个锁,一个读锁,一个写锁,通过读写锁,可以提高兵法性能。

Java中提供的读写锁的实现是ReentrantReadWriteLock,提供的特性如下:

  • 公平性选择,支持公平和非公平的锁获取方式
  • 重进入
  • 锁降级 遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成读锁。
阅读 348

推荐阅读
刨刨代码的根
用户专栏

刨根问底拦不住~

0 人关注
8 篇文章
专栏主页