Não use o ThreadGroup. Péssima idéia. A classe ThreadGroup é famosa por dar problemas. Se for no Java 5, use um ExecutorService.
É muito fácil, primeiro você cria um executor service:
ExecutorServices threadPool = Executors.newCachedThreadPool();
Além do CachedThreadPool a classe Executors também te dá a possibilidade de criar Pools de tamanho fixo, tamanho único ou "agendáveis". Escolha o mais adequado para você.
Depois, você usa o seu ExecutorService para disparar os seus Runnables:
Future<?> result = threadPool.submit(seuRunnable);
Se suas Threads retornam algum valor, considere usar um Callable ao invés de um Runnable. Basicamente, um Callable é um Runnable que retorna valor.
O que é aquele objeto Future retornado? Ele vai guardar o resultado da sua operação futura. A grande vantagem é que ele possui o método get() que espera até que a Thread obtenha o resultado do Callable (ou finalize o método run()).
Portanto, para esperar sua Thread terminar, basta fazer:
result.get();
Quando você terminar (ou quiser terminar) um ExecutorService usa o método shutdown(). Ele proibirá novas Threads de entrarem no Pool e aguardará pacificamente que as demais Threads sejam finalizadas.
Se sua intenção é chamar o interrupt() de todas as Threads, além de finalizar o Pool, use shutdownNow() ao invés de shutDown().