文章目录

  • Java并发编程的艺术(推荐指数:☆☆☆☆☆☆)
    • 并发编程的挑战
    • Java并发机制的底层实现原理
      • Volatile的应用
        • 实现原理
      • synchronized的实现原理与应用
        • 对象头
        • 锁详解
      • 原子操作的实现原理
        • 原子操作的实现
        • CAS的三大问题:
    • Java内存模型
      • Java内存模型的基础
        • 并发编程模型的两个关键问题:
        • Java内存模型的抽象结构
        • 从源代码到指令序列的重排序
        • 并发编程模型的分类
        • happens-before简介
      • 重排序
        • 数据依赖性
        • as-if-serial语义
        • 程序顺序规则
        • 重排序对多线程的影响
      • 顺序一致性
        • 数据竞争与顺序一致性
        • 顺序一致性内存模型
        • 同步的顺序一致性效果
        • 未同步程序的执行特性
      • vloatile的内存语义
        • volatile的特性
        • volatile写-读建立的happens-before关系
        • volatile写-读的内存语义
        • volatile内存语义的实现
        • JSR-133为什么要增强volatile的内存语义
      • 锁的内存语义
        • 锁的释放和获取的内存语义
      • final域的内存语义
        • final域的重排序规则
        • 写final域的重排序规则
        • 读final域的重排序规则
        • final域为引用类型
        • final引用不能从构造函数内“溢出”
        • final语义在处理器中的实现
        • JSR-133为什么要增强final的语义
      • happens-before
        • JMM的设计
        • happens-before的定义
        • happens-before规则
      • 延迟初始化的单例模式实现
        • 双重检查锁定的实现
        • 问题的根源
        • 基于volatile的解决方案
        • 基于类初始化的解决方案
      • Java内存模型综述
        • 处理器的内存模型
        • 各种内存模型之间的关系
        • JMM的内存可见性保证
        • JSR-133对旧内存模型的修补
    • Java并发编程的基础
      • 线程简介
        • 线程的状态
        • Deamon线程
      • 启动和终止线程
        • 理解中断
        • 过期的suspend()、resume()和stop()
        • 安全地终止线程
      • 线程间通信
        • volatile和synchronized关键字
        • 等待通知机制
        • 等待/通知的经典范式
        • 管道输入/输出流
        • Thread.join的使用
      • 线程应用实例
        • 数据库连接池示例
        • 线程池技术及其示例
        • 一个基于线程池技术的简单Web服务器
    • Java中的锁
      • Lock接口
      • 队列同步器
        • 队列同步器的接口
        • 实现分析
      • 重入锁
      • 读写锁
        • 锁降级
      • LockSupport工具
        • Condition接口
        • Condition的使用案例
        • Condition的实现简析
    • Java并发容器和框架
      • ConcurrentHashMap
      • ConcurrentLinkedQueue
      • Java中的阻塞队列
      • Fork/Join框架
    • Java中原子操作的类
      • 基本类型
      • 原子数组更新
      • 原子更新引用类型
      • 原子更新字段类
    • Java中的并发工具类
      • CountDownLatch
      • 同步屏障CyclicBarrier
        • CyclicBarrier和CountDownLatch的区别
      • 控制并发线程数的Semaphore
      • 线程间交换数据的Exchanger
    • Java中的线程池
        • 阻塞队列的分类:
        • RejectedExecutionHandler(饱和策略)的分类:
        • 向线程池提交任务的时候有两种方法:execute和submit
        • 关闭线程池的注意事项:
      • 线程池的监控
    • Executor框架
      • 简介
        • 两级调度模型
        • 架构
      • FutureTask
    • 并发编程实践
      • 生产者和消费者模式
      • 附录:一些可能用用得上的linux命令

Java并发编程的艺术(推荐指数:☆☆☆☆☆☆)

本书源码

并发编程的挑战

  1. 上下文切换会影响多线程的速度,导致在计算量较少的时候,单线程比多线程执行的更快

  2. 如何减少上下文的切换:

    无锁并发编程:比如ID按照hash算法取模

    CAS算法

    使用最少线程:避免创建不必要的线程

    协程:单线程里面完成任务调度和切换

  3. 多线程需要避免死锁,一种死锁的场景:有两个锁,线程A先获取锁A,然后睡眠,接着获取锁B,线程B则相反,从而造成死锁

  4. 避免死锁的的方法:

    避免一个线程同时收取多个锁

    避免一个线程在锁内占用多个资源,尽量保证每个锁只占用一个资源

    尝试使用定时锁

    对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

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

Volatile的应用

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

实现原理

volatile可见性的实现是通过LOCK前缀的指令,主要产生两个作用:

将当前处理器缓存行的数据写回到系统内存:

这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效:通过缓存一致性协议,识别总线上传播的数据来鉴别数据是否过期

使用优化:通过追加字节来优化性能

用一种追加字节的方式来优化队列出队和入队的性能,处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分添加,因此头尾可能在一个缓存行中,影响并发性能

synchronized的实现原理与应用

很多人都会称呼synchronized为重量级锁,1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了 减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。锁对象的判断:

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

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,代码块同步是使用monitorenter
和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,本质都是获取对线的monitor锁

对象头

synchronized用的锁是存在Java对象头里的Mark Wod里面,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头,多出来的一个字宽存储的是长度。

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,Mark Word里存储的数据会随着锁标志位的变化而变化

锁详解

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

偏向锁:

偏向锁的形成:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁的撤销:偏向锁的撤销 偏向锁使用了一种等到竞争出现才释放锁的机制 。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如
有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0

轻量级锁:

加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

解锁:会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。两个线程同时争夺锁,导致锁膨胀的流程图

原子操作的实现原理

原子操作的实现原理 32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址 最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的。处理器提供总线锁定缓存锁定两个机制来保证复杂内存操作的原子性。

但是有两种情况下处理器不会使用缓存锁定:

当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定

有些处理器不支持缓存锁定

原子操作的实现

在Java中可以通过锁和循环CAS的方式来实现原子操作

循环CAS的案例:

public class Counter {private AtomicInteger atomicI = new AtomicInteger(0);private int           i       = 0;public static void main(String[] args) {final Counter cas = new Counter();List<Thread> ts = new ArrayList<Thread>(600);long start = System.currentTimeMillis();for (int j = 0; j < 100; j++) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {cas.count();cas.safeCount();}}});ts.add(t);}for (Thread t : ts) {t.start();}for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(cas.i);System.out.println(cas.atomicI.get());System.out.println(System.currentTimeMillis() - start);}private void safeCount() {for (;;) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;}}}private void count() {i++;}}

CAS的三大问题:

  1. ABA问题:JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
  2. 循环时间长开销大
  3. 只能保证一个共享变量的原子操作

Java内存模型

Java内存模型的基础

并发编程模型的两个关键问题:

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行

Java内存模型的抽象结构

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,它涵盖缓存,写缓冲区,寄存器等。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

从源代码到指令序列的重排序

为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

编译器优化的重排序
指令级并行的重排序
内存系统的重排序

重排序可能会导致多线程程序出现内存可见性问题。JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序

并发编程模型的分类

由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序。重排序的分类:

Load-Load

Load-SStore

Store-Store

Store-Load

常见的处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖的操作做重排序

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,主要有4类内存屏障:

LoadLoad Barriers

StoreStore Barriers

LoadStore Barriers

StoreLoad Barriers

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果

happens-before简介

JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,happens-before关系具有传递性。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!(可见和执行不一样)happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序,注意这里指的是单个线程内。

as-if-serial语义

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

程序顺序规则

之前说过,happen-before不一定是前一个在后一个操作之前执行,原因:

如果A happens-before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。如果操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens-before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法,JMM允许这种重排序

重排序对多线程的影响

在单线程程序中,对存在控制依赖的操(依赖某个标志位进行控制运行分支)作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果

顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照

数据竞争与顺序一致性

如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(synchronized、volatile和final)的正确使用

顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

一个线程中的所有操作必须按照程序的顺序来执行。
不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。在JMM中就没有这个保证,未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见。

同步的顺序一致性效果

添加同步后,在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取到的值不会无中生有(Out Of Thin Air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间(Pre-zeroed Memory)分配对象时,域的默认初始化已经完成了

未同步程序在两个模型中的执行特性有如下几个差异:

1)顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。这一点前面已经讲过了,这里就不再赘述。
2)顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程
能看到一致的操作执行顺序。这一点前面也已经讲过,这里就不再赘述。
3)JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性

vloatile的内存语义

volatile的特性

理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步,简而言之,volatile变量自身具有下列特性:

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

volatile写-读建立的happens-before关系

从JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信

从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义

volatile写-读的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

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

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息

volatile内存语义的实现

为了实现volatile内存语义,JMM会限制编译器重排序和处理器重排序

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

JSR-133为什么要增强volatile的内存语义

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序.为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义

锁的内存语义

锁的释放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中

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

CAS操作同时具有volatile读和volatile写的内存语义(禁止重排序以及刷新缓存数据到内存)。公平锁和非公平锁的内存语义:

公平锁和非公平锁释放时,最后都要写一个volatile变量state。
公平锁获取时,首先会去读volatile变量。
非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义

final域的内存语义

final域的重排序规则

对于final域,编译器和处理器要遵守两个重排序规则:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

写final域的重排序规则

  1. JMM禁止编译器把final域的写重排序到构造函数之外。
  2. 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障

读final域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用

final域为引用类型

对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

final引用不能从构造函数内“溢出”

写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实,要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“逸出

final语义在处理器中的实现

写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

JSR-133为什么要增强final的语义

在旧的Java内存模型中,一个最严重的缺陷就是线程可能看到final域的值会改变。为了修补这个漏洞,JSR-133专家组增强了final的语义。通过为final域增加写和读重排序规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值

happens-before

JMM的设计

在设计JMM时,需要考虑两个关键因素:

程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型

专家组在设计JMM时的核心目标就是找到一个好的平衡点:一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松

JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证.只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行

happens-before的定义

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)

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

as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的

happens-before规则

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回

延迟初始化的单例模式实现

双重检查锁定的实现

首先是一个线程不安全的实现,它的问题:并发的时候执行,可能会多次初始化

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

可以优化为线程安全的实现:

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

但是直接使用synchronized会导致一些性能开销,因此,又可以优化为双重检查:

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

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

问题的根源

双重检查锁定问题的原因:可能存在分配初始化对象和赋值对象的内存地址这两个操作进行了重排序(下面的2和3):

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

所有线程在执行Java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果,上面的重排序正好不违反这个原则。

基于volatile的解决方案

当声明对象的引用为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;}static class Instance {}
}

基于类初始化的解决方案

Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了

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

Java内存模型综述

处理器的内存模型

顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会以顺序一致性内存模型为参照。由于各种处理器内存模型的强弱不同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同。JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为Java程序员呈现了一个一致的内存模型。

各种内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。处理器内存模型和语言内存模型都比顺序一致性内存模型要弱。

JMM的内存可见性保证

  1. 单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同
  2. 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证
  3. 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)

JSR-133对旧内存模型的修补

JSR-133对JDK 5之前的旧内存模型的修补主要有两个:

  1. 增强volatile的内存语义。旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。

  2. 增强final的内存语义。在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了两个重排序规则。在保证final引用不会从构造函数内逸出的情况下,final具有了初始化安全性

Java并发编程的基础

线程简介

现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性,需要注意的是线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。·

线程的状态

java有6中线程的状态:

NEW RUNNABLE BLOCKED WAITING TIME_WAITING TERMINATED

Java将操作系统中的运行和就绪两个状态合并称为运行状态,阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态

Deamon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

启动和终止线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程

理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位

如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false

许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。比如下面的案例输出结果为:false,true

public class Interrupted {public static void main(String[] args) throws Exception {// sleepThread不停的尝试睡眠Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");sleepThread.setDaemon(true);// busyThread不停的运行Thread busyThread = new Thread(new BusyRunner(), "BusyThread");busyThread.setDaemon(true);sleepThread.start();busyThread.start();// 休眠5秒,让sleepThread和busyThread充分运行TimeUnit.SECONDS.sleep(5);sleepThread.interrupt();busyThread.interrupt();System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());// 防止sleepThread和busyThread立刻退出TimeUnit.SECONDS.sleep(2);}static class SleepRunner implements Runnable {@Overridepublic void run() {while (true) {SleepUtils.second(10);}}}static class BusyRunner implements Runnable {@Overridepublic void run() {while (true) {}}}
}

过期的suspend()、resume()和stop()

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下

安全地终止线程

public class Shutdown {public static void main(String[] args) throws Exception {Runner one = new Runner();Thread countThread = new Thread(one, "CountThread");countThread.start();TimeUnit.SECONDS.sleep(1);countThread.interrupt();Runner two = new Runner();countThread = new Thread(two, "CountThread");countThread.start();TimeUnit.SECONDS.sleep(1);two.cancel();}private static class Runner implements Runnable {private long i;private volatile boolean on = true;@Overridepublic void run() {while (on && !Thread.currentThread().isInterrupted()) {i++;}System.out.println("Count i = " + i);}public void cancel() {on = false;}}
}

线程间通信

volatile和synchronized关键字

同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的

任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待通知机制

案例:

public class WaitNotify {static boolean flag = true;static Object  lock = new Object();public static void main(String[] args) throws Exception {Thread waitThread = new Thread(new Wait(), "WaitThread");waitThread.start();TimeUnit.SECONDS.sleep(1);Thread notifyThread = new Thread(new Notify(), "NotifyThread");notifyThread.start();}static class Wait implements Runnable {public void run() {// 加锁,拥有lock的Monitorsynchronized (lock) {// 当条件不满足时,继续wait,同时释放了lock的锁while (flag) {try {System.out.println(Thread.currentThread() + " flag is true. wait @ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.wait();} catch (InterruptedException e) {}}// 条件满足时,完成工作System.out.println(Thread.currentThread() + " flag is false. running @ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));}}}static class Notify implements Runnable {public void run() {// 加锁,拥有lock的Monitorsynchronized (lock) {// 获取lock的锁,然后进行通知,通知时不会释放lock的锁,// 直到当前线程释放了lock后,WaitThread才能从wait方法中返回System.out.println(Thread.currentThread() + " hold lock. notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.notifyAll();flag = false;SleepUtils.second(5);}// 再次加锁synchronized (lock) {System.out.println(Thread.currentThread() + " hold lock again. sleep @ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));SleepUtils.second(5);}}}
}

等待/通知的经典范式

WaitNotify示例中可以提炼出等待/通知的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者),等待方遵循如下原则:

1)获取对象的锁
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3)条件满足则执行对应的逻辑

通知方遵循如下原则。

1)获得对象的锁
2)改变条件
3)通知所有等待在对象上的线程

管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存:

任何main线程的输入均通过PipedWriter写入,而printThread在另一端通过PipedReader将内容读出并打印

public class Piped {public static void main(String[] args) throws Exception {PipedWriter out = new PipedWriter();PipedReader in = new PipedReader();// 将输出流和输入流进行连接,否则在使用时会抛出IOExceptionout.connect(in);Thread printThread = new Thread(new Print(in), "PrintThread");printThread.start();int receive = 0;try {while ((receive = System.in.read()) != -1) {out.write(receive);}} finally {out.close();}}static class Print implements Runnable {private PipedReader in;public Print(PipedReader in) {this.in = in;}public void run() {int receive = 0;try {while ((receive = in.read()) != -1) {System.out.print((char) receive);}} catch (IOException ex) {}}}
}

Thread.join的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回,案例:

public class Join {public static void main(String[] args) throws Exception {Thread previous = Thread.currentThread();for (int i = 0; i < 10; i++) {// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回Thread thread = new Thread(new Domino(previous), String.valueOf(i));thread.start();previous = thread;}TimeUnit.SECONDS.sleep(5);System.out.println(Thread.currentThread().getName() + " terminate.");}static class Domino implements Runnable {private Thread thread;public Domino(Thread thread) {this.thread = thread;}public void run() {try {thread.join();} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + " terminate.");}}
}

线程应用实例

数据库连接池示例

public class ConnectionPool {private LinkedList<Connection> pool = new LinkedList<Connection>();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内无法获取到连接,将会返回nullpublic 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;}}}
}

测试用例:这里使用了CountDownLatch

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 = 50;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和未获取到的数量notGotConnection 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();}}
}

线程池技术及其示例

如果服务端每次接受到一个任务,创建一个线程,然后进行执行,这在原型阶段是个不错的选择,但是面对成千上万的任务递交进服务器时,如果还是采用一个任务一个线程的方式,那么将会创建数以万记的线程,这不是一个好的选择。线程池技术能够很好地解决这个问题,它预先创建了若干数量的线程,并且不能由用户直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务的执行。

public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job> {// 线程池最大限制数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<Job> jobs                   = new LinkedList<Job>();// 工作者列表private final List<Worker>    workers                = Collections.synchronizedList(new ArrayList<Worker>());// 工作者线程的数量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");}// 按照给定的数量停止Workerint count = 0;while (count < num) {workers.get(count).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) {// 如果工作者列表是空的,那么就waitwhile (jobs.isEmpty()) {try {jobs.wait();} catch (InterruptedException ex) {// 感知到外部对WorkerThread的中断操作,返回Thread.currentThread().interrupt();return;}}// 取出一个Jobjob = jobs.removeFirst();}if (job != null) {try {job.run();} catch (Exception ex) {// 忽略Job执行中的Exception}}}}public void shutdown() {running = false;}}
}

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

public class SimpleHttpServer {// 处理HttpRequest的线程池static ThreadPool<HttpRequestHandler> threadPool = new DefaultThreadPool<HttpRequestHandler>(11);// 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;}}// 启动SimpleHttpServerpublic 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;}@Overridepublic 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("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("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);}}}// 关闭流或者Socketprivate static void close(Closeable... closeables) {if (closeables != null) {for (Closeable closeable : closeables) {try {closeable.close();} catch (Exception ex) {// 忽略}}}}
}

Java中的锁

Lock接口

在Lock接
口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性

注意:

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

Lock接口的实现基本都是通过聚合了一个同步器(AQS)的子类来完成线程访问控制的

队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作

队列同步器的接口

同步器可以重写的方法:

tryAcquire(int arg)

tryRelease(int arg)

tryAcquireShared(int arg)

tryReleaseShare(int arg)

isHeldExclusively()

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。

下面是独占锁的一个实现:

public class Mutex implements Lock {// 静态内部类,自定义同步器private static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -4387327721959839431L;// 是否处于占用状态protected boolean isHeldExclusively() {return getState() == 1;}// 当状态为0的时候获取锁public boolean tryAcquire(int acquires) {assert acquires == 1; // Otherwise unusedif (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}// 释放锁,将状态设置为0protected boolean tryRelease(int releases) {assert releases == 1; // Otherwise unusedif (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));}
}

实现分析

  1. 同步队列:

    同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态

  2. 队列中节点获取锁的方式:当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,原因:头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点;维护同步队列的FIFO原则

自定义同步组件:TwinsLock,只允许至多两个线程同时访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock

public class TwinsLock implements Lock {private final Sync sync = new Sync(2);private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -7889272986162341211L;Sync(int count) {if (count <= 0) {throw new IllegalArgumentException("count must large than zero.");}setState(count);}public int tryAcquireShared(int reduceCount) {for (;;) {int current = getState();int newCount = current - reduceCount;if (newCount < 0 || compareAndSetState(current, newCount)) {return newCount;}}}public boolean tryReleaseShared(int returnCount) {for (;;) {int current = getState();int newCount = current + returnCount;if (compareAndSetState(current, newCount)) {return true;}}}final ConditionObject newCondition() {return new ConditionObject();}}public void lock() {sync.acquireShared(1);}public void unlock() {sync.releaseShared(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireSharedInterruptibly(1);}public boolean tryLock() {return sync.tryAcquireShared(1) >= 0;}public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(time));}@Overridepublic Condition newCondition() {return sync.newCondition();}
}

重入锁

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

重入的实现:

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题:
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放

公平锁和非公平锁的区别

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。非公平的锁可能会导致饥饿,但是会减少上下文切换的次数,因此默认都是采用非公平锁。

读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。读写锁的一个示例:

Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的

public class Cache {private static final Map<String, Object>    map = new HashMap<String, Object>();private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();private static final Lock                   r   = rwl.readLock();private static final Lock                   w   = rwl.writeLock();public static final Object get(String key) {r.lock();try {return map.get(key);} finally {r.unlock();}}public static final Object put(String key, Object value) {w.lock();try {return map.put(key, value);} finally {w.unlock();}}public static final void clear() {w.lock();try {map.clear();} finally {w.unlock();}}
}

锁降级

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

public class ProcessData {private static final ReentrantReadWriteLock rwl       = new ReentrantReadWriteLock();private static final Lock                   readLock  = rwl.readLock();private static final Lock                   writeLock = rwl.writeLock();private volatile boolean                    update    = false;public void processData() {readLock.lock();if (!update) {// 必须先释放读锁readLock.unlock();// 锁降级从写锁获取到开始writeLock.lock();try {if (!update) {// 准备数据的流程(略)update = true;}readLock.lock();} finally {writeLock.unlock();}// 锁降级完成,写锁降级为读锁}try {// 使用数据的流程(略)} finally {readLock.unlock();}}}

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新

LockSupport工具

LockSupport可以完成阻塞和唤醒一个线程,park开头的方法用来阻塞当前线程,unpark方法唤醒一个被阻塞的线程。在Java 6中,LockSupport新增了3个含有阻塞对象的park方法,方便查看线程dump信息中在哪个对象上阻塞。

Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。主要的差别在于Condition支持多个等待队列,支持在等待状态时不响应中断,以及支持等待到将来的某个时间

Condition的使用案例

Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的

public class ConditionUseCase {Lock      lock      = new ReentrantLock();Condition condition = lock.newCondition();public void conditionWait() throws InterruptedException {lock.lock();try {condition.await();} finally {lock.unlock();}}public void conditionSignal() throws InterruptedException {lock.lock();try {condition.signal();} finally {lock.unlock();}}
}

有界队列的使用案例:有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”

public class BoundedQueue<T> {private Object[]  items;// 添加的下标,删除的下标和数组当前数量private int       addIndex, removeIndex, count;private Lock      lock     = new ReentrantLock();private Condition notEmpty = lock.newCondition();private Condition notFull  = lock.newCondition();public BoundedQueue(int size) {items = new Object[size];}// 添加一个元素,如果数组满,则添加线程进入等待状态,直到有“空位”public void add(T t) throws InterruptedException {lock.lock();try {while (count == items.length)notFull.await();items[addIndex] = t;if (++addIndex == items.length)addIndex = 0;++count;notEmpty.signal();} finally {lock.unlock();}}// 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素@SuppressWarnings("unchecked")public T remove() throws InterruptedException {lock.lock();try {while (count == 0)notEmpty.await();Object x = items[removeIndex];if (++removeIndex == items.length)removeIndex = 0;--count;notFull.signal();return (T) x;} finally {lock.unlock();}}
}

Condition的实现简析

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

Condition节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node

Java并发容器和框架

ConcurrentHashMap

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会

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

ConcurrentLinkedQueue

实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。

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

Java中的阻塞队列

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

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

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程

JDK 7提供了7个阻塞队列,如下。

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Fork/Join框架

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

工作窃取算法:

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

使用案例:

public class CountTask extends RecursiveTask<Integer> {private static final int THRESHOLD = 2; // 阈值private int              start;private int              end;public CountTask(int start, int end) {this.start = start;this.end = end;}@Overrideprotected Integer compute() {int sum = 0;// 如果任务足够小就计算任务boolean canCompute = (end - start) <= THRESHOLD;if (canCompute) {for (int i = start; i <= end; i++) {sum += i;}} else {// 如果任务大于阈值,就分裂成两个子任务计算int middle = (start + end) / 2;CountTask leftTask = new CountTask(start, middle);CountTask rightTask = new CountTask(middle + 1, end);//执行子任务leftTask.fork();rightTask.fork();//等待子任务执行完,并得到其结果int leftResult = leftTask.join();int rightResult = rightTask.join();//合并子任务sum = leftResult + rightResult;}return sum;}public static void main(String[] args) {ForkJoinPool forkJoinPool = new ForkJoinPool();// 生成一个计算任务,负责计算1+2+3+4CountTask task = new CountTask(1, 4);// 执行一个任务Future<Integer> result = forkJoinPool.submit(task);try {System.out.println(result.get());} catch (InterruptedException e) {} catch (ExecutionException e) {}}}

获取异常

if(task.isCompletedAbnormally())
{System.out.println(task.getException());
}

Java中原子操作的类

基本类型

主要有:

AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型

原子数组更新

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

AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素。
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下

使用案例:

public class AtomicIntegerArrayTest {static int[]              value = new int[] { 1, 2 };static AtomicIntegerArray ai    = new AtomicIntegerArray(value);public static void main(String[] args) {ai.getAndSet(0, 3);System.out.println(ai.get(0));System.out.println(value[0]);}
}

原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

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

使用举例:

public class AtomicReferenceTest {public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();public static void main(String[] args) {User user = new User("conan", 15);atomicUserRef.set(user);User updateUser = new User("Shinichi", 17);atomicUserRef.compareAndSet(user, updateUser);System.out.println(atomicUserRef.get().getName());System.out.println(atomicUserRef.get().getOld());}public 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;}}
}

原子更新字段类

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

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

使用案例:

public class AtomicIntegerFieldUpdaterTest {private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");public static void main(String[] args) {User conan = new User("conan", 10);System.out.println(a.getAndIncrement(conan));System.out.println(a.get(conan));}public static class User {private String      name;public volatile int old;public User(String name, int old) {this.name = name;this.old = old;}public String getName() {return name;}public int getOld() {return old;}}
}

Java中的并发工具类

CountDownLatch

使用场景:等所有的子任务都完成后,在进行下一步操作,案例:多线程解析Excel,等所有任务都完成在输出结果,我们可以使用join来实现:

public class JoinCountDownLatchTest {public static void main(String[] args) throws InterruptedException {Thread parser1 = new Thread(new Runnable() {@Overridepublic void run() {}});Thread parser2 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("parser2 finish");}});parser1.start();parser2.start();parser1.join();parser2.join();System.out.println("all parser finish");}}

也可以通过CountDownLatch实现:

一个线程调用countDown方法happen-before,另外一个线程调用await方法

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

同步屏障CyclicBarrier

让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。打个比方,就像赛跑时的起跑线一样,等所有线程到了起跑线的位置,才能一起运行。下面的案例中,如果把new CyclicBarrier(2)修改成new CyclicBarrier(3),则主线程和子线程会永远等待,
因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个线程都不会继续执行

public class CyclicBarrierTest {static CyclicBarrier c = new CyclicBarrier(2);public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {try {c.await();} catch (Exception e) {}System.out.println(1);}}).start();try {c.await();} catch (Exception e) {}System.out.println(2);}
}

另一个案例:例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水

public class BankWaterService implements Runnable {/*** 创建4个屏障,处理完之后执行当前类的run方法*/private CyclicBarrier c = new CyclicBarrier(4, this);/*** 假设只有4个sheet,所以只启动4个线程*/private Executor executor = Executors.newFixedThreadPool(4);/*** 保存每个sheet计算出的银流结果*/private ConcurrentHashMap<String, Integer> sheetBankWaterCount = newConcurrentHashMap<>();private void count() {for (int i = 0; i < 4; i++) {executor.execute(() -> {// 计算当前sheet的银流数据,计算代码省略sheetBankWaterCount.put(Thread.currentThread().getName(), 1);// 银流计算完成,插入一个屏障try {c.await();} catch (InterruptedException |BrokenBarrierException e) {e.printStackTrace();}});}}@Overridepublic void run() {int result = 0;// 汇总每个sheet计算出的结果for (Map.Entry<String, Integer> sheet : sheetBankWaterCount.entrySet()) {result += sheet.getValue();}//将结果输出sheetBankWaterCount.put("result", result);System.out.println(result);}public static void main(String[] args) {BankWaterService bankWaterCount = new BankWaterService();bankWaterCount.count();}
}

CyclicBarrier和CountDownLatch的区别

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断

控制并发线程数的Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接

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() {@Overridepublic void run() {try {s.acquire();System.out.println("save data");s.release();} catch (InterruptedException e) {}}});}threadPool.shutdown();}
}

线程间交换数据的Exchanger

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

案例:Exchanger可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致

public class ExchangerTest {private static final Exchanger<String> exgr       = new Exchanger<String>();private static ExecutorService         threadPool = Executors.newFixedThreadPool(2);public static void main(String[] args) {threadPool.execute(new Runnable() {@Overridepublic void run() {try {String A = "银行流水A";// A录入银行流水数据exgr.exchange(A);} catch (InterruptedException e) {}}});threadPool.execute(new Runnable() {@Overridepublic void run() {try {String B = "银行流水B";// B录入银行流水数据String A = exgr.exchange("B");System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:" + A + ",B录入是:" + B);} catch (InterruptedException e) {}}});threadPool.shutdown();}
}

Java中的线程池

线程池的好处:

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性

主要分为四种情况:

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法

maximumPoolSize的注意事项:

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

阻塞队列的分类:

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

RejectedExecutionHandler(饱和策略)的分类:

AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。

向线程池提交任务的时候有两种方法:execute和submit

它们的区别:

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

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成

关闭线程池的注意事项:

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法

线程池的监控

几个重要的属性和方法:

taskCount:线程池需要执行的任务数量

completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount

largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过

getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减

getActiveCount:获取活动的线程数

Executor框架

简介

两级调度模型

在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU

在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上

架构

Executor框架主要由3大部分组成如下:

任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口

任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口,Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)

异步计算的结果。包括接口Future和实现Future接口的FutureTask类

ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。其中CachedThreadPool需要特殊说明一下:

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

CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源

ScheduledThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建2种类型的ScheduledThreadPoolExecutor,如下:

ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。
SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。

案例:

public class ConcurrentTask {private final ConcurrentMap<Object, Future<String>> taskCache = new ConcurrentHashMap<Object, Future<String>>();private String executionTask(final String taskName) throws ExecutionException, InterruptedException {while (true) {Future<String> future = taskCache.get(taskName); //1.1,2.1if (future == null) {Callable<String> task = () -> {//......return taskName;};//1.2创建任务FutureTask<String> futureTask = new FutureTask<>(task);future = taskCache.putIfAbsent(taskName, futureTask); //1.3if (future == null) {future = futureTask;futureTask.run(); //1.4执行任务}}try {return future.get(); //1.5,2.2线程在此等待任务执行完成} catch (CancellationException e) {taskCache.remove(taskName, future);}}}}

FutureTask

FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态。

1)未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。
2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。
3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态

案例:当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行

public class ConcurrentTask {private final ConcurrentMap<Object, Future<String>> taskCache = new ConcurrentHashMap<Object, Future<String>>();private String executionTask(final String taskName) throws ExecutionException, InterruptedException {while (true) {Future<String> future = taskCache.get(taskName); //1.1,2.1if (future == null) {Callable<String> task = () -> {//......return taskName;};//1.2创建任务FutureTask<String> futureTask = new FutureTask<>(task);future = taskCache.putIfAbsent(taskName, futureTask); //1.3if (future == null) {future = futureTask;futureTask.run(); //1.4执行任务}}try {return future.get(); //1.5,2.2线程在此等待任务执行完成} catch (CancellationException e) {taskCache.remove(taskName, future);}}}}

并发编程实践

生产者和消费者模式

生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力

在多核时代,多线程并发处理速度比单线程处理速度更快,所以可以使用多个线程来生产数据,同样可以使用多个消费线程来消费数据。而更复杂的情况是,消费者消费的数据,有可能需要继续处理,于是消费者处理完数据之后,它又要作为生产者把数据放在新的队列里,交给其他消费者继续处理

Java中的线程池类其实就是一种生产者和消费者模式的实现方式。生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然慢一些

附录:一些可能用用得上的linux命令

  1. top命令查看进程情况
  2. 查看GC:sudo /opt/java/bin/jstat -gcutil 31177 1000 5
  3. dump线程:sudo -u admin /opt/taobao/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
  4. 查看有多少台机器连接到某个端口上:netstat -nat | grep 12200 –c
  5. ps命令查看下线程数:ps -eLf | grep java -c
  6. 查看网络流量:cat /proc/net/dev
  7. 查看系统平均负载:cat /proc/loadavg
  8. 查看内存使用情况:cat /proc/meminfo
  9. 查看CPU利用率:cat /proc/stat

Java并发编程的艺术(推荐指数:☆☆☆☆☆☆)相关推荐

  1. 【推荐】《Java 并发编程的艺术》迷你书

    本文源自InfoQ发表的<Java 并发编程的艺术>电子书  作者:方腾飞  序言:张龙 免费下载此迷你书 推荐序 欣闻腾飞兄弟的<聊聊并发>系列文章将要集结成InfoQ迷你书 ...

  2. 《Java 并发编程的艺术》迷你书

    本文源自InfoQ发表的<Java 并发编程的艺术>电子书  作者:方腾飞  序言:张龙 免费下载此迷你书 推荐序 欣闻腾飞兄弟的<聊聊并发>系列文章将要集结成InfoQ迷你书 ...

  3. Java并发编程的艺术_Conc

    Java并发编程的艺术 1 并发编程的挑战 1.1 上下文切换 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制.时间片是CPU分配给各个线程的时间,因为时间片 ...

  4. Java并发编程的艺术 记录(一)

    模拟死锁 package com.gjjun.concurrent;/*** 模拟死锁,来源于<Java并发编程的艺术>* @Author gjjun* @Create 2018/8/12 ...

  5. Java并发编程的艺术,解读并发编程的优缺点

    并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在 ...

  6. 《Java并发编程的艺术》笔记

    <Java并发编程的艺术>笔记 第1章 并发编程的挑战 1.1 上下文切换 CPU通过时间片分配算法来循环执行任务,任务从保存到再加载的过程就是一次上下文切换. 减少上下文切换的方法有4种 ...

  7. 《Java并发编程的艺术》——线程(笔记)

    文章目录 四.Java并发编程基础 4.1 线程简介 4.1.1 什么是线程 4.1.2 为什么要使用多线程 4.1.3 线程优先级 4.1.4 线程的状态 4.1.5 Daemon线程 4.2 启动 ...

  8. Java并发编程的艺术(一)

    看<java并发编程的艺术>这本书,想着看的时候做个简单的总结,方便以后直接看重点. 一.并发编程的挑战 1.上下文切换 Cpu时间片通过给每个线程分配CPU时间片来实现多线程机制,时间片 ...

  9. # Java 并发编程的艺术(二)

    Java 并发编程的艺术(二) 文章目录 Java 并发编程的艺术(二) 并发编程的挑战 上下文切换 如何减少上下文的切换 死锁 资源限制的挑战 Java 并发机制的底层实现原理 volatile 的 ...

最新文章

  1. LeetCode简单题之数组元素积的符号
  2. spring定时每天早上八点_Spring Boot教程(13) – 简单定时任务
  3. [转贴] C++内存管理检测工具 Valgrind
  4. 快速排序 python菜鸟教程-快速排序
  5. spring--(4)级联属性赋值
  6. WINCE6.0深入理解TOC
  7. 用JAVASCRIPT实现静态对象、静态方法和静态属性
  8. easy ui example
  9. Python2/3 list set性能测试
  10. python输入水果数量_Python 水果统计
  11. 邮箱有什么用_大公司为什么要用企业邮箱?大公司企业邮箱用什么比较好?
  12. 搜索 | 电商行业模版驱动业务增长实践
  13. 杭电OJ-2104_hide handkerchief超简洁代码
  14. 将阿拉伯数字转换成中文大写的好算法
  15. Android应用程序与SurfaceFlinger服务的连接过程分析
  16. Java多线程同步屏障计算_Java多线程之CountDownLatch和CyclicBarrier同步屏障的使用
  17. G.711U在RTP中的展现
  18. [SharePoint 2010] Visual Studio 2010內撰寫視覺化WebPart超簡單
  19. c++语言boolean例子,C++语言——99个常见编程编程 学习小结
  20. 品牌方如何筛选高性价比商业UP主?拆解影响B站up主广告报价的因素

热门文章

  1. 【企业数字化转型】中台战略
  2. 川土微电子|推出带隔离电源的双通道数字隔离器
  3. pandas使用query函数查询dataframe中某一个数据列在指定数据范围的数据行(rows where value is between two values in dataframe)
  4. 【运行报错】Centos 6 无法使用 yum
  5. 锐龙9 7845HX 和锐龙9 6900HX选哪个 r9 7845HX 和6900HX差距
  6. AR与VR开发实战pdf
  7. python语言画成圆相切_在python中如何使用循环结构画四个相切的圆
  8. 汕尾市海陆丰民间开胃小食摘录
  9. 笔记一:画笔、笔刷认识
  10. 最新国内云计算服务商市场占有率排名,阿里云腾讯云华为云谁排名高