java线程原理多线程并发基础

Lock底层实现

2018-08-02  本文已影响0人  kindol

Lock完全用Java写成,无关JVM实现。

总概

在java.util.concurrent.locks包中有很多Lock的实现类,如ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路大同小异,下面主要讲ReentrantLock

ReentrantLock把所有Lock接口的操作都委派到Sync类上,该类继承了AbstractQueuedSynchronizer,多数需要覆盖的方法都是在Sync类实现的,而NonfairSync、FairSync只是在tryAcquire方法和lock方法上有所不同。

static abstract class Sync extends AbstractQueuedSynchronizer  

Sync又有两个子类

static final class NonfairSync extends Sync {
    /**
     * 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);
    }

    //默认是非公平锁,所以nonfairTryAcquire方法放在了Sync里面
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
    
    protected final boolean tryAcquire(int acquires) {...}
}

显然是为了支持公平锁与非公平锁

Reentrant.lock()的调用过程(非公平锁)

T4.gif

AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流程。

加锁的实现

AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。

在代码的实现上,首先会有调用公平锁(或者非公平锁)的lock,内部调用acquire(1),再到AQS里面

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

当中的tryAcquire在下面列出,也就是尝试获取锁,如果失败,那么将其加入CLH队列中,看看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;
}

这段代码中先构造了一个节点,接着获取当前同步队列中尾节点,如果尾节点不为空,尝试使用compareAndSetTail设置尾节点,如果成功则会直接返回,失败则调用enq方法不断尝试加入尾节点,失败的原因可能不只是并发,还有可能是空队列

private Node enq(final Node node) {
    //死循环
    for (;;) {
        Node t = tail;
        //如果是空队列
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
        //设置尾节点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

紧接着,返回了当前新建立的node节点,看回到acquire方法中,发现addWaiter外面还包了一层函数,没错,就是acquireQueued,这个方法的作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回,再进去看看

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //死循环,不过这个方法既然是阻塞那就肯定不会死循环了!!!
        for (;;) {
            final Node p = node.predecessor();
            如果当前加入的节点前面一个节点是头结点,那么阻塞该节点前再次调用tryAcquire尝试获取锁
            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);
    }
}

当中又出现了新的方法shouldParkAfterFailedAcquire,不过这次不仔细看了,这个方法主要检查Node节点的mode信息,mode信息是在第一次创建该节点的时候传入的,检查规则有3个:

当然了,如果parkAndCheckInterrupt()返回false,那么设置interrupted = true(意味着解锁了),之后又进入无限循环;在这里可以看到,并不是得到解锁的线程一定能获得锁,必须在这个死循环里调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞

重入锁

需要解决两个问题:

  1. 线程再次获得锁
  2. 锁的最终释放(线程重复n次获得锁,随后在第n次释放该锁后,其他线程能够获得该锁)

处理方式:
在覆盖tryAcquire的时候(包含公平与非公平锁),加入再次获取同步状态处理逻辑——通过判断当前线程是否为获得锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true。

protected final boolean tryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   //当前锁没被线程占有
   if (c == 0) {        
        //hasQueuedPredecessors()是公平锁需要加入的
       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;
}

那么在释放重入锁的时候,也是对同步状态值进行减少

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //判断当前线程是否为自身
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

解锁

跟加锁思路差不多,不过更简单,先tryRelease再unparkSuccessor

公平锁与非公平锁的实现区别

公平锁表示:在锁的获取顺序就应该符合请求的绝对时间请求,也就是FIFO

跟非公平锁的方法相比,公平锁tryAcquire的实现唯一的不同就是判断条件多了个hasQueuedPredecessors(),即加入了同步队列中当前节点是否有前驱节点的判断,有则表示前面的节点应该比当前节点先获得锁

ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

读写锁

遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级成为读锁

读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键

LockSupport

LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也称为构建同步组件的基础工具。

park阻塞当前线程、unpark唤醒一个被阻塞的线程

上一篇下一篇

猜你喜欢

热点阅读