Concurrent programming 3: How to design thread-safe classes

Table of contents

1. Design thread-safe classes

1.1 - Collect synchronization requirements

1.2 - Operations that depend on state: a priori conditions

1.3 - Ownership of state

2. Close the instance

2.1 - The Java Listener Pattern

2.2 - Vehicle Tracking Based on the Listener Pattern: Using Snapshots

3. Thread-safe delegation: use concurrent containers

4. Add functionality to existing thread-safe classes: composition


1. Design thread-safe classes

        In the process of designing thread-safe classes, the following three basic elements need to be included:

  • Find all the variables that make up the state of the object. //all member variables
  • Find the invariance conditions that constrain the state variables.
  • Establish concurrent access management policies for object state. //Synchronize access to shared variables

        Synchronization Policy (Synchronization Policy) defines how to coordinate access to its state without violating the object's invariant conditions or postconditions . Synchronization strategy stipulates how to combine immutability , thread closure and locking mechanism to maintain thread security , and also specifies which variables are protected by which locks . //post-condition: verify after operation

1.1 - Collect synchronization requirements

        To ensure the thread safety of a class, it is necessary to ensure that its invariant conditions cannot be violated in the case of concurrent access . Both objects and variables have a state space, that is, all possible values. The smaller the state space, the easier it is to determine the state of the thread. The more fields of type final are used, the easier it is to analyze the possible states of an object. //The state is stable during concurrency, avoiding race conditions

Similarly, some post-conditions         will be included in the operation to determine whether the state transition is valid. If the current state of the calculator counter is 17, then the next valid state can only be 18. When the next state needs to depend on the current state, the operation must be a compound operation . //When concurrent, composite operations may have invalid values ​​and race conditions -> atomic operations

        Additional synchronization and encapsulation are required because invariance conditions and postconditions impose various constraints on states and state transitions. If there is an invalid state transition in an operation, the operation must be atomic . Alternatively, if such constraints are not enforced in the class, then requirements such as encapsulation or serialization can be relaxed for greater flexibility or performance.

        Thread safety cannot be ensured without knowledge of the invariant conditions and postconditions of the object. To meet various constraints on the effective value of state variables or state transitions, it is necessary to rely on atomicity and encapsulation.

1.2 - Operations that depend on state: a priori conditions

        Class invariance conditions and postconditions constrain which states and state transitions are valid on objects. Some object methods also contain some state-based preconditions (Precondition) . For example, you cannot remove an element from an empty queue; the queue must be in a "non-empty" state before removing an element. If an operation contains state-based prior conditions , then this operation is called a state-dependent operation. //A priori condition -> if the condition is true, then it can be operated

1.3 - Ownership of state

        In many cases, ownership and encapsulation are always related: an object encapsulates the state it owns, that is, it owns the state it encapsulates.

        The owner of the state variable will decide which locking protocol to use to maintain the integrity of the variable state. Ownership means control. However, if you publish a reference to a mutable object, you no longer have exclusive control, at best "shared control" . Classes generally do not own objects passed in from constructors or methods, unless those methods are specifically designed to transfer ownership of objects passed in (for example, factory methods of synchronized container wrappers).

        Like all shared objects, they must be shared safely . In order to prevent mutual interference caused by multiple threads accessing the same object concurrently, these objects should be either thread-safe objects, or de facto immutable objects, or objects protected by locks. //When an object does not have exclusive control, it is a shared object

2. Close the instance

        If an object is not thread-safe, there are several techniques to make it safe for use in a multithreaded program. You can ensure that the object can only be accessed by a single thread (thread closure), or protect all access to the object with a lock.

        Encapsulation simplifies the implementation process of thread-safe classes, and it provides an instance confinement mechanism (Instance Confinement) , which is usually referred to as "closure". When an object is encapsulated within another object, all code paths that can access the encapsulated object are known. The code is easier to analyze than if the object is accessible by the entire program. By combining the closure mechanism with an appropriate locking strategy, it is possible to ensure that non-thread-safe objects can be used in a thread-safe manner .

        Encapsulating data inside an object restricts data access to the object's methods, making it easier to ensure that threads always hold the correct lock when accessing data.

//线程安全的类
public class PersonSet {

    //非线程安全对象
    private final Set<Person> mySet = new HashSet<Person>();

    //使用同步方法操作非线程安全对象-同步容器常用
    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }
    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }
}

        The above code  PersonSet illustrates how to make a class a thread-safe class through mechanisms such as closure and locking (even if the state variables of this class are not thread-safe). The state of PersonSet is managed by HashSet, and HashSet is not thread-safe. But since mySet is private and doesn't escape, the HashSet is enclosed in PersonSet. The only code paths that can access mySet are addPerson and containsPerson, both of which acquire the lock on PersonSet when executed. The state of PersonSet is completely protected by its built-in lock, so PersonSet is a thread-safe class. //In this example, it is also possible to use a synchronous container/concurrent container to wrap the Person

        The closure mechanism makes it easier to construct thread-safe classes, because when the state of the class is closed, it is not necessary to examine the entire program when analyzing the thread safety of the class.

2.1 - The Java Listener Pattern

        An object following the Java monitor pattern encapsulates all mutable state of the object , protected by the object's own built-in locks . The code to use the Java monitor mode looks like this:

//通过一个私有锁来保护状态
public class PrivateLock {
    //私有锁
    private final Object myLock = new Object();

    private Widget widget;

    void someMethod() {
        synchronized (myLock) {
            // Access or modify the state of widget
        }
    }
}

        There are many advantages to using a private lock object instead of the object's built-in lock (or any other publicly accessible lock). A private lock object can encapsulate the lock so that client code cannot acquire the lock , but client code can also access the lock through public methods in order to (correctly or incorrectly) participate in its synchronization strategy.

2.2 - Vehicle Tracking Based on the Listener Pattern: Using Snapshots

//车辆追踪:线程安全的类
public class MonitorVehicleTracker {

    /**
     * 成员变量:不可变对象,不对外暴露
     */
    private final Map<String, MutablePoint> locations;

    /**
     * 构造方法
     */
    public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
        this.locations = deepCopy(locations);
    }

    /**
     * 同步方法:获取数据时,每次都拷贝(快照)
     */
    public synchronized Map<String, MutablePoint> getLocations() {
        return deepCopy(locations);
    }

    /**
     * 同步方法:获取位置
     */
    public synchronized MutablePoint getLocation(String id) {
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }

    /**
     * 同步方法:设置位置
     */
    public synchronized void setLocation(String id, int x, int y) {
        MutablePoint loc = locations.get(id);
        if (loc == null) {
            throw new IllegalArgumentException("No such ID: " + id);
        }
        loc.x = x;
        loc.y = y;
    }

    /**
     * 深拷贝:获取快照
     */
    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        Map<String, MutablePoint> result = new HashMap<>();
        for (String id : m.keySet()) {
            result.put(id, new MutablePoint(m.get(id)));
        }
        //返回不可修改的视图:一致性快照
        return Collections.unmodifiableMap(result);
    }
}

//非线程安全的类
public class MutablePoint {

    public int x, y;

    public MutablePoint() {
        x = 0;
        y = 0;
    }

    public MutablePoint(MutablePoint p) {
        this.x = p.x;
        this.y = p.y;
    }
}

        Although in the above code, the class MutablePoint is not thread-safe, but the tracker class is thread-safe. Neither the Map object nor the mutable Point object it contains is published. When it is necessary to return the position of the vehicle, use the MutablePoint copy constructor or deepCopy method to copy the correct value to generate a new Map object, and the value in this object is the same as the key value and value in the original Map object .

        In part, this implementation maintains thread safety by copying mutable data before returning to client code . Normally, this is not a performance problem, but it will greatly reduce performance in the case of very large vehicle containers, and since the data is copied every time getLocation is called, there will be an error condition - although the vehicle's The actual location changes, but the returned information remains the same. // Whether it is appropriate to use snapshots depends on specific needs

3. Thread-safe delegation: use concurrent containers

        In the code DelegatingVehicleTracker does not use any explicit synchronization, all access to state is managed by ConcurrentHashMap, and all keys and values ​​of Map are immutable.

//线程安全的类
public class DelegatingVehicleTracker {

    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point>           unmodifiableMap;

    public DelegatingVehicleTracker(Map<String, Point> points) {
        locations       = new ConcurrentHashMap<>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    //不可变的值可以被自由地共享与发布
    public Map<String, Point> getLocations() {
        //返回静态拷贝而非实时拷贝
        return unmodifiableMap;
    }

    public Point getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null) {
            throw new IllegalArgumentException("invalid vehicle name: " + id);
        }
    }

    // getLocations 的替代版本
    public Map<String, Point> getLocationsAsStatic() {
        return Collections.unmodifiableMap(new HashMap<>(locations));
    }
}

//不可变对象:不可变的值可以被自由地共享与发布
public final class Point {
    public final int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

        The above code slightly changes the behavior of the vehicle tracker class. In the vehicle tracker using the monitor mode, a snapshot of the vehicle's position is returned, while in the vehicle tracker using the delegate, an unmodifiable but real-time vehicle is returned. Location view. This means that if thread A calls getLocations and thread B subsequently modifies the location of certain points, those changes will be reflected in the Map returned to thread A. // Use concurrent containers to obtain real-time changes in data

        If a class is composed of multiple independent and thread-safe state variables, and all operations do not involve invalid state transitions, then thread safety can be delegated to the underlying state variables.

4. Add functionality to existing thread-safe classes: composition

The Composition method         can be used when adding an atomic operation to an existing class . In the following code, ImprovedList realizes the operation of List by delegating the operation of List object to the underlying List instance, and also adds an atomic putIfAbsent method. //Enhance and wrap the original object

//@ThreadSafe
public class ImprovedList<T> implements List<T> {

    private final List<T> list;

    /**
     * 入参list是线程安全的
     */
    public ImprovedList(List<T> list) {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains) {
            list.add(x);
        }
        return !contains;
    }

    // Plain vanilla delegation for List methods.
    // Mutative methods must be synchronized to ensure atomicity of putIfAbsent.

    public int size() {
        return list.size();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }

    public boolean contains(Object o) {
        return list.contains(o);
    }

    public Iterator<T> iterator() {
        return list.iterator();
    }

    public Object[] toArray() {
        return list.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return list.toArray(a);
    }

    public synchronized boolean add(T e) {
        return list.add(e);
    }

    public synchronized boolean remove(Object o) {
        return list.remove(o);
    }

    public boolean containsAll(Collection<?> c) {
        return list.containsAll(c);
    }

    public synchronized boolean addAll(Collection<? extends T> c) {
        return list.addAll(c);
    }

    public synchronized boolean addAll(int index, Collection<? extends T> c) {
        return list.addAll(index, c);
    }

    public synchronized boolean removeAll(Collection<?> c) {
        return list.removeAll(c);
    }

    public synchronized boolean retainAll(Collection<?> c) {
        return list.retainAll(c);
    }

    public boolean equals(Object o) {
        return list.equals(o);
    }

    public int hashCode() {
        return list.hashCode();
    }

    public T get(int index) {
        return list.get(index);
    }

    public T set(int index, T element) {
        return list.set(index, element);
    }

    public void add(int index, T element) {
        list.add(index, element);
    }

    public T remove(int index) {
        return list.remove(index);
    }

    public int indexOf(Object o) {
        return list.indexOf(o);
    }

    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }

    public ListIterator<T> listIterator() {
        return list.listIterator();
    }

    public ListIterator<T> listIterator(int index) {
        return list.listIterator(index);
    }

    public List<T> subList(int fromIndex, int toIndex) {
        return list.subList(fromIndex, toIndex);
    }

    public synchronized void clear() {
        list.clear();
    }
}

        ImprovedList adds an extra layer of locking through its own built-in locking . It doesn't care whether the underlying List is thread-safe. Even if List is not thread-safe or its locking implementation is modified, ImprovedList will provide a consistent locking mechanism to achieve thread safety.

        While the extra layer of synchronization may incur a slight performance penalty, ImprovedList is more robust than emulating another object's locking strategy. In fact, we use the Java monitor pattern to wrap an existing List and ensure thread safety as long as we have the only external reference to the underlying List within the class. //Use the built-in lock of the class in the enhanced class instead of holding the lock of the original List object, so that the program is more robust: when locking, only the enhanced object will be locked instead of the List class

Guess you like

Origin blog.csdn.net/swadian2008/article/details/129201405