VarHandle: A powerful tool to ensure variable read and write visibility, order, and atomicity in Java9

Article directory

  • 1. What is VarHandle
    • 0. JMM
    • 1. Implementation of lock-free technology before jdk9
  • Two, VarHandle use
    • 1. Quick start with VarHandle
    • 2. Common methods of VarHandle
    • 3. Practical case 1: Solve visibility (lighter than volatile)
    • 4. Practical case 2: Solve instruction reordering (lighter than volatile)
      • (1) Case study: partial ordering
      • (2) Case study: total ordering

1. What is VarHandle

0, JMM

In-depth explanation of the JMM memory model, exploring the deep mysteries of volatile, synchronized and VarHandle

1. Implementation of lock-free technology before jdk9

Before JDK9, the lock-free technology in Java was mainly reflected in the atomic operation class represented by AtomicInteger, and its bottom layer was implemented by Unsafe, and the problem of Unsafe is security and portability.

In addition, volatile mainly uses the Store-Load barrier to control the order. This barrier is still too strong. Is there a more lightweight solution?

2. VarHandle usage

1. Quick start with VarHandle

VarHandle was introduced in Java9 to provide a finer-grained memory barrier to ensure the visibility, order, and atomicity of reading and writing of shared variables. Provides better security and portability, replacing some functions of Unsafe.

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class TestVarHandle {<!-- -->
int x; // shared variable
static VarHandle X; // The operation object VarHandle of the shared variable is time-consuming to create, so it is best not to create it repeatedly
\t
static {<!-- -->
try {<!-- -->
// Initialize VarHandle: you need to specify the variable to be protected, shared variable name shared variable type in a certain class
X = MethodHandles. lookup()
.findVarHandle(TestVarHandle.class, "x", int.class);
} catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
e.printStackTrace();
}
}
\t
    public static void main(String[] args) {<!-- -->
        TestVarHandle obj = new TestVarHandle();
        X.set(obj, 10); // set value
        Object o = X.get(obj); // get the value
        System.out.println(o);
    }
}

2. Common methods of VarHandle

3. Practical case 1: Solve visibility (lighter than volatile)

The following example is a classic case, the loop will never stop, the specific reason will not be analyzed here, it is because of the visibility. Solving this problem is very simple, just mark stop with volatile.

// never ending loop - visibility issue
public class TestInfinityLoop {<!-- -->
    static boolean stop = false; //stop mark

    public static void main(String[] args) throws InterruptedException {<!-- -->
        Thread t = new Thread(() -> {<!-- -->
            try {<!-- -->
                Thread. sleep(1000);
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
            }
            stop = true; // write of volatile
        });
        System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        t. start();
        foo();
    }

    private static void foo() {<!-- -->
        while (true) {<!-- -->
            boolean b = stop; // volatile read
            if (b) {<!-- -->
                break;
            }
        }
        System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    }
}


The VarHandle introduced by jdk9 can replace volatile, and can also realize the visibility of shared variables:

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Date;

public class TestVarHandleOpaque {<!-- -->
    static boolean stop;

    static VarHandle STOP;

    static {<!-- -->
        try {<!-- -->
            STOP = MethodHandles. lookup()
                    .findStaticVarHandle(TestVarHandleOpaque.class, "stop", boolean.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {<!-- -->
        new Thread(() -> {<!-- -->
            while (true) {<!-- -->
                if ((boolean) STOP. getOpaque()) {<!-- -->
                    break;
                }
            }
            System.out.println("quit " + new Date());
        }).start();

        System.out.println("start " + new Date());
        Thread. sleep(1000);
        STOP. setOpaque(true);
    }
}

4. Practical case 2: Solve instruction reordering (lighter than volatile)

Similarly, the following example can use VarHandle to solve the problem of instruction reordering:

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

@JCStressTest
@State
// r1 r2
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE)
@Outcome(id = "0, 2", expect = Expect.ACCEPTABLE)
@Outcome(id = "1, 2", expect = Expect.ACCEPTABLE)
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING)
public class Test4Possible {<!-- -->
    int a;
    int b;
    @Actor // Thread 1
    public void action1(II_Result r) {<!-- -->
        a = 1;
        r.r2 = b;
    }
    @Actor // Thread 2
    public void action2(II_Result r) {<!-- -->
        b = 2;
        r.r1 = a;
    }
}

public class TestVarHandle {<!-- -->
    /**
     * Case 1: Demonstrates that VarHandle can be used to solve partial ordering problems
     */
    @JCStressTest
    @Outcome(id = {<!-- -->"0, 1","0, 0","1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE\ ")
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "INTERESTING")
    @State
    public static class Case1{<!-- -->
        int x;
        int y;
        static VarHandle Y;
        static {<!-- -->
            try {<!-- -->
                Y = MethodHandles. lookup(). findVarHandle(Case1. class, "y", int. class);
            } catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
                e.printStackTrace();
            }
        }

        @Actor
        public void actor1(II_Result r) {<!-- -->
            x = 1;
            Y. setRelease(this, 1);
        }

        @Actor
        public void actor2(II_Result r) {<!-- -->
            r.r1 = (int) Y.getAcquire(this);
            r.r2 = x;
        }
    }

    /**
     * Case 2: Demonstrate that VarHandle can solve the total ordering situation
     */
    @JCStressTest
    @Outcome(id = {<!-- -->"1, 0","0, 0","0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE\ ")
    @Outcome(id = "1, 1", expect = Expect.FORBIDDEN, desc = "INTERESTING")
    @State
    public static class Case2{<!-- -->
        int x;
        int y;

        static VarHandle X;
        static VarHandle Y;
        static {<!-- -->
            try {<!-- -->
                X = MethodHandles. lookup(). findVarHandle(Case2. class, "x", int. class);
                Y = MethodHandles. lookup(). findVarHandle(Case2. class, "y", int. class);
            } catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
                e.printStackTrace();
            }
        }

        @Actor
        public void actor1(II_Result r) {<!-- -->
            r.r2 = (int) X.getAcquire(this);
            Y. setRelease(this, 1);
        }

        @Actor
        public void actor2(II_Result r) {<!-- -->
            r.r1 = (int) Y.getAcquire(this);
            X. setRelease(this, 1);
        }
    }


    /**
     * Case 3: Demonstration that VarHandle cannot solve the total ordering situation
     */
    @JCStressTest
    @Outcome(id = {<!-- -->"1, 0","1, 1","0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE\ ")
    @Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case3{<!-- -->
        int x;
        int y;

        static VarHandle X;
        static VarHandle Y;
        static {<!-- -->
            try {<!-- -->
                X = MethodHandles. lookup(). findVarHandle(Case3. class, "x", int. class);
                Y = MethodHandles. lookup(). findVarHandle(Case3. class, "y", int. class);
            } catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
                e.printStackTrace();
            }
        }

        @Actor
        public void actor1(II_Result r) {<!-- -->
            Y. setRelease(this, 1);
            r.r2 = (int) X.getAcquire(this);
        }

        @Actor
        public void actor2(II_Result r) {<!-- -->
            X. setRelease(this, 1);
            r.r1 = (int) Y.getAcquire(this);
        }
    }
}

(1) Case study: partial ordering

Case 1, if y=1 occurs first, then the previous Store barrier will prevent x=1 from going down, and the subsequent Load barrier will prevent the subsequent r.r2=x from going up

If it happens after y=1, then the Store barrier will prevent x=1 from going down, and the Load barrier will prevent the subsequent r.r2=x from going up. Similarly, the execution order of r.r2=x and x=1 cannot be controlled ( case 2 case 3) It is also possible that x=1 is before r.r1=y, but the result is the same as case 3

(2) Case study: total ordering





In case 4, note that the code between the red barriers can still be rearranged, and the blue ones are the same, because in case 3, the red Load barrier can only prevent the code below from being uploaded, and the Store barrier can only prevent the code above Can’t go down, but in between, code order is not guaranteed