美文网首页《JAVA并发编程实战》示例程序
番外篇 信号量Semaphore和线程池的差异

番外篇 信号量Semaphore和线程池的差异

作者: 小超_8b2f | 来源:发表于2019-05-07 17:41 被阅读0次

    一、首先要明白Semaphore和线程池各自是干什么?

    信号量Semaphore是一个并发工具类,用来控制\color{red}{可同时并发的}线程数,其内部维护了一组虚拟许可,通过构造器指定许可的数量,每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。

    线程池用来控制\color{red}{实际工作的(总的)}线程数量,通过线程复用的方式来减小内存开销。线程池可同时工作的线程数量是一定的,超过该数量的线程调用需进入\color{red}{任务队列}等待,直到有可用的工作线程来执行任务队列中的任务。

    信号量Seamphore创建多少线程实际就会有多少线程执行,只是可同时执行的线程数量会受到限制。但使用线程池,不管你提交多少任务到线程池,实际可执行的线程数是一定的。

    以下通过具体的案例来说明二者具体的区别。

    1)使用Semaphore:

    public static void testSeamphore() {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread() {
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() 
    + " start running **********************");
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName() 
    + " stop running  ----------------------");
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
        }
    }
    

    我们创建了5个线程,信号量为2,打印如下:

    Thread-0 start running **********************
    Thread-1 start running **********************
    Thread-1 stop running ----------------------
    Thread-0 stop running ----------------------
    Thread-2 start running **********************
    Thread-3 start running **********************
    Thread-3 stop running ----------------------
    Thread-2 stop running ----------------------
    Thread-4 start running **********************
    Thread-4 stop running ----------------------

    通过控制台容易观察,每次最多会打印2条***的记录。可以看出来总共创建的\color{red}{5个线程}都执行完毕,\color{red}{5个线程对象互不相同}

    2)使用线程池:

    public static void testPool() {
        ExecutorService executorService = new ThreadPoolExecutor(2, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread() {
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() 
    + " start running **********************");
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName() 
    + " stop running  ----------------------");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.submit(thread);
        }
        executorService.shutdown();
    }
    

    我们创建了一个核心容量为2,总容量为5的线程池,并行执行5个线程任务。打印如下:

    pool-1-thread-2 start running **********************
    pool-1-thread-1 start running **********************
    pool-1-thread-2 stop running ----------------------
    pool-1-thread-1 stop running ----------------------
    pool-1-thread-2 start running **********************
    pool-1-thread-1 start running **********************
    pool-1-thread-1 stop running ----------------------
    pool-1-thread-2 stop running ----------------------
    pool-1-thread-1 start running **********************
    pool-1-thread-1 stop running ----------------------

    通过控制台容易观察虽然每次最多也是两条***的打印记录,但是执行任务的\color{red}{始终是2个固定的线程},这两条工作线程不是我们自己创建的,是线程池提供的。它们是pool-1-thread-1和pool-1-thread-2。

    总结:

    • 信号量的调用,当达到数量后,线程还是存在的,只是被挂起了而已。而线程池,同时执行的线程数量是固定的,超过了数量的只能等待。
    • 线程池控制的是线程数量,而信号量控制的是并发数量,虽然说这个看起来一样,但是还是有区别的。
    • 线程池是线程复用的;信号量是线程同步的

    二、Semaphore作为互斥锁的体现

    Semaphore实现互斥锁的方式是使用初始值为1的Semaphore对象,这样每条线程获取许可后必须释放许可,其它线程才能获取许可,当前拥有许可的线程就拥有了互斥锁。

    以下是具体案例:

    public static void testMutex() {
        Semaphore semaphore = new Semaphore(1);
        for (int i = 0; i < 5; i++) {
            new Thread() {
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "已获得许可");
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName() + "已释放许可");
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
    

    控制台打印如下:

    Thread-0已获得许可
    Thread-0已释放许可
    Thread-1已获得许可
    Thread-1已释放许可
    Thread-2已获得许可
    Thread-2已释放许可
    Thread-3已获得许可
    Thread-3已释放许可
    Thread-4已获得许可
    Thread-4已释放许可

    可以看出,任何一个线程在释放许可之前,其它线程都拿不到许可。这样当前线程必须执行完毕,其它线程才可执行。这样就实现了互斥。

    三、Semaphore先release后acquire

    Seamphore有一种特殊的使用场景,即先释放许可,后申请许可,此时会额外增加一个许可。

    实际编程中要额外小心,如下的实例,通过new Semaphore(0)创建的信号量,默认许可数是0,如果先调用release,会增加一个许可,再次acquire便可以获取新增的许可。

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(0);
        System.out.println(semaphore.availablePermits());
        semaphore.release();
        System.out.println(semaphore.availablePermits());
        semaphore.acquire(); //阻塞
        System.out.println(semaphore.availablePermits());
    }
    

    所以上面的代码实际上不会发生阻塞,而是直接输出0 1 0。本例中如果将release和acquire调换位置,则一定会发生阻塞。

    0
    1
    0


    四、结合信号量和线程池,控制线程池任务提交的速率

    @ThreadSafe
    public class BoundedExecutor {
        private final ExecutorService executor;
        private final Semaphore semaphore;
    
        public BoundedExecutor(ExecutorService executor, int bound) {
            this.executor = executor;
            this.semaphore = new Semaphore(bound);
        }
    
        public void submitTask(final Runnable command) {
            try {
                semaphore.acquire();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            command.run();
                        }finally {
                            semaphore.release();
                        }
                    }
                });
            } catch (InterruptedException e) {
                semaphore.release();
            }
        }
    
        public void stop(){
            this.executor.shutdown();
        }
    
        static class MyThread extends Thread {
            public String name;
            public MyThread(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                System.out.println("Thread-"+name+" is running....");
                try {
                    Thread.sleep(new Random().nextInt(10000));
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            ExecutorService executorService = new ThreadPoolExecutor(2,2,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(5));
            BoundedExecutor executor = new BoundedExecutor(executorService, 5);
            for (int i = 0; i < 100; i++) {
                executor.submitTask(new MyThread(""+i));
            }
            executor.stop();
        }
    }
    

    多线程是提高并发量,信号量是控制并发。

    \color{red}{但是为什么要控制线程池提交的速率呢?这么做有什么意义呢?岂不是与使用线程池的目的背道而驰了?}

    参考文献:Java信号量Semaphore详解

    相关文章

      网友评论

        本文标题:番外篇 信号量Semaphore和线程池的差异

        本文链接:https://www.haomeiwen.com/subject/xzlyoqtx.html