并发编程系列之锁基础篇

前言

上节我们介绍了线程的相关知识,今天我们开始逛逛Java中锁的相关旅途,今天我们先介绍基础景点,主要讲解下Java中的Lock接口和AQS,OK,让我们开始今天的并发之旅吧。

 

景点一:Lock接口

什么是Lock对象?

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。

synchronized 方法或语句的通过对每个对象使用隐式监视器锁来实现同步,强制所有锁获取和释放均要出现在一个结构块中:当获取了多个锁时,它们必须以相反的顺序释放,即必须先获取后释放再获取再释放的顺序,这种情况显然没有Lock接口的显式实现加锁解锁方便,Lock接口拥有对锁获取和释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

 

如何使用Lock接口?

Lock是一个接口,它定义了获取锁和释放锁的基本操作,主要有如下几个方法:

Lock是个接口,不能直接使用,我们需要借助Lock的具体实现来操作lock,主要的实现类有ReentrantLock, Condition, ReadWriteLock;以ReentrantLock为例,我们看下面的伪代码:

Lock lock = new ReentrantLock();
       // 获取锁
       lock.lock();
       try {
           // do some thing
       }finally {
           // 释放锁
           lock.unlock();
       }

这里有二点要注意:

  • 获取锁的过程不要一定要写在try外面,避免因为获取锁失败,导致lock锁被无故释放

  • 一定要在finally里面释放锁,保证锁获取之后,最终能够释放锁

 

Lock接口比synchronized优势

lock和synchronized相比,主要优势在于提供了下列几种特有的性质:

  • 尝试非阻塞的获取锁 tryLock():当前线程尝试获取锁,如果没有此时锁没有被其他线程获取到,则当前线程获取成功并持有锁

  • 能够被中断的获取锁 lockInterruptibly():获取到锁的线程能够被响应中断,被中断的线程将抛出异常,同时释放所持有的锁

  • 超时获取锁 tryLock(long, TimeUnit):在指定时间内获取锁,如果时间到了,还未获取到锁就立即返回

 

景点二:队列同步器(AbstractQueuedSynchronizer)

什么是AQS?

队列同步器是用来构建锁或者其他同步组件的基础元素,主要是使用一个int成员变量来表示同步状态,通过一个FIFO队列来完成资源的获取线程的排队工作,可以理解为,锁是面向开发者的,队列同步器是面向锁的实现的;

 

如何使用AQS?

同步器的设计是基于模板的。使用者需要重写同步器指定的方法,然后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法。而这些模板方法就是调用同步器使用者重写的方法;

重写同步器时,需要使用同步器提供的三个方法来访问或者修改同步状态:

  • getState():获取当前同步状态

  • setState(int  newState):设置当前同步状态

  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能保证状态设置的原子性

 

可重写的方法有如下五个:

  • tryAcquire(int arg) :独占式获取同步状态,该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态

  • tryRelease(int arg) :独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态

  • tryAcquireShared(int  arg) :共享式获取同步状态,返回大于等于0的值,表示获取成功,否则失败

  • tryReleaseShared(int arg): 共享式释放同步状态

  • isHeldExclusively() :当前同步器是否在独占模式下被线程占用,一般该方法表示是否被前当线程多独占

 

同步器提供的模板方法:

  • acquire(int arg) :独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg) 方法

public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }
  • acquireInterruptibly(int arg): 与acquire(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回

public final void acquireInterruptibly(int arg)
       throws InterruptedException {
   if (Thread.interrupted())
       throw new InterruptedException();
   if (!tryAcquire(arg))
       doAcquireInterruptibly(arg);
}
  • tryAcquireNanos(int arg,long nanos):在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);
   }
  • release(int arg) :独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒

public final boolean release(int arg) {
       if (tryRelease(arg)) {
           Node h = head;
           if (h != null && h.waitStatus != 0)
               unparkSuccessor(h);
           return true;
       }
       return false;
   }
  • acquireShared(int arg): 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。与独占式的不同是同一时刻可以有多个线程获取到同步状态

public final void acquireShared(int arg) {
       if (tryAcquireShared(arg) < 0)
           doAcquireShared(arg);
   }
  • acquireSharedInterruptibly(int arg) :与acquire(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回

public final void acquireSharedInterruptibly(int arg)
           throws InterruptedException {
       if (Thread.interrupted())
           throw new InterruptedException();
       if (tryAcquireShared(arg) < 0)
           doAcquireSharedInterruptibly(arg);
   }
  • (int arg,long nanos):在acquireSharedInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
          throws InterruptedException {
      if (Thread.interrupted())
          throw new InterruptedException();
      return tryAcquireShared(arg) >= 0 ||
          doAcquireSharedNanos(arg, nanosTimeout);
  }
  • releaseShared(int arg) :共享式释放同步状态

public final boolean releaseShared(int arg) {
       if (tryReleaseShared(arg)) {
           doReleaseShared();
           return true;
       }
       return false;
   }
  • getQueuedThreads(): 获取等待在同步队列上的线程集合

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;
   }

同步器提供的上述方法主要分为三类:

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

  • 共享式获取与释放同步状态

  • 查询同步队列中的等待线程情况

 

队列同步器的实现

同步队列

同步器依赖内部的同步队列(FIFO)来完成同步状态的管理,过程如下:当前线程获取同步状态失败时,同步器就会将当前线程以及等待状态的信息构成一个节点并加入到同步队列中,同时阻塞当前线程,当同步状态释放时,会将首节点的线程唤醒,再次尝试获取同步状态:

对于Node节点我们来了解下,看下面源码对Node的定义:

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;
       /**
        *   等待状态值,分为以下状态值:
        *
        *   SIGNAL:     值为-1 ,后续节点处于等待状态,而当前节点的线程如果
        *               释放了同步状态或者取消等待,节点进入该状态不会变化          
        *   CANCELLED:  值为 1,由于在同步队列中等待的线程等待超时或者被中断
        *               需要从同步队列中取消等待,节点进入该状态将不会变化                    
        *   CONDITION:  值为-2,节点在等待队列中,节点线程等待在Condition上,
        *               当其他线程对Condition调用了signal()方法后,该节点将会
        *               从等待队里中转移到同步队列中,加入对同步状态的获取中    
        *   PROPAGATE:  值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去
        *            
        *   0:          初始化状态
        */  
       volatile int waitStatus;
       // 前驱节点,当节点加入同步队列时被设置(尾部添加)
       volatile Node prev;
       // 后继节点
       volatile Node next;
       // 获取同步状态的线程
       volatile Thread thread;
       // 等待队列中的后继节点。如果当前节点是共享的,那么这个字段是一个shared常量,
       // 也就是说节点类型(独占或共享)和等待队列中个后继节点共用同一个字段
       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;
       }
   }

我们再来介绍下同步队列的结构:同步器拥有首节点和尾节点,没有成功获取同步状态的线程会成为节点加入该队列的尾部,如下图:

节点加入尾节点:如果一个线程没有获得同步队列,那么包装它的节点将被加入到队尾,显然这个过程应该是线程安全的。因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递一个它认为的尾节点和当前节点,只有设置成功,当前节点才被加入队尾,并真正与上个尾节点建立连接,过程如下:

首节点设置:首节点是获取同步状态成功的节点,首节点线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,并且与之前的首节点断开联系,过程如下图:

 

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

独占式同步状态的获取:独占式获取同步状态的方法是acquried(int arg),该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列移除,其源代码如下:

// 同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等操作
// 1.调用自定义同步器的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态
// 2.如果获取失败,就构造一个独占式(Node.EXCLUSIVE)的同步节点,并通过addWaiter方法加入到同步节点的尾部
// 3.最后调用acquiredQueued方法,该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程中断来实现
public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

addWaiter方法如下:

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)) {
               pred.next = node;
               return node;
           }
       }
       enq(node);
       return node;
   }

enq方法如下:利用死循环不断地尝试设置尾节点

private Node enq(final Node node) {
   // 死循环

       for (;;) {
           Node t = tail;          

           // 如果尾节点为空 则进行初始化

           if (t == null) {
               if (compareAndSetHead(new Node()))
                   tail = head;
           } else {
               node.prev = t;
               // 利用CAS设置尾节点
               if (compareAndSetTail(t, node)) {
                   t.next = node;
                   return t;
               }
           }
       }
   }

 

acquireQueued方法如下:节点进入同步队列以后,就要进入一个等待阶段。这是一个自旋的过程,每个节点都在不停地观察,看看有没有机会获取同步状态。如果获取到同步状态,就可以从自旋过程中退出

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);
       }
   }

节点自旋过程如下图:

独占式同步状态的释放:最后队列调用同步器的release的方法进行同步状态的释放,该方法释放了同步状态后,就会唤醒其后继节点,源码如下:

public final boolean release(int arg) {
       if (tryRelease(arg)) {
           Node h = head;
           if (h != null && h.waitStatus != 0)
               unparkSuccessor(h);
           return true;
       }
       return false;
   }

上述方法执行完,会唤醒头节点的后继节点线程,unparkSuccessor通过使用LockSupport在唤醒处于等待状态的线程:

private void unparkSuccessor(Node node) {
       int ws = node.waitStatus;
       if (ws < 0)
           compareAndSetWaitStatus(node, ws, 0);
       Node s = node.next;
       if (s == null || s.waitStatus > 0) {
           s = null;
           for (Node t = tail; t != null && t != node; t = t.prev)
               if (t.waitStatus <= 0)
                   s = t;
       }
       if (s != null)
           LockSupport.unpark(s.thread);
   }

 

共享式同步状态获取与释放

共享式获取与独占式获取的区别就是同一时刻是否可以多个线程同时获取到同步状态。以文件的读写来说,读操作的话同一时刻可以有很多线程在进行并阻塞写操作,但是写操作只能有一个线程在写并阻塞所有读操作。

共享式同步状态的获取:调用同步器的acquireShare(int arg) 方法可以共享式地获取同步状态,源码如下:

public final void acquireShared(int arg) {
       if (tryAcquireShared(arg) < 0)
           doAcquireShared(arg);
   }

doAcquireShared方法如下:在这个方法中,同步器调用tryAcquireShared方法尝试获取同步状态,tryAcquireShared返回值是一个int类型,当返回值大于0时,表示能够获取到同步状态。因此同步队列里的节点结束自旋状态的条件就是tryAcquireShared返回值大于0

private void doAcquireShared(int arg) {
       final Node node = addWaiter(Node.SHARED);
       boolean failed = true;
       try {
           boolean interrupted = false;
           for (;;) {
               final Node p = node.predecessor();
               if (p == head) {
                   int r = tryAcquireShared(arg);
                   if (r >= 0) {
                       setHeadAndPropagate(node, r);
                       p.next = null; // help GC
                       if (interrupted)
                           selfInterrupt();
                       failed = false;
                       return;
                   }
               }
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

 

共享式同步状态的释放:该方法在释放同步状态后,将会唤醒后续处于等待状态的节点,和独占式最大的区别是tryReleaseShared方法必须确保是同步状态线程安全释放,因为释放同步状态的操作会同时来自多个线程,所以一般使用CAS来保证线程安全,源码如下:

public final boolean releaseShared(int arg) {
       if (tryReleaseShared(arg)) {
           doReleaseShared();
           return true;
       }
       return false;
   }

 

private void doReleaseShared() {
       for (;;) {
           Node h = head;
           if (h != null && h != tail) {
               int ws = h.waitStatus;
               if (ws == Node.SIGNAL) {
                   if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                       continue;            // loop to recheck cases
                   unparkSuccessor(h);
               }
               else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                   continue;                // loop on failed CAS
           }
           if (h == head)                   // loop if head changed
               break;
       }
   }

 

独占式超时获取同步状态

通过调用同步器的doAcquireNanos方法可以超时获取同步状态,也就是说可以在给定时间获取同步状态,如果获取到了则返回true,否则返回false,源码如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
       throws InterruptedException {
       long lastTime = System.nanoTime();
       final Node node = addWaiter(Node.EXCLUSIVE);
       boolean failed = true;
       try {
           for (;;) {
               final Node p = node.predecessor();
               if (p == head && tryAcquire(arg)) {
                   setHead(node);
                   p.next = null; // help GC
                   failed = false;
                   return true;
               }
               if (nanosTimeout <= 0)
                   return false;
               if (shouldParkAfterFailedAcquire(p, node) &&
                   nanosTimeout > spinForTimeoutThreshold)
                   LockSupport.parkNanos(this, nanosTimeout);
                   
               // 先计算最后期限deadline,deadline=系统当前时间+nanosTimeout(超时时间),
                                         // 当线程唤醒后用deadline-系统当前时间,如果小于0,那么超时,
          // 否则还需要睡眠nanosTimeout = deadline - 系统当前时间

               long now = System.nanoTime();
               nanosTimeout -= now - lastTime;
               lastTime = now;
               if (Thread.interrupted())
                   throw new InterruptedException();
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章