博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[书]java并发编程的艺术笔记
阅读量:4058 次
发布时间:2019-05-25

本文共 53998 字,大约阅读时间需要 179 分钟。

本文属于自己整理的读书笔记,便于回顾.内容绝大部分来自书籍:,版权归原作者所有.


第1章 并发编程的挑战

1.多线程一定比单线程快?

不一定,如同在同时阅读两本书时的来回切换切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度.

并发执行的速度会比串行慢:这是因为线程有创建和上下文切换的开销.

2.如何减少上下文切换

减少上下文切换的方法有无锁并发编程(避免使用锁)、CAS算法(Java的Atomic包使用CAS算法来更新数据,而不需要加锁)、使用最少线程(避免创建不需要的线)和使用协程.

3.一段死锁的代码

public class DeadLockDemo {
privat static String A = "A"; private static String B = "B"; public static void main(String[] args) { new DeadLockDemo().deadLock();}private void deadLock() { Thread t1 = new Thread(new Runnable() { @Override publicvoid run() { synchronized (A) { try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }synchronized (B) { System.out.println("1"); } }}}); Thread t2 = new Thread(new Runnable() { @Override publicvoid run() { synchronized (B) { synchronized (A) { System.out.println("2"); } } }}); t1.start(); t2.start(); }}

4.避免死锁:

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内占用多个资源,
  • 使用定时锁(尝试使用lock.tryLock(timeout))
  • 对于数据库锁,加锁和解锁必须在一个数据库连接中

第2章 Java并发机制的底层实现原理

5.volatile

在多处理器开发中保证了共享变量的“可见性“,一般它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度.

6.volatile的两条实现原则

  • Lock前缀指令(volatile变量相关操作转变成编程汇编代码,回带lock前缀)会引起处理器缓存回写到内存
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效(迫使工作内存中的变量重新读取主内存的最新的共享变量的值)

7.synchronized

synchronized实现同步的基础:Java中的每一个对象都可以作为锁.

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

8 . Synchonized在JVM里的实现原理:

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细不一样。代码块同步是使用monitorenter和monitorexit指令实现的,方法同步在JVM规范中没有指明.

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处.任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.

9.锁的升级与对比

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,锁可以升级但不能降级(提供获得锁和释放锁的效率).

这里写图片描述

这里写图片描述

10.Java如何实现原子操作

原子操作:不可被中断的一个或一系列操作

Java如何实现原子操作:

  • 使用循环CAS实现原子操作
  • 使用锁机制实现原子操作

CAS :

CAS 操作 :cas操作需要两个值,一个旧值A(期望操作前的值),一个新值B,操作时,先比较旧值A有没有发生修改,没有发生变化,才交换成新值B,否则不做交换.

CAS仍然存在三大问题

ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作.

  • ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
  • 循环时间长开销大解决:如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升
  • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作.

第3章 Java内存模型

11.java memory model:

Java的并发采用的是共享内存模型;

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
这里写图片描述

如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。

12.重排序:

代码实际执行顺序和代码编写顺序并不是一样的:

重排序分为三种:
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

13.happens-before:

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系:(深入jvm虚拟机中描述的更为详细):

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

14.as-if-serial语义

as-if-serial:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变.为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序

15.顺序一致性

如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同.即经过正确同步,程序执行应该具有内存一致性.

从上面的示意图可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系)。

16.volatile的内存语义

volatile变量自身特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写的内存语义:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量.

下面对volatile写和volatile读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息

保守策略的JMM**内存屏障插入策略**:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的前面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

17 .锁的内存语义

对锁释放和锁获取的内存语义做个总结:

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。·线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

18.Java线程之间的通信

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式。

1)A线程写volatile变量,随后B线程读这个volatile变量。
2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

concurrent包的源代码通用实现模式:

首先,声明共享变量为volatile
然后,使用CAS的原子条件更新来实现线程之间的同步
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

19.happens-before

这里写图片描述

happens-before关系本质上和as-if-serial语义是一回事.

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

20.双重检查锁前后

非线程安全的初始化对象:

public class UnsafeLazyInitialization {    private static Instance instance;    public static Instance getInstance() {        if (instance == null) // 1:A线程执行        instance = new Instance(); // 2:B线程执行        return instance;    }}

加synchronized :早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销

public class SafeLazyInitialization {    private static Instance instance;    public synchronized static Instance getInstance() {        if (instance == null)        instance = new Instance();        return instance;    }}

双重检查锁定(Double-Checked Locking):

public class DoubleCheckedLocking { // 1    private static Instance instance; // 2    public static Instance getInstance() { // 3        if (instance == null) { // 4:第一次检查        synchronized (DoubleCheckedLocking.class) { // 5:加锁            if (instance == null) // 6:第二次检查            instance = new Instance(); // 7:问题的根源出在这里            } // 8        } // 9        return instance; // 10    } // 11}

问题根源:

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化

前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate(); // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象instance = memory; // 3:设置instance指向刚分配的内存地址

上面3行伪代码中的2和3之间,可能会被重排序:

memory = allocate(); // 1:分配对象的内存空间instance = memory; // 3:设置instance指向刚分配的内存地址// 注意,此时对象还没有被初始化!ctorInstance(memory); // 2:初始化对象

根据java语言规范,所有线程在执行Java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。上面3行伪代码的2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。

单线程的执行示意图:

这里写图片描述
多线程执行示意图:
这里写图片描述

解决办法:

1.基于volatile的解决方案

public class SafeDoubleCheckedLocking {    private volatile static Instance instance;        public static Instance getInstance() {            if (instance == null) {            synchronized (SafeDoubleCheckedLocking.class) {                if (instance == null)                instance = new Instance(); // instance为volatile,现在没问题了                }            }        return instance;    }}

加入volatile关键字之后,图3-38 的2和3之间的重排序将会被禁止,所以就不会再有问题了.

2.基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

public class InstanceFactory {    private static class InstanceHolder {        public static Instance instance = new Instance();    }    public static Instance getInstance() {        return InstanceHolder.instance ; // 这里将导致InstanceHolder类被初始化    }}

示意图:

这里写图片描述
这个方案的实质是:允许3.8.2节中的3行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序.

结论:

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化
字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果需要对实例字段使用线程安全的延迟初始化,请使用基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

PS :java类初始化的同步机制:(看书)


第4章 Java并发编程基础

21.线程的状态

这里写图片描述

22.Java线程状态变迁

这里写图片描述

Thread.join()源码:

等待Thread执行结束之后,才继续其他线程的工作,(但是这样会使多线程变单线程):
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。

// 加锁当前线程对象public final synchronized void join() throws InterruptedException {    // 条件不满足,继续等待    while (isAlive()) {        wait(0);    } // 条件符合,方法返回}

23.ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
在代码4-15所示的例子中,构建了一个常用的Profiler类,它具有begin()和end()两个方法,而end()方法返回从begin()方法调用开始到end()方法被调用时的时间差,单位是毫秒。
list 4-15:

package loveStudy.core.listener;import java.util.concurrent.TimeUnit;public class Profiler {
// 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次 private static final ThreadLocal
TIME_THREADLOCAL = new ThreadLocal
() { protected Long initialValue() { return System.currentTimeMillis(); } }; public static final void begin() { TIME_THREADLOCAL.set(System.currentTimeMillis()); } public static final long end() { return System.currentTimeMillis() - TIME_THREADLOCAL.get(); } public static void main(String[] args) throws Exception { Profiler.begin(); TimeUnit.SECONDS.sleep(1); System.out.println("Cost: " + Profiler.end() + " mills"); }}

24:等待超时模式

等待超时模式的伪代码:

// 对当前对象加锁    public synchronized Object get(long mills) throws InterruptedException {        long future = System.currentTimeMillis() + mills;        long remaining = mills;        // 当超时大于0并且result返回值不满足要求        while ((result == null) && remaining > 0) {            wait(remaining);            remaining = future - System.currentTimeMillis();        }        return result;    }

25.一个简单的数据库连接池示例

我们使用等待超时模式来构造一个简单的数据库连接池,在示例中模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。

首先看一下连接池的定义。它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用fetchConnection(long)方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用releaseConnection(Connection)方法将连接放回线程池,示例如代码清单4-16所示。

import java.sql.Connection;import java.util.LinkedList;public class ConnectionPool {
private LinkedList
pool = new LinkedList
(); public ConnectionPool(int initialSize) { if (initialSize > 0) { for (int i = 0; i < initialSize; i++) { pool.addLast(ConnectionDriver.createConnection()); } } } public void releaseConnection(Connection connection) { if (connection != null) { synchronized (pool) { // 连接释放后需要进行通知,这样其他消费者能够感知到连接池中已经归还了一个连接 pool.addLast(connection); pool.notifyAll(); } } }// 在mills内无法获取到连接,将会返回null public Connection fetchConnection(long mills) throws InterruptedException { synchronized (pool) { // 完全超时 if (mills <= 0) { while (pool.isEmpty()) { pool.wait(); } return pool.removeFirst(); } else { long future = System.currentTimeMillis() + mills; long remaining = mills; while (pool.isEmpty() && remaining > 0) { pool.wait(remaining); remaining = future - System.currentTimeMillis(); } Connection result = null; if (!pool.isEmpty()) { result = pool.removeFirst(); } return result; } } }}

由于java.sql.Connection是一个接口,最终的实现是由数据库驱动提供方来实现的,考虑到只是个示例,我们通过动态代理构造了一个Connection,该Connection的代理实现仅仅是在commit()方法调用时休眠100毫秒,示例如下所示:

import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.sql.Connection;import java.util.concurrent.TimeUnit;public class ConnectionDriver {
static class ConnectionHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("commit")) { TimeUnit.MILLISECONDS.sleep(100); } return null; } }// 创建一个Connection的代理,在commit时休眠100毫秒 public static final Connection createConnection() { return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(), new Class
[] { Connection.class }, new ConnectionHandler()); }}

下面通过一个示例来测试简易数据库连接池的工作情况,模拟客户端ConnectionRunner获取、使用、最后释放连接的过程,当它使用时连接将会增加获取到连接的数量,反之,将会增加未获取到连接的数量,代码如下:

import java.sql.Connection;import java.util.concurrent.CountDownLatch;import java.util.concurrent.atomic.AtomicInteger;public class ConnectionPoolTest {
static ConnectionPool pool = new ConnectionPool(10); // 保证所有ConnectionRunner能够同时开始 static CountDownLatch start = new CountDownLatch(1); // main线程将会等待所有ConnectionRunner结束后才能继续执行 static CountDownLatch end; public static void main(String[] args) throws Exception { // 线程数量,可以修改线程数量进行观察 int threadCount = 10; end = new CountDownLatch(threadCount); int count = 20; AtomicInteger got = new AtomicInteger(); AtomicInteger notGot = new AtomicInteger(); for (int i = 0; i < threadCount; i++) { Thread thread = new Thread(new ConnetionRunner(count, got, notGot), "ConnectionRunnerThread"); thread.start(); } start.countDown(); end.await(); System.out.println("total invoke: " + (threadCount * count)); System.out.println("got connection: " + got); System.out.println("not got connection " + notGot); } static class ConnetionRunner implements Runnable {
int count; AtomicInteger got; AtomicInteger notGot; public ConnetionRunner(int count, AtomicInteger got, AtomicInteger notGot) { this.count = count; this.got = got; this.notGot = notGot; } public void run() { try { start.await(); } catch (Exception ex) { } while (count > 0) { try { // 从线程池中获取连接,如果1000ms内无法获取到,将会返回null // 分别统计连接获取的数量got和未获取到的数量notGot Connection connection = pool.fetchConnection(1000); if (connection != null) { try { connection.createStatement(); connection.commit(); } finally { pool.releaseConnection(connection); got.incrementAndGet(); } } else { notGot.incrementAndGet(); } } catch (Exception ex) { } finally { count--; } } end.countDown(); } }}

26.线程池接口的默认实现

public class DefaultThreadPool
implements ThreadPool
{
// 线程池最大限制数 private static final int MAX_WORKER_NUMBERS = 10; // 线程池默认的数量 private static final int DEFAULT_WORKER_NUMBERS = 5; // 线程池最小的数量 private static final int MIN_WORKER_NUMBERS = 1; // 这是一个工作列表,将会向里面插入工作 private final LinkedList
jobs = new LinkedList
(); // 工作者列表 private final List
workers = Collections.synchronizedList(new ArrayList
()); // 工作者线程的数量 private int workerNum = DEFAULT_WORKER_NUMBERS; // 线程编号生成 private AtomicLong threadNum = new AtomicLong(); public DefaultThreadPool() { initializeWokers(DEFAULT_WORKER_NUMBERS); } public DefaultThreadPool(int num) { workerNum = num > MAX_WORKER_NUMBERS ? MAX_WORKER_NUMBERS : num < MIN_WORKER_NUMBERS ? MIN_WORKER_NUMBERS : num; initializeWokers(workerNum); } public void execute(Job job) { if (job != null) { // 添加一个工作,然后进行通知 synchronized (jobs) { jobs.addLast(job); jobs.notify(); } } } public void shutdown() { for (Worker worker : workers) { worker.shutdown(); } } public void addWorkers(int num) { synchronized (jobs) { // 限制新增的Worker数量不能超过最大值 if (num + this.workerNum > MAX_WORKER_NUMBERS) { num = MAX_WORKER_NUMBERS - this.workerNum; } initializeWokers(num); this.workerNum += num; } } public void removeWorker(int num) { synchronized (jobs) { if (num >= this.workerNum) { throw new IllegalArgumentException("beyond workNum"); } // 按照给定的数量停止Worker int count = 0; while (count < num) { Worker worker = workers.get(count); if (workers.remove(worker)) { worker.shutdown(); count++; } } this.workerNum -= count; } } public int getJobSize() { return jobs.size(); }// 初始化线程工作者 private void initializeWokers(int num) { for (int i = 0; i < num; i++) { Worker worker = new Worker(); workers.add(worker); Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.incrementAndGet()); thread.start(); } } // 工作者,负责消费任务 class Worker implements Runnable { // 是否工作 private volatile boolean running = true; public void run() { while (running) { Job job = null; synchronized (jobs) { // 如果工作者列表是空的,那么就wait while (jobs.isEmpty()) { try { jobs.wait(); } catch (InterruptedException ex) { // 感知到外部对WorkerThread的中断操作,返回 Thread.currentThread().interrupt(); return; } } // 取出一个Job job = jobs.removeFirst(); } if (job != null) { try { job.run(); } catch (Exception ex) { // 忽略Job执行中的Exception } } } } public void shutdown() { running = false; } }}

从线程池的实现可以看到,当客户端调用execute(Job)方法时,会不断地向任务列表jobs中添加Job,而每个工作者线程会不断地从jobs上取出一个Job进行执行,当jobs为空时,工作者线程进入等待状态。

添加一个Job后,对工作队列jobs调用了其notify()方法,而不是notifyAll()方法,因为能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法获得更小的开销(避免将等待队列中的线程全部移动到阻塞队列中)。

27.一个基于线程池技术的简单Web服务器

(见github)

public class SimpleHttpServer {    // 处理HttpRequest的线程池    static ThreadPool
threadPool = new DefaultThreadPool
(1); // SimpleHttpServer的根路径 static String basePath; static ServerSocket serverSocket; // 服务监听端口 static int port = 8080; public static void setPort(int port) { if (port > 0) { SimpleHttpServer.port = port; } } public static void setBasePath(String basePath) { if (basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()) { SimpleHttpServer.basePath = basePath; } }// 启动SimpleHttpServer public static void start() throws Exception { serverSocket = new ServerSocket(port); Socket socket = null; while ((socket = serverSocket.accept()) != null) { // 接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行 threadPool.execute(new HttpRequestHandler(socket)); } serverSocket.close(); } static class HttpRequestHandler implements Runnable { private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { String line = null; BufferedReader br = null; BufferedReader reader = null; PrintWriter out = null; InputStream in = null; try { reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String header = reader.readLine(); // 由相对路径计算出绝对路径 String filePath = basePath + header.split(" ")[1]; out = new PrintWriter(socket.getOutputStream()); // 如果请求资源的后缀为jpg或者ico,则读取资源并输出 if (filePath.endsWith("jpg") || filePath.endsWith("ico")) { in = new FileInputStream(filePath); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int i = 0; while ((i = in.read()) != -1) { baos.write(i); } byte[] array = baos.toByteArray(); out.println("HTTP/1.1 200 OK"); out.println("Server: Molly"); out.println("Content-Type: image/jpeg"); out.println("Content-Length: " + array.length); out.println(""); socket.getOutputStream().write(array, 0, array.length); } else { br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath))); out = new PrintWriter(socket.getOutputStream()); out.println("HTTP/1.1 200 OK"); out.println("Server: Molly"); out.println("Content-Type: text/html; charset=UTF-8"); out.println(""); while ((line = br.readLine()) != null) { out.println(line); } } out.flush(); } catch (Exception ex) { out.println("HTTP/1.1 500"); out.println(""); out.flush(); } finally { close(br, in, reader, out, socket); } } } // 关闭流或者Socket private static void close(Closeable... closeables) { if (closeables != null) { for (Closeable closeable : closeables) { try { closeable.close(); } catch (Exception ex) { } } } }}

第5章 Java中的锁

28.Lock接口

Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁.

使用便捷:
注意:不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

Lock lock = new ReentrantLock();    lock.lock();//不要写在try块内try {} finally {    lock.unlock();//保证在获取到锁之后,最终能够被释放}

Lock的API:

这里写图片描述

29.队列同步器AQS

getState()、setState(int newState)和compareAndSetState(int expect,int update)

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作.

同步器的设计是基于模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

通过一个独占锁的示例来深入了解一下同步器的工作原理:

(独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能
处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁)

class Mutex implements Lock {    // 静态内部类,自定义同步器    private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态 protected boolean isHeldExclusively() { return getState() == 1; } // 当状态为0的时候获取锁 public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 释放锁,将状态设置为0 protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } } // 仅需要将操作代理到Sync上即可 private final Sync sync = new Sync(); public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }}

上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现

一个可靠自定义同步组件的门槛。

30.重入锁

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方
法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁.
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

31.读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,它具有的特性:公平性选择,重进入,锁降级.

(1).写锁的获取与释放:

  • 如果存在读锁,则写锁不能被获取.原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作.只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
  • 写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态
  • 写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放.

(2).读锁的获取与释放

  • 读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态
  • 读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。

(3).锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。(现有写锁,在获取读锁,然后释放写锁的过程)

32.LockSupport工具

LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能.

LockSupport提供的阻塞和唤醒方法:
这里写图片描述

33.Condition接口

Object的监视器方法与Condition接口的对比:

这里写图片描述

Condition的(部分)方法以及描述:

这里写图片描述

Condition的实现分析:

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。


第6章 Java并发容器和框架

–并发编程大师Doug Lea不遗余力地为Java开发者提供了非常多的并发容器和框架.

34.ConcurrentHashMap的实现原理与使用

线程不安全的HashMap:

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。示例如下:
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

final HashMap
map = new HashMap
(2); Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }, "ftf" + i).start(); } } }, "ftf"); t.start(); t.join();

效率低下的HashTable:

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap的锁分段技术可有效提升并发访问率:

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁.ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问.

ConcurrentHashMap的结构:

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁.

这里写图片描述

ConcurrentHashMap的初始化:(看书)

为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size).

ConcurrentHashMap的操作:get操作、put操作和size操作

get操作:

先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素

public V get(Object key) {    int hash = hash(key.hashCode());    return segmentFor(hash).get(key, hash);}

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile类型.如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

transient volatile int count;volatile V value;

put操作:

put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁.
插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其在HashEntry数组里.为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容.

size操作:

要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和
–> count是一个volatile变量,多线程下,是不是将所有的segement的count相加就可以得到size了呢?
–>不是的,虽然相加时可以获取每个Segment的count的最新值,但是可能累加前使用的count发生了变化,那么统计结果就不准了。所以,最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。.
–> 因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是**先尝试2次通过不锁住**Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小.

35.ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现,该算法在Michael&Scott算法上进行了一些修改。

入队列的过程:

默认情况下head节点存储的元素为空,tail节点等于head节点.
单线程的角度:
这里写图片描述

通过调试入队过程并观察head节点和tail节点的变化,发现入队主要做两件事情:第一是将入队节点设置成当前队列尾节点的下一个节点;第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点.

多线程如何通过CAS实现入队:

两件事情:第一是定位出尾节点;第二是使用
CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
学习大师的代码:

public boolean offer(E e) {        if (e == null)            throw new NullPointerException();        // 入队前,创建一个入队节点        Node
n = new Node
(e); retry: // 死循环,入队不成功反复入队。 for (;;) { // 创建一个指向tail节点的引用 Node
t = tail; // p用来表示队列的尾节点,默认情况下等于tail节点。 Node
p = t; for (int hops = 0;; hops++) { // 获得p节点的下一个节点。 Node
next = succ(p); // next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点 if (next != null) { // 循环了两次及其以上,并且当前节点还是不等于尾节点 if (hops > HOPS && t != tail) continue retry; p = next; } // 如果p是尾节点,则设置p节点的next节点为入队节点。 else if (p.casNext(null, n)) { /* * 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点, * 更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点 */ if (hops >= HOPS) casTail(t, n); // 更新tail节点,允许失败 return true; } // p有next节点,表示p的next节点是尾节点,则重新设置p节点 else { p = succ(p); } } } }

出队列:看书吧…


36.Java中的阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

阻塞队列不可用时,这两个附加操作提供了4种处理方式:

这里写图片描述

  • 抛出异常:当队列满时,若再插入元素,会抛出IllegalStateException(”Queue
    full”)异常。当队列空时,获取元素会抛出NoSuchElementException异常。
  • 返回特殊值:插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null

  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。

  • ·超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

JDK 7提供了7个阻塞队列:

·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。(缓存系统的设计,定时任务调度..)
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。


37.Fork/Join框架

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。(分而治之)

工作窃取算法:大任务分成若干独立子任务,将子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应.有的线程会早于其他线程把自己的任务做完,这个时候回去窃取其他线程的队列中的任务去做.

所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从尾部拿任务执行.

Fork/Join框架的设计:

如果让我们来设计一个Fork/Join框架,该如何设计?
步骤1 分割任务 :将大任务分割,直到分割出的子任务足够小;
步骤2 执行任务并合并结果 :子任务分别放在双端队列里,然后几个启动线程分
别从双端队列里获取任务执行.执行结果统一放在一个队列中.最后合并数据;

Fork/Join使用两个类来完成以上两件事情。

①ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
·RecursiveAction:用于没有返回结果的任务。
·RecursiveTask:用于有返回结果的任务。
②ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。

示例代码:

package loveStudy.core.concurrency;import java.util.concurrent.ExecutionException;import java.util.concurrent.ForkJoinPool;import java.util.concurrent.ForkJoinTask;import java.util.concurrent.RecursiveTask;/** * @ClassName: CountTask * @Description: RecursiveTask 有返回值的任务:计算1+2+3+4,希望每个子任务最   * 多执行两个数的相加,那么我们设置分割的阈值是2 * @author zhangdi * @version */public class CountTask extends RecursiveTask
{
private static final int THRESHOLD = 2;// private int start; private int end; public CountTask(int start, int end) { super(); this.start = start; this.end = end; } @Override protected Integer compute() { int sum = 0; boolean canCompute = (end - start) <= THRESHOLD; if (canCompute) { // 如果任务足够小就计算任务 for (int i = start; i < end; i++) { sum += i; } } else { // 如果任务大于阈值,就分裂成两个子任务计算 int mid = (start + end) / 2; CountTask leftTask = new CountTask(start, mid); CountTask rightTask = new CountTask(mid, end); leftTask.fork(); rightTask.fork(); Integer join1 = leftTask.join(); Integer join2 = rightTask.join(); sum = join1 + join2; } return sum; } public static void main(String[] args) { ForkJoinPool forkJoinPool = new ForkJoinPool(); CountTask countTask = new CountTask(1, 4); ForkJoinTask
submit = forkJoinPool.submit(countTask); try { System.out.println("result:"+submit.get()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}

ForkJoinTask与一般任务的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务.

Fork/Join框架的实现原理:

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,(任务+线程)ForkJoinTask数组负责将存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务

ForkJoinTask的fork方法实现原理:

调用fork方法,ForkJoinWorkerThread调用pushTask方法异步地执行这个任务,然后立即返回结果.

public final ForkJoinTask
fork() { ((ForkJoinWorkerThread) Thread.currentThread()) .pushTask(this); return this;}

第7章 Java中的13个原子操作类

java.util.concurrent.atomic包:Atomic包里一共提供了13个类,属于4种类型的原子

更 新方式,分别是原子更新基本类型原子更新数组、原子更新引用和原子更新属性(字段).
Atomic包里的类基本都是使用Unsafe实现的包装类.

38.原子更新基本类型类

AtomicBoolean:原子更新布尔类型。

·AtomicInteger:原子更新整型。
·AtomicLong:原子更新长整型。
以AtomicInteger为例(三者类似):

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的(AtomicInteger里的value)相加,并返回结果。 -相加,获取结果
  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。 -比较设置值
  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。 -自增
  • void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“”。 –懒设置
  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值 -获取旧值,设置新值

Atomic包提供了3种基本类型的原子更新,但是Java的基本类型里还有char、float和double等.–>Atomic包里的类基本都是使用Unsafe实现的

Unsafe:

/** * 如果当前数值是expected,则原子的将Java变量更新成x *  * @return 如果更新成功则返回true */    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);    public final native boolean compareAndSwapLong(Object o, long offsetlong expected,long x);

39. 原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类。

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。
  • AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,常用方法:
- int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相 加。- boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

40.原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类:

- AtomicReference:原子更新引用类型。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)
栗子:

package loveStudy.core.concurrency;import java.util.concurrent.atomic.AtomicReference;public class AtomicReferenceTest {    public static void main(String[] args) {        AtomicReference
arf = new AtomicReference
(); User user = new User("zhangsan", 14); arf.set(user); User uptUser = new User("lisi", 15); boolean compareAndSet = arf.compareAndSet(user, uptUser); System.out.println(arf.get().getName()); System.out.println(arf.get().getOld()); } static class User { private String name; private int old; public User(String name,int old) { this.name = name; this.old = old; } public String getName() { return name; } public int getOld() { return old; } }}

41.原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。

·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
·AtomicLongFieldUpdater:原子更新长整型字段的更新器。
·AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题.

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。

以AstomicIntegerFieldUpdater为例:

public class AtomicIntegerFieldUpdaterTest {    public static void main(String[] args) {        AtomicIntegerFieldUpdater
newUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "old"); // 设置柯南的年龄是10岁 User conan = new User("conan", 10); // 柯南长了一岁,但是仍然会输出旧的年龄 System.out.println(newUpdater.getAndIncrement(conan)); // 输出柯南现在的年龄 System.out.println(newUpdater.get(conan)); } public static class User { private String name; public volatile int old;//public volatile public User(String name, int old) { this.name = name; this.old = old; } public String getName() { return name; } public int getOld() { return old; } }}

第8章 Java中的并发工具类

42.CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作.

join也可以实现线程的等待.join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待

public class CountDownLatchTest {    static CountDownLatch c = new CountDownLatch(2);    public static void main(String[] args) throws InterruptedException {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(1);                c.countDown();                System.out.println(2);                c.countDown();            }        }).start();        c.await();        System.out.println("3");    }}

CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N(这个值只能被设置一次)。

当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。

更多:

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

这里写图片描述

在实时系统中的使用场景:

让我们尝试罗列出在java实时系统中CountDownLatch都有哪些使用场景。我所罗列的都是我所能想到的。如果你有别的可能的使用方法,请在留言里列出来,这样会帮助到大家。

  • 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
  • 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
  • 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。

给出一个 CountDownLatch 的示例:系统开启时,先要检测一下数据库,网络,缓存是否已经启用,接下来才能做其他事情:

//抽象类,用于判断service状态,保存latch的引用public abstract class BaseHealthChecker implements Runnable {
private CountDownLatch _latch; private String _serviceName; private boolean _serviceUp; public BaseHealthChecker(CountDownLatch _latch, String _serviceName) { super(); this._latch = _latch; this._serviceName = _serviceName; this._serviceUp = false; } @Override public void run() { try { verifyService(); _serviceUp = true; } catch (Throwable t) { t.printStackTrace(System.err); _serviceUp = false; } finally { if (_latch != null) { _latch.countDown();//计数器减一 } } } public String getServiceName() { return _serviceName; } public boolean isServiceUp() { return _serviceUp; } //This methos needs to be implemented by all specific service checker public abstract void verifyService();}
public class NetworkHealthChecker extends BaseHealthChecker {
public NetworkHealthChecker(CountDownLatch _latch) { super(_latch, "network service"); } @Override public void verifyService() { System.out.println("checking.." + this.getServiceName()); try { Thread.sleep(8000);//模拟一下检测场景 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getServiceName() + "is up"); }}
public class DatabaseHealthChecker extends BaseHealthChecker {
public DatabaseHealthChecker(CountDownLatch _latch) { super(_latch, "database service"); } @Override public void verifyService() { System.out.println("checking.."+this.getServiceName()); try { Thread.sleep(8000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getServiceName()+"is up"); }}
public class CacheHealthChecker extends BaseHealthChecker {
public CacheHealthChecker(CountDownLatch _latch) { super(_latch, "Cache service"); } @Override public void verifyService() { System.out.println("checking.."+this.getServiceName()); try { Thread.sleep(8000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(this.getServiceName()+"is up"); }}
/**  * @ClassName: ApplicationStartupUtil  * @Description: 主启动类,它负责初始化闭锁,然后等待,直到所有服务都被检测完。     */ public class ApplicationStartupUtil {
//List of service checkers private static List
_services; //This latch will be used to wait on private static CountDownLatch _latch; //单例 private final static ApplicationStartupUtil INSTRANCE= new ApplicationStartupUtil(); public static ApplicationStartupUtil getInstance(){ return INSTRANCE; } public static boolean checkExternalServices() throws Exception{ //Initialize the latch with number of service checkers _latch = new CountDownLatch(3); _services = new ArrayList
(); _services.add(new DatabaseHealthChecker(_latch)); _services.add(new CacheHealthChecker(_latch)); _services.add(new NetworkHealthChecker(_latch)); //fixed thread pool ExecutorService threadPool = Executors.newFixedThreadPool(_services.size()); for (final BaseHealthChecker baseHealthChecker : _services) { threadPool.execute(baseHealthChecker); } //Now wait till all services are checked _latch.await(); //Services are file and now proceed startup for(final BaseHealthChecker v : _services) { if( ! v.isServiceUp()) { return false; } } return true; }}
/**  * @ClassName: Main  * @Description: 测试类  */ public class Main {
public static void main(String[] args) { boolean result = false; try { result = ApplicationStartupUtil.checkExternalServices(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("External services validation completed ! Result was :: "+ result); }}

43.同步屏障CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

CyclicBarrier的应用场景:

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景.

CyclicBarrier和CountDownLatch的区别:

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。

CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断.


44.控制并发线程数的Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源 (可以理解为臂板控制,车辆放行)

应用场景:

Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法
Semaphore(int permits)接受一个整型的数字,表示可用的许可证数(最大并发量),Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证.

public class SemaphoreTest {    private static final int THREAD_COUNT = 30;    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);    private static Semaphore s = new Semaphore(10);    public static void main(String[] args) {        for (int i = 0; i < THREAD_COUNT; i++) {            threadPool.execute(new Runnable() {                @Override                public void run() {                    try {                        s.acquire();                        System.out.println("save data");                        s.release();                    } catch (InterruptedException e) {                    }                }            });        }        threadPool.shutdown();    }}

45.线程间交换数据的Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

应用场景:

Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致.

public class ExchangerTest {    private static Exchanger
exch = new Exchanger
(); private static ExecutorService executors = Executors.newFixedThreadPool(2); //private static ExecutorService threadPool = Executors.newFixedThreadPool(2); public static void main(String[] args) { executors.execute(new Runnable() { @Override public void run() { String A = "银行流水A"; try { exch.exchange(A); System.out.println("A录入信息:"+exch.exchange("A")); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); executors.execute(new Runnable() { @Override public void run() { try { String B = "银行流水B"; // B录银行流水数据 //String A0 = exch.exchange("B"); String A = (String) exch.exchange(B); System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:" + A + ",B录入是:" + B); } catch (InterruptedException e) { } } }); executors.shutdown(); }}

第9章 Java中的线程池

46.线程池的实现原理

这里写图片描述

这里写图片描述

ThreadPoolExecutor执行execute方法分下面4种情况。

corePool–>blockQueue–>maxPool–>RejectedExecutionHandler
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁.


47.线程池创建及参数

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,milliseconds,runnableTaskQueue, handler);

1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建(进入队列)。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列

3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字,代码如下:

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

6) keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。TimeUnit(线程活动保持时间的单位).


48.向线程池提交任务:execute和submit的区别

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完

关闭线程池:

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表.

shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程.

shutdownNow: 设置为STOP状态; isShutdown返回trueshutdown: 设置为SHUTDOWN状态;isShutdown也返回true

可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数.CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务.


第10章 Executor框架

49.Executor框架的结构

  • 任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口
接口Runnable和接口Callable
  • 任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的
    ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口
    ThreadPoolExecutorScheduledThreadPoolExecutor
接口Executor -->接口ExecutorService-->类:ThreadPoolExecutor和ScheduledThreadPoolExecutor
  • 异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
接口Future->类FutureTask

Executor框架的类与接口:

这里写图片描述

Executor框架的使用示意图:

这里写图片描述

主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule)`)。

然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnablecommand));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable<T>task))。

如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(到目前为止的JDK中,返回的是FutureTask对象)

主线程可以执行FutureTask.get()方法来等待任务执行完成 或者

FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行.


50.Executor框架的成员

Executor框架的主要成员:

ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。

(1)ThreadPoolExecutor : Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool(他们的构造函数都可以接收ThreadFactory,如SingleThreadExecutor)

FixedThreadPool:FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器

SingleThreadExecutor:适用于需要保证顺序地执行各个任务

public static ExecutorService newSingleThreadExecutor()public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器

(2)ScheduledThreadPoolExecutor 可以创建2种类型的ScheduledThreadPoolExecutor

·ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。·SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场

(3) Future接口

Future
submit(Callable
task)
Future
submit(Runnable task, T result)Future<> submit(Runnable task)

到目前最新的JDK 8为止,Java通过上述API返回的是一个FutureTask对象。但从API可以看到,Java仅仅保证返回的是一个实现了Future接口的对象。在将

来的JDK实现中,返回的可能不一定是FutureTask。

(4)Runnable接口和Callable接口

Runnable不会返回结果;
Callable会返回结果;
除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个
Runnable包装成一个Callable

//把一个Runnable包装成一个Callable的APIpublic static Callable callable(Runnable task) // 假设返回对象Callable1//把一个Runnable和一个待返回的结果包装成一个Callable的APIpublic static 
Callable
callable(Runnable task, T result) // 假设返回对象Callable2

如果提交的是对象Callable1,FutureTask.get()方法

将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。


51.ThreadPoolExecutor

(看书)

(1)FixedThreadPool详解

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue
());}

FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

使用无界队列作为工作队列会对线程池带来如下影响

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或
shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)

(2).SingleThreadExecutor详解

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue
()));}

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与

FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)

(3).CachedThreadPool详解

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue
());}

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但它的的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程.

更多的东西再去看书吧…


52.FutureTask的实现

FutureTask的实现基于AbstractQueuedSynchronizer(以下简称为AQS)。java.util.concurrent中的很多可阻塞类(比如ReentrantLock)都是基于AQS来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。JDK 6中AQS被广泛使用,基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、

CountDownLatch和FutureTask

每一个基于AQS实现的同步器都会包含两种类型的操作,如下。

·至少一个acquire操作。这个操作阻塞调用线程,除非/直到AQS的状态允许这个线程继续执行。FutureTask的acquire操作为get()/get(long timeout,TimeUnit unit)方法调用。
·至少一个release操作。这个操作改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。

基于“复合优先于继承”的原则,FutureTask声明了一个内部私有的继承于AQS的子类

Sync,对FutureTask所有公有方法的调用都会委托给这个内部子类。

FutureTask的设计示意图:

这里写图片描述

TODO …

你可能感兴趣的文章
No.174 - LeetCode1305 - 合并两个搜索树
查看>>
No.175 - LeetCode1306
查看>>
No.182 - LeetCode1325 - C指针的魅力
查看>>
mysql:sql alter database修改数据库字符集
查看>>
mysql:sql truncate (清除表数据)
查看>>
yuv to rgb 转换失败呀。天呀。谁来帮帮我呀。
查看>>
驱动TFT要SDRAM做为显示缓存
查看>>
使用file查看可执行文件的平台性,x86 or arm ?
查看>>
qt 创建异形窗体
查看>>
简单Linux C线程池
查看>>
内存池
查看>>
linux 驱动开发 头文件
查看>>
ipconfig,ifconfig,iwconfig
查看>>
opensuse12.2 PL2303 minicom
查看>>
网络视频服务器移植
查看>>
Encoding Schemes
查看>>
移植QT
查看>>
如此调用
查看>>
计算机的发展史
查看>>
带WiringPi库的交叉编译如何处理一
查看>>