low-level memory order guarantees

volatile

Keyword volatile is used when declaring variables and tells the JVM that the value of this variable can be changed by different threads. The syntax is simple:

public class VolatileExample {
    private volatile int counter;
    // остальные методы
}

Accommodation volatile before the variable type ensures that all threads will see the current value counter.

Primitive types

For primitive types, use volatile gives:

  • Visibility of changes: If one thread changes the value volatile variable, other threads will immediately see this change.

  • Prohibition of reordering: The compiler and processor will not reorder read and write operations with volatile variables.

Example:

public class FlagExample {
    private volatile boolean isActive;

    public void activate() {
        isActive = true;
    }

    public void process() {
        while (!isActive) {
            // Ждем активации
        }
        // Продолжаем обработку
    }
}

Objects

When used with objects volatile provides the appearance of changing the reference to an object, but not its internal state.

Example:

public class ConfigUpdater {
    private volatile Config config;

    public void updateConfig() {
        config = new Config(); // Новая ссылка будет видна всем потокам
    }

    public void useConfig() {
        Config localConfig = config;
        // Используем localConfig
    }
}

If an object's internal state can change, it must be made thread-safe in other ways, such as through immutable objects or synchronization.

How volatile affects reading and writing variables

Keyword volatile affects the interaction of threads with the variable as follows:

  • Reading: Every time you access volatile the variable the thread reads its current value from main memory, not from the processor cache.

  • Recording: When changing volatile variable, its new value is immediately written to the main memory, making it available to other threads.

Example without volatile:

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

In a multithreaded environment, different threads may operate on a stale value. countas it may be cached.

Example with volatile:

public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Now each thread will work with the current value count.

Be careful though.: operation count++ is not atomic, even with volatile. For atomicity, use AtomicInteger.

Volatile Limitations

  1. Lack of atomicity of complex operations

    volatile does not make operations atomic. Increment operations count++decrement, and other complex operations can lead to race conditions.

    Example of a problem:

    public class NonAtomicVolatile {
        private volatile int count = 0;
    
        public void increment() {
            count++;
        }
    }

    Here count++ consists of reads, increments, and writes that can be interrupted by other threads.

  2. Does not provide access synchronization

    volatile does not replace blocks synchronized or objects Lock. It does not prevent multiple threads from accessing a block of code at the same time.

Examples of use

Stop Stream Flags

In server applications or services, there is often a need to gracefully stop a thread or task, for example when the application terminates or when settings change.

public class Worker implements Runnable {
    private volatile boolean isRunning = true;

    @Override
    public void run() {
        while (isRunning) {
            // Выполняем задачи
            performTask();
        }
    }

    public void stop() {
        isRunning = false;
    }

    private void performTask() {
        // Реализация задачи
    }
}

Why volatile: Variable isRunning is used to control the thread execution cycle. Without volatile A thread may not see changes to a variable made by another thread due to processor-level variable caching.

Method stop() can be called from another thread by setting isRunning V false. Thanks to volatile The current thread will immediately see this change and shut down gracefully.

Double checking Singleton initialization

Let's say we want to create a lazy =initialization Singleton without the performance penalty of redundant synchronization:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // Инициализация ресурсов
    }

    public static Singleton getInstance() {
        if (instance == null) { // Первая проверка (без синхронизации)
            synchronized (Singleton.class) {
                if (instance == null) { // Вторая проверка (с синхронизацией)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Without volatile it is possible for instructions to be reordered, causing another thread to see a partially constructed object instance.

volatile ensures that the entry in instance occurs only after the object has been fully initialized, and all subsequent reads will see the current state.

Configuration caching with instant refresh

In applications where configuration parameters can be updated dynamically, it is necessary to ensure that all worker threads immediately receive the current settings without the need to restart:

public class ConfigurationManager {
    private volatile Config currentConfig;

    public ConfigurationManager() {
        // Инициализируем конфигурацию по умолчанию
        currentConfig = loadDefaultConfig();
    }

    public Config getConfig() {
        return currentConfig;
    }

    public void updateConfig(Config newConfig) {
        currentConfig = newConfig;
    }

    private Config loadDefaultConfig() {
        // Загрузка конфигурации по умолчанию
        return new Config(...);
    }
}

public class Worker implements Runnable {
    private ConfigurationManager configManager;

    public Worker(ConfigurationManager configManager) {
        this.configManager = configManager;
    }

    @Override
    public void run() {
        while (true) {
            Config config = configManager.getConfig();
            // Используем актуальную конфигурацию
            process(config);
        }
    }

    private void process(Config config) {
        // Обработка с использованием текущей конфигурации
    }
}

Variable currentConfig can be updated by one thread (for example, when an administrator changes settings) and should be immediately visible to other threads performing tasks.

When updating the configuration using the method updateConfig new meaning currentConfig becomes immediately available to all workflows thanks to volatile.

Use for one-time events

Often volatile used to signal the occurrence of a certain event, after which it is necessary to change the application's behavior:

public class EventNotifier {
    private volatile boolean eventOccurred = false;

    public void waitForEvent() {
        while (!eventOccurred) {
            // Ожидание события
        }
        // Реакция на событие
    }

    public void triggerEvent() {
        eventOccurred = true;
    }
}

Use volatile for simple state flags and publishing immutable objects. Avoid complex operations with volatile variables without additional synchronization.

Let's move on to the next topic of the article – Memory Fences

Memory Fences

Memory Fence — is a mechanism that prevents the compiler or processor from reordering memory read and write operations. This means that memory-related operations will be performed in the expected order.

Types of Memory Fences:

  1. Load Barrier: Ensures that all read operations before the barrier are completed before any read operations after the barrier begin.

  2. StoreStore Barrier: Ensures that all write operations before the barrier are completed before any write operations after the barrier begin.

  3. LoadStore Barrier: Ensures that all read operations before the barrier are completed before any write operations after the barrier begin.

  4. StoreLoad Barrier: Ensures that all write operations before the barrier are completed before any read operations after the barrier begin. This is the most “strong” barrier.

Java has several facilities for managing memory barriers:

  1. Keyword volatile

  2. Classes from the package java.util.concurrent.atomic

  3. Class Unsafe (with caution)

  4. VarHandle

Let's look at them in more detail.

Classes from the java.util.concurrent.atomic package

Plastic bag java.util.concurrent.atomic contains classes that provide atomic operations on variables of different types:

  • AtomicInteger

  • AtomicLong

  • AtomicReference

  • AtomicBoolean

  • And others

These classes use low-level synchronization primitives.

Example of use AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet(); // Атомарное увеличение значения на 1
    }

    public int getValue() {
        return counter.get(); // Атомарное получение текущего значения
    }
}

Unsafe Methods

Class sun.misc.Unsafe Provides low-level operations on memory, including methods for setting memory barriers.

Class Unsafe is an internal API and is not intended for general use. Using it may result in non-portable code and potential bugs.

In Java 9 and above, access to Unsafe limited by the modular system.

However, for educational purposes, let's look at an example:

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeMemoryFenceExample {
    private static final Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new RuntimeException("Не удалось получить доступ к Unsafe", e);
        }
    }

    public void storeLoadFence() {
        unsafe.storeFence(); // Применение StoreStore Barriers
        // Ваш код
        unsafe.loadFence(); // Применение LoadLoad Barriers
    }
}

Use Unsafe only if you fully understand the risks and there are no alternatives.

Recommended to use VarHandle or high-level classes from java.util.concurrent.

Examples of using Memory Fences for thread synchronization

Implementing a non-blocking counter with AtomicLong:

import java.util.concurrent.atomic.AtomicLong;

public class NonBlockingCounter {
    private AtomicLong counter = new AtomicLong(0);

    public void increment() {
        counter.getAndIncrement(); // Атомарное увеличение значения
    }

    public long getValue() {
        return counter.get();
    }
}

AtomicReference to implement a non-blocking stack:

import java.util.concurrent.atomic.AtomicReference;

public class LockFreeStack<T> {
    private AtomicReference<Node<T>> head = new AtomicReference<>(null);

    private static class Node<T> {
        final T value;
        final Node<T> next;

        Node(T value, Node<T> next) {
            this.value = value;
            this.next = next;
        }
    }

    public void push(T value) {
        Node<T> newHead;
        Node<T> oldHead;

        do {
            oldHead = head.get();
            newHead = new Node<>(value, oldHead);
        } while (!head.compareAndSet(oldHead, newHead));
    }

    public T pop() {
        Node<T> oldHead;
        Node<T> newHead;

        do {
            oldHead = head.get();
            if (oldHead == null) {
                return null; // Стек пуст
            }
            newHead = oldHead.next;
        } while (!head.compareAndSet(oldHead, newHead));

        return oldHead.value;
    }
}

VarHandle – A Modern Approach to Memory Fences

Starting with Java 9, was introduced VarHandle as a more powerful alternative Atomic classes and Unsafe.

Features VarHandle:

  • Allows you to perform operations with different levels of memory guarantees.

  • More flexible and secure than Unsafe.

Example of use VarHandle:

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandleExample {
    private int value = 0;
    private static final VarHandle VALUE_HANDLE;

    static {
        try {
            VALUE_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleExample.class, "value", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void setValue(int newValue) {
        VALUE_HANDLE.setRelease(this, newValue); // Обеспечивает StoreStore Barrier
    }

    public int getValue() {
        return (int) VALUE_HANDLE.getAcquire(this); // Обеспечивает LoadLoad Barrier
    }
}

Implementing a simple counter with VarHandle:

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandleCounter {
    private int count = 0;
    private static final VarHandle COUNT_HANDLE;

    static {
        try {
            COUNT_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleCounter.class, "count", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void increment() {
        int prevValue;
        do {
            prevValue = (int) COUNT_HANDLE.getVolatile(this);
        } while (!COUNT_HANDLE.compareAndSet(this, prevValue, prevValue + 1));
    }

    public int getCount() {
        return (int) COUNT_HANDLE.getVolatile(this);
    }
}

Conclusion

Correct application volatile and Memory Fences enables you to create efficient and reliable multithreaded applications.

This evening we will have an open lesson on defining variable scopes in Java. In this lesson, we will look at how variable scopes affect program behavior and how to use them correctly using practical examples. If the topic is relevant – sign up using the link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *