06-Java多线程、并发工具类之Semaphore和Exchanger

一、Semaphore

1.1 概念

  • 信号量 Semaphore 是一个控制访问多个共享资源的计数器,和 CountDownLatch 一样,其本质上是一个“共享锁”。
    Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

1.2 示例

  • 为了简单起见我们假设停车场仅有5个停车位。一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车,然后又来三辆,这个时候由于只有两个停车位,所有只能停两辆,其余一辆必须在外面候着,直到停车场有空车位。当然,以后每来一辆都需要在外面候着。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆,要看选择的机制是公平还是非公平)。从程序角度看,停车场就相当于信号量Semaphore ,其中许可数为5,车辆就相对线程。当来一辆车时,许可数就会减1 。当停车场没有车位了(许可数== 0 ),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数 + 1,然后放进来一辆车。信号量 Semaphore 是一个非负整数( >=1 )。当一个线程想要访问某个共享资源时,它必须要先获取 Semaphore。当 Semaphore > 0 时,获取该资源并使Semaphore – 1 。如果Semaphore值=0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore 则 +1 。

1.2.1 示例代码

  • 代码
/**
 * @author by mozping
 * @Classname Park
 * @Description TODO
 * @Date 2019/5/28 11:43
 */
public class Park {

    //初始化5代表有5个车位
    static Semaphore semaphore = new Semaphore(5);

    //Car线程模拟车辆去停车
    static class Car implements Runnable {

        @Override
        public void run() {
            try {
                //1.开始停车,先获取许可(相当于获取一个车位)
                semaphore.acquire();
                //2.模拟停车3秒钟
                System.out.println(Thread.currentThread().getName() + " 开始准备停车,剩余车位是: " + semaphore.availablePermits() + " -- " + new Date());
                SleepTools.randomSecond(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //3.停车完毕释放(归还许可,相当于释放车位)
                semaphore.release();
                System.out.println(Thread.currentThread().getName() + " 线程停车完毕,剩余车位是: " + semaphore.availablePermits() + " -- " + new Date());
            }
        }
    }

    public static void main(String[] args) {

        //模拟10辆车去停车,只有5个车位,需要同步
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Car(), "[Car-" + (i + 1) + "]");
            thread.start();
        }
    }
}
  • 打印
[Car-4] 开始准备停车,剩余车位是: 2 -- Tue May 28 12:19:49 CST 2019
[Car-1] 开始准备停车,剩余车位是: 4 -- Tue May 28 12:19:49 CST 2019
[Car-6] 开始准备停车,剩余车位是: 0 -- Tue May 28 12:19:49 CST 2019
[Car-2] 开始准备停车,剩余车位是: 3 -- Tue May 28 12:19:49 CST 2019
[Car-5] 开始准备停车,剩余车位是: 1 -- Tue May 28 12:19:49 CST 2019
[Car-2] 线程停车完毕,剩余车位是: 0 -- Tue May 28 12:19:52 CST 2019
[Car-3] 开始准备停车,剩余车位是: 0 -- Tue May 28 12:19:52 CST 2019
[Car-8] 开始准备停车,剩余车位是: 0 -- Tue May 28 12:19:53 CST 2019
[Car-5] 线程停车完毕,剩余车位是: 1 -- Tue May 28 12:19:53 CST 2019
[Car-7] 开始准备停车,剩余车位是: 0 -- Tue May 28 12:19:59 CST 2019
[Car-4] 线程停车完毕,剩余车位是: 1 -- Tue May 28 12:19:59 CST 2019
[Car-6] 线程停车完毕,剩余车位是: 1 -- Tue May 28 12:20:02 CST 2019
[Car-10] 开始准备停车,剩余车位是: 0 -- Tue May 28 12:20:02 CST 2019
[Car-3] 线程停车完毕,剩余车位是: 2 -- Tue May 28 12:20:09 CST 2019
[Car-9] 开始准备停车,剩余车位是: 1 -- Tue May 28 12:20:09 CST 2019
[Car-8] 线程停车完毕,剩余车位是: 2 -- Tue May 28 12:20:09 CST 2019
[Car-1] 线程停车完毕,剩余车位是: 2 -- Tue May 28 12:20:10 CST 2019
[Car-7] 线程停车完毕,剩余车位是: 3 -- Tue May 28 12:20:26 CST 2019
[Car-10] 线程停车完毕,剩余车位是: 4 -- Tue May 28 12:20:29 CST 2019
[Car-9] 线程停车完毕,剩余车位是: 5 -- Tue May 28 12:20:33 CST 2019
  • 结论
我们看到最开始的5个线程可以立刻获取到许可进行停车,后面的车辆就需要前面的车辆停车完毕之后才能进行停车。
但是我们看到的打印顺序会有些不一样,比如Car-5停车完毕之后,Car-8才能开始停车,但是打印的顺序会有不一样,
最开始的5个线程打印顺序也不一样,如果要求一样的话可以把release和后面的打印交换位置,但是计算许可的时候就需要+1
因为那样打印的时候还没有归还许可。

1.3 源码分析

  • Semaphore支持2种获取许可的模式,公平和非公平模式。内部实现机制如下:内部Sync类继承自AQS,内部实现对AQS类的state变量的操作,实际上state就是对应许可的数目,许可的增减操作就是对state变量的相关操作。
  • 公平模式:FairSync。FairSync继承Sync,在获取许可时判断是否有前驱节点,有的话自己就返回,不会获取许可,没的话才会去获取许可。
  • 非公平模式:NonfairSync。NonfairSync继承Sync,在获取许可的时候,不会维护线程的关系,直接操作state变量。默认是非公平模式的。
  • 下面是的内部类之间的关系图
    [外链图片转存失败(img-XJ6U5TbE-1565593298954)(https://note.youdao.com/yws/api/personal/file/0DA81E1F7ED44DBBBD4E4883499784C5?method=download&shareKey=edcaf00e886c03c1f0dd766a9305c91f)]

1.3.1 构造方法

  • 需要一个参数指定初始的许可数目,默认是非公平模式
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

1.3.2 Sync

  • semaphore的同步功能实现,使用AQS的state代表许可数目,子类实现公平和非公平的模式
/**
     * Synchronization implementation for semaphore.  Uses AQS state
     * to represent permits. Subclassed into fair and nonfair
     * versions.
     * semaphore的同步功能实现,使用AQS的state代表许可数目,子类实现公平和非公平的模式
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        //初始化许可数目
        Sync(int permits) {
            setState(permits);
        }

        //获取许可数目
        final int getPermits() {
            return getState();
        }

        //非公平模式下获取许可方法
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();//当前许可数目
                int remaining = available - acquires;//计算剩余许可数目
                if (remaining < 0 || compareAndSetState(available, remaining)) //修改许可数量为剩余许可数量
                    return remaining; //返回剩余许可数量
            }
        }

        //释放许可的方法
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases; //计算可用许可数
                if (next < current) // overflow releases不能为负数
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next)) //修改许可数量
                    return true;
            }
        }

        //减少许可的方法
        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();// 获取许可
                int next = current - reductions;  //减少reductions个许可后,可用的许可是next
                if (next > current) // underflow  这里说明reductions是负数,不允许
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next)) //将许可数量设置为next
                    return;
            }
        }

        // 获取可用的所有许可
        final int drainPermits() {
            for (;;) { // 无限循环
                int current = getState(); // 获取许可
                //许可如果为0那就直接返回,许可大于0,那么就将许可设置为0,并且返回当前的许可数目,即获取了当前可以用的全部许可
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

1.3.3 NonfairSync

  • 非公平模式
/**
     * NonFair version
     * 非公平模式下信号量的实现
     */
    static final class NonfairSync extends java.util.concurrent.Semaphore.Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        //初始化
        NonfairSync(int permits) {
            super(permits);
        }

        //获取许可,调用Sync的nonfairTryAcquireShared方法,非公平模式下获取许
        //可,nonfairTryAcquireShared里面会直接操作state变量,通过CAS设置state
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

1.3.4 FairSync

  • 公平模式
/**
     * Fair version
     * 公平模式下信号量的实现
     */
    static final class FairSync extends java.util.concurrent.Semaphore.Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        //初始化
        FairSync(int permits) {
            super(permits);
        }

        //获取许可,因为是公平模式,需要考虑线程的公平性,(先排队的线程要先获取到许可)
        //因此会判断自己在线程排队队列中是否为前驱节点,有的话自己就返回-1,实际上就是
        // 获取失败,如果没有前驱的话就CAS操作state获取许可,后面这个过程和非公平模式是
        // 一样的
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                        compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

1.3.5 获取许可

1.3.5.1 acquire()
  • acquire
    public void acquire() throws InterruptedException {
        //调用AQS的方法,共享模式获取state,可以响应中断
        sync.acquireSharedInterruptibly(1);
    }
1.3.5.2 acquireUninterruptibly()
  • acquireUninterruptibly
public void acquireUninterruptibly() {
        //调用AQS的方法,不能响应中断
        sync.acquireShared(1);
    }
1.3.5.3 tryAcquire()
  • 尝试获取许可,获取成功返回true,反之返回false,方法不会阻塞,会立即返回
    public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }
1.3.5.4 tryAcquire(long timeout, TimeUnit unit)
    /**
    * 支持超时和中断的获取许可方法,如果没有获取到许可,线程会阻塞,直到下面条件之一
    * 得到满足:
    * 1.有线程释放许可,并且该线程获取到许可
    * 2.超时
    * 3.线程被中断
    */
    public boolean tryAcquire(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
1.3.5.5 acquire(int permits)
  • 获取指定数量的许可,支持响应中断
public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }
1.3.5.6 acquireUninterruptibly(int permits)
  • 获取指定数量的许可,不能响应中断
public void acquireUninterruptibly(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireShared(permits);
    }
1.3.5.7 tryAcquire(int permits)
  • 尝试获取指定数量的许可,获取成功返回true,反之返回false,线程不会阻塞,会立即返回
public boolean tryAcquire(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        return sync.nonfairTryAcquireShared(permits) >= 0;
    }
1.3.5.8 tryAcquire(int permits, long timeout, TimeUnit unit)
    /**
    * 支持超时和中断的获取指定数量许可方法,如果没有获取到许可,线程会阻塞,直到下面条件之一
    * 得到满足:
    * 1.有线程释放许可,并且该线程获取到许可
    * 2.超时
    * 3.线程被中断
    */
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
            throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
    }
  • 小结:其实上面8个方法中有4中场景,另外4中只是指定了许可的数量,默认前面四种是获取1个许可,
1.可以响应中断(阻塞式,可以响应中断)
2.不能响应中断(阻塞式,不能响应中断)
3.不会阻塞(尝试获取,立即返回)
4.超时阻塞(阻塞获取,支持超时和字段)

1.3.6 释放许可

1.3.6.1 release()
  • 释放一个许可
public void release() {
        sync.releaseShared(1);
    }
1.3.6.2 release(int permits)
  • 释放多个许可
    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

1.3.7 其他API

1.3.7.1 availablePermits()
  • 获取可用许可数量
public int availablePermits() {
        return sync.getPermits();
    }
1.3.7.2 drainPermits()
  • 获取当前全部可用许可
    public int drainPermits() {
        return sync.drainPermits();
    }
1.3.7.3 reducePermits()
  • 减少指定数量的许可
protected void reducePermits(int reduction) {
        if (reduction < 0) throw new IllegalArgumentException();
        sync.reducePermits(reduction);
    }
1.3.7.4 isFair()
  • 判断是否为公平模式
1.3.7.5 hasQueuedThreads()
  • 返回值来表示是否有其他线程正在等待获取信号量,但是不准确,这个方法主要是用来判断系统状态
public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
1.3.7.6 getQueueLength()
  • 返回等待获取信号量的线程数量的估计值
public final int getQueueLength() {
        return sync.getQueueLength();
    }
1.3.7.7 getQueuedThreads()
  • 返回等待获取信号量的线程集合
protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }

二、Exchanger

2.1 概念

  • Exchanger 是一个用于线程间协作的工具类,Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此
    的数据。这两个线程通过exchange 方法交换数据,如果第一个线程先执行exchange 方法,它会一直等待第二个线程也执行exchange 方法,当两个
    线程都到达同步点时,这两个线程就可以交换数据。
  • Exchanger 使用是非常简单的,但是实现原理和前面几种工具比较却是最难的,前面几种工具都是通过同步器或者锁来实现,而Exchanger是一种
    无锁算法,和前面SynchronousQueue 一样,都是通过循环 cas 来实现线程安全,因此这种方式就会显得比较抽象和麻烦。

2.2 示例

  • 代码
/**
 * @author by mozping
 * @Classname ExchangeTest
 * @Description Exchange的使用
 * @Date 2019/1/2 18:50
 */
public class ExchangeTest {

    private static final Exchanger<ArrayList<Integer>> exchange = new Exchanger<>();

    public static void main(final String[] args) {

         new Thread(new Runnable() {
            @Override
            public void run() {
                ArrayList<Integer> arr1 = new ArrayList<>();
                ArrayList<Integer> arr11 = new ArrayList<>();
                arr1.add(1);
                arr1.add(2);
                arr1.add(3);
                try {
                    arr11 =  exchange.exchange(arr1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SleepTools.randomSecond(5);
                System.out.println("Thread1原集合:"+arr1.toString());
                System.out.println("Thread1新集合:"+arr11.toString());
            }
        }).start();

         new Thread(new Runnable() {
            @Override
            public void run() {
                ArrayList<Integer> arr2 = new ArrayList<>();
                ArrayList<Integer> arr22 = new ArrayList<>();
                arr2.add(11);
                arr2.add(22);
                arr2.add(33);
                try {
                    arr22 = exchange.exchange(arr2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SleepTools.randomSecond(5);
                System.out.println("Thread2原集合:" + arr2.toString());
                System.out.println("Thread2新集合:" + arr22.toString());

            }
        }).start();

    }
}

  • 打印
Thread2原集合:[11, 22, 33]
Thread2新集合:[1, 2, 3]
Thread1原集合:[1, 2, 3]
Thread1新集合:[11, 22, 33]

2.3 源码分析

  • Exchanger部分的源码相对比较抽象,没有使用到AQS和锁机制,采用CAS来实现的,而且相对来说使用场景不多,以后有时间再分析吧。

三、小结

  • 我们重点是分析了Semaphore的使用和源码部分,Semaphore也是基于AQS来实现的,结合之前的CountDownLatch也是基于AQS来实现的,我们也看到了AQS在并发编程中的重要性,因为还没有重点分析AQS,所以实际上关于AQS的部分API我们还不是很清楚里面的实现和细节,另外Sync继承自AQS后我们看到其自身的代码是很简单的,后面我们分析AQS后就能明白,这样的简单是建立在AQS的基础之上的,AQS也是整个并发编程的基石。

四、参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值