Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can anonymous classes be completely immutable?

In the book Java Concurrency In Practice, there is this example of an almost immutable object which is at risk of failure if not properly published:

// Taken from Java Concurrency In Practice
// p.51 Listing 3.15: Class at risk of failure if not properly published.
public class Holder {
    private int n;

    public Holder(int n) { this.n = n; }

    public void assertSanity() {
        if(n != n)
            throw new AssertionError("This statement is false.");
    }
}

// p.50 Listing 3.14: Publishing an object without adequate synchronization. Don't do this.
class Client {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

If I understand the chapter in the book correctly, adding final to the n field of the Holder class will make the object completely immutable and eliminate the chance of getting the AssertionError thrown even if it's still published without adequate synchronization like it's done in the Client class.

Now I'm wondering how anonymous classes behave in this respect. Please see the following example:

public interface IHolder {
    void assertSanity();
}

class IHolderFactory {
    static IHolder create(int n) {
        return new IHolder() {
            @Override
            public void assertSanity() {
                if (n != n)
                    throw new AssertionError("This statement is false.");
            }
        };
    }
}

class IHolderClient {
    public IHolder holder;

    public void initialize() {
        // is this safe?
        holder = IHolderFactory.create(42);
    }
}

It's published without adequate synchronization just like in the example from the book, but the difference is that now the Holder class has become an interface and there is a static factory method which returns an anonymous class implementing the interface, and the anonymous class uses the method parameter n.

My question is: is there any chance of getting the AssertionError from my latter example thrown? If there is, what is the best way to make it completely immutable and eliminate the problem? Does it change something if it was written in a functional way like the following?

class IHolderFactory {
    static IHolder create(int n) {
        return () -> {
            if (n != n)
                throw new AssertionError("This statement is false.");
        };
    }
}
like image 970
Kohei Nozaki Avatar asked Mar 16 '21 09:03

Kohei Nozaki


2 Answers

This is a very tricky issue.

JLS, §17.4.1. Shared Variables says:

Local variables (§14.4), formal method parameters (§8.4.1), and exception handler parameters (§14.20) are never shared between threads and are unaffected by the memory model.

This seems to contradict the fact that you can use them within an inner class or lambda expression that can be shared between threads, but those constructs capture the value of the variable and use the value. This process, however, is not very well specified.

The only mentioning I could ever find, is in §15.27.2 explaining the (effective) final requirement:

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

In practice, the captured values are stored in synthetic final fields of the inner class or the class generated at runtime for the lambda expression. So you will never see the error with the current implementation.

This, however, is not specified anywhere. The language specification says little about the bytecode format and the virtual machine specification says little about the language constructs.

So, local variables, formal method parameters, and exception handler parameters are explicitly excluded from the JMM and their captured values are not variables in the JMM’s regard and not even mentioned there. The question is what does that mean.

Are they generally immune to data races (my interpretation) or are they unsafe and we do no get any guaranty from the JMM at all? In the latter case, it would even imply that we were not able to make them safe, as any safe publication mechanism gets its safety from guarantees of the JMM which does not cover our case. It’s worth noting that the JMM also does not cover the outer this reference nor an instance’s implicit reference to a Class object returned by getClass().

So while I’d consider them immune to data races, I wish that was specified more explicit.

like image 101
Holger Avatar answered Oct 12 '22 13:10

Holger


It does not matter if you use an anonymous class or lambda, you have zero synchronization mechanisms here to correctly publish the reference; as such, this code can throw that Exception.

In other words, there are tools and conditions that you must meet so that your code is safe: these are using final, volatile or some sort of synchronized or locks, etc. Since you use none, no guarantees are provided.

The Java Language Specification offers these guarantees only when you use special semantics, like final that you have shown in the first example. There are others too, but making an object immutable is the simplest, most trivial way. This is the best article that I am aware of on this subject, you might want to read it.

like image 40
Eugene Avatar answered Oct 12 '22 13:10

Eugene