Java线程并发中的锁——Lock(上)

Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock的代码使用示例:

public class LockTest {
    public void lockTest(){
        Lock lock = new ReentrantLock();
        lock.lock();
        try{}
        catch(Exception t){
        }finally{
            lock.unlock();
        }
    }
}

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

Lock接口方法

  void lock();

获取锁,调用该方法线程将会获取锁,当获取锁后从该方法返回

 void lockInterruptibly() throws InterruptedException;

可中断获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程

boolean tryLock();

尝试非阻塞的获取锁,调用该方法后会立刻返回,获取成功返回true,否则返回false

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

超时获取锁,当前线程在以下情况会返回
1、当前线程在超时范围内获取锁
2、当前线程在超时范围内被中断
3、超时时间结束返回false

void unlock();

释放锁

Condition newCondition();

获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。

Lock接口与synchronized的区别

尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁:与synchronized不同,获取到的锁能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁:在指定的时间截止之前获取锁,如果截止时间之前仍旧无法获取锁,则返回

队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

队列同步器接口说明

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
getState():获取当前同步状态
setState():设置当前同步状态
compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
同步器可重写的方法

// 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后通过CAS设置同步状态
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

// 独占式释放同步锁,等待获取同步状态的线程有机会获取到同步状态
protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
// 共享式获取同步状态,返回大于0表示成功,反之失败 
protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
// 共享式释放同步状态
protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
// 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程独占
protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

同步器提供的模板方法
独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁

// 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则进入同步队列等待,该方法将会调用重写的
// tryAccquire方法
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//  与acquire方法相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,该方法
// 会抛出InterruptedException并返回
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

// 在acquireInterruptibly方法上添加了超时延迟,如果当前线程在超时时间内没有获取到同步状态返回false,否则返回true
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

// 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的唯一区别是同一时刻可以有
// 多个线程获取到同步状态
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

// 与acquireShared,只是该方法响应中断
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
// 在acquireShared基础上添加了超时间
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }
// 独占式释放同步状态,该方法会在释放同步状态之后将同步队列第一个节点包含的线程唤醒
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

// 共享式释放同步状态 
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
// 获取等待在同步队列上的线程集合 
public final Collection<Thread> getQueuedThreads() {
        ArrayList<Thread> list = new ArrayList<Thread>();
        for (Node p = tail; p != null; p = p.prev) {
            Thread t = p.thread;
            if (t != null)
                list.add(t);
        }
        return list;
    }

下面看个独占锁的例子

package com.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Mutex implements Lock {

    // 静态内部类,自定义同步器
    @SuppressWarnings("serial")
    private static class Sync extends AbstractQueuedSynchronizer{
        // 是否处于占用状态
        protected boolean isHeldExclusively(){
            return getState() == 1;
        }

        // 当状态==0获取锁,更新state=1
        public boolean tryAcquire(int acquires){
            if(compareAndSetState(0, 1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将状态设置为0
        public boolean tryRelease(int release){
            if(getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个condition,每个condition都包含一个condition队列
        Condition newCondition(){
            return new ConditionObject();
        };
    }
    // 仅需要将操作代理到Sync上
    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);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() { 
        return sync.isHeldExclusively(); 
    }

    public boolean hasQueuedThreads() { 
        return sync.hasQueuedThreads(); 
    }
}

上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

队列同步器原理分析

队列同步

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点

    /**
     * 等待状态
     * 1、CANCELLED,值为1,由于同步队列中等待的线程等待超时或者被中断需要从同步队列中取消等待,节点进入该状态将不会变化
     * 2、SIGNL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程运行
     * 3、CONDITION,值为-2,节点在等待队列中,节点线程等待在condition上,当其他线程对condition调用了signal()方法后,该节点将会从等待
     *  队列中转移到同步队列,加入到同步状态获取中
     * 4、PROPAGATE,值为-3,表示下一次共享式同步状态获取将会被无条件传播下去
     * 5、INITIAL,值为0,初始状态
     */
     volatile int waitStatus;
     /**
      * 前驱节点,当节点加入同步队列时被设置,(尾部添加)
      */
     volatile Node prev;
     /**
      * 后继节点
      */
     volatile Node next;
     /**
      * 获取同步状态的线程
      */
     volatile Thread thread;

     /**
      * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是shared常量,也就是说节点类型(独占、共享)和等待队列中的后继节点公用一个字段
      */
     Node nextWaiter;

节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如下图所示。
这里写图片描述
上图中同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
同步器将节点加入到同步队列的过程如下图所示。
这里写图片描述
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程如下图所示。
这里写图片描述
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值