You could create queues for each group and when a task terminates you read the corresponding queue and submit the next task if needed.
For that you somehow need to identify when a Runnable terminates. With the standard JDK classes you cannot really do that in a simple way but using the Guava library you can wrap your ExecutorService into a ListeningExecutorService. If you submit a task through this wrapper it will return you a ListenableFuture instead of the plain Java Future. A ListenableFuture will let you register callbacks that will be executed when the task terminates. In these callbacks you can check your queues and submit the next tasks in the same group.