Java 深入分析

Java 深入分析 - 并发 同步器

2017-08-06  本文已影响0人  林柚柚_

小概

synchronized 存在许多缺陷,使得使用非常困难,一旦开始请求锁便不能停止

作为一门面向对象的语言,我们希望锁也能变得可扩展,因此如何抽象将变得十分有必要,我们应当模仿 synchronized [1] 的实现,并在细节处进行扩展

Jdk 中有一系列 同步器,几乎所有同步器都是基于 AbstractQueuedSynchronizer 实现,我们由此开始讨论

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer 是一份同步模板,同步器通过封装继承该模板的匿名子类达到同步效果,该类能够省去同步器用于同步方面的很多代码,因此便将其抽象封装成了一份模板, AbstractQueuedSynchronizer 是理解同步器的重中之重

内部主要基于两方面实现

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private transient volatile Node tail;

    private transient volatile Node head;

    private volatile int state;

    static final class Node {

        volatile Node prev;

        volatile Node next;

        volatile Thread thread;

        volatile int waitStatus;

        Node nextWaiter;

        // ...
    }

    // ...

}

同步节点等待状态分为四种

AbstractQueuedSynchronizer 支持 独占式共享式 访问资源,但 如何获取和释放并没有实现,这和同步状态息息相关,默认交给子类

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

独占式获取与释放

独占式意味着只有一个线程可以占用资源,并排斥其他线程

AbstractQueuedSynchronizer 中独占式获取,仅有 head 占有资源,其后继节点将被挂起

独占式释放后,会搜索第一个非 CANCELLED 节点,并将其唤醒

共享式获取与释放

独占式访问资源时,排斥其他访问

共享式访问资源时,其他共享式的访问均被允许,而独占式应当被排斥

中断获取

AbstractQueuedSynchronizer 支持获取时对中断的响应,在获取途中如若发生中断,会立即 抛出中断异常,那么获取操作将马上得到释放

超时获取

指定超时时间,如果 获取超过限定时间,获取操作会马上返回

我们如此次假设,超时时间为 timeout,执行一次获取操作前的时间为 begin,此次获取失败后的时间为 now

那么还能够执行获取操作的时间,就应当限定在 timeout -= (now - begin) 内,超出时间范围需立马返回

Condition

wait / notify / notifyAllsynchronized 配套使用

那么这里相对应的就是 await / signal / signalAll,在 Condition 接口中,还支持 非中断响应等待超时等待

关于 Condition 的实现部分和 synchronized 其实是差不多的

能够等待或唤醒的线程,必定是已经获取锁的线程,此处不需要额外的 CAS 操作

Condition 内部维护了一个 等待队列

    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
        // ...
    }

如何实现获取与释放

实现获取与释放,应当基于 控制同步状态,我们通过操作同步状态来达到需要的特定要求,下面给出一个例子,例中为一把最大两个线程同时占用的共享锁,其中同步状态表示占有资源的线程个数,只有同步状态在限定范围内时才会返回,由此来完成共享逻辑

public class TwinsLock implements Lock {

    private final SharedSyn syn = new SharedSyn(2);

    @Override
    public void lock() {
        syn.acquireShared(1);
    }

    @Override
    public void unlock() {
        syn.releaseShared(1);
    }

    // ...

    private static class SharedSyn extends AbstractQueuedSynchronizer {

        private final int maxCount;

        Syn(int maxCount) {
            this.maxCount = maxCount;
        }

        @Override
        public int tryAcquireShared(int count) {
            while (true) {
                int currentCount = getState();
                int newCount = currentCount + count;
                if (newCount <= maxCount &&
                    compareAndSetState(currentCount, newCount)) {
                    return newCount;
                }
            }
        }

        @Override
        public boolean tryReleaseShared(int count) {
            while (true) {
                int currentCount = getState();
                int newCount = currentCount - count;
                if (compareAndSetState(currentCount, newCount)) {
                    return true;
                }
            }
        }
    }

}

ReentrantLock

ReentrantLock 具有以下性质

内部基于 AbstractQueuedSynchronizer 实现,在 ReentrantLock 中,同步状态表示占有资源的线程个数

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {...}

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {...}

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {...}

可重入

当一个持有锁的线程,再次访问资源时应当允许访问,即为可重入

ReentrantLock 中,当前 state != 0 时,即有线程在占用资源,然后发现 持有锁的人就是自己,将会允许入内,并且 state++

在释放时,资源的 最终释放将会在 state = 0

公平与非公平

ReentrantLock 实现了两种获取机制,一种公平获取,另一种非公平获取,默认为非公平竞争模式

公平获取相当与排队买饭 - 进入等待同步队列

非公平获取相当与插队抢饭 - 绕过同步队列,直接尝试获取,失败再进入等待队列

非公平锁在被唤醒时也可能直接插队

ReentrantReadWriteLock

前面提到的 ReentrantLocksynchronized 都是独占锁,而此时的 ReentrantReadWriteLock 是独占式和共享式共存的一种锁,执行写时是独占模式,执行读时是共享模式

也就是说,写时排斥一切操作,读时容纳其他读操作,但排斥写操作,因此,在读多写少的情况下,ReentrantReadWriteLock 同步器效率将会非常高

ReentrantReadWriteLock 由于存在两种模式,如何设计读写状态变得十分关键

读写状态

ReentrantLock 中,只存在一种模式,因此同步状态可表示为占有资源的线程数量,但 ReentrantReadWriteLock 中却两种模式并存

最简单的一种方式,就是将身为 Integer同步状态分成两部分,高 16 位代表共享式,低 16 位代表独占式

设同步状态为 state

获取与释放

我们通过一个具体的例子来讨论,ReentrantReadWriteLock 应该如何实现获取与释放

读操作用 R 表示,写操作用 W 表示,此时同步队列如下表所示,如果全是 不同线程的访问,那么:W -> 3R -> W -> 2R

读写锁在获取过程中也是可重入的,也就是说持有锁的同一线程,是能重复获取锁的

锁降级

以上例子代表读写分开的操作,但 如果一个操作将读与写杂糅在一起,如果要保证这一操作的原子性,比如写完后马上读,必须在写锁释放前获取到读锁,不然其他线程很可能在这一瞬间占领写锁,原子性便会从此丧失,这种操作叫做 锁降级

    public void writeAndRead() {
        writeLock.lock();
        try {
            // write...
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        try {
            // read...
        } finally {
            readLock.unlock();
        }
    }

其他同步器

除了上面描述的两种同步器,Jdk 中还有许多同步器,如 CyclicBarrierCountDownLatchSemaphore ... 我们在这里就不一一阐述了,有兴趣可以看看源码

参考

  1. 《Java并发编程的艺术》

  1. Java 深入分析 - 并发 Synchronized

上一篇下一篇

猜你喜欢

热点阅读