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

一、首先要明白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详解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容