[Java]内存可见性及锁的效率测试

200 阅读4分钟

先说结论

可以刷内存可见性的根本的方式有:volatile、 synchronized

延伸出来的其他方式有:AtomicInteger(使用了volatile)、System.out.println(使用了synchronized)、lock() 底层是volatile实现

建议:使用volatile和AtomicInteger无锁式代码效率高出同步器及锁很多,使用AtomicInteger最安全


更新内容

happens before规则具体理解:

——如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1 写入v及之前的写操作都对线程2可见。也就是说,如果你感知到了volatile变量v的变化,那么在v之前的所有写操作你都可以感知的到,就算写的是非volatile变量。

——AQS 中的 state 是 volatile的. volatile为了保证可见性, 会在机器指令中加入lock指令, lock强制把缓存(工作内存)写回内存(主内存), 并失效其它线程的缓存行(MESI). 这里要注意的是, lock并不仅仅只把被volatile修饰的变量写回主内存, 而是把工作内存中的变更都写入主内存~

所以如果有任何一个volatile变量的修改或者是上锁、Atomic操作,都会将所有变量刷一遍,不止是volatile变量

www.zhihu.com/question/41…


测试内容

分两个线程分别只输出奇偶数并自增,到一百万为止。

主要有4种方式,这里测试了三种,主要思路是让两个线程while循环加if判断,这里重点在于两个线程间的数据通信,也就是刷主存的问题,如果不通信,则有可能会两边都陷入while死循环

  1. synchronized+notify+wait

获取和释放同步器的时候,都会刷主存,所以使用这个没有任何内存可见性问题

效率比较低,100w需要8-9s,优点是不容易出错

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

获取和释放锁的时候,都会刷主存

效率还不错,非公平锁只需要600ms,公平锁效率比较差,需要8s

缺点是1、需要注意使用非公平锁,2由于是手动获取和释放锁,所以植入的位置要考虑好,一定要放在变量判断条件之前,比如下面这个写法就会陷入死循环,因为没有利用到锁的内存可见性

    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);
    }

加入System.out.println之后,可以在卡住之后几百毫秒的时间重新继续运行下去,因为它也有刷主存的作用,但是这样效率变得极低,而且不知道为什么还会卡住循环个一小段时间才能继续下去。看上去是要多次执行才有效果

    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);
    }

正确的做法是这样,在判断前获取锁刷一下就可以了

    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);
    }

更好的做法是将参数i设置成volatile类型,或者设置成AtomicInteger类型,后者还能保证原子性

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

效率也非常高,仅需40ms


最后说说System.out.println以及idea-DEBUG模式对内存可见性的影响

System.out.println方法中有同步synchronized的逻辑,会首先从主存获取数据,最后又会将数据刷到主存中,在多线程环境下会对程序运行有很大影响。而且效率也会极大降低,尽量不要使用

idea的DEBUG模式,经常会有一些迷幻效果,不断点和断点的效果不一致,这个最根本的原因就在于断点之后,会主动去刷一遍主存,这个时候你断点获得的数据和运行时的数据不一定是一致的,所以也有可能有的死锁状态,一个断点就解开了

一般debug或者日志遇到诡异的情况,可以先考虑这两点