Problems encountered

Recently, I am maintaining an old project of the company, which is a completely independent application. There is a user check-in plus points interface, in order to prevent the user multiple requests caused by the database to insert multiple records. In short, it is a problem of ensuring the idempotency of a new interface. We don’t need to talk about other solutions, such as database unique index, token mechanism, single application can use synchronized or JDK built-in ReentrantLock, etc.

Discussion and conclusion

My initial thought was to simply add the synchronized keyword to the method to qualify it. This is definitely the easiest one.

public  synchronized Rsp addUserIntegral(AddUserIntegralReq req){
    // check whether the user has checked in today
    // 2. If you do not check in, add credits
}
Copy the code

But there is a problem with synchronized, which modifies methods, which hold a lock on the current object (this); At the same time, our application exists in Spring, so the current object is a singleton object by default, which will cause that when Zhang SAN adds the integral, Li Si cannot add it! The concurrency performance is greatly reduced. A different approach is required, where each request is not the same object as the lock.

I came up with the idea of passing in an HttpServletRequest object as a lock object (naturally a different object for each request), and it turns out you can’t do that. There is no mutual exclusion in locking.

After a nap, it’s easy to think of using the AddUserIntegralReq method parameter as a lock object, where each user’s request is different from the same user’s request. In this way, it can be mutually exclusive for different users. Here is the class structure of AddUserIntegralReq

public class AddUserIntegralReq{
    private String userAccount; // This is the only primary key
    private Integer points;
    //getter setter ...
}
Copy the code

I tried to reset the class’s hashCode and equals methods to use this as a lock object. Here is the result:

public class Test {

    public void test(Demo demo){
        synchronized (demo){
            if (demo.id.equals("1")){
                System.out.println(demo.getId());
                while(true);
            }else{ System.out.println(demo.getId()); }}}static class Demo{
        private String id;
        private Integer points;
        //getter setter ...
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null|| getClass() ! = o.getClass())return false;
            Demo demo = (Demo) o;
            return Objects.equals(id, demo.id) && Objects.equals(points, demo.points);
        }
        @Override
        public int hashCode(a) {
            returnObjects.hash(id, points); }}}Copy the code

Test code:

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        / / thread 1
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                Demo demo = new Demo();
                demo.setId("1");
                demo.setPoints(10);
                test.test(demo);
            }
        }).start();
        Thread.sleep(1000);/ / thread 2
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                Demo demo = new Demo();
                demo.setId("1");
                demo.setPoints(10);
                test.test(demo);
            }
        }).start();
    }
Copy the code

Create an “identical object”. Assuming synchronized uses equals to determine whether an object is identical, the test code would print only one “1”, but both threads print “1” instead. The Demo object held by synchronized does not achieve the mutually exclusive effect I want. It is deduced that synchronized is used to compare == address and hold the same lock object.

Although it is concluded that synchronized uses address comparison and holds the same lock object, how to solve the actual business situation encountered above? I came up with the idea of using userAccount as the lock object because it is a String. If it is not a new String, it is stored in the constant pool and can be held by different references. I was so excited that I changed the code immediately

    public void test(Demo demo){
        synchronized (demo.getId()){
            if (demo.id.equals("1")){
                System.out.println(demo.getId());
                while(true);
            }else{ System.out.println(demo.getId()); }}}Copy the code

Test code unchanged

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        / / thread 1
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                Demo demo = new Demo();
                demo.setId("1");
                demo.setPoints(10);
                test.test(demo);
            }
        }).start();
        Thread.sleep(1000);/ / thread 2
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                Demo demo = new Demo();
                demo.setId("1");
                demo.setPoints(10);
                test.test(demo);
            }
        }).start();
    }
Copy the code

This time the result is perfectly as expected, except that thread 1 prints “1”.

So I will test the Demo to write the lock way move to the formal code, occasionally buy ga ~!! Flipped over, and here’s what it looked like after I flipped over

public  Rsp addUserIntegral(AddUserIntegralReq req){
    synchronized(req.getUserAccount()){
         // check whether the user checked in today
         // add credits if there is no check-in}}Copy the code

Nani!! Req.getuseraccount () does not return the same object from getUserAccount() twice! Why is this?

It just clicked! Spring MVC problem!! Two requests in the Controller using the @requestBody annotation to convert the json string passed in from the front end into an object. A wild guess: When JackSon converts a JSON String to an object, all the attributes of the object are on the Java heap, which means that the String is assigned in a way similar to demo.setid (new String(“1”)). The result is that the userAccount object obtained twice is an object on the heap rather than the same object in the constant pool.

Demo demo = new Demo();
demo.setId("1");
demo.setPoints(10);
String s = JSONUtil.toJsonStr(demo);
Demo demo1 = JSONUtil.toBean(s, Demo.class);
Demo demo2 = JSONUtil.toBean(s, Demo.class);
System.out.println(demo1.getId() == demo2.getId());
Copy the code

The output false is broken, but the original task is not completed. It ended up having to be implemented in a very lame way (if you think there is a better way to do this, please let me know)


private static final Map<String,Object> lockMap = new ConcurrentHashMap<>();
public  Rsp addUserIntegral(AddUserIntegralReq req){
    String userAccount = req.getUserAccount();
    if(! lockMap.contains(userAccount)){ lockMap.put(userAccount,new Object());
    }
    try{
      synchronized(lockMap.get(userAccount)){
         // Business logic}}finally{
        // Prevent memory overflowlockMap.remove(userAccount); }}Copy the code

This is equivalent to mapping a unique primary key to a unique object.

PS: If you have a better way, please let me know