primers
In the previous article we discussed that the main cause of thread safety is the destruction of visibility or order caused by multithreading operations on shared memory, resulting in memory consistency errors.
So how do you design concurrent code to solve this problem?
We usually use the following methods:
- Thread closed
- Immutable object
- synchronous
Release and escape
Before we do that, let’s look at the concepts of release and escape.
Publishing means making an object usable outside of its current scope, such as passing references to objects to methods of other classes, returning references to them in a method, and so on.
In many cases we want to make sure that internal objects are not published. Publishing some internal state can break encapsulation and allow users to change their state at will, thereby undermining thread safety.
In some cases, we need to publish some internal objects, and in thread-safe cases, we need to synchronize them properly.
When an object is published when it should not be published, the situation is called escape.
Copy the code
- public class Escape {
- private List<User> users = Lists.newArrayList();
- public List<User> getUsers() {
- return users;
- }
- public void setUsers(List<User> users) {
- this.users = users;
- }
- }
GetUsers has escaped its scope and the private variable is published because any caller can modify the array.
Publishing users also indirectly publishes a reference to the User object.
Copy the code
- public class OuterEscape {
- private String str = “Outer’s string”;
- public class Inner {
- public void write() {
- System.out.println(OuterEscape.this.str);
- }
- }
- public static void main(String[] args) {
- OuterEscape out = new OuterEscape();
- OuterEscape.Inner in = out.new Inner();
- in.write();
- }
- }
The inner class holds a reference to the enclosing class that created the inner class, so the inner class can use the private properties and methods of the enclosing class that created the inner class.
Copy the code
- public class ConstructorEscape {
- private Thread t;
- public ConstructorEscape() {
- System.out.println(this);
- t = new Thread() {
- public void run() {
- System.out.println(ConstructorEscape.this);
- }
- };
- t.start();
- }
- public static void main(String[] args) {
- ConstructorEscape a = new ConstructorEscape();
- }
- }
This reference is shared by thread T, so the publication of thread T will result in the publication of the ConstructorEscape object, which will cause the ConstructorEscape object to escape, since the ConstructorEscape object was not constructed when it was published
Summarize the steps for safe publishing
- Find all the variables that make up the state of the object
- Find the invariance conditions that constrain the state variables
- Establish concurrent access policies for object state
Thread closed
The idea of thread closure is simple. Since thread safety problems are caused by multiple threads accessing shared variables, if we can avoid manipulating shared variables and each thread accesses its own variable, there will be no thread-safety problems. This is the simplest way to achieve thread-safety.
Thread-controlled escape rules can help you determine if access to certain resources in your code is thread-safe. If a resource is created, consumed, and destroyed in the same thread, it is thread-safe to use it.
Resources can be objects, arrays, files, database connections, sockets, and so on. In Java, you don’t have to actively destroy an object, so “destroy” means no more references to the object. Even if the object itself is thread-safe, if the object contains other resources (files, database connections), the entire application may no longer be thread-safe. For example, two threads create separate database connections. Each connection itself is thread-safe, but the same database they connect to may not be thread-safe
Let’s look at several implementations of thread closure:
The stack is closed
Stack closure is a special case of thread closure in which objects can only be accessed through local variables stored in the thread’s own stack. That is, local variables are never shared by multiple threads. Therefore, local variables of the underlying type are thread-safe.
A local reference to an object is different from a local variable of the underlying type. Although the reference itself is not shared, the object to which the reference refers is not stored on the thread’s stack. All objects are stored in the shared heap. If objects created in a method do not escape from that method, it is thread-safe. In fact, even if you pass this object as an argument to another method, it is still thread-safe as long as it is not available to another thread.
Copy the code
- public void someMethod(){
- LocalObject localObject = new LocalObject();
- localObject.callMethod();
- method2(localObject);
- }
- public void method2(LocalObject localObject){
- localObject.setValue(“value”);
- }
As above, the LocalObject is not returned by the method, nor is it passed to an object outside the someMethod() method. Each thread executing someMethod() creates its own LocalObject object and assigns a value to a LocalObject reference. Therefore, the LocalObject here is thread-safe. In fact, the entire someMethod() is thread-safe. Even when LocalObject is passed as a parameter to other methods of the same class or to methods of other classes, it is still thread-safe. Of course, if a LocalObject is passed to another thread in some way, it is no longer thread-safe
Program control thread closure
Thread closed through program implementation, that is to say we can’t use language features the object close to a specific thread, it lead to less reliable hypothesis this way we can ensure that only one thread to write of a Shared object, the object of “read – modified – written” in any case will not appear unexpectedly state conditions. If we make the object volatile, we guarantee that the object is visible. Any thread can read the object, but only one thread can write to it. Thus, the security is properly guaranteed by volatile modification alone, which is more suitable but slightly more complicated to implement than using synchoronized modification directly.
Program-controlled thread closure is not a specific technique, but rather a design approach to avoid thread-safety problems by putting all the code that handles an object’s state into a single thread.
ThreadLocal
ThreadLocal is essentially program-controlled thread closure, but Java itself helps handle it. Consider the Java Thread and ThreadLocal classes:
- The Thread class maintains an instance variable of ThreadLocalMap
- ThreadLocalMap is a Map structure
- The set method of a ThreadLocal takes the current thread, takes the current thread’s threadLocalMap object, treats the ThreadLocal object as the key, and puts the value into the Map as the value
- The get method of a ThreadLocal retrieves the current thread’s threadLocalMap object, then uses the ThreadLocal object as the key and retrieves the corresponding value
Copy the code
- public class Thread implements Runnable {
- ThreadLocal.ThreadLocalMap threadLocals = null;
- }
- public class ThreadLocal<T> {
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map ! = null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e ! = null)
- return (T)e.value;
- }
- return setInitialValue();
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map ! = null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- }
The design of a ThreadLocal is simple: it sets up an internal Map of thread objects that can hold some data. The JVM undergoes that Thread objects do not see each other’s data.
Using ThreadLocal requires that each ThreadLocal store a separate object that cannot be shared across multiple ThreadLocal threads, otherwise the object is not thread-safe
ThreadLocal memory leak
A ThreadLocalMap uses a weak reference to a ThreadLocal as its key. If a ThreadLocal has no external strong reference to it, then the ThreadLocal will be reclaimed during GC. There is no way to access the values of these null-key entries. If the current thread does not terminate, the values of these null-key entries will always have a strong reference chain: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value can never be reclaimed, causing a memory leak.
Get (),set(), and remove() remove all null-key values from the ThreadLocalMap.
You can avoid this problem by calling ThreadLocal’s remove() method every time you finish using it
Immutable object
An object that cannot be modified after it is created is called immutable. An accepted principle in concurrent programming is to use immutable objects whenever possible to create simple, reliable code
Immutable objects are especially useful in concurrent programming. Since they cannot be modified after being created, there are no memory consistency errors caused by manipulating shared variables
But programmers are generally not keen on using immutable objects because they worry about the overhead of creating new objects each time. In fact, this overhead is often overestimated, and is offset by some of the efficiency gains from using immutable objects
Let’s start with an example of using synchronization to address thread safety
Copy the code
- public class SynchronizedRGB {
- // Values must be between 0 and 255.
- private int red;
- private int green;
- private int blue;
- private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public SynchronizedRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public void set(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- synchronized (this) {
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- }
- public synchronized int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public synchronized String getName() {
- return name;
- }
- }
Copy the code
- SynchronizedRGB color =
- new SynchronizedRGB(0, 0, 0, “Pitch Black”);
- .
- int myColorInt = color.getRGB(); / / 1
- String myColorName = color.getName(); / / 2
- GetName does not match getRGB if another thread calls set after 1
- synchronized (color) {
- int myColorInt = color.getRGB();
- String myColorName = color.getName();
- }
- // Both statements must be executed synchronously
Principles for creating immutable objects
- Methods for modifying mutable objects are not provided. (Including methods to modify fields and methods to modify field reference objects)
- Define all fields of the class as final, private.
- Subclasses are not allowed to override methods. The easy way is to declare the class final. A better way is to declare the constructor private and create the object through the factory method.
- If the class field is a reference to a mutable object, it is not allowed to modify the referenced object.
- References to mutable objects are not shared. When a reference is passed as an argument to a constructor that refers to an external mutable object, do not save the reference. If you must, create a copy of the mutable object and then save the reference to the copied object. Also, if you need to return an internal mutable object, do not return the mutable object itself, but a copy of it
Modified example
Copy the code
- final public class ImmutableRGB {
- // Values must be between 0 and 255.
- final private int red;
- final private int green;
- final private int blue;
- final private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public ImmutableRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public String getName() {
- return name;
- }
- }
Facts are immutable objects
If an object is mutable, but there is no possibility that it will change while the program is running, it is called a de facto immutable object and does not require additional thread-safe protection
synchronous
When we have to use shared variables and need to change them frequently, we need to use synchronization to achieve thread-safety.
Java Synchronized/Lock volatite CAS is used to implement synchronization.
Synchronized is an exclusive lock that assumes the worst and is executed only when ensuring that other threads do not interfere, causing all other threads requiring the lock to hang and wait for the thread holding the lock to release it.
Volatile variables are a lighter synchronization mechanism than locks because they are used without context switches and thread scheduling, but they have some limitations: they cannot be used to build atomic compounds, so they cannot be used when a variable depends on an old value.
CAS is an optimistic lock that executes an operation each time without locking, assuming no conflicts, and retries until it succeeds if it fails because of conflicts.
Synchronization solves three interrelated problems:
- Atomicity: Which instructions must be indivisible
- Visibility: The result of execution by one thread is visible to another thread
- Orderliness: The result of an operation by one thread is unordered by another thread
conclusion
It’s important to understand the concept of thread safety, which is dealing with object state. If the object you’re dealing with is stateless (immutable) or can avoid being shared by multiple threads (thread-closed), then you can rest assured that the object is probably thread-safe. Thread synchronization techniques are used when it is unavoidable that the object state must be shared with multiple threads for access.
This understanding extends to the architectural level, where the business layer is best designed to be stateless so that the business layer is scalable and can handle high concurrency smoothly by scaling horizontally.
So we can deal with thread safety at several levels:
- Can we make it stateless immutable. Statelessness is the safest.
- Can thread close
- Which synchronization technology is used (Synchronized/Lock volatite CAS)