为什么信号量没有起作用?
感谢真的菜 (强)老师,飞哥,见哥,别问,问就是感谢!!!
下段代码达到我们的预期了吗?
代码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失败抛出来的。
继续跟踪源码,报错的时候addWorker是在什么条件下返回flase。
由此看出,当有新任务来的时候,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中关键节点按照调用先后顺序列出来:
- selectSemaphore.acquire();
- new Worker , 然后 addWorker
- getTask
- w.lock();
- selectSemaphore.release();(对应runWorker中task.run(); )
- 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的情况。