《Java并发编程实战》-2

对象的共享

3.1 可见性

在没有同步的情况下,编辑器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

3.1.1 失效数据

线程安全的可变整数类

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

   public synchronized int get() { return value; }
   public synchronized void set(int vale) { this.value = value; }
}

3.1.2 非原子的64位操作

在多线程环境中使用共享且可变的long和double等类型的变量时,应该使用关键字volatile来声明它们,或者用锁保护起来。

3.1.3 加锁与可见性

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的新值,所有执行读操作或者写操作的线程必须在同一个锁上同步。

3.1.4 Volatile变量

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序声明周期时间的发生(例如,初始化或关闭)。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足一下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

3.2 发布与逸出

安全的对象构造过程

不要再构造过程中使this引用逸出。

3.3 线程封闭

3.1 Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭的指责完成由程序实现来承担。

3.3.2 栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。

3.3.3 ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值得对象关联起来。ThreadLocal提供get与set等访问接口或方法,这些方法为每个使用该变量得线程都存有一份独立得副本,因此get总是返回由当前执行线程在调用set时设置得最新值。

使用ThreadLocal来维持线程封闭性

 prvate static ThreadLocal<Connection> connectionHolder 
 = new ThreadLocal<Connection>() {
     public Connection initalValue() {
        return DriverManager.getConnection(DB_URL);    
    }
 };

public static Connection getConnection() {
    return connectionHolder.get();
}

3.4 不变性

不可变对象一定是线程安全得。

当满足一下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)

3.4.1 Final域

正如“除非需要更高的可见性,否者应将所有的域都声明为私有域”是一个良好的编程习惯,”除非需要某个域是可变的,否则就应该将其声明为final域“也是一个良好的编程习惯。

3.4.2 实例:使用Volatile类型来发布不可变对象

3.5 安全发布

3.5.1 不正确的发布:正确的对象被破坏

3.5.2 不可变对象与初始化安全性

任何线程都可以在不需要同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

3.5.3 安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确的构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

3.5.4 事实不可变对象

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

3.5.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变量”,从而为整数和对象引用提供原子的更新操作。