Skip to content

mtumilowicz/java12-concurrency-singleton-pattern-workshop

Repository files navigation

Build Status License: GPL v3

java12-concurrency-singleton-workshop

preface

  • goals of this workshop:
    • basics of object publication in the multithreaded environment
    • immutability in the context of thread safety
    • basics threats while sharing objects in multithreaded environment
    • become acquainted with double-checked locking
  • workshop: workshop package, answers: answers package

introduction

  • the risks of improper publication are consequences of the absence of a happens-before ordering between publishing a shared object and accessing it from another thread
  • unfortunately, simply storing a reference to an object into a public field, is not enough to publish that object safely
  • improper publication could allow another thread to observe a partially constructed object
    // Unsafe publication
    public Holder holder;
    
    public void initialize() {
        holder = new Holder(42);
    }
    
    // setters, getters
    public class Holder {
        private int n;
        public Holder(int n) { this.n = n; }
    
        // a thread may see a stale value (ex. default - 0) the first time it reads a field and then a 
        // more up-to-date value the next time, which is why assertSanity can throw AssertionError
        public void assertSanity() {
            if (n != n) {
                throw new AssertionError("This statement is false.");
            }
        }
    }
    
    • two things can go wrong
      • other threads could see a null reference
      • other threads could see an up-to-date value for the holder reference, but stale values for the state of the Holder
        public void initialize() {
            var holder = new Holder(42);
            holder.setN(50);
            this.holder = holder;
        }
        
        could be reordered to
        public void initialize() {
            var holder = new Holder(42);
            this.holder = holder;
            holder.setN(50);
        }
        

unsafe publication

  • the possibility of reordering in the absence of a happens-before relationship explains why publishing an object without adequate synchronization can allow another thread to see a partially constructed object
  • unsafe publication can happen as a result of an incorrect lazy initialization
    @NotThreadSafe
    public class UnsafeLazyInitialization {
        private static Resource resource;
        public static Resource getInstance() {
            if (resource == null) {
                resource = new Resource(); // unsafe publication
            }
            return resource;
        }
    }
    
    • note that: resource = new Resource() is not atomic - it something like
      local = calloc(sizeof(Resource.class)); // allocate memory
      local.<init>(); // constructor
      helper = local; // assign to field
      
    • since neither thread used synchronization, B could possibly see A’s actions in a different order than A performed them
    • so even though A initialized the Resource before setting resource to reference it, B could see the write to resource as occurring before the writes to the fields of the Resource
  • with the exception of immutable objects, it is not safe to use an object that has been initialized by another thread unless the publication happens-before the consuming thread uses it

safe publication

  • objects that are not immutable must be safely published, which usually entails synchronization by both the publishing and the consuming thread
  • to publish an object safely, both the reference to the object and the object’s state must be made visible to other threads at the same time
  • a properly constructed object can be safely published by:
    • initializing an object reference from a static initializer
      • static initializers are run by the JVM at class initialization time, after class loading but before the class is used by any thread
      • static initializers are executed by the JVM at class initialization time
        • because of internal synchronization in the JVM, this mechanism is guaranteed to safely publish any objects initialized in this way [JLS 12.4.2]
    • storing a reference to it into a volatile field or AtomicReference
    • storing a reference to it into a final field of a properly constructed object
    • storing a reference to it into a field that is properly guarded by a lock

case studies

singleton eager initialization

class EagerSingleton {
    private static Resource resource = new Resource();

    public static Resource getResource() {
        return resource;
    }
}

singleton lazy initialization

  • it sometimes makes sense to defer initialization of objects that are expensive to initialize until they are actually needed
  • the treatment of static fields with initializers (or fields whose value is initialized in a static initialization block is somewhat special and offers additional thread-safety guarantees
@ThreadSafe
public class SafeLazyInitialization {
    private static Resource resource;
    public synchronized static Resource getInstance() { // do we need this synchronization?
        if (resource == null)
            resource = new Resource();
        return resource;
    }
}
@ThreadSafe
public class LazySingleton {
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }
    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}
  • the first call to getResource by any thread causes ResourceHolder to be loaded and initialized (the JVM defers initializing the ResourceHolder class until it is actually used [JLS 12.4.1]), at which time the initialization of the Resource happens through the static initializer

double-checked locking

@NotThreadSafe
class DoubleCheckedLockingSingleton {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (resource == null)
                    resource = new Resource();
            }
        }
        return resource;
    }
}
  • creating with constructor is not an atomic operation
    Point sharedPoint = new Point(x, y);
    
    // compiled into
    local1 = calloc(sizeof(Point));
    local1.<init>(x, y);
        Object.<init>();
        this.x = x;
        this.y = y;
    sharedPoint = local1;
    
    • operations could be reordered
      Point sharedPoint = new Point(x, y);
      
      // compiled into
      local1 = calloc(sizeof(Point));
      sharedPoint = local1;
      ...
      
  • DCL after compilation and reordering
    @NotThreadSafe
    class DoubleCheckedLockingSingleton {
        private static Resource resource;
    
        public static Resource getInstance() {
            if (resource == null) {
                synchronized (DoubleCheckedLockingSingleton.class) {
                    if (resource == null) {
                        local = calloc(sizeof(Singleton));
                        sharedInstance = local; // sharedInstance is non-null, but constructor hasn't run
                        local.<init>();
                    }
                }
            }
            return resource;
        }
    }
    
  • the real problem with DCL is the assumption that the worst thing that can happen when reading a shared object reference without synchronization is to erroneously see a stale value (in this case, null)
  • but the worst case - it is possible to see a current value of the reference but stale values for the object’s state - object could be seen to be in an invalid or incorrect state
  • the lazy initialization holder idiom offers the same benefits and is easier to understand

immutability context

  • without initialization safety, immutable objects like String can change their value (in case of no synchronization)
  • security architecture relies on the immutability of String
    • lack of initialization safety could create security vulnerabilities
  • initialization safety guarantees that for properly constructed objects, all threads will see the correct values of final fields that were set by the constructor, regardless of how the object is published.
    • any variables that can be reached through a final field of a properly constructed object (such as the elements of a final array or the contents of a HashMap referenced by a final field) are also guaranteed to be visible to other threads
  • for objects with final fields, initialization safety prohibits reordering any part of construction with the initial load of a reference to that object
  • all writes to final fields made by the constructor, as well as to any variables reachable through those fields, become "frozen" when the constructor completes, and any thread that obtains a reference to that object is guaranteed to see a value that is at least as up to date as the frozen value
    • writes that initialize variables reachable through final fields are not reordered with operations following the post-construction freeze
@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;
    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
    }
    public String getAbbreviation(String s) {
        return states.get(s);
    }
}
  • a number of small changes to SafeStates would take away its thread safety
    • if states were not final, or if any method other than the constructor modified its contents, initialization safety would not be strong enough to safely access SafeStates without synchronization
    • if SafeStates had other nonfinal fields, other threads might still see incorrect values of those fields
    • allowing the object to escape during construction invalidates the initialization-safety guarantee

Releases

No releases published

Packages

No packages published