Java multithreading interview questions
A process is a self-contained running environment, which can be regarded as a program or an application. A thread is a task executed in a process. The Java runtime environment is a single process that contains different classes and programs. Threads can be called lightweight processes. Threads require fewer resources to create and reside in a process, and can share resources within the process.
In a multi-threaded program, multiple threads are executed concurrently to improve the efficiency of the program. The CPU will not enter an idle state because a thread needs to wait for resources. Multiple threads share heap memory, so it is better to create multiple threads to perform some tasks than to create multiple processes. For example, Servlets are better than CGI because Servlets support multi-threading while CGI does not.
When we create a thread in a Java program, it is called a user thread. A daemon thread is a thread that executes in the background and does not prevent the JVM from terminating. When no user threads are running, the JVM closes the program and exits. Child threads created by a daemon thread are still daemon threads.
There are two ways to create a thread: one is to implement the Runnable interface, and then pass it to the Thread constructor to create a Thread object; the other is to directly inherit the Thread class. If you want to know more you can read this article on how to create threads in Java.
When we create a new thread in a Java program, its status is New. When we call the thread's start() method, the status is changed to Runnable. The thread scheduler allocates CPU time to threads in the Runnable thread pool and changes their status to Running. Other thread states include Waiting, Blocked and Dead. Read this article to learn more about thread lifecycle.
Of course, but if we call Thread's run() method, it will behave like a normal method. In order to execute our code in a new thread, we must use the Thread.start() method.
We can use the Sleep() method of the Thread class to pause the thread for a period of time. It should be noted that this does not terminate the thread. Once the thread is awakened from sleep, the thread's status will be changed to Runnable, and it will be executed according to the thread schedule.
Each thread has a priority. Generally speaking, high-priority threads will have priority when running, but this depends on the implementation of thread scheduling, which is OS dependent. We can define the priority of threads, but this does not guarantee that high-priority threads will execute before low-priority threads. Thread priority is an int variable (from 1-10), 1 represents the lowest priority, 10 represents the highest priority.
The thread scheduler is an operating system service that is responsible for allocating CPU time to threads in the Runnable state. Once we create a thread and start it, its execution depends on the implementation of the thread scheduler. Time slicing refers to the process of allocating available CPU time to available Runnable threads. Allocating CPU time can be based on thread priority or the time the thread waits. Thread scheduling is not controlled by the Java virtual machine, so it is better for the application to control it (that is, do not make your program dependent on thread priority).
Context switching is the process of storing and restoring CPU state, which enables thread execution to resume execution from the point of interruption. Context switching is an essential feature of multitasking operating systems and multithreaded environments.
We can use the joint() method of the Thread class to ensure that all threads created by the program end before the main() method exits. Here is an article about the joint() method of Thread class.
When resources can be shared between threads, inter-thread communication is an important means of coordinating them. The wait()/notify()/notifyAll() methods in the Object class can be used to communicate between threads about the status of resource locks. Click here to learn more about thread wait, notify and notifyAll.
Each object in Java has a lock (monitor, which can also be a monitor), and methods such as wait() and notify() are used to wait for the object's lock or notify other threads that the object's monitor is available. There are no locks or synchronizers available for any object in Java threads. That's why these methods are part of the Object class so that every class in Java has basic methods for inter-thread communication
When a thread needs to call the wait() method of an object, the thread must own the lock of the object. Then it will release the object lock and enter the waiting state until other threads call the notify() method on the object. Similarly, when a thread needs to call the object's notify() method, it will release the object's lock so that other waiting threads can obtain the object lock. Since all of these methods require the thread to hold the object's lock, which can only be achieved through synchronization, they can only be called in synchronized methods or synchronized blocks.
The sleep() and yield() methods of the Thread class will run on the currently executing thread. So it doesn't make sense to call these methods on other threads that are waiting. That's why these methods are static. They can work in the currently executing thread and prevent programmers from mistakenly thinking that these methods can be called in other non-running threads.
There are many ways to ensure thread safety in Java - synchronization, using atomic concurrent classes, implementing concurrent locks, using the volatile keyword, using immutable classes and thread-safe classes. You can learn more in the thread safety tutorial.
When we use the volatile keyword to modify a variable, the thread will read the variable directly and not cache it. This ensures that the variables read by the thread are consistent with those in memory.
A synchronized block is a better choice because it does not lock the entire object (of course you can also make it lock the entire object). Synchronized methods lock the entire object, even if there are multiple unrelated synchronized blocks in the class, which usually causes them to stop executing and need to wait to obtain the lock on the object.
The thread can be set as a daemon thread using the setDaemon(true) method of the Thread class. It should be noted that this method needs to be called before calling the start() method, otherwise an IllegalThreadStateException will be thrown.
ThreadLocal is used to create thread local variables. We know that all threads of an object will share its global variables, so these variables are not thread-safe. We can use synchronization technology. But when we don't want to use synchronization, we can choose ThreadLocal variables.
Each thread will have their own Thread variables, and they can use the get()/set() methods to get their default values or change their values within the thread. ThreadLocal instances typically want their associated thread state to be private static properties. In the ThreadLocal example article you can see a small program about ThreadLocal.
ThreadGroup is a class whose purpose is to provide information about thread groups.
The ThreadGroup API is relatively weak and does not provide more functions than Thread. It has two main functions: one is to obtain the list of active threads in the thread group; the other is to set the uncaught exception handler (ncaught exception handler) for the thread. However, in Java 1.5, the Thread class also added the setUncaughtExceptionHandler(UncaughtExceptionHandler eh) method, so ThreadGroup is obsolete and is not recommended to continue to be used.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("exception occured:"+e.getMessage());} });
A thread dump is a list of JVM active threads, which is very useful for analyzing system bottlenecks and deadlocks. There are many ways to obtain thread dumps - using Profiler, Kill -3 command, jstack tool, etc. I prefer jstack tool because it is easy to use and comes with JDK. Since it is a terminal-based tool, we can write some scripts to periodically generate thread dumps for analysis. Read this document to learn more about generating thread dumps.
Deadlock refers to a situation where more than two threads are blocked forever. This situation requires at least two more threads and more than two resources.
To analyze the deadlock, we need to look at the thread dump of the Java application. We need to find out which threads are in BLOCKED status and the resources they are waiting for. Each resource has a unique id, using this id we can find out which threads already own its object lock.
Avoiding nested locks, using locks only where needed and avoiding indefinite waits are common ways to avoid deadlocks. Read this article to learn how to analyze deadlocks.
java.util.Timer is a tool class that can be used to schedule a thread to execute at a specific time in the future. The Timer class can be used to schedule one-time tasks or periodic tasks.
java.util.TimerTask is an abstract class that implements the Runnable interface. We need to inherit this class to create our own scheduled tasks and use Timer to schedule its execution.
Here are examples about java Timer.
A thread pool manages a group of worker threads and also includes a queue for placing tasks waiting to be executed.
java.util.concurrent.Executors provides an implementation of the java.util.concurrent.Executor interface for creating thread pools. The Thread Pool example shows how to create and use a thread pool, or read the ScheduledThreadPoolExecutor example to learn how to create a periodic task.
Java concurrency interview questions
An atomic operation refers to an operation task unit that is not affected by other operations. Atomic operations are a necessary means to avoid data inconsistency in a multi-threaded environment.
int++ is not an atomic operation, so when one thread reads its value and adds 1, another thread may read the previous value, which will cause an error.
In order to solve this problem, we must ensure that the increase operation is atomic. Before JDK1.5, we could use synchronization technology to do this. As of JDK 1.5, the java.util.concurrent.atomic package provides int and long type loading classes that automatically guarantee that their operations are atomic and do not require the use of synchronization. You can read this article to learn about Java's atomic classes.
The Lock interface provides more scalable lock operations than synchronized methods and synchronized blocks. They allow for more flexible structures that can have completely different properties, and can support multiple related classes of conditional objects.
Its advantages are:
Read more about lock examples
The Executor framework was introduced in Java 5 with the java.util.concurrent.Executor interface. The Executor framework is a framework for asynchronous tasks that are called, scheduled, executed, and controlled according to a set of execution strategies.
Unlimited thread creation can cause application memory to overflow. So creating a thread pool is a better solution because the number of threads can be limited and these threads can be recycled and reused. It is very convenient to create a thread pool using the Executors framework. Read this article to learn how to create a thread pool using the Executor framework.
The characteristics of java.util.concurrent.BlockingQueue are: when the queue is empty, the operation of obtaining or deleting elements from the queue will be blocked, or when the queue is full, the operation of adding elements to the queue will be blocked.
The blocking queue does not accept null values. When you try to add a null value to the queue, it will throw a NullPointerException.
Blocking queue implementations are thread-safe, and all query methods are atomic and use internal locks or other forms of concurrency control.
The BlockingQueue interface is part of the Java collections framework and is mainly used to implement the producer-consumer problem.
Read this article to learn how to implement the producer-consumer problem using blocking queues.
Java 5 introduced the java.util.concurrent.Callable interface in the concurrency package, which is very similar to the Runnable interface, but it can return an object or throw an exception.
The Callable interface uses generics to define its return type. The Executors class provides some useful methods to execute tasks within Callable in the thread pool. Since the Callable task is parallel, we have to wait for the result it returns. The java.util.concurrent.Future object solves this problem for us. After the thread pool submits the Callable task, a Future object is returned. Using it, we can know the status of the Callable task and get the execution result returned by the Callable. Future provides the get() method so that we can wait for the Callable to end and obtain its execution results.
Read this article to know more examples about Callable, Future.
FutureTask is a basic implementation of Future, which we can use with Executors to process asynchronous tasks. Usually we don't need to use the FutureTask class, but it becomes very useful when we plan to override some methods of the Future interface and keep the original basic implementation. We can just inherit from it and override the methods we need. Read the Java FutureTask example to learn how to use it.
Java collection classes are fail-fast, which means that when the collection is modified and a thread uses an iterator to traverse the collection, the iterator's next() method will throw a ConcurrentModificationException exception.
Concurrent containers support concurrent traversal and concurrent updates.
The main classes are ConcurrentHashMap, CopyOnWriteArrayList and CopyOnWriteArraySet. Read this article to learn how to avoid ConcurrentModificationException.
Executors provide some utility methods for the Executor, ExecutorService, ScheduledExecutorService, ThreadFactory and Callable classes.
Executors can be used to easily create thread pools.
Original text: journaldev.com Translation: ifeve Translator: Zheng Xudong