In most situations it is easier to use tasks instead of threads. You start with an ExecutorService, which restricts the number of threads and is shared across all service operations:
// inject with IoC framework
ExecutorService executor = Executors.newFixedThreadPool(10);
You use the method invokeAll to execute a task for each person. If the tasks do not finish within the given period, then the remaining tasks will be automatically cancelled. In this case, an exception is thrown when invoking the get method of the corresponding future. That means there is no need for additional exception handling.
public List<MyData> getMyData(MyParams params) throws Exception {
List<Callable<MyData>> tasks = new ArrayList<>();
for (Person p : persons) {
tasks.add(new Callable<MyData>() { // use Lambda in Java 8
public MyData call() {
MyData d = new MyData();
d.setPerson(p);
d.setTypeA(getTypeAFromDatabase1(p));
d.setTypeB(getTypeBFromDatabase2(p));
d.setTypeC(getTypeCFromSomeWebService(p));
return d;
}
});
}
List<MyData> result = new ArrayList<>();
for (Future<MyData> future : executor.invokeAll(tasks, 3000, TimeUnit.MILLISECONDS)) {
result.add(future.get());
}
return result;
}
There is no need to check the interrupted state within the callable. If a blocking operation is called within one of the methods, the method will automatically abort execution with an InterruptedException or some other exception (if it is implemented correctly). It is also possible to set the interrupted state instead of throwing an exception. However, that makes less sense for methods with return values.