From Managed Threads to Independent Tasks: Rethinking Concurrency from Java to Go (Part 1)

Published: (December 27, 2025 at 06:06 PM EST)
3 min read
Source: Dev.to

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 Thread objects
  • 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.

Java Thread

The Same Problem in Go

Here, my thinking immediately changes:

  • I don’t create threads
  • I start work using the go keyword
  • 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.

Go routine

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 Thread represents 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 go keyword 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?”.

comparison

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.
  • WaitGroup helps 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.

Back to Blog

Related posts

Read more »