DEV Community

Juthi Sarker Aka
Juthi Sarker Aka

Posted on

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

When I started learning Go after Java, I assumed concurrency would feel familiar.
Both languages support running code in parallel, so I expected the concepts to transfer easily.

What surprised me was not the syntax, but the way Go encourages you to think about concurrency.

This post is Part 1 of a small series where I compare Java threading and Go concurrency from a learner’s point of view. I’m not trying to cover everything here. Instead, I want to focus on the first mental shift that helped things click for me: moving from managing threads to coordinating tasks.


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

In Java, the most direct way is to use threads.

Java Thread

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.

The Same Problem in Go

Now let’s look at how the same problem is solved in Go.

Go routine

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

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

Although both versions solve the same problem, what happens behind the scenes — and how we reason about it — is quite different.
On the Java side
Each Thread represents a real execution unit managed by the JVM and the operating system. When start() is called, the JVM schedules the thread, and it runs independently with its own stack.
When the main thread calls join(), it is essentially saying:

“Pause here and wait until this specific thread finishes.”

To complete the program, 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.


On the Go side
In 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 WaitGroup.
When I call wg.Add(2), I’m not saying which goroutines I’m waiting for — I’m saying how much work needs to be completed.

Each goroutine signals completion with wg.Done(), and 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.

comparison


What This Comparison Helped Me Understand
After working through this small example, I realized that the biggest difference between Java and Go is not the syntax, but what I focus on while writing concurrent code.

In Java, my thinking naturally starts with threads:

  • how many threads I am creating
  • when each thread starts
  • when each thread finishes

In Go, my attention shifts to the tasks themselves:

  • what work can run at the same time
  • how many tasks are currently in progress
  • when all tasks are complete

This change in focus made concurrency feel easier to understand and reason about.


Why This Matters Beyond This Example
Even though this is a very small example, the same idea appears repeatedly in Go.

Concurrency in Go often starts by identifying independent tasks and then coordinating their completion, rather than managing threads directly. Once I understood this, it became easier to read and write concurrent Go code.


Takeaway from Part 1

  • From this comparison, I took away three simple points:
  • Java made me think in terms of threads and their lifecycle
  • Go made me think in terms of tasks and completion

WaitGroup helped me focus on what needs to finish, not how it runs

This mental shift was the first step for me in understanding Go’s concurrency model.

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.

Top comments (0)