Java security coding guidelines: visibility and atomicity

Introduction

Many variables are defined in a java class. There are class variables and instance variables. These variables will encounter some visibility and atomicity issues during the access process. Here we take a closer look at how to avoid these problems.

Visibility of immutable objects

An immutable object is an object that cannot be modified after initialization, so is an immutable object introduced into the class, and all modifications to an immutable object are immediately visible to all threads?

In fact, immutable objects can only guarantee the safety of object usage in a multithreaded environment, but not the visibility of objects.

Let's discuss variability first. Let's consider the following example:

public final class ImmutableObject {
    
    
    private final int age;
    public ImmutableObject(int age){
    
    
        this.age=age;
    }
}

We define an ImmutableObject object, the class is final, and the only field inside is also final. So this ImmutableObject cannot be changed after initialization.

Then we define a class to get and set this ImmutableObject:

public class ObjectWithNothing {
    
    
    private ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

In the above example, we defined a refObject to an immutable object, and then defined the get and set methods.

Note that although the ImmutableObject class itself is immutable, our reference to the object refObject is mutable. This means that we can call the setImmutableObject method multiple times.

Let's talk about visibility again.

In the above example, in a multi-threaded environment, will getImmutableObject return a new value every time setImmutableObject?

the answer is negative.

When the source code is compiled, the order of the instructions generated in the compiler is not exactly the same as the order of the source code. The processor may execute instructions in an out-of-order or parallel manner (as long as the final execution result of the program is consistent with the execution result in a strict serial environment in the JVM, this reordering is allowed). And the processor also has a local cache. When the results are stored in the local cache, other threads cannot see the results. In addition, the order in which caches are submitted to main memory may also change.

How to solve it?

The simplest solution to visibility is to add the volatile keyword. The volatile keyword can use the happens-before rule of the Java memory model to ensure that volatile variable modifications are visible to all threads.

public class ObjectWithVolatile {
    
    
    private volatile ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

In addition, the same effect can be achieved by using the lock mechanism:

public class ObjectWithSync {
    
    
    private  ImmutableObject refObject;
    public synchronized ImmutableObject getImmutableObject(){
    
    
        return refObject;
    }
    public synchronized void setImmutableObject(int age){
    
    
        this.refObject=new ImmutableObject(age);
    }
}

Finally, we can also use atomic classes to achieve the same effect:

public class ObjectWithAtomic {
    
    
    private final AtomicReference<ImmutableObject> refObject= new AtomicReference<>();
    public ImmutableObject getImmutableObject(){
    
    
        return refObject.get();
    }
    public void setImmutableObject(int age){
    
    
        refObject.set(new ImmutableObject(age));
    }
}

Ensure the atomicity of compound operations of shared variables

If it is a shared object, then we need to consider atomicity in a multithreaded environment. If it is a compound operation on shared variables, such as: ++, – *=, /=, %=, +=, -=, <<=, >>=, >>>=, ^= etc., it looks like A statement, but it is actually a collection of multiple statements.

We need to consider the safety of multithreading.

Consider the following example:

public class CompoundOper1 {
    
    
    private int i=0;
    public int increase(){
    
    
        i++;
        return i;
    }
}

In the example, we accumulate int i. But ++ is actually composed of three operations:

  1. Read the value of i from the memory and write it into the CPU register.
  2. The value of i+1 in the CPU register
  3. Write the value back to i in memory.

If in a single-threaded environment, there is no problem, but in a multi-threaded environment, because it is not an atomic operation, problems may occur.

There are many solutions, the first is to use the synchronized keyword

    public synchronized int increaseSync(){
    
    
        i++;
        return i;
    }

The second is to use lock:

    private final ReentrantLock reentrantLock=new ReentrantLock();

    public int increaseWithLock(){
    
    
        try{
    
    
            reentrantLock.lock();
            i++;
            return i;
        }finally {
    
    
            reentrantLock.unlock();
        }
    }

The third is to use the Atomic atomic class:

    private AtomicInteger atomicInteger=new AtomicInteger(0);

    public int increaseWithAtomic(){
    
    
        return atomicInteger.incrementAndGet();
    }

Ensure the atomicity of multiple Atomic atomic operations

If a method uses multiple atomic operations, although a single atomic operation is atomic, the combination is not necessarily.

Let's look at an example:

public class CompoundAtomic {
    
    
    private AtomicInteger atomicInteger1=new AtomicInteger(0);
    private AtomicInteger atomicInteger2=new AtomicInteger(0);

    public void update(){
    
    
        atomicInteger1.set(20);
        atomicInteger2.set(10);
    }

    public int get() {
    
    
        return atomicInteger1.get()+atomicInteger2.get();
    }
}

In the above example, we define two AtomicIntegers, and operate on the two AtomicIntegers in the update and get operations respectively.

Although AtomicInteger is atomic, the combination of two different AtomicIntegers is not. You may encounter problems during multithreaded operations.

Similarly, we can use synchronization mechanisms or locks to ensure data consistency.

Ensure the atomicity of the method call chain

If we want to create an instance of an object, and the instance of this object is created through chained calls. Then we need to ensure the atomicity of chain calls.

Consider the following example:

public class ChainedMethod {
    
    
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethod setAdress(String adress) {
    
    
        this.adress = adress;
        return this;
    }

    public ChainedMethod setAge(int age) {
    
    
        this.age = age;
        return this;
    }

    public ChainedMethod setName(String name) {
    
    
        this.name = name;
        return this;
    }
}

A very simple object, we define three properties, each set will return a reference to this.

Let's see how to call in a multi-threaded environment:

        ChainedMethod chainedMethod= new ChainedMethod();
        Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1"));
        t1.start();

        Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2"));
        t2.start();

Because in a multithreaded environment, the above set method may be confusing.

How to solve it? We can first create a local copy, which is thread-safe because it is accessed locally, and finally copy the copy to the newly created instance object.

The main code is as follows:

public class ChainedMethodWithBuilder {
    
    
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethodWithBuilder(Builder builder){
    
    
        this.adress=builder.adress;
        this.age=builder.age;
        this.name=builder.name;
    }

    public static class Builder{
    
    
        private int age=0;
        private String name="";
        private String adress="";

        public static Builder newInstance(){
    
    
            return new Builder();
        }
        private Builder() {
    
    }

        public Builder setName(String name) {
    
    
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
    
    
            this.age = age;
            return this;
        }

        public Builder setAdress(String adress) {
    
    
            this.adress = adress;
            return this;
        }

        public ChainedMethodWithBuilder build(){
    
    
            return new ChainedMethodWithBuilder(this);
        }
    }

Let's see how to call:

      final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1];
        Thread t1 = new Thread(() -> {
    
    
            builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                .setAge(1).setAdress("www.flydean.com1").setName("name1")
                .build();});
        t1.start();

        Thread t2 = new Thread(() ->{
    
    
            builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                .setAge(1).setAdress("www.flydean.com1").setName("name1")
                .build();});
        t2.start();

Because the variables used in the lambda expression must be final or final equivalent, we need to construct a final array.

Read and write 64bits value

In Java, 64bits long and double are treated as two 32bits.

Therefore, a 64-bit operation is divided into two 32-bit operations. This leads to atomicity problems.

Consider the following code:

public class LongUsage {
    
    
    private long i =0;

    public void setLong(long i){
    
    
        this.i=i;
    }
    public void printLong(){
    
    
        System.out.println("i="+i);
    }
}

Because the reading and writing of long is divided into two parts, if the setLong and printLong methods are called multiple times in a multi-threaded environment, problems may occur.

The solution is simple, just define long or double variables as volatile.

private volatile long i = 0;

The code of this article:

learn-java-base-9-to-20/tree/master/security

This article has been included in http://www.flydean.com/java-security-code-line-visibility-atomicity/

The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to discover!

Welcome to pay attention to my official account: "programs those things", know technology, know you better!

Guess you like

Origin blog.csdn.net/superfjj/article/details/108791925