一、并发问题
- 上下文切换:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
- 多线程不一定快:多线程有线程创建和上下文切换的开销。
- 减少上下文切换:
- 无锁并发:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
- 使用协程:在单线程里实现多任务调度,并在单线程里维持多个任务间的切换
- 死锁:线程A持有资源,线程B持有资源;他们都想申请对方的资源,这两个线程就会相互等待而进入死锁状态。(互相等待对方释放锁)
- 代码:
public class Test {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
new Thread(() ->{
synchronized (o1) {
try {
System.out.println("get o1");
Thread.sleep(1000);
} catch (Exception ignored){}
synchronized (o2) {
System.out.println("get o2");
}
}
}).start();
new Thread(() ->{
synchronized (o2) {
try {
System.out.println("get o2");
Thread.sleep(1000);
} catch (Exception ignored){}
synchronized (o1) {
System.out.println("get o1");
}
}
}).start();
}
}
避免死锁的办法
-
避免一个 线 程同 时获 取多个锁
-
避免一个 线 程在 锁 内同 时 占用多个 资 源,尽量保 证 每个 锁 只占用一个 资 源
-
尝试 使用定 时锁 ,使用 lock.tryLock ( timeout )来替代使用内部 锁 机制
-
对 于数据 库锁 ,加 锁 和解 锁 必 须 在一个数据 库连 接里,否 则 会出 现 解 锁 失 败
- 资源限制
- 程序的执行速度受限于计算机硬件或者是软件资源。
- 问题:导致并发执行变为串行执行,开启并发线程速度可能会很慢。因为上下文切换占用了大量的时间。
二、java并发底层实现
Java 代 码 在 编译 后会 变 成 Java 字 节码 ,字 节码 被 类 加 载 器加 载 到 JVM 里, JVM 执 行字节码 ,最 终 需要 转 化 为汇编 指令在 CPU 上 执 行, Java 中所使用的并 发 机制依 赖 于 JVM 的 实现和CPU 的指令。
2.1、volatile
- volatile是轻量级的synchronized,保证多处理器开发的共享变量的可见性。
- 可见性的意思是一个线程修改共享变量的时候,其它线程可以读到这个值。
- volatile的成本比synchronized成本更低,不会引起线程的上下文切换和调度。
1、volatile的定义与实现原理
- 如果一个字段被声明为volatile,那么java线程的内存模型确保所有线程看到这个变量的值是一致的。
- volatile相关的CPU术语介绍,在下面。
2、volatile如何保证可见性
2.2、Synchronized
2.2.1对象头
-
synchronized 用的 锁 存在 Java 对 象 头 里。如果 对 象是数 组类 型, 则 虚 拟机用12个字节 存 储对 象 头 ,如果 对 象是非数 组类 型, 则 用 8个字节 存 储对象头
- Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位
- 在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
2.2.2锁升级
锁状态:无 锁 状 态 、偏向 锁 状 态 、 轻 量 级锁状态 和重量 级锁 状 态 , 这 几个状 态 会随着 竞 争情况逐 渐 升 级 。 锁 可以升 级 但不能降 级, 目的是 为了提高获 得 锁 和 释 放 锁 的效率
- 偏向锁
- 轻量级锁
-
轻量级加锁
-
线程执行同步块之前,JVM会在线程里面创建锁记录的空间,并且把对象头的Mark Word复制到锁记录中。
-
然后通过CAS来把Mark Word替换为指向锁记录的指针。
-
如果成功那么就获取到轻量级的锁。
-
如果失败说明有其他线程竞争,那么线程就尝试自旋获取锁。
-
轻量级解锁
- 锁的优缺点
2.3、java中实现原子操作
三、java内存模型
3.1、JMM基础
3.2、重排序
3.2.1、数据依赖性
- 如果两个操作对一个共享变量操作,而且有一个是写操作,那么两个操作就是数据依赖的。
- 编译器和处理器都是遵循数据依赖性的(单个处理器中)
3.2.2、as-if-serial
- as-if-serial语义就是不管怎么重排序,执行结果都是不会变的。
- 所以编译器和处理器不会对数据依赖的指令进行重排序
3.2.3、程序顺序规则
- 只要指令之间有可见性的关系,那么就不能够重排序。如果前一个操作不需要对后面的操作可见,那么就可以重排序
3.2.4、重排序对多线程的影响
3.3、顺序一致性
3.4、volatile的内存语义
插入内存屏障保证禁止指令重排
3.5、锁的内存语义
3.5.1、 锁的释放-获取建立的happens-before关系
3.5.2、锁的释放和获取的内存语义
-
线程释放锁的时候,JMM会把该线程对应的本地内存共享变量刷新到主内存
-
线程获取锁的时候,JMM会把线程的本地内存设置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
-
可以看到锁释放的内存语义和volatile写的是一样的。获取锁语义和volatile读是一样的
-
线程A释放锁,实质是线程A对某个要获取锁的线程发出要对共享变量修改的消息
-
线程B获取锁,实质是线程B接收了某个线程发出的修改消息
-
线程A释放锁和线程B获取锁,实质上是线程A通过主内存向线程B发送消息
3.6、final域的内存语义
3.7、happens-before
3.7.1、happens-before的定义
-
如果线程A的写操作a和线程B的读操作b存在happens-before,那么JMM可以保证a一定是对于b是可见的。
-
JSR-133的定义
-
JMM的承诺
-
as-if-serial语义保证单线程内执行结果不被改变,happens-before关系保证同步的多线程执行结果不会改变。
-
as-if-serial语义给程序员创建了幻境,单线程程序按照程序顺序执行,happens-before的幻境就是正确的同步多线程是按照happens-before指定的顺序执行的。
3.7.2、happens-before规则
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 传递性
- start()规则
- join()规则
3.8、DCL和延迟初始化
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getUniqueInstance(){
//判断对象是否实例过
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
-
为uniqueInstance分配内存空间
-
初始化uniqueInstance
-
将uniqueInstance指向分配好的内存地址;
JVM具有指令重排,指向顺序可能变成1->3->2。如,线程T1执行了1和3,T2调用方法发现uniqueInstance不为null直接返回,然而uniqueInstance还没被初始化。volatile可以禁止指令重排
3.9、JMM综述
四、多线程
4.1、线程简介
4.1.1、什么是线程
现代操作系 统 在运行一个程序 时 ,会 为 其 创 建一个 进程。现 代操作系 统调 度的最小 单 元是 线 程,也叫 轻 量 级进 程(Light Weight Process ),在一个 进 程里可以 创 建多个 线 程, 这 些 线 程都 拥 有各自的 计 数器、堆 栈和局部 变 量等属性,并且能 够访问 共享的内存 变 量。 处 理器在 这 些 线 程上高速切 换 , 让 使用者感觉到 这 些 线 程在同 时执 行(并发)。
4.1.2、为什么使用多线程
- 更多的处理器核心,以及超线程技术的广泛运用
- 更快的响应时间
4.1.3、线程优先级
- 现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
- Java中,可以使用
Thread
类的setPriority()
方法来设置线程的优先级。线程的优先级介于1到10,其中10是最高优先级,1是最低优先级。默认情况下,线程的优先级为5。
- 注:线程优先级是提示给操作系统的,操作系统可能会根据自己的调度策略来决定线程的实际执行顺序。因此,即使设置了线程的优先级,也不能保证线程一定会按照优先级顺序执行。
4.1.4、线程状态
- NEW:初始转态,线程被构建,但还没有调用start()方法
- RUNNABLE:运行状态
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,进入该状态表示当前线程需要等待其他线程通知或中断
- TIME_WAITING:超时等待,在指定的时间自行返回
- TERMINATED:终止状态,表示当前线程已经执行完毕
- 线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。
4.1.5、Daemon线程
Daemon线程是一种支持型线程(守护线程),主要被用作程序中后台调度以及支持性工作。这 意味着,当一个Java虚拟机中所有用户线程结束时,Java虚拟机将会退出,所有Daemon线程都需要立即终止。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。
4.2、启动和终止线程
4.2.1、构造线程
-
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
//当前线程就是该线程的父线程
Thread parent = currentThread();
this.group = g;
将daemon、priority属性设置为父线程的对应属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 将父线程的InheritableThreadLocal复制过来
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Set thread ID */
this.tid = nextThreadID();
}
在上述 过 程中,一个新构造的 线 程 对 象是由其 parent 线 程来 进 行空 间 分配的,而 child 线程继 承了 parent 是否 为 Daemon 、 优 先 级 和加 载资 源的 contextClassLoader 以及可 继承的 ThreadLocal ,同 时还 会分配一个唯一的 ID 来 标识这 个 child 线 程。至此,一个能 够 运行的 线 程对 象就初始化好了,在堆内存中等待着运行
4.2.2、启动线程
线 程 在初始化完成之后, 调 用 start() 方法就可以启 动该 线 程。s tart() 方法的含义是:当前 线 程(即 parent 线 程)同步告知 Java 虚 拟 机,只要 线 程 规 划器空 闲 , 应 立即启 动调用start() 方法的 线 程。
4.2.3、中断
4.2.4、过期的suspend()、resume()、和stop()
4.3、线程间通信
4.3.1、volatile和synchronized关键字
4.3.2、等待/通知机制
一个 线 程修改了一个 对 象的 值 ,而另一个 线 程感知到了 变 化,然后 进 行相 应的操作,整个过 程开始于一个 线 程,而最 终执 行又是另一个 线 程。前者是生 产 者,后者就是消 费 者, 这种模式隔离了 “ 做什么 ” ( what )和 “ 怎么做 ” ( How ),在功能 层 面上 实现 了解耦,体系 结 构上具 备了良好的伸 缩 性。
-
使用 wait() 、 notify() 和 notifyAll() 时 需要先 对调 用 对 象加 锁。
-
调 用 wait() 方法后, 线 程状 态 由 RUNNING 变 WAITING ,并将当前 线 程放置到 对象的等待 队列。
-
notify() 或 notifyAll() 方法 调 用后,等待 线 程依旧不会从 wait() 返回,需要 调 用 notify()或 notifAll() 的 线 程 释 放 锁 之后,等待 线 程才有机会从 wait() 返回。
-
notify() 方法将等待 队 列中的一个等待 线 程从等待 队 列中移到同步 队 列中,而notifyAll()方法 则 是将等待 队 列中所有的 线 程全部移到同步 队 列,被移 动 的 线 程状 态 由 WAITING变为 BLOCKED。
-
从 wait() 方法返回的前提是 获 得了 调 用 对 象的 锁。
-
从上述 细节 中可以看到,等待 / 通知机制依托于同步机制,其目的就是确保等待 线程从wait() 方法返回 时 能 够 感知到通知 线 程 对变 量做出的修改。
过程:WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行
4.3.3、等待/通知的今典范式
4.3.4、Thread.join()
- 如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回
-
JDK 中 Thread.join() 方法的源 码
public final synchronized void join(final long millis) throws InterruptedException {
while (isAlive()) {
wait(0);
}
}
当 线 程 终 止 时 ,会 调 用 线 程自身的 notifyAll() 方法,会通知所有等待在 该线 程 对 象上的 线
程。可以看到 join() 方法的 逻辑结 构与 等待 / 通知 经 典范式一致
4.3.5、ThreadLocal
- ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
五、java中的锁
5.1、Lock接口
-
锁 是用来控制多个 线 程 访问 共享 资 源的方式,一般来 说 ,一个 锁 能 够 防止多个 线 程同时访问 共享 资 源(但是有些 锁 可以允 许 多个 线 程并 发 的 访问 共享 资 源,比如 读 写 锁 )。在 Lock接口出 现 之前, Java 程序是靠 synchronized 关 键 字 实现锁 功能的,而 Java SE 5 之后,并 发包中新增了L ock 接口(以及相关 实现类 )用来 实现锁 功能,它提供了与 synchronized 关 键 字 类 似的同步功
能,只是在使用 时 需要 显 式地 获 取和 释 放 锁 。 虽 然它缺少了(通 过 synchronized 块或者方法所提 供的) 隐 式 获 取 释 放 锁 的便捷性,但是却 拥 有了 锁获 取与 释 放的可操作性、可中断的 获 取 锁以及超 时获 取 锁 等多种 synchronized 关 键 字所不具 备 的同步特性
-
使用
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
//在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放
lock.unlock();
}
- Lock接口提供的synchronized关键字所不具备的特性
-
Lock是一个接口,它定义了锁获取和释放的基本操作
5.2、队列同步器-ASQ
-
队列同步器AbstractQueuedSynchronizer用来构建锁,或者其它同步组件。用一个int成员变量表示同步状态。通过内置的FIFO队列完成资源获取线程的排队工作。
-
同步器的实现主要是继承,同步器需要提供(getState()、setState(int newState)和compareAndSetState(int expect,int update))方法来获取同步的状态。
-
同步器支持独占或者是共享地获取锁。
-
同步器是 实现锁 (同步 组 件)的关 键 ,在 锁 的 实现中聚合同步器,利用同步器 实现锁 的 语义 。可以 这样 理解二者之 间 的关系: 锁 是面向使用者的,它定 义 了使用者与 锁交互的接口(比如可以允 许 两个 线 程并行 访问 ), 隐 藏了 实现细节 ;同步器面向的是 锁 的 实现者, 它 简 化了 锁 的 实现 方式,屏蔽了同步状 态 管理、 线 程的排 队 、等待与 唤 醒等底 层 操作。 锁和同步器很好地隔离了使用者和 实现 者所需关注的 领 域。
5.2.1、AQS接口
-
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
-
实现 自定 义 同步 组 件 时 ,将会调用同步器提供的模板方法
同步器提供的模板方法基本上分 为 3 类 :独占式 获 取与 释 放同步状 态 、共享式 获 取与 释 放
同步状 态 和 查询 同步 队 列中的等待 线程情况
5.2.2、AQS的实现
- 同步队列:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态
-
独占式同步状态获取与释放
-
共享式同步状态获取与释放:共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态。
-
独占式超时获取同步状态:通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false
5.3、重入锁
重入 锁 ReentrantLock , 顾 名思 义 ,就是支持重 进 入的 锁 ,它表示 该锁 能 够 支持一个 线 程对资 源的重复加 锁 。除此之外, 该锁 的 还 支持 获 取 锁时 的公平和非公平性 选择 。
- 实现重进入:是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再
次成功 获 取。
- 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到
该锁 。 锁 的最 终释 放要求 锁对 于 获 取 进 行 计 数自增, 计 数表示当前 锁 被重复 获 取的次数,而锁被 释 放 时 , 计 数自减,当 计 数等于 0 时 表示 锁 已 经 成功 释 放。
- 公平与非公平获取锁
- 公平锁:如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
- 非公平锁:
5.4、读写锁
- 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升;
-
一般情况下, 读 写 锁 的性能都会比排它 锁 好,因 为 大多数 场 景 读 是多于写的。在 读多于写的情况下, 读 写 锁 能 够 提供比排它 锁 更好的并 发 性和吞吐量。 Java 并 发 包提供 读 写 锁的实现是 ReentrantReadWriteLock
- 写锁的获取与释放:写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
- 读锁的获取与释放:读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
- 锁降级:锁降级指的是写锁降级成为读锁。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
5.5、LockSupport
当需要阻塞或 唤 醒一个 线 程的 时 候,可使用 LockSupport 工具 类 来完成相应工作。 LockSupport 定 义 了一 组 的公共静 态 方法, 这 些方法提供了最基本的 线 程阻塞和 唤醒功能,而 LockSupport 也成 为 构建同步 组 件的基 础工具。LockSupport 定 义 了一 组 以 park 开 头 的方法用来阻塞当前 线 程,以及unpark(Thread thread) 方法来 唤 醒一个被阻塞的 线 程。
5.6、Condition
任意一个 Java 对 象,都 拥 有一 组监视 器方法(定 义 在 java.lang.Object 上),主要包括 wait()、wait(long timeout) 、 notify() 以及 notifyAll() 方法, 这 些方法与 synchronized 同步关 键字配合,可以 实现 等待 / 通知模式。 Condition 接口也提供了 类 似 Object 的 监视 器方法,与 Lock 配合可以 实现等待/通知模式
5.6.1Condition接口
Condition 定 义 了等待 / 通知两种 类 型的方法,当前 线 程 调 用 这 些方法 时 ,需要提前 获取到Condition 对 象关 联 的 锁 。 Condition 对 象是由 Lock 对 象( 调 用 Lock 对 象的 newCondition() 方法)创建出来的, 换 句 话说 , Condition 是依 赖 Lock 对 象的。
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();
}
}
5.6.2、Condition的实现
ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部 类 ,因 为 Condition的操作需要获 取相关 联 的 锁 ,所以作 为 同步器的内部 类 也 较为 合理。每个 Condition 对 象都包含着一个队列(以下称 为 等待 队 列), 该队 列是 Condition 对 象 实现 等待 / 通知功能的关 键。 下面将分析 Condition 的 实现 ,主要包括:等待 队 列、等待和通知。
- 等待队列:等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
- 等待:调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中
- 通知:调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
六、java并发容器和框架
6.1、ConcurrentHashMap的实现原理与使用
ConcurrentHashMap是线程安全且高效的HashMap
6.1.1、为什么使用ConcurrentHashMap
- HashMap不安全
- HashTable效率低(synchronized)
- CurrentHashMap采用锁分段技术可有效提升并发访问率
6.1.2、ConcurrentHashMap的结构
6.1.3、ConcurrentHashMap初始化
6.1.4、定位Segment
6.1.5、ConcurrentHashMap的操作
6.2、ConcurrentLinkedQueue
- 在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。
- ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。
6.3、阻塞队列
6.3.1、阻塞队列实现原理:
6.4、Fork/Join框架
把大任 务分割成若干个小任 务 ,最 终汇总 每个小任 务结 果后得到大任 务结 果的框架
七、java中的原子类
原子更新基本 类 型、原子更新数 组、原子更新引用和原子更新属性,Atomic 包里的 类 基本都是使用 Unsafe 实现 的包装 类。采用CAS
- 原子更新基本类型:AtomicBoolean、AtomicInteger、AtomicLong
- Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现
- 原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 原子更新引用:AtomicReference
- 原子更新属性
八、java中的并发工具
8.1、CountDownLatch
允 许 一个或多个 线 程等待其他 线 程完成操作
8.2、CyclicBarrier
让一组线 程到达一个屏障(同步点) 时 被阻塞,直到最后一个 线 程到达屏障 时,屏障才会开 门 ,所有被屏障 拦 截的 线 程才会 继续 运行。
- 使用:CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
- 场景:可以用于多线程计算数据,最后合并计算结果的场景
- CyclicBarrier和CountDownLatch的区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景
8.3、Semaphore
是用来控制同 时访问 特定 资 源的 线 程数量,它通 过协调 各个 线程,以保 证 合理的使用公共 资 源
应用场景:流量控制
8.4、Exchanger
是一个用于 线 程 间协 作的工具 类 。 Exchanger 用于 进 行 线 程 间的数据交换 。它提供一个同步点,在 这 个同步点,两个 线 程可以交 换 彼此的数据。 这 两个 线 程通过exchange 方法交 换 数据,如果第一个 线 程先 执 行 exchange() 方法,它会一直等待第二个 线程也执 行 exchange 方法,当两个 线 程都到达同步点 时 , 这 两个 线 程就可以交 换 数据,将本 线 程生产出来的数据 传递给对 方
九、java中的线程池
9.1、线程池的实现原理
ThreadPoolExecutor执行:
-
如果当前运行的 线 程少于 corePoolSize , 则创 建新 线 程来 执 行任 务 (注意, 执 行 这 一步 骤
需要 获 取全局 锁)。
-
如果运行的 线 程等于或多于 corePoolSize , 则 将任 务 加入 BlockingQueue。
-
如果无法将任 务 加入 BlockingQueue ( 队 列已 满 ), 则创 建新的 线 程来 处 理任 务(注意,执行 这 一步 骤 需要 获 取全局 锁)。
-
如果 创 建新 线 程将使当前运行的 线 程超出 maximumPoolSize ,任 务 将被拒 绝 ,并 调 用
RejectedExecutionHandler.rejectedExecution()方法
ThreadPoolExecutor 采取上述步 骤 的 总 体 设计 思路,是 为 了在 执 行 execute() 方法 时,尽可能地避免 获 取全局 锁 (那将会是一个 严 重的可伸 缩 瓶 颈 )。在 ThreadPoolExecutor 完成 预热之后(当前运行的 线 程数大于等于 corePoolSize ),几乎所有的 execute() 方法 调 用都是 执 行步 骤 2,而步 骤 2 不需要 获 取全局 锁 。
9.2、线程池的使用
ThreadPoolExecutor创建线程池
参数
-
corePoolSize (核心线程数 ):当提交一个任 务 到 线 程池 时 , 线 程池会 创 建一个线程来 执 行任 务 ,即使其他空 闲 的基本 线 程能 够执 行新任 务 也会 创 建 线 程,等到需要 执行的任 务 数大于 线 程池基本大小 时 就不再 创 建。如果 调 用了 线 程池的 prestartAllCoreThreads()方法,线 程池会提前 创 建并启 动 所有核心 线 程。
-
BlockingQueue(阻塞 队 列):用于保存等待 执 行的任 务 的阻塞 队 列。可以 选择以下几个阻塞 队列。
-
ArrayBlockingQueue :是一个基于数 组结 构的有界阻塞 队 列,此 队 列按 FIFO (先 进先出)原则对 元素 进行排序。
-
LinkedBlockingQueue :一个基于 链 表 结 构的阻塞 队 列,此 队 列按 FIFO排序元素,吞吐量通常要高于 ArrayBlockingQueue 。静 态 工厂方法 Executors.newFixedThreadPool() 使用了 这 个 队列。
-
SynchronousQueue :一个不存 储 元素的阻塞 队 列。每个插入操作必 须 等到另一个 线 程 调用移除操作,否 则 插入操作一直 处 于阻塞状 态 ,吞吐量通常要高于 Linked-BlockingQueue ,静 态工厂方法 Executors.newCachedThreadPool 使用了 这 个 队列。
-
PriorityBlockingQueue :一个具有 优 先 级 的无限阻塞 队 列。
-
maximumPoolSize (最大 线 程数): 线 程池允 许创 建的最大 线 程数。如果 队 列 满了,并且已 创 建的 线 程数小于最大 线 程数, 则线 程池会再 创 建新的 线 程 执 行任 务 。 值得注意的是,如果使用了无界的任 务队 列 这 个参数就没什么效果。
-
ThreadFactory:用于 设 置 创 建 线 程的工厂,可以通 过线 程工厂 给 每个 创 建出来的 线 程设置更有意 义 的名字。
-
RejectedExecutionHandler (拒绝 策略):当 队 列和 线 程池都 满 了, 说 明 线 程池 处 于 饱和状 态 ,那么必 须 采取一种策略 处 理提交的新任务。
-
AbortPolicy:直接抛出异常(默认)
CallerRunsPolicy:让调用者所在线程来运行任务
DiscardOldestPolicy:丢弃队列里最近的一个任务
DiscardPolicy:不处理,丢弃掉
-
keepAliveTime(线程存活时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
-
TimeUnit ( 线 程活 动 保持 时间 的 单 位)
提交任务
关闭线程池:
- 通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程
- 只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法
合理配置线程数
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
- 任务的优先级:高、中和低
- 任务的执行时间:长、中和短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接
-
质 不同的任 务 可以用不同 规 模的 线 程池分开 处 理。 CPU 密集型任 务应配置尽可能小的线 程,如配置 N cpu +1 个 线 程的 线 程池。由于 IO 密集型任 务线 程并不是一直在 执 行任 务 , 则应配置尽可能多的 线 程,如 2*N cpu 。混合型的任 务
-
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越 长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU
线程池监控
如果在系 统 中大量使用 线 程池, 则 有必要 对线 程池 进 行 监 控,方便在出 现问题时,可以根据 线 程池的使用状况快速定位 问题 。
十、Executor框架
在 Java 中,使用 线 程来异步 执 行任 务 。 Java 线 程的 创 建与 销 毁 需要一定的开 销 ,如果我们为 每一个任 务创 建一个新 线 程来 执 行, 这 些 线 程的 创 建与 销 毁 将消耗大量的 计 算 资 源。同 时,为 每一个任 务创 建一个新 线 程来 执 行, 这 种策略可能会使 处 于高 负 荷状 态 的 应 用最 终 崩 溃。 Java 的 线 程既是工作 单 元,也是 执 行机制。从 JDK 5 开始,把工作 单 元与 执行机制分离开来。工作 单 元包括 Runnable 和 Callable ,而 执 行机制由 Executor 框架提供。
10.1、Executor
在 HotSpot VM 的 线 程模型中, Java 线程(java.lang.Thread )被一 对 一映射 为 本地操作系统线 程。 Java 线 程启 动时 会 创 建一个本地操作系 统线 程;当 该 Java 线 程 终 止 时 , 这 个操作系 统线程 也会被回收。操作系 统 会 调 度所有 线 程并将它 们 分配 给 可用的 CPU。在上 层 , Java 多 线 程程序通常把 应 用分解 为 若干个任 务 ,然后使用用 户级 的 调度器( Executor 框架)将 这 些任 务 映射 为 固定数量的 线 程;在底 层 ,操作系 统 内核将 这 些 线程映射到硬件 处 理器上。
结构
-
任 务 。包括被 执 行任 务 需要 实现 的接口: Runnable 接口或 Callable接口。
-
任 务 的 执 行。包括任 务执 行机制的核心接口 Executor ,以及 继 承自 Executor的 ExecutorService 接口。 Executor 框架有两个关 键类实现 了 ExecutorService接口 ( ThreadPoolExecutor 和 ScheduledThreadPoolExecutor)。
-
异步 计 算的 结 果。包括接口 Future 和 实现 Future 接口的 FutureTask 类 。
Executor 框架包含的主要的 类 与接口
-
Executor 是一个接口,它是 Executor 框架的基 础 ,它将任 务 的提交与任 务 的 执行分离开来。
-
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
-
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执
行命令。 ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更 强大。
-
Future 接口和 实现 Future 接口的 FutureTask 类 ,代表异步 计 算的 结果。
-
Runnable 接口和 Callable 接口的 实现类 ,都可以被 ThreadPoolExecutor 或Scheduled和ThreadPoolExecutor 执 行。
Executor框架成员
- ThreadPoolExecutor:ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor
- SingleThreadExecutor:单个线程,corePoolSize和maximumPoolSize被设置为1,用无界队列
- FixedThreadPool:固定线程数,KeepAliveTime设置为0L,用无界队列
- CachedThreadPool:大小无界的线程池,corePoolSize被设置为0,maximumPoolSize被设置为Integer.MAX_VALUE
- ScheduledThreadPoolExecutor:要用来在给定的延迟之后运行任务,或者定期执行任务。DelayQueue无界队列
-
Future接口
-
Runnable接口:无返回结果
-
Callable接口:有返回结果
-
Executors
10.2、FutureTask
Future 接口和 实现 Future 接口的 FutureTask 类 ,代表异步 计 算的 结 果。
面试题