Java常见Lock(四):lock之Semaphore、CountDownLatch、StampedLock

Semaphore

public class Semaphore implements java.io.Serializable

  1. 一个计数信号量。
  2. 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

 从概念上讲,信号量维护了一个许可集state。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
 当信号量==1时,是一个互斥信号量,可作一个相互排斥锁。
 分为公平和非公平两种模式。公平模式是FIFO顺序来选择现场、获得许可。非公平模式不对线程获取许可的顺序做任何保证,允许插队。

成员变量
	 /** 所有操作都通过该AbstractQueuedSynchronizer子类Sync */
    private final Sync sync;
构造方法
    /**
     * 创建具有给定的许可数和非公平的公平设置的 Semaphore。
     */
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    /**
     * 创建具有给定的许可数和给定的公平设置的 Semaphore。
     */
    public Semaphore(int permits, boolean fair) {
        sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
    }

acquire() or acquire(int permits):acquire()等价于acquire(1), 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。

	/*Semaphore.Sync调用的AQS.acquireSharedInterruptibly*/
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    /*tryAcquireShared在Semaphore.FairSync or NonFairSync中覆写*/
    /**
     * Fair version
     */
    final static class FairSync extends Sync {
        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            Thread current = Thread.currentThread();
            for (;;) {
                Thread first = getFirstQueuedThread();
                if (first != null && first != current)
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
    /**
     * NonFair version
     */
    final static class NonfairSync extends Sync {
        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }
    
    /**
     * NonFair version
     */
    final int nonfairTryAcquireShared(int acquires) {
	    for (;;) {
	        int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
	            compareAndSetState(available, remaining))
                return remaining;
            }
    }
    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

从源码看出FairSync自己覆写了tryAcquireShared,NonfairSync也覆写了,但调用的是父类Sync的nonfairTryAcquireShared方法。

acquireUninterruptibly() :acquireUninterruptibly() == acquireUninterruptibly(1),从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。

acquireUninterruptibly和acquire基本类似,只是前者如果当前的线程在等待许可时被中断,则它会继续等待并且它在队列中的位置不受影响。后者会抛出InterruptedException。

acquire总体可分为四步:

  1. 如果acquire(arg)成功,那就没有问题,已经拿到锁,整个lock()过程就结束了。如果失败进行操作2。
  2. 创建一个独占或共享节点(Node)并且此节点加入CHL队列(CHL队列是一个非阻塞的 FIFO 队列)末尾。进行操作3。
  3. 自旋尝试获取锁,失败根据前一个节点来决定是否挂起(park()),直到成功获取到锁。进行操作4。
  4. 如果当前线程已经中断过,那么就中断当前线程(清除中断位)。

带有超时性质的会在第三步加入超时判断,如果操作是不可中断的,则没有第四步,若acquireUninterruptibly,会一直自旋尝试。

boolean tryAcquire():等价于tryAcquire(1),仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。

即使已将此信号量设置为使用公平排序策略,但是调用 tryAcquire 也将 立即获取许可(如果有一个可用),而不管当前是否有正在等待的线程。在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。如果希望遵守公平设置,则使用 tryAcquire(permits, 0, TimeUnit.SECONDS) ,它几乎是等效的(它也检测中断)。

    public boolean tryAcquire(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        return sync.nonfairTryAcquireShared(permits) >= 0;
    }

tryAcquire其实和acquire实现前部分是相同的,后者多了若当时未获取到锁会自旋等待,有公平和非公平两种策略,而tryAcquire只走非公平策略。
tryAcquire也支持给定的等待时间内自旋尝试获取锁。

    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);//给定的等待时间内自旋尝试获取锁
    }

release() :等价于release(1),释放给定数目的许可,将其返回到信号量。

	/*Sync父类AQS.releaseShared方法*/
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();//唤醒等待线程
            return true;
        }
        return false;
    }
    /*Sync覆写父类AQS.tryReleaseShared方法*/
   protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int p = getState();
                if (compareAndSetState(p, p + releases))
                    return true;
            }
        }

从源码中看出信号量获取、释放动作是由子类Sync实现,而动作成功与否的后续行为由AQS来实现,主要是线程的阻塞、唤醒,即 LockSupport.park() 和 LockSupport.unpark()。

CountDownLatch

public class CountDownLatch

  1. 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
  2. 用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
  3. CountDownLatch 是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。
  4. CountDownLatch 的一个有用特性是,它不要求调用 countDown 方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个 await。
    #####构造方法
    /**
     *  构造一个用给定计数初始化的 CountDownLatch。
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

await() :使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

   public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);//AQS父类方法
}
   /*AQS.acquireSharedInterruptibly方法*/
   public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    /*Sync覆写父类AQStryAcquireShared方法*/
	public int tryAcquireShared(int acquires) {
		return getState() == 0? 1 : -1;
	}

从源码看出计数判断子类维护, AQS定义volatile int state保证其可见性。
AQS再根据子类的结果判断是否唤醒线程,await(long timeout, TimeUnit unit) 相比增加了超时判断操作。

void countDown():递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

    public void countDown() {
        sync.releaseShared(1);
    }
    
    /*AQS.releaseShared*/
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
	    /*Sync覆写的AQS.releaseShared*/
        public boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

long getCount(): 返回当前计数。

    public long getCount() {
        return sync.getCount();
    }
    
    int getCount() {
	    return getState();
    }

Semaphore、CountDownLatch(把信号量或计数也理解成锁):

  1. 都可以做互斥信号量。
  2. Semaphore支持可中断、不可中断和超时三种获取锁操作,而CountDownLatch只有可中断和超时两种。
  3. 锁的维护均有自己的内部类Sync实现(获取、释放),维护后的行为由AQS实现,线程的阻塞和唤醒。
  4. Semaphore可以理解成实例化时固定创建N个许可,同时至多有N个线程获得许可。其它线程只能等已拥有的线程释放许可,才可能获得许可。
  5. CountDownLatch可以理解实例化时创建N个前置条件,只有所有前置条件都被完成countDown,等待线程才会被释放,否则一直等待。

StampedLock

public class StampedLock implements java.io.Serializable

  1. 支持读、写、乐观读锁三种模式。
  2. jdk1.8中java.util.concurrent.locks新增的一个API。
  3. 基于CLH(自旋锁),CLH锁是一种自旋锁,即自旋等待,保证没有饥饿的发生,且可保证FIFO操作顺序.
writeLock独占锁,同时只有一个线程可以获取该锁,其它写线程必须等待
readLock共享锁,写锁未被持有时,同时多个线程可以获取该锁,如果持有,其它线程阻塞
OptimisticRead乐观读锁,乐观地认为写锁未被持有,操作数据前没有通过CAS设置状态,
write和read类似
writeLock() 阻塞且不可中断
tryWriteLock() 立即返回
tryWriteLock(long time, TimeUnit unit)有时间限制的阻塞,可响应中断
writeLockInterruptibly()阻塞、可响应中断
unlockWrite(long stamp)释放锁

乐观读锁:在获取锁时,会判断写锁是否被其它线程持有,若持有,则立即返回0,否则返回一个非0的stamp版本信息,此时可以认为拥有读锁了,但在获取数据至获取到数据及处理数据的过程中,写锁有可能被其它线程持有,即数据可能发生了变化,所以在使用乐观读锁时,要考虑该问题,比如使用violate保证内存可见性,或者判断在此期间判断是否存在写变更,然后重新获取变更后的数据或抛出异常。 但不可否认,乐观读锁适用于读多写少的场景,显著提高程序吞吐量。

在使用获取锁时,不要使用带有中断状态的线程去执行获取阻塞且不响应中断锁的操作,容易造成无限死循环,造成系统满负荷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值