DEV Community

Juthi Sarker Aka
Juthi Sarker Aka

Posted on

From Managed Threads to Independent Tasks (Part 2: Sharing Data — Locks, Atomics, and Ownership)

In Part 1, I looked at how Java and Go approach concurrency differently at a high level.
Java tends to frame concurrency around threads, while Go emphasizes tasks and coordination.
After understanding that shift, a new question came up naturally:

What happens when concurrent tasks need to share data?

This is where concurrency becomes more challenging, and where Java and Go begin to differ more noticeably.


The Problem: Shared State

Consider a simple requirement:

Multiple concurrent tasks need to update a shared counter.

This kind of problem appears frequently:

  • tracking active jobs
  • counting processed items
  • reporting progress

Although this example uses a counter, the same ideas apply to other shared state such as maps, slices, or in-memory caches that multiple concurrent tasks need to access safely.


How This Is Commonly Done in Java

In Java, sharing data safely usually means protecting it.
Using synchronized

synchronized Java
Here, Java ensures that only one thread can access the counter at a time.

Using AtomicInteger

atomic integer java
With atomics, we avoid explicit locks, but the idea is the same:
shared memory must be handled carefully.

How This Feels in Java

When writing code like this in Java, the typical concerns are:

  • This data is shared
  • Multiple threads can access it
  • It must be protected from race conditions

Concurrency often feels like defending shared state from concurrent access.


The Same Problem in Go

Go provides similar tools — but it also encourages a different way of thinking.


Option 1: Protect Shared Data with a Mutex

The most direct translation from Java looks like this:

Go mutex
This approach works and is perfectly valid Go.

A mutex is a good fit when you truly have shared state — such as maps, caches, or simple counters — that multiple goroutines need to read and update safely. In these cases, protecting the data with a mutex is often the simplest and clearest solution.


Option 2: Avoid Sharing Data by Giving It an Owner

Go also makes it easy to approach the problem differently:

Don’t share the data at all.

Instead, give the data a single owner and communicate with it.

go channel
In this design:

  • only one goroutine owns count
  • other goroutines send messages
  • no locks are needed

Compared to Java, this approach offers a different and often simpler way to reason about concurrency.


A Small Note on Channels
Channels in Go can be buffered or unbuffered.

In this example, the channel is unbuffered, meaning sending and receiving must happen at the same time. This makes coordination very explicit.

Buffered channels allow limited queuing and can be useful when some decoupling between goroutines is needed. I’ll explore this more in the next part.


The Key Difference I Noticed
At this point, the mental difference became clear.

Java’s usual approach

  • Share data between threads
  • Protect it with locks or atomics
  • Ensure thread safety at every access point

Go’s encouraged approach

  • Reduce shared ownership
  • Let one goroutine own the data
  • Communicate through channels

Both approaches are valid.
But Go makes it easier to design away shared state instead of constantly protecting it.


A Simple Rule That Helped Me

While learning Go, this simple checklist helped me decide what to use:

  • If multiple goroutines must update the same data → consider a single owner goroutine
  • If shared access is unavoidable → use a mutex
  • If it’s just a counter or flag → consider atomic operations

Atomic operations work well for simple counters or flags, but once multiple values need to stay consistent, a mutex or single-owner goroutine is usually the safer choice.

This mental model helps clarify Go’s approach to concurrency.


Takeaway from Part 2

  • Java concurrency often focuses on protecting shared data
  • Go encourages reducing shared ownership
  • Mutexes are useful and sometimes the right choice
  • Asking who owns this data? can simplify concurrent designs

This difference helps explain the intent behind Go’s concurrency model.


What’s Next

In Part 3, I’ll look at:

  • worker pools and backpressure
  • buffered vs unbuffered channels in practice
  • graceful shutdown
  • why goroutines are cheap (at a high level)

Top comments (0)