为什么信号量没有起作用?

感谢真的 (强)老师,飞哥,见哥,别问,问就是感谢!!!

下段代码达到我们的预期了吗?

代码v1

public static void main(String[] args) throws InterruptedException {

    Semaphore selectSemaphore = new Semaphore(2);
    ExecutorService pool = Executors.newFixedThreadPool(16);

    for (int i = 0; i < 10000; i++) {
        selectSemaphore.acquire();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // do nothing
                } finally {
                    selectSemaphore.release();
                }
            }
        });
    }
    Thread.sleep(10000);
    pool.shutdown();
}

乍一看,上面的代码似乎没什么问题,而且运行起来并不会报错。但是,代码达到我们想要的效果了吗?

尝试分析上述代码想到达到的效果。代码定义了一个permits=2的信号量selectSemaphore,目的是为了限制每次进入线程池的线程数,而我们定义的线程池pool中核心线程数和最大线程数远大于2(也没有大特别多hhh),一切放佛都很美好,怎么会有问题呢?

代码v2

在菜老师提示下,对代码定义线程池的地方进行“微调”,第二版代码如下:

public static void main(String[] args) throws InterruptedException {

    Semaphore selectSemaphore = new Semaphore(2);
    int corePoolSize = 16;
    int maximumPoolSize = 32;
    ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 10,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
    for (int i = 0; i < 10000; i++) {
        selectSemaphore.acquire();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                // do nothing
                } finally {
                    selectSemaphore.release();
                }
            }
        });
    }
    Thread.sleep(10000);
    pool.shutdown();
}

改过线程池定义部分代码之后,立刻报错。报错内容如下:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@6acbcfc0 rejected from java.util.concurrent.ThreadPoolExecutor@5f184fc6[Running, pool size = 32, active threads = 0, queued tasks = 0, completed tasks = 91]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at lock.SemaErrorSample.main(SemaErrorSample.java:16)

信号量没起作用吗?

代码v1 vs 代码v2

在两段代码中,改动的地方只有pool定义部分,为什么改了代码之后就报错了呢?两种定义方式不同的地方是什么?不同的部分又是如何导致上述错误的呢?

代码v1中,创建线程池代码为Executors.newFixedThreadPool(16),点进去newFixedThreadPool,源码如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

其中,new LinkedBlockingQueue<Runnable>()源码如下:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

也就是说,代码v1中线程池任务等待队列长度为Integer.MAX_VALUE。而在代码v2中,创建线程池代码为ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));,线程池任务等待队列长度为1。

但是,按照代码中信号量的设置,应该不会有任务进入到线程池等待队列中去。

遇码不决,打开debug。发现错误是由于addWorker失败抛出来的。
java.util.concurrent.ThreadPoolExecutor#execute
继续跟踪源码,报错的时候addWorker是在什么条件下返回flase。
java.util.concurrent.ThreadPoolExecutor#addWorker
由此看出,当有新任务来的时候,execute会先看当前的线程数有没有超过核心线程数,如果没超过就直接新建一个线程来执行新的任务,如果超过了就看看workQueue有没有满,没满就将新任务放进缓存队列中,满了就新建一个线程来执行新的任务,如果线程池中的线程数已经达到maximumPoolSize,则根据相应的策略拒绝任务。

百思不得其解,为什么workerCount会到maximumPoolSize?

代码v3

毫无头绪,一筹莫展。又开始看我们自己写的代码逻辑。这次我在run方法里添了一行代码,

public static void main(String[] args) throws InterruptedException {

    Semaphore selectSemaphore = new Semaphore(2);
    int corePoolSize = 16;
    int maximumPoolSize = 32;
    ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 10,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
    for (int i = 0; i < 10000; i++) {
        selectSemaphore.acquire();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    selectSemaphore.release();
                }
            }
        });
    }
    Thread.sleep(10000);
    pool.shutdown();
}

结果,惊喜出现了!!!虽然还是报了同样的错,但是情况略有好转,比代码v2会运行更多次的循环,并且随着run方法中sleep的时间值变大,有可能会不报错。对比结果就不贴出来了,读者可自行实验。

run执行时间对结果影响分析

问题似乎从代码问题变成了概率问题,我们传入的run方法执行时间是如何影响线程池的呢?为什么run方法执行太快更有可能会报错?

首先,定位到问题有可能和run方法运行时间有关之后,所以着重看java.util.concurrent.ThreadPoolExecutor#runWorker方法的实现。
其次,线程池中线程是如何重复利用的呢?我们传进去的run(即task.run();)方法执行完之后,什么时候该线程又可以继续执行下一个task呢?

runWorker代码如下:

final void runWorker(ThreadPoolExecutor.Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

其中,task.run();为我们自己的代码中run方法中的逻辑。
通过看runWorker源码,正好发现在while循环中,getTask方法是线程池可以重复利用线程的“秘诀”,在此不展开介绍。

结论

防止疯狂翻页,将文章一开始提到的代码中关键内容再贴出来:

    Semaphore selectSemaphore = new Semaphore(2);
    ExecutorService pool = Executors.newFixedThreadPool(16);

    for (int i = 0; i < 10000; i++) {
        selectSemaphore.acquire();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // do nothing
                } finally {
                    selectSemaphore.release();
                }
            }
        });
    }

结合上述分析,对代码v1-v3中关键节点按照调用先后顺序列出来:

  1. selectSemaphore.acquire();
  2. new Worker , 然后 addWorker
  3. getTask
  4. w.lock();
  5. selectSemaphore.release();(对应runWorker中task.run(); )
  6. w.unlock();

上述第5步对应代码v1 v2 v3中不同的run方法实现,即第5步运行时间不固定。而线程运行一个task需要的时间至少为上述第3-6步的时间,run执行太快,会导致task加入队列的速度大于task执行的速度。

极端情况下,假设线程池中所有Worker都卡在了上述第5步结束,即当前线程池中所有线程都还没有执行w.unlock();,因此都不能再接受下一个任务。同时,由于所有线程是卡在第5步结束(释放信号量之后),所以当前运行上下文中信号量selectSemaphore=2。
而此时,因为selectSemaphore已经被release,所以代码v1中下一个for循环中selectSemaphore.acquire();成功,可以执行下一步submit,所以尝试加入等待队列中,而由于我们设置的等待队列大小=1,小于信号量的permits=2,所以会有一个任务被拒绝

同理,如果线程池的等待队列大小>=selectSemaphore的permits,就不会出现task被reject的情况。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值