From Managed Threads to Independent Tasks: Rethinking Concurrency from Java to Go (Part 1)
Source: Dev.to
A Simple but Real Problem
Let’s start with a very common requirement: run multiple tasks at the same time and wait until all of them are finished. This pattern appears everywhere — running startup tasks, calling multiple APIs, or doing background work in parallel.
How I Would Solve This in Java
When I write this code, I’m very aware that I’m working with threads:
- I explicitly create
Threadobjects - I must remember to
start()them - I must
join()each thread to wait for completion
I’m responsible for their lifecycle. Concurrency in Java feels like managing workers. Threads are visible, and handling them correctly is part of the job.

The Same Problem in Go
Here, my thinking immediately changes:
- I don’t create threads
- I start work using the
gokeyword - I don’t wait for individual workers
I wait once for all tasks to finish. Goroutines feel lightweight and disposable. The code focuses on what runs concurrently, not how threads are managed.

Concurrency vs Parallelism
In both Java and Go, this example expresses concurrency — tasks that can make progress independently. Whether they actually run in parallel depends on CPU cores and the runtime scheduler.
How Concurrency Is Actually Handled in Java vs Go
Java
- Each
Threadrepresents a real execution unit managed by the JVM and the operating system. - Calling
start()schedules the thread, which runs with its own stack. - The main thread calls
join()to “pause here and wait until this specific thread finishes.” - Every started thread must be joined explicitly.
Concurrency in Java feels very thread‑centric — you create threads, track them, and make sure none are left running.
Go
- The
gokeyword launches a goroutine, not an OS thread. Goroutines are lightweight and are scheduled onto a smaller number of OS threads by the Go runtime. - Instead of joining individual goroutines, Go uses a
sync.WaitGroup. wg.Add(n)declares how many units of work need to be completed (without naming the goroutines).- Each goroutine calls
wg.Done()when it finishes. - The main function blocks once with
wg.Wait()until all work is finished.
The focus shifts from “Which threads am I waiting for?” to “Has all the work finished?”.

The Mental Shift I Noticed
Even with such a small example, the difference is clear. The biggest difference between Java and Go is not the syntax, but what you focus on while writing concurrent code.
- Java: thinking starts with threads — how many threads, when each starts, when each finishes.
- Go: attention shifts to tasks — what work can run at the same time, how many tasks are in progress, when all tasks are complete.
What This Comparison Helped Me Understand
After working through this example, I realized that the shift from managing threads to coordinating tasks makes concurrency easier to understand and reason about.
Why This Matters Beyond This Example
The same idea appears repeatedly in Go. Concurrency often starts by identifying independent tasks and then coordinating their completion, rather than managing threads directly. Once this mindset is adopted, reading and writing concurrent Go code becomes much smoother.
Takeaway from Part 1
- Java makes you think in terms of threads and their lifecycle.
- Go makes you think in terms of tasks and completion.
WaitGrouphelps you focus on what needs to finish, not how it runs.
In Part 2, I’ll look at what happens when concurrent tasks need to share data, and how Java and Go approach that problem differently.