JavaSwingText components and Document: Documents and multiple threads


Documents and multiple threads

Document is supposed to allow multithreading. Unfortunately, it doesn't work the way it is now, and will never with Document alone.

(Some of the following is not true since JDK1.4beta2, see below)

Read locking (Shared)

While having a read lock, a thread may assume that the Document doesn't change. If another thread is currently writing to the Document, the thread that wants to acquire a read lock will block until the write is finished (released its write lock). It may be that accessing individual properties (like calling getLength()) implicitly obtains a read lock (or is atomic, so that it isn't needed), but by the time the method returns, the result needn't be correct any more, so there is little you can do with it, except if you only want a snapshot of the property. It may be that these methods crash because they cannot handle another thread changing the property underneath. So to read a Document's state, in theory you should always have a read lock (but access without read locks happens all over the place).

It is possible for a Document to give multiple threads a read lock, since they won't interfere.

There is a method for that: the unfortunately named "render". The Runnable given to render() is executed while the Document is locked (if it supports locking at all). It may assume the Document doesn't change.

Creating a Runnable class for every kind of inspection you would want to do is very clumsy. Possibly that's why AbstractDocument adds readLock() and readUnlock(), which allow to get a read lock without having to create a Runnable for the task to be done. render() then just calls readLock(); runnable.run(); readUnlock(); with exception handling to always release the lock. All Swing code only uses this (if the Document is an AbstractDocument) and not render(), thus it only locks at all for AbstractDocument.

Write locking (Exclusive)

To change the Document, you need a write lock. That will ensure that a) no multiple threads write at the same time and either get confused by Document changes between calls or even because the modification methods don't handle being called multiple times at once; and b) no other thread has a read lock at the same time. The thread that wants to write will block until there are no more readers or/and until a previous writer released its lock.

There is no way to acquire a write lock in Document!

Even if one assumes that insertString() and remove() get a write lock (which they do in AbstractDocument), that is only useful for single modifications, which, most importantly, do not cover replacements (which, by the way, aren't supported atomically by Document, and can never be because of the structure of DocumentEvent).

Furthermore, you cannot even call insertString() and remove() in a thread-safe way (except for giving index 0, and length 0) because by the time these methods internally get a write lock, the indices may not be valid anymore (or at least represent some different position in the Document than the one you meant to modify). Either there had to be methods to get/release a write lock; which you need to have before calculating the index arguments; or it should be possible to upgrade the read lock to a (or the implicit) write lock (if necessary, wait until the reader is the only one). The former is of course a lot better, since it also covers multiple modifications, the latter might be useful anyway, but will deadlock if multiple threads do this (because all threads wait to get a write lock, which they can only get when all other threads release their read lock, which they don't want to do), so it cannot be granted unconditionally.

AbstractDocument will deadlock when you try to get a write lock from a Thread that already has a read lock (wait for the reader to finish, which is never does, since it waits for the write lock).

import javax.swing.text.*;

class DeadLock
{
    public static void main(String[] args)
        throws Exception
    {
        PlainDocument d = new PlainDocument();

        d.insertString(0, "ABC", null);

        d.readLock();

        System.err.println("Read-locked");

        d.remove(0, 3);

        d.readUnlock();

        System.err.println("Unlocked");
    }
}

Until recently (see below), AbstractDocument further didn't allow a thread that already has a write lock to get it a second time (See also bug report evaluation (#4458513)). That means about all subclasses that override insertString() or remove() and access any properties of the document, or do multiple calls to super.insertString(), super.remove() or other modification methods couldn't be made thread-safe any more. They would have to do:

public void insertString(int index, String s, AttributeSet a)
{
    {
        writeLock();
    }
    try
    {
        if (getLength() ...)
           super.insertString(...);
    }
    finally
    {
        writeUnlock();
    }
}

but now super.insertString() will throw an IllegalStateException because the lock cannot be obtained multiple times.

Here is an example of insertString() that loses its thread safety or at least doesn't fulfil its function any more if multiple threads are involved:

public void insertString(int index, String s, AttributeSet a)
    throws BadLocationException
{
    if (s == null) return;

    int strLen = s.length();

    if (strLen == 0) return;

    int len = getLength();

    if (strLen + len > maxLength)
        s = s.substring(0, maxLength - len);

// But now getLength() may have changed since the call above!
// One may easily end up with a Document with more than the allowed
// number of characters.

    super.insertString(index, s, a); // only in the superclass is the writeLock obtained.
}

This isn't a real problem though, the calls to insertString() and remove() cannot have been thread-safe at all (except for inserting at the beginning, or removing nothing, which isn't really useful). In all other cases (i.e. except for inserting at index 0), the calling code must have a had a write lock before the call (which is still impossible directly the way it is now).

Ignoring that boundary case (There also are setLogicalStyle(0, ...), setParagraphAttributes(0, 0, ...) in StyledDocument and the protected insert(0, ...) in DefaultStyledDocument.), it isn't even necessary for the modification method to get a write lock on its own, since the calling code already must have had one to perform the call in the first place (which is not possible by default)

setText

JDK1.4beta2

Finally, the AbstractDocument write lock can be acquired multiple times, so one can introduce methods in a subclass that do multiple calls to Document methods while the Document is locked (so nothing may change inbetween).

Still writeLock() and writeUnlock() are protected final. They only make sense if they are public. Otherwise, one call still almost not (see above) call any method with correct arguments if they are public. That can be added simply in this way:

public void lock()
{
    writeLock();
}

public void unlock()
{
    writeUnlock();
}

Still, for the typical use case of multiple threads, loading a document in a background thread, this is not enough. That requires that the background thread holds a read lock all the time that is upgraded to a write lock when it is actually writing. As it is now, as soon as the write lock is released, other threads can write into the document (like the event-dispatch thread when the user enters text). During background-loading that should not be possible or the loading thread loses control over where and how the loaded content should be inserted (and it need not even make sense anymore with the changes that other threads have done). During loading, no other threads may be allowed to write, but in the intervals when the background thread isn't writing, they should be allowed to read, or background loading is pointless in the first place, since the event-dispatch thread will block as soon as it tries to read the document.


(C) 2001-2009 Christian Kaufhold (swing@chka.de)