0

In the Java Concurrency in Practice book, it says that

"Static initializers are run by the JVM at class initialization time, after class loading but before the class is used by any thread. Because the JVM acquires a lock during initialization [JLS 12.4.2] and this lock is acquired by each thread at least once to ensure that the class has been loaded, memory writes made during static initialization are automatically visible to all threads."(Goetz 16.2.3)

Idea 1: first interpretation

I first thought that meant that the JVM decides upon seeing some class using the static field, to temporarily have all threads try to acquire the lock used by the static initialization and if that lock was never let go, then it would halt all threads to wait for that lock forever.

Idea 2: possible interpretation that makes more sense especially with how the sample code is behaving

If it was the case that, only after the static field was initialized, then the JVM has all threads try to acquire the lock used by the static initialization this would be fine. The other threads that wasn't the first to use the static field, would be just fine and not halt because it's not waiting on the lock. However, I am not sure if that is the case. Can anyone confirm that idea 2 is the correct interpretation?

Finally here is the program which looks something like this and keeps on printing thread-0 and thread-1

public class StaticBlockAndLineInterpretation {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> keepLooping()).start();
        new Thread(() -> keepLooping()).start();
        Thread.sleep(2500);
        int x = AllThreadsStopper.threadStopper;
    }

    static void keepLooping() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            System.out.println("This is thread " + Thread.currentThread().getName());
        }
    }
}

class AllThreadsStopper {
    static int threadStopper;

    static {
        try {
            threadStopper = haltAllThreadsAndNeverReturn();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static int haltAllThreadsAndNeverReturn() throws InterruptedException {
        System.out.println("haltAllThreadsAndNeverReturn called");
        new CountDownLatch(1).await();
        return 0;
    }
}


console output snippet:
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0
    This is thread Thread-1
    haltAllThreadsAndNeverReturn called
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0 and so forth...
katiex7
  • 863
  • 12
  • 23
  • 1
    “*The other threads that wasn't the first to use the static field*…”—in your example code, the other threads *never* use the static field, so it’s not about being the first or not. – Holger Feb 23 '18 at 17:31

1 Answers1

3

The cited section is, of course, about threads using that class, as otherwise, without shared data, there is no need to discuss thread safety.

JLS§12.4.2 describes acquiring the lock as part of the initialization procedure of the class, where threads have to go into the blocked state when they detect that another thread is currently performing the initialization:

For each class or interface C, there is a unique initialization lock LC. The mapping from C to LC is left to the discretion of the Java Virtual Machine implementation. The procedure for initializing C is then as follows:

  1. Synchronize on the initialization lock, LC, for C. This involves waiting until the current thread can acquire LC.
  2. If the Class object for C indicates that initialization is in progress for C by some other thread, then release LC and block the current thread until informed that the in-progress initialization has completed, at which time repeat this step.
  3. If the Class object for C indicates that initialization is in progress for C by the current thread, then this must be a recursive request for initialization. Release LC and complete normally.
  4. If the Class object for C indicates that C has already been initialized, then no further action is required. Release LC and complete normally.

Note that this implies doing nothing if the class turns out to be already initialized (in 4.), but still acquiring and releasing the lock, which is the formal definition of the memory visibility constraint and happens-before relationship which Brian Goetz mentioned.

But being part of the formal definition of class initialization, it only applies to code actually triggering a class initialization, which is specified in JLS §12.4.1., “When Initialization Occurs”:

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • T is a class and an instance of T is created.
  • A static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).
  • T is a top level class (§7.6) and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.

When a class is initialized, its superclasses are initialized (if they have not been previously initialized), as well as any superinterfaces (§8.1.5) that declare any default methods (§9.4.3) (if they have not been previously initialized).

Since in your case, the two threads are not performing any of the specified actions, not even indirectly, they are not triggering a class initialization, hence, not attempting to acquire the class initialization lock.

You can easily cause blocking of your example’s threads by inserting an action from the list, e.g.

static void keepLooping() {
    while (true) {
        try {
            Thread.sleep(1000);
            new AllThreadsStopper();
        } catch (InterruptedException e) {}
        System.out.println("This is thread " + Thread.currentThread().getName());
    }
}

Since creating of an instance of a class triggers the initialization of the class, the threads now get blocked.

For completeness, §12.4.2. also mentions:

An implementation may optimize this procedure by eliding the lock acquisition in step 1 (and release in step 4/5) when it can determine that the initialization of the class has already completed, provided that, in terms of the memory model, all happens-before orderings that would exist if the lock were acquired, still exist when the optimization is performed.

Which is what Brian Goetz’ book is about when he says “This technique can be combined with the JVM’s lazy class loading to create a lazy initialization technique that does not require synchronization on the common code path”. It’s very efficient, as threads may access the initialized class without synchronization costs, once the initialization has completed. In your example, where the initialization never completes, this optimization is not possible and threads have to acquire the lock, if they are using the class in terms of JLS §12.4.1.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Yes, after reading your post's example I realized I had misinterpreted what Goetz has said in his book "the JVM acquires a lock during initialization [JLS 12.4.2] and this lock is acquired by each thread at least once to ensure that the class has been loaded". I misunderstood the meaning of initialization, but not anymore since you you had let me know through quoting the actual JLS. – katiex7 Feb 23 '18 at 18:36
  • Like you said, only the main thread was doing the initialization in my code, so it would block, but the other threads were not, so they don't, but in your example they do block because of initialization triggered by "making an instance of AllThreadsStopper" My idea 1 and 2 was mistaken and not the message Goetz was trying to convey, thanks for clearing that up – katiex7 Feb 23 '18 at 18:37