Java并发拾遗(四)——锁

一、锁的语义

锁机制是Java中最重要的同步机制,其能够使临界区的代码互斥执行,且执行的结果对下一个拿到锁的线程立即可见。个人感觉,大家普遍对锁的互斥性有普遍的理解,很容易忽略了锁提供的可见性的保证。试想,如果锁仅仅提供了互斥,在临界区代码执行完之后,不把相应的执行结果刷回主内存,那么下一个线程拿到锁之后,很可能看不到上一个线程的执行结果,这必然会出现问题的。所以,锁的语义包含了volatile的语义:

1. 互斥性:临界区代码互斥执行

2. 原子性:临界区代码的执行对外具有原子性

3. 可见性:临界区的代码的结果在锁释放时刷回主内存,对下一个拿到锁的线程立即可见

4. 重排序:以临界区为屏障,禁止临界区两侧的指令刺穿屏障进行重排序

二、锁语义的实现

在Java很多的并发工具类中,都依赖了AQS(AbstractQueuedSynchronizer)来实现同步。而在AQS中,偷偷的藏了一个volatile变量state,在加锁的时候,读这个变量,在释放锁的时候,写这个变量。so,利用这个volatile变量,很鸡贼的实现了上面的可见性与重排序的保证。

至于锁如何实现互斥性与原子性,这就得看Java AQS的源代码了。

三、补充(2017.5.7)

之前以为已经理解了锁的语义,然而实际上并木有。。。在effective Java中,有个double-check的栗子,代码如下:

    private volatile FieldType field;
    FieldType getField() {
        FieldType result = field;
        if (result == null) {
            synchronized (this) {
                result = field;
                if (result == null) {
                    field = result = new FieldType();
                }
            }
        }
        return result;
    }  

代码本身容易懂,然而当按照之前对锁语义的理解,即锁能够保证可见性(即在同步块内部对变量的写之后,其他所有线程都是对这个变量立即可见的)。这样子的话,就无法解释对field声明为volatile的意义了,因为synchronize已经完全足够了。

在深入理解之后,发现自己之前的理解有偏差,syachronized不是万能的。synchronize仅仅能保证自己写完的变量,对下一个获取到这个锁的变量是可见的。要是这时候存在另一个线程,并没有获取锁,而只是普通的读,那么是不一定能看到刚刚线程再同步块内部的写操作的结果的。因此,这段代码才需要将field声明为volatile,以来弥补外边一层check的普通读,否则double-check很可能就失去意义了。

换句话说,要是这段代码将两次check全部放在代码块里,就不会有问题了。然而当外层的check是普通的读时,就一定得加上volatile来解决可见性问题了,不是把共享变量塞到同步块中完成写操作就万事大吉了,还需要考虑到读这个变量的可见性问题。

换另一个角度,加锁时,相当于对共享变量的volatile读,释放锁时相当于volatile写。volatile 写 happens-before 后续对这个变量的volatile读,然而,volatile 写并不happens-before 后续对这个变量的普通读。

原文地址:https://www.cnblogs.com/dosmile/p/6736111.html