Java Concurrency Interview Questions (+ Answers) – Java并发性面试问题(+答案)

最后修改: 2016年 11月 21日

1. Introduction

1.绪论

Concurrency in Java is one of the most complex and advanced topics brought up during technical interviews. This article provides answers to some of the interview questions on the topic that you may encounter.

在技术面试中,Java中的并发性是最复杂和高级的话题之一。本文就你可能遇到的关于该主题的一些面试问题提供答案。

Q1. What Is the Difference Between a Process and a Thread?

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

Both processes and threads are units of concurrency, but they have a fundamental difference: processes do not share a common memory, while threads do.

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

From the operating system’s point of view, a process is an independent piece of software that runs in its own virtual memory space. Any multitasking operating system (which means almost any modern operating system) has to separate processes in memory so that one failing process wouldn’t drag all other processes down by scrambling common memory.

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

The processes are thus usually isolated, and they cooperate by the means of inter-process communication which is defined by the operating system as a kind of intermediate API.

因此,这些进程通常是隔离的,它们通过进程间通信的方式进行合作,而进程间通信是由操作系统定义的一种中间API。

On the contrary, a thread is a part of an application that shares a common memory with other threads of the same application. Using common memory allows to shave off lots of overhead, design the threads to cooperate and exchange data between them much faster.

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

Q2. How Can You Create a Thread Instance and Run It?

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

To create an instance of a thread, you have two options. First, pass a Runnable instance to its constructor and call start(). Runnable is a functional interface, so it can be passed as a lambda expression:

要创建一个线程的实例,你有两个选择。首先,将一个Runnable实例传递给其构造函数,并调用start()Runnable是一个函数接口,所以它可以作为一个lambda表达式来传递。

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

Thread also implements Runnable, so another way of starting a thread is to create an anonymous subclass, override its run() method, and then call start():

Thread也实现了Runnable,所以启动线程的另一种方法是创建一个匿名子类,重载其run()方法,然后调用start()

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

Q3. Describe the Different States of a Thread and When Do the State Transitions Occur.

Q3.描述线程的不同状态以及何时发生状态转换。

The state of a Thread can be checked using the Thread.getState() method. Different states of a Thread are described in the Thread.State enum. They are:

一个Thread的状态可以通过Thread.getState()方法来检查。Thread的不同状态在Thread.State枚举中被描述。它们是

  • NEW — a new Thread instance that was not yet started via Thread.start()
  • RUNNABLE — a running thread. It is called runnable because at any given time it could be either running or waiting for the next quantum of time from the thread scheduler. A NEW thread enters the RUNNABLE state when you call Thread.start() on it
  • BLOCKED — a running thread becomes blocked if it needs to enter a synchronized section but cannot do that due to another thread holding the monitor of this section
  • WAITING — a thread enters this state if it waits for another thread to perform a particular action. For instance, a thread enters this state upon calling the Object.wait() method on a monitor it holds, or the Thread.join() method on another thread
  • TIMED_WAITING — same as the above, but a thread enters this state after calling timed versions of Thread.sleep(), Object.wait(), Thread.join() and some other methods
  • TERMINATED — a thread has completed the execution of its Runnable.run() method and terminated

Q4. What Is the Difference Between the Runnable and Callable Interfaces? How Are They Used?

Q4.可运行接口和可调用接口之间的区别是什么?它们是如何使用的?

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.

Runnable接口有一个单一的 run方法。它代表一个必须在单独的线程中运行的计算单元。Runnable接口不允许该方法返回值或抛出未检查的异常。

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.

Callable接口有一个call方法,代表一个有值的任务。这就是为什么call方法返回一个值。它也可以抛出异常。Callable一般用于ExecutorService实例,以启动一个异步任务,然后调用返回的Future实例来获取其值。

Q5. What Is a Daemon Thread, What Are Its Use Cases? How Can You Create a Daemon Thread?

Q5.什么是守护线程,它的使用案例是什么?你怎样才能创建一个守护线程?

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.

守护线程是一个不妨碍JVM退出的线程。当所有非守护线程被终止时,JVM会简单地放弃所有剩余的守护线程。守护线程通常用于为其他线程执行一些支持性或服务性任务,但你应该考虑到它们可能在任何时候被放弃。

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

要将一个线程作为守护程序启动,你应该在调用start()之前使用setDaemon()方法。

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.

奇怪的是,如果你把它作为main()方法的一部分来运行,信息可能不会被打印。如果main()线程在守护进程达到打印消息的程度之前就终止了,就会发生这种情况。一般来说,你不应该在守护进程中做任何I/O,因为它们甚至不能执行它们的finally块和关闭资源,如果被放弃的话。

Q6. What Is the Thread’s Interrupt Flag? How Can You Set and Check It? How Does It Relate to the Interruptedexception?

Q6.什么是线程的中断标志?你如何设置和检查它?它与中断异常的关系如何?

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.

中断标志,或中断状态,是一个内部线程标志,当线程被中断时被设置。要设置它,只需在线程对象上调用thread.interrupt()即可.

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.

如果一个线程目前在抛出InterruptedException的方法中(wait, join, sleep等),那么这个方法会立即抛出InterruptedException。线程可以自由地按照自己的逻辑来处理这个异常。

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.

如果一个线程不在这样的方法内,并且thread.interrupt()被调用,则不会发生任何特殊情况。线程有责任使用static Thread.interrupted()或实例isInterrupted()方法来定期检查中断状态。这些方法的区别在于,static Thread.interrupted()会清除中断标志,而isInterrupted()不会。

Q7. What Are Executor and Executorservice? What Are the Differences Between These Interfaces?

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.

ExecutorExecutorServicejava.util.concurrent框架的两个相关接口。Executor是一个非常简单的接口,只有一个execute方法,接受Runnable实例来执行。在大多数情况下,这是你的任务执行代码应该依赖的接口。

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.

ExecutorService扩展了Executor接口,具有处理和检查并发任务执行服务的生命周期的多种方法(在关闭的情况下终止任务)以及用于更复杂的异步任务处理的方法,包括Futures

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

有关使用ExecutorExecutorService的更多信息,请参阅文章Java ExecutorService指南

Q8. What Are the Available Implementations of Executorservice in the Standard Library?

Q8.标准库中Executorservice的可用实现有哪些?

The ExecutorService interface has three standard implementations:

ExecutorService接口有三个标准实现。

  • 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. What Is Java Memory Model (Jmm)? Describe Its Purpose and Basic Ideas.

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.

Java内存模型是第17.4章中描述的Java语言规范的一部分。它规定了在一个并发的Java应用程序中,多个线程如何访问公共内存,以及一个线程的数据变化如何对其他线程可见。虽然JMM相当短小精悍,但如果没有强大的数学背景,可能很难掌握。

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.

对内存模型的需求来自于这样一个事实:你的Java代码访问数据的方式并不是它在低层实际发生的方式。内存的写入和读取可能会被Java编译器、JIT编译器,甚至是CPU重新排序或优化,只要这些读写的可观察结果是一样的。

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.

更糟糕的是,不同内存访问架构的存在会打破Java “一次编写,到处运行 “的承诺。对程序员来说,令人高兴的是,JMM规定了一些保证,在设计多线程应用程序时,你可以依靠这些保证。坚持这些保证可以帮助程序员写出稳定的多线程代码,并且在不同的架构之间可以移植。

The main notions of JMM are:

JMM的主要概念是。

  • 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. What Is a Volatile Field and What Guarantees Does the Jmm Hold for Such Field?

Q10。什么是不稳定的领域,Jmm对这种领域有什么保证?

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.

根据Java内存模型,volatile字段具有特殊的属性(见问题9)。volatile变量的读和写是同步动作,这意味着它们有一个总的顺序(所有线程将观察这些动作的一致顺序)。根据这个顺序,对易失性变量的读取保证能观察到对该变量的最后一次写入。

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.

如果你有一个字段被多个线程访问,至少有一个线程向它写东西,那么你应该考虑把它变成volatile,否则对某个线程从这个字段中读出的东西就有一点保证。

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.

volatile的另一个保证是写入和读取64位值(longdouble)的原子性。如果没有易失性修饰符,对这种字段的读取可能会观察到由另一个线程部分写入的值。

Q11. Which of the Following Operations Are Atomic?

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.

int(32位)变量的写入保证是原子性的,无论它是否是volatile。例如,在32位架构上,一个long(64位)变量可以分两步写入,所以默认情况下,没有原子性的保证。然而,如果你指定了volatile修饰符,long变量就能保证被原子化地访问。

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.

增量操作通常分多步进行(检索数值、改变数值和写回),所以无论变量是否易失性,它都不能保证是原子性的。如果你需要实现数值的原子增量,你应该使用AtomicIntegerAtomicLong等类。

Q12. What Special Guarantees Does the Jmm Hold for Final Fields of a Class?

Q12.Jmm对一个班级的最终领域有什么特别的保证?

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.

JVM基本上保证一个类的最终字段将在任何线程掌握该对象之前被初始化。如果没有这个保证,在一个对象的所有字段被初始化之前,由于重新排序或其他优化,对该对象的引用可能会被发布,也就是对另一个线程可见。这可能会导致对这些字段的恶意访问。

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.

这就是为什么在创建一个不可变的对象时,你应该总是让它的所有字段final,即使它们不能通过getter方法访问。

Q13. What Is the Meaning of a Synchronized Keyword in the Definition of a Method? of a Static Method? Before a Block?

Q13.在方法的定义中,同步关键词的含义是什么? 静态方法的定义?在一个块之前?

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关键字意味着任何进入这个区块的线程都必须获得监视器(括号中的对象)。如果监视器已经被另一个线程获取,那么前一个线程将进入BLOCKED状态并等待监视器被释放。

synchronized(object) {
    // ...
}

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

一个synchronized实例方法具有相同的语义,但实例本身充当监视器。

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

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

对于一个静态同步方法,监视器是代表声明类的Class对象。

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

Q14. If Two Threads Call a Synchronized Method on Different Object Instances Simultaneously, Could One of These Threads Block? What If the Method Is Static?

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.

如果该方法是static,那么监视器就是Class对象。对于两个线程来说,监视器是相同的,所以其中一个线程可能会阻塞并等待另一个线程退出synchronized方法。

Q15. What Is the Purpose of the Wait, Notify and Notifyall Methods of the Object Class?

Q15.对象类的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.

拥有对象监视器的线程(例如,进入由对象守护的同步部分的线程)可以调用object.wait()来暂时释放监视器,给其他线程一个获得监视器的机会。例如,这可能是为了等待某个条件。

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.

当另一个获取监视器的线程满足条件时,它可以调用object.notify()object.notifyAll()并释放监视器。notify方法唤醒了处于等待状态的单个线程,而notifyAll方法唤醒了所有等待这个监视器的线程,它们都在竞争重新获得锁。

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 waits 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.

下面的BlockingQueue实现展示了多个线程如何通过wait-notify模式协同工作。如果我们一个元素放到一个空队列中,所有在take方法中等待的线程都会被唤醒并尝试接收该值。如果我们put一个元素到一个完整的队列,put方法等待get方法的调用。get方法删除了一个元素,并通知在put方法中等待的线程,队列中有一个空位可以容纳一个新的项目。

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. Describe the Conditions of Deadlock, Livelock, and Starvation. Describe the Possible Causes of These Conditions.

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. Describe the Purpose and Use-Cases of the Fork/Join Framework.

问题17。请描述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.

fork/join框架允许将递归算法并行化。使用类似ThreadPoolExecutor的东西来并行化递归的主要问题是,你可能很快就会耗尽线程,因为每个递归步骤都需要自己的线程,而堆栈上的线程则是空闲和等待。

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.

分叉/连接框架的入口是ForkJoinPool类,它是ExecutorService的一个实现。它实现了偷工减料的算法,空闲的线程试图从繁忙的线程中 “偷 “出工作。这允许在不同的线程之间分散计算,并在使用较少的线程的情况下取得进展,而不是像通常的线程池那样需要。

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

更多关于fork/join框架的信息和代码样本可以在“Guide to the Fork/Join Framework in Java”这篇文章中找到。

Next »

Java Class Structure and Initialization Interview Questions

« Previous

Java Type System Interview Questions