Threading Model
The IntelliJ Platform is a highly concurrent environment. Code is executed in many threads simultaneously. In general, as in a regular Swing application, threads can be categorized into two main groups:
Event Dispatch Thread (EDT) – also known as the UI thread. Its main purpose is handling UI events (such as reacting to clicking a button or updating the UI), but the platform uses it also for writing data. EDT executes events taken from the Event Queue. Operations performed on EDT must be as fast as possible to not block other events in the queue and freeze the UI. There is only one EDT in the running application.
background threads (BGT) – used for performing long-running and costly operations, or background tasks
It is possible to switch between BGT and EDT in both directions. Operations can be scheduled to execute on EDT from BGT (and EDT) with invokeLater()
methods (see the rest of this page for details). Executing on BGT from EDT can be achieved with background processes.
Read-Write Lock
The IntelliJ Platform data structures (such as Program Structure Interface, Virtual File System, or Project root model) aren't thread-safe. Accessing them requires a synchronization mechanism ensuring that all threads see the data in a consistent and up-to-date state. This is implemented with a single application-wide read-write (RW) lock that must be acquired by threads requiring reading or writing to data models.
If a thread requires accessing a data model, it must acquire one of the locks:
Read Lock | Write Intent Lock | Write Lock |
---|---|---|
Allows a thread for reading data. | Allows a thread for reading data and potentially upgrade to the write lock. | Allows a thread for reading and writing data. |
Can be acquired from any thread concurrently with other read locks and write intent lock. | Can be acquired from any thread concurrently with read locks. | Can be acquired only from EDT concurrently with a write intent lock acquired on EDT. |
Can't be acquired if write lock is held on another thread. | Can't be acquired if another write intent lock or write lock is held on another thread. | Can't be acquired if any other lock is held on another thread. |
The following table shows compatibility between locks in a simplified form:
Read Lock | Write Intent Lock | Write Lock | |
---|---|---|---|
Read Lock | |||
Write Intent Lock | |||
Write Lock |
The described lock characteristics conclude the following:
multiple threads can read data at the same time
once a thread acquires the write lock, no other threads can read or write data
Acquiring and releasing locks explicitly in code would be verbose and error-prone and must never be done by plugins. The IntelliJ Platform enables write intent lock implicitly on EDT (see Locks and EDT for details) and provides an API for accessing data under read or write locks.
Locks and EDT
Although acquiring all types of locks can be, in theory, done from any threads, the platform implicitly acquires write intent lock and allows acquiring the write lock only on EDT. It means that writing data can be done only on EDT.
The scope of implicitly acquiring the write intent lock on EDT differs depending on the platform version:
Write intent lock is acquired automatically when operation is invoked on EDT with Application.invokeLater()
.
Write intent lock is acquired automatically when operation is invoked on EDT with methods such as:
and other similar methods
It is recommended to use Application.invokeLater()
if the operation is supposed to write data. Use other methods for pure UI operations.
Accessing Data
The IntelliJ Platform provides a simple API for accessing data under read or write locks in a form of read and write actions.
Read and write actions allow executing a piece of code under a lock, automatically acquiring it before an action starts, and releasing it after the action is finished.
Read Actions
API
ReadAction
run()
orcompute()
:val psiFile = ReadAction.compute<PsiFile, Throwable> { // read and return PsiFile }PsiFile psiFile = ReadAction.compute(() -> { // read and return PsiFile });
Alternative APIs
- val psiFile = ApplicationManager.application.runReadAction { // read and return PsiFile }PsiFile psiFile = ApplicationManager.getApplication() .runReadAction((Computable<PsiFile>)() -> { // read and return PsiFile });
Note that this API is considered low-level and should be avoided.
Kotlin
runReadAction()
:val psiFile = runReadAction { // read and return PsiFile }Note that this API is obsolete since 2024.1. Plugins implemented in Kotlin and targeting versions 2024.1+ should use suspending
readAction()
. See also Coroutine Read Actions.
Rules
Reading data is allowed from any thread.
Reading data on EDT invoked with Application.invokeLater()
doesn't require an explicit read action, as the write intent lock allowing to read data is acquired implicitly.
Reading data is allowed from any thread.
Reading data on EDT doesn't require an explicit read action, as the write intent lock allowing to read data is acquired implicitly.
In all other cases, it is required to wrap read operation in a read action with one of the API methods.
Objects Validity
The read objects aren't guaranteed to survive between several consecutive read actions. Whenever starting a read action, check if the PSI/VFS/project/module is still valid. Example:
Between executing first and second read actions, another thread could invalidate the virtual file:
Write Actions
API
WriteAction
run()
orcompute()
:WriteAction.run<Throwable> { // write data }WriteAction.run(() -> { // write data });
Alternative APIs
- ApplicationManager.application.runWriteAction { // write data }ApplicationManager.getApplication().runWriteAction(() -> { // write data });
Note that this API is considered low-level and should be avoided.
Kotlin
runWriteAction()
:runWriteAction { // write data }Note that this API is obsolete since 2024.1. Plugins implemented in Kotlin and targeting versions 2024.1+ should use suspending
writeAction()
.
Rules
Writing data is only allowed on EDT invoked with Application.invokeLater()
.
Write operations must always be wrapped in a write action with one of the API methods.
Modifying the model is only allowed from write-safe contexts (see Invoking Operations on EDT and Modality).
Writing data is only allowed on EDT.
Write operations must always be wrapped in a write action with one of the API methods.
Modifying the model is only allowed from write-safe contexts, including user actions and SwingUtilities.invokeLater()
calls from them (see Invoking Operations on EDT and Modality).
Modifying PSI, VFS, or project model from inside UI renderers or SwingUtilities.invokeLater()
calls is forbidden.
Invoking Operations on EDT and Modality
Operations that write data on EDT should be invoked with Application.invokeLater()
because it allows specifying the modality state (ModalityState
) for the scheduled operation. This is not supported by SwingUtilities.invokeLater()
and similar APIs.
ModalityState
represents the stack of active modal dialogs and is used in calls to Application.invokeLater()
to ensure the scheduled runnable can execute within the given modality state, meaning when the same set of modal dialogs or a subset is present.
To better understand what problem ModalityState
solves, consider the following scenario:
A user action is started.
In the meantime, another operation is scheduled on EDT with
SwingUtilities.invokeLater()
(without modality state support).The action from 1. now shows a dialog asking a Yes/No question.
While the dialog is shown, the operation from 2. is now processed and does changes to the data model, which invalidates PSI.
The user clicks Yes or No in the dialog, and it executes some code based on the answer.
Now, the code to be executed as the result of the user's answer has to deal with the changed data model it was not prepared for. For example, it was supposed to execute changes in the PSI that might be already invalid.
Passing the modality state solves this problem:
A user action is started.
In the meantime, another operation is scheduled on EDT with
Application.invokeLater()
(supporting modality state). The operation is scheduled withModalityState.defaultModalityState()
(see the table below for other helper methods).The action from 1. now shows a dialog asking a Yes/No question. This adds a modal dialog to the modality state stack.
While the dialog is shown, the scheduled operation waits as it was scheduled with a "lower" modality state than the current state with an additional dialog.
The user clicks Yes or No in the dialog, and it executes some code based on the answer.
The code is executed on data in the same state as before the dialog was shown.
The operation from 1. is executed now without interfering with the user's action.
The following table presents methods providing useful modality states to be passed to Application.invokeLater()
:
Description | |
---|---|
Used if none specified | If invoked from EDT, it uses the If invoked from a background process started with This is the optimal choice in most cases. |
| The operation can be executed when the modality state stack doesn't grow since the operation was scheduled. |
| The operation can be executed when the topmost shown dialog is the one that contains the specified component or is one of its parent dialogs. |
| The operation will be executed after all modal dialogs are closed. If any of the open (unrelated) projects displays a per-project modal dialog, the operation will be performed after the dialog is closed. |
| The operation will be executed as soon as possible regardless of modal dialogs (the same as with Don't use it unless absolutely needed. |
Read Action Cancellability
BGT shouldn't hold read locks for a long time. The reason is that if EDT needs a write action (for example, the user types something in the editor), it must be acquired as soon as possible. Otherwise, the UI will freeze until all BGTs have released their read actions. The following diagram presents this problem:
Sometimes, it is required to run a long read action, and it isn't possible to speed it up. In such a case, the recommended approach is to cancel the read action whenever there is a write action about to occur and restart that read action later from scratch:
In this case, the EDT won't be blocked and the UI freeze is avoided. The total execution time of the read action will be longer due to multiple attempts, but not affecting the UI responsiveness is more important.
The canceling approach is widely used in various areas of the IntelliJ Platform: editor highlighting, code completion, "go to class/file/…" actions all work like this. Read the Background Processes section for more details.
Cancellable Read Actions API
To run a cancellable read action, use one of the available APIs:
ReadAction.nonBlocking()
which returnsNonBlockingReadAction
(NBRA). NBRA handles restarting the action out-of-the-box.ReadAction.computeCancellable()
which computes the result immediately in the current thread or throws an exception if there is a running or requested write action.
In both cases, when a read action is started and a write action occurs in the meantime, the read action is marked as canceled. Read actions must check for cancellation often enough to trigger actual cancellation. Although the cancellation mechanism may differ under the hood (Progress API or Kotlin Coroutines), the cancellation handling rules are the same in both cases.
Always check at the start of each read action if the objects are still valid, and if the whole operation still makes sense. With ReadAction.nonBlocking()
, use expireWith()
or expireWhen()
for that.
Avoiding UI Freezes
Don't Perform Long Operations on EDT
In particular, don't traverse VFS, parse PSI, resolve references, or query indexes.
There are still some cases when the platform itself invokes such expensive code (for example, resolve in AnAction.update()
), but these are being worked on. Meanwhile, try to speed up what you can in your plugin as it will be generally beneficial and will also improve background highlighting performance.
Action Update
For implementations of AnAction
, plugin authors should specifically review the documentation of AnAction.getActionUpdateThread()
in the Actions section as it describes how threading works for actions.
Minimize Write Actions Scope
Write actions currently have to happen on EDT. To speed them up, as much as possible should be moved out of the write action into a preparation step which can be then invoked in the background or inside an NBRA.
Slow Operations on EDT Assertion
Some of the long operations are reported by SlowOperations.assertSlowOperationsAreAllowed()
. According to its Javadoc, they must be moved to BGT. This can be achieved with the techniques mentioned in the Javadoc, background processes, Application.executeOnPooledThread()
, or coroutines (recommended for plugins targeting 2024.1+). Note that the assertion is enabled in IDE EAP versions, internal mode, or development instance, and regular users don't see them in the IDE. This will change in the future, so fixing these exceptions is required.
Event Listeners
Listeners mustn't perform any heavy operations. Ideally, they should only clear some caches.
It is also possible to schedule background processing of events. In such cases, be prepared that some new events might be delivered before the background processing starts – and thus the world might have changed by that moment or even in the middle of background processing. Consider using MergingUpdateQueue
and NBRA to mitigate these issues.
VFS Events
Massive batches of VFS events can be pre-processed in the background with AsyncFileListener
.
FAQ
How to check whether the current thread is the EDT/UI thread?
Use Application.isDispatchThread()
.
If code must be invoked on EDT and the current thread can be EDT or BGT, use UIUtil.invokeLaterIfNeeded()
. If the current thread is EDT, this method will run code immediately, or will schedule a later invocation if the current thread is BGT.
Why write actions are currently allowed only on EDT?
Reading data model was often performed on EDT to display results in the UI. The IntelliJ Platform is more than 20 years old, and in its beginnings Java didn't offer features like generics and lambdas. Code that acquired read locks was very verbose. For convenience, it was decided that reading data can be done on EDT without read locks (even implicitly acquired).
The consequence of this was that writing had to be allowed only on EDT to avoid read/write conflicts. The nature of EDT provided this possibility out-of-the-box due to being a single thread. Event queue guaranteed that reads and writes were ordered and executed one by one and couldn't interweave.
Why can write intent lock be acquired from any thread but write lock only from EDT?
In the current platform state, technically, write intent lock can be acquired on any thread (it is done only on EDT in practice), but write lock can be acquired only on EDT.
Write intent lock was introduced as a "replacement" for EDT in the context of acquiring write lock. Instead of allowing to acquire write lock on EDT only, it was planned to make it possible to acquire it from under write intent lock on any thread. Write intent lock provides read access that was also available on EDT. This behavior wasn't enabled in production, and the planned locking mechanism has changed. It is planned to allow for acquiring write lock from any thread, even without a write intent lock. Write intent lock will be still available and will allow performing read sessions finished with data writing.