Module 3 - Thread Safety

Module Overview

Understand thread safety, synchronization, and atomic methods to prevent data corruption and race conditions in concurrent Java applications.

Learning Objectives

  • Identify whether a given method executes atomically
  • Recall that each thread has its own stack, but all threads share the heap
  • Recall that Java uses locks to support synchronized code
  • Explain the conditions that will cause a deadlock
  • Design and implement a class that prevents race conditions by declaring one or more of its methods as synchronized
  • Use atomic types to develop consistent read and write functionality across threads
  • Use immutable objects to share consistent state across threads
  • Design and implement functionality that shares a consistent state across threads
  • Recall that synchronized code can only be called by one thread at a time
  • Recall that a deadlock is a condition where two or more threads can never continue because they are waiting for each other to finish
  • Explain that the JVM blocks threads that try to execute a synchronized method on an object that is already locked

Thread Safety Fundamentals

Thread safety ensures that your code works correctly when accessed by multiple threads simultaneously. Understanding memory architecture is key to thread safety.

Key concepts:

  • Each thread has its own stack for local variables
  • All threads share the same heap for objects
  • Race conditions occur when multiple threads access shared data and at least one modifies it
// Thread-unsafe counter example
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // This is not atomic!
    }
    
    public int getCount() {
        return count;
    }
}

Lesson overview

You've learned a lot about threads in this unit, and you might be thinking of different ways to apply those lessons to your code. However, you need to be careful when using multiple threads. Otherwise, threads can conflict, causing data to become inconsistent between them. This lesson you'll learn techniques for thread safety to protect the data in your threads.

This reading covers memory considerations with threads as well as the basics of synchronization.

The next reading explains atomic object types, which we can use to safely share changing state between threads, and how to use immutable objects to share unchanging state between threads.

Introduction to thread safety

Webservers handle requests from many users concurrently. These requests often involve personal data, as login credentials, account balances, and contact info. Thread safety allows us to keep each thread's data current and separate.

As an example, let's say an Amazon user adds $10 to their gift card that currently has $25. At the same time, one of their family members adds $5 to the same gift card. Because these are separate requests, each is handled in their own thread.

Suppose the $5 thread gets the balance ($25), then the JVM pauses it and runs the $10 thread. The $10 thread gets the balance (still $25), adds $10, and sets the balance to $35. When the $5 thread continues running, it has no way to tell that another thread has changed the balance. It doesn't even realize it's been paused. It just adds its $5 to the balance it already got ($25), and sets the balance to $30.

Both users think they've succesfuly made their deposit. But our system lost $10.

Thread safety addresses these types of problems by ensuring threads get exclusive access to resources, so their data can't be modified by other threads while they're paused.

The relationship between threads and the heap

Before digging into our gift card problem, let's look closely at how threads use memory. As we learned previously, threads run code at the same time as our main program. Each thread has its own stack that is independent of the main execution stack. As you can see in Figure 1, the object references on the stack point to values stored in the heap. All object instances are stored in the heap, which is used by the entire application. Multiple threads can reference the same object in the heap, so any changes one thread makes to a shared object in the heap can impact other threads.

How synchronized methods work

Consider the audio system at a conference, where many attendees request a microphone to ask questions. A moderator chooses one attendee and gives them a microphone. When they give the microphone back to the moderator, all attendees are then free to request a microphone again.

Of course, each attendee is free to do other things that don't require a microphone: they can listen to the speakers, read the closed-caption display, and take pictures. No matter what, even if there are many microphones, the moderator makes sure only one person at a time can use a microphone.

Java uses the synchronized keyword to achieve similar behavior:

public synchronized void addOne(){
    this.count++;
}

Java treats each object instance like its own conference audio system. When we mark a method with the synchronized keyword, the JVM treats it like a microphone: it enforces that only one thread at a time can call any synchronized method on that instance.

Java accomplishes this through "locks". Every Java Object has a lock. To call a synchronized method on an instance, a thread must acquire the instance's lock. The JVM manages these locks and ensures that only one thread can acquire the lock at any time. If any other thread tries to acquire the lock, the JVM suspends it and marks it as BLOCKED. Once the thread that has the lock releases it, the JVM marks all the threads waiting on it as RUNNABLE. The next thread that requests the lock will acquire it and continue, while the others will become BLOCKED again.

You many wonder why we need to synchronize the method at all, since count++ is a single statement. To the computer, this is actually three operations! First, it must "load the value of count from memory". Then it must "increment the value". Finally, it must "save the value of count into memory". If another thread executes at the same time, it might read or save count in memory while the first thread is still performing one of its three operations.

Let's see synchronization in action. Here's an example using the synchronized keyword to solve our gift card problem from earlier:

public class GiftCardProcessor {

    public static void main(String[] args) {
        GiftCard giftCard = new GiftCard(25);

        Thread depostThread1 = new Thread(new GiftCardDepositer(giftCard, 5));
        Thread depostThread2 = new Thread(new GiftCardDepositer(giftCard, 10));

        giftCard.printBalance();
        depositThread1.start();
        depositThread2.start();
    }
}

public class GiftCard {
    private int balance;

    public GiftCardBalance(int balance) {
        this.balance = balance;
    }

    public void printBalance() {
        System.out.println("Current Balance is: " + balance);
    }

    public synchronized void addBalance(int addAmount) {
        System.out.println("Balance is " + balance);
        balance += addAmount;
        System.out.println("Added " + addAmount + ", new balance is " + balance);
    }
}

public class GiftCardDepositer implements Runnable {
    private GiftCard giftCard;
    private int amount;

    public GiftCardDepositer(GiftCard giftCard, int deposit) {
        this.giftCard = giftCard;
        amount = deposit;
    }

    public void run() {
        giftCard.addBalance(amount);
    }
}

In this example, when our two threads both execute addBalance(), we don't have to worry about our modifying the balance at the same time. Because addBalance() is synchronized, only one thread can update our GiftCard instance at a time. Now, no matter when a thread pauses, the other thread will wait until it's done with its method.

An example of that output could be:

Current Balance is: 25
Balance is 25
Added 5, new balance is 30
Balance is 30
Added 10, new balance is 40

Does this example of two threads accessing the same value simultaneously seem familiar? In the Executor Services lesson, you learned about race conditions and the problems they cause through the CargoDemo code. GiftCardProcessor is an example of solving those problems using synchronization. If you look back, you can locate where synchronization would solve the problem in the earlier lesson.

Avoiding deadlocks

Since synchronization helps to achieve thread safety, shouldn't we just synchronize every method? Unfortunately, there can be too much of a good thing. While synchronization can achieve thread safety, it can lead to unwanted effects on a system such as slowdowns and a state known as a deadlock.

image2.png

Figure 2 illustrates an example of deadlock, where two threads have some form of codependency in their variables. Neither thread can proceed until the other finishes due to locks on those variables. To put it into a real-world example, imagine two chefs making boiled potatoes in a kitchen with only one sink and one colander. One chef holds the colander, and has filled it with potatoes to wash in the sink. The other chef has cooked their potatoes, and is holding a pot of boiling hot water over the sink to drain it. The chef blocking the colander is waiting for the sink, and the chef blocking the sink is waiting for the colander. Neither can continue, so they'll both wait forever.

When possible, try to avoid situations of codependency in your synchronized methods to prevent deadlocks. To get an idea of what we mean, imagine our kitchen situation as if it were a Java application:

public class Chef implements Runnable {
    private Colander colander;
    private Sink sink;
    private Pot pot;
    private Potato potato;

    // Constructor omitted

    public void run() {
        sink.wash(colander, potato);
        // Other steps to transfer, prepare, and boil potatoes omitted
        colander.drain(sink, pot);
    }
}

public class Sink {

    public synchronized void wash(Colander colander, Ingredient ingredient) {
        colander.rinse(ingredient);
        // Other steps omitted
    }

    public synchronized pour(Pot pot) {
        pot.empty();
        // Implementation omitted
    }
}

pubic class Colander {
    private contents = null;

    public synchronized void rinse(Ingredient ingredient) {
        this.contents = ingredient;
    }

    public synchronized void drain(Sink sink, Pot pot) {
        this.contents = pot.getIngredient();
        sink.pour(pot);
    }
}

public class Kitchen {
    Sink sink = new Sink();
    Colander colander = new Colander();
    Chef chef1 = new Chef(sink, colander)
    Chef chef2 = new Chef(sink, colander)

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        executor.submit(chef1);
        executor.submit(chef2);
        executor.shutdown();
    }
}

Imagine chef1 has finished washing their potato and is calling the synchronized colander.drain() method. Because no other thread has acquired the lock on colander, the JVM gives it to chef1. Running at the same time, chef2 tries to call the synchronized method sink.wash(). Because no other thread has acquired the lock on sink, the JVM gives it to chef2.

When chef1 reaches sink.pour(), it will block waiting for the lock on sink. It will have to wait until chef2 finishes sink.wash(). Unfortunately, that can never happen: chef2 is about to call colander.rinse(), which is synchronized, so it will have to wait until chef1 finishes colander.drain()!

While this application can cause a deadlock, due to the nature of how threads run it won't always cause a deadlock. For instance, chef1 might reach sink.pour() before chef2 even starts sink.wash(). Then chef1's thread will have both locks, and chef2's will block. When chef1 returns from sink.pour(), it releases the sink lock, and chef2 can continue.

Since deadlocks may not happen frequently enough to show up in testing, it's important to identify deadlocks manually.

Deadlocks can only happen when multiple threads try to lock multiple objects in a different order. That usually happens when multiple synchronized methods call each other.

In this case, we could:

  • Build synchronized drain and wash methods in a singleton object like Kitchen, then remove synchronization from the Sink and Colander. Each Chef will now lock a single class, so deadlocks cannot occur.
  • Change the classes so Sink methods call Colander methods, but not vice-versa. Now each Chef must lock a Sink before trying to lock a Colander, eliminating the deadlock.
  • Give each Chef their own Colander instance, so they only block on a single Sink.

It's easier to verify that all classes go through a single synchronized class than that they all call methods in the same order or have sufficient resources to avoid mutually exclusive locks. We recommend using only one class with synchronized methods in your app.

Using and Avoiding Synchronization

Since synchronizing many methods can cause deadlocks or delays, we must avoid it if we can. Beside restricting synchronization to a single class, we also avoid synchronizing methods that read data without modifying it.

For instance, the GiftCard class we explored has an unsynchronized printBalance() method. This returns a single value, so it is always consistent with the state of the object at some point in time. Users understand that when they see an unexpected value, they should check whether someone else was making changes at the same time. Instead of synchronizing read methods, we prefer to provide audit trails that explain how modifications occurred.

On the other hand, read methods that return collections of data may return inconsistent results. Consider a system with lists of "premium", "standard", "inactive", and "banned" members.

public class Membership {
    List premium;
    List standard;
    List inactive;
    List banned;

    /* Removes the member from current list and move to banned list */
    public synchronized boolean banMember(Member member) {
        /* Implementation omitted for brevity */
    }

    /* Reports the list of people with a premium membership. */
    public void exportPremiumMembers() {
        /* Implementation omitted */
    }

    /* Reports the members of all lists. */
    public void exportAllMemberRosters() {
        /* Implementation omitted */
    }
}

The exportPremiumMembers() method probably doesn't need to be synchronized. Even if we call banMember() while it's iterating over the premium list, and ban a member that has already been exported, the export as a whole still represents the state of the system at some point in time. It's not inconsistent or confusing.

However, exportAllMemberRosters() should probably be synchronized. If we call banMember() while exportAllMemberRosters() is running, and we ban a premimum member who has already been exported, then exportAllMemberRosters() may export the same member again as a banned member! This would be inconsistent and confusing. If users depend on that report to make business decisions, we can ensure consistent data by synchronizing exportAllMemberRosters().

We synchronize as little as necessary. We try to restrict synchronization to the data-modifying methods of a single class, and we only synchronize read methods if they could return inconsistent collections of data that impact our users.

Summary

The synchronized keyword helps us write thread safe code by ensuring multiple threads cannot execute synchronized methods on a single object at the same time. That said, you must take care when writing your synchronized code to avoid deadlocks, where multiple threads are waiting for each other to unlock before execution can continue.

Next up

In the next reading, we'll tackle atomic objects and put synchronization to use in some more coding examples.

Applying Atomic Methods

Java provides atomic classes for common operations that need to be thread-safe without using explicit synchronization. These classes use low-level atomic hardware operations for better performance.

Common atomic classes:

  • AtomicInteger, AtomicLong, AtomicBoolean
  • AtomicReference for object references
  • AtomicIntegerArray, AtomicLongArray, etc. for arrays
// Thread-safe counter using AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet(); // Atomic operation
    }
    
    public int getCount() {
        return count.get();
    }
    
    // Additional atomic operations
    public void add(int value) {
        count.addAndGet(value);
    }
    
    public boolean compareAndSet(int expected, int update) {
        return count.compareAndSet(expected, update);
    }
}

Overview

You've learned a lot about threads in this unit, and you might be thinking of different ways to apply those lessons to your code. However, you need to be careful when using multiple threads. Otherwise, threads can conflict, causing data to become inconsistent between them. This lesson you'll learn techniques for thread safety to protect the data in your threads.

The previous reading covered memory considerations with threads as well as the basics of synchronization.

This reading explains atomic object types, which we can use to safely share changing state between threads, and how to use immutable objects to share unchanging state between threads.

Applying synchronization to objects

How would you describe the behavior of a synchronized method? A synchronized method uses locks to ensure only one thread at a time can use any of the object's synchronized methods. You might say that synchronized methods can't be divided between threads.

We can sum up that description of synchronization by saying those methods execute atomically. What if, instead of atomically executing methods, we wanted threads to atomically interact with variables? Luckily, Java's tools ensure we don't have to write synchronized methods for every variable just to ensure thread safety.

Atomic object types

As mentioned, Java has some atomic objects built-in to make thread safety easier. AtomicInteger is one such example, a wrapper of the native Java int that enforces synchronized access. Atomic objects work in much the same way as the synchronized methods we discussed in the last reading. In fact, they implement synchronization in their own functionality. Several atomic object types are linked to primitive java types, as well as a generic wrapper object to make any other class atomic.

  • AtomicInteger: represents int
  • AtomicLong: represents long
  • AtomicBoolean: represents boolean
  • AtomicReference: wraps an object reference to make the reference atomic.

Functions of atomic objects

Each atomic object type has its own list of methods and operations that can be referenced in Java documentation. There are three basic methods that are used by all these classes that are important for you to know. They are:

  • get(): gets the object's value, with the latest changes from other threads visible
  • set(value): writes the object's value so that it's visible to other threads
  • compareAndSet(expect, update): sets the object's value to update, only if the object's current value is expect

Using atomic objects for read/writing

Looking back at the CargoDemo example from the Intro to Threads lesson, we can see the code snippet suffers from race conditions. There was a brief mention that locking could offer a better solution. Below is the initial code with a race condition:

public class CargoDemo {
    public static int currentInventory;

    public static void main(String[] args) {
        currentInventory = 100;
        System.out.println("Start of day inventory: " + currentInventory);
        
        Thread shippingThread = new Thread(new ShippingManager());
        Thread deliveryThread = new Thread(new DeliveryManager());
        shippingThread.start();
        deliveryThread.start();
    }
}

public class DeliveryManager implements Runnable {
    public void run() {
        CargoDemo.currentInventory += 300;
        System.out.println("DeliveryManager inventory: " + CargoDemo.currentInventory);
    }
}

public class ShippingManager implements Runnable {
    public void run() {
        CargoDemo.currentInventory -= 100;
        System.out.println("ShippingManager inventory: " + CargoDemo.currentInventory);
    }
}

Remember that += and -= are one statement to Java, but three operations to the JVM: get the current value from memory, calculate the new value, and save the calculated value in memory.

If both threads retrieved and calculated the value at the same time, and the ShippingManager saved its calculation before the DeliveryManager, you might see this output:

Start of day inventory: 100
ShippingManager inventory: 0
DeliveryManager inventory: 400

This isn't good because the final state of our system is wrong. The DeliveryManager is overwriting the changes done by ShippingManager, with each thread updating currentInventory independently. In a real-world scenario, our output indicates our inventory is larger than it actually is, which would certainly cause concern.

To solve this problem, let's rewrite currentInventory as an AtomicInteger. This allows thread-safe reading and writing to the currentInventory variable. To do this, we must change the initialization of currentInventory and use AtomicInteger.addAndGet() to both add to and retrieve currentInventory in one synchronized call.

import java.util.concurrent.atomic.AtomicInteger;
public class CargoDemo {
    public static AtomicInteger currentInventory;

    public static void main(String[] args) {
        currentInventory = new AtomicInteger(100);
        System.out.println("Start of day inventory: " + currentInventory.get());
        
        Thread shippingThread = new Thread(new ShippingManager());
        Thread deliveryThread = new Thread(new DeliveryManager());
        shippingThread.start();
        deliveryThread.start();
    }
}

public class DeliveryManager implements Runnable {
    public void run() {
        System.out.println("DeliveryManager inventory: " + CargoDemo.currentInventory.addAndGet(300));
    }
}

public class ShippingManager implements Runnable {
    public void run() {
        System.out.println("ShippingManager inventory: " + CargoDemo.currentInventory.addAndGet(-100));
    }
}

Now if the ShippingManager and DeliveryManager update currentInventory at the same time, whichever one gets there first will make updates, and the other will wait until it's done.

If the shippingThread executes first, the output would be:

Start of day inventory: 100
ShippingManager inventory: 0
DeliveryManager inventory: 300

If the deliveryThread executes first, the output would be:

Start of day inventory: 100
DeliveryManager inventory: 400
ShippingManager inventory: 300

Either way, the final value is correct. We've eliminated the race condition!

Using immutable objects with threads

Previously, we learned how immutable objects ensure the details contained within a class instance stay constant. Immutable objects are an important part of thread safety. Let's think back to an example we saw for ExecutorService. In that lesson, we used an ExecutorService to create different aspects of a user account in parallel. Let's add a little more logic to that example and examine how immutable objects are critical to concurrency.

A big piece left out of the ExecutorService example was how each of the separate threads would know which user they were populating. Let's look at a revision that passes in the user account, and examine some issues that might come up.

public class AmazonAccountGenerator {
    private final UserAccount userAccount;
    public AmazonAccountGenerator(UserAccount userAccount) {
        this.userAccount = userAccount;
    }
    public void createAccount() {
        ExecutorService executor = Executors.newCachedThreadPool();
        Runnable uploadPhoto = () -> {
            System.out.println("Uploading photo to account " +
                    userAccount.getAccountId());
        };
        Runnable submitAccountInfo = () -> {
            System.out.println("Submitting account info to account " +
                    userAccount.getAccountId());
        };
        Runnable submitBankInfo = () -> {
            System.out.println("Securely submitting bank info to account " +
                    userAccount.getAccountId());
        };
        executor.submit(uploadPhoto);
        executor.submit(submitAccountInfo);
        executor.submit(submitBankInfo);
        executor.shutdown();
    }
}

Here we have the AmazonAccountGenerator class. When a user sets up a new account, the main AccountManager creates a new instance of this class. It gives it a UserAccount object that has the core account information, such as accountId and the user's name. Once an AmazonAccountGenerator is created, AccountManager starts creating the account by calling createAccount. This method creates an ExecutorService and a lambda Runnable for each of the account creation subtasks. Then, it executes those Runnable instances. Each Runnable still only has dummy code, which refers to the UserAccount instance we passed into the generator instance.

As you may recall, lambda functions access in-scope properties and variables from the class where they were created. In this case, the userAccount property on our generator is in-scope to the lambdas. They can access and use that userAccount instance. Each thread needs to see the accountId so that the resources are linked to the main account properly.

In our simple example, each of those threads complete very quickly. They're not really doing anything, so they take almost no time to complete. In reality, that would not be the case. Each of those threads may have quite a few things to do before completing. While they were working, what if the main AccountManager changed the UserAccount reference, or updated its account number? If that were to happen while the threads were running, it's likely one of the resources would be created and linked to the wrong user account!

You may notice that in our AmazonAccountGenerator class we declare the userAccount property to be final. This is important. The final keyword ensures the reference stored in that property cannot change once it's set. This is a perfect first step, as it ensures the UserAccount reference we passed into our generator at creation cannot change. It's a big piece in helping us know the threads will always use the same UserAccount object. That's only part of it though.

If all our threads are reliant on having the same information as each other (also known as having the same state), we also need to make sure the data in the UserAccount instance remains constant. This is where immutable objects come into play. Immutable objects are classes where the data inside the class cannot change once it's been set. We can accomplish this through a variety of techniques, such as using the final keyword, removing setters from the class, and using defensive copying in the constructor and the getters. As a refresher, look at how we implemented our immutable UserAccount class below.

public final class UserAccount {
    private final String accountId;
    private final String firstName;
    private final String lastName;
    private final Date creationDate;

    public UserAccount(String accountId, String firstName, String lastName, Date creationDate) {
        this.accountId = accountId;
        this.firstName = firstName;
        this.lastName = lastName;
        this.creationDate = new Date(creationDate.getTime());
    }

    public String getAccountId() {
        return accountId;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Date getCreationDate() {
        return new Date(this.creationDate.getTime());
    }
}

This UserAccount is an example of an immutable class. We used defensive copying for the creationDate, final for all properties, and no setters. By designing the UserAccount class this way, we ensure the properties of the instance we pass to our AmazonAccountGenerator can never change after creation. Combine this with declaring the userAccount reference final, and userAccount will be consistent regardless of how threads access it.

It's worth noting, you should be careful when sharing state between threads. Since it's impossible to know exactly where the threads are in their execution, you run the risk of variables getting out of sync. Even with care, it's possible for references to get passed around and for another section of code to change the data without realizing it will affect the thread. Therefore, it's important to use immutable objects with your threads whenever those values shouldn't change. This ensures that the state of the data in the thread is constant and unchanging for the life of the thread.

When does a method execute atomically?

To answer this question, let's review what we have learned in this lesson. Atomically executing means only one thread can execute the method or access the variable at a time. Any other thread will block until the current thread finishes the atomic operation. We use the synchronized keyword to restrict methods to a single thread. Alternatively, atomic objects, like AtomicInteger, provide atomic access to a specific variable. Use these to hold important values that are accessed and updated by multiple threads. Look at the code snippet below:

private int count;

public void addCount() {
    System.out.println("Count:" + ++count);
}

Notice our snippet isn't using any form of synchronization, and it's not atomic. If multiple threads called addCount() at once, they could overwrite each other, throwing off the final value of count. Now, compare the following two code snippets:

private int count;
public synchronized void addCount() {
    System.out.println("Count: " + ++count);
}
private AtomicInteger count;
public void addCount() {
    System.out.println("Count: " + count.incrementAndGet());
}

They're both using different forms of synchronization, and both are thread-safe, but only the synchronized method executes atomically. The synchronized keyword ensures all its operations will execute before another thread can call it.

The AtomicInteger method is thread-safe, because only one thread can modify the shared value at a time. When many threads execute it, the end value will always be correct. However, a thread could be paused after incrementing but before executing its println(), and another thread could increment and print the value before the first thread resumed and printed its value. Therefore, the entire method is not atomic, and we could see print statements in an unexpected order.

Conclusion

When working with concurrency, it's important to consider thread safety. We don't want threads inadvertently changing values that other threads need. We need to protect the integrity of data. We must be certain we don't end up having threads double-charging a credit card or giving a shipping company the address from the wrong customer's user account. Fortunately, Java provides many ways for programmers to ensure thread safety. We showed you how atomic objects are great for important variables that should only be accessed by one thread at a time. The built-in methods to set and get information safely make synchronization easy and automatic. We also showed you how to apply synchronization to your own methods so only one thread can execute them at a time. Additionally, if we need to share state that shouldn't be changing between threads, we can use immutable objects to ensure those values can't change while the threads run.

Synchronization and Locks

Java provides synchronization mechanisms to prevent race conditions. The synchronized keyword ensures that only one thread can execute a method or block at a time.

Important synchronization concepts:

  • Every object in Java has an intrinsic lock (monitor)
  • The synchronized keyword uses this lock to prevent concurrent access
  • Only one thread can hold an object's lock at a time
// Thread-safe counter using synchronized methods
public class ThreadSafeCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// More granular synchronization using synchronized block
public class BetterThreadSafeCounter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized(lock) {
            count++;
        }
    }
    
    public int getCount() {
        synchronized(lock) {
            return count;
        }
    }
}

Thread Safety Best Practices

Designing thread-safe code requires careful consideration of data access patterns and synchronization needs.

Key tips for writing thread-safe code:

  • Minimize shared mutable state
  • Prefer immutable objects when possible
  • Use synchronization only when necessary
  • Keep synchronized blocks small and focused
  • Consider using thread-safe collections like ConcurrentHashMap
  • Avoid leaking the "this" reference during construction
// Thread-safe singleton pattern example
public class ThreadSafeSingleton {
    // Volatile ensures visibility across threads
    private static volatile ThreadSafeSingleton instance;
    
    // Private constructor prevents external instantiation
    private ThreadSafeSingleton() {}
    
    // Double-checked locking pattern
    public static ThreadSafeSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
}

Guided Project

Unsynchronized

We're going to simulate concurrency to give you concrete experience with thread safety. We'll be running this code:

// Add some Amazon Coins to the account
public class AddCoinsTask implements Runnable {
    private Account account;
    private int amount;

    public AddCoinsTask(Account account, int amount) {
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        Team.answerQuestions();
        account.setBalance(account.getBalance + amount);
    }
}

Your team will act as one AddCoinsTask. The instructor will be the JVM. Your AddCoinsTask has already been instantiated with an account and 1 coin to add. You've been assigned to a Thread, and your run() method has just been called.

Team.answerQuestions(): Discuss this question with your team:

How would you modify AddCoinsTask and Account to be thread-safe?

account.setBalance(): Your teacher should have included a link to a file on Drive that contains the Account information. (It's like a real-life Account reference!) Download the file, update the balance according to the code (remember, you were instantiated with 1 coin!), and upload it back to the same location.

Now discuss with your team:

What do you think the number will be after all threads/teams are done?

Modify your previous answer in the class digest to include this number.

Return to the class meeting as soon as you're done.

Resources