Java并发编程:用Lock接口自定义同步组件

简介

本文将简单介绍Lock接口和AbstractQueuedSynchronizer同步器,并基于它们两个实现一个取名为互斥锁的同步组件

Lock接口

在Java SE 5之后并发包中提供了Lock接口来实现同步组件,当然也能够实现锁,因为锁是一种特定的同步组件。它实现的锁能够提供和Synchronized的类似的同步功能并且拥有更多的灵活性。这些灵活性主要表现在一下三个方面:

  1. 可以尝试非阻塞地获取锁:tryLock()相关方法没有获取到锁能够理解返回而不是阻塞等待
  2. 能被中断地获取锁:获取到锁的线程能够响应中断,中断时中断异常抛出并且锁释放
  3. 超时获取锁:如果指定时间内没有获取到锁则返回

同步器

队列同步器AbstractQueuedSynchronizer(AQS,简称同步器)是用来构建同步组件的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的设计是基于模板方法模式的。定义同步器时,我们可以通过继承AQS来实现。AQS是一个抽象类,其中我们需要关注的方法方法可大致分为3类:

  1. 访问或修改同步状态的3个方法,分别是:
    • getState():获取当前同步状态
    • setState(int newState): 设置当前同步状态
    • compareAndSetState(int expect, int update): 使用CAS原子地设置当前状态
  2. 可以在子类中重写的方法;
  3. 定义好的模板方法,这些模板方法会调用2中重写好的方法,它们基本也可以分为三类:
    • 独占式获取与释放同步状态
    • 共享式获取与释放同步状态
    • 查询同步队列中的等待线程情况

自定义同步组件

有了上面两方面的基础知识,我们就可以自定义同步组件了。
自定义同步组件就是写一个Lock接口的实现类,该实现基本都是通过聚合一个同步器的子类来完成线程访问控制的。而同步器的子类推荐被定义为同步组件的静态内部类。
下面自定义了一个“独占锁”的同步组件,其目的是同一个时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待。代码如下:

package site.pengcheng.concurrent.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author pengchengbai
 * @description
 * @date 2020/5/24 11:35 上午
 */
public class Mutex implements Lock {

    private static class Sync extends AbstractQueuedSynchronizer {

        // 是否处于占用状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 当状态为0的时候获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }


    /**
     * 下面的方法表示将同步组件的操作代理给自定义同步器Sync
     */

    private final Sync sync = new Sync();
    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    // 返回一个Condition,每个condition都包含了一个condition队列
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

通过这个示例可以看出,当用户使用独占锁这个同步组件的时候,并不会直接和内部的同步器打交道,而是使用Mutex提供的方法(Lock中定义方法)。之前也说到,AQS是用来构建同步组件的基础框架,它们这种“合作”方式可以大大降低实现一个自定义同步组件的门槛。

总结

同步器是实现同步组件的关键。通过聚合同步器实现同步组件的时候,内部利用了同步器实现具体语义,对使用者隔离了实现细节。可以这样理解,同步组件(比如锁)是面向使用者的,它定义了使用者与其交互的接口,屏蔽了具体实现;同步器是面向同步组件的,它屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。


文章作者: 木白
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 木白 !
  目录