Using invariance to solve concurrency problem

Using invariance to solve concurrency problem

Invariance pattern

We know that concurrency is the most likely cause of thread insecurity. The main reason is that there is data competition. If multiple threads read only and do not modify the same variable, there will be no thread safety problem. This idea is actually very simple, but it is also the most easily ignored by us. Using this idea, we have derived a pattern - invariance pattern, also known as Immutability, A simple description is that after an object is created, its state will not change.

Invariance class

Definition of immutability class

In Java, the immutability of an object is generally described by final modification, so is the immutable class. As long as all the attributes of the class are modified by fianl and only the methods are allowed to be read-only, this class is immutable. However, for the sake of strictness, this class is also required to be modified by final, because the class modified by final cannot be inherited, After the class value is modified, the variability of other class values can be avoided.

Non variability class verification

There are many immutable classes in the SDK, but we may not notice it at ordinary times. For example, the wrapper classes of some basic classes such as Integer, Long, Double and so on are basically immutable classes. Here, the typical representative String is taken as an example to prove immutability. I believe most people understand a little about String, such as immutability of String, character array storage at the bottom and so on. The source code is as follows.

The figure above verifies that the attributes of immutable classes are generally modified by final, and the classes are also modified by final.

Many people have doubts here. The properties of the String class are indeed modified by final, but the methods are not all read-only. For example, replace modifies the value of the variable? On the surface, it is true. We can analyze the source code as follows

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        // String array of existing values
        char[] val = value; /* avoid getfield opcode */

        while (++i < len) {
            // Find the first place where oldChar appears
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            // The immutability of new value character array is reflected here!!!!
            char buf[] = new char[len];
            // Put the value before oldChar into the character array of the new value
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                // Judge whether the current value is an old character
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            // Returns a new value array object
            return new String(buf, true);
        }
    }
    return this;
}

From the source code, we get an immutable class. If you need to modify the attribute value, you can only re create an object and assign the new attribute value to the new object, so as to achieve the purpose of modification. The difference between the mutable class and the mutable class is that the mutable class modifies the value of its own attribute, The immutable class will create a new object after modifying the attribute value once, which is bound to cause memory consumption.

Is this problem similar to the thread creation problem? If new Thread() is required to create a thread object when executing a subtask, the performance consumption caused by the creation and destruction of threads will directly affect the application. In order to solve this problem, the SDK introduces the thread pool to create multiple threads at one time. It takes time to apply to the thread pool, There is no need to return the thread pool when it is used, so as to avoid the performance consumption caused by repeated creation and destruction of threads. Does the immutable class also have the idea of pooling?

Of course, it does exist. This is one of our design patterns called enjoy meta pattern.

Share meta mode to solve repeated creation of objects

What is the meta model

Flyweight Pattern: a software design pattern. It uses shared objects to minimize memory usage and share information to as many similar objects as possible; It is suitable for a large number of objects that use unacceptably large amounts of memory simply because of repetition. Usually, some states in an object can be shared. It is common to pass them to external structures when needed.

Generally speaking, the meta sharing mode is similar to the idea of pooling. When creating an object through the meta sharing mode, first check whether the object exists in the object pool. If it exists in the object pool, use it. If it does not exist, create it and put it into the object pool at the same time, so as to reduce the creation of objects.

This idea has now been applied to the packaging classes of basic data types such as Long, Integer, Short and Byte. Here, take Integer as an example, it does not use the meta mode.

How does the SDK use the meta mode

The maximum interval that Integer can express is [- 231231-1], but there are not many commonly used ones, so the SDK management caches the commonly used numbers between [- 128127], as shown below. It will be created when the JVM is started and will never change. In Integer You can see the method call in the valueof () method. The simplified code is as follows

public static Integer valueOf(int i) {
    if (i >= -128 && i <= 127)
        return IntegerCache.cache[i + (128)];
    return new Integer(i);
}
// Cache object class
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        high = 127;
        // 127 - (-128)  +1
        cache = new Integer[(high - low) + 1];
        int j = low;// -128
        // Cache data starts at - 128 and ends at 127
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
    private IntegerCache() {}
}

Here we can explain why using wrapper classes of basic data types as locks has thread safety problems. The code verification is as follows

Suppose there are two classes a and B, which do not interfere with each other. When calling their respective setAX or setBY, first obtain the a1 and b1 object locks and execute them in two threads t1 and t2. Here, it should be asynchronous logic. t2 will complete the execution first. t1 needs to wait for the execution of setAX to unlock, but is this really the case?

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("====Start calling setAX");
            A a = new A();
            a.setAX();
        });

        Thread t2 = new Thread(()->{
            System.out.println("====Start calling setBY");
            B b = new B();
            b.setBY();
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

class A{
    Long a1=Long.valueOf(1);

    public void setAX() {
        synchronized (a1) {
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println("implement setAX complete");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class B {
    Long b1=Long.valueOf(1);

    public void setBY(){
        synchronized (b1) {
            System.out.println("implement setBY complete");
        }
    }
}

The execution results are as follows. When t1 is blocked, t2 does not call the setBY method, but is also blocked. The setBY method is not executed until t1 releases the lock thread t2.

If you change the values of a1 and b1 to 128, for values outside the cache, thread t2 will complete the execution first and will not wait for t1 to complete the execution.

It is concluded that if the values of a1 and b1 are between [- 128127], because the shared element mode is adopted, the value in the cache is taken, that is, the same value, so there will be a mutually exclusive relationship between the concurrent execution of t1 and t2 threads. If the values of a1 and b1 are not within this interval, there will be no mutually exclusive relationship between t1 and t2 threads.

Attention points of invariant mode

All properties of the object are final and cannot be guaranteed to be immutable, as shown below

If the attribute is of object type Foo, even if the current class Bar is an immutable class, you can still modify the attribute value of Foo through the setAge method.

class Foo{
    int age=0;
    String name="abc";
}

final class Bar{
    Foo foo;
    public Bar(Foo foo){
        this.foo = foo;
    }
    void setAge(int age){
        foo.age = age;
    }
}

How to publish immutable classes correctly

Although an immutable class is thread safe, it does not mean that its reference is thread safe. The following code

Foo is thread safe for immutable classes, while Bar thread is unsafe, and it has Foo's object reference foo, so the modification of foo under multithreading has no visibility and atomicity.

final class Foo{
    final int age=0;
    final String name="abc";
}

class Bar{
    Foo foo;
    public void setFoo(Foo foo){
        this.foo = foo;
    }
}

How to solve it? Use atomic reference

class Bar{
    AtomicReference<Foo> atomicReference = new AtomicReference<>(new Foo());
    public void setFoo(Foo foo){
        Foo foo1 = atomicReference.get();
        atomicReference.compareAndSet(foo1,foo);
    }
}

The simplest invariance class

The object with invariance class has only one state, which needs to be maintained by the invariance attributes in the object, and there is a simpler invariance class, that is stateless. What is stateless? In fact, classes with no attributes but only methods are called stateless. Stateless objects have no thread safety problems. Why? In fact, it is very simple, because as long as the variables inside the method are local variables, local variables are thread safe.

Keywords: Java

Added by keevitaja on Thu, 10 Mar 2022 18:48:14 +0200