今天刚看了一点Java并发编程实践,随手拿着Java源码分析了一下。
看到ReadWriteLock部分兴起,跟着源代码看到了ReentrantReadWriteLock,对其实现的读写分离锁原理非常好奇,平时都是基于如何使用,但是对其实现机制不甚了解,于是驱动自己看了看源码
大体上明白了其中的原理,就是设置了两个标识:
第一个exclusiveOwnerThread,标识线程是否是Write线程,如果有线程设置,则认为该线程为写线程,其它线程必须等待。
第二个标识是:state,该标识是读写线程会修改该值。
有点疑惑的是:
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable { /** Use serial ID even though all fields transient. */ private static final long serialVersionUID = 3737899427754241961L; /** * Empty constructor for use by subclasses. */ protected AbstractOwnableSynchronizer() { } /** * The current owner of exclusive mode synchronization. */ private transient Thread exclusiveOwnerThread; /** * Sets the thread that currently owns exclusive access. A * <tt>null</tt> argument indicates that no thread owns access. * This method does not otherwise impose any synchronization or * <tt>volatile</tt> field accesses. */ protected final void setExclusiveOwnerThread(Thread t) { exclusiveOwnerThread = t; } /** * Returns the thread last set by * <tt>setExclusiveOwnerThread</tt>, or <tt>null</tt> if never * set. This method does not otherwise impose any synchronization * or <tt>volatile</tt> field accesses. * @return the owner thread */ protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; } }
这个类中关于exclusiveOwnerThread的读写居然没有加锁保护,并且代码注释上也明确说明不需要这个锁保护。
这让我困惑不已,为啥不需要加锁保护,从代码的调用结构上看try*系列的方法都会对该值作变更,这不就已经涉及到多线程操作了吗?
后来想了想,自己给了个说法:
看了看try*系列方法实现,可以列举其中一个方法说明:
final boolean tryWriteLock() { Thread current = Thread.currentThread(); int c = getState(); if (c != 0) { int w = exclusiveCount(c); if (w == 0 ||current != getExclusiveOwnerThread()) return false; if (w == MAX_COUNT) throw new Error("Maximum lock count exceeded"); } if (!compareAndSetState(c, c + 1)) return false; setExclusiveOwnerThread(current); return true; }
该方法中w == 0 ||current != getExclusiveOwnerThread() 这句代码十分关键,这就是这句代码主导了不使用同步锁也能保证判断的正确性。
首先分析有两种线程有可能会调用该方法:第一种写线程,第二种读线程
关键点:一个线程对一个变量的前后读写,在本线程是始终可以看到结果的。
如果是写线程调用的时候,那么肯定是该写线程首先对exclusiveOwnerThread进行了赋值写入(获取锁定),那么在同一个线程中赋值是可见的,也就是在接下来的释放过程中,并不需要内存同步。
如果是读线程调用的时候,唯一有可能发生意外的情况是:
该读线程是由前面的写线程转变而来,可能会想到current == getExclusiveOwnerThread()成立的情况,但是这种情况是不存在的,因为要从写线程转变成读线程,首先就是得有释放写锁的过程(如果没有锁的释放过程,那么程序逻辑本身就存在错误了),也就是有了setExclusiveOwnerThread(null)的过程,那么转变过后的读线程应该看到的null或者是锁被释放过后其它线程设置的值,而非以前本线程在身为写线程时数值。
注意区别:读线程和写线程中获取读锁的区别,这里的读线程是指只获取读锁的线程,与写线程中先获取写锁,然后再获取读锁是有区别的。
如果该线程是从上一次调用的写线程转化而来,那么这次看到的自己的内存exclusiveOwnerThread应该是null,一定不会是自己。那就保证 if (w == 0 ||current != getExclusiveOwnerThread())正确性了。
一句话就是:如果是写线程那么看到的exclusiveOwnerThread一定是自己,如果是读线程那么看到的exclusiveOwnerThread一定不是自己!
那么有可能读到不是最新的exclusiveOwnerThread值已经不重要!重要的是保证了如果是读线程,那么current != getExclusiveOwnerThread()一定正确!