There are different levels of evil, and not all of them are actually considered evil in every situation.
Creating threads on demand instead of using a pool is usually considered evil, but this is not true for Java EE and runs for almost any type of server application.
In Java EE, creating your own thread in an EJB container is not particularly allowed. This relates, in particular, to the fact that Java EE containers can discreetly store contextual data in a local thread store, which is lost if the code runs in its own thread.
A web container, however, does not have such restrictions, and, according to the specification, it is more or less legal to have thread pools. This, for example, is the reason why people started Quartz from the web module in the EAR, even if they only used EJB modules or why the code from the web module could register callback listeners in unmanaged JMS queues, but EJBs could not do this.
However, in practice, creating threads (through pools) almost always works, if you remember that if you use, for example, EJB, you need to get instances from JNDI in the code running in these threads and not pass EJB references to these unmanaged threads. Of course, you need to take care to disable your pool, but almost every listener when loading into Java EE has a corresponding disabled listener where you can do this.
Java EE has some official ways that reduce the need to create your own pools:
However, some algorithms require separate thread pools to prevent blocking. Since none of the Java EE solutions gives you an absolute guarantee that the work is performed by different thread pools, sometimes there simply is no other plausible way than to create your own pool.
So, in the latter situation, it is actually more evil to open your code for locks than to create your own thread pool.