Interrupting Executor Tasks

November 19, 2014

There’s this usecase that is not quite rare, when you want to cancel a running executor tasks. For example, you have ongoing downloads that you want to stop, or you have ongoing file copying that you want to cancel. So you do:

ExecutorService executor = Executors.newSingleThreadExecutor(); 
Future<?> future = executor.submit(new Runnable() {
    public void run() {
        // Time-consuming or possibly blocking I/O
// or

Unfortunately, that doesn’t work. Calling shutdownNow() or cancel() doesn’t stop the ongoing runnable. What these methods do is simply call .interrupt() on the respective thread(s). The problem is, your runnable doesn’t handle InterruptedException (and it can’t). It’s a pretty common problem described in multiple books and articles, but still it’s a bit counterintuitive.

So what do you do? you need a way to stop the slow or blocking operation. If you have a long/endless loop, you can just add a condition whether Thread.currentThread().isInterrupted() and don’t continue if it is. However, generally, the blocking happens outside of your code, so you have to instruct the underlying code to stop. Usually this is by closing a stream or disconnecting a connection. But in order to do that, you need to do quite a few things.

  • Extend Runnable
  • Make the “cancellable” resources (e.g. the input stream) an instance field, which
  • provide a cancel method to your extended runnable, where you get the “cancellable” resource and cancel it (e.g. call inputStream.close())
  • Implement a custom ThreadFactory that in turn creates custom Thread instances that override the interrupt() method and invoke the cancel() method on your extended Runnable
  • Instantiate the executor with the custom thread factory (static factory methods take it as an argument)
  • Handle abrupt closing/stopping/disconnecting of your blocking resources, in the run() method

The bad news is, you need to have access to the particular cancellable runnable in your thread factory. You cannot use instanceof to check if it’s of an appropriate type, because executors wrap the runnables you submit to them in Worker instances which do not expose their underlying runnables.

For single-threaded executors that’s easy – you simply hold in your outermost class a reference to the currently submitted runnable, and access it in the interrupt method, e.g.:

private final CancellableRunnable runnable;

runnable = new CancellableRunnable() {
    private MutableBoolean bool = new MutableBoolean();
    public void run() {
        while (bool.booleanValue()) {
            // emulating a blocking operation with an endless loop
    public void cancel() {
        // usually here you'd have inputStream.close() or connection.disconnect()

ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
    public Thread newThread(Runnable r) {
       return new Thread(r) {
           public void interrupt() {

Future<?> future = executor.submit(runnable);

(CancellableRunnable is a custom interface that simply defines the cancel() method)

But what happens if your executor has to run multiple tasks at the same time? If you want to cancel all of them, then you can keep a list of submitted CancellableRunnable instance and simply cancel all of them when interrupted. Thus runnables will be cancelled multiple times, so you have to account for that.

If you want fine-grained control, e.g. by cancelling particular futures, then there is no easy solution. You can’t even extend ThreadPoolExecutor because the addWorker method is private. You have to copy-paste it.

The only option is not to rely on future.cancel() or executor.shutdownAll() and instead keep your own list of CancellableFuture instances and map them to their corresponding futures. So whenever you want to cancel some (or all) runnables, you do it the other way around – get the desired runnable you want to cancel, call .cancel() (as shown above), then get its corresponding Future, and cancel it as well. Something like:

Map<CancellableRunnable, Future<?>> cancellableFutures = new HashMap<>();
Future<?> future = executor.submit(runnable);
cancellableFutures.put(runnable, future);

//now you want to abruptly cancel a particular task

(Instead of using the runnable as key, you may use some identifier which makes sense in your usecase and store both the runnable and future as a value under that key)

That’s a neat workaround, but anyway I’ve submitted a request for enhancement of the java.util.concurrent package, so that in a future release we do have the option to manage that usecase.


Development Overhead

November 13, 2014

What does a developer spend his time on? Writing code, debugging, thinking and communicating with colleagues (that includes meetings). Anything that is beyond these activities is unnecessary overhead (some meetings are also unnecessary, but that’s a different topic).

And yet, depending on our language and tools, we have to do a lot more to support the process of writing code. These activities include, but are not limited to:

  • manually format your code – the code has to be beautifully aligned and formatted, but that’s an extra effort.
  • using search and replace instead of refactoring – few languages and tools support good refactoring, and that’s priceless in a big project
  • manually invoking compilation – compile on save gives you immediate feedback; the need to manually run a compiler is adding a whole unnecessary step to your coding process
  • slow compilation – ontop of the previous issue, if your compiler is slow, it’s just a dead time (mandatory xkcd)
  • slow time-to-deploy – if the time from writing the code code to running it is more than a few seconds, then you are wasting enormous amounts of time. E.g. if you need to manually make builds and copy files on a local server.
  • clunky resource navigation – if you can’t go to a given source file in a couple of keystrokes
  • infrastructure problems – you depend on a database, a message queue, possibly some external service. Installing and supporting these components on your development machine can be painful. Recently we spent one day trying to integrate 3 components, some of which had docker instances. And on neither Windows, nor Mac, Docker worked properly. It was painful error-google-try-error-google process to even get things started. Avoid immature, untested tools (not bashing docker here, just an example, and it might have already been improved/fixed)
  • OS issues – if your OS crashes every couple of days, your I/O is blocking your UI, you sometimes lose your ALT+TAB functionality (which are things that I’ve been experiencing when using Ubuntu), then your OS is wasting a significant amount of your time.

Most of the manual tasks above can be automated, and the others should not exist at all. If you are using Java, for example, you can have a stable IDE, with automatic formatting and refactoring, with compile-on-save, with save-and-refresh for webapps. And you can use an operating system that doesn’t make you recompile the kernel every now and then in order to keep it working (note: hyperbole here).

It’s often a tradeoff. If I have to compare Java to Groovy in terms of productivity, for example, the (perceived) verbosity of Java is a minor nuisance compared to the lack of refactoring, formatting, etc, etc, in groovy (at least that was the case a few years ago; and it’s still the same with scala nowadays). Yes, you have to write a few lines more, but it’s a known process. If you have immature tools that are constantly breaking or just don’t work (and that is the case, unfortunately), it’s unknown how you should process. And you may end up wasting 10 minutes in manual “labour”, which would kill the productivity that a language gives you. For me Linux was also such a tradeoff – having the terminal is sometimes useful indeed, but it did not justify the effort in keeping the system working (and it completely died after a version upgrade).

Because I really feel all that overhead draining my productivity, I am very picky when it comes to the technologies I use. Being able to type faster or write less lines of code is fine, but you have to weigh that against the rest of the procedures you are forced to do. And that’s part of the reason why I prefer an IDE over a text editor, I don’t use Emacs and I don’t like Scala, and I don’t use Linux.

Your experience may very well be different (and if facebook checking is already taking half of your day, then nothing above really matters). But try to measure (or at least observe) how much time you spend not doing actual programming (or thinking) and have to do “automatable” or redundant stuff instead. And try to ignore the feeling of accomplishment when you do something that you don’t have to do in the first place. And if your preferred technologies turn out to be silently draining productivity, then consider changing them (or improving them, if you have the spare time).


On Java Generics and Erasure

November 5, 2014

“Generics are erased during compilation” is common knowledge (well, type parameters and arguments are actually the ones erased). That happens due to “type erasure”. But it’s wrong that everything specified inside the <..> symbols is erased, as many developers are assuming. See the code below:

public class ClassTest {
  public static void main(String[] args) throws Exception {
    ParameterizedType type = (ParameterizedType) 
    ParameterizedType fieldType = (ParameterizedType) 
    ParameterizedType paramType = (ParameterizedType) 
        Foo.class.getMethod("foo", List.class)
  class Foo<E extends CharSequence> {
    public List<Bar> children = new ArrayList<Bar>();
    public List<StringBuilder> foo(List<String> foo) {return null; }
    public void bar(List<? extends String> param) {}
  class Bar extends Foo<String> {}

Do you know what that prints?

class java.lang.String
class ClassTest$Bar
class java.lang.String
class java.lang.StringBuilder
interface java.lang.CharSequence

You see that every single type argument is preserved and is accessible via reflection at runtime. But then what is “type erasure”? Something must be erased? Yes. In fact, all of them are, except the structural ones – everything above is related to the structure of the classes, rather than the program flow. In other words, the metadata about the type arguments of a class and its field and methods is preserved to be accessed via reflection.

The rest, however, is erased. For example, the following code:

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
   String s =;

will actually be transformed to this (the bytecode of the two snippets is identical):

List list = new ArrayList();
Iterator it = list.iterator();
while (it.hasNext()) {
   String s = (String);

So, all type arguments you have defined in the bodies of your methods will be removed and casts will be added where needed. Also, if a method is defined to accept List<T>, this T will be transformed to Object (or to its bound, if such is declared. And that’s why you can’t do new T(). (by the way, one open question about this erasure)

So far we covered the first two points of the type erasure definition. The third one is about bridge methods. And I’ve illustrated it with this stackoverflow question (and answer).

Two “morals” of all this. First, java generics are complicated. But you can use them without understanding all the complications.

Second, do not assume that all type information is erased – the structural type arguments are there, so make use of them, if needed (but don’t be over-reliant on reflection)


The DSL Jungle

October 21, 2014

DSLs are a common thing in the programming world nowadays. Many frameworks and tools decide to build a DSL for their…specific things. Builds tools are the primary candidates, but testing frameworks, web frameworks and whatnot also decide to define a DSL. With these DSLs you define build steps, web routing rules, test acceptance criteria, etc.

What is the most common thing about all these DSLs? Two things. First, they are predominantly about configuration. Some specific way of configuring something specific to the tool or framework. The second thing is that you copy-paste code. Everytime I’m confronted with some DSL that is meant to help with my programming task, I end up copy-pasting examples or existing code, and then modifying it. Even though I’ve been working with a DSL for 8 months (from time to time), I just don’t remember its syntax.

And you may say “yeah, that’s because you use bad DSLs”. Well, then I haven’t seen a good one yet. I’m currently using sbt, spray routing, cucumber for scala, previously I’ve used groovy and grails DSLs, and a few others along the way.

But is it bad that you copy-paste existing pieces of code? Not always. You can, of course, base your configuration on existing, working pieces. But there are three issues – duplicate code, autocomplete and exploration. You know copy-pasting is wrong and leads to duplication. Not only that, but you may forget to change or remove something in the pasted code. And if you want to add some property, it would be good to be able to auto-complete it, rather than mistyping or, or forgetting whether it was “filePath”, “filepath”, “file-path” or just “path”. Having 2-3 DSLs in parts of a big project, you can’t remember all property names, so the alternative is to go and see the documentation (if you don’t have a working piece with that particular property to copy-paste from). Exploration is an even bigger issue. Especially when learning, or remembering how to do certain things with a given DSL, it is crucial to be able to explore the possibilities. What properties does this have, that might be useful? What does this property do exactly and does it have subproperties? What can I nest under this item? This is very important, regardless of your knowledge of the tool/framework.

But with most DSLs you don’t have that. They either have some bizarre syntax, or they are JSON-based, or they look like the language you are using, but not quite, and hence even an IDE finds it difficult to understand them (spray being such an example). You either look at the documentation, or you copy-paste, or both. And you are kind of lost in this DSL jungle of ever so “cooler” DSLs that do a wide variety of things.

And now I’ll drop the X-bomb. I love XML. Trusting the “XML configuration files are evil” meme has lead to many incomprehensible configurations, that are “short and easy to read and write”. Easy, if you remembered what those double-percentage signs meant compared to the single percentage signs, and where exactly to put the parentheses.

In almost every scenario where someone decided that a DSL is a good idea, XML would have worked brilliantly. Using an XSD schema (which, I agree, is a bit tedious to write) you can make any XML-aware tool be turned into an IDE for configuration. Take the maven pom file, for example. Did you forget what element you could nest under “build”? Hit CTRL+space and you’ll find out. Being unified, you can read the XML configuration of any framework or tool that uses it, not just this particular one, that is the n-th DSL in a single project. While XML is verbose, it is straightforward and standard. (To make a distinction: your application properties file is fine with key-value pairs, YAML, or something like typesafe, but that’s not coming from a framework, and it’s not a DSL in the narrower sense)

So if you are writing a tool, and can’t make some configuration available via annotations or via very simple code (builders, setters, fluent interfaces), don’t go for a DSL. Don’t write DSLs where you can easily use XML. It will look good on your, but your users will copy-paste all the time and may actually hate it. So please don’t contribute to the DSL jungle.

And do you know why that is? Remember the initial note that these are DSLs you use when programming. Well, DSLs are not for programmers. DSLs are for non-programmers to express business logic in (almost) prose. Or at least their usage should be limited to that, where they can really excel. If you are making a tool for business analysts, feel free to design the most awesome DSL. If you are building a tool for programmers, don’t.


Validate Configuration on Startup

October 15, 2014

Do you remember that time when you spent a whole day trying to fix a problem, only to realize that you have mistyped a configuration setting? Yes. And it was not just one time.

Avoiding that is not trivial, as not only you, but also the frameworks that you use should take care. But let me outline my suggestion.

Always validate your configuration on startup of your application. This involves three things:

First, check if your configuration values are correct. Test database connection URLs, file paths, numbers and periods of time. If a directory is missing, a database is unreachable, or you have specified a non-numeric value where a number or period of time is expected, you should know that immediately, rather the application has been used for a while.

Second, make sure all required parameters are set. If a property is required, fail if it has not been set, and fail with a meaningful exception, rather than an empty NullPointerException (e.g. throw new IllegalArgumentException("database.url is required"))

Third, check if only allowed values are set in the configuration file. If a configuration is not recognized, fail immediately and report it. This will save you from spending a whole day trying to find out why setting the “request.timeuot” property didn’t have effect. This is applicable to optional properties that have default values, and comes with the extra step of adding new properties to a predefined list of allowed properties, and possibly forgetting to do that leading to an exception, but that is unlikely to waste more than a minute.

A simple implementation of the last suggestion would like like this:

Properties properties = loadProperties();
for (Object key : properties.keySet()) {
  if (!VALID_PROPERTIES.contains(key)) {
    throw new IllegalArgumentException("Property " + key +
      " is not recognized as a valid property. Maybe a typo?");

Implementing the first one is a bit harder, as it needs some logic – in your generic properties loading mechanism you don’t know if a property is a database connection url, a folder, a timeout. So you have to do these checks in the classes that know the purpose if each property. Your database connection handler knows how to work with a database url, your file storage handler knows what a backup directory is, and so on. This can be combined with the required property verification. Here, a library like Typesafe config may come handy, but it won’t solve all problems.

This is not only useful for development, but also for newcomers to the project that try to configure their local server, and most importantly – production, where you can immediately find out if there has been a misconfiguration in this release.

Ultimately, the goal is to fail as early as possible if there is any problem with the supplied configuration, rather than spending hours chasing typos, missing values and services that are accidentally not running.


Scala – the Good, the Bad and the Very Ugly [presentation]

October 6, 2014

The other day I gave a talk on a tech conference about my experience with Scala. Ironically, just two weeks after I wrote that I don’t like Scala, I started working with it on a daily basis, so I now have a better overview. And it’s not all black and white, but many of my arguments still hold.

You can check the slides (the talk was not in English). And I’d like to emphasize the final conclusion point: don’t give the users of your language, API or product all the possible options – they will misuse them.


Load-Testing Guidelines

September 18, 2014

Load-testing is not trivial. It’s often not just about downloading JMeter or Gatling, recording some scenarios and then running them. Well, it might be just that, but you are lucky if it is. And what may sound like “Captain Obvious speaking”, it’s good to be reminded of some things that can potentially waste time.

So, when you run the tests, eventually you will hit a bottleneck, and then you’ll have to figure out where it is. It can be:

  • client bottleneck – if your load-testing tool uses HttpURLConnection, the number of requests sent by the client is quite limited. You have to start from that and make sure enough requests are leaving your load-testing machine(s)
  • network bottlenecks – check if your outbound connection allows the desired number of requests to reach the server
  • server machine bottleneck – check the number of open files that your (most probably) linux server allows. For example, if the default is 1024, then you can have at most 1024 concurrent connections. So increase that (limits.conf)
  • application server bottleneck – if the thread pool that handles requests is too low, requests may be kept waiting. If some other tiny configuration switch (e.g. whether to use NIO, which is worth a separate article) has the wrong value, that may reduce performance. You’d have to be familiar with the performance-related configurations of your server.
  • database bottlenecks – check the CPU usage and response times of your database to see if it’s not the one slowing the requests. Misconfiguring your database, or having too small/few DB servers, can obviously be a bottleneck
  • application bottleneck – these you’d have to investigate yourself, possibly using some performance monitoring tool (but be careful when choosing one, as there are many “new and cool”, but unstable and useless ones). We can divide this type in two:
    • framework bottleneck – if a framework you are using has problems. This might be a web framework, a dependency injection framework, an actor system, an ORM, or even a JSON serialization tool
    • application code bottleneck – if you are misusing a tool/framework, have blocking code, or just wrote horrible code with unnecessarily high computational complexity

You’d have to constantly monitor the CPU, memory, network and disk I/O usage of the machines, in order to understand when you’ve hit the hardware bottleneck.

One important aspect is being able to bombard your servers with enough requests. It’s not unlikely that a single machine is insufficient, especially if you are a big company and your product is likely to attract a lot of customers at the start and/or making a request needs some processing power as well, e.g. for encryption. So you may need a cluster of machines to run your load tests. The tool you are using may not support that, so you may have to coordinate the cluster manually.

As a result of your load tests, you’d have to consider how long does it make sense to keep connections waiting, and when to reject them. That is controlled by connect timeout on the client and registration timeout (or pool borrow timeout) on the server. Also have that in mind when viewing the results – too slow response or rejected connection is practically the same thing – your server is not able to service the request.

If you are on AWS, there are some specifics. Leaving auto-scaling apart (which you should probably disable for at least some of the runs), you need to have in mind that the ELB needs warming up. Run the tests a couple of times to warm up the ELB (many requests will fail until it’s fine). Also, when using a load-balancer and long-lived connections are left open (or you use WebSocket, for example), the load balancer may leave connections from itself to the servers behind it open forever and reuse them when a new request for a long-lived connection comes.

Overall, load (performance) testing and analysis is not straightforward, there are many possible problems, but is something that you must do before release. Well, unless you don’t expect more than 100 users. And the next time I do that, I will use my own article for reference, to make sure I’m not missing something.


EasyCamera Now in Maven Central

September 11, 2014

Several months ago I created the EasyCamera project (GitHub). As there has been a lot of interest, I just released it to Maven central, so that you don’t need to checkout & build it yourself. The packaging is also changed to aar now.

I would appreciate all reported issues and feedback about the API. Let me just remind the usage:

EasyCamera camera =;
CameraActions actions = camera.startPreview(surface);
PictureCallback callback = new PictureCallback() {
    public void onPictureTaken(byte[] data, CameraActions actions) {
        // store picture

I won’t paste the code that you would normally have to write, but you should follow 10 steps, and I believe the EasyCamera API is a bit friendlier, easier to discover and harder to misuse.


Musical Scale Generator

September 7, 2014

We all know the C-major scale: do-re-mi-fa-sol-la-ti-do. But what’s behind it? And how many other scales there are? It’s complicated. Let me do a brief introduction into the theory first, without trying to be precise or complete.

In use are more than a dozen scales, and the popular one in the western world are the major, minor (natural, harmonic), and the “old modes”: Dorian, Lydian, Locrian, etc. All these are heptatonic (7-tone) scales. There are also pentatonic (5-tone) scales, and also other scales like Turkish, Indian, Arabic. All of them share a common purpose: to constraint melodies in order to make them sound pleasant. The notes in each scale trigger a different level of consonance with each other, which in turn provides different “feel”. The predominant scales all fall within the so called chromatic scale, which consists of all the 12 note octave on a piano keyboard (counting both white and black keys).

How are the scales derived? There are two main aspects: the harmonic series and temperament. The harmonic series (closely related to the concept of an overtone) are derived from the physical behaviour of the musical instruments, and more precisely – oscillation (e.g. of a string). The harmonic (or overtone) series produce ever-increasing pitches, which are then transposed into a single octave (the pitch space between the fundamental frequency and 2 times that frequency). This is roughly how the chromatic scale is obtained. Then there is temperament – although the entirely physical explanation sounds a perfect way to link nature and music, in practice the thus obtained frequencies are not practical to play on musical instruments, and also yield some dissonances. That’s why musicians are tuning their instruments by changing the frequencies obtained from the harmonic series. There are multiple ways to do that, one of which is that 12-tone equal temperament, where an octave is divided in 12 parts, which are equal on a logarithmic scale (because pitch changes are perceived as the logarithm of their frequencies).

But what does that have to do with programming? Computers can generate an almost infinite amount of musical scales that follow the rules of the scales already proven to be good. Why limit ourselves to 7-tone scales out of 12 tones, when we can divide the octave into 24 parts and make a scale of 15 tones? In fact, some composers and instrument makers, the most notable being Harry Partch, have experimented with such an approach, and music has been written in such “new” scales (although not everyone would call it “pleasant”). But with computers we can test new scales in seconds, and write music in them (or let the computer write it) in minutes. In fact, I see this as one way for advancing the musical landscape with the help of computers (algorithmic composition aside).

That’s why I wrote a scale generator. It takes a few input parameters – the fundamental frequency, on which you want to base the scale (by default C=262.626); the size of the scale (by default 7); the size of the ‘chromatic scale’ out of which the scale will be drawn (by default 12); and the final parameter specifies whether to use equal temperament or not.

The process, in a few sentences: it starts by calculating the overtones (harmonics), skipping the 7th (for reasons I don’t fully understand). Then transposes all of them into the same octave (it does so, by calculating the ratio from a given harmonic to its tonic (the closest power-of-two multiple of the fundamental frequency), and then using that ratio calculates the frequency from the fundamental frequency itself. It does that until the “chromatic scale size” parameter value is reached. Then it finds the perfect interval (perfect fifth in case of heptatonic (diatonic) scale), i.e. the one with ratio 3/2. If equal temperament is enabled, the previous chromatic scale is replaced with an equal-tempered one. Then the algorithm makes a “circle” from the tones in the chromatic scale (the circle of fifths is one example), based on the perfect interval, and starting from the tone before the fundamental frequency, enumerates N number of tones, where N is the size of the scale. This is the newly formed scale. Note that starting from each note in the scale we just obtained (and continuing in the next octave when we run out of tones) would yield a completely different scale (this is the difference between C-major and a A-minor – they use the same notes)

Finally, my tool plays the generated scale (using low-level sound wave generation, which I copied from somewhere and is beyond the scope of this discussion) and also, using a basic form of my music composition algorithm, composes a melody in the given scale. It sounds terribly at first, because it’s not using any instrument, but it gives a good “picture” of the result. And the default arguments result in the familiar major scale being played.

Why is this interesting? Because hopefully music will evolve, and we will be able to find richer scales pleasant to listen to, giving composers even more material to work with.


Caveats of HttpURLConnection

September 5, 2014

Does this piece of code look ok to you?

HttpURLConnection connection = null;
try {
   connection = (HttpURLConnection) url.openConnection();
   try (InputStream in = url.getInputStream()) {
     return streamToString(in);
} finally {
   if (connection != null) connection.disconnect();

Looks good – it opens a connection, reads from it, closes the input stream, releases the connection, and that’s it. But while running some performance tests, and trying to figure out a bottleneck issue, we found out that disconnect() is not as benign as it seems – when we stopped disconnecting our connections, there were twice as many outgoing connections. Here’s the javadoc:

Indicates that other requests to the server are unlikely in the near future. Calling disconnect() should not imply that this HttpURLConnection instance can be reused for other requests.

And on the class itslef:

Calling the disconnect() method may close the underlying socket if a persistent connection is otherwise idle at that time.

This is still unclear, but gives us a hint that there’s something more. After reading a couple of stackoverflow and answers (1, 2, 3, 4) and also the android documentation of the same class, which is actually different from the Oracle implementation, it turns out that .disconnect() actually closes (or may close, in the case of android) the underlying socket.

Then we can find this bit of documentation (it is linked in the javadoc, but it’s not immediately obvious that it matters when calling disconnect), which gives us the whole picture:

The keep.alive property (default: true) indicates that sockets can be reused by subsequent requests. That works by leaving the connection to the server (which supports keep alive) open, and then the overhead of opening a socket is no longer needed. By default, up to 5 such sockets are reused (per destination). You can increase this pool size by setting the http.maxConnections property. However, after increasing that to 10, 20 and 50, there was no visible improvement in the number of outgoing requests.

However, when we switched from HttpURLConnection to apache http client, with a pooled connection manager, we had 3 times more outgoing connections per second. And that’s without fine-tuning it.

Load testing, i.e. bombarding a target server with as many requests as possible, sounds like a niche use-case. But in fact, if your application invokes a web service, either within your stack, or an external one, as part of each request, then you have the same problem – you will be able to make fewer requests per second to the target server, and consequently, respond to fewer requests per second to your users.

The advice here is: almost always prefer apache http client – it has a way better API and it seems way better performance, without the need to understand how exactly it functions underneath. But be careful of the same caveats there as well – check pool size and connection reuse. If using HttpURLConnection, do not disconnect your connections after you read their response, consider increasing the socket pool size, and be careful of related problems.