1. 概述

Java并发是我们面试过程中遇到的最复杂、最高级的技术话题之一。 本文整理了关于此话题下常见的面试问题,以及对应的回答。

Q1. 进程和线程之间有什么区别 ?

进程和线程都是并发单元,但是它们有一个根本的区别:进程不共享公共内存,而线程共享。

从操作系统视角来看,进程是一个独立的软件,运行在自己的虚拟内存空间中。所有多任务操作系统(现代操作系统基本属于)都必须在内存中隔离进程,这样一个失败的进程就不会因为扰乱公共内存而拖累所有其他进程。

因此,进程之间通常是隔离的,它们通过进程间通信进行协作,而进程间通信被操作系统定义为一种中间API。

相反,线程是应用程序的一部分,它与同一应用程序的其他线程共享一个公共内存。使用公共内存可以减少大量开销,使得线程间协作和数据交换更加快速。

Q2. 如何创建线程实例并运行 ?

有2种创建线程的方法。第一种,将Runnable实例传递给Thread构造函数,并调用start方法。因为Runnable是一个函数式接口,所以可以将其作为lambda表达式传递:

Thread thread1 = new Thread(() ->
  System.out.println("Hello World from Runnable!"));
thread1.start();

Thread实现了Runnable接口,所以另一种方法是创建一个匿名子类,并重写run()方法,然后调用start()

Thread thread2 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello World from subclass!");
    }
};
thread2.start();

Q3. 请描述线程的几种状态,以及何时发生状态转换。

可以通过Thread.getState()方法检查线程的状态。Thread.State枚举中描述了线程拥有的几种状态,它们是:

  • NEW(新建) — 一个新的Thread实例,但尚未调用Thread.start()
  • RUNNABLE(可运行) — 一个运行中的线程。之所以称为"可运行",是因为它在任何给定时间上可能正在运行,也可能正在等待线程调度器发出的下个时间片段。当您调用Thread.start()时,新线程进入RUNNABLE状态。
  • BLOCKED(阻塞) — 如果一个正在运行的线程需要进入同步部分,但是由于另一个线程持有这部分的锁而无法进入,那么它就会被阻塞
  • WAITING(无限等待) — 如果线程等待另一个线程执行特定操作,则会进入此状态。例如,一个线程调用它持有的monitor上的Object.wait()方法,或在另一个线程上调用thread.join()方法时进入这种状态
  • TIMED_WAITING(超时等待) — 和上述一样,只不过调用的是带有超时参数的Thread.sleep()Object.wait()Thread.join()方法等
  • TERMINATE(终止) — 线程已执行完Runnable.run()方法并终止

Q4. Runnable 和 Callable 接口的区别是什么?如何使用?

The Runnable interface has a single run method. It represents a unit of computation that has to be run in a separate thread. The Runnable interface does not allow this method to return value or to throw unchecked exceptions.

The Callable interface has a single call method and represents a task that has a value. That's why the call method returns a value. It can also throw exceptions. Callable is generally used in ExecutorService instances to start an asynchronous task and then call the returned Future instance to get its value.

什么是守护线程(Daemon Thread),其用例是什么? 如何创建守护进程线程 ?

A daemon thread is a thread that does not prevent JVM from exiting. When all non-daemon threads are terminated, the JVM simply abandons all remaining daemon threads. Daemon threads are usually used to carry out some supportive or service tasks for other threads, but you should take into account that they may be abandoned at any time.

To start a thread as a daemon, you should use the setDaemon() method before calling start():

Thread daemon = new Thread(()
  -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

Curiously, if you run this as a part of the main() method, the message might not get printed. This could happen if the main() thread would terminate before the daemon would get to the point of printing the message. You generally should not do any I/O in daemon threads, as they won't even be able to execute their finally blocks and close the resources if abandoned.

Q6. 什么是线程的中断标记? 如何设置和检查? 它与InterruptedException有何关系?

The interrupt flag, or interrupt status, is an internal Thread flag that is set when the thread is interrupted. To set it, simply call thread.interrupt() on the thread object_._

If a thread is currently inside one of the methods that throw InterruptedException (wait, join, sleep etc.), then this method immediately throws InterruptedException. The thread is free to process this exception according to its own logic.

If a thread is not inside such method and thread.interrupt() is called, nothing special happens. It is thread's responsibility to periodically check the interrupt status using static Thread.interrupted() or instance isInterrupted() method. The difference between these methods is that the static Thread.interrupted() clears the interrupt flag, while isInterrupted() does not.

Q7. 什么是Executor 和 Executorservice? 这些接口的区别是什么?

Executor and ExecutorService are two related interfaces of java.util.concurrent framework. Executor is a very simple interface with a single execute method accepting Runnable instances for execution. In most cases, this is the interface that your task-executing code should depend on.

ExecutorService extends the Executor interface with multiple methods for handling and checking the lifecycle of a concurrent task execution service (termination of tasks in case of shutdown) and methods for more complex asynchronous task handling including Futures.

For more info on using Executor and ExecutorService, see the article A Guide to Java ExecutorService.

Q8. Executorservice 的实现类有哪些?

The ExecutorService interface has three standard implementations:

  • ThreadPoolExecutor — for executing tasks using a pool of threads. Once a thread is finished executing the task, it goes back into the pool. If all threads in the pool are busy, then the task has to wait for its turn.
  • ScheduledThreadPoolExecutor allows to schedule task execution instead of running it immediately when a thread is available. It can also schedule tasks with fixed rate or fixed delay.
  • ForkJoinPool is a special ExecutorService for dealing with recursive algorithms tasks. If you use a regular ThreadPoolExecutor for a recursive algorithm, you will quickly find all your threads are busy waiting for the lower levels of recursion to finish. The ForkJoinPool implements the so-called work-stealing algorithm that allows it to use available threads more efficiently.

Q9. 什么是Java内存模型 (Jmm)? 请描述其目的和基本思想。

Java Memory Model is a part of Java language specification described in Chapter 17.4. It specifies how multiple threads access common memory in a concurrent Java application, and how data changes by one thread are made visible to other threads. While being quite short and concise, JMM may be hard to grasp without strong mathematical background.

The need for memory model arises from the fact that the way your Java code is accessing data is not how it actually happens on the lower levels. Memory writes and reads may be reordered or optimized by the Java compiler, JIT compiler, and even CPU, as long as the observable result of these reads and writes is the same.

This can lead to counter-intuitive results when your application is scaled to multiple threads because most of these optimizations take into account a single thread of execution (the cross-thread optimizers are still extremely hard to implement). Another huge problem is that the memory in modern systems is multilayered: multiple cores of a processor may keep some non-flushed data in their caches or read/write buffers, which also affects the state of the memory observed from other cores.

To make things worse, the existence of different memory access architectures would break the Java's promise of “write once, run everywhere”. Happily for the programmers, the JMM specifies some guarantees that you may rely upon when designing multithreaded applications. Sticking to these guarantees helps a programmer to write multithreaded code that is stable and portable between various architectures.

The main notions of JMM are:

  • Actions, these are inter-thread actions that can be executed by one thread and detected by another thread, like reading or writing variables, locking/unlocking monitors and so on
  • Synchronization actions, a certain subset of actions, like reading/writing a volatile variable, or locking/unlocking a monitor
  • Program Order (PO), the observable total order of actions inside a single thread
  • Synchronization Order (SO), the total order between all synchronization actions — it has to be consistent with Program Order, that is, if two synchronization actions come one before another in PO, they occur in the same order in SO
  • synchronizes-with (SW) relation between certain synchronization actions, like unlocking of monitor and locking of the same monitor (in another or the same thread)
  • Happens-before Order — combines PO with SW (this is called transitive closure in set theory) to create a partial ordering of all actions between threads. If one action happens-before another, then the results of the first action are observable by the second action (for instance, write of a variable in one thread and read in another)
  • Happens-before consistency — a set of actions is HB-consistent if every read observes either the last write to that location in the happens-before order, or some other write via data race
  • Execution — a certain set of ordered actions and consistency rules between them

For a given program, we can observe multiple different executions with various outcomes. But if a program is correctly synchronized, then all of its executions appear to be sequentially consistent, meaning you can reason about the multithreaded program as a set of actions occurring in some sequential order. This saves you the trouble of thinking about under-the-hood reorderings, optimizations or data caching.

Q10. 什么是 Volatile 字段,Java内存模型对此字段有何保证?

A volatile field has special properties according to the Java Memory Model (see Q9). The reads and writes of a volatile variable are synchronization actions, meaning that they have a total ordering (all threads will observe a consistent order of these actions). A read of a volatile variable is guaranteed to observe the last write to this variable, according to this order.

If you have a field that is accessed from multiple threads, with at least one thread writing to it, then you should consider making it volatile, or else there is a little guarantee to what a certain thread would read from this field.

Another guarantee for volatile is atomicity of writing and reading 64-bit values (long and double). Without a volatile modifier, a read of such field could observe a value partly written by another thread.

Q11. 以下哪些操作是原子操作 ?

  • writing to a non-volatile int;
  • writing to a volatile int;
  • writing to a non-volatile long;
  • writing to a volatile long;
  • incrementing a volatile long?

A write to an int (32-bit) variable is guaranteed to be atomic, whether it is volatile or not. A long (64-bit) variable could be written in two separate steps, for example, on 32-bit architectures, so by default, there is no atomicity guarantee. However, if you specify the volatile modifier, a long variable is guaranteed to be accessed atomically.

The increment operation is usually done in multiple steps (retrieving a value, changing it and writing back), so it is never guaranteed to be atomic, wether the variable is volatile or not. If you need to implement atomic increment of a value, you should use classes AtomicInteger, AtomicLong etc.

Q12. Java内存模型对Final字段有何特殊保证?

JVM basically guarantees that final fields of a class will be initialized before any thread gets hold of the object. Without this guarantee, a reference to an object may be published, i.e. become visible, to another thread before all the fields of this object are initialized, due to reorderings or other optimizations. This could cause racy access to these fields.

This is why, when creating an immutable object, you should always make all its fields final, even if they are not accessible via getter methods.

Q13. 方法定义中的Synchronized关键字含义是什么?静态方法、代码块呢?

The synchronized keyword before a block means that any thread entering this block has to acquire the monitor (the object in brackets). If the monitor is already acquired by another thread, the former thread will enter the BLOCKED state and wait until the monitor is released.

synchronized(object) {
    // ...
}

A synchronized instance method has the same semantics, but the instance itself acts as a monitor.

synchronized void instanceMethod() {
    // ...
}

For a static synchronized method, the monitor is the Class object representing the declaring class.

static synchronized void staticMethod() {
    // ...
}

Q14. 如果两个线程同时调用不同对象实例上的同步方法,那么其中一个线程会阻塞吗? 如果是静态方法呢?

If the method is an instance method, then the instance acts as a monitor for the method. Two threads calling the method on different instances acquire different monitors, so none of them gets blocked.

If the method is static, then the monitor is the Class object. For both threads, the monitor is the same, so one of them will probably block and wait for another to exit the synchronized method.

Q15. Object类的Wait、Notify和Notifyall方法的用途是什么

A thread that owns the object's monitor (for instance, a thread that has entered a synchronized section guarded by the object) may call object.wait() to temporarily release the monitor and give other threads a chance to acquire the monitor. This may be done, for instance, to wait for a certain condition.

When another thread that acquired the monitor fulfills the condition, it may call object.notify() or object.notifyAll() and release the monitor. The notify method awakes a single thread in the waiting state, and the notifyAll method awakes all threads that wait for this monitor, and they all compete for re-acquiring the lock.

The following BlockingQueue implementation shows how multiple threads work together via the wait-notify pattern. If we put an element into an empty queue, all threads that were waiting in the take method wake up and try to receive the value. If we put an element into a full queue, the put method _wait_s for the call to the get method. The get method removes an element and notifies the threads waiting in the put method that the queue has an empty place for a new item.

    public class BlockingQueue<T> {

        private List<T> queue = new LinkedList<T>();

        private int limit = 10;

        public synchronized void put(T item) {
            while (queue.size() == limit) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            if (queue.isEmpty()) {
                notifyAll();
            }
            queue.add(item);
        }
    
        public synchronized T take() throws InterruptedException {
            while (queue.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            if (queue.size() == limit) {
                notifyAll();
            }
            return queue.remove(0);
        }
        
    }

Q16. 请描述死锁、活锁、饥饿,以及可能的产生条件。

Deadlock is a condition within a group of threads that cannot make progress because every thread in the group has to acquire some resource that is already acquired by another thread in the group. The most simple case is when two threads need to lock both of two resources to progress, the first resource is already locked by one thread, and the second by another. These threads will never acquire a lock to both resources and thus will never progress.

Livelock is a case of multiple threads reacting to conditions, or events, generated by themselves. An event occurs in one thread and has to be processed by another thread. During this processing, a new event occurs which has to be processed in the first thread, and so on. Such threads are alive and not blocked, but still, do not make any progress because they overwhelm each other with useless work.

Starvation is a case of a thread unable to acquire resource because other thread (or threads) occupy it for too long or have higher priority. A thread cannot make progress and thus is unable to fulfill useful work.

Q17. 请描述Fork/Join的用途和用例。

The fork/join framework allows parallelizing recursive algorithms. The main problem with parallelizing recursion using something like ThreadPoolExecutor is that you may quickly run out of threads because each recursive step would require its own thread, while the threads up the stack would be idle and waiting.

The fork/join framework entry point is the ForkJoinPool class which is an implementation of ExecutorService. It implements the work-stealing algorithm, where idle threads try to “steal” work from busy threads. This allows to spread the calculations between different threads and make progress while using fewer threads than it would require with a usual thread pool.

More information and code samples for the fork/join framework may be found in the article “Guide to the Fork/Join Framework in Java”.