Overview

Contents

Introduction

The Open APIs have a well-defined threading model, i.e. a set of conventions and requirements that should be used in regards to use of threads and long-running computations by all users of the Open APIs. This model applies both to API code itself; code in the IDE's private implementation; and all code present in modules.

Rather than attempt to make all calls in the APIs unconditionally thread-safe, which is not practical for the same reasons that much Java Platform code is not thread-safe in this way, the APIs are designed to have different threading requirements depending on which section of the APIs is being used. For the most part the division corresponds to API packages, although there are other considerations.

This document will begin by outlining several different domains, each of which has its own threading characteristics. Then, the APIs will be divided functionally into the different domains. Programmers who pay attention to this division and use the appropriate threading models throughout their code are of course not guaranteed freedom from deadlocks, UI freezes, race conditions, and multithreaded data structure corruption, but the risk of these types of errors is greatly reduced. Where appropriate, notes are included as to how extensively a domain has been tested for safety.

Threading-Model Domains

Thread-safe

Several areas of the Open APIs are designed to be completely thread-safe, which is to say that calls to these APIs may be made from any thread without concern for deadlocks or corruptions. Most importantly, the FileSystems API, DataSystems API, and Java Hierarchy API fall into this category. All code in these APIs is designed to expect concurrent access from multiple threads, and is automatically synchronized against such a possibility.

Implementations of abstract API objects in these packages should also follow this convention and use language-level synchronization where needed. Such implementations include custom file systems, data loaders, data objects, cookie supports, hierarchy element implementations, etc. Note that AbstractFileSystem assumes much of the burden of synchronization for typical file system implementations, including the standard LocalFileSystem.

Accesses to individual nodes are also thread-safe, e.g. metaproperties such as display name and icon, as well as properties, sheets, and sheet sets. As for other APIs, implementations of these objects must retain this thread safety, so that for example any module-provided node property must somehow synchronize its getter and setter methods, if the property class does not do this automatically. Individual operations on the node hierarchy (i.e. operations on Children) are also thread-safe and atomic; however, some users may wish to use their mutex for multi-stage operations.

Swing abstract text documents are assumed to be thread-safe for callers, so implementors of editors must take care to synchronize these objects to prevent corruption. For atomicity of transactions on documents, callers should use NbDocument.runAtomic(...) or NbDocument.runAtomicAsUser(...) as needed. Note that this thread-safety does not extend to GUI components built using the documents, such as EditorSupport.Editor.

Note that these packages do not generally protect the programmer against delays, i.e. some calls may require a user-perceptible amount of time to complete. In such cases, GUI code should be careful when using the APIs, lest temporary screen freezes or jumpiness occur. That said, all calls in these packages which are expected to be slow provide a mechanism alleviating the problem:

FileSystems has been extensively tested for thread safety and appears to be quite robust. DataSystems has not undergone as extensive testing but still has no known threading problems. The implementation of thread-safe nodes is quite new as of this writing (build 383), but it is expected that the reimplementation will solve a number of previously intractable threading problems, as well as simplifying the caller-side semantics. Thread-safe documents are entirely up to the editor implementation, though there is associated API code such as updates Line.Sets which should be safe.

Unsynchronized

Some utility APIs are unsynchronized and are only suitable for access from within a single thread, as is common in utility classes such as the Java Collections API. Such APIs do not interact directly with other APIs and so this is reasonable. Such APIs include enumerations, safe I/O, and data transfer.

AWT Event Queue

A number of pieces of the API are designed to operate as GUI components; like in any AWT/Swing GUI application, care must be taken to only access these APIs within the AWT event queue, and not to consume an unreasonable amount of time or block when running code within them. Most importantly, the entire Explorer API is AWT-threaded, as well as the Window System API.

To make calls into these APIs from non-event-queue threads, the normal Swing mechanisms should be used, e.g. SwingUtilities.invokeLater(...).

Note that nodes are not accessed in the event queue, they are mutexed; however, Explorer views are (and must be, since they are visual components). To interface between these domains, the Visualizer class and some associated models such as NodeTreeModel are used by Explorer views to provide event-queue-safe calling conventions while being asynchronously updated from the node hierarchy. Other code which must run in the event queue could also use these classes to interact safely with the node hierarchy.

Startup

Certain code is run (in the NetBeans private implementation of the APIs) in the application main thread during IDE startup time. The main impact of this on module authors is that some code may be run in this main thread, specifically module install code. Generally such code should avoid waiting for the event queue (though non-Window-System GUI objects such as dialogs may be created at install or restore item, and Window System top components at install time only).

Module authors should be cautious about code run from ModuleInstall implementation methods, using only thread-safe calls or tasks.

TopComponent externalization methods may also be called in this main thread, during Window System startup, as well as potentially in the event queue, and thus should be as cautious.

Mutexes

Certain APIs are thread-safe, but for implementation purposes require internal synchronization, and this synchronization is also exposed to caller-side code which requires transactional operations; this is generally done using Mutexes.

Each mutex follows the usual model of permitting any number of simultaneous readers, or a single writer, to exist in the mutex block. (Also, re-entering a mutex with the same type of access is harmless, as is entering in read mode while already holding write mode. Trying to re-enter in write mode while holding a read mode lock will cause a quick deadlock; to catch these, run the IDE with the system property -Dnetbeans.debug.threads=true.) There are four calls which permit threads to access a mutex: Mutex.readAccess(Runnable), Mutex.readAccess(Mutex.Action), Mutex.writeAccess(Runnable), and Mutex.writeAccess(Mutex.Action). Collectively these provide not only read-versus-write access to the mutex, but also the ability to either run code asynchronously whenever the mutex is next available, versus running code with a return value and blocking for it. (Compare similar calls in SwingUtilities.)

Typically an API subsystem has an associated public mutex or mutexes; other mutexes may be constructed if needed. The principal one is for the node hierarchy: Children.MUTEX is used by all API code which manipulates the parent-child relationships among nodes. Simple caller-side code need not be concerned with this, as basic operations such as getting a list of children of a node, or adding one, will be automatically thread-safe (and block if needed); more advanced users, such as those wishing to add a node to a child list conditionally based on the existence of another node in another list, may explicitly run such code within the children mutex, being careful to choose write access if there is any possibility of a change being made in that thread.

(Recall that non-hierarchy-related node operations, such as manipulation of node properties, do not require this mutex and ought not use it unless otherwise needed.)

There is a special mutex which has an interesting implementation, namely to synchronize with the AWT event queue. Mutex.EVENT makes no distinction between read and write access, permitting only one thread at a time in each. Mutex.EVENT.readAccess(Runnable) (or Mutex.EVENT.writeAccess(Runnable)) has an effect equivalent to SwingUtilities.invokeLater, while Mutex.EVENT.readAccess(Mutex.Action) (or Mutex.EVENT.writeAccess(Mutex.Action)) is equivalent to SwingUtilities.invokeAndWait. However, if the calling thread is already in the event queue, this mutex executes the code immediately (rather than waiting for other pending events to be processed).

The advantage of Mutex.EVENT over directly calling SwingUtilities (besides the special behavior for code already in the event queue) is that it is possible to construct a large amount of code to expect some mutex, and then quickly switch between the event-queue mutex and some independent mutex. This was used to advantage when creating an independent mutex for node children (which previously had been an alias for the event-queue mutex)!

Engines

There are a couple places in the APIs where certain types of operations are clearly expected to take macroscopic time, if they terminate at all, and thus have a special isolated threading model. This applies to compiling and executing. In both cases, the caller-side API is completely thread-safe (all API calls should return quickly and not affect other threads). However, before being run, implementation code is first safely wrapped in an implementation-defined engine whose exact details are not visible from an API perspective.

For example, the implementation of CompilationEngine accepts a compiler job prepared by caller-side code, and then begins running it (calling CompilerJob.start() and so on) in a totally separate thread group, insulated from the calling thread and the event queue both. ExecutionEngine behaves similarly for execution. (Note: some compilers and executors may additionally run external processes, and would typically block on the Java process to exit.)

All communication with such running processes is mediated through the engine, which typically consists of events being fired through the engine, and more importantly in the caller-side code being returned some subclass of Task which may be used to control the process in a simple way (see below).

Since these engine-driven processes are run quite independently of other threads in the IDE, they must not block on such threads. It is fine, however, for them to access primitive APIs such as FileSystems and DataSystems, since such accesses can safely coexist with accesses from elsewhere in the IDE.

The execution engine runs all of its executed tasks independently of one another, i.e. it is possible to be test-running multiple applications simultaneously. However, the compilation engine (in its current implementation) imposes its own threading structure on jobs:

  1. All jobs are enqueued and executed sequentially, so a compiler group started within a job must not wait for a different job to terminate, or the engine will deadlock.
  2. Within a job there are one or more compilation levels (each consisting of one or more groups); levels are executed sequentially in their natural order, as they represent the results of compiler dependency analysis.
  3. Within a level, several groups may be run simultaneously to exploit hardware parallelism if available.
  4. Each group is responsible for its own execution, and typically will run one or more chores (possibly using the execution engine) sequentially in a single thread.
API users can at least be assured that compilation levels will be run in the proper order.

Note that the Debugger API is technically completely thread-safe; this is possible since there is no implementation to speak of in it! In practice, a debugger would generally be an external process, which means it would use the execution engine to manage its progress; it is completely up to the debugger implementation to be thread-safe and responsive, as there is no special engine for it to use.

Tasks

For explicit control over the performance of miscellaneous long-running tasks, which require their own threads, the APIs provides the utility class Task which occurs throughout the APIs as the result of various asynchronous operations. In and of itself it does not do much interesting except provide thread-safe status information, an exit status, and the ability to fire termination events.

However, it is typically used in conjunction with a RequestProcessor which does provide a separate thread or thread group to contain the running tasks, as well as other functionality including task priorities, and delayed scheduling (with the option to cancel a pending job). Although callers can create their own request processors, typical usage is to use a single instance in the system with RequestProcessor.postRequest(Runnable); this ensures that all long-running code in the IDE (parsing, internal database updates, and so forth) is serialized and prioritized, rather than contending all at once for processor time and triggering virtual memory thrashes (as might result from the naive use of background threads).

It is also useful to post a request to the processor in the middle of GUI code, if the request might take a perceptible amount of time to complete.

Division of APIs into Domains

The Open APIs are divided into threading domains as follows:
Modules
Unsynchronized (though there is little implementation in this API). Most code to really handle modules is present in the private implementation, and thus has no documented model.
TopManager and Places
Generally thread-safe; TopManager.notify(...) and similar calls (customize and select nodes) use the event queue in GUI mode, however, and is assumed to block (so do not hold any locks when you call it). TopManager.notifyException(...) is thread-safe.
FileSystems
Thread-safe.
DataSystems
Thread-safe. But org.openide.util.datatransfer is unsynchronized.
Explorer
Event queue only. Includes event-queue-safe interface to node hierarchy.
Nodes
Thread-safe, with the option to use a global mutex as needed for hierarchy operations.
Actions
Threading is handled by the ActionManager which is itself normally used from the event thread. The action manager should ensure that actions are run in a fashion appropriate for the IDE mode. For example, in GUI mode no two actions should be running in the same thread at the same time. In non-GUI mode, probably all actions would be run synchronously. Actions which definitely require the event thread should use it; most actions will either not take much time all told (e.g. SaveAction); create a GUI object and return to the caller (e.g. InstantiateAction); create a modal dialog and block on it (or whatever TopManager.notify and similar do; e.g. CustomizeAction); or invoke some process in an engine (e.g. ExecuteAction).
Window System
Event queue.
Options
Thread-safe.
Compiler
Thread-safe for the outer API (caller-side); engine used for inner API code.
Execution
As for Compiler.
Debugger
Unspecified in the APIs (no implementation) - assumed thread-safe.
Editor
Mostly thread-safe. Text documents should be locked using special locking provisions if desired. Document preparation may also be run as a task. Contains a default editor pane implementation which is displayed in the event queue, though document-related operations are still thread-safe.
Java Hierarchy
Thread-safe. Parsing may be run as a task. Element nodes are subject to the same threading model as any other nodes.
Utility Classes
Variable: org.openide.awt should be used from the event queue; org.openide.util.enum and org.openide.util.io are unsynchronized. org.openide.util is thread-safe.

Built on December 12 2001.  |  Portions Copyright 1997-2001 Sun Microsystems, Inc. All rights reserved.