JDK-6522773 : Decreasing ScheduledThreadPoolExecutor core pool size causes busy spin
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.concurrent
  • Affected Version: 6
  • Priority: P3
  • Status: Closed
  • Resolution: Fixed
  • OS: windows_2003
  • CPU: x86
  • Submitted: 2007-02-08
  • Updated: 2011-05-17
  • Resolved: 2011-05-17
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7
7 b09Fixed
Related Reports
Relates :  
Description
FULL PRODUCT VERSION :
1.6.0
build 1.6.0-b105

ADDITIONAL OS VERSION INFORMATION :
ALL Windows (XP, 2003)

A DESCRIPTION OF THE PROBLEM :
Very high CPU load when decreasing the CorePoolSize of ThreadPoolExecutor.
Decreasing it at runtime through setCorePoolSize() .

It results from a change in the implementation of ThreadPoolExecutor.getTask()  .

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run this simple code using JDK6:

package com.clearforest.bugs.jdk6;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CPUBugDemo
{
	static private AtomicInteger					sm_counter = new AtomicInteger(0);
	static private ScheduledFuture[]				sm_workers	= null;
	static private ScheduledThreadPoolExecutor		sm_thrdPool = new ScheduledThreadPoolExecutor(10);
	
	/**
	 * @param args
	 */
	public static void main(String[] args)
	{

		Runnable rnbl = new Runnable()
		{
			public void run()
			{
				Execute1();
			}
		};
		
		sm_workers = new ScheduledFuture[10];
		long lDelay = 100L;
		for(int i = 0 ; i < sm_workers.length ; i++)
		{
			sm_workers[i] = sm_thrdPool.scheduleAtFixedRate(rnbl, lDelay, 1000L, TimeUnit.MILLISECONDS);
			lDelay += (100L);
		}

		{
			try
			{
				Thread.sleep(3000);
				System.out.println("************************************************************");
				// The next line will cause very high CPU load!
				sm_thrdPool.setCorePoolSize(sm_thrdPool.getCorePoolSize() - 3);
			}
			catch(Exception ex)
			{
				
			}
		}
		
	}

	static private void Execute1()
	{
		int tmpInt = sm_counter.getAndIncrement();
		if( ( tmpInt % 10) == 0)
			System.out.println(Long.toString(Thread.currentThread().getId()) + " ==> " + Integer.toString(tmpInt));
	}
}


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No CPU load
ACTUAL -
Very high CPU load

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package com.clearforest.bugs.jdk6;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CPUBugDemo
{
	static private AtomicInteger					sm_counter = new AtomicInteger(0);
	static private ScheduledFuture[]				sm_workers	= null;
	static private ScheduledThreadPoolExecutor		sm_thrdPool = new ScheduledThreadPoolExecutor(10);
	
	/**
	 * @param args
	 */
	public static void main(String[] args)
	{

		Runnable rnbl = new Runnable()
		{
			public void run()
			{
				Execute1();
			}
		};
		
		sm_workers = new ScheduledFuture[10];
		long lDelay = 100L;
		for(int i = 0 ; i < sm_workers.length ; i++)
		{
			sm_workers[i] = sm_thrdPool.scheduleAtFixedRate(rnbl, lDelay, 1000L, TimeUnit.MILLISECONDS);
			lDelay += (100L);
		}

		{
			try
			{
				Thread.sleep(3000);
				System.out.println("************************************************************");
				// The next line will cause very high CPU load!
				sm_thrdPool.setCorePoolSize(sm_thrdPool.getCorePoolSize() - 3);
			}
			catch(Exception ex)
			{
				
			}
		}
		
	}

	static private void Execute1()
	{
		int tmpInt = sm_counter.getAndIncrement();
		if( ( tmpInt % 10) == 0)
			System.out.println(Long.toString(Thread.currentThread().getId()) + " ==> " + Integer.toString(tmpInt));
	}
}

---------- END SOURCE ----------
-

Comments
EVALUATION To clarify the problem further: If an STPE is started with corePoolSize = N, and all core threads have been started, and if the queue is never empty (always holding periodic tasks), then when corePoolSize is decreased, so that N > corePoolSize, then TPE.getTask() will spin, because the condition N > corePoolSize triggers a timed wait, but STPE always uses a keepAliveTime of ZERO, so the timed wait returns immediately, but no worker threads are ever terminated because the queue is never empty. The submitter's test is not suitable for a regression test, because it requires watching a cpu monitor such as "top". Here's a test case that will hang with all post-b93 jdks, but should not: import java.util.concurrent.*; public class Bug2 { public static void main(String[] args) throws Throwable { final int size = 10; final ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(size); final Runnable nop = new Runnable() { public void run() {}}; for (int i = 0; i < size; i++) pool.scheduleAtFixedRate(nop, 100L * (i + 1), 1000L, TimeUnit.MILLISECONDS); while (pool.getPoolSize() != size) Thread.yield(); pool.setCorePoolSize(size - 3); while (pool.getPoolSize() != size - 3) Thread.yield(); pool.shutdownNow(); pool.awaitTermination(1L, TimeUnit.DAYS); } }
08-02-2007

EVALUATION This is due to STPE using a keepAlive time of 0, together with getTask continuing to retry if the work queue is non-empty. getTask should be able to return null on timeout even when the queue is non-empty.
08-02-2007

EVALUATION Confirmed. Introduced in 6-b93, presumably by the changes for 6440728: ThreadPoolExecutor can fail to execute successfully submitted tasks as specified Here's a test case that I find more readable: import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; // Watch the execution of this program in "top". public class Bug { static final AtomicInteger counter = new AtomicInteger(0); static final ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(10); public static void main(String[] args) throws Throwable { Runnable rnbl = new Runnable() { public void run() { int n = counter.getAndIncrement(); if ((n % 10) == 0) System.out.printf("%d => %d%n", Thread.currentThread().getId(), n);}}; for (int i = 0; i < 10; i++) pool.scheduleAtFixedRate(rnbl, 100L * (i + 1), 1000L, TimeUnit.MILLISECONDS); Thread.sleep(3000); System.out.println("Decreasing core pool size now"); pool.setCorePoolSize(pool.getCorePoolSize() - 3); } }
08-02-2007