Java并发编程实战 原子变量与非阻塞同步机制总结

锁的劣势
现代的许多JVM都对非竞争锁获取和锁释放等操作进行了极大的优化 但如果有多个线程同时请求锁 那么JVM就需要借助操作系统的功能 如果出现了这种情况 那么一些线程将被挂起并且在稍后恢复运行 当线程恢复执行时 必须等待其他线程执行完它们的时间片以后 才能被调度执行 在挂起和恢复线程等过程中存在着很大的开销 并且通常存在着较长时间的中断 如果在基于锁的类中包含有细粒度的操作(例如同步容器类 在其大多数方法中只包含了少量操作) 那么当在锁上存在着激烈的竞争时 调度开销与工作开销的比值会非常高

硬件对并发的支持
独占锁是一项悲观技术——它假设最坏的情况(如果你不锁门 那么捣蛋鬼就会闯入并搞得一团糟) 并且只有在确保其他线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去
对于细粒度的操作 还有另外一种更高效的方法 也是一种乐观的方法 通过这种方法可以在不发生干扰的情况下完成更新操作 这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰 如果存在 这个操作将失败 并且可以重试(也可以不重试)

比较并交换
在大多数处理器架构(包括IA32和Sparc)中采用的方法是实现一个比较并交换(CAS)指令 (在其他处理器中 例如PowerPC 采用一对指令来实现相同的功能:关联加载与条件存储) CAS包含了3个操作数——需要读写的内存位置V 进行比较的值A和拟写入的新值B 当且仅当V的值等于A时 CAS才会通过原子方式用新值B来更新V的值 否则不会执行任何操作 无论位置V的值是否等于A 都将返回V原有的值 (这种变化形式被称为比较并设置 无论操作是否成功都会返回) CAS的含义是 我认为V的值应该为A 如果是 那么将V的值更新为B 否则不修改并告诉V的值实际为多少 CAS是一项乐观的技术 它希望能成功地执行更新操作 并且如果有另一个线程在最近一次检查后更新了该变量 那么CAS能检测到这个错误

模拟CAS操作

@ThreadSafe
public class SimulatedCAS {
    @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized int compareAndSwap(int expectedValue,
                                           int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue)
            value = newValue;
        return oldValue;
    }

    public synchronized boolean compareAndSet(int expectedValue,
                                              int newValue) {
        return (expectedValue
                == compareAndSwap(expectedValue, newValue));
    }
}

CAS的典型使用模式是:首先从V中读取值A 并根据A计算新值B 然后再通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其他值) 由于CAS能检测到来自其他线程的干扰 因此即使不使用锁也能够实现原子的读-改-写操作序列

非阻塞的计数器

基于CAS实现的非阻塞计数器

@ThreadSafe
public class CasCounter {
    private SimulatedCAS value;

    public int getValue() {
        return value.get();
    }

    public int increment() {
        int v;
        do {
            v = value.get();
        } while (v != value.compareAndSwap(v, v + 1));
        return v + 1;
    }
}

CasCounter不会阻塞 但如果其他线程同时更新计数器 那么会多次执行重试操作(在实际情况中 如果仅需要一个计数器或序列生成器 那么可以直接使用AtomicInteger或AtomicLong 它们能提供原子的递增方法以及其他算术方法)
初看起来 基于CAS的计数器似乎比基于锁的计数器在性能上更差一些 因为它需要执行更多的操作和更复杂的控制流 并且还依赖看似复杂的CAS操作 但实际上 当竞争程度不高时 基于CAS的计数器在性能上远远超过了基于锁的计数器 而在没有竞争时甚至更高
CAS的主要缺点是 它将使调用者处理竞争问题(通过重试 回退 放弃) 而在锁中能自动处理竞争问题(线程在获得锁之前将一直阻塞)

JVM对CAS的支持
在Java5.0之前 如果不编写明确的代码 那么就无法执行CAS 在Java5.0中引入了底层的支持 在int long和对象的引用等类型上都公开了CAS操作 并且JVM把它们编译为底层硬件提供的最有效方法 在支持CAS的平台上 运行时把它们编译为相应的(多条)机器指令 在最坏的情况下 如果不支持CAS指令 那么JVM将使用自旋锁 在原子变量类(例如java.util.concurrent.atomic中的AtomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作 而在java.util.concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类

原子变量类
原子变量比锁的粒度更细 量级更轻 并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的 原子变量将发生竞争的范围缩小到单个变量上 这是你获得的粒度最细的情况(假设算法能够基于这种细粒度来实现) 更新原子变量的快速(非竞争)路径不会比获取锁的快速路径慢 并且通常会更快 而它的慢速路径肯定比锁的慢速路径快 因为它不需要挂起或重新调度线程 在使用基于原子变量而非锁的算法中 线程在执行时更不易出现延迟 并且如果遇到竞争 也更容易恢复过来

原子变量是一种 更好的volatile

通过CAS来维持包含多个变量的不变性条件

@ThreadSafe
        public class CasNumberRange {
    @Immutable
            private static class IntPair {
        // INVARIANT: lower <= upper
        final int lower;
        final int upper;

        public IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values =
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setLower(int i) {
        while (true) {
            IntPair oldv = values.get();
            if (i > oldv.upper)
                throw new IllegalArgumentException("Can't set lower to " + i + " > upper");
            IntPair newv = new IntPair(i, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public void setUpper(int i) {
        while (true) {
            IntPair oldv = values.get();
            if (i < oldv.lower)
                throw new IllegalArgumentException("Can't set upper to " + i + " < lower");
            IntPair newv = new IntPair(oldv.lower, i);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}

性能比较:锁与原子变量

基于ReentrantLock实现的随机数生成器

@ThreadSafe
public class ReentrantLockPseudoRandom extends PseudoRandom {
    private final Lock lock = new ReentrantLock(false);
    private int seed;

    ReentrantLockPseudoRandom(int seed) {
        this.seed = seed;
    }

    public int nextInt(int n) {
        lock.lock();
        try {
            int s = seed;
            seed = calculateNext(s);
            int remainder = s % n;
            return remainder > 0 ? remainder : remainder + n;
        } finally {
            lock.unlock();
        }
    }
}

基于AtomicInteger实现的随机数生成器

@ThreadSafe
public class AtomicPseudoRandom extends PseudoRandom {
    private AtomicInteger seed;

    AtomicPseudoRandom(int seed) {
        this.seed = new AtomicInteger(seed);
    }

    public int nextInt(int n) {
        while (true) {
            int s = seed.get();
            int nextSeed = calculateNext(s);
            if (seed.compareAndSet(s, nextSeed)) {
                int remainder = s % n;
                return remainder > 0 ? remainder : remainder + n;
            }
        }
    }
}

在高度竞争的情况下 锁的性能将超过原子变量的性能 但在更真实的情况下 原子变量的性能将超过锁的性能 这是因为锁在发生竞争时会挂起线程 从而降低了CPU的使用率和共享内存总线上的同步通信量(这类似于在生产者-消费者设计中的可阻塞生产者 它能降低消费者上的工作负载 使消费者的处理速度赶上生产者的处理速度) 另一方面 如果使用原子变量 那么发成调用的类负责对竞争进行管理 与大多数基于CAS的算法一样 AtomicPseudoRandom在遇到竞争时将立即重试 这通常是一种正确的方法 但在激烈竞争环境下却导致了更多的竞争

非阻塞算法
如果在某种算法中 一个线程的失败或挂起不会导致其他线程也失败或挂起 那么这种算法就被称为非阻塞算法 如果在算法的每个步骤中都存在某个线程能够执行下去 那么这种算法也被称为无锁(Lock-Free)算法 如果在算法中仅将CAS用于协调线程之间的操作 并且能正确地实现 那么它既是一种无阻塞算法 又是一种无锁算法 无竞争的CAS通常都能执行成功 并且如果有多个线程竞争同一个CAS 那么总会有一个线程在竞争中胜出并执行下去 在非阻塞算法中通常不会出现死锁和优先级反转问题(但可能会出现饥饿和活锁问题 因此在算法中会反复地重试)

非阻塞的栈
在实现相同功能的前提下 非阻塞算法通常比基于锁的算法更为复杂 创建非阻塞算法的关键在于 找出如何将原子修改的范围缩小到单个变量上 同时还要维护数据的一致性 在链式容器类(例如队列)中 有时候无需将状态转换操作表示为对节点链接的修改 也无需使用AtomicReference来表示每个必须采用原子操作来更新的链接
栈是最简单的链式数据结构:每个元素仅指向一个元素 并且每个元素也只被一个元素引用

使用Treiber算法(Treiber 1986)构造的非阻塞栈

@ThreadSafe
        public class ConcurrentStack <E> {
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();

    public void push(E item) {
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        do {
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    public E pop() {
        Node<E> oldHead;
        Node<E> newHead;
        do {
            oldHead = top.get();
            if (oldHead == null)
                return null;
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

    private static class Node <E> {
        public final E item;
        public Node<E> next;

        public Node(E item) {
            this.item = item;
        }
    }
}

非阻塞的链表
CAS的基本使用模式:在更新某个值时存在不确定性 以及在更新失败时重新尝试 构建非阻塞算法的技巧在于:将执行原子修改的范围缩小到单个变量上 这在计数器中很容易实现 在栈中也很简单 但对于一些更复杂的数据结构 例如队列 散列表或树 则要复杂得多
链接队列比栈更为复杂 因为它必须支持对头节点和尾节点的快速访问 因此 它需要单独维护的头指针和尾指针 有两个指针指向位于尾部的节点:当前最后一个元素的next指针 以及尾节点 当成功地插入一个新元素时 这两个指针都需要采用原子操作来更新 初看起来 这个操作无法通过原子变量来实现 在更新这两个指针时需要不同的CAS操作 并且如果第一个CAS成功 但第二个CAS失败 那么队列将处于不一致的状态 而且 即使这两个CAS都成功了 那么在执行这两个CAS之间 仍可能有另一个线程会访问这个队列 因此 在为链接队列构建非阻塞算法时 需要考虑到这两种情况

Michael-Scott(Michael and Scott 1996)非阻塞算法中的插入算法

@ThreadSafe
public class LinkedQueue <E> {

    private static class Node <E> {
        final E item;
        final AtomicReference<LinkedQueue.Node<E>> next;

        public Node(E item, LinkedQueue.Node<E> next) {
            this.item = item;
            this.next = new AtomicReference<LinkedQueue.Node<E>>(next);
        }
    }

    private final LinkedQueue.Node<E> dummy = new LinkedQueue.Node<E>(null, null);
    private final AtomicReference<LinkedQueue.Node<E>> head
            = new AtomicReference<LinkedQueue.Node<E>>(dummy);
    private final AtomicReference<LinkedQueue.Node<E>> tail
            = new AtomicReference<LinkedQueue.Node<E>>(dummy);

    public boolean put(E item) {
        LinkedQueue.Node<E> newNode = new LinkedQueue.Node<E>(item, null);
        while (true) {
            LinkedQueue.Node<E> curTail = tail.get();
            LinkedQueue.Node<E> tailNext = curTail.next.get();
            if (curTail == tail.get()) {
                if (tailNext != null) {
                    // Queue in intermediate state, advance tail
                    tail.compareAndSet(curTail, tailNext);
                } else {
                    // In quiescent state, try inserting new node
                    if (curTail.next.compareAndSet(null, newNode)) {
                        // Insertion succeeded, try advancing tail
                        tail.compareAndSet(curTail, newNode);
                        return true;
                    }
                }
            }
        }
    }
}

原子的域更新器

在ConcurrentLinkedQueue中使用原子的域更新器

private class Node<E> {
	private final E item;
	private volatile Node<E> next;

	public Node(E item) {
		this.item = item;
}
}

private static AtomicReferenceFieldUpdater<Node, Node> nextUpdater
	= AtomicReferenceFieldUpdater.newUpdater(Node.calss, Node.class, "next");

原子的域更新器类表示现有volatile域的一种基于反射的 视图 从而能够在已有的volatile域上使用CAS 在更新器类中没有构造函数 要创建一个更新器对象 可以调用newUpdate工厂方法 并制定类和域的名字 域更新器类没有与某个特定的实例关联在一起 因而可以更新目标类的任意实例中的域 更新器类提供的原子性保证比普通原子类更弱一些 因为无法保证底层的域不被直接修改——compareAndSet以及其他算术方法只能确保其他使用原子域更新器方法的线程的原子性

ABA问题
ABA问题是一种异常现象:如果在算法中的节点可以被循环使用 那么在使用 比较并交换 指令时就可能出现这种问题(主要在没有垃圾回收机制的环境中) 在CAS操作中将判断 V的值是否仍然为A 并且如果是的话就继续执行更新操作 在大多数情况下 这种判断是完全足够的 然而 有时候还需要知道 自从上次看到V的值为A以来 这个值是否发生了变化 在某些算法中 如果V的值首先由A变成B 再由B变成A 那么仍然被认为是发生了变化 并需要重新执行算法中的某些步骤
如果在算法中采用自己的方式来管理节点对象的内存 那么可能出现ABA问题 在这种情况下 即使链表的头节点仍然指向之前观察到的节点 那么也不足以说明链表的内容没有发生改变 如果通过垃圾回收器来管理链表节点仍然无法避免ABA问题 那么还有一个相对简单的解决方案:不是更新某个引用的值 而是更新两个值 包括一个引用和一个版本号 即使这个值由A变为B 然后又变为A 版本号也将是不同的 AtomicStampedReference(以及AtomicMarkableReference)支持在两个变量上执行原子的条件更新 AtomicStampedReference将更新一个 对象-引用 二元组 通过在引用上加上 版本号 从而避免ABA问题 类似地 AtomicMarkableReference将更新一个 对象引用-布尔值 二元组 在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为 已删除的节点

小结
非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维护线程的安全性 这些底层的原语通过原子变量类向外公开 这些类也用做一种 更好的volatile变量 从而为整数和对象引用提供原子的更新操作
非阻塞算法在设计和实现时非常困难 但通常能够提供更高的可伸缩性 并能更好地防止活跃性故障的发生 在JVM从一个版本升级到下一个版本的过程中 并发性能的主要提升都来自于(在JVM内部以及平台类库中)对非阻塞算法的使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
大学生参加学科竞赛有着诸多好处,不仅有助于个人综合素质的提升,还能为未来职业发展奠定良好基础。以下是一些分析: 首先,学科竞赛是提高专业知识和技能水平的有效途径。通过参与竞赛,学生不仅能够深入学习相关专业知识,还能够接触到最新的科研成果和技术发展趋势。这有助于拓展学生的学科视野,使其对专业领域有更深刻的理解。在竞赛过程中,学生通常需要解决实际问题,这锻炼了他们独立思考和解决问题的能力。 其次,学科竞赛培养了学生的团队合作精神。许多竞赛项目需要团队协作来完成,这促使学生学会有效地与他人合作、协调分工。在团队合作中,学生们能够学到如何有效沟通、共同制定目标和分工合作,这对于日后进入职场具有重要意义。 此外,学科竞赛是提高学生综合能力的一种途径。竞赛项目通常会涉及到理论知识、实际操作和创新思维等多个方面,要求参赛者具备全面的素质。在竞赛过程中,学生不仅需要展现自己的专业知识,还需要具备创新意识和解决问题的能力。这种全面的综合能力培养对于未来从事各类职业都具有积极作用。 此外,学科竞赛可以为学生提供展示自我、树立信心的机会。通过比赛的舞台,学生有机会展现自己在专业领域的优势,得到他人的认可和赞誉。这对于培养学生的自信心和自我价值感非常重要,有助于他们更加积极主动地投入学习和未来的职业生涯。 最后,学科竞赛对于个人职业发展具有积极的助推作用。在竞赛中脱颖而出的学生通常能够引起企业、研究机构等用人单位的关注。获得竞赛奖项不仅可以作为个人履历的亮点,还可以为进入理想的工作岗位提供有力的支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值