JDK-8292027 : ForkJoinPool and ThreadPoolExecutor do not use Thread::start to start worker threads
  • Type: Bug
  • Component: core-libs
  • Affected Version: 19
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2022-08-05
  • Updated: 2022-11-25
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.
Other
tbdUnresolved
Related Reports
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
Is independent of system architecture or OS, is JDK 19 specific:
openjdk version "19-ea" 2022-09-20
OpenJDK Runtime Environment (build 19-ea+30-2169)
OpenJDK 64-Bit Server VM (build 19-ea+30-2169, mixed mode, sharing)

A DESCRIPTION OF THE PROBLEM :
In JDKs prior to v19, a subclass of ForkJoinWorkerThread would call the start() method when being started by a ForkJoinPool.  In JDK 19, that is no longer happening.  Application code which is dependent on the start() method being invoked is broken.

REGRESSION : Last worked in version 18.0.2

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the demo application (class jdkbug.JDKBugDemo) provided.  No command line arguments are required.




EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
On JDK 16, the output is listed below.  You can see that for each thread spawned, the start() method is called, as we see (for example) "TestForkJoinWorkerThread (4) 18 is starting." messages.

*****

TestForkJoinWorkerThread (0) 14 is starting.
TestForkJoinWorkerThread (0) 14 is running.
WorkThread (0) 14: Initializing task counter.
TestForkJoinWorkerThread (1) 15 is starting.
TestForkJoinWorkerThread (1) 15 is running.
WorkThread (1) 15: Initializing task counter.
TestForkJoinWorkerThread (2) 16 is starting.
TestForkJoinWorkerThread (3) 17 is starting.
TestForkJoinWorkerThread (2) 16 is running.
WorkThread (2) 16: Initializing task counter.
TestForkJoinWorkerThread (4) 18 is starting.
TestForkJoinWorkerThread (3) 17 is running.
TestForkJoinWorkerThread (4) 18 is running.
WorkThread (4) 18: Initializing task counter.
WorkThread (3) 17: Initializing task counter.
WorkerThread (14) 0: 0 terminating
WorkerThread (17) 3: 0 terminating

*****
Result:
lorem ipsum dolor sit amet, consectetur adipiscing elit. curabitur non pellentesque odio. proin eget tortor a ex viverra molestie eget a diam. sed accumsan mauris ut ipsum tempor, nec iaculis sem accumsan. etiam id lectus consectetur, suscipit dui in, commodo odio. mauris ullamcorper pulvinar ultrices. nam eleifend eros id nunc rutrum pretium. etiam imperdiet elit urna, ac ullamcorper ante auctor eu. proin condimentum nunc hendrerit odio ultrices sagittis. praesent et tristique enim. nam volutpat accumsan elit sed posuere. lorem ipsum dolor sit amet, consectetur adipiscing elit. duis id tortor id dolor semper finibus eu ut ante. nulla facilisi. phasellus sapien tellus, pulvinar in efficitur nec, accumsan nec tortor. vivamus aliquam, dolor at vestibulum placerat, dui odio semper neque, id aliquam neque justo eu orci.
WorkerThread (15) 1: 0 terminating
WorkerThread (16) 




ACTUAL -
In JDK 19, we get the following output (Note that the starting messages are no longer being generated, meaning start() isn't called.):

*****

TestForkJoinWorkerThread (0) 21 is running.
WorkThread (0) 21: Initializing task counter.
TestForkJoinWorkerThread (1) 22 is running.
WorkThread (1) 22: Initializing task counter.
TestForkJoinWorkerThread (2) 23 is running.
WorkThread (2) 23: Initializing task counter.
TestForkJoinWorkerThread (3) 24 is running.
WorkThread (3) 24: Initializing task counter.
WorkerThread (24) 3: 0 terminating

*****
Result:
lorem ipsum dolor sit amet, consectetur adipiscing elit. curabitur non pellentesque odio. proin eget tortor a ex viverra molestie eget a diam. sed accumsan mauris ut ipsum tempor, nec iaculis sem accumsan. etiam id lectus consectetur, suscipit dui in, commodo odio. mauris ullamcorper pulvinar ultrices. nam eleifend eros id nunc rutrum pretium. etiam imperdiet elit urna, ac ullamcorper ante auctor eu. proin condimentum nunc hendrerit odio ultrices sagittis. praesent et tristique enim. nam volutpat accumsan elit sed posuere. lorem ipsum dolor sit amet, consectetur adipiscing elit. duis id tortor id dolor semper finibus eu ut ante. nulla facilisi. phasellus sapien tellus, pulvinar in efficitur nec, accumsan nec tortor. vivamus aliquam, dolor at vestibulum placerat, dui odio semper neque, id aliquam neque justo eu orci.
WorkerThread (23) 2: 0 terminating
WorkerThread (21) 0: 


---------- BEGIN SOURCE ----------

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;

public class JDKBugDemo {
	static int counter = 0;
	static String orig_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non pellentesque odio. Proin eget tortor a ex viverra molestie eget a diam. Sed accumsan mauris ut ipsum tempor, nec iaculis sem accumsan. Etiam id lectus consectetur, suscipit dui in, commodo odio. Mauris ullamcorper pulvinar ultrices. Nam eleifend eros id nunc rutrum pretium. Etiam imperdiet elit urna, ac ullamcorper ante auctor eu. Proin condimentum nunc hendrerit odio ultrices sagittis. Praesent et tristique enim. Nam volutpat accumsan elit sed posuere. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis id tortor id dolor semper finibus eu ut ante. Nulla facilisi. Phasellus sapien tellus, pulvinar in efficitur nec, accumsan nec tortor. Vivamus aliquam, dolor at vestibulum placerat, dui odio semper neque, id aliquam neque justo eu orci.";
	static ForkJoinPool pool = new ForkJoinPool(5, new WorkerThreadFactory(), new UEH(), false);
	

	public static void main(String[] args) throws Exception {
		new JDKBugDemo().doDemo();

	}
	
	private void doDemo() throws Exception {
        System.out.println("*****\n");
             
        char[] output = new char[orig_text.length()];
        RecursiveToLower t = new RecursiveToLower(orig_text.toCharArray(), output);
        pool.invoke(t);
        
        do {
        	TimeUnit.SECONDS.sleep(1);
        	
        } while (!t.isDone());

        pool.shutdown();
        
        System.out.println("\n*****\nResult:\n" + new String(output));
	}
	
	static class WorkerThreadFactory implements ForkJoinWorkerThreadFactory {
	    @Override
	    public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
	        return new TestForkJoinWorkerThread(counter++, pool);
	    }
	}
	
	static class UEH implements Thread.UncaughtExceptionHandler {
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			System.out.println("UEH reporting from " + t + ": " + t);			
		}		
	}

	
	class RecursiveToLower extends RecursiveAction {
	    private char[] text;
	    private char[] result;
	    private int start;
	    private int length;
	    private final int maxLength = 200;
	    
	    public RecursiveToLower(char[] text, char[] result) {
	    	this.text = text;
	    	this.result = result;
	    	start = 0;
	    	length = text.length;
	    }
	    
	    public RecursiveToLower(char[] text, char[] result, int start, int length) { 
	    	this.text = text; 
	    	this.result = result;
	    	this.start = start;
	    	this.length = length;
	    }
	    
	    private void computeDirectly() {
	    	for (int index = start; index < start + length; index++) {
	    		result[index] = Character.toLowerCase(text[index]);
	    	}
	    }
	 
	    protected void compute() {
	    	if (length > maxLength) {
	    		int halfLength = length / 2;
	    		RecursiveToLower subtask1 = new RecursiveToLower(text, result, start, halfLength);
	    		RecursiveToLower subtask2 = new RecursiveToLower(text, result, start + halfLength, length - halfLength);
	    		invokeAll(subtask1, subtask2);
	    	} else {
	    		computeDirectly();
	    	}
	    }
	}
	
	public static class TestForkJoinWorkerThread extends ForkJoinWorkerThread {
		private static ThreadLocal<Integer> taskCounter = new ThreadLocal();
		
		private int id;

		protected TestForkJoinWorkerThread(int id, ForkJoinPool pool) {
			super(pool);
			this.id = id;
		}

		@Override
	    protected void onStart() {
	        super.onStart();
	        System.out.printf("WorkThread (%d) %d: Initializing task counter.\n", id, this.getId());
	        taskCounter.set(0);
	    }
		
		@Override
	    protected void onTermination(Throwable exception) {
	        System.out.printf("WorkerThread (%d) %d: %d terminating\n", getId(), id, taskCounter.get());
	        super.onTermination(exception);
	    }
		
		public void addTask() {
	        int counter = taskCounter.get().intValue();
	 
	        counter++;
	        taskCounter.set(counter);
	    }

		public void run() {
			System.out.printf("TestForkJoinWorkerThread (%d) %d is running.\n", id, this.getId());
			super.run();
		}
		
		public void start() {
			System.out.printf("TestForkJoinWorkerThread (%d) %d is starting.\n", id, this.getId());
			super.start();
		}
	}
}
---------- END SOURCE ----------

FREQUENCY : always



Comments
Yes, there is a change of behavior that needs to be documented in a RN for JDK 19. If ThreadPoolExecutor or ForkJoinPool are created with a thread factory that creates threads that override the "start" method then the override won't be invoked when the thread is started. The change has no impact on ForkJoinWorkerThread.onStart which can be overridden so that custom code is executed on the worker thread when it starts. The change in behavior stems from starting working threads in a "thread container" (for serviceability support) where starting the thread does not run the no-arg "start" method. We need to think more about this scenario as restoring things to call an overridden method means splitting the thread start into two steps with arbitrary code running between the two steps.
25-11-2022

The observations on Windows 10: JDK 18: Passed. JDK 19ea+19: Passed. JDK 19ea+25: Failed, no starting messages generated
08-08-2022