DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Deadlock-Free Synchronization in Java
  • Java Best Practices Quick Reference
  • Mastering Thread-Local Variables in Java: Explanation and Issues
  • Unlocking Performance: Exploring Java 21 Virtual Threads [Video]

Trending

  • Memory Leak Due to Time-Taking finalize() Method
  • System Coexistence: Bridging Legacy and Modern Architecture
  • Introduction to Retrieval Augmented Generation (RAG)
  • Software Delivery at Scale: Centralized Jenkins Pipeline for Optimal Efficiency
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. Java Concurrency: Synchronization

Java Concurrency: Synchronization

Check out this post to learn more about Java concurrency and synchronization.

By 
Brian Hannaway user avatar
Brian Hannaway
·
Sep. 16, 19 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
23.8K Views

Join the DZone community and get the full member experience.

Join For Free

Java synchronization

Learn more about Java concurrency and synchronization

In a previous post, we looked at running tasks across multiple threads using the ExecutorService. Accessing and manipulating an object from multiple threads can pose a problem when the object in question holds state. If multiple threads attempt to modify shared state, behavior can unpredictable and result in data being left in an inconsistent state.

You may also like: Java Concurrency in Depth: Part 1

Unsynchronized Code

Take the simple BankAccount class below. It holds state in the form of a balance, which can be increased and decreased using the credit and debit methods. When used in a single-threaded context, this object will behave as expected, crediting and debiting the amounts specified.

public class BankAccount {

  private double balance;

  public void credit(double amount){
    balance = balance + amount; 
  }

  public void debit(double amount){ 
    balance = balance - amount; 
  }

  public double getBalance(){
    return balance;
  }
}


However, when multiple threads call the credit and debit methods simultaneously, we can end up with unpredictable behavior and an incorrect balance. To see this in action, the test method below uses an  ExecutorService to submit 100 account debits and 100 account credits, each for £100.

@Test
public void testBankAccountWithoutSynchronization1(){

  BankAccount bankAccount = new BankAccount();

  ExecutorService executorService = Executors.newFixedThreadPool(10);

  IntStream.rangeClosed(1, 100).forEach(i->{ 
    executorService.submit(()-> { 
      bankAccount.debit(100); 
    });

    executorService.submit(()-> { 
      bankAccount.credit(100); 
    });
  });

  executorService.shutdown(); 
  System.out.println("Final Balance: " + bankAccount.getBalance()); 
}


After all debits and credits have run, we'd expect the account to have a balance of zero. However, what I ended up with was a balance of -100 as shown below. Note that if you're running the sample code, it may take a number of runs before you see a non-zero result.   

Final Balance: -100.0

So, What's the Problem?

The credit method takes the current balance, adds a specified amount to it, and then assigns the new value back to balance. The problem is that adding a value to balance and then assigning that result back to  balance is not an atomic operation. That means that it doesn't happen as one distinct unit of work. Instead, a thread may add the specified amount to the current balance, and before it has a chance to assign the new value back to balance, the balance variable may be updated by another thread. At this point, the balance value held by the first thread is stale and the resulting variable assignment is incorrect.

public void credit(double amount){ 
    balance = balance + amount;       
}


To resolve the issue, we need to ensure that balance = balance + amount becomes an atomic operation, performed in isolation by one thread at a time.

Synchronized Methods

The simplest way to achieve this is by marking the method as synchronized. A synchronized method uses an implicit lock to ensure that only 1 thread at a time can enter the method. As a result, any shared state referenced inside the method is no longer vulnerable to being manipulated by multiple threads at the same time. In this instance, the shared state we're protecting is the account balance.

public synchronized void credit(double amount){
    balance = balance + amount;         
}


Note: Using synchronized on an instance method will use the current objects intrinsic lock for controlling access to the method. Using  synchronized on a static method, however, uses the intrinsic lock associated with the class, not the object. This is an important distinction, as it is possible for one thread to hold an objects' intrinsic lock, while another thread, at the same time, can hold the intrinsic lock of the class.

Synchronized Blocks

While synchronizing an entire method can be useful, it's sometimes preferable to only synchronize a portion of a method instead. If you think about it, synchronizing a method creates a bottleneck by allowing only 1 thread into the method at a time. This bottleneck, known as contention, is a result of multiple threads competing to acquire a single lock. Contention can have an adverse effect on performance, so it can be preferable to synchronize only vulnerable code rather than an entire method. Thankfully, the  synchronized block below allows us to do exactly that.

public void debit(double amount){

  // execute some non vulnerable code here...

  //vulnerable code is executed inside synchronized block
  synchronized (this) {
    balance = balance - amount; 
  }

  // execute some more non vulnerable code here...
}


When defining a synchronized block, you must specify an object on which to lock. The intrinsic lock of the specified object is used to control access to the synchronized block. A typical approach is to use the current object by specifying this (shown above). 

A final thread-safe version of the original BankAccount class is shown below.

public class SynchronizedBankAccount {

  private double balance;

  public synchronized void credit(double amount){
    balance = balance + amount; 
  }

  public void debit(double amount){ 
    synchronized (this) {
      balance = balance - amount; 
    } 
  }

  public double getBalance(){
    return balance;
  }
}


Granular Control With the Lock Interface

While synchronized methods and blocks are sufficient in most instances, there are times when greater control is required. The Lock interface defines a set of locking operations that provide developers with more granular control for writing thread-safe code. ReentrantLock is a common implementation of the Lock interface and one we're going to discuss in next few sections.

ReentrantLock — What Is Reentrance?

The term reentrant refers to a locks ability to be acquired multiple times by the same thread. Implicit locking implemented via a synchronized method or block is reentrant. This means that a thread in a  synchronized method may call into another synchronized method without blocking itself. While a reentrant lock may be acquired many times by the same thread, it must also be released the same number of times before the lock can be acquired by another thread.

Creating a ReentrantLock

The code snippet below creates a ReentrantLock using its no argument constructor.

private Lock lock = new ReentrantLock();

A second constructor takes a boolean to indicate whether or not the lock should apply a fairness policy. If set to true, a fairness policy is implemented that ensures that the longest waiting thread will acquire the lock when it becomes available. This avoids high priority threads monopolizing CPU time, while lower-priority threads are left to wait for long periods.

private Lock lock = new ReentrantLock(true);

Locking and Unlocking

The code snippet below is an updated version of the BankAccount credit method we looked at earlier. Instead of using the synchronized keyword I've used a  ReentrantLock to control access to the vulnerable code. When a thread enters the method it will call lock in an attempt to acquire mutually exclusive access to the ReentrantLock. If the lock hasn't already been acquired by another thread, it will be acquired by the current thread, which will then be allowed to proceed.

If the lock has already been acquired by another thread when lock is called, the current thread will block until the lock becomes available again.

public void credit(double amount){

  try{   
    lock.lock();
    balance = balance + amount;    
  }
  finally{
    lock.unlock(); 
  }  
}


After the balance has been updated the unlock method is called to signal that the current thread is finished and the lock is released. At this point, the lock can be acquired by a waiting thread.

Note that unlock should always be called inside a finally block. This ensures that the lock is always released, even if an exception is thrown after the lock is acquired. Its imperative that the lock is released, as failing to do so will result in other threads being blocked indefinitely.

More Flexible Locking and Unlocking

The lock method we looked at above attempts to acquire a lock, waiting indefinitely if it's not available. This is the same behavior as a  synchronized method or block. There are, however, times when we may want to limit the amount of time we're willing to wait for a lock, especially if we have a large number of threads competing for the same lock. Rather than have many threads blocked waiting for access, we could take some other course of action when a lock isn't available. Luckily, ReentrantLock provides this flexibility via two flavours of the tryLock method.

Acquiring a Lock With tryLock()

ThetryLock method checks for the availability of the lock, returning true if the lock is available. When tryLock returns true the current thread acquires the lock and is free to execute whatever vulnerable code is being protected. If the lock is held by another thread and not immediately available tryLock will return false, allowing the application to immediately take some other course of action.

Acquiring a Lock With tryLock(long time, TimeUnit unit)

An overloaded version of tryLock takes a time and time unit that determines how long the current thread should wait for the lock to become available. If thelock is available when tryLock is called, the current thread will acquire the lock and the method will return true immediately. At this point, the current thread is free to execute whatever vulnerable code is being protected.

If on the other hand the lock has already been acquired by another thread, the current thread will wait for the lock to be released. If the lock is released within the specified period of time, the current thread will acquire the lock and tryLock will return true. If the time period elapses and the lock still hasn't been released, tryLock will return false. At this point, the current thread has not acquired the lock and an alternative course of action can be taken.

The debit method below shows tryLock in action. On line 3, the current thread waits up to two seconds for the lock to become available. If the lock is available or becomes available within two seconds, the current thread is free to execute whatever vulnerable code is being protected. If thelock doesn't become available after two seconds, the debit amount is added to a queue for processing later.

public void debit(double amount) throws InterruptedException{

  if(lock.tryLock(2000, TimeUnit.MILLISECONDS)){
    try{ 
      balance-=amount; 
    }
    finally{
      lock.unlock(); 
    }
  }
  /* lock isn't available right now so do something else */
  else{
    /* add debit amount to queue for processing later */
  } 
}


ReentrantReadWriteLock

Another lock implementation worth looking at is the ReentrantReadWriteLock. As the names suggests, the ReentrantReadWriteLock encapsulates both a read and write lock inside a single lock implementation.

Why Would I Need a Read & Write Lock?

When a thread attempts to read from or write to a HashMap using any of the above approaches, it will obtain mutually exclusive access to the lock, blocking all other threads from reading or writing at the same time. While this is the desired behavior when writing to a HashMap, there is no reason why we should stop multiple threads reading from the HashMap concurrently. Reads do not manipulate the HashMap in any way, so there is no reason to limit reads to only 1 thread at a time.

ReentrantReadWriteLock

Like ReentrantLock, the ReentrantReadWriteLock can be instantiated with an optional boolean to indicate whether or not a fairness policy should be established. If a fairness policy is chosen, threads will acquire the lock in the order they've requested it.

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

Acquiring a Write Lock

public void saveTransaction(String transaction) throws InterruptedException {

    try{
        readWriteLock.writeLock().lock();
        transactionHistory.put(UUID.randomUUID().toString(), transaction); 
    }
    finally{
        readWriteLock.writeLock().unlock(); 
    }  
}


When the write lock is acquired by a thread, that thread has mutually exclusive access to the ReentrantReadWriteLock. At this point, no other thread can obtain a read or write lock, meaning that the HashMap can not be read from or written to by any other thread.

Acquiring a Read Lock

public List<String> getTransactions(){

  List<String> transactions = null;

  try{
    readWriteLock.readLock().lock();
    transactions = transactionHistory.values().stream().collect(Collectors.toList());
  }
  finally{
    readWriteLock.readLock().unlock(); 
  }

  return transactions;
}


Unlike the write lock, the read lock can be held by multiple threads. In the sample code, this allows multiple threads to read from the Map concurrently.

Side Note

While the example above serves to demonstrate the fundamentals of ReentrantReadWriteLock, in reality, we wouldn’t use it to synchronize access to a HashMap. Instead, we’d make use of the Collections class, which has a method that takes a Map and returns a thread-safe equivalent, as shown below:

Map<String, String> transactionHistory = Collections.synchronizedMap(new HashMap<String, String>());

Wrapping Up

The sample code for this post is available on GitHub, so feel free to pull it down and have a play around. If you have any comments, questions, or suggestions, please leave a comment below.

Further Reading

Java Concurrency in Depth: Part 1

A Birds-Eye-View on Java Concurrency Frameworks

[DZone Refcard] Core Java Concurrency

Threading Java (programming language) code style Blocks

Published at DZone with permission of Brian Hannaway, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Deadlock-Free Synchronization in Java
  • Java Best Practices Quick Reference
  • Mastering Thread-Local Variables in Java: Explanation and Issues
  • Unlocking Performance: Exploring Java 21 Virtual Threads [Video]

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!