《Java 并发编程实战》

简介

1.1 并发简史

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

线程会共享进程范围内的资源,如文件句柄和内存句柄,但是每个线程都有各自的程序计数器、栈以及局部变量。

由于同一进程中的所有线程都将共享进程的内存空间,因此这些线程 都能访问相同的变量并在同一堆上分配对象,因此造成的多个线程访问同一个变量的问题。

1.2 线程的优势

  • 发挥多处理器的强大能力
  • 建模的简单性
  • 异步事件的简化处理

1.3 线程带来的风险

  • 安全问题
    多线程共享相同的内存地址,并且是并发运行,因此它们可能访问或修改其他线程正在使用的变量。
  • 活跃性问题
    无意中造成无限循环问题。(死锁)
  • 性能问题
    当线程调度器临时挂起活跃线程用转而运行另一个线程时,就会频繁地出现上下文切换操作,这种操作将带来极大的开销。

1.4 线程无处不在

即使在程序中没有显式地创建线程,但在框架中仍可能会创建线程。

线程安全性

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的可变的状态进行访问。

2.1 什么是线程安全性

当多线程访问某个类时,这个类始终能表现出正确的行为,那么就成这个类是线程安全的。

无状态对象一定是线程安全的。

2.2 原子性

竞态条件:由于不恰当的执行时序而出现不正确的结果。
先检查后执行是一种竞态条件。
延迟初始化的竞态条件:

public class LazyInitRace {
	private ExpensiveObject object = null;
	public ExpensiveObject getInstance(){
		if(object == null){
			object  = new ExpensiveObject();
		}
	}
}

复合操作:对于访问同一个状态的所有操作来说,这个操作是以原子的方式来执行的。

atomic包的类操作都是原子的。

2.3 加锁机制

2.3.1 内置锁

Java提供了内置的锁机制来实现原子性:同步代码块。
同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
synchronized(lock){

}

2.3.2 重入

内置锁是可重入。重入的实现方法是每个锁关联一个计数器和一个所有者线程,当计数器为0时认为这个锁没有被任何线程持有。

2.4 用锁保护状态

每一个共享的和可变的变量,都应该只有一个锁来保护,从而让维护人员知道是哪一个锁。

对象的共享

3.1 可见性

为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

public class NoVisibility {
    private static boolean ready;
    private static int num;

    private static class ReadThread extends Thread {
        @Override
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        new ReadThread().start();
        num = 42;
        ready = true;
    }
}

因为重排序可能导致NoVisibility 的执行结果无法预料。

3.1.1 失效的数据

NoVisibility 展示了缺乏同步的程序中可能产生错误的结果:失效数据。

3.1.2 非原子的64位操作

线程在没有同步的情况下读取变量时可能得到一个失效值,但这个值是由之前某个线程设置的值,并不是随机值。这种安全性保证也成为最低安全性。
在非volatile 的long和double类型,jvm允许将64位的读操作或者写操作分解为两个32位操作。在多线程中使用共享切可变的long和double等类型的变量也是不安全的。

3.1.3 加锁与可见性

内置锁可以用于确保某个线程一一种可预测的方式来查看另一个线程的执行结果。
在这里插入图片描述

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

3.1.4 volatile 变量

当变量声明为volatile后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作重排序。

volatile 的典型用法:检测某个状态标记以判断是否退出循环。

volatile boolean asleep;
while (!asleep){
   countSomeSheep()
}

volatile是比synchronized关键字更轻量级的同步机制。

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

3.2 发布与逸出

发布一个对象指的是:使对象能在当前作用域以外的地方使用。
当某个不该发布的对象发布时称为逸出。

	发布一个对象
	 private static Set<Sercet> sercetSet;
	 private void initialize() {
	     sercetSet = new HashSet<Sercet>();
	 }
  使内部的可变状态逸出
    private String [] states = new String[]{"ak","al"};

    public String[] getStates() {
        return states;
    }

最后一种发布对象或其内部状态的机制是发布一个内部的类实例。

不要在构造过程中使this逸出
在这里插入图片描述

3.3 线程封闭

当访问共享的可变的数据时,通常需要使用同步,一种避免使用同步的方法就是不共享数据。如果在单线程捏访问数据就不需要同步,这种技术称为线程封闭

3.3.1 Ad-hoc

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

3.3.2 栈封闭

栈封闭是线程封闭的一种特例。在栈封闭中只有通过局部变量才能访问对象。
在这里插入图片描述

3.3.3 ThreadLocal类

ThreadLocal 这个类能使线程中的某个值与保存的某个值关联起来。
ThreadLocal 对象防止对可变的单实例变量或者全局变量进行共享。

3.4 不可变

满足同步需求的另一个方法是使用不可变对象。
当满足以下条件时,对象才是不可变的:

  • 对象创建后其状态不能更改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的,在构造函数中没有this逸出。
public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<>();

    public ThreeStooges() {
        stooges.add("a");
        stooges.add("b");
        stooges.add("c");
    }

    public boolean isStooges(String name) {
        return stooges.contains(name);
    }
}

每个

3.4.1 final域

final类型的域是不能修改的,但 final域所引用的是可变对象,那么被引用的对象是可以更改的。
在Java内存模型中,final还有特殊的语义。如果是不可变对象,线程获取了该对象的引用后,就不必担心另一个线程会修改对象的状态。
如果要更新这些变量,可以创建一个新的对象,那边其他使用对象的线程会看到对象处于一致状态。
在这里插入图片描述

3.5 安全发布

3.5.1不正确的发布
public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("不一致");
        }
    }
}
3.5.3 安全发布的常用模式

可变对象必须通过安全的方式来发布,对象的引用及对象的状态必须同时对其他线程可见。

3.5.4

如果对象重技术上来看是可变的,但是其状态在发布后不会在变,称为事实不可变对象。
例如Date是可变的,放到Collections.synchronizedMap()中,就可以安全的被发布。

3.5.4 可变对象

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制发布。
  • 事实不可变对象必须通过安全方式发布
  • 可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来。
3.5.5 安全地共享对象

对象的组合

4.1 设计线程安全的类

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

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变条件
  • 建立对象状态的并发访问的策略
4.1.1 收集同步需求

要保证类的安全性,就需要确保它的不变性条件不会在并发访问下被破坏。
如果不了解对象的不变条件和后验条件,那么就不能保证线程安全性。

4.1.2 依赖状态的操作

类的不变条件和后验条件约束了在对象上有哪些状态和状态转换是有效的。
在某些对象方法中还包含一些基于状态的先验条件。如果某个操作中包含基于状态的先验条件就称为依赖状态的操作。

4.1.3 状态的所有权

对象封装它拥有的状态,对它封装的状态拥有所有权。

4.2 实例封闭

将数据封装在对象内部,可以将数据的访问限制在对象方法上,从而更容确保线程在访问时总能持有正确的锁。
在这里插入图片描述
一些基本的容器并非线程安全的,例如ArrayList和HashMap,但类库提供了包装器工厂方法,使得这些非线程安全的类可以在多线程环境安全地使用。这些工厂方法通过装饰器模式,将容器类封装在一个同步的包装对象中。

4.2.1 Java监视器模式

遵循监视器模式的对象会把会对象的所有可变状态都封装起来,并由自己的内置锁来保护。

4.3 线程安全性的委托

public class NumberRange {
    // 不变性条件 lower < upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    // 注意 ,不安全的先检查后执行
    public void setLower(int i) {
        if (i > upper.get()) {
            throw new IllegalArgumentException("");
        }
        lower.set(i);
    }

    // 注意 ,不安全的先检查后执行
    public void setUpper(int i) {
        if (i < lower.get()) {
            throw new IllegalArgumentException("");
        }
        lower.set(i);
    }

}

AtomicInteger 虽然是线程安全的,但是通过组合得到的类却不是。setLower 和setUpper 都不是线程安全的,它们没有足够的加锁机制保证这些操作的原子性。

NumberRange 可以通过加锁机制来维护不变性来确保其线程安全。例如使用一个锁来保护 lower 和 upper,也可以不发布避免客户代码破坏其不变性条件。

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

4.3.1 发布底层的状态变量

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

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

假设需要一个线程安全的链表,它需要提供一个原子的若没有则添加操作,同步的list类已经实现了大部分的功能,我们可以根据它提供的contains方法和add方法构造一个 putIfAbsent操作。

public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E e) {
        boolean contains = !contains(e);
        if (contains) {
            add(e);
        }
        return contains;
    }
}
4.4.1 客户端加锁机制

对于ListHelper,list 使用的锁不是ListHelper对象,而是list。 由于不同的对象使用了不同的锁,putIfAbsent操作就不是原子的。

// 线程不安全
public class ListHelper<E> {
    private List<E> list = Collections.synchronizedList(new ArrayList<>());

    public synchronized boolean putIfAbsent(E e) {
        boolean contains = !list.contains(e);
        if (contains) {
            list.add(e);
        }
        return contains;
    }
}
// 线程安全
public class ListHelper<E> {
    private List<E> list = Collections.synchronizedList(new ArrayList<>());

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

    }
}
4.4.2 组合

当为现有的了添加一个原子操作时,有更好的方法:组合。

public class ImproveList<T> implements  List<T>{
    private final List<T> list;

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

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

4.5 将同步策略文档化

基础构建模块

委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可。

5.1 同步容器类

者都是早期JDK的一部分,此外还包括在JDK1.2当中添加的一些功能相似的类,这些同步的封装类是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次都只有一个线程能够访问容器的状态。

5.1.1 同步容器类的问题

在并发编程当中,虽然同步容器类是线程安全的,但是在某些情况下可能需要额外的客户端加锁来保护复合操作。常见的复合操作包括:迭代、跳转以及条件运算。

public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}

public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

上述两个函数中,虽然Vector是线程安全的,但是获取Vector大小与获取/删除之间没有锁保护,当获得Vector大笑之后,如另外一个线程删除了Vector中的最末尾位置的元素,则每个函数的最后一句代码执行将报错。因此,对于复合操作,需要在符合操作上用锁来保证操作的原子性:

public static Object getLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
}

public static void deleteLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

可能会出现ConcurrentModificationException 的迭代操作:

 for (int i = 0; i <verctor.size() ; i++) {
            verctor.get(i);
        }
 // 客户端加锁保证安全性
 synchronized (verctor) {
        for (int i = 0; i <verctor.size() ; i++) {
            verctor.get(i);
        }
    }
5.1.2 迭代与ConcurrentModificationException

在对集合进行迭代操作的过程中,如果修改了原集合,将导致异常的发生。同样,如果在迭代期间modCount被其他线程修改,那么同样将发生ConcurrentModificationException异常。由于使用同步类容器需要保证在对容器进行复合操作及其他一些操作要进行客户端加锁,导致了实现线程安全的同步操作的保障将分散代码的各个地方,这将增加代码实现的难度以及维护的难度。正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略以及简化维护工作。因此,更能实现该目的的并行容器,也就成了更好的选择。

5.1.3 隐藏迭代器

在对所有共享容器进行迭代的地方都需要加锁。constainsAll/removeAll等都隐藏迭代。

5.2 并发容器

同步容器将所有对容器状态的访问都串行化,以实现他们的线程安全性,但这种方法的代价是严重降低并发性,当多个线程竞争访问容器的锁时,吞吐量将严重降低。
并发容器是针对多线程并发访问设计的。

通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
在这里插入图片描述

5.2.1 ConcurrentHashMap

ConcurrentHashMap并不是在每个方法上都用同一个锁进行同步并使得只能有一个线程访问容器,而是使用一种粒度更细的锁机制来实现更大程度的共享,这种机制成为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。,所谓分段锁,简单来说就是将数据进行分段,每一段锁用于锁容器中的一部分数据,那么当多线程访问容器里的不容数据段的数据时,线程间就不会存在锁竞争,从而可以有效地提高并发访问效率。有些方法需要跨段,比如size(),就需要按照顺序锁定所有的段,完成操作后,再按顺序释放锁。有关分段锁的应用,可以参看ConcurrentHashMap分段锁技术。

5.2.2 额外的原子操作

由于ConcurrentHashMap不能被加锁来执行独自访问,因此我们无法使用客户端加锁来创建新的原子操作。ConcurrentMap 中已经声明putIfAbsent,remove,replace等原子操作。

5.2.3 CopyOnWriteArrayList

CopyOnWriteArrayList用于替代同步List,其在迭代期间不需要对容器进行加锁或复制。

// get
 public E get(int index) {
       return get(getArray(), index);
}
 private transient volatile Object[] array;
// 迭代
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}
//add
public boolean add(E e) {
     final ReentrantLock lock = this.lock;
     lock.lock();
     try {
         Object[] elements = getArray();
         int len = elements.length;
         Object[] newElements = Arrays.copyOf(elements, len + 1);
         newElements[len] = e;
         setArray(newElements);
         return true;
     } finally {
         lock.unlock();
     }
 }

add操作把原数组复制一份,新建一个数组,添加完数据后再把引用指向新数组。
get 操作返回的是当前的数组array,array被声明为volatile ,其他线程可以看到最新的数据。
Iterator()返回的是COWIterator对象,持有final修饰的当前数组snapshot,所有其他线程新增数据也是不可见的。

显然每次修改都会复制底层数组,在容器数据量较大时,性能会下降。仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器。

5.2 阻塞队列和生产者-消费者模式

阻塞队列提供了可阻塞的put和take方法。如果队列满了put将阻塞到有空间可用,如果队列为空,take将阻塞到有元素可用。队列可以是有界和无界的,无界的队列put将不会阻塞。

阻塞队列支持生产者消费者模式,该模式将找出需要完成的工作,和执行工作分开。生产者-消费者模式能简化开发过程,因为消除了生产者和消费者之间的代码依赖性,此外,该模式还将生产数据的过程和使用数据的过程解耦开来。
  生产者与消费者的角色是相对的,某种环境下消费者可能会成为其他的生产者。

在构建高可用的程序时,有界队列是一种强大的资源管理工具:能抑制并防止生产过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

在类库中,BlockingQueue有多种实现,其中LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,二者分别和LinkedList和ArrayList类似,但比同步List有更好并发性能。PriorityBlockingQueue是一个按优先级排序的队列,可以按照某种排序而不是FIFO来处理数据,和其他的有序容器一样。
还有一个BlockingQueue的实现是SynchronousQueue,实际上维护的不是真正的队列,它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移除队列。以洗盘子为例,相当于没有盘架,洗完直接放入空闲的烘干机。
这种方式降低了生产者移动到消费者的延迟。

5.3.2 串行线程封闭

java.util.concurrent中实现的各种阻塞队列都包含了足够的内部同步机制,从而安全地将对象从生产者发布到消费者线程。
对应可变对象,生产者-消费者模式这种设计与阻塞队列一起,促进了串行线程封闭。

5.3.3 双端队列与工作密取

Deque 是 Double ended queue (双端队列) 的缩写,读音和 deck 一样,蛋壳。

Deque 主要实现类有ArrayDeque 和 LinkedBlockingDeque。

工作密取
在 生产者-消费者 模式中,所有消费者都从一个工作队列中取元素,一般使用阻塞队列实现;
而在 工作密取 模式中,每个消费者有其单独的工作队列,如果它完成了自己双端队列中的全部工作,那么它就可以从其他消费者的双端队列末尾秘密地获取工作。

vs 生产者-消费者
工作密取模式对比传统的 生产者-消费者模式,更为灵活。

因为多个线程不会因为在同一个工作队列中抢占内容发生竞争。

在大多数时候,它们只是访问自己的双端队列。即使需要访问另一个队列时,也是从队列的尾部获取工作,降低了队列上的竞争程度。

5.4 阻塞方法与中断方法

线程可能会阻塞或者暂停执行,原因有多种:等待I/O操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。当线程阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED, WAITING或TIMED_WATING)。阻塞操作与执行时间很长的普通操作的差别在于,被阻塞的线程必须等待某个不受它控制的事件发生后才能继续执行,例如等待I/O操作完成,等待某个锁变成可用,或者等待外部计算的结束。当某个外部事件发生时,线程被置回RUNNABLE状态,并可以再次被调度执行。

和Thread.sleep一样, BlockingQueue的put和take方法会抛出编译器异常 InterruptedException; __如果一个方法抛出InterruptedException, 说明这个方法是一个阻塞方法。
处理InterruptedException的方式:
(1) 直接传递异常

不捕获异常直接抛出 或捕获异常做简单清理再次抛出

(2) 恢复中断

当代码是Runnable的一部分时只能捕获InterruptedException, 并通过调用当前线程的interrupt()方法恢复中断状态(否则中断状态会消失), 这样更高层代码将看到引发了一个中断。

// 恢复中断状态
public class TaskRunnable implements Runnable{
    BlockingQueue<Task> queue;
    ...
    public void run{
        try{
            processTask(queue.take());
        }catch(InterruptedException e){
           Thread.currentThread().interrupt();
        }
    }
}

5.5 同步工具类

同步工具类可以是任意一个对象,只要它可以根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁。在平台类库中还包含一些其他同步工具类,如果还是不能满足需要,我们可以创建自己的同步工具类。

同步工具类都包含一些特定的结构化属性:他们封装了一些状态,这些状态将决定执行同步工具类的线程时继续执行还是等待。此外还提供操作状态的方法,以及另外一些方法用于高效地等待同步工具类进入到预期状态。(这段话通过使用一个同步工具类就可以理解了)

5.5.1 闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态。

  • 闭锁相当于一扇门,在闭锁达到结束之前,门是关着的,任何线程不能通过,当到结束状态时,这扇门会打开,之后状态不会再改变,门打开后任何线程都允许通过。
  • 闭锁是一次性对象,一旦进入终止状态就不能被重置,而栅栏可以重置。

CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组时间发生。闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量。countDown方法表示递减计数器,表示一个事件发生了,而await方法等待直到计数器为0,表示所有事件都已经发生。如果计数器的值非零,那么就会一直等待下去,或者等待中被打断,或者超时。

public class TestHarness {
    public long timeTasks(int nThread) throws InterruptedException {
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate = new CountDownLatch(nThread);

        Thread t;
        for(int i=0; i<nThread; i++) {
            t = new Thread() {
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + " ready....");
                        startGate.await();
                        try {
                            System.out.println(Thread.currentThread().getName() + " running ....");
                        } finally {
                            endGate.countDown();
                        }
                    } catch (InterruptedException ignored) {
                    }
                }
            };

            t.start();
        }

        Thread.sleep(2000);

        long start = System.nanoTime();
        System.out.println("all ready");
        startGate.countDown();
        endGate.await();
        long end = System.nanoTime();
        return end - start;
    }

    public static void main(String[] args) throws InterruptedException {
        TestHarness test = new TestHarness();
        long time = test.timeTasks(5);
        System.out.println("time spent: " + time);
    }
}
// 执行结果
Thread-3 ready....
Thread-1 ready....
Thread-2 ready....
Thread-0 ready....
Thread-4 ready....
all ready
Thread-3 running ....
Thread-1 running ....
Thread-2 running ....
Thread-0 running ....
Thread-4 running ....
time spent: 2169650920

startGate.countDown() 同时释放所有工作线程。而 endGate.await(); 使主线程等待最后一个线程执行完成,而不是顺序地等待每个线程执行完成。

5.5.2 FutureTask

FutureTask也可以做闭锁(实现了Future的语义,表示一种抽象的可计算的结果)。通过Callable实现,相当于一个可生成结果的Runnable。
三种状态:

  • 等待运行
  • 正在运行
  • 运行完成
    “运行完成”表示计算的所有可能结束的状态,包含正常结束,由于取消而结束和由于异常而结束。当进入完成状态,他会停止在这个状态上。

Furture.get()获取执行结果的值,取决于执行的状态,如果任务完成,会立即返回结果,否则一直阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask负责将计算结果从执行任务的线程传递到调用这个线程的线程,而且确保了传递过程中结果的安全发布。

public class PreLoader {
	private final FutureTask<Product> future = new FutureTask<Product>(new Callable<Product>(){
		public Product call() throws Exception{
			//执行一些耗时任务
			return loadProduct();
		}
	});

	private final Thread thread = new Thread(future);

	public void start(){
		thread.start();
	}

	private Product loadProduct() {
		Product product = new Product();
		//可从数据库或者其他方式获取,耗时任务
		return product;
	}

	public Product get(){
		try {
			return  future.get();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		return null;
	}

}
public class FutureTaskTest {

    public static void main(String[] args)  {
        long startTime = System.currentTimeMillis();
        System.out.println("主线程开始...");

        FutureTask<Integer> future = new FutureTask<>(new Task());
        System.out.println("进行Task任务计算的子线程开始...");
        new Thread(future).start();;

        try {
            System.out.println("主线程正在执行自己的任务...");
            Thread.sleep(1000);
            System.out.println("主线程尝试获取Task结果...");

            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
            System.out.println("主线程获取到结果为:"+future.get());
            System.out.println("时间过去"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

    }

}

class Task implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //花3s模拟计算过程
        Thread.sleep(3000);
        //模拟计算结果是1
        return 1;
    }

}
5.5.3 信号量

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量。计数信号量还可以用来实现某种资源池(如:数据库连接池),或者对容器施加边界。

class BoundedHashSet<T> {
    private Set<T> set;
    private Semaphore semaphore;

    public BoundedHashSet(int bound) {
        set = Collections.synchronizedSet(new HashSet());
        semaphore = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException {
        semaphore.acquire();// 尝试获取信号量
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        } finally {
            if (!wasAdded) {// 如果添加失败就释放信号量,添加成功就占用一个信号量
                semaphore.release();
            }
        }
    }

    public boolean remove(T o) throws InterruptedException {
        boolean remove = set.remove(o);
        if (remove)// 如果删除成功之后就释放一个信号量
            semaphore.release();
        return remove;
    }
}
  public static void main(String[] args) throws InterruptedException {
        BoundedHashSet set = new BoundedHashSet(5);
        set.add("a");
        set.add("b");
        set.add("c");
        set.add("d");
        set.add("e");
        set.add("f");
        System.out.println(set);
    }

BoundedHashSet 在满足容量后添加数据会一直阻塞。

5.5.4 栅栏

闭锁是一次性操作,一旦进入终止状态就不能重置。
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个时间发生。闭锁用于等待事件,而栅栏用于等待其他线程。

public class CyclicBarrierWorker implements Runnable {

    private int id;
    private CyclicBarrier cyclicBarrier;

    public CyclicBarrierWorker(int id, CyclicBarrier cyclicBarrier) {
        this.id = id;
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            System.out.println(id + "th people wait, waiting " + cyclicBarrier.getNumberWaiting());
            int returnIndex = cyclicBarrier.await(); // 大家等待最后一个线程到达
            System.out.println(id + " th people go, returnIndex:" + returnIndex);
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        final int NUM = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUM,new Runnable() {
            @Override
            public void run() {
                System.out.println("go on together!");
            }
        });

        for (int i=1; i<=NUM; i++) {
            new Thread(new CyclicBarrierWorker(i, cyclicBarrier)).start();
        }
    }

}

Exchanger(两个线程进行数据交换)
另一种栅栏是Exchanger,它是一种两方(two-party)栅栏,各方在栅栏位置互换数据。当两方执行不对称操作时Exchanger会非常有用,例如一个线程向缓存中写数据,另一线程读数据,这两个线程可以使用Exchanger汇合,并将满的缓冲区和空的缓冲区互换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

public class ExchangerDemo {

    private static final Exchanger<List<String>> ex = new Exchanger<List<String>>();

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
    * 内部类,数据生成者
    */
    class DataProducer implements Runnable {
        private List<String> list = new ArrayList<String>();

        @Override
        public void run() {
            System.out.println("生产者开始生产数据");
            for (int i = 1; i <= 5; i++) {
                System.out.println("生产了第" + i + "个数据,耗时1秒");
                list.add("生产者" + i);
                sleep(1000);
            }

            System.out.println("生产数据结束");
            System.out.println("开始与消费者交换数据");

            try {
                //将数据准备用于交换,并返回消费者的数据
                list = ex.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("结束与消费者交换数据");

            System.out.println("\n遍历生产者交换后的数据");
            for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
                System.out.println(iterator.next());
            }

        }

    }


    /**
     * 内部类,数据消费者
     */
    class DataConsumer implements Runnable {
        private List<String> list = new ArrayList<String>();

        @Override
        public void run() {
            System.out.println("消费者开始消费数据");
            for (int i = 1; i <= 5; i++) {
                System.out.println("消费了第" + i + "个数据");
                // 消费者产生数据,后面交换的时候给生产者
                list.add("消费者" + i);
             }

            System.out.println("消费数据结束");
            System.out.println("开始与生产者交换数据");

            try {
                // 进行数据交换,返回生产者的数据
                list = (List<String>) ex.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            sleep(1000);
            System.out.println("\n开始遍历消费者交换后的数据");
            for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
                System.out.println(iterator.next());
            }
        }

    }


    public static void main(String[] args) {
         ExchangerDemo et = new ExchangerDemo();
         new Thread(et.new DataProducer()).start();
         new Thread(et.new DataConsumer()).start();
    }

}

5.6 构建高效且可伸缩的结果缓存

首先我们定义一个Computable接口,该接口包含一个compute()方法,该方法是一个耗时很久的数值计算方法。Memoizer1是第一个版本的缓存,该版本使用hashMap来保存之前计算的结果,compute方法将首先检查需要的结果是否已经在缓存中,如果存在则返回之前计算的值,否则重新计算并把结果缓存在HashMap中,然后再返回。
HashMap版本

interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;//耗时计算
}
 
public class Memoizer1<A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new HashMap<A, V>();
    private final Computable<A, V> c;
 
    public Memoizer1(Computable<A, V> c) {
        this.c = c;
    }
 
    public synchronized V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

HashMap不是线程安全的,因此要确保两个线程不会同时访问HashMap,Memoizer1采用了一种保守的方法,即对整个方法进行同步。这种方法能确保线程安全性,但会带来一个明显的可伸缩问题:每次只有一个线程可以执行compute。
ConcurrentHashMap版本

ublic class Memoizer2 <A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
    private final Computable<A, V> c;
 
    public Memoizer2(Computable<A, V> c) {
        this.c = c;
    }
 
    public V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

但是这个版本的缓存还是有问题的,如果线程A启动了一个开销很大的计算,而其他线程并不知道这个线程正在进行,那么很可能会重复这个计算。
FutureTask版本1
我们可以在map中存放Future对象而不是最终计算结果,Future对象相当于一个占位符,它告诉用户,结果正在计算中,如果想得到最终结果,请调用get()方法。Future的get()方法是一个阻塞方法,如果结果正在计算中,那么它会一直阻塞到结果计算完毕,然后返回;如果结果已经计算完毕,那么就直接返回。

public class Memoizer3<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;
 
    public Memoizer3(Computable<A, V> c) {
        this.c = c;
    }
 
    public V compute(final A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
            };
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = ft;
            cache.put(arg, ft);
            ft.run(); // call to c.compute happens here
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            cache.remove(arg);
        }
        return null;
    }
}

Memoizer3解决了上一个版本的问题,如果有其他线程在计算结果,那么新到的线程会一直等待这个结果被计算出来,但是他又一个缺陷,那就是仍然存在两个线程计算出相同值的漏洞。这是一个典型的"先检查再执行"引起的竞态条件错误,我们先检查map中是否存在结果,如果不存在,那就计算新值,这并不是一个原子操作,所以两个线程仍有可能在同一时间内调用compute来计算相同的值。
FutureTask版本2
Memoizer3存在这个问题的原因是,复合操作"若没有则添加"不具有原子性,我们可以改用ConcurrentMap中的原子方法putIfAbsent,避免了Memoizer3的漏洞。

public class Memoizer <A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache
            = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;
 
    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }
 
    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw LaunderThrowable.launderThrowable(e.getCause());
            }
        }
    }
}

第一部分小结
1、可变状态是至关重要的。
所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性、
2、尽可能将域声明为final类型,除非需要它们是可变的。
3、不可变对象一定是线程安全的。
不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护复制等机制。
4、封装有助于管理复杂性。
在编写线程安全的程序时,虽然可以将所有与数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更易于维护不可变性条件:将同步机制封装在对象中,更易于遵循同步策略。
5、用锁来保护每个可变变量。
6、当保护同一个不变形条件中的所有变量时,要使用同一个锁。
7、在执行复合操作期间,要持有锁。
8、如果多个线程访问同一个可变变量时没有同步机制,那么程序会出现问题。
9、不要故作聪明地推断出不需要使用同步。
10、在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
11、将同步策略文档化。

任务执行

任务:通常是一些抽象的且离散的工作单元。大多数并发应用程序都是围绕"任务执行"来构造的,把程序的工作分给多个任务,可以简化程序的组织结构便于维护。

6.1 在线程中执行任务

任务的独立性:任务并不依赖于其他任务的状态,结果和边缘效应。独立的任务可以实现并行执行

1.串行地执行任务
所有的任务放在单个线程中串行执行,程序简单,安全性高,不涉及同步等情况,缺点也显而易见,无法提高吞吐量和响应速度,适合任务数量很少并且执行时间很长时,或者只为单个用户使用,并且该用户每次只发出一个请求。
2.显示的创建线程

为每一个请求创建一个线程,将任务的处理从主线程中分离出来,多个任务可以并行处理,充分利用了系统资源,提高吞吐量和相应速度,要求处理代码必须是线程安全的

3.无限创建线程的不足

线程生命周期的开销非常高;太多线程会消耗系统资源,空闲线程的内存空间占用,大量线程竞争CPU时产生其他性能开销;稳定性:破坏这些限制底层操作系统对线程的限制很可能抛出OutOfMemoryError异常.

6.2 Executor框架

public interface Executor {
    void execute(Runnable command);
}

Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
Executor框架提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。
基于Executor的Web服务器

class TaskExecutorWebServer {
    private static final int NTHREAD = 100;
    private static final Executor exe = Executors.newFixedThreadPool(NTHREAD);

    public static void main(String[] args) {
        ServerSocket socket = new ServerSocket(80);
        while(true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }
}
6.2.2 执行策略

通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和修改执行策略。

执行策略包括:
①在什么线程中执行任务
②任务按照什么顺序执行(FIFO,LIFO,优先级)
③有多少个任务可并发执行
④在队列中有多少个任务在等待执行
⑤如果系统由于过程而需要拒绝一个任务,应该选择哪一个任务?另外,如何通知应用程序有任务被拒绝?
⑥在执行一个任务之前或之后,应该进行哪些动作

各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。

每当看到下面这中形式的代码时:
new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread。

6.2.3 线程池

管理一组同构工作线程的资源池。以实现线程的重用,降低资源消耗,提高响应速度等。
1.newFixedThreadPool(int) :创建一个定额线程池,每提交一个任务创建一个线程,达到数量限制后不再增加,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的异常而结束,那么线程池会补充一个新的线程)

2.NewCachedThreadPool() : 创建一个可缓存的线程池,线程池的规模不存在任何限制,当线程多余任务时,回收空闲线程;当任务增加时,创建新线程。

3.NewSingleThreadExecutor:单线程的Executor,如果这个线程异常结束,会创建另一个线程来替代。NewSingleThreadExecutor能确保依照任务在队列中的顺序串行执行(例如FIFO、LIFO、优先级)。

4.NewScheduleThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

6.2.4 Executor的生命周期

ExecutorSevice接口就是为了解决执行服务的生命周期问题,扩展了Executor接口。它添加了一些用于声明周期管理的方法(同时还有一些用于任务提交的便利方法):

public interface ExecutorService extends Executor {  
    void shutdown();  
    List<Runnable> shutdownNow();  
    boolean isShutdown();  
    boolean isTerminated();  
    boolean awaitTermination(long timeout, TimeUnit unit)  
        throws InterruptedException;  
    // ......其他用于任务提交的便利方法  
}

ExecutorService的三种状态:运行、关闭、已终止 。

6.2.4 延迟任务与周期任务

延迟任务:在100ms后执行任务
周期任务:每100ms执行一次任务

Timer类负责管理延迟任务以及周期任务,但它本身存在缺陷,因此通常要用ScheduleThreadPoolExecutor的构造函数或newScheduleThreadPool工厂方法来创建该类对象。

Timer的缺陷在于,Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。

Timer还有一个问题就是,Timer线程不会捕获异常,当TimerTask抛出未检查异常时将终止定时线程。Timer也不会恢复线程的执行,而是会错误地任务整个Timer都被取消了。这就造成:已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不会被调度。称之为“线程泄漏”。

public class OutOfTime {
    public static void main(String[] args) throws InterruptedException{
        Timer timer = new Timer();
        timer.schedule(new myTask(), 1);//这里抛出RuntimeException之后,整个Timer线程会被终止
        Thread.sleep(1000);
        timer.schedule(new myTask(), 1);
        Thread.sleep(5000);
    }

    static class myTask extends TimerTask {

        @Override
        public void run() {
            throw new RuntimeException();
        }

    }
}

6.3 找出可利用的并行性

6.3.2 携带结果的任务 Callable和Future

Exceutor框架使用Runnable作为最基本的任务形式,但是Runnable有一种很大的局限性,它不能返回一个值或抛出一个受检查的异常。

Callable是一种更好的任务形式,它能返回一个值或者抛出一个异常

public interface Callable<V> {
    V call() throws Exception;
}

Executor执行任务时有4个生命周期阶段:创建,提交,开始,完成。
对于已提交但尚未开始的任务,调用shutdownNow()可以取消并返回这些任务;对于已经开始的任务,只有当他们能响应中断时,才能取消。
Java 8 style:

Callable<String> callableTask = () -> {  
    return "this is a callable task....";  
};

Future表示一个任务的生命周期。主要提供了一些方法用于判断任务处于哪个阶段,还可以获取任务的结果甚至是取消任务。它本身还有一层隐含意义是,任务的生命周期只能前进,不能后退,当一个任务处于“完成”状态,就永远停留在“完成”状态上。这一点和ExecutorService的生命周期一样。

public interface Future<V> {
     boolean cancel(boolean mayInterruptIfRunning);
     boolean isCancelled();
     boolean isDone();
     V get() throws InterruptedException, ExecutionException;
     V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

异构任务并行化存在的局限
A与B两个完全不同的任务通过并行方式可以实现小幅度的性能提升,但是如果想大幅度的提升存在一定的困难。因此,得出一个结论是,只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出真正的性能提升。

6.3.5 CompletionService和BlockingQueue

CompletionService用来将Executor与BlockingQueue进行结合, 将Callable任务提交给它执行, 然后使用类似队列中的take和poll在结果完整时获得这个结果。

public class Test {
    private final ExecutorService executor = Executors.newCachedThreadPool();

    void renderPage(String source) {
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        CompletionService<ImageData> service = new ExecutorCompletionService<ImageData>(
                executor);

        for (final ImageInfo imageInfo : imageInfos) {
            service.submit(new Callable<ImageData>() {
                public ImageData call() throws Exception {
                    return imageInfo.downloadImage();
                }
            });
        }

        renderText(source);

        for (int i = 0; i < imageInfos.size(); i++) {
            Future<ImageData> f;
            try {
                f = service.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

6.3.7 为任务设置时限

有这样一种需求: 让一个任务在指定时间内做完, 如果超出了指定的时限就使用默认的结果

满足这样需求的是Future对象的带限时的get()方法

    V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;

注意任务超时后应该立刻停止这个任务, 此时可以使用Future的cancel()方法

  public class RenderWithTimeBudget {

      private static final Ad DEFAULT_AD = new Ad();
      private static final long TIME_BUDGET = 1000;
      private static final ExecutorService exec = Executors.newCachedThreadPool();

      Page renderPageWithAd() throws InterruptedException {
  
          long endNanos = System.nanoTime() + TIME_BUDGET;
  
          Future<Ad> f = exec.submit(new FetchAdTask());
  
          // Render the page while waiting for the ad
          Page page = renderPageBody();
          Ad ad;
  
          try {
              // Only wait for the remaining time budget
              long timeLeft = endNanos - System.nanoTime();
              ad = f.get(timeLeft, NANOSECONDS);

          } catch (ExecutionException e) {
              ad = DEFAULT_AD;
          } catch (TimeoutException e) {
              ad = DEFAULT_AD;
              f.cancel(true);
          }

          page.setAd(ad);
       
          return page;
      }

      private Page renderPageBody() {
          return new Page();
      }

      static class Ad {
      }

      static class Page {
          public void setAd(Ad ad) {
          }
      }

      static class FetchAdTask implements Callable<Ad> {
          public Ad call() {
              return new Ad();
          }
      }
  }

invokeAll方法
invokeAll方法支持将多个任务提交到一个ExecutorService并获得结果。invokeAll方法的参数为一组任务,并返回一组Future。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能够将各个Future与其表示的Callable关联起来。

  • Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。
  • 要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。

取消与关闭

任务和线程的启动很容易。然而,有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或者应用程序需要被快速关闭。

要使任务和线程能安全/快速/可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程,但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的工作。

7.1 任务取消

如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的

协作机制能设置某个标志位,任务会定期查看这个标志,如果设置了标志,那么任务将提前结束。

public class PrimeGenerator implements Runnable{
    private final List<BigInteger> primes = new ArrayList<>();
    private volatile boolean cancelled;

    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while(!cancelled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }
    
    public void cancel() { cancelled = true; }
    
    public synchronized List<BigInteger> get() {
        return new ArrayList<>(primes);
    }
}
7.1.1 中断
/**
 * 如果生成者的生成速度大于消费者的消费速度,队列将被填满,put方法将会阻塞。
 * 当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,
 * 如果用cancel()设置cancelled标志,生产者却永远都不能检查这个标志,因为它无法从阻塞的put方法中恢复过来。
 */
public class BrokenPrimeProductor extends Thread{
    private final BlockingQueue<BigInteger> primes;
    private volatile boolean cancelled = false;
    public BrokenPrimeProductor(BlockingQueue<BigInteger> primes) {
        this.primes = primes;
    }
    
    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        
        while(!cancelled)
            try {
                primes.put(p=p.nextProbablePrime());
            } catch (InterruptedException e) { } 
    }
    
    public void cancel() { cancelled = true; }
    
    public boolean needMorePrimes() {
        return !cancelled;
    }
    
    /**
     * 消费者取消生产者的生成状态
     * @throws InterruptedException
     */
    public void consumePrimes() throws InterruptedException {
        BlockingQueue<BigInteger> primes = ...;
        BrokenPrimeProductor producer = new BrokenPrimeProductor(primes);
        producer.start();
        try {
            while(needMorePrimes())
                consume(primes.take());
        } finally {
            producer.cancel();
        }
    }
    
    public void consume(BigInteger prime) {
        System.out.println(prime);
    }
}

每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread中包涵了中断线程以及查询线程中断状态的方法。interrupt方法能中断目标线程,而isInterrupted方法能返回目标线程的中断状态,静态的interrupted方法将清除当前的中断状态,并返回它之前的值。

public class Thread {
    public void interrupt() {//中断...}
    public boolean isInterrupted(){//检查是否被中断}
    public static boolean interrupted(){//清除当前线程的中断状态,并返回之前的值,这也是清除中断状态的唯一方法}
    /*
    *阻塞库方法:会检查线程何时中断,发现中断时提前返回
    *相应中断时的操作包括清除中断状态,抛出InterruptedException异常
    * Object的wait方法同为阻塞方法
    */
    public static native void sleep(long millis) throws InterruptedException;
}

调用interrupt并不意味者立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。会在下一个取消点中断自己,如wait, sleep,join等

/**
 *使用中断而不是BrokenPrimeProductor中的cancled标志来取消
 */
public class PrimeProductor extends Thread{
    
    private final BlockingQueue<BigInteger> primes;
    
    public PrimeProductor(BlockingQueue<BigInteger> primes) {
        this.primes = primes;
    }
    
    @Override
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while(!Thread.currentThread().isInterrupted()) //通过被中断来取消
                primes.put(p=p.nextProbablePrime());
        } catch (InterruptedException e) {
            /*允许线程退出*/
        }   
    }
    /**通过中断来取消*/
    public void cancel() { interrupt(); }
}
7.1.2 中断策略

由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

7.1.3 响应中断

当调用可中断的阻塞函数时,有2种实用策略可用于处理InterruptedException

  • 将InterruptedException传递给调用者
BlockingQueue<BigInteger> queue;
public Task getNextTask() throws InterruptedException {
    return queue.take();
}
  • 恢复中断状态:从而使调用栈中的上层代码能够对其进行处理
public Task getNextTask(BlockingQueue<Task> queue) {
    boolean isinterrupted = false;
    try{
        while(true) {
          try{
              return queue.take();
          } catch(InterruptedException e) {
               isinterrupted = true;
              //重新尝试
          }
        }
    } finally {
        if(isinterrupted)
            Thread.currentThread().interrupt();
    }
}
7.1.5 通过Future来实现取消

ExecutorService.submit()将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示取消操作是否成功(只是表示能否接受中断,而不是表示任务是否能检测并处理中断)。

  • 如果mayInterruptIfRunning为true且任务当前正在某个线程运行,那么这个线程可以被中断。
  • 如果mayInterruptIfRunning为false,那么意味着若任务还没启动,则不要运行它。

执行任务的线程是由标准的Executor创建的,它实现了一种中断策略使得任务可以通过中断被取消,如果任务在标准Executor中运行,并通过它们的Future来取消任务,那么可以设置mayInterruptIfRunning。当尝试取消某个任务时,不宜直接中断线程池,因为你不知道当中断请求到达时线程正在运行什么任务,只能通过任务的Future来实现。

public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
    Future<?> task = taskExec.submit(r);
    try{
        task.get(timeout,unit);
    }catch(TimeoutException e){
        // 接下来任务将被取消
    }catch(ExecutionException e){
        // 如果任务中抛出了异常,则重新抛出该异常
        throw launderThrowable(e.getCause());
    }finally {
        // 如果任务已经结束,取消操作也不会带来任何影响
        // 如果任务正在运行,那么将被中断
        task.cancel();
    }
}

当Future.get抛出InterruptedException 或 TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。

7.1.6 处理不可中断的阻塞

线程阻塞的原因
java.io包中的同步Socket I/O:

在服务器应用程序中,最常见的阻塞I/O就是对套接字进行读取和写入。虽然InputStream和OutputStream的read和write方法不会响应中断,但通过关闭底层的套接字,可以使由于执行read和write等方法被阻塞的线程抛出一个SocketException.

  • java.io包中的同步I/O:

当中断一个正在InterruptibaleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(会使得其他在这条链路上阻塞的线程通用抛出ClosedByInterruptException)。
当关闭一个InterruptibaleChannel,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibaleChannel。

  • Selector的异步I/O:

如果一个线程正在调用java.nio.channels中的Select.select方法阻塞了,那么调用close或者wakeup方法会使线程抛出ClosedSelectorException并提前返回。

  • 获得某个锁:

如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定能获得到锁,所以不会理会中断请求。(但是在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时能响应中断)。

7.1.7 采用newTaskFor来封装非标准的取消

我们可以通过newTaskFor方法是ThreadPoolExecutor中的新增功能。当把一个Callable提交给ExecutorService时,submit()会返回一个Future,我们可以通过这个Future来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,该接口拓展了Future和Runnable(由FutureTask实现)。

7.2 停止基于线程的服务

7.2.2 关闭ExecutorService

ExecutorService提供了两种关闭方法:

使用shutdown正常关闭:速度相对shutdownNow来说更慢,安全性高,会等待队列中所有的任务都执行完才关闭。
使用shutdownNow强行关闭:速度快,但是安全性低,首先关闭当前正在执行的任务,然后返回未启动的任务清单。

一种关闭生产者-消费者服务的方式就是使用毒丸(Poison Pill)对象:毒丸是指一种放在队列上的对象,其含义是:当得到这个对象,立即停止。在FIFO队列中,毒丸对象将会确保消费者在关闭之前首先完成队列中的所有工作,再提交毒丸。毒丸对象之前的所有工作都会得到处理,而生产者在提交毒丸对象以后,将不会提交任何的工作。

当生产者和消费者的数量较大时,这种方法将变得难以使用。只有在无界队列中,毒丸对象才能可靠的工作。

7.2.2 shutdownNow的局限性

当通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在执行的任务并且返回已提交但是尚未执 行的任务(以便调用者线程把这些任务写入日志或者做其他后续处理);shutdownNow返回的List可能与提交给ExecutorService的Runnable不同,它们可能被封装或者修改过。
但是我们无法通过普通方法找出哪些任务已经开始但是尚未结束,我们无法得知状态,除非执行线程中有某些检查。

7.2 处理非正常的线程终止

导致线程提前死亡的最主要原因就是RuntimeException,由于某些异常表示了某种编程错误或其他类似的不可修复的错误,因此它们不会被捕获。它们不会在调用栈中逐层传递,而是默认的在控制台输出栈追踪信息,并终止线程。
任何代码都可能抛出一个RuntimeException。每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目的认为它一定会正常返回,或者一定会抛出在方法原型中声明的异常。对调用的代码越不熟悉,越应该对其行为保持怀疑。
在任务处理线程的生命周期中,将通过某种抽象机制(如Runnable)来调用许多未知的代码,我们应该对这些线程能否表现出正确的行为表示怀疑。因此,这些线程应该在try-catch代码块中调用这些任务,就能捕获未检测的异常了,或者也可以使用try-finally代码块来确保框架能够知道线程非正常退出的情况。

public void run(){
    Throwable thrown = null;
    try{
        while(!isInterrupted){
            runTask(getTaskFromWorkQueue());
        } catch (Throwable e){
            thrown = e;
        } finally {
            threadExited(this,thrown);
        }
    }
}

未捕获异常的处理
在Thread API中同样提供了UncaughtExceptionHandler,它能检测某个线程由于捕获异常而终结的情况。这个与前面的工作者线程是互补的,通过将二者结合在一起,可以有效的防止线程泄露问题。

public interface UncaughtExceptionHandler {
    void uncaughtExceptionHandle(Thread t, Throwable e);
}

只有通过execute提交的任务,才能将异常交给捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务抛出异常,那么将被Future.get封装在ExecutionException中重新抛出。

public class MyThread implements  Runnable {

    @Override
    public void run() {
        throw new NullPointerException("我的异常");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread());
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        thread.start();
    }
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("-------------Exception-----------");
        System.out.println("线程信息:"+t.toString());
        System.out.println("异常信息:"+e.getMessage());
    }
}

7.4 JVM关闭

JVM既可以正常关闭,也可以强行关闭。
正常关闭的触发方式:

  • 当最后一个“正常(非守护)”线程结束
  • 当调用了System.exit
  • 通过特点于平台的方法关闭(例如发送给你SIGINT信号,CTRL-C等)

也可以通过调用Runtime.halt或者在操作系统中发送SIGKILL等

7.4.1 关闭钩子

在正常关闭中,JVM首先调用所有已注册的关闭钩子(Shutdown Hook)。关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。

public void start(){
    Runtime.getRuntime().addShutdownHook(new Thread(){
        public void run(){
            try{
                LogService.this.stop();
            }catch(InterruptedException ignored){}
        }
    });
}
7.4.2 守护线程

有时候你希望创建一个线程来执行一些辅助工作,但是又不希望这个线程阻碍JVM的关闭,那么你可以使用守护线程(Daemon Thread)。

线程分为守护线程和普通线程。在JVM启动时创建的所有线程,除了主线程,其他都是守护线程(例如GC或其他辅助工作的线程)。
当创建一个新线程时,新线程将继承创建它的线程的守护状态。因此主线程创建的都是普通线程。

7.4.3 终结器

垃圾回收期对定义了finalize方法的对象会进行特殊处理:在回收期释放它们以后,调用它们的finalize方法,从而保证一些持久化的资源被释放。

避免使用终结器。

线程池的使用

8.1 在任务与执行策略之间的隐性耦合

Executror框架可以将任务的提交与任务的执行解耦开。但是虽然Executor框架为制定和修改执行策略提供了很大的灵活性,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确地制定执行策略,其中包括:

  • 依赖性任务

大多数行为正确的任务都是独立的:它们不依赖于其他任务的执行时序、执行结果或其他效果。当在线程池中执行独立任务时,可以任意修改线程池大小和配置,这些修改只会对执行性能产生影响。如果提交给线程池的任务需要依赖于其他任务,那么隐含的对执行策略带来了约束,此时必须小心地维持这些执行策略以避免产生活跃性问题。

  • 使用线程封闭机制的任务
    与线程池相比,单线程的Executor能够对并发性做出更强的承诺。它们能确保任务不会并发的执行。对象可以封闭在任务线程中,使得在该线程执行的任务在访问该对象时不需要同步。这种情况将在任务与执行策略之间形成隐性的耦合:即任务要求其执行所在的Executor是单线程的。如果将Executor从单线程环境改为线程池环境,那么将会失去线程安全。
  • 对响应时间敏感
    果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将会降低由该Executor管理的服务性。
  • 使用threadLocal的任务
    hreadLocal使每个线程都拥有某个变量的一个私有版本。只要条件允许,Executor可以自由的重用这些线程。如果从任务中抛出一个未受检查的异常,那么将用一个新的工作者线程来替代抛出异常的线程。只有线程本地值的生命周期受限于任务的生命周期时,在线程池中的线程使用ThreadLocal才有意义,而在线程池中的线程中不应该使用ThreadLocal在任务之间传递值。
8.1.1 线程饥饿死锁

如果所有正在执行任务的线程都由于等待其他仍处于工作队列的任务而阻塞,那么会发生同样的问题。这种现象被称为线程饥饿死锁(Thread Starvation Deadlock)

public class ThreadDeadLock{
    ExecutorService exec = Executors.newSingleThreadExecutor();
    public class RenderPageTask implements Callable<String> {
        public String call throws Exception {
            Future<String> header,footer;
            header = exec.submit(new LoadFileTask("header.html"));
            footer = exec.submit(new LoadFileTask("footer.html"));
            String page = renderBody();
            // 这里将发生死锁:由于当前任务在等待子任务的结果
            return header.get() + page + footer.get();
        }
    }
}
8.1.2 运行时间较长的任务

如果任务阻塞时间过长,那么即便不出现死锁,任务的的响应性也很差。执行时间较长可能会造成线程池阻塞,增加执行时间较短任务的服务时间。如果线程数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。
通过限定任务等待资源的时间,不要无限制的等待,来缓解执行时间任务较长任务的影响。平台类库的大多数阻塞方法都提供了限时版本和无限时版本,例如Thread.join,BlockingQueue.put,CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标为失败,然后终止任务或者将任务放回队列以供随后执行。这样无论任务最终是否能执行成功,至少任务能顺利继续执行下去。不过如果线程池中总是充满被阻塞的任务,那么可能是线程池的规模过小。

8.2 设置线程池的大小

要想正确的设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU?多大的内存?任务是计算密集型、I/O密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源?并且如果它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池可以根据各自的工作负载来调整。

  • 对于计算密集型的任务,在拥有N个Cpu的系统上,当线程池的大小为N+1时,通常能有最优的利用率:即使当计算密集型的线程偶尔由于页缺失故障或者其他原因暂停时,这个“额外”的线程也能保证cpu的时钟周期不会被浪费。
  • 对于包含IO操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。如果要正确的设置线程池的大小,你需要估算任务的等待时间和计算时间的比值。

CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。

线程数量=cpu的数量cpu期望利用率(1 + 任务等待时间/任务处理时间)。
比如一个8核CPU,希望这部分工作的CPU使用率20%,任务等待时间允许200ms,每个任务执行10ms。
那么线程数量=80.2(1+200/10)= 33

8.3 配置ThreadPoolExcecutor

如果默认的构造函数不能满足需求,那么可以通过ThreadPoolExecutor的构造函数,并且根据自己的需求来定制。ThreadPoolExecutor定义了很多构造函数。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
8.3.1 线程的创建和销毁

线程池的基本大小(corePoolSize)、最大大小(maximumPoolSize)、存活时间等因素共同负责线程的创建与销毁。
通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占有的资源(回收线程时会产生额外的延迟,因为当需求增加时,必须创建新的线程来满足需求)。

  • newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,然后创建的线程不会超时。
  • newCachedThreadPool工厂方法将线程池最大的大小设置为Integer.MAX_VALUE,而基本大小设置为0,超时时间设置为1分钟,这样创建出来的线程可以被无限扩展,当需求降低的时候自动收缩
8.3.2 管理队列任务

在有限的线程池中限制可并发执行的任务数量(单线程的Executor是一种特例:它们能确保不会有任务并发执行,因为它们通过线程封闭来实现线程安全性。)
如果无限制的创建线程,那么将导致系统的不稳定性,并且通过固定大小的线程池(而不是收到一个请求就创建一个线程)来解决这样的问题。然而这个方案并不完整。在高负载的情况下,应用程序仍可能耗尽资源。如果新请求的到达速率超过了线程池的处理速率,那么新来的请求将累积起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。通过一个Runnable和一个链表节点来表示一个等待中的任务,当然比用线程来表示开销低很多。但是如果客户提交给服务器请求的速率超过了服务器的处理速度,那么资源仍可能被耗尽。

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排列方法有3种:

  • 无界队列
    newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有线程都处于忙碌,那么任务将在队列中等待,如果任务快速的到达,超过了cpu处理任务的速度,那么队列将无限制的增加。
  • 有界队列
    更稳妥的策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue等。有界队列可以避免资源耗尽。但是带来了一个新问题:当队列填满以后该怎么办?(饱和策略可以解决这个问题)。
  • 同步移交(Synchronous Handoff)
    对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue并不是一个真正的队列,而是一种在线程间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将会创建一个新的线程。否则根据饱和策略,这个任务将被拒绝。
    在newCachedThreadPool中就使用了SynchronousQueue。

当使用像LinkedBlockingQueue或ArrayBlockingQueue这样的FIFO队列,任务的执行顺序和它们的到达顺序相同,如果想进一步控制任务的执行顺序,可以使用PriorityBlockingQueue,内容根据自然顺序或者Comparable定义。

8.3.3 饱和策略

当有界队列被填满后,饱和策略开始发挥作用。
ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandle来修改。如果某个任务被提交到已经关闭的Executor时,也会触发饱和策略。
JDK提供了几种不同的RejectedExecutionHandle实现,每种实现都包含不同的策略:

  • AbortPolicy:中止策略,是默认的饱和策略。该策略将会抛出未检查的RejectedExecutionException,调用者可以捕获这个异常,然后根据需求来编写自己的处理代码。
  • DiscardPolicy:抛弃策略,会悄悄的抛弃该任务。
  • DiscardOldestPolicy:抛弃最旧的策略,会抛弃下一个将被执行的任务,然后尝试提交当前任务。(如果是优先队列,则会抛弃优先级最高的任务,因此不要将DiscardOldestPolicy和优先队列一起使用)
  • CallerRunsPolicy:调用者运行策略,实现了一种调节机制,既不会
  • 抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。当线程池中的所有线程都被占用,并且工作队列被填满的时候,下一个任务会在调用execute时在主线执行。由于执行需要一定时间,因此主线至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理完正在执行的任务。在这期间,主线程不会调用accept,因此请求将会被保存到TCP层的队列中而不是在应用程序的队列中,如果持续过载,TCP层最终发现它的请求队列被填满,因此同样会开始抛弃请求。从线程池 -> 工作队列 -> 应用程序 -> TCP层,最终到达客户端,这种策略能够实现一种平缓的性能降低。
/**
 * 创建一个固定大小的线程池,同时使用“调用者运行”的饱和策略
 */
ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS, N_THREADS, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(CAPACITY));
executor.setRejectedExecutionHandle(new ThreadPoolExecutor.CallerRunsPolicy);
8.3.4 线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程。
ThreadFactory接口只定义了一个方法Thread new Thread(Runnable r),每当线程池需要创建一个新线程时都会调用这个方法。如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executors中的privilegedThreadFactory工厂来定制自己的线程工厂。通过这样的方式创建出来的线程,将于privilegedThreadFactory拥有同样的访问权限。如果不使用privilegedThreadFactory,线程池创建的线程将从在需要更新线程时调用execute或submit的客户端程序中继承访问权限,从而导致一些令人困惑的安全问题。

8.3.5 在调用构造函数后再定制ThreadPoolExecutor

在调用完成ThreadPoolExecutor的构造函数之后,仍然可以设置大多数传递给它的构造函数的参数。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转化为ThreadPoolExecutor

ExecutorService exec = Executors.newCachedThreadPool();
if(exec instanceof ThreadPoolExecutor){
    ((ThreadPoolExecutor) exec).setCorePool(10);
}else {
    throw new AssertionError("Oops,bad assumpion");
}

在Executors中包含一个unconfigurableExecutorService工厂方法,该方法可以对ExecutorService进行包装,如果你将ExecutorService暴露给不信任的代码,又不期望其被修改,就可以通过unconfigurableExecutorService来包装它。

8.3.6 拓展ThreadPoolExecutor

ThreadPoolExecutor是可拓展的,它提供了几个可以在子类化中改写的方法:

beforeExecute
afterExecute
terminated

这几个方法有利于拓展ThreadPoolExecutor的行为。在执行任务的线程池中将调用beforeExecute和afterExecute方法,以便与添加日志,计时。无论是从run中正常返回,还是抛出一个异常而返回,afterExcute都会被调用,如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,afterExecute也不被调用。
在线程池关闭操作时执行terminated,可以用来释放Executor在其生命周期里分配的各种资源,还可以发送通知,记录日志等。

避免活跃性危险

在安全性与活跃性之间通常存在着某些制衡。

我们使用加锁机制来确保线程安全,单如果过度的使用加锁,则可能导致锁顺序死锁(Lock-Ordering Deadlock)。
我们使用线程池和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁(Resource Deadlock)。

10.1 死锁

死锁:每个人都拥有其他人需要的资源,而同时又等待其他人已经拥有的资源,并且每个人在得到需要的资源前不会放弃已拥有的资源。

10.1.1 锁顺序死锁

如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁的问题。

public class LeftRightDeadlock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight() {
        synchronized (left) {
            synchronized (right) {
                doSomething();
            }
        }
    }

    public void rightLeft() {
        synchronized (right) {
            synchronized (left) {
                doSomethingElse();
            }
        }
    }

    void doSomething() {
    }

    void doSomethingElse() {
    }
}


10.1.2 动态的锁顺序死锁

有时候,并不能清楚地知道是否在锁顺序上有足够的控制权来避免死锁的发生。

/**
 * 容易发生死锁 
 */
public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException {
    synchronized(fromAccount){
        synchronized(toAccount){
            if(fromAccount.getBalance.compareTo(account) < 0){
                throw new InsufficientFundsException();
            } else {
                fromAccount.debit(amount);
                toAccount.credit(amount);
            }
        }
    }
}

所有的线程看起来都是按照相同的顺序来获取锁,事实上锁的顺序取决于传递给transferMoney的顺序。如果一个线程从X向Y转账,另一个线程从Y向X转账,那么就有可能发送锁顺序死锁。

10.1.3 在协作对象之间发生死锁

如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在外部方法中可能获得其他锁(这可能会产生死锁),或阻塞时间过长,导致其他线程无法即使获得当前被持有的锁。

10.1.4 开放调用

方法调用相当于一种抽象屏障,因为你无需了解被调用方法中所执行的操作。但也正是由于不知道在被调用方法中执行的操作,因此在持有锁的时候对调用某个外部方法难以进行分析,从而可能出现死锁。
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用(Open Call)。依赖于开放调用的类通常能表现出更好的行为,并且与那些在调用方法时需要持有锁的类相比,也更容易编写。通过尽可能的使用开放调用,将更容易找出那些需要获取多个锁的代码路径,因此也更容易确保采用一致的顺序来获取锁。

10.1.5 开放调用资源死锁

如当多个线程相互持有彼此正在等待的锁而又不释放自己已持有的锁时会发生死锁,当它们在相同的资源集合上等待时,也会发生死锁。
假设有两个资源池,例如两个不同数据库的连接池。资源池通常采用信号量来实现当资源池为空时的阻塞行为。如果一个任务需要连接两个数据库,并且在请求这两个资源时不会始终遵守相同的顺序,那么也可能出现死锁(资源池越大,死锁概率越小)。
另一种基于资源的死锁形式就是线程饥饿死锁:一个任务提交另一个任务,并等待被提交任务在单线程的Executor中执行完成。这种情况下,第一个任务将永远的等待下去,并使得另一个任务以及在这个Executor中执行的所有其他任务都停止执行。如果某些任务需要等待其他任务的结果,那么这些任务往往是产生线程饥饿死锁的主要来源。有界线程池/资源池与相互依赖的任务不能一起使用。

10.2 死锁的避免和诊断

如果一个程序每次最多只能获取一个锁,那么就不会产生锁顺序死锁。当然,这种情况通常并不现实。如果必须获得很多锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入文档并始终遵守。

在使用细粒度锁的程序中,可以通过使用一种两阶段策略(Two-Part Strategy)来检查代码中的死锁:
首先,找出在什么地方将获得多个锁(使这个集合尽量小)
对所有这些实例进行全局分析,从而确保它们在整个程序中获得锁的顺序都是一致的。
尽可能使用开放性调用,这样可以极大地简化分析过程。

10.2.1 支持定时的锁

有一项技术可以检测死锁和从死锁中恢复过来,即显式使用Lock类中的定时tryLock功能来代替内置锁机制。当使用内置锁的时,只要没获得到锁,就会永远的等待下去,而显式锁则可以指定一个超时时限,在等待超过该时间后,tryLock会返回一个失败信息。如果超时时限比获取锁的时间长很多,那么可以在发生某个意外情况后重新获得控制权。

10.2.2 通过线程转储(Thread Dump)信息来分析死锁(JVM支持)

Window:Ctrl + Break
Unix:Ctrl + \ 或者 kill -3 ,输出到了/proc//fd/1
jstack:jstack >> 输出文件

10.3 其他活跃性危险

10.3.1 饥饿

当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿(Starvation)
导致原因:

优先级处理不当
持有锁时执行无法结束的结构(无限循环、无限制等待某个资源)

10.3.2 糟糕的响应性

GUI程序使用了后台线程(运行时间长),会与前台事件竞争CPU
不良的锁管理:eg:某线程长时间占用锁

10.3.3 活锁(Livelock)

活锁通常是由过度的错误恢复代码造成的。将不可修复的错误地认为可修复。
不会阻塞线程,但也不会继续执行完成,因为线程将不断地执行相同的操作,而且总是失败。
eg:调事物 ——>回滚 ——>再调——>再回滚——>再调——>再回滚——>…

2个过于礼貌的人,在路上面对面相遇了,彼此都让出对方的路,然而又在另一条路上相遇了…。因此他们就这样反复避让下去。

解决办法: 在重试机制中引入随机性。eg:都稍后再处理,稍后的时间随机。

性能与可伸缩性

11.1 对性能的思考

造成开销的操作包含:

  1. 线程之间的协调(比如:锁、触发信号以及内存同步等)
  2. 添加的上下文切换
  3. 线程的创建和销毁
  4. 线程的调度
11.1.1 性能与可伸缩性

更有效地利用现有处理资源
尽可能地利用新的处理资源

11.2 Amdahl定律

Amdahl定律一般称阿姆达尔定律,它描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速力,这个值取决于程序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高的加速比为:

Speedup ≤ 1/(F+(1-F)/N)

11.3 线程引入的开销

11.3.1 上下文切换

可运行的线程数量 CPU数量,将某个正在运行的线程调度出来,然后让其他线程在CPU上运行。这将导致一次上下文切换。有一定的开销。新线程被切换进来,他所需要的数据不在当前处理器的本地缓存,因此首次调度会慢。
  这就是为什么调度器会为每个可运行的线程分配一个最小执行时间,将上下文切换的开销分摊到不会中断的执行时间上,从而提高整体吞吐量。
Unix的 vmstat 检测

11.3.2内存同步

同步操作的性能开销:synchronized 和volatile提供的可见性保证中会使用到内存栅栏(Memory Barrier),它可以刷新缓存,使缓存无效。

区分有竞争的同步和无竞争的同步

synchronized针对无竞争的同步进行了优化(volatile通常是无竞争的)。
现代的JVM通过优化可以去掉一些不发生竞争的锁。

可通过锁消除优化去掉的锁获取操作:

/**
一些更完备的JVM能通过逸出分析(Escape Analysis)来找出不会发布到堆的本地对象引用(因此这个引用是线程本地的)。
对List的唯一引用就是stooges,并且所有封闭在栈中的变量都会自动成为线程本地变量。
在执行过程中,至少会将Vector上的锁 获取/释放 4次(每次调用add或toString都会执行一次)。
然而,一个智能运行的编译器通常会分析这些调用,
从而是stooges及其内部状态不会逸出,因此可以自动去掉这4次对锁的获取操作。

这个编译器优化成为锁消除优化(Lock Elision)* 英[ɪˈlɪʒn]:省音,省略部分读音*
IBM 的JVM支持,HotSpot预期从7开始支持。
*/
public String getStoogeNames() {
    Vector stooges = new Vector();
    stooges.add("Moe");
    stooges.add("Larry");
    stooges.add("Curly");
    return stooges.toString();
}
11.3.3 阻塞

非竞争的操作只需在JVM中处理,而竞争的同步可能需要OS的介入,从而增加开销。
在锁上发生竞争时,竞争失败的线程肯定会发生阻塞。
阻塞实现方式

  • 自旋等待(Spin-Wating:通过循环不断地尝试获取锁,直到成功)(适合等待时间短的切换)
  • 操作系统挂起(适合等待时间长的)

11.4减少锁的竞争

在并发情况下,对可伸缩性最主要的威胁就是独占方式的资源锁。

有3种方式降低锁的竞争程度

  1. 减少锁的持有时间
  2. 降低锁的请求频率
  3. 使用带有协调机制的独占锁,这些机制运行更高的并发性
11.4.1 缩小锁的范围(快进快出)

缩短锁的持有时间,将一些与锁无关的代码移出代码块儿。尤其是那些开销较大的操作,以及可能被阻塞的操作,例如I/O操作。

@ThreadSafe
public class AttributeStore {
    @GuardedBy("this")
    private final Map<String,String> attributes = new HashMap<>();
    //只有Map.get()方法才真正需要➕锁,却➕到了整个方法上
    public synchronized boolean userLocationMatches(String name, String regexp) {
        String key = "user." + name + ".location";
        String location = attributes.get(key); //只有Map.get()方法才真正需要➕锁
        if(location == null)
            return false;
        else
            return Pattern.matches(regexp, location);
    }
}
@ThreadSafe
public class BetterAttributeStore {
    @GuardedBy("this")
    private final Map<String,String> attributes = new HashMap<>();//共享状态
    
    public boolean userLocationMatches(String name, String regexp) {
        String key = "user." + name + ".location";
        String location;
        synchronized(this) {
            location = attributes.get(key);//共享状态只在此处被访问
        }
        if(location == null)
            return false;
        else
            return Pattern.matches(regexp, location);
    }
}
11.4.2 减小锁的粒度

降低线程请求锁的频率可以通过两种手段来实现:

  • 锁分解
  • 锁分段

如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解为多个锁,每个锁只保护一个变量,从而提高可伸缩性并降低锁被请求的频率。

锁分解示例:

public class RestState {
    @Generated("this")
    public final Set<String> users;//食客
    @Generated("this")
    public final Set<String> reserves;//预定

    /** ... */

    public void addUser(String user){
        synchronized (users){
            users.add(user);
        }
    }

    public void addReserves(String reserv){
        synchronized (reserves){
            reserves.add(reserv);
        }
    }
}
11.4.3 锁分段

ConcurrentHashMap中实现了一个包含16个锁的数组,每个所保护所有散列同的1/16,其中第N个散列同由第(N mod 16)个锁来保护。大约能把对于锁的请求减少到原来的1/16。这使得ConcurrentHashMap能够支持多达16个并发写入器。(使得拥有大量处理器的系统在高访问量的情况下实现更高的并发性)

基于散列的Map中使用锁分段技术:

@ThreadSafe
public class StripedMap {
//同步策略:buckets[n]由Locks[n%N_LOCKS]来保护
    private static final int N_LOCKS = 16;
    private final Node[] buckets;
    private final Object[] locks;
    
    private static class Node{
        Node pre;
        Node next;
        Object key;
        Object value;
        public Node next() {return next;}
    }
    
    public StripedMap(int numBuckets) {
        buckets = new Node[numBuckets];
        locks = new Object[N_LOCKS];
        for (int i = 0; i < N_LOCKS; i++)
            locks[i] = new Object();
    }
    
    private final int hash(Object key) {
        return Math.abs(key.hashCode() % buckets.length);
    }
    
    public Object get(Object key) {
        int hash = hash(key);
        synchronized (locks[hash % N_LOCKS]) {
            for(Node m = buckets[hash]; m!= null; m= m.next())
                if(m.key.equals(key))
                    return m.value;
        }
        return null;
    }
    
    public void clear() {
        for(int i = 0; i < buckets.length; i++) {
            synchronized (locks[i % N_LOCKS]) {
                buckets[i] = null;
            }
        }
    }
    //...
}
11.4.4 避免热点域

当每个操作都请求多个变量时,锁的粒度很难降低,常见的优化措施例如将一些反复计算的结果缓存起来,这时候就会引入一些“热点域”。

HashMap的size方法实现就是一个热点域的例子,此方法用于计算Map中的元素数量,在插入和移除元素时更新一个计数器,在单线程或完全同步的实现中,这种使用独立计数器的做法能提高类似size和isEmpty这些方法的执行速度,但却导致更难提升实现的可伸缩性,因为每个修改map的操作都要更新这个共享的计数器(即使使用了锁分段),计数器这时候就是一个热点域。

ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段中的元素数量进行相加,而不是维护一个全局计数,它为每个分段都维护了一个独立的计数来避免枚举每个元素,并通过每个分段的锁来维护这个值,有效地避免了热点域问题。

11.4.5 一些替代独占锁的方法

并发容器、读-写锁 、不可变对象、原子变量。

11.4.6 监测CPU的利用率

CPU利用不充足的原因:负载不充足、I/O密集、外部限制、锁竞争 等

11.4.7 向对象池说“不”

在对象池中,对象能被循环使用,而不是由垃圾收集器回收,并在需要时重新分配。
多线程向对象池请求一个对象时,需要使用同步协调对象次的访问。

11.5 比较Map的性能

ConcurrentHashMap的的实现中假设,大多数常用操作都是获取某个已经存在的值,因此它对各种get操作进行了优化从而提供最高的性能和并发性。
ConcurrentHashMap大多数读操作并不会加锁,并且在写入操作和其它一些读操作中使用了锁分段技术。因此多个线程能并发地访问Map而不发生阻塞。

11.6 减少上下文切换的开销

并发程序的测试

测试并发程序而言,所面临的主要挑战在于:潜在的错误发生具有不确定性,需要比普通的串行程序测试更广的范围并且执行更长的时间。
并发测试大致分为两类:安全性测试和活跃性测试。
  安全测试 ----- 通常采用测试不变性条件的形式,即判断某个类的行为是否与其他规范保持一致。
  活跃性测试 ----- 包括进展测试和无进展测试两个方面(很难量化)。
  性能测试----- 性能测试与活跃性测试相关,主要通过:吞吐量、响应性、可伸缩性衡量。

12.1 正确性测试

测试并发类设计单元测试时,首先要执行与测试串行类时相同的分析----找出需要检查的不变性条件与后验条件。(不变性条件:判断状态是有效还是无效,后验条件:判断状态改变后是否有效)。接下来讲通过构建一个基于Semaphore来实现的缓存的有界缓存,测试缓存的正确性。

12.2对阻塞行为与对中断响应的测试

测试阻塞行为,当然线程被阻塞不再执行时,阻塞才是成功的,为了让阻塞行为效果更明显,可以在阻塞方法中抛出异常。
  当阻塞发生后,要使方法解除阻塞最简单的方式是采用中断,可以在阻塞方法发生后,线程阻塞后再中断它,当然这要求阻塞方法提取返回或者抛出InterrupedException来响应中断。

显式锁

Java5之前在协调对共享对象的访问时可使用的机制只有synchronized和volatile。
Java5 增加了一种新的机制ReentrantLock。

13.1Lock与ReentrantLock

Lock接口中定义了一种无条件、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。其中tryLock();是轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁,tryLock(long timeout, TimeUnit unit) 是通过定时释放已获得的锁,放弃本次操作。

public interfece Lock
{
    void lock();//显式加锁
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();//轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁
    boolean tryLock(long timeout, TimeUnit unit) 
        throw InterruptedException;//定时锁获取
    void unlock();
    Condition newCondition();
}

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock同样提供了可重入的加锁语义。
  大多情况下,内置锁能很好的工作,但在功能上仍存在一些局限性。例如:无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限等待下去。内置锁在获取的过程中无法中断。内置锁必须在获取该锁的代码块中释放,无法实现非阻塞结构的加锁规则,很难实现带有时间限制的操作。(Lock接口中对应每个方法就是解决内置锁不足)

ReentrantLock使用时必须在finally块中释放锁,如果忘记会非常“危险”,当然可以使用静态分析工具FindBugs中的“未释放锁”检查器检查。

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    // 更新对象状态
    // 捕获异常,并在必要时恢复不变性条件
} finally {
    lock.unlock();//一定要记得在finally块里释放
}

13.1.1 轮询锁与定时锁

Lock接口中有可定时可轮询的锁获取方法tryLock,解决了内置锁死锁的难题。(在内置锁中,死锁恢复程序的唯一方法是重新启动程序,而放置死锁的唯一方法是构造程序时避免出现不一致的锁顺序)。重新改写10章中动态顺序死锁问题,使用tryLock来获取两个锁,如果不能同时获取,那么就回退并重新尝试。

public class DeadlockAvoidance {
    private static Random rnd = new Random();

     public boolean transferMoney(Account fromAcct,
                                  Account toAcct,
                                  DollarAmount amount,
                                  long timeout,
                                  TimeUnit unit)
          throws InsufficientFundsException, InterruptedException {
         long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
         long randMod = getRandomDelayModulusNanos(timeout, unit);
         long stopTime=System.nanoTime()+unit.toNanos(timeout);
         //使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试
         while(true){
             if(fromAcct.lock.tryLock()){  //使用tryLock来获取锁
                 try{
                     if(toAcct.lock.tryLock()){
                         try{
                             if(fromAcct.getBalance().compareTo(amount)<0)
                                 throw new InsufficientFundsException();
                             else{
                                 fromAcct.debit(amount);
                                 toAcct.credit(amount);
                                 return true;
                             }
                         }finally{
                             toAcct.lock.unlock();
                         }
                     }
                 }finally{
                     fromAcct.lock.unlock();  //无论成功与否都会释放所有锁
                 }
             }
             //如果在指定时间内不能获得所有需要的锁,那么transferMoney将返回一个失败状态,从而使该操作平缓地失败。
             if(System.nanoTime()<stopTime)
                 return false;      
             //在休眠时间中包含固定部分和随机部分,从而降低发生活锁的可能性。
             NANOSECONDS.sleep(fixedDelay+rnd.nextLong()%randMod);
         }
     }

     private static final int DELAY_FIXED = 1;
     private static final int DELAY_RANDOM = 2;

     static long getFixedDelayComponentNanos(long timeout, TimeUnit unit) {
            return DELAY_FIXED;
     }

     static long getRandomDelayModulusNanos(long timeout, TimeUnit unit) {
            return DELAY_RANDOM;
     }

     static class DollarAmount implements Comparable<DollarAmount> {
            public int compareTo(DollarAmount other) {
                return 0;
            }
            DollarAmount(int dollars) {
            }
        }
     class Account {
            public Lock lock;
            void debit(DollarAmount d) {
            }
            void credit(DollarAmount d) {
            }
            DollarAmount getBalance() {
                return null;
            }
     }
     class InsufficientFundsException extends Exception {
     }
}

在实现具有时间限制的操作时,定时锁非常有用。 当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定时间内给出结果,那么程序就会提前结束。 当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作。

public class TimedLocking {
    private Lock lock = new ReentrantLock();
    //定时的tryLock能够在这个带有时间限制的操作中实现独占加锁行为。
    public boolean trySendOnSharedLine(String message,
                                       long timeout,TimeUnit unit)
                                  throws InterruptedException{
        long nanosToLock=unit.toNanos(timeout)
                -estimatedNanosToSend(message);
        if(!lock.tryLock(nanosToLock,NANOSECONDS)) //如果不能再指定时间内获得锁,就失败
            return false;
        try{
            return sendOnSharedLine(message);
        }finally {
            lock.unlock();
        }
    }
    private boolean sendOnSharedLine(String message) {
        //传送信息
        return true;
    }
    long estimatedNanosToSend(String message) {
            return message.length();
    }   
}

13.1.2 锁获取操作可中断

可中断的锁获取操作能在可取消的操作中使用加锁。

InterruptibleLocking 使用了lockInterruptibly来实现sendOnSharedLine,以便在一个可取消的任务中调用它。 定时的tryLock同样能响应中断,因此当需要一个定时的和可中断的锁获取操作时,可以使用tryLock方法。

//   13-5   可中断的锁获取操作
public class InterruptibleLocking {
    private Lock lock = new ReentrantLock();
    public boolean sendOnSharedLine(String message)
            throws InterruptedException {
        lock.lockInterruptibly();
        try {
            return cancellableSendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }
    private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
        /* send something */
        return true;
    }
}

13.1.3 非块结构加锁(可以不要像内置锁获取释放都基于代码块)

在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块,可以避免可能的编码错误,但有时候需要更加灵活的加锁规则。

13.2性能考虑因素

java5.0刚出显式锁时,ReentrantLock确实极大的与内置锁体现出吞吐率的差距,ReentrantLock能提供更高的吞吐量。但到了java6中,内置锁的性能得到极大改善,性能并不会由于竞争而急剧下降,并且与ReentrantLock可伸缩性基本相当。

13.3 公平性

在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。 在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁(在Semaphore中同样可以选择采用公平或非公平的获取顺序)。

在激烈竞争的情况下,非公平锁的性能高于公平锁,其中的一个原因时:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。

假设线程A持有一个锁,并且线程B请求这个锁。由于A持有这个锁,因此B挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。此时,如果C也请求这个锁,那么C很可能在B被完全唤醒之前获得,使用及释放这个锁。
这是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。

13.4 在Synchronized和ReentrantLock之间如何选用

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
内置锁相比ReentrantLock优点在于:
  a、内置锁在线程转储中能给出哪些帧中获得了哪些锁,并能识别和检测发生死锁的线程。而ReentrantLock在java5.0时还不知道哪些线程持有ReentrantLock,但在java6.0中提供了一个接口,通过对接口注册可以访问ReentrantLock的加锁信息。
  b、内置锁自动加锁与释放锁,ReentrantLock需要在finally中手动释放锁。

13.5 读/写ReadWriteLock锁

ReentrantLock和内置锁相同属于互斥锁,每次最多只能有一个线程持有ReentrantLock。互斥是一种保守的加锁策略,虽然可以避免“写/写”冲突和“写/读”冲突,但是也避免了“读/读”冲突,在许多情况下大多数的操作都是读操作,那么互斥这一保守的加锁策略会影响并发的读取性能。
在这种情况下就可以使用读/写锁ReadWriteLock

:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

小结:与内置锁相比,显式的Lock在处理锁上更加灵活,但是ReentrantLock不能完全替代synchronized。当访问被保护对象以读取操作为主,那么读/写锁才能提高程序的可伸缩性

构建自定义的同步工具

14.1 状态依赖性的管理

像FutureTask、Semaphore和BlockingQueue类中的操作有基于状态为前提的类称为状态依赖性类。例如,不能从一个空的队列中删除元素、或者获取一个尚未结束的任务的计算结果。(依赖状态的个人理解:某个操作依赖于/等着什么状态条件改变,状态依赖性的管理需要考虑:在依赖条件不满足时该干什么?如何监测依赖状态的改变不浪费CPU资源,响应性高?)

  例如在生产者—消费者的设计经常会像ArrayBlockingQueue的有界缓存,在有界缓存的put和take操作中都有状态依赖条件:不能从空缓存中获取元素,也不能将元素放入已满缓存中。当依赖条件不满足时,这些依赖状态的操作put、take可以抛出一个异常或者返回一个错误,也可保持阻塞直到对象进入正确的状态。
  
下面通过BaseBoundedBuffer有界缓存对依赖条件的失败不同处理介绍有界缓存的几种实现。

@ ThreadSafe
public abstract class BaseBoundedBuffer<E> {
       @GuardeBy( "this" ) private final E[] buf;
       @GuardeBy( "this" ) private int tail;
       @GuardeBy( "this" ) private int head;
       @GuardeBy( "this" ) private int count;
       protected BaseBoundedBuffer( int capacity) {
             this .buf = (E[]) new Object[capacity];
      }
       protected synchronized final void doPut(E E) {
            buf[tail] = E;
             if (++tail == buf.length) {
                  tail = 0;
            }
            ++count;
      }
       protected synchronized final E doTake() {
            E E = buf[head];
            buf[head] = null ;
             if (++head == buf.length) {
                  head = 0;
            }
            --count;
             return E;
      }
       public synchronized final boolean isFull() {
             return count == buf.length;
      }
       public synchronized final boolean isEmpty() {
             return count == 0;
      }
}

GrumpyBoundedBuffer将前提条件的失败传递给调用者,虽然这种方法实现起来很简单,但是调用者必须做好捕获异常的准备,并且当如果在多个地方都要调用put和take方法时,并且前提条件还失败,这样就要不断的重试。

@ ThreadSafe
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
       public GrumpyBoundedBuffer( int size){
             super (size);
      }    
       public synchronized void put(V v){
             if (isFull()){
                   throw new BufferFullException ();
            }
            doPut(v);
      }      
       public synchronized V take(){
             if (isEmpty())
                   throw new BufferEmptyExeption ();
             return doTake();
      }
}

调用者要调用GrumpyBoundedBuffer的take和put方法,取出值进行操作,可以采用两种方式:1、在循环中不断重试,这种方法称为忙等待或者自旋等待。自旋导致CPU时钟周期浪费。2、当缓存状态不满足时,进行休眠。低响应性。

@ ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
       public SleepyBoundedBuffer( int size) {
             super (size);
      }
       public void put(V v) throws InterruptedException{
             while (true ){
                   synchronized (this ){
                         if (!isFull()){
                              doPut(v);
                               return ;
                        }
                  }
                  Thread.sleep(SLEEP_GRANULARITY);
            }
      }    
       public V take() throws InterruptedException{
             while (true ){
                   synchronized (this ){
                         if (!isEmpty()){
                               return doTake();
                        }
                  }
                  Thread.sleep(SLEEP_GRANULARITY);
            }
      }
}


SleepyBoundedBuffer就是通过“轮询与休眠”重试机制实现put、take方法,从而使调用者无需每次调用都实现重试的逻辑。如果依赖条件不满足,那么当前执行的线程首先会释放锁并且休眠一段时间,从而让其他线程能够访问缓存。当线程醒来时,它将重新请求锁并重新尝试操作,因而线程能反复在休眠以及测试状态条件过程中切换,直到可以执行位置。
  从调用者看来,这种方法能很好的运行。但是如何选择合适的休眠时间间隔,就要在响应性和CPU使用率之间权衡,休眠时间间隔越小,响应性越高,但消耗的CPU资源越高。

14.1.3 条件队列

条件队列能及时响应依赖状态的改变且不浪费CPU。
条件队列:装入的数据项是等待先验条件成立而被挂起的线程。

每个java对象可以作为一个锁,每个对象同样可以作为一个条件队,并且Object中的wait、notify和notifyAll方法就构成内部条件队列的api。对象的内置锁和内部条件队列是相互关联的,要调用某个对象中条件队列的wait、notify和notifyAll任何一个方法,必须先持有该对象上的锁。

下面的BoundedBuffer中,当执行notifyAll()方法时,线程会从wait()地方唤醒(本来是不满足条件谓词,执行wait然后挂起了线程)。使用条件队列会明显比“休眠”有界缓存更加高效,响应性也更高。

@ ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> { 
       // 条件谓词:not-full (!isFull())
       // 条件谓词:not-empty (!isEmpty())     
       public BoundedBuffer( int size) {
             super (size);
      }   
       // 阻塞并直道:not-full
       public synchronized void put(V v) throws InterruptedException{
             while (isFull()){
                  wait();
            }
            doPut(v);
            notifyAll();
      }     
       // 阻塞并直道:not-empty
       public synchronized V take() throws InterruptedException{
             while (isEmpty()){
                  wait();
            }
            V v = doTake();
            notifyAll();
             return v;
      }
}

14.2 使用条件队列

14.2.1 条件谓词

锁、条件谓词、条件队列三者关系并不复杂,但是wait方法返回并不意味着线程正在等待的条件谓词变真了,一个条件队列与多个条件谓词相关是很常见的情况。因此,每次线程都从wait方法唤醒,都必须再次测试条件谓词,由于现场在条件谓词不为真的情况下也可以反复醒来,因此必须在一个循环中调用wait,并且每次迭代中都测试条件谓词。

14.2.2 过早唤醒

条件等待的标准形式如下:

void stateDependentMethod() throws InterruptedException{
	synchronized(lock){
		while(!conditionPredition)
			lock.wait();
		doSomething();
	}
}

14.2.3 丢失的信号

信号丢失:指线程必须等待一个已经为真的条件,但在开始等待之前没有检测条件谓词。好比:启动了烤面包机去拿报纸,当烤面包机铃声响了,你没有听到还在等待烤面包机的铃声,因此可能会等待很长的时间。信号丢失也是一种活跃性故障。

14.2.4 通知

只有同时满足下面两个条件,才能用notify而不是notifyAll。但是大多数的类并不满足下面两个条件,因此普遍认可做法是notifyAll,虽然会比notify低效,但是可以确保正确。
  1、只有一个条件谓词与条件队列相关,并且每个线程从wait返回后都执行相同的操作。
  2、在条件变量上的每次通知,最多只能唤醒一个线程来执行。

14.3 显式的Condition对象

在Java中,除了提供内置锁和内置条件队列,还提供显式锁和显式条件队列。其中显式锁为Lock,显示条件队列为Condition对象。

一个Condition是和一个Lock关联起来的,就像一个内置条件队列和一个内置锁关联起来一样。要创建一个Condition,可以在相关联的Lock上调用newCondition()方法。每个内置锁只能有一个与之关联的内置条件队列,与之不同的是,每个Lock上可以有多个与他关联的Condition,这就使得我们对Condition的控制更加细粒度化。

对于上面的BoundedBuffer类,使用显式条件队列进行改进,如下:(signal比signalAll更高效,能极大的减少在每次缓存操作中发生的上下文切换和锁请求的次数。)

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionBoundedBuffer<T>  {
	private Lock lock=new ReentrantLock();
	private Condition notFull=lock.newCondition();
	private Condition notEmpty=lock.newCondition();	
	private int head,tail,count;
	private Object[] buf;
	
	public ConditionBoundedBuffer(int capacity){
		buf=new Object[capacity];
		head=0;tail=0;count=0;
	}	
	public void add(T t) throws InterruptedException{
		lock.lock();
		try{
			while(count==buf.length)
				notFull.wait();
			buf[tail]=t;
			if(++tail==buf.length)
				tail=0;
			count++;
			notEmpty.signal();
		}finally{
			lock.unlock();
		}
	}
	public T get() throws InterruptedException{
		lock.lock();
		try{
			while(count==0)
				notEmpty.wait();
			Object obj=buf[head];
			buf[head]=null;
			if(++head==buf.length)
				head=0;
			count--;
			notFull.signal();
			return (T)obj;
		}finally{
			lock.unlock();
		}
	}
}


14.4 Semaphore 剖析

在ReentrantLock和Semaphore两个接口中存在很多共同点,两个类都可以做一个“阀门”,每次都只允许一定数量线程通过,两者其实都是使用了一个共同的基类,即AbstractQueuedSynchronizer(AQS),ReentrantLock和Semaphore都实现了Synchronizer。
一个常见的练习,使用Lock来实现计数信号量。

@ThreadSafepublic class SemaphoreOnLock {    
	private final Lock lock = new ReentrantLock();    // CONDITION PREDICATE: permitsAvailable (permits > 0)
    private final Condition permitsAvailable = lock.newCondition();    @GuardedBy("lock") private int permits;
    SemaphoreOnLock(int initialPermits) {
        lock.lock();        try {
            permits = initialPermits;
        } finally {
            lock.unlock();
        }
    }    // BLOCKS-UNTIL: permitsAvailable
    public void acquire() throws InterruptedException {
        lock.lock();        try {            while (permits <= 0)
                permitsAvailable.await();
            --permits;
        } finally {
            lock.unlock();
        }
    }    public void release() {
        lock.lock();        try {
            ++permits;
            permitsAvailable.signal();
        } finally {
            lock.unlock();
        }
    }
}


14.5 AbstractQueuedSynchronizer

大多数的开发者都不会直接用AQS,常见的标准同步器类集合能满足大多数的需求。java.util.concurrent中许多可阻塞的类,例如ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch、SynchronousQueue和FutureTask都是基于AQS构建的,不需要过于深入了解实现的细节。

原子变量与非阻塞同步

近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法采用底层的原子机器指令,例如比较交换指令来代替锁确保数据在并发访问中的一致性。
  非阻塞算法广泛运行在操作系统和JVM实现进程、垃圾回收机制以及锁和其他并发数据结构,非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞,并且极大的减少调度开销

15.1 锁的劣势

1:无论哪个线程持有守护变量的锁,都只能采用独占方式来访问这些变量。
2:如果有多个线程同时请求锁,那么一些线程将被挂起或者采取自旋方式并在在稍后恢复运行,然而在挂起和恢复过程中存在很大的开销。(虽然说采用volatile变量不会发生上下文切换或线程调度等操作,并且也能保证可见性,但是volatile不能用于构建原子的复合操作)
3:当一个线程正在等待锁时,它不能做其他任何事情。 如果一个线程在持有锁的情况下被延迟执行(例如发生了缺页错误,调度延迟,或者其他类似情况),那么所有需要这个锁的线程都无法执行下去。

15.2 硬件对并发的支持

15.2.1 比较交换指令CAS

大多数的处理器采用的原子机器指令实现非阻塞算法是用比较交换指令CAS。

CAS包含了3个操作数——需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。 无论位置V的值是否等于A,都将返回V原有的值(这种变化形式被称为比较并设置,无论操作是否成功都会返回)。

CAS的含义是:“我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉V的实际为多少”。

@ThreadSafe
public class SimulatedCAS {
   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){
       //如果相同就将值设置为newValue。并返回true
       return (expectedValue==compareAndSwap(expectedValue, newValue)); 
   } 
}

15.2.2 非阻塞计数器

15.3 原子变量类

原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最小的情况。 更新原子变量的快速(非竞争)路径比获取锁的快速路径块,而慢速路径也一样,因为它不需要挂起或重新调度线程。 在使用基于原子变量而非锁的算法中,线程在执行时更不易出现延迟,并且如果遇到竞争,也更容易恢复过来。
 原子变量相当于一种泛化的volatile变量,能够支持原子的和有条件的读-该-写操作。原子变量共有12个原子变量类,可分为4组:标量类(scalars),更新器类(field updaters),数组类以及复合变量类(compound variables)。 其中最常用的原子变量就是标量类:AtomicInteger,AtomicLong,AtomicBoolean以及AtomicReference。所有原子变量类都支持CAS,此外,AtomicInteger和AtomicLong还支持算术运算。

15.3.1 原子变量是一种"更好的volatile"

AtomicInteger表示一个int类型的值,并提供了get和set方法,这些Volatile类型的int变量在读取和写入上有着相同的内存语义。 它还提供了一个原子的compareAndSet方法(如果该方法成功执行,那么将实现与读取、写入一个volatile变量相同的内存效果),以及原子的添加,递增递减等方法。
  原子数组类(只支持Integer,Long和Reference)中的元素可以实现原子更新。原子数组类为数组的元素提供了volatile类型的访问语义。基本变量类是不可修改的,而原子变量时可修改的。
  我们曾经用不可变对象volatile引用来原子地更新多个状态变量,此处我们可以通过原子变量来维护多个变量的不变性条件。

//   15-3   通过CAS来维持包含多个变量的不变性条件
@ThreadSafe
public class CasNumberRange {
    private static class IntPair{
        final int lower;   //不变性条件,lower<upper
        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; //get得到引用,IntPair类型
    }
    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;
        }
    } 
}

15.3.2 锁与原子变量的性能比较

可以看出,在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实的情况下,原子变量的性能将超过锁的性能。 这是因为锁在发生竞争时会挂起线程,从而降低了CPU的使用率和共享内存总线上的同步信号量。(类似于生产者-消费者模式中的可阻塞生产者,它能降低消费者上的工作负载,使消费者的处理速度赶上生产者的处理速度)。
在实际情况中,原子变量在可伸缩性上要高于锁,因此在应对常见的竞争程度时,原子变量的效率会更高。在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能更有效地避免竞争。

15.4 非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,这种算法被称为非阻塞算法。 在实现相同功能的前提下,非阻塞算法通常比基于锁的算法更为复杂。

15.4.4 ABA问题

ABA问题:如果在算法中的节点可以被循环使用,那么在使用”比较并交换”指令时就可能出现这种问题(主要在没有垃圾回收机制的环境中)。在CAS操作中将判断“V的值是否仍为A”,并且如果是的话就继续执行更新操作。有时候还需直到“自从上次看到V的值为A以来,这个值是否发生了变化”。在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。

内存模型

16.1 什么是内存模型,为什么须要它

如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果

  • 编译器把变量保存在本地寄存器而不是内存中
  • 编译器中生成的指令顺序,可以与源代码中的顺序不同
  • 处理器采用乱序或并行的方式来执行指令
  • 保存在处理器本地缓存中的值,对于其他处理器是不可见
16.1.1 平台的模型

每个处理器都拥有自己的缓存,并且定期地与主内存进行协调,在不同的处理器架构中提供了不同级别的缓存一致性,即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。JVM通过在适当的位置上插入内存栅栏来屏蔽在JMM与底层平台内存模型之间的差异。Java程序不需要指定内存栅栏的位置,而只需通过正确地使用同步来找出何时将访问共享状态。

16.1.2 重排序

各种使操作延迟或者看似乱序执行的不同原因,都可以归为重排序,内存级的重排序会使程序的行为变得不可预测。

public class ReorderingDemo {
    static int x = 0, y = 0, a = 0, b = 0;

    public static void main(String[] args) throws Exception {
        HashSet<String> objects = new HashSet<>();
        for (int i = 0; i < 10000; i++) {
            x = y = a = b = 0;
            Thread one = new Thread() {
                public void run() {
                    a = 1;
                    x = b;
                }
            };
            Thread two = new Thread() {
                public void run() {
                    b = 1;
                    y = a;
                }
            };
            one.start();
            two.start();
            one.join();
            two.join();
            objects.add(x + "_" + y);
            System.out.println(x + "_" + y);
        }
        System.out.println(objects);
    }
    // [0_1, 1_0, 1_1]
}
16.1.3 Java内存模型简介

JMM通过各种操作来定义,包括对变量的读写操作,监视器monitor的加锁和释放操作,以及线程的启动和合并操作,JMM为程序中所有的操作定义了一个偏序关系,成为Happens-before,要想保证执行操作B的线程看到A的结果,那么A和B之间必须满足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 发布

造成不正确发布的真正原因:"发布一个共享对象"与"另一个线程访问该对象"之间缺少一种Happens-Before的关系

16.2.1 不安全的发布
public class UnsafeLazyInitialization {
    private static Object resource;

    public static Object getInstance(){
        if (resource == null){
            resource = new Object(); //不安全的发布
        }
        return resource;
    }
}

原因一:线程B看到了线程A发布了一半的对象

原因二:即使线程A初始化Resource实例之后再将resource设置为指向它,线程B仍可能看到对resource的写入操作将在对Resource各个域的写入操作之前发生。因为线程B看到的线程A中的操作顺序,可能与线程A执行这些操作时的顺序并不相同。

16.2.2 安全的发布

例:BlockingQueue的同步机制保证put在take后执行,A线程放入对象能保证B线程取出时是安全的

借助于类库中现在的同步容器、使用锁保护共享变量、或都使用共享的volatile类型变量,都可以保证对该变量的读取和写入是按照happens-before排序的

happens-before事实上可以比安全发布承诺更强的可见性与排序性。

16.2.3 安全初始化模式

方式一:加锁保证可见性与排序性,存在性能问题

public class UnsafeLazyInitialization {
    private static Object resource;

    public synchronized static Object getInstance(){
        if (resource == null){
            resource = new Object(); //不安全的发布
        }
        return resource;
    }
}

方式二:提前初始化,可能造成浪费资源

 public class EagerInitialization {
     private static Object resource = new Object();
     public static Object getInstance(){
         return resource;
     }
 }

方式三:延迟初始化,建议

public class ResourceFactory {
    private static class ResourceHolder{
        public static Object resource = new Object();
    }
    public static Object getInstance(){
        return ResourceHolder.resource;
    }
}

方式四:双重加锁机制,注意保证volatile类型,否则出现一致性问题

public class DoubleCheckedLocking {
    private static volatile Object resource;
    public static Object getInstance(){
        if (resource == null){
            synchronized (DoubleCheckedLocking.class){
                if (resource == null){
                    resource = new Object();
                }
            }
        }
        return resource;
    }
}

16.3 初始化过程中的安全性

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

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域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值