Depois de tentar todas as soluções possíveis, finalmente encontrei uma solução para inserir 100.000 linhas em 5 segundos!
Coisas que tentei:
1) Substituído os IDs AUTOINCREMENT/GENERATED do banco de dados/hibernate por IDs autogerados usando AtomicInteger
2) Ativando batch_inserts com batch_size=50
3) Liberando o cache após cada número 'batch_size' de chamadas persist()
4) multithreading (não tentei este)
Por fim, o que funcionou foi usar uma consulta multi-inserção nativa e inserindo 1000 linhas em uma consulta de inserção sql em vez de usar persist() em cada entidade. Para inserir 100.000 entidades, crio uma consulta nativa como esta
"INSERT into MyTable VALUES (x,x,x),(x,x,x).......(x,x,x)"
[1000 inserções de linha em uma consulta de inserção sql] Agora leva cerca de 3 segundos para inserir 100.000 registros! Então o gargalo era o próprio orm! Para inserções em massa, a única coisa que parece funcionar são consultas de inserção nativas!