How to

#Java #Multithreading #interview #Concurrency

Interview Questions

Multithreading

Tell us about the Java memory model?

The Java Memory Model (JMM) describes the behavior of threads in the Java runtime environment. It is part of the semantics of the Java language, a set of rules that define the execution of multithreaded programs and the rules by which threads can interact with each other through main memory.

Formally, the memory model defines a set of actions for inter-thread interaction (these actions include, in particular, reading and writing a variable, acquiring and releasing a monitor, reading and writing to a volatile variable, starting a new thread), and it also defines the relationship between these actions - the happens-before relationship - an abstraction that indicates if operation X is happens-before operation Y, then all code following operation Y performed in one thread will see all changes made by another thread before operation X.

Several fundamental rules govern the happens-before relationship:

Several main areas relevant to the memory model can be identified:

Visibility. One thread may temporarily hold the value of some fields not in main memory, but in registers or the local cache of the processor, so another thread running on another processor, reading from main memory, may not see the latest changes to the field. Conversely, if a thread has been working with registers and local caches for a period, reading data from there, it may not immediately see changes made by another thread in main memory.

The following Java keywords relate to visibility: synchronized, volatile, final.

From the Java perspective, all variables (except for local variables declared inside a method) are stored in main memory, which is accessible to all threads. Moreover, each thread has a local working memory where it stores copies of the variables it works with, and during program execution, the thread works only with these copies. It should be noted that this description does not impose a requirement for implementation, it is merely a model that explains program behavior; thus, local memory does not necessarily have to be cache memory; it can be processor registers, or threads may not even have local memory.

When entering a synchronized method or block, a thread updates the contents of local memory, and when exiting a synchronized method or block, the thread writes changes made in local memory back to main memory. This behavior of synchronized methods and blocks follows from the rules for the happens-before relationship: since all operations on memory occur before the monitor is released and the monitor’s release occurs before the monitor is acquired, all memory operations made by a thread before leaving the synchronized block must be visible to any thread that enters the synchronized block for the same monitor. It is very important that this rule works only if threads synchronize using the same monitor!

Regarding volatile variables, writing to such variables occurs in main memory, bypassing local memory, and reading a volatile variable is also performed from main memory, meaning the variable value cannot be stored in registers or local memory of the thread, and the read operation of this variable will reliably return the last value written to it.

Additionally, the memory model defines extra semantics for the final keyword regarding visibility: after an object has been correctly constructed, any thread can see the values of its final fields without further synchronization. “Correctly constructed” means that the reference to the object being instantiated must not be accessed until the constructor has completed. Such semantics for the final keyword enable the creation of immutable objects containing only final fields; such objects can be freely passed between threads without synchronization guarantees being required during the transfer.

One issue related to final fields is that the implementation allows changing the values of such fields after the object has been created (this can happen, for example, using reflection). If the value of a final field is a constant known at compile time, changes to such a field may have no effect, as accesses to this variable may have been replaced by the compiler with the constant. The specification also allows for other optimizations related to final fields; for example, operations reading a final variable may be reordered with operations that could potentially modify such a variable. Therefore, it is recommended to change final fields of an object only within the constructor; otherwise, the behavior is unspecified.

Reordering. To increase performance, the processor/compiler may rearrange some instructions/operations. Or rather, from the perspective of a thread observing the execution of operations in another thread, the operations can be executed out of the order they appear in the source code. The same effect can be observed when one thread places results of the first operation in a register or local cache, while the result of the second operation goes directly into main memory. Then, when the second thread accesses main memory, it may see the result of the second operation before seeing the result of the first operation, when all registers or caches synchronize with main memory. Another reason for reordering could be that the processor might decide to change the order of execution of operations if, for example, it believes that such an ordering would execute faster.

The issue of reordering is also governed by a set of rules for the happens-before relationship, and these rules have a consequence concerning the order of operations that is relevant in practice: the read and write operations of volatile variables cannot be reordered with the read and write operations of other volatile and non-volatile variables. This consequence allows a volatile variable to be used as a flag signaling the completion of some action. In other instances, the rules concerning the ordering of execution of operations guarantee the ordering of operations for specific cases (such as acquiring and releasing a monitor), while leaving the compiler and processor free to optimize in all other cases.

Back to the Table of Contents

What is “thread safety”?

Thread safety is a property of an object or code that guarantees that during execution or use by multiple threads, the code will behave as expected. For example, a thread-safe counter will not miss any counts, even if the same instance of this counter is used by multiple threads.

Back to the Table of Contents

What is the difference between “concurrency” and “parallelism”?

Concurrency is a way to simultaneously solve multiple tasks.

Signs:

Parallelism is a way of executing different parts of one task.

Signs:

Back to the Table of Contents

What is “cooperative multitasking”? What type of multitasking does Java use? What is the reasoning behind this choice?

Cooperative multitasking is a way of dividing CPU time among threads, where each thread must voluntarily yield control to the next.

The advantages of this approach are simplicity of implementation and lower overhead for context switching.

The disadvantages are that if one thread hangs or behaves incorrectly, the entire system hangs, and other threads may never gain control.

Java uses preemptive multitasking, where the operating system makes the decision to switch between threads of a process.

In contrast to cooperative multitasking, control is passed to the operating system regardless of the state of running applications, which means that individual hanging threads of a process generally do not “freeze” the entire system. Regular switching between tasks also improves application responsiveness and enhances the promptness of freeing resources that are no longer in use.

In implementation, preemptive multitasking differs from cooperative multitasking, particularly in that it requires handling system interrupts from a hardware timer.

Back to the Table of Contents

What are ordering, as-if-serial semantics, sequential consistency, visibility, atomicity, happens-before, mutual exclusion, safe publication?

Ordering is a mechanism that defines when one thread can see the out-of-order execution of instructions from another thread. The CPU can reorder processor instructions and execute them in arbitrary order as long as there are no observable differences for the thread inside. The guarantee provided by this mechanism is called as-if-serial semantics.

Sequential consistency is the same as as-if-serial semantics, guaranteeing that within a single thread, the side effects of all operations will be such as if all operations were executed sequentially.

Visibility determines when actions in one thread become visible to another thread.

Happens-before is a logical constraint on the order of execution of the program’s instructions. If it is specified that writing to a variable and subsequently reading it are related by this dependency, it does not matter how instructions are reordered during execution; at the time of reading, all writes associated with the process will already be committed and visible.

Atomicity refers to the atomicity of operations. An atomic operation appears as a single indivisible command to the processor, which can either be fully executed or not executed at all.

Mutual exclusion (mutual exclusion lock, semaphore with one state) is the mechanism that guarantees a thread exclusive access to a resource. This is used to prevent concurrent access to a shared resource. At any given time, only one thread can own such a resource. A simple example: synchronized(obj) { … }.

Safe publication? - the act of making objects visible to other threads from the current one while respecting the constraints of visibility. Ways of such publication in Java:

Back to the Table of Contents

What is the difference between a process and a thread?

A process - is an instance of a program during execution, an independent object that has been allocated system resources (such as CPU time and memory). Each process runs in a separate address space: one process cannot access the variables and data structures of another. If a process wants access to foreign resources, inter-process communication must be used. This could be pipes, files, communication channels between computers, and much more.

For each process, the OS creates what is referred to as “virtual address space,” to which the process has direct access. This space belongs to the process, contains only its data, and is entirely at its disposal. The operating system is responsible for how the virtual space of a process is mapped to physical memory.

A thread (thread) - is a specific way of executing a process, defining the sequence in which code is executed within the process. Threads are always created in the context of some process, and their entire lifecycle occurs only within its boundaries. Threads can execute the same code and manipulate the same data, and they share kernel object descriptors since the descriptor table is created not per thread, but per process. Since threads consume considerably fewer resources than processes, it is often better to create additional threads during the execution of a task and avoid the creation of new processes.

Back to the Table of Contents

What are “green threads,” and does Java have them?

Green threads (lightweight threads) are threads emulated by the virtual machine or execution environment. Creating a green thread does not imply the creation of an actual OS thread.

The Java virtual machine takes care of switching between different green threads, whereas the machine operates as a single OS thread. This has several advantages. OS threads are relatively expensive on most POSIX systems. Furthermore, switching between native threads is considerably slower than switching between green threads.

This all means that in certain situations, green threads are much more beneficial compared to native threads. The system may support a significantly higher number of green threads than OS threads. For instance, it is much more practical to launch a new green thread for a new HTTP connection to a web server instead of creating a new native thread.

However, there are drawbacks. The biggest one is that you cannot execute two threads simultaneously. Since there is only one native thread, it is the only one called by the OS scheduler. Even if you have multiple processors and several green threads, only one processor can invoke a green thread. This is because, from the OS job scheduler’s point of view, it all appears as a single thread.

Starting from version 1.2, Java supports native threads, and since then, they have been used by default.

Back to the Table of Contents

How can a thread be created?

Back to the Table of Contents

What are the differences between Thread and Runnable?

Thread is a class, a kind of wrapper around a physical thread.

Runnable is an interface that represents an abstraction of an executing task.

In addition to helping resolve the multiple inheritance problem, a significant advantage of using Runnable is that it allows you to logically separate the task execution logic from the direct thread management.

Back to the Table of Contents

What is the difference between the start() and run() methods?

Although start() invokes the run() method within itself, this is not the same as simply calling run(). If run() is called as a regular method, it executes in the same thread, and no new thread is started, as happens when the start() method is called.

Back to the Table of Contents

How can you forcibly start a thread?

There is no way to forcibly start a thread in Java. This is managed by the JVM, and Java does not provide any API to control this process.

Back to the Table of Contents

What is a “monitor” in Java?

A monitor, or mutex - is a means of controlling access to a resource. A monitor can have at most one owner at any given moment. Thus, if someone is using the resource and has captured the monitor for exclusive access, then another thread wanting to use the same resource must wait for the monitor to be released, acquire it, and only then start using the resource.

It’s convenient to represent a monitor as the ID of the object that has captured it. If this ID equals 0 – the resource is free. If not 0 – the resource is occupied. You can queue up and wait for its release.

In Java, every object instance has a monitor that is controlled directly by the virtual machine. It is used as follows: any non-static synchronized method, when called, first attempts to capture the monitor of the object it is called on (to which it can reference as this). If successful, the method executes. If not, the thread stops and waits until the monitor is released.

Back to the Table of Contents

Define the concept of “synchronization.”

Synchronization is the process that allows threads to execute concurrently.

In Java, every object has one lock, meaning that only one thread can access critical code within the object at a time. This synchronization helps prevent the corruption of the object’s state. If a thread obtains the lock, no other thread can enter the synchronized code until the lock is released. When the thread holding the lock exits the synchronized code, the lock is released. Now another thread can acquire the object’s lock and execute the synchronized code. If a thread attempts to acquire the object’s lock while another thread owns it, the thread enters a Blocked state until the lock is released.

Back to the Table of Contents

What synchronization methods exist in Java?

Back to the Table of Contents

What states can a thread be in?

Threads can be in one of the following states:

Back to the Table of Contents

Can new instances of a class be created while a static synchronized method is executing?

Yes, new instances of the class can be created since static fields do not belong to the class instances.

Back to the Table of Contents

Why would a private mutex be necessary?

An object for synchronization is made private so that external code cannot synchronize on it and inadvertently cause a deadlock.

Back to the Table of Contents

How do wait() and notify()/notifyAll() methods work?

These methods are defined in the Object class and are meant for inter-thread interaction during inter-thread synchronization.

When the wait() method is called, the thread releases the lock on the object and transitions from the Running state to the Waiting state. The notify() method signals one of the waiting threads to transition back to the Runnable state. However, it is impossible to determine which of the waiting threads will become runnable. The notifyAll() method causes all waiting threads for the object to return to the Runnable state. If no thread is waiting on the wait() method, calling notify() or notifyAll() does nothing.

A thread can only call the wait() or notify() methods for a specific object if it currently holds the lock on that object. wait(), notify(), and notifyAll() must only be called from synchronized code.

Back to the Table of Contents

What is the difference between notify() and notifyAll()?

The point is that there may be multiple threads waiting on the wait() method of a single monitor. When notify() is called, only one of them exits wait() and attempts to acquire the monitor, thus continuing work from the next operation after the wait() call. Which one will exit is unknown beforehand. However, when notifyAll() is called, all threads waiting on wait() exit wait(), and all attempt to acquire the monitor. It is clear that at any moment in time, the monitor can only be held by one thread, while the others wait in line. The order of the queue is determined by the Java thread scheduler.

Back to the Table of Contents

Why are the wait() and notify() methods called only in a synchronized block?

The monitor must be captured explicitly (via a synchronized block) because the wait() and notify() methods are not synchronized.

Back to the Table of Contents

What is the difference between the wait() method with a parameter and without it?

wait()

Back to the Table of Contents

What are the differences between the Thread.sleep() and Thread.yield() methods?

The yield() method causes the current thread to move from the running state to the runnable state, giving other threads the opportunity to be activated. However, the next thread chosen for execution may not be another.

The sleep() method causes the current thread to sleep for a specified amount of time, changing its state from running to waiting.

Back to the Table of Contents

How does the Thread.join() method work?

When a thread calls join() on another thread, the currently executing thread will wait until the other thread it is joining is finished:

void join()
void join(long millis)
void join(long millis, int nanos)

Back to the Table of Contents

What is a deadlock?

Deadlock is a phenomenon in which all threads are in a waiting state. This occurs when the following conditions are met:

  1. mutual exclusion: at least one resource is held in non-shareable mode, meaning only one thread can use the resource at any given time.
  2. hold and wait: a thread holds at least one resource and is waiting for additional resources that are held by other threads.
  3. no preemption: the operating system does not forcibly release resources; if they are held, they must be returned by the holding threads immediately.
  4. circular wait: a thread is waiting for a resource held by another thread, which in turn is waiting for a resource locked by the first thread.

The simplest way to avoid deadlock is to prevent circular wait. This can be achieved by obtaining monitors for shared resources in a predetermined order and releasing them in reverse order.

Back to the Table of Contents

What is a livelock?

A livelock is a type of deadlock where several threads are performing useless work in an endless loop while trying to acquire any resources. Their states continually change in relation to each other. No actual error occurs, but the system’s efficiency drops to 0. This often arises from attempts to prevent deadlock.

A real example of livelock is when two people meet in a narrow hallway and each steps aside to be polite, leading them to move back and forth infinitely without making any progress in the direction they want to go.

Back to the Table of Contents

How can you check if a thread holds the monitor of a certain resource?

The Thread.holdsLock(lock) method returns true when the current thread holds the monitor on a given object.

Back to the Table of Contents

On which object does synchronization occur when calling a static synchronized method?

A synchronized static method does not have access to this, but has access to the Class object; there is only one instance of it, and it serves as the monitor for synchronizing static methods. Thus, the following construction:

public class SomeClass {

    public static synchronized void someMethod() {
        // code
    }
}

is equivalent to this:

public class SomeClass {

    public static void someMethod(){
        synchronized(SomeClass.class){
            // code
        }
    }
}

Back to the Table of Contents

What is the purpose of the keywords volatile, synchronized, transient, native?

volatile - this modifier forces threads to disable access optimization and use the single instance of the variable. If the variable is a primitive type, this suffices to ensure thread safety. However, if the variable is a reference to an object, the synchronization only concerns the value of that reference. The actual data contained within the object will not be synchronized!

synchronized - this reserved word allows achieving synchronization in methods or blocks of code marked with it.

The keywords transient and native have nothing to do with multithreading; the first is used to indicate fields of a class that should not be serialized, while the second signals that the method is implemented in platform-dependent code.

Back to the Table of Contents

What are the differences between volatile and Atomic variables?

volatile forces the use of a single instance of the variable but does not guarantee atomicity. For example, the operation count++ will not become atomic simply because count is declared volatile. On the other hand, the class AtomicInteger provides an atomic method to perform such complex operations atomically, for instance, getAndIncrement() is an atomic replacement for the increment operator, and it can be used to atomically add one to the current value. Similarly, atomic versions are constructed for other data types.

Back to the Table of Contents

What are the differences between java.util.concurrent.Atomic*.compareAndSwap() and java.util.concurrent.Atomic*.weakCompareAndSwap()?

Back to the Table of Contents

What does “thread priority” mean?

Thread priorities are used by the thread scheduler to make decisions about when each thread is allowed to run. Theoretically, high-priority threads receive more CPU time than low-priority threads. In practice, the share of CPU time allotted to a thread often depends on various factors beyond its priority.

To set a thread’s priority, the final void setPriority(int level) method of the Thread class is used. The level value ranges from Thread.MIN_PRIORITY = 1 to Thread.MAX_PRIORITY = 10. The default priority is Thread.NORM_PRIORITY = 5.

The current thread priority can be obtained by calling the method: final int getPriority() on an instance of the Thread class.

Back to the Table of Contents

What are “daemon threads”?

Daemon threads run in the background with the application but are not integral to its operation. If a process can operate in the background to serve the main execution threads and its activity serves solely to support the main application threads, this process can be started as a daemon thread using the setDaemon(boolean value) method called on the thread before it starts. The method boolean isDaemon() allows checking whether a specified thread is a daemon or not. The fundamental property of daemon threads is that the main application thread can terminate the daemon thread when the main method’s code concludes, regardless of whether the daemon thread is still running.

Back to the Table of Contents

Can the main thread of a program be a daemon?

No. Daemon threads are designed to describe background processes that serve only the main execution threads and cannot exist without them.

Back to the Table of Contents

What does it mean to “sleep” a thread?

It means to suspend it for a specified period of time by calling the static method Thread.sleep(), passing the required amount of time in milliseconds. Before this time elapses, the thread may be awoken from its waiting state calls to interrupt() that throw InterruptedException.

Back to the Table of Contents

What are the differences between the Runnable and Callable interfaces?

Back to the Table of Contents

What is FutureTask?

FutureTask is an cancellable asynchronous computation within a concurrent Java application. This class provides a basic implementation of Future, with methods to start and stop the computation, request the state of the computation, and extract results. The result can be obtained only when the computation has finished; the retrieval method will block if the computation has not yet completed. FutureTask objects can be used to wrap Callable and Runnable objects. Since FutureTask implements Runnable, it can be passed to an Executor for execution.

Back to the Table of Contents

What are the differences between CyclicBarrier and CountDownLatch?

CountDownLatch (countdown latch) allows any number of threads in a code block to wait until a specific number of operations being executed in other threads has finished before they are released to continue their activity. When creating a CountDownLatch(int count) object, the number of operations that must complete for the latch to release the blocked threads is provided as an argument.

A life example of CountDownLatch could be collecting a tour group: the tour does not start until a certain number of people have gathered.

CyclicBarrier implements the “Barrier” synchronization pattern. A cyclic barrier is a synchronization point where a specified number of parallel threads meet and block. Once all threads arrive, an optional action is performed (or not, if the barrier was initialized without it), and after it is performed, the barrier breaks, releasing the waiting threads. The number of parties that must “meet” is provided as an argument to the constructors of CyclicBarrier(int parties) and CyclicBarrier(int parties, Runnable barrierAction), along with an optional action that must occur when the parties meet, but before they are released.

CyclicBarrier is an alternative to the join() method, which only “collects” threads after they have completed execution.

CyclicBarrier is similar to CountDownLatch, but the main difference between them is that a “latch” can only be used once—once its count reaches zero, while a “barrier” can be reused multiple times, even after it “breaks.”

Back to the Table of Contents

What is a race condition?

A race condition is a design error in a multithreaded system or application where the behavior of the code depends directly on the order in which threads are executed. A race condition occurs when a thread that should execute first loses the race and another thread executes first: this changes the behavior of the code, leading to non-deterministic errors.

Back to the Table of Contents

Is there a way to solve the race condition problem?

Common solutions include:

There are no obvious ways to detect and fix race conditions. The best way to prevent races is correct design of a multi-threaded system.

Back to the Table of Contents

How can you stop a thread?

Currently, Java follows a notification-based order for stopping threads (although JDK 1.0 had several methods for controlling thread execution, such as stop(), suspend(), and resume() - in later versions of JDK, all of these were marked as deprecated due to potential risks of deadlocks).

To stop a thread properly, you can use the interrupt() method of the Thread class. This method sets an internal interrupt status flag. The state of this flag can later be checked with methods like isInterrupted() or Thread.interrupted() (for the current thread). The interrupt() method can also wake a thread out of a waiting or sleeping state; i.e., if methods like sleep() or wait() were called on the thread, the current state would be interrupted, and an InterruptedException would be thrown. In this case, the flag will not be set.

The action scheme would be as follows:

A potential issue with this approach is blocking on thread I/O. If a thread is blocked on reading data, calling interrupt() in that state will not wake it. Solutions can vary depending on the type of data source. If reading from a file, a prolonged block is very unlikely, and one could simply wait for the exit from the read() method. If, however, reading is somehow related to networking, it’s advisable to use non-blocking I/O from Java NIO.

Another option for implementing the stop method (as well as the suspend one) is to create your own version of interrupt(). That is, declare flags in the thread class for stopping and/or suspending and set them using predefined methods from outside. The action process remains the same: check the flag’s setting and decide based on any changes. However, this approach has its disadvantages. First, it doesn’t “revive” threads in the waiting state. Secondly, setting a flag in one thread does not mean that another thread will immediately see it. The virtual machine uses a thread’s data cache, meaning the update of the variable in the second thread may happen after an indefinite time span (although permissible is to declare the flag variable as volatile).

Back to the Table of Contents

The forced stopping (suspension) of a thread, stop() interrupts the thread at an indeterminate execution point, making it entirely unclear what to do with the resources it owns. The thread may open a network connection—what then should be done with the data that has not yet been read? What guarantees are there that after the thread is restarted (if suspended) it can continue reading them? If the thread locked a shared resource, how to release that lock without risking inconsistency in the system? The same applies to database connections—if a thread is stopped in the middle of a transaction, who will close it? Who and how will release resources?

Back to the Table of Contents

What happens when an exception is thrown in a thread?

Back to the Table of Contents

What is the difference between interrupted() and isInterrupted()?

The interruption mechanism in Java is implemented using an internal flag called the interrupt status. Interrupting a thread by calling Thread.interrupt() sets this flag. The methods Thread.interrupted() and isInterrupted() allow checking whether a thread is interrupted.

When an interrupted thread checks the interrupt status by calling the static method Thread.interrupted(), the interrupt status is reset.

The non-static method isInterrupted() is used by one thread to check the interrupt status of another thread without altering the interrupt flag.

Back to the Table of Contents

What is a thread pool?

Creating a thread is a time- and resource-intensive operation. The number of threads that can be launched within a single process is also limited. To avoid these issues and, in general, manage multiple threads more efficiently, Java has implemented a mechanism for thread pools, created when the application starts, from which threads for processing requests are then taken and reused. Thus, there’s no loss of threads, the application can be balanced in terms of the number of threads and their creation frequency.

Starting from Java 1.5, the Java API provides the Executor framework, which allows creating various types of thread pools:

Executors methods for creating pools:

Back to the Table of Contents

What should the size of a thread pool be?

When configuring the size of a thread pool, it is essential to avoid two mistakes: having too few threads (the queue for execution will grow, consuming a lot of memory) or too many threads (slowing down the entire system due to frequent context switches).

The optimal size of a thread pool depends on the number of available processors and the nature of the tasks in the working queue. On an N-processor system for a workload that will be performing purely CPU-bound tasks, maximum CPU utilization can be achieved with a thread pool containing either N or N+1 threads. For tasks that might be waiting for I/O (input-output) – for example, tasks reading an HTTP request from a socket – the size of the pool may need to be greater than the number of available processors because not all threads will be working all the time. With profiling, one can assess the relationship of wait time (WT) to processing time (ST) for a typical request. If we label this ratio WT/ST, then on an N-processor system, approximately N*(1 + WT/ST) threads will be needed for full processor utilization.

CPU usage is not the only factor to consider when configuring the size of a thread pool. As the thread pool grows, one might encounter constraints from the scheduler, available memory, or other system resources such as the number of sockets, open file descriptors, or database connection channels.

Back to the Table of Contents

What happens if the thread pool queue is already full, but a new task is submitted?

If the thread pool queue is full, the submitted task will be “rejected.” For example, the submit() method of the ThreadPoolExecutor throws a RejectedExecutionException, after which the RejectedExecutionHandler is invoked.

Back to the Table of Contents

What is the difference between the submit() and execute() methods of a thread pool?

Both methods serve to submit tasks to the thread pool, but there is a small difference between them.

execute(Runnable command) is defined in the Executor interface and runs the submitted task without returning anything.

submit() is an overloaded method defined in the ExecutorService interface. It can accept Runnable and Callable tasks and returns a Future object that can be used to control and manage the execution process, obtaining its result.

Back to the Table of Contents

What are the differences between a stack and a heap in terms of multithreading?

Stack - is a memory segment closely associated with threads. Each thread has its own stack, which stores local variables, method parameters, and the call stack. A variable stored in the stack of one thread is not visible to other threads.

Heap - is a shared memory segment accessible to all threads. Objects, regardless of their locality, are created in the heap. To improve performance, a thread usually caches values from the heap in its stack; in this case, to indicate to the thread that a variable should be read from the heap, the volatile keyword is used.

Back to the Table of Contents

How can you share data between two threads?

Data can be shared between threads using a shared object or concurrent data structures, such as BlockingQueue. The Exchanger class synchronizer is designed for data exchange between threads. It is parameterized with the type of data that must be exchanged between threads. Data exchange occurs through the single method of this class, exchange(). To work, an instance of the Exchanger must be passed to the constructors of the threads, and access it in the run() method. This method blocks the thread until another thread passes its data into the Exchanger.

Back to the Table of Contents

Which JVM startup option is used to control the size of a thread’s stack?

-Xss

Back to the Table of Contents

How do you get a thread dump?

Java execution environments based on HotSpot only generate a dump in HPROF format. Developers have several interactive methods for generating dumps and one event-based method for generating dumps.

Interactive methods:

Event-based method:

Back to the Table of Contents

What is a ThreadLocal variable?

ThreadLocal is a class that allows having different values for a given variable for each thread.

Each thread, i.e., an instance of the Thread class, has an associated table of ThreadLocal variables. The keys in the table are references to objects of the ThreadLocal class, and the values are references to the objects “captured” by the ThreadLocal variables, implying that ThreadLocal variables differ from ordinary variables in that each thread has its own, individually initialized instance of the variable. Access to the value can be obtained through the get() or set() methods.

For example, if we declare a ThreadLocal variable: ThreadLocal<Object> locals = new ThreadLocal<Object>();. Then, in a thread, we call locals.set(myObject), and the key in the table will be a reference to the locals object while the value will be a reference to the myObject object. At this point, another thread has the opportunity to “put” a different value inside locals.

It’s important to note that ThreadLocal isolates not just the references to objects but also the objects themselves. If the isolated references within threads point to the same object, collisions may occur.

It is also crucial to emphasize that because ThreadLocal variables are isolated within threads, the initialization of such a variable must occur in the same thread where it will be used. It would be an error to initialize such a variable (invoke the set() method) in the main application thread since, in this case, the value passed in the set() method will be “captured” for the main thread, and when calling get() in the target thread, it will return null.

Back to the Table of Contents

What are the differences between synchronized and ReentrantLock?

Starting from Java 5, the Lock interface was introduced, providing more effective and finer control over resource locking. ReentrantLock is a popular implementation of Lock, which provides the same basic behavior and semantics as synchronized, but with extended capabilities such as lock polling, the ability to wait for a lock for a specified duration, and interruptible lock waiting. Moreover, it offers much higher performance in situations of high contention.

What is meant by reentrant locking? Simply put, there is a count associated with a lock, and if the thread that holds the lock acquires it again, the count increases, meaning that the lock is to be released twice for actual unlock. This is analogous to the semantics of synchronized; if a thread enters a synchronized block protected by a monitor it already owns, it will be allowed to continue functioning, and the lock will not be released when the thread exits the second (or subsequent) synchronized block; it will only be released when it exits the first synchronized block in which it entered under the protection of the monitor.

Lock lock = new ReentrantLock();

lock.lock();
try {
  // update object state
}
finally {
  lock.unlock();
}

In summary, it can be said that when contention for the lock is either absent or very low, synchronized may be faster. If there is significant contention for access to the resource, ReentrantLock will likely provide some advantage.

Back to the Table of Contents

What is ReadWriteLock?

ReadWriteLock is an interface extending the basic Lock interface. It is used to improve performance in a multithreaded process and operates with a pair of related locks (one for read operations, and one for write operations). The read lock can be held simultaneously by multiple reading threads until a writing lock is required. The write lock is exclusive.

The class ReentrantReadWriteLock implements the ReadWriteLock interface, which supports up to 65535 write locks and the same number of read locks.

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock rLock = rwLock.readLock();
Lock wLock = rwLock.writeLock();

wLock.lock();
try {
    // exclusive write
} finally {
    wLock.unlock();
}

rLock.lock();
try {
    // shared reading
} finally {
    rLock.unlock();
}

Back to the Table of Contents

What is a blocking method?

A blocking method is one that blocks until the task is done; for example, the accept() method in ServerSocket blocks while waiting for a client connection. Here, blocking means that control will not return to the calling method until the task completes. There are also asynchronous or non-blocking methods that may finish before the task is completed.

Back to the Table of Contents

What is the Fork/Join framework?

The Fork/Join framework, introduced in JDK 7, is a set of classes and interfaces that allow leveraging the advantages of modern multi-processor architecture. It is designed for executing tasks that can be recursively divided into smaller sub-tasks, which can then be solved in parallel.

The resolution of all subtasks (including the breaking down of tasks) occurs in parallel.

For some tasks, the join phase is not required. For example, in parallel QuickSort—the array is recursively divided into smaller and smaller ranges until it reduces to a trivial case of one element. Although on some level a join will still be necessary since there still remains the need to wait until all subtasks have completed execution.

Another wonderful advantage of this framework is that it uses a work-stealing algorithm: threads that have completed their own subtasks can “steal” subtasks from other threads that are still busy.

Back to the Table of Contents

What is a Semaphore?

A semaphore is a new type of synchronizer: a counter semaphore implementing the semaphore synchronization pattern. Access is managed via a counter: the initial counter value is given in the constructor when creating the synchronizer; when a thread enters the designated code block, its counter value decreases by one, and when the thread exits, the counter increases. If the counter value is zero, the current thread blocks until someone exits the protected block. A semaphore is used for protecting expensive resources that are available in limited numbers, such as database connections in a pool.

Back to the Table of Contents

What is a double-checked locking Singleton?

The double-checked locking Singleton is one of the ways to create a thread-safe singleton class. This method attempts to optimize performance by locking only when the singleton instance is created for the first time.

class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;

    static DoubleCheckedLockingSingleton getInstance() {
        DoubleCheckedLockingSingleton current = instance;
        if (current == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                current = instance;

                if (current == null) {
                    instance = current = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return current;
    }
}

It is necessary to mention that the requirement for volatile is mandatory. The Double Checked Lock’s issue lies in Java’s memory model, specifically in the order of object creation, when there can be situations in which another thread may obtain and begin using (based on the condition that the pointer is non-null) an incompletely constructed object. Although this problem has been partially addressed in JDK 1.5, the recommendation to use volatile for Double Checked Lock remains in effect.

Back to the Table of Contents

How do you create a thread-safe Singleton?

public class Singleton {
	public static final Singleton INSTANCE = new Singleton();
}
public enum Singleton {
	INSTANCE;
}
public class Singleton {
	private static Singleton instance;

	public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
public class Singleton {
        private static volatile Singleton instance;

        public static Singleton getInstance() {
		SuperClass localInstance = instance;
		if (localInstance == null) {
			synchronized (Singleton.class) {
				localInstance = instance;
				if (localInstance == null) {
					instance = localInstance = new Singleton();
				}
			}
		}
		return localInstance;
	}
}
public class Singleton {

	public static class SingletonHolder {
		public static final Singleton HOLDER_INSTANCE = new Singleton();
	}

	public static Singleton getInstance() {
		return SingletonHolder.HOLDER_INSTANCE;
	}
}

Back to the Table of Contents

What are the benefits of immutable objects?

Immutability helps simplify writing multithreaded code. An immutable object can be used without any synchronization. Unfortunately, Java does not have an @Immutable annotation to make an object immutable; therefore, developers must create a class with the required characteristics themselves. This involves following some general principles: initializing all fields only in the constructor, avoiding any methods like setX() that modify class fields, ensuring that there are no leaks of references, organizing separate storage for mutable objects, and so forth.

Back to the Table of Contents

What is busy spin?

Busy spin is a technique programmers use to make a thread wait under a certain condition. Unlike traditional methods like wait(), sleep() or yield(), which yield the processor’s time, this method will instead execute an empty loop. This is necessary to maintain the processor cache since, in multi-core systems, there is a likelihood that the suspended thread will resume on another core, which will lead to reconstructing the state of the processor cache, a procedure that can be quite costly.

Back to the Table of Contents

List the principles you follow in multithreaded programming.

When writing multithreaded programs, it’s important to adhere to certain rules that help ensure good performance of the application along with ease of debugging and simplicity of further code maintenance.

Back to the Table of Contents

Which of the following statements about threads is incorrect?

  1. If the start() method is called twice for the same Thread object, an exception is generated during execution.
  2. The order in which threads were started may not match the order of their actual execution.
  3. If the run() method is called directly for a Thread object, an exception is generated during execution.
  4. If the sleep() method is called for a thread while it is executing synchronized code, the lock is not released.

The correct answer is 3. If the run() method is called directly for a Thread object, no exception is generated during execution. However, the code written in the run() method will be executed by the current thread, rather than a new one. Thus, the correct way to start the thread is to call the start() method, which leads to the run() method being executed in a new thread.

Calling the start() method twice on the same Thread object will lead to the generation of IllegalThreadStateException during execution; therefore, statement 1 is correct. Statement 2 is true, as the order of execution of threads is determined by the Thread Scheduler, independent of which thread was started first. Statement 4 is correct, as a thread will not release locks that it holds when it enters the Waiting state.

Back to the Table of Contents

Given three threads T1, T2, and T3. How to implement execution in the sequence T1, T2, T3?

Such a sequence of execution can be achieved in several ways, for example, by simply using the join() method to start a thread once another has completed its execution. To implement the specified sequence, you need to start the last thread first and then call the join() method in reverse order, meaning T3 calls T2.join, and T2 calls T1.join; thus, T1 will complete execution first, and T3 last.

Back to the Table of Contents

Write a minimal non-blocking stack (only two methods — push() and pop()).

import java.util.concurrent.atomic.AtomicReference;

class NonBlockingStack<T> {
    private final AtomicReference<Element> head = new AtomicReference<>(null);

    NonBlockingStack<T> push(final T value) {
        final Element current = new Element();
        current.value = value;
        Element recent;
        do {
            recent = head.get();
            current.previous = recent;
        } while (!head.compareAndSet(recent, current));
        return this;
    }

    T pop() {
        Element result;
        Element previous;
        do {
            result = head.get();
            if (result == null) {
                return null;
            }
            previous = result.previous;
        } while (!head.compareAndSet(result, previous));
        return result.value;
    }

    private class Element {
        private T value;
        private Element previous;
    }
}

Back to the Table of Contents

Write a minimal non-blocking stack (only two methods — push() and pop()) using Semaphore.

import java.util.concurrent.Semaphore;

class SemaphoreStack<T> {
    private final Semaphore semaphore = new Semaphore(1);
    private Node<T> head = null;

    SemaphoreStack<T> push(T value) {
        semaphore.acquireUninterruptibly();
        try {
            head = new Node<>(value, head);
        } finally {
            semaphore.release();
        }

        return this;
    }

    T pop() {
        semaphore.acquireUninterruptibly();
        try {
            Node<T> current = head;
            if (current != null) {
                head = head.next;
                return current.value;
            }
            return null;
        } finally {
            semaphore.release();
        }
    }

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

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