JAVA并发包解析(二)——常用同步器分析1

235 阅读3分钟

前言

上一篇文章分析AQS的实现原理,AQS留下了几个try开头的方法让子类实现,根据这几个方法可以实现不同功能的同步器,看一下concurrent包下各种同步器的实现细节。

正文

CountDownLatch

CountDownLatch是一个计数器,可以让线程等待其他线程执行完毕后再进行执行,比如执行一段业务逻辑需要进行多个远程调用来聚合结果,可以使用多线程来加速接口的访问速度,使用CountDownLatch来等待远程调用的结果。示例代码如下:

  public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(2);
        startThread(1, count);
        startThread(2, count);
        count.await();
        System.out.println("结束");
    }

    public static void startThread(int id, CountDownLatch count) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    System.out.println("执行线程" + id);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    count.countDown();
                }
            }
        }).start();
    }

CountDownLatch维护了一个计数器,调用await方法,当计数器不为0的时候阻塞。根据这个思路,await应该调用的是AcquireShared方法,当tryAcquireShared判断不为0时候进入自旋。源码如下:

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

使用构造函数传入一个初始值,tryReleaseShared使用原子方法给计数器减1,当为0的时候返回true,意味着AQS的releaseShared方法可以唤醒阻塞线程。

public void countDown() {
        sync.releaseShared(1);
    }

  public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

++可以看到CountDownLatch的实现并不复杂,只要调用了await且state不为0,那么线程就会阻塞住直到其他线程将计数器减为0++

ReentrantLock

ReentrantLock是一个独占的可重入锁,基本上功能和synchronized差不多,但是ReentrantLock是可重入且支持公平和非公平模式的。

//当前线程持否持有锁
rotected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
final boolean isLocked() {
            return getState() != 0;
        }

final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

由于ReentrantLock是排他锁,只能一个线程持有锁,state代表了重入的次数,当state为0的时候表示没有线程占有锁,所以需要几个方法来判断当前线程是否持有锁。

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

释放锁的方法,如果当前线程没有持有锁就抛出异常。逻辑是将state减少1,如果减少为0则表示释放了锁,这时候AQS则会触发通知唤醒下一个节点。

ReentrantLock支持公平和飞公平模式,看一下两种模式的区别

  • 非公平锁
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;
        }

当state为0的时候尝试获取锁,如果成功则设置当前线程为锁持有者,如果锁已经被持有则判断当前线程是否持有锁,如果是就重入次数+1

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

非公平锁的首先尝试获取锁,如果没有获取锁才进入acquire方法

  • 公平锁
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;
        }

公平锁的主要的区别在于hasQueuedPredecessors,是否有其他线程正在排队获取锁,如果有其他线程正在排队获取锁,那么这个当前线程不会获取锁而是加入自旋排队。

final void lock() {
            acquire(1);
        }

公平锁直接进入AQS的acquire方法,执行tryAcquire逻辑判断是否有其他线程正在等待

公平锁和非公平锁的区别在于:非公平在调用lock方法的时候会尝试直接获取锁,这个时候有其他线程正在阻塞中也有可能直接获取到,但是公平锁则是有先后顺序。

++ReentrantLock也支持条件变量,使用newCondition来新建一个条件变量,当获取锁以后可以使用wait来阻塞当前线程,之前的文章提到过Condition使用方法++

CyclicBarrier

CyclicBarrier内部的一组线程互相等待执行完毕后再执行下一步,和CountDownLatch有些相似;CountDownLatch只能使用一次,而CyclicBarrier能使用多次,其中实现原理也不相同。示例代码如下:

private static CyclicBarrier cyclicBarrier;
    public static void main(String[] args) throws InterruptedException {
        cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("执行");
            }
        });
        startThread(1);
        startThread(2);
    }

    public static void startThread(int id) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    System.out.println("执行线程" + id);
                    cyclicBarrier.await();
		System.out.println("线程到达" + id);
                } catch (BrokenBarrierException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

源码如下:

private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    /** The number of parties */
    private final int parties;
    /* The command to run when tripped */
    private final Runnable barrierCommand;
    /** The current generation */
    private Generation generation = new Generation();

    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;

CyclicBarrier内部使用了ReentrantLock和Condition的组合,看一下await方法

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

调用了dowait不带超时时间

lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

dowait的上半部分代码,首先加锁在判断线程是否被中断抛出异常;否则对count进行自减,当count为0的时候说明所有线程都已经准备好了,如果我们事先传递了Runnable就执行这段逻辑,并且调用nextGeneration。

private void nextGeneration() {  
        trip.signalAll();
        count = parties;
        generation = new Generation();
    }

nextGeneration的作用是唤醒被阻塞的线程并且重置计数器以便下次使用。如果index不为0说明还有其他线程没有执行完毕,所以进入下一段代码

 for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }

其实核心使用条件变量将这个线程给阻塞,其他就是对异常情况的处理,因为在阻塞过程中可能会有其他线程重置了CyclicBarrier,于是需要抛出异常。

++CyclicBarrier核心就是使用count来进行计数,ReentrantLock和Condition的配合使用,每次调用都将count减少,当某个调用减少为0的时候就唤醒所有被阻塞的线程++

总结

这篇文章讲了 CountDownLatch、ReentrantLock和CyclicBarrier,在AQS的的基础上进行扩展,实现起来非常简单。