JAVA并发编程 - Lock的底层原理

文章目录

  • 前言
  • 一、Lock是什么?
  • 二、Lock的使用
  • 三、AbstractQueuedSynchronizer
        • 1、定义
        • 2、内部结构
        • 3、实现原理
        • 4、公平锁和非公平锁
  • 四、ReentrantLock内部结构
  • 五、ReentrantLock获取锁流程
        • 非公平锁尝试获取锁的过程
        • 当前线程加入双向链表的过程
        • 首节点自旋过程
        • 小结
  • 六、ReentrantLock释放锁流程
  • 总结


前言

总所周知,Java中可以通过加锁,来保证多个线程访问某一个公共资源时,资源的访问不会出问题。Java提出了两种方式来加锁,一是通过关键字加锁:synchronized,二是通过java类lock来加锁。synchronized是底层托管给JVM执行的,在java 1.6 以后做了很多优化,使用很方便,性能也很好。但本文我们详细介绍的是lock(/滑稽)(文中截取的部分源码来自Java 11版本)


一、Lock是什么?

Lock是JUC(java.util.concurrent)包下的一个接口,在java 1.5 版本引入的线程同步工具,用于保证多线程下安全的访问共享资源。
Lock的实现类:(本文会以ReentrantLock为例去分析)
JAVA并发编程 - Lock的底层原理_第1张图片
Lock接口中的方法:

// 尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
// 尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

二、Lock的使用

多线程下访问共享资源时, 访问前加锁,访问后解锁,解锁的操作一般放入finally块中。

// 共享资源
private static int count = 0;
public static void main(String[] args) {
	// 创建锁
    Lock lock = new ReentrantLock();
    for (int i = 0; i < 100; i++) {
    	// 多线程
        new Thread(() -> {
            lock.lock();   // 加锁
            try {
                count++;   //操作共享资源
            } finally {
                lock.unlock();   // 解锁
            }
        }).start();
    }
}

三、AbstractQueuedSynchronizer

这段可能你会觉得比较突然,但是相信我,看懂AQS会帮助你更容易理解ReentrantLock。

1、定义

AbstractQueuedSynchronizer 抽象队列同步器,简称AQS。

2、内部结构

AQS维护了一个volatile的state和一个CLH(FIFO)双向队列。
JAVA并发编程 - Lock的底层原理_第2张图片
state是一个由volatile修饰的int型互斥变量,0表示没有任务线程使用该资源,而大于等于1表示已经有线程正在持有锁资源。
CLH队列是内部类Node来维护的FIFO队列。

3、实现原理

一个线程获取锁资源的时候,会判断state是否等于0(无锁状态),如果是,则把这个state更新为1,表示占用到锁。而这个过程中,如果多个线程同时做这样的操作,就会导致线程的安全性问题。因此AQS采用了CAS机制,来保证互斥变量state更新的原子性。未获得锁的线程通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出的原则放到CLH双向链表中,当获得锁的线程释放锁后,会从这个双向链表的头部去唤醒下一个等待的线程再去竞争锁。

4、公平锁和非公平锁

在竞争锁资源时,公平锁要判断双向链表中是否有阻塞的线程,如果有则需要去排队等待。而非公平锁的处理方式是,不管双向链表中是否有阻塞的线程在排队等待,它都会去尝试修改state变量去竞争锁,这对链表中排队的线程来说是非公平的。

四、ReentrantLock内部结构

一个实现锁功能的关键成员变量Sync类型的sync,Sync继承AQS。

private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}

Sync在ReentrantLock中有两个实现类NonfairSync和FairSync,正好对应了ReentrantLock的非公平锁、公平锁两大类型。

static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}

ReentrantLock默认是非公平锁实现,在实例化时可以指定选择公平锁或者非公平锁

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

五、ReentrantLock获取锁流程

lock.lock()

public void lock() {
    sync.acquire(1);
}

可以看到调用的是,AQS的acquire()(Sync没有重写acquire()方法)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里干了三件事情:

tryAcquire:会尝试通过CAS获取一次锁。

addWaiter:将当前线程加入双向链表(等待队列)中

acquireQueued:通过自旋,判断当前队列节点是否可以获取锁

非公平锁尝试获取锁的过程
protected final boolean tryAcquire(int acquires) {
	// AQS的nonfairTryAcquire()方法
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state
    int c = getState();
    if (c == 0) {
    	// 目前没有线程获取锁,通过CAS(乐观锁)去修改state的值
        if (compareAndSetState(0, acquires)) {
        	// 设置持有锁的线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁的持有者是当前线程(重入锁)
    else if (current == getExclusiveOwnerThread()) {
    	// state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
当前线程加入双向链表的过程
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
    	// 获取末位节点
        Node oldTail = tail;
        if (oldTail != null) {
        	// 当前节点的prev设置为原末位节点
            node.setPrevRelaxed(oldTail);
            // CAS确保在线程安全的情况下,将当前线程加入到链表的尾部
            if (compareAndSetTail(oldTail, node)) {
            	// 原末位节点的next设置为当前节点
                oldTail.next = node;
                return node;
            }
        } else {
        	// 链表为空则初始化
            initializeSyncQueue();
        }
    }
}
首节点自旋过程
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 首节点线程去尝试竞争锁
            if (p == head && tryAcquire(arg)) {
            	// 成功获取到锁,从首节点移出(FIFO)
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}
小结

JAVA并发编程 - Lock的底层原理_第3张图片

六、ReentrantLock释放锁流程

lock.unlock()

public void unlock() {
	// AQS的release()方法
    sync.release(1);
}
public final boolean release(int arg) {
	// Sync的tryRelease()方法
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
	// 获取状态
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 修改锁的持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放锁就是对AQS中的状态值State进行修改。


总结

  1. AQS的数据结构是一个volatile的state和一个双向队列。
  2. lock实际上是通过修改AQS中的state来控制锁的持有情况。
  3. lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java 1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。在非必要的情况下,建议使用synchronized做同步操作。

希望这篇文章,能对你理解锁的实现有所帮助。

你可能感兴趣的:(JAVA基础,java,java-ee,后端)