Java并发编程专题二(锁和同步器AQS)

136 阅读15分钟

1. Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时 访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接 口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增 了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功 能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提 供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以 及超时获取锁等多种synchronized关键字所不具备的同步特性。

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。

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

image.png

2.队列同步器 AQS

2.1 内部类Node

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

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。 试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此

同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。

static final class Node {
        
        //初始化两个节点引用
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

       
        static final int CANCELLED =  1;  //取消的
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

      
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;

        Node nextWaiter;

        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }


image.png

2.2 一些引用:

用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作


    private transient volatile Node head; //维护一个头节点和尾节点的引用
    private transient volatile Node tail;

    
    private volatile int state; //同步状态,用volatile修饰

    //获取当前同步状态
    protected final intgetState() {  return state; }

   //设置新的同步状态
    protected final void setState(int newState) { state = newState;}

   //通过unsafe类的CAS修改同步状态
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

2.3 模版方法

//提供给子类重写,独占式的获取同步状态,实现该方法需要查询当前状态并判断是否符合预期,然后用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();
    }

    
  
   //独占式获取同步状态,获取成功则返回,否则进入同步队列等待
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

   //和上面这个方法相同,但是响应中断
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

   //在上面的方法中增加了时间限制
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(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 void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

   //和上面相同,响应中断
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

    
    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 releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }


2.4 加入队列和设置头节点

同步器将节点加入到同步队列的过程:加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

image.png

设置首节点的过程:同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节 点设置成为原首节点的后继节点并断开原首节点的next引用即可。

image.png

2.5 独占式同步状态获取与释放

image.png 调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是 由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同 步队列中移出

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&    //tryAcquire保证线程安全的获取同步状态
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该 节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的 唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { //通过compareAndSetTail(pred, node)确保尾节点被正确添加
                pred.next = node;
                return node;
            }
        }
   			//将并发添加节点的请求通过CAS变 得“串行化”了
        enq(node); //如果多个线程获取同步状态失败,并发的添加到list,也许会顺序混乱
        return node;
    }

private Node enq(final Node node) {
        for (;;) { //通过“死循环”来保证节点的正确添加
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) { //只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线 程不断地尝试设置
                    t.next = node;
                    return t;
                }
            }
        }
    }

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自 省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这 个自旋过程中(并会阻塞节点的线程):

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) { //当前线程在“死循环”中尝试获取同步状 态,而只有前驱节点是头节点才能够尝试获取同步状态
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会 唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

第二,维护同步队列的FIFO原则。该方法中,节点自旋获取同步状态的行为如所示:

image.png

acquire方法调用流程:

image.png

前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获 取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果 对于锁这种并发组件而言,代表着当前线程获取了锁。

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能 够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释 放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。该方法代码如代码清单5-6所示。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
             // 从队列中唤醒一个等待中的线程(遇到CANCEL的直接跳过)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在获取同步状态时,同步器维 护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列 (或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步 器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

公平锁和非公平锁实现


ReentrantLock reentrantLock = new ReentrantLock(true); //公平锁

static final class FairSync extends Sync {

        final void lock() {
            acquire(1);
        }
        
        
        public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
        }
        
        
        //公平锁重写了tryAcquire方法
        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;
        }
    }

非公平锁(默认实现):

    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() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    
    protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        
    
    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;
        }

读写锁的实现

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升.

读写锁的特性:

image.png

常见方法:


//返回读锁被获取的次数,即使一个线程获取了n次读锁也返回n
    public int getReadLockCount() {
        return sync.getReadLockCount();
    }
//返回当前线程获取读锁的次数,使用ThreadLocal来保存    
    public int getReadHoldCount() {
        return sync.getReadHoldCount();
    }
    
    //判断写锁是否被获取
    public boolean isWriteLocked() {
        return sync.isWriteLocked();
    }
    
    public int getWriteHoldCount() {
        return sync.getWriteHoldCount();
    }
    

读写状态的设计:

读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,使得该状态的设计成为读写锁实现的关键。 果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写。

image.png

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次 读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态 值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移 16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是 S+0x00010000。

写锁的获取和释放:

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当 前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态

protected final boolean tryAcquire(int acquires) { 
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在读锁或者当前获取线程不是已经获取写锁的线程
                if (w == 0 || current != getExclusiveOwnerThread())
                       return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                       throw new Error("Maximum lock count exceeded");
               setState(c + acquires);
               return true;
       }
       if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
               return false;
       }
       setExclusiveOwnerThread(current);
       return true;
}

如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如 果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当 前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写 锁一旦被获取,则其他读写线程的后续访问均被阻塞. 写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对 后续读写线程可见

锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程.

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果 当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修 改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。 RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的 也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的

LockSupport工具

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread) 方法来唤醒一个被阻塞的线程

image.png

Condition接口

使用方法: 一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionUseCase {

    public Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public static void main(String[] args)  {
        ConditionUseCase useCase = new ConditionUseCase();
        ExecutorService executorService = Executors.newFixedThreadPool (2);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                useCase.conditionWait();
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                useCase.conditionSignal();
            }
        });
    }

    public void conditionWait()  {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            System.out.println(Thread.currentThread().getName() + "等待信号");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "拿到信号");
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
    public void conditionSignal() {
        lock.lock();
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "发出信号");
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }

}

实现方法:等待队列

    //await()方法过程相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中
        public final void await() throws InterruptedException {
            // 判断当前线程是否中断
            if (Thread.interrupted())
                throw new InterruptedException();
            // 当前线程加入等待队列    
            Node node = addConditionWaiter();
            // 释放同步状态
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 判断此节点是否在同步队列中,若不在直至在同步队列为止
            while (!isOnSyncQueue(node)) {
                // 阻塞当前线程
                LockSupport.park(this);
                // 若线程已经中断则退出
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 竞争同步状态
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }


先是判断线程是否中断,若中断直接抛出异常,否则调用addConditionWaiter()将线程包装成节点加入等待队列


        private Node addConditionWaiter() {
            // 获取等待队列的尾节点
            Node t = lastWaiter;
            // 若尾节点状态不为CONDITION,清除节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 清除等待队列中所有状态不为CONDITION的节点
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 将当前线程包装成Node节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 尾插节点
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            // 将节点置为尾节点    
            lastWaiter = node;
            return node;
        }


//将节点加入等待队列并没有使用CAS,因为调用await()方法的线程必定是获取了锁的线程,即此过程是由锁来保证线程安全,成功加入等待队列后,调用fullyRelease()释放同步状态

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // 获取同步状态
            int savedState = getState();
            // 释放锁
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

调用AQS的模板方法release()方法释放同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,否则抛出异常。随后调用isOnSyncQueue()判断节点是否在同步队列

final boolean isOnSyncQueue(Node node) {
    // 若状态为CONDITION或者前驱节点为null
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 若后继节点不为null,表明节点肯定在同步队列中    
    if (node.next != null) // If has successor, it must be on queue
        return true;
    // 从同步队列尾节点找节点    
    return findNodeFromTail(node);
}

若节点不在同步队列会一直在while循环体中,当此线程被中断或者线程关联的节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法)会结束循环调用acquireQueued()方法获取,否则会在循环体中通过LockSupport.park()方法阻塞线程

signal()/signalAll() signal()

public final void signal() {
    // 判断当前线程是否为获取锁的线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 唤醒条件队列中的第一个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

复制代码 isHeldExclusively()方法需要子类重写,其目的在于判断当前线程是否为获取锁的线程

protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

doSignal

signalAll()

    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        // 将等待队列中节点从头节点开始逐个移出等待队列,添加到同步队列
        do {
            Node next = first.nextWaiter;
            first.nextWaiter = null;
            transferForSignal(first);
            first = next;
        } while (first != null);
    }

signalAll()方法相当于对等待队列中的每个节点均执行一次signal()方法,将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程