Java secure coding guide: visibility and atomicity

brief introduction

There are many variables defined in Java classes, including class variables and instance variables. In the process of accessing these variables, you will encounter some visibility and atomicity problems. Let's learn more about how to avoid these problems.

Visibility of immutable objects

Immutable objects are objects that cannot be modified after initialization. Is immutable object introduced into the class, and all modifications to immutable objects are immediately visible to all threads?

In fact, immutable objects can only ensure the security of object use in multi-threaded environment, but can not ensure 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 in it is also final. Therefore, this immutableobject cannot be changed after initialization.

Then we define a class to get and set the 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 reference refobject to an immutable object, and then defined the get and set methods.

Let's talk about visibility.

In the above example, in a multithreaded environment, does each setimmutableobject cause getimmutableobject to return a new value?

The answer is No.

After compiling the source code, the order of instructions generated in the compiler is not exactly the same as that of the source code. The processor may execute instructions out of order or in parallel (in the JVM, this reordering is allowed as long as the final execution result of the program is consistent with that in the strict serial environment). Moreover, 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 the cache is submitted to the main memory may also change.

How to solve it?

The simplest way to solve the visibility problem is to add volatile keyword. Volatile keyword can use the happens before rule of JAVA memory model, so as to ensure that the variable modification of volatile is 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 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, we need to consider atomicity in a multithreaded environment. If it is a compound operation on shared variables, such as + +, - * =, / =,% =, + =, - =, < =, > > =, > > =, > > =, ^ =, it looks like a statement, but it is actually a collection of multiple statements.

We need to consider the security under 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 + + actually consists of three operations:

In a single threaded environment, there is no problem, but in a multi-threaded environment, problems may occur because they are not atomic operations.

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 atomic atomic classes:

    private AtomicInteger atomicInteger=new AtomicInteger(0);

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

Ensure the atomicity of multiple atomic class operations

If a method uses operations of multiple atomic classes, although a single atomic operation is atomic, it is not necessarily combined.

Let's take 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 defined two atomicintegs and operated on them in the update and get operations respectively.

Although atomicinteger is atomic, it is not when two different atomicintegers are combined. You may encounter problems during multithreading operations.

Similarly, we can use synchronization mechanism or lock 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 chain call. 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 returns a reference to this.

Let's see how to call in a multithreaded 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 confused.

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 lambda expressions must be final or final equivalent, we need to build a final array.

Read and write the value of 64bits

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

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, problems may occur if setlong and printlong methods are called multiple times in a multithreaded environment.

The solution is simple. Just define the long or double variable as volatile.

private volatile long i = 0;

Code for this article:

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

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>