Lesson 3
Data Sharing and Synchronization
Synchronization and Data Sharing Between Threads

Welcome back to our journey into concurrency in Java! In our last lesson, we explored the thread lifecycle and fundamental thread operations. We learned about different thread states and key methods such as start(), sleep(), and join(). This foundational knowledge will serve us well as we now dive into an essential aspect of concurrency: synchronization and data sharing between threads.

What You'll Learn

In this lesson, you will gain the knowledge necessary to manage shared data between threads and prevent common concurrency issues:

  • Understand how threads can share data through shared variables.
  • Recognize the risks of unsynchronized data access and race conditions.
  • Learn how to use the synchronized keyword to prevent concurrency issues.

This lesson will equip you with practical skills for managing shared data safely in a concurrent environment.

Shared Variables and Synchronization

When multiple threads operate on shared variables, it can lead to data inconsistencies and unpredictable behavior if not managed correctly. Shared variables are pieces of data that multiple threads access and modify simultaneously, such as instance fields in a class or elements of a shared collection.

Shared access allows threads to communicate and collaborate effectively, but without proper management, it introduces significant risks. Let’s explore the common problems with shared variables in a multithreaded environment.

Common Issues with Shared Variables

When multiple threads access shared data without appropriate control, it can lead to:

  • Race Conditions: These occur when two or more threads try to modify a shared variable concurrently, leading to inconsistent results. For example, if multiple threads attempt to increase a shared counter at the same time, some increments may be lost due to overlapping access.

  • Non-Atomic Operations: Operations like incrementing a shared counter (count++) involve multiple steps: reading the current value, incrementing it, and then writing it back. These steps can be interrupted by other threads if they are not synchronized, resulting in unpredictable behavior and incorrect results.

Understanding Shared Data with Synchronization

Consider a scenario where two threads increment a shared counter. The following code snippet demonstrates how we can manage shared data effectively using synchronization:

Java
1public class SynchronizedCounter { 2 private int count = 0; 3 4 // Adding synchronized to prevent issues 5 public synchronized void increment() { 6 count++; 7 } 8 9 public synchronized int getCount() { 10 return count; 11 } 12}

In this example:

  • The increment() method increases the count by 1.
  • The synchronized keyword ensures that only one thread at a time can execute the increment() method, which prevents inconsistencies in shared data.

If we didn't use synchronized, multiple threads could execute the increment() method at the same time, leading to race conditions. For instance, both threads might read the current value of count as 100, increment it to 101, and write the value back. The correct final value should have been 102, but due to this overlap, it will only be 101, causing a lost update. Synchronization ensures that only one thread modifies count at a time, preserving data integrity.

Running Threads with Shared Data

Let’s create two threads that increment a shared counter 1000 times each. This will illustrate how synchronization ensures data integrity:

Java
1public class Main { 2 public static void main(String[] args) throws InterruptedException { 3 SynchronizedCounter counter = new SynchronizedCounter(); 4 Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); }); 5 Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); }); 6 7 t1.start(); 8 t2.start(); 9 t1.join(); 10 t2.join(); 11 12 System.out.println("Final count with synchronization: " + counter.getCount()); 13 } 14}

Here’s what’s happening:

  • Shared Counter: We create a SynchronizedCounter instance, which will be incremented by two threads.
  • Creating Threads:
    • Two threads (t1 and t2) are created using the Thread class constructor, each taking a Runnable as a parameter.
    • The lambda expression (() -> { for (int i = 0; i < 1000; i++) counter.increment(); }) is a shorthand way to define the run() method of a Runnable implementation.
  • Starting Threads:
    • Calling t1.start() and t2.start() moves the threads from the NEW state to the RUNNABLE state, starting their execution concurrently.
  • Joining Threads: We use t1.join() and t2.join() to ensure that the main thread waits for both threads to finish before printing the result.

The synchronized keyword in the increment() method ensures only one thread modifies count at a time, preventing race conditions. Without it, the final count may be less than 2000 due to overlapping operations and lost updates.

Using the `synchronized` Keyword

The synchronized keyword is a fundamental mechanism for preventing race conditions and ensuring data integrity. Here’s how it helps:

  • Method Synchronization: Declaring a method as synchronized ensures that only one thread can execute it at a time, preventing overlapping operations.
  • Atomic Updates: By locking the method, synchronized ensures that the updates to shared variables (like count++) happen atomically, meaning that no other thread can intervene until the operation is complete.

These measures guarantee that no two threads can modify count at the same time, thereby avoiding inconsistencies.

Importance of Synchronization

Synchronization is essential for managing shared data in concurrent applications. Here’s why:

  • Data Integrity: Proper synchronization ensures that shared variables maintain the correct values, avoiding race conditions.
  • Safe Execution: It allows us to make programs more predictable and prevents issues caused by concurrent access.
  • Real-World Relevance: Many real-world systems, from financial transaction processing to real-time applications, rely on synchronization to maintain consistency and reliability.

By understanding how to use the synchronized keyword to manage shared data, you will be able to build robust concurrent programs that handle shared resources effectively.

Ready to tackle some exercises to see these concepts in action? Let’s get started!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.