[Java] Memory visibility and lock efficiency test

[Java] Memory visibility and lock efficiency test

Let's talk about the conclusion first

The fundamental ways to brush memory visibility are : volatile, synchronized

Other ways to extend are : AtomicInteger (using volatile), System.out.println (using synchronized), lock(), the underlying implementation is volatile

Suggestion : using volatile and AtomicInteger lock-free code is more efficient than synchronizers and locks, using AtomicInteger is the safest


Update content :

The specific understanding of the happens before rule:

If thread 1 writes volatile variable v, then thread 2 reads v, then thread 1 writes v and previous write operations are visible to thread 2. In other words, if you perceive the change of volatile variable v, you can perceive all write operations before v, even if you write a non-volatile variable.

The state in AQS is volatile. In order to ensure visibility, volatile will add a lock instruction to the machine instructions. Lock forces the cache (working memory) to be written back to the memory (main memory), and invalidates the cache lines of other threads (MESI). ). It should be noted here that lock does not only write the variables modified by volatile back to the main memory, but also writes the changes in the working memory to the main memory~

So if any volatile variable is modified or locked or Atomic operation, all variables will be refreshed, not only volatile variables

www.zhihu.com/question/41...


Test content

In two threads, only the odd and even numbers are output and incremented, until one million.

There are four main methods, three of which are tested here. The main idea is to make two threads while loop plus if judgment. The focus here is on data communication between two threads, that is, the problem of flushing the main memory. Both sides may fall into a while loop

  1. synchronized+notify+wait

When the synchronizer is acquired and released, the main memory will be flushed, so there is no memory visibility problem using this

The efficiency is relatively low, 100w needs 8-9s, the advantage is that it is not easy to make mistakes

public class Test3 implements Runnable{

    public static int i =0;

    public static final int total =1000000;

    public boolean flag =true;

    public  void  count() throws InterruptedException {
        Long time = System.currentTimeMillis();
       //wait() 
        synchronized (Test3.class) {
            if (flag) {
                while (i < total) {
                    if (i % 2 == 0) {
                        i++;
                        Test3.class.notify();
                    } else {
                        Test3.class.wait();
                    }
                }
            } else {
                while (i < total) {
                    if (i % 2 == 1) {
                        i++;
                        Test3.class.notify();
                    } else {
                        Test3.class.wait();
                    }
                }
            }
        }
        System.out.println(System.currentTimeMillis() - time);
    }

    public Test3(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            count();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Test3 t1 = new Test3(true);
        Test3 t2 = new Test3(false);
        Thread aThread = new Thread(t1);
        Thread bThread = new Thread(t2);
        aThread.start();
        bThread.start();
    }
}
 
  1. lock

When acquiring and releasing the lock, the main memory will be swiped

The efficiency is not bad, the unfair lock only needs 600ms, the fair lock efficiency is relatively poor, it needs 8s

The disadvantages are 1. You need to pay attention to the use of unfair locks. 2. Because the locks are acquired and released manually, the position of the implant must be considered and must be placed before the variable judgment conditions. For example, the following writing will fall into an infinite loop, because there is no Exploit the memory visibility of the lock

    public  void  count() throws InterruptedException {
        Long time = System.currentTimeMillis();


            if (flag) {
                while (i < total) {
                    if (i % 2 == 0) {
                        test4.lock();
                        i++;
                        test4.unlock();
                    }
                }
            } else {
                while (i < total) {
                    if (i % 2 == 1) {
                        test4.lock();
                        i++;
                        test4.unlock();
                    }
                }
            }
        System.out.println(System.currentTimeMillis() - time);
    }
 

After adding System.out.println, it can continue to run for a few hundred milliseconds after it is stuck, because it also has the function of flushing the main memory, but the efficiency becomes extremely low, and I don t know why the loop is stuck. It will take a while to continue. It seems that it takes multiple executions to be effective

    public  void  count() throws InterruptedException {
        Long time = System.currentTimeMillis();


            if (flag) {
                while (i < total) {
                    if (i % 2 == 0) {
                        test4.lock();
                        i++;
                        test4.unlock();
                    }
                    System.out.println(i);
                }
            } else {
                while (i < total) {
                    if (i % 2 == 1) {
                        test4.lock();
                        i++;
                        test4.unlock();
                    }
                    System.out.println(i);
                }
            }
        System.out.println(System.currentTimeMillis() - time);
    }
 

The correct approach is this, just get the lock and brush it before judging.

    public  void  count() throws InterruptedException {
        Long time = System.currentTimeMillis();
            if (flag) {
                while (i < total) {
                    test4.lock();
                    if (i % 2 == 0) {
                        i++;
                        test4.unlock();
                    }else{
                        test4.unlock();
                    }
                }
            } else {
                while (i < total) {
                    test4.lock();
                    if (i % 2 == 1) {
                        i++;
                        test4.unlock();
                    }else{
                        test4.unlock();
                    }
                }
            }
        System.out.println(System.currentTimeMillis() - time);
    }
 

A better approach is to set the parameter i to volatile type, or to AtomicInteger type, the latter can also guarantee atomicity

    public static volatile int i =0;
 
    public static  AtomicInteger i = new AtomicInteger(0);
 

The efficiency is also very high, only 40ms


Finally, talk about the impact of System.out.println and idea-DEBUG mode on memory visibility

The System.out.println method has the logic of synchronization, which first obtains data from the main memory, and finally flushes the data to the main memory, which will have a great impact on the operation of the program in a multi-threaded environment. And the efficiency will be greatly reduced, try not to use

The DEBUG mode of idea often has some psychedelic effects, and the effects of constant clicks and breakpoints are inconsistent. The most fundamental reason is that after the breakpoint, the main memory will be actively refreshed. At this time, the data obtained by the breakpoint is consistent with The data at runtime is not necessarily consistent, so there may be deadlock conditions, a breakpoint will be solved

In general debug or log encounters weird situations, you can consider these two points first