synchronized锁可以让临界区互斥执行,但是我们常常忽略其内存语义

synchronized的内存语义定义如下:

  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而 使得被监视器保护的临界区代码必须从主内存中读取共享变量,与volatile读有相同的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷 新到主内存中,与volatile写有相同的内存语义

我们通过一个例子验证synchronized的内存语义:

public class SynchronizedMemory {
    static class ReorderExample {
        int a = 0;
        boolean flag = false;

        public void writer() {
            a = 1;
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
        }

        public void reader() {
//            synchronized (this) { // 进入synchronized块会读主内存,进入之后就不会了,所以这里加synchronized无效,需要加到if-flag位置
                while (true) {
                    if (flag) {
                        int i = a * a;
                        System.out.println(i);
                        break;
                    }
                    // println方法使用了synchronized,会从主内存读入flag,所以if可以读到flag被线程writer更新的值
//                    System.out.println(111111); // 使用synchronized (this){// no code here}同样的效果
                }
//            }
        }
    }

    public static void main(String[] args) throws Exception {
        ReorderExample reorderExample = new ReorderExample();
        Thread t1 = new Thread(() -> {
            reorderExample.writer();
        });
        Thread t2 = new Thread(() -> {
            reorderExample.reader();
        });
        t2.start();
        t1.start();
        t1.join();
        System.out.println(reorderExample.flag);
        t2.join();

    }
}

上面代码会打印true并且陷入无限等待的状态

分析原因

reader线程的flag变量存储在本地内存中,其值为改变前的旧值false,且reader线程在writer线程写入flag新值true并更新到主内存后没有更新其本地内存,导致reader线程死循环

利用synchronized的内存语义解决问题

当我们在while-true和if-flag之间加入synchronized,由于每次进入synchronized代码块会强制从主内存中读取flag变量,所以我们能看到输出1和true

同样的道理我们在if-flag的判断语句块之后调用打印也会输出1和true,查看打印的源码中得知其使用了synchronized锁,同样会强制reader线程从主内存中读取flag变量

其他

synchronized获取锁的内存定义中:

线程获取锁时,应该是线程对应的本地内存无效,使得线程对应的本地变量从主内存读取,而不仅仅是临界区的。

因为打印常数或者使用空的synchronized块都能使得reader线程读取到flag更新后的值

参考:

测试使用的是jdk1.8,参考书籍《Java并发编程的艺术》第3.5节