Design principle: Immutability immutability and the Synchronization method for mutable variables

Immutability in design patterns means that the state of an object cannot be changed after it is created. This is a programming idea and design principle. In some cases, using immutable objects can bring a number of benefits:

  1. Simplify code make things very simple: Immutable objects don’t change their state after creation, so there is no need to consider object state changes, which makes the code simpler, easier to understand and maintain.

  2. Thread-safe Inherently thread-safe: Immutable objects are thread-safe because they do not share mutable state across multiple threads. This eliminates the need for synchronization and locking, improving performance and reliability.

  3. Support for sharing No risks in sharing: Immutable objects can be safely shared by multiple clients because they do not modify the shared state. This saves memory and computing resources.

  4. Reduced risk of errors: Since the state of immutable objects does not change after creation, errors caused by state changes can be reduced and system stability improved.

There are several ways to create immutability objects:

  • Make all member variables of the class private and read-only (final). Make all fields final and private.
  • Initialize all member variables in the constructor and make sure they don’t change after object creation. Ensure that no methods may be overridden.
  • Does not provide any method (setter) to modify member variables. Don’t provide any mutators.
  • If the class contains references to mutable objects, make sure to create copies of these references when returning them, to avoid client code modifying internal state. Ensure security of any mutable components.

For example, the following is a non-immutable code:

public class Complex {
double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }
public double setRealPart(double re) { this. re = re; }
public double setImaginaryPart(double im) { this.im = im; }

After the modification of immutable is:

public final class Complex {
private final double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// Getters without corresponding setters
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }

There may be doubts here, if the set method is removed, how to meet the modification requirements? At this point, you can add a modification method to return only the modified copy to avoid affecting the original object, for example:

// subtract, multiply, divide similar to add
public Complex add(Complex c) {
return new Complex(re + c. re, im + c. im);
}

Similarly, if you have to write the set method, just return a new copy of an object.

Limitations of Immutability:

Immutability is just a design idea, not the only standard, because on the one hand, writing immutable code sometimes causes additional memory allocation and garbage collection overhead, thereby reducing the performance of the program; on the other hand, because each modification needs to create a new object, which may result in many similar objects existing in memory at the same time. The most important thing is, but in the case of frequently modifying the state of the object, using mutable objects will be more concise and efficient (for example, to record a person’s bank account, is it necessary to create a new account object every time a user makes a transaction? Obviously What to do at this time is to minimize the mutable part, and do a good job of thread protection for the mutable part), so you don’t have to write immutable code.

Other ways to ensure process security

As mentioned above, if you can write immutable parts, write immutable, but if you have to change, we use some other methods to achieve thread safety.

Let’s first look at a “thread unsafe” example:

@NotThreadSafe
public class UnsafeSequence {
private int value;
public int getNext() {
return value ++ ;
}
}

This code is not thread-safe, because the value variable may be auto-incremented (value + + ) in the getNext() method A race condition occurs. The value + + operation actually includes three steps:

  1. Read the current value of value.
  2. Add 1 to the value of value.
  3. Write the new value back to value.

In a multi-threaded environment, if two or more threads execute the getNext() method at the same time, these steps may be interleaved, resulting in the loss of value updates.

In this case, we obviously increased the variable by 1 through two threads, and should add 2 in total, but only increased by 1 in the end. For this particular example, there is no way to make it thread-safe just by using immutability. This is because the main purpose of the UnsafeSequence class is to generate an increasing sequence, which means it needs to maintain a mutable state internally (in this case the value variable). However, other methods can be used to make this code thread-safe, such as synchronization or atomic operations.

synchronized keyword

The synchronized keyword is a mechanism used to achieve synchronization in Java. It is used to ensure that access and modification of shared resources are mutually exclusive in a concurrent environment. When a thread is executing a method or code block modified by the synchronized keyword, other threads must wait until the current thread completes the operation on the shared resource. This helps prevent multiple threads from simultaneously accessing and modifying shared resources, thereby avoiding race conditions and data inconsistencies. The following is an example of using this keyword to solve the thread unsafety above:

public class SafeSequence {
    private int value;

    public synchronized int getNext() {
        return value ++ ;
    }
}

For synchronization, we also divide coarse-grained synchronization (lock the entire object) and fine-grained fine-grained synchronization (lock some resources), depending on the actual situation, the more things locked, the greater the impact on performance (will be slower).

AtomicInteger

Another alternative is to use the AtomicInteger class in the java.util.concurrent.atomic package, which provides atomic operations to ensure the atomicity of self-increment operations:

import java.util.concurrent.atomic.AtomicInteger;

public class SafeSequence {
    private AtomicInteger value = new AtomicInteger();

    public int getNext() {
        return value. getAndIncrement();
    }
}

volatilekeyword

The volatile keyword is a little weaker than the synchronized function. It provides a synchronization mechanism, but it does not provide mutually exclusive access. In other words, using the volatile keyword can ensure the visibility of variables, that is, when a thread modifies the value of a volatile variable, other threads can immediately see the change. However, it does not guarantee atomicity, that is, in a concurrent environment, multiple threads can still access and modify volatile variables at the same time, which may lead to inconsistent state. Not as powerful as synchronized. Here the volatile keyword cannot solve the thread unsafe problem above, it is just proposed for introduction.

private static volatile boolean stopRequested;

Thread Confinement thread closure

The volatile and synchronized just mentioned are used to deal with the synchronization problem when sharing variables among multiple threads, while thread closure is directly avoided by sharing variables among multiple threads. Variables to eliminate synchronization problems and directly solve the problem at its root.

Here are some ways to achieve thread confinement:

  1. Use local variables Local variables: Local variables are only visible in the method in which they are declared, so they naturally belong to the thread that owns the method. When the method call ends, the local variables are removed from the stack without affecting other threads.

  2. Defensive copy defense copying: When you need to pass an object from one thread to another, you can create a copy of the object so that each thread has its own copy, avoiding the problem of multiple threads accessing the same object. (This actually goes back to immutability.)

  3. Using ThreadLocal (for Java): ThreadLocal is a special Java class that allows you to store a separate value for each thread. When you need to share data between threads, you can use ThreadLocal to ensure that each thread has its own private copy.

  4. Features adapted to other programming languages:

    • JavaScript: Since JavaScript runs in a single-threaded environment, there is no need to consider thread closure issues.
    • Python: Python makes a clear distinction between multithreading and multiprocessing. Multiple processes cannot share state except through special objects.

By using these methods, the effect of thread confinement can be realized, but it can be found that as long as immutability is realized, it will greatly help to achieve thread confinement, Immutability Simplifies Thread Confinement! Since the state of immutable objects does not change, they are inherently thread-safe in a multi-threaded environment. Once an object is created, any thread can safely access it without worrying about other threads modifying it.

Summary:

This article first talks about the advantages and disadvantages of immutability and implementation examples. One of its advantages is thread safety. But for variables that are not suitable to be written as immutable, in order to achieve thread safety, we can also use thread closure or use keywords such as synchronized or other synchronization mechanisms (such as explicit locks) to ensure that access to shared resources is atomic and Ordered.

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge Java skill tree Variables and constants in JavaDefinition of variables 116173 people are studying systematically