【Java并发包】Semaphore使用详解以及源码解读

Java并发包下有很多并发操作的API,这些工具类为多线程环境提供很多常用操作,最为常见同步操作,我们可以使用synchronized关键字来进行同步操作。不过使用synchronized进行多线程之间的同步过于繁琐,有些操作要配合wait和notify使用,不便于我们操作,并且在有些情况下需要控制并发的线程数量,这一点是synchronized做不到的,因此,这篇文章主要来讲解semaphore
API的使用,它所提供的功能完全是synchronized关键字的升级版,它的很多功能仅仅使用synchronized关键无法做到的。

转载本文请标明原文链接:【原文链接
Semaphore从字面上理解是信号量等含义。这个类的主要作用是控制线程并发的数量,如果不进行限制,CPU的资源将会被耗尽,每一个线程所运行的任务异常的缓慢,严重的影响我们的工作,所以很有必要限制并发的线程数量。

Semaphore类中有很常用API,如下图所示:
这里写图片描述

Semaphore有两个构造函数,一个带有一个参数,另一个带有两个参数。
对于带一个参数的构造器Semaphore(int permits),它的参数permits是int类型变量,代表同一时间内,最多允许多少个线程执行acquire()方法和release()方法之间的代码。acquire()方法默认分配一个许可,release()释放许可,它们两个配合使用。先来看一个Demo:

package cn.just.thread.concurrent;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    //构造器参数:permits 表示同一时间内允许多少个线程执行acquire()和release()之间的代码
    public static Semaphore semaphore=new Semaphore(1);
    public static void testSemap() throws InterruptedException{
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName()+" begin time= "+System.currentTimeMillis());
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName()+" end time= "+System.currentTimeMillis());
        semaphore.release();
    }
    public static void main(String[] args) throws InterruptedException{
        Thread thread1=new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    testSemap();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread1.setName("A");
        Thread thread2=new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    testSemap();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread2.setName("B");
        Thread thread3=new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    testSemap();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread3.setName("C");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

上面程序的运行结果如下:

A begin time= 1526481444303
A end time= 1526481446303
B begin time= 1526481446304
B end time= 1526481448304
C begin time= 1526481448304
C end time= 1526481450305

上面的代码通过构造Semaphore,允许最多并发一个线程。从运行结果我们看出通过使用semaphore API,我们控制了同一时间只运行一个线程执行指定代码,并且只有在一个线程运行完毕释放了许可之后才会允许下一个线程获取许可进行执行。
不过在上面说到可以使用Semaphore(int permits)构造器允许并发多个线程,这就意味着这些并发的线程之间并不能保证线程安全,如果多个线程共同访问一个共享实例变量,极有可能会产生脏数据。
这一点我们可以通过acquire()方法的源码得以证明。

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

首先它调用了内部工具类Sync的方法acquireSharedInterruptibly,并且传入1这个参数,默认获取一个许可,Sync扩展了AbstractQueuedSynchronizer(AQS),AQS是Java并发包的基础,很多工具类都是由它实现,建议不熟悉AQS的读者参考相关资料,这里就不加以解释了。

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
            //如果可用许可为负数
        if (tryAcquireShared(arg) < 0)
            //共享许可
            doAcquireSharedInterruptibly(arg);
    }

在acquireSharedInterruptibly()方法中它会响应中断并且抛出异常,然后检查是否有可用的许可,如果可用的许可没有则返回一个负数,则会进入许可共享模式:

 /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //将当前进程加入等待队列,该队列是一个双向链表,并且使用了CAS操作来创建
        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;
                    }
                }
                //检查并且更新获取许可失败的节点的状态,如果当前线程阻塞则返回true,否则获取当前节点的前一个节点。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
//等待队列
 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;
    }

shouldParkAfterFailedAcquire方法的源码如下:
这里写图片描述

也就是说当一个线程调用acquire方法获取许可的时候,它会首先获取许可,如果没有可用许可则进入共享模式,并且将当前线程加入等待队列去获取许可,直到获取成功才返回退出循环。

acquire方法还有一个带参方法acquire(int permits),它的功能是每次调用一次该方法,就使用permits个许可。比如如果执行acquire(2)这个方法,一共有10个许可,那么10/2=5,就同一时间内允许5个线程执行acqurie()与release()之间的代码。

在上面的源码中我们可以看到acquire()方法响应中断,同样他也有不响应中断的方法acquireUninterruptibly()。

availablePermits()方法获取当前可用许可的个数,经常用于程序中调试使用。

 public int availablePermits() {
        return sync.getPermits();
    }

而drainPermits()方法获取当前可用许可数立即返回并且将许可数置为0。

public int drainPermits() {
        return sync.drainPermits();
    }

  final int drainPermits() {
            for (;;) {
                //获取当前许可数并且清0
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }

getQueueLength()方法的作用是取得等待许可的线程的个数。

public final int getQueueLength() {
        return sync.getQueueLength();
    }
 public final int getQueueLength() {
        int n = 0;
        //遍历队列,获取队列中线程数
        for (Node p = tail; p != null; p = p.prev) {
            if (p.thread != null)
                ++n;
        }
        return n;
    }

hasQueuedThreads()方法的作用是判断有没有线程在等待这个许可。

public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
public final boolean hasQueuedThreads() {
    //如果等待队列为空表示没有线程等待,如果不为空则表示还有线程进行等待
        return head != tail;
    }

在上面的介绍中,我们介绍了Semaphore的只带一个参数的API,它还有一个带两个参数的API,下面来看看这个构造器的作用。Semaphore(int permits, boolean fair)它的第二个参数是Boolean类型的参数,可以设置是否为公平信号量。如果设置为true,则表示为公平信号量,所谓公平信号量是获得许可的顺序与线程的启动无关,但不代表这百分之百的贤启动的线程先获取信号量,仅仅是在概率上有所提高和保证。而非公平信号量就是与线程启动顺序无关。

 public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

tryAcquire()方法的作用是尝试获取一个许可,如果获取不到返回false,否则返回true,它具有无阻塞的特点。

public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                //如果剩下的许可小于0则返回,最后tryAcquire返回false表示没有获取到许可
                //如果有可用许可,则使用CAS操作设置可用许可数,设置成功返回剩下的许可数,最后tryAcquire返回true表示可以获取到许可。
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

下面看一个简单的例子:

package cn.just.thread.concurrent;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo02 {
    //构造器参数:permits 表示同一时间内允许多少个线程执行acquire()和release()之间的代码
        public static Semaphore semaphore=new Semaphore(1);
        public static void testSemap() throws InterruptedException{
            if(semaphore.tryAcquire()){
                System.out.println("ThreadName="+Thread.currentThread().getName()+"首选进入");
                semaphore.release();
            }else{
                System.out.println("ThreadName="+Thread.currentThread().getName()+"没有成功进入");
            }

        }
        public static void main(String[] args) throws InterruptedException{
            Thread thread1=new Thread(){
                @Override
                public void run() {
                    super.run();
                    try {
                        testSemap();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread1.setName("A");
            Thread thread2=new Thread(){
                @Override
                public void run() {
                    super.run();
                    try {
                        testSemap();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread2.setName("B");

            thread1.start();
            thread2.start();
        } 
}

运行一次结果为:

ThreadName=B没有成功进入
ThreadName=A首选进入

可见如果获取不到许可tryAcquire()方法直接返回,并不进行阻塞。

同样的带参的tryAcquire(int permits)的作用是尝试获取permits个许可,如果可以获取到返回true,否则返回false。还有带两个参数的tryAcquire(long timeout, TimeUnit unit)方法是允许在指定时间内尝试是否可以获取一定数量的许可,如果可以获取返回true,否则返回false。

以上是Semaphore类的常用API介绍,有兴趣的读者可以自己尝试动手写一写加强记忆与理解。

Semaphore可以有效地对并发执行的任务的线程数进行控制,因此我们可以将其应用在pool池技术中,下面来看看如何使用Semaphore创建一个同时允许若干个线程访问池中数量的数据访问池,并且在池中使用重入锁进行了同步操作,只允许一个线程获取数据。(当然我们可以将其扩展为线程池以及数据库连接池等)。下面看代码实现:

package cn.just.thread.concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用semaphore实现pool池
 * @author shinelon
 *
 */
public class ListPool {
    private int poolMaxSize=3;
    private int semaphorePermits=5;
    private List<String> list=new ArrayList<String>();
    private Semaphore semaphore=new Semaphore(semaphorePermits);
    private ReentrantLock reentrantLock=new ReentrantLock();
    private Condition condition=reentrantLock.newCondition();
    /**
     * 构造并且初始化连接池
     */
    public ListPool(){
        super();
        for(int i=0;i<poolMaxSize;i++){
            list.add(" 数据"+i);
        }
    }
    /**
     * 获取数据
     * @return
     */
    public String get() {
        String getString=null;
        try{
        semaphore.acquire();
        reentrantLock.lock();
        while(list.size()==0){
            condition.await();
        }
        //从队列的头中取
        getString=list.remove(0);
        reentrantLock.unlock();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return getString;
    }
    /**
     * 向池中放回数据
     * @param getString
     */
    public void put(String getString){
        reentrantLock.lock();
        list.add(getString);
        condition.signalAll();
        reentrantLock.unlock();
        semaphore.release();
    }
    public static class MyThread extends Thread{
        private ListPool listPool=new ListPool();
        public MyThread(ListPool listPool) {
            this.listPool=listPool;
        }
        @Override
        public void run() {
            for(int i=0;i<Integer.MAX_VALUE;i++){
                String getString=listPool.get();
                System.out.println("线程"+Thread.currentThread().getName()+"获得"+getString);
                listPool.put(getString);
            }
        }

        public static void main(String[] args) {
            ListPool listPool=new ListPool();
            MyThread[] threadArray=new MyThread[12];
            for(int i=0;i<threadArray.length;i++){
                threadArray[i]=new MyThread(listPool);
            }
            for(int i=0;i<threadArray.length;i++){
                threadArray[i].start();
            }
        }
    }
}

运行部分结果如下所示:
这里写图片描述

至此,本篇文章结合源码讲解了如何使用Semphore的使用,并且使用简单的例子进行说明,如有任何问题,欢迎留言讨论,谢谢!!!


阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37142346/article/details/80344996
个人分类: java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

【Java并发包】Semaphore使用详解以及源码解读

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭