《Java并发编程实战》-3

对象的组合

4.1 设计线程安全的类

在设计线程安全类的过程中,需要包含一下三个基本要素:

  • 找出构成对象状态的所有变量。
  • 找出约束状态变量的不变性条件。
  • 建立对象状态的并发访问管理策略。

4.1.1 收集同步需求

如果不了解对象的不变性与厚颜条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要结组于原子性和封装性。

4.1.2 依赖状态的操作

4.13 状态的所有权

4.2 实例封闭

将数据封闭在对象内部,可以将数据的访问限制在对象的方法上,从而更加容易确保在访问数据时总能持有正确的锁。

封闭机制更加易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无需检查整个程序。

4.2.1 Java监视器模式

通过一个私有锁来保护状态

public class PrivateLock {
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod() {
        synchronized(myLock) {
            // 访问或修改Widget的状态
        }
    }
}

4.2.2 实例:车辆追踪

4.3 线程安全性的委托

4.3.1 示例:基于委托的车辆跟踪器

4.3.2 独立的状态变量

4.3.3 当委托失效时

如果一个类时由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性安全性委托给底层的状态变量。

如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以说安全地发布这个变量。

4.4 在现有的线程安全类中添加功能

扩展Vector并增加一个"若没有则添加"方法

@TheadSafe
public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if(absent) {
            add(x)
        }
        return absent;
    }
}

"扩展"方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。

4.4.1 客户端加锁机制

非现象安全的"若没有则添加"(不要这么做)

@NotThreadSafe
public class ListHelper<E> {
    public List<E> list = Colletions.synchronizedList(new ArrayList<E>());

    pblic synchronized boolean putIfAbsent(E x) {
        boolean absent = list.contains(x);
        if(absent)
            list.add(x);
        return absent;
    }
}

ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但是却使用了不同的锁。(list本身的锁和ListHelper对象的锁)

通过客户端加锁来实现“若没有则添加”(线程安全)

@ThreadSafe
public class ListHelper<E> {
    public List<E> list = Colletions.synchronizedList(new ArrayList<E>());

    pblic boolean putIfAbsent(E x) {
        synchronized(list) {
        boolean absent = list.contains(x);
        if(absent)
            list.add(x);
        return absent;
        }
    }
}

4.4.2 组合

通过组合实现“若没有则添加”

@ThreadSafe
public class ImporvedList<T> implements List<T> {
    private final List<T> list;

    public ImprovedList(List<T> list) { this.list = list;}

    public synchronized boolean putIfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains)
            list.add(x);
        return !contains;
    }

    public synchronized void clear() {
        lsit.clear();
    }

    // ... 按照类似的方法委托List的其他方法
}

事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

4.5 将同步策略文档化

在文档中说明客户端代码需要了解的线程安全性保证,以及代码人员需要了解的同步策略。

《Java并发编程实战》-15 第16章 Java内存模型

第16章 Java内存模型


16.1 什么内存模型,为什么需要它

JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对于其他线程可见。

16.1.1 平台的内存模型

在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期地与主内存进行协调。

Java程序不需要指定内存栅栏的位置,而只需通过正确地使用同步来找出何时访问共享状态。

16.1.2 重排序

各种使操作延迟或者看似乱序执行的不同原因,都可以归为重排序。

Java内存模型简介

Java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有操作定义了一个偏序关系,称之为Happens-Before关系,那么JVM可以对它们任意地重排序。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地重排序。

Happens-Before的规则包括:

  • 程序顺序规则。 如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
  • 监视器锁定规则。 在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则。 对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则。 子线程上对Thread.Start的调用必须在该线程中执行任何操作之前执行。
  • 线程结束规则。 线程中的任何操作都必须在其他线程测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  • 中断规则。 当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrup调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
  • 终结器规则。 对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 传递性 如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

16.1.4 借助同步

由于Happens-Before,因此有时候可以“借助(Piggyback)”现有同步机制的可见性属性。这需要将Happens-Before的程序顺序规则与其他某个顺序规则(通常是监视器锁规则或者volatile变量规则)结合起来,从而对某个未被锁保护的变量的访问操作进行排序。

在类库中提供的其他Happens-Before排序包括:

  • 将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行。
  • 在CountDownLatch上的倒数操作将在线程从闭锁的await方法中返回之前执行。
  • 释放Semaphore许可的操作将在从该Semaphore上获得一个许可之前执行。
  • Future表示的任务的所有操作将在从Future.get中返回之前执行。
  • 向Executor提交一个Runnable或Callable的操作将在任务开始之前执行。
  • 一个线程到达CyclicBarrier或Exchanger的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果CyclicBarrier使用一个栅栏操作,那么到达栅栏的操作将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。

16.2 发布

16.2.1 不安全的发布

除了不可变对象以外,使用被另一个线程初始化的对象通常都是被安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。

16.2.2 安全的发布

事实上,Happens-Before比安全发布提供了提供了更加可见性与顺序保证。

16.2.3 安全初始化模式

程序安全的延迟初始化

@ThreadSafe
public calss SafeLazyInitialization {
    private static Resource resource;
    public synchronized static Resource getInstance() {
        if (resource == null )
            resource = new Resource();
        return resource;
    }
}

由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁已确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。

@ThreadSafe
public calss ResourceFactory {
    private static Resource = new Resource();
    public static Resource getResource{
        return ResourceHolder.resource;
    }
}

16.2.4 双重检查加锁

DDL(双重检查加锁)已经被广泛地废弃了。

16.3 初始化过程中的安全性

初始化安全性将确保,对于被正确构造的对象,所有线程都能看到由构造函数为对象给各个final域设置的正确性,而不管采用何种方式来发布对象。而且,对于可以通过被正确构造对象中某个final域到达的任意变量(例如某个final数组中的元素,或者由一个final域引用的HashMap的内容)将同样对于其他线程是可见的。

@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("a",1)
        ...
        states.put("b",2)
        states.put("c",3)
    }
    public String getAbbreviation(String s) {
        return states.get(s);
    }
}

初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。

小结

Java内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的。其中包括确保这些操作是按照一种Happens-Before的偏序关系进行排序,而这种关系是基于内存操作和同步操作级别来定义的。

《Java并发编程实战》-14 第14章 构建自定义的同步工具

第15章 原子变量与非阻塞同步机制


15.1 锁得劣势

如果在基于锁的类中包含有细粒度的操作(例如同步容器类,在其他大多数方法中只包含了少量操作),那么当在锁上存在着激烈的竞争时,调度开销与工作开销的比值会非常高。

与锁相比,volatile变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换或线程调度等操作。

锁定还存在其他以下缺点。当一个线程正在等待锁时,它不能做任何其他事情。

15.2 硬件对并发的支持

现在几乎所有的现代处理器中都包含了某种形式的原子读-改-写指令,例如比较并交换(Compare-and-Swap)或者关联加载/条件储存(Load-Linked/Store-Conditional)。操作系统和JVM使用这些指令来实现锁和并发的数据结构。

15.2.1 比较并交换

CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另一个线程在最近一次检测后更新了该值,那么CAS能检测到这个错误。

当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同:当获取锁失败时,线程将被挂起来),而是被告知在这次竞争中失败。

CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何其他线程将V的值修改为其他值)。

15.2.2 非阻塞的计算器

一个很管用的经验法则是 :在大多数处理器上,在无竞争的锁获取和释放的“快速代码路径”上的开销,大约是CAS开销的两倍。

15.2.3 JVM对CAS的支持

在原子变量类(例如java.util.cocurrent.atomic中的AtomicXxx)中使用了底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util.concurrent中大多数类在实现时则直接或间接地使用了这些原子变量类。

15.3 原子变量类

原子变量比锁的粒度更细,量级更轻,并且对于多处理器系统上实现高性能的并发代码来说时非常关键的。

java中共有12个原子变量类,可分为4组:

  • 标量类(Scalar)
    AtomicInteger、 AtomicLOng、 AtomicBoolean以及AtomicReference。这些类都支持CAS,但是它们不宜用作基于散列的容器中的键值。
  • 更新器类
  • 数组类
    原子数组类(只支持Integer、Long和Reference版本)中的元素可以实现原子更新。
  • 复合变量。

15.3。1 原子变量是一种"更好的volatile"

原子变量相比volatile修饰的变量更有原子安全性。

性能比较:锁与原子变量

在中低难度的竞争下,原子变量能提供更高的可伸缩性,而在高清度的竞争下,锁能够更有效地避免竞争。

15.4.1 非阻塞的栈

栈是通过compareAndSet来修改的,因此将采用原子操作来更新top的引用,或者在发现存在其他线程干扰的情况下,修改操作将失败。

15.4.2 非阻塞的链表

15.4.2 原子的域更新器

原子的域更新类表示现有volatile域的一种基于反射的“视图”,从而能够在已有的volatile域上使用CAS。在更新器类中没有构造函数,要创建一个更新器对象,可以调用newUpdater工厂方法,并制定类和域的名字。

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

private class Node<E> {
    private final E item;
    private volatile Node<E> next;
    public Node(E item) {
        this.item = item;
    }
}
private static AtomicReferencePieldUpdater<Node, Node> nextUpdater
    = AtomicReferenceFiledUpdater(Node.class, Node.class, "next");

几乎在所有情况下,普通原子变量的性能都很不错,只有在很少的情况下才需要使用原子的域更新器.(如果在执行原子更新的同时还需要维持现有类的串行化形式,那么原子的域更新器将非常有用。)

15.4.4 ABA问题

ABA问题是一种异常现象:如果在算法中的接地点可以被循环使用,那么在使用“比较并交换”指令时就可能出现这种问题(主要在没有垃圾回收机制的环境中)。

AtomicStampedReference(以及AtomicMarkableReference)支持在两个变量上执行原子的条件更新。

AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题。
类似地,AtomicMarkableReference将更新一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的节点”。

小结

非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用做一种“更好的volatile变量”,从而为整数和对象引用提供原子的更新操作。