一、并发问题
- 上下文切换: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并发底层实现
2.1、volatile
- volatile是轻量级的synchronized,保证多处理器开发的共享变量的可见性。
- 可见性的意思是一个线程修改共享变量的时候,其它线程可以读到这个值。
- volatile的成本比synchronized成本更低,不会引起线程的上下文切换和调度。
1、volatile的定义与实现原理
- 如果一个字段被声明为volatile,那么java线程的内存模型确保所有线程看到这个变量的值是一致的。
- volatile相关的CPU术语介绍,在下面。
2、volatile如何保证可见性
- 当一个线程修改了一个volatile变量的值,会将这个值刷新到主内存中,并且会使用某种机制通知其他线程该变量发生变化。其他线程需要读取这个变量时,则会去主内存中读取最新的值。
- 具体机制:volatile共享变量进行写操作的时候多出lock前缀的汇编
-
lock前缀的指令在多核处理器引发的事件
-
把当前的处理器缓存行的数据写到系统的内存
-
这个写回内存的操作会使其它CPU里面缓存的该地址的数据无效。
-
-
即使写回了内存,其它的处理器缓存的数据其实还是旧的。
-
每个处理器通过嗅探总线传播的数据,检查自己的缓存是不是过期了。
-
如果发现过期了那么就把缓存行设置为无效的状态。并且重新去内存读取数据。
-
-
-
小结:volatile的底层原理其实就是通过lock信号和MESI协议通知所有的处理器缓存失效,并且把数据更新到了内存。
2.2、Synchronized
- Java SE1.6对synchronized进行优化,引入偏向锁和轻量级锁,以及锁存储结构和升级过程。
- synchronized实现同步的基础
- 普通方法锁的是当前实例对象
- 静态同步方法锁的是当前类Class对象
- 同步方法块,锁住的是synchronized括号里面的对象
- 线程访问同步代码的时候必须要得到锁,退出或抛出异常时必须释放锁
- 锁 到底存在哪里呢? 锁 里面会存 储 什么信息呢?
- 同步方法块使用的是monitorenter和monitorexit,monitorenter指令指向同步代码块开始的位置,monitorexit指向同步代码块结束的位置;线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
- 同步方法:使用ACC_SYNCHRONIZED标识,指明该方法是一个同步方法
- 本 质 是 对一个 对 象的 监视 器( monitor ) 进 行 获 取,而 这 个 获 取 过 程是排他的,也就是同一 时刻只能有一个线 程 获 取到由 synchronized 所保 护对 象的 监视 器
- monitor对象存在每个java对象的对象头中
2.2.1对象头
- synchronized 用的 锁 存在 Java 对 象 头 里。如果 对 象是数 组类 型, 则 虚 拟机用12个字节 存 储对 象 头 ,如果 对 象是非数 组类 型, 则 用 8个字节 存 储对象头
- Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位
- 在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
2.2.2锁升级
- 偏向锁
-
若不存在多线程竞争,经常由某个线程获取。为了让次线程获取锁的代价更低,引入了偏向锁
-
线程访问获取锁的时候,会在对象头和栈帧的锁记录存储锁偏向的线程ID,那么该线程获取锁的时候就不需要去CAS来加锁和解锁。只需要检测Mark Word里面的线程ID是否指向当前线程
-
偏向锁的撤销
-
偏向锁是等待要竞争的时候才会释放锁的机制。
-
如果有其他线程竞争锁,那么首先是暂停持有锁的线程,并且检查是不是存活,如果是拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程。要么就是恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。
-
如果持有的线程结束,那么就标记为无锁的状态。
-
另一个线程如果CAS失败那么就会暂停持有的线程,持有的线程会解锁,并且把锁的线程ID设置为空。然后恢复线程。
-
-
- 轻量级锁
-
轻量级加锁
-
线程执行同步块之前,JVM会在线程里面创建锁记录的空间,并且把对象头的Mark Word复制到锁记录中。
-
然后通过CAS来把Mark Word替换为指向锁记录的指针。
-
如果成功那么就获取到轻量级的锁。
-
如果失败说明有其他线程竞争,那么线程就尝试自旋获取锁。
-
-
轻量级解锁
-
通过CAS把Mark Word恢复到对象头。
-
如果成功,那么说明没有竞争,失败说明有竞争。这个时候释放锁,并且唤醒等待的线程。因为已经锁膨胀所以Mark Word指向的是重量级锁的
-
如果失败那么就会膨胀为重量级的锁。这个时候竞争的线程2会把锁改为10也就是重量级锁。
-
-
- 锁的优缺点
2.3、java中实现原子操作
-
使用循环CAS实现原子操作:实际上就是检查和赋值合成一个原子操作。通过CMPXCHG指令完成自旋。
-
CAS实现原子操作的三大问题
-
ABA问题
-
循环时间长开销大。如果CAS不成功,就会一直自旋。可
-
只能保证一个共享变量的原子操作,对于多个变量它是无法处理的。
-
-
-
使用锁机制实现原子操作:保证了只有获取锁的线程才能够操作锁定的内存区域。
三、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的定义
-
如果一个操作happens-before另一个操作,第一个操作对第二个操作的可见,并且第一个操作在第二个操作之前
-
两个操作存在happens-before关系,并不意味着Java平台具体实现要按照这个顺序执行。如果重排序之后结果一致,那么这种重排序是可以的。
-
-
JMM的承诺
-
JMM对程序员承诺A happens-before B,也就是A的结果对B是可见的。A的执行顺序在B之前。
-
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 = new Singleton();分三步
-
为uniqueInstance分配内存空间
-
初始化uniqueInstance
-
将uniqueInstance指向分配好的内存地址;
JVM具有指令重排,指向顺序可能变成1->3->2。如,线程T1执行了1和3,T2调用方法发现uniqueInstance不为null直接返回,然而uniqueInstance还没被初始化。volatile可以禁止指令重排
3.9、JMM综述
四、多线程
4.1、线程简介
4.1.1、什么是线程
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线程。
- Daemon 属性需要在启 动线 程之前 设 置,不能在启 动线 程之后 设 置。
- 在Java虚拟机退出时Daemon线程中的finally块并不一定会执行
4.2、启动和终止线程
4.2.1、构造线程
-
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。
- Thread中对线程进行初始化
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、启动线程
4.2.3、中断
- 中断可以理解 为线 程的一个 标识 位属性,它表示一个运行中的 线 程是否被其他 线 程 进行了中断操作。中断好比其他 线 程 对该线 程打了个招呼,其他 线 程通 过调 用 该线 程的interrupt()方法 对 其 进 行中断操作。
4.2.4、过期的suspend()、resume()、和stop()
- suspend() 、 resume() 和 stop() 方法完成了 线 程的 暂 停、恢复和终止工作
- 不建 议 使用的原因主要有: suspend() 方法 在 调 用后, 线 程不会 释 放已 经 占有的资源(比如 锁 ),而是占有着 资 源 进 入睡眠状 态 , 这样 容易引 发 死 锁问题 。同 样 , stop() 方法在终结一个 线 程 时 不会保 证线 程的 资 源正常 释 放,通常是没有 给 予 线 程完成 资 源 释放工作的机会,因此会 导 致程序可能工作在不确定状 态
4.3、线程间通信
4.3.1、volatile和synchronized关键字
- volatile可以用来修饰字段(成员变量),告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。过多地使用volatile是不必要的,因为它会降低程序执行的效率。
- synchronized 可以修 饰 方法或者以同步 块 的形式来 进 行使用,确保多个 线程在同一个 时 刻,只能有一个 线 程 处 于方法或者同步 块 中,保 证 了 线 程 对变 量 访问 的可 见性和排他性。
- 对 象、 对 象的 监视 器、同步 队 列和 执 行 线 程之 间 的关系
- 任意 线 程 对 Object ( synchronized 保 护 )的 访问 ,首先要 获得Object 的 监视 器。如果 获 取失 败 , 线 程 进 入同步 队 列, 线 程状 态变为 BLOCKED 。当 访问Object的前 驱 ( 获 得了 锁 的 线 程) 释 放了 锁 , 则该释 放操作 唤 醒阻塞在同步 队 列中的 线程,使其重新尝试对监视 器的获取。
-
4.3.2、等待/通知机制
- 等待 / 通知的相关方法是任意 Java 对 象都具 备 的,因 为这 些方法被定 义 在所有 对象的父类java.lang.Object 上
- 等待 / 通知机制,是指一个 线 程 A 调 用了 对 象 O 的 wait() 方法 进 入等待状 态 ,而另一个 线 程B调 用了 对 象 O 的 notify() 或者 notifyAll() 方法, 线 程 A 收到通知后从 对 象 O 的 wait() 方法返回, 进而执 行后 续 操作。上述两个 线 程通 过对 象 O 来完成交互,而 对 象上的 wait() 和 notify/notifyAll()的关系就如同开关信号一 样 ,用来完成等待方和通知方之间的交互工作。
- 使用 wait() 、 notify() 和 notifyAll() 时 需要先 对调 用 对 象加 锁。
- 调 用 wait() 方法后, 线 程状 态 由 RUNNING 变 WAITING ,并将当前 线 程放置到 对象的等待 队列。
- notify() 或 notifyAll() 方法 调 用后,等待 线 程依旧不会从 wait() 返回,需要 调 用 notify()或 notifAll() 的 线 程 释 放 锁 之后,等待 线 程才有机会从 wait() 返回。
- notify() 方法将等待 队 列中的一个等待 线 程从等待 队 列中移到同步 队 列中,而notifyAll()方法 则 是将等待 队 列中所有的 线 程全部移到同步 队 列,被移 动 的 线 程状 态 由 WAITING变为 BLOCKED。
- 从 wait() 方法返回的前提是 获 得了 调 用 对 象的 锁。
- 从上述 细节 中可以看到,等待 / 通知机制依托于同步机制,其目的就是确保等待 线程从wait() 方法返回 时 能 够 感知到通知 线 程 对变 量做出的修改。
4.3.3、等待/通知的今典范式
- 等待方
- 获 取 对 象的 锁
- 如果条件不 满 足,那么 调 用 对 象的 wait() 方法,被通知后仍要 检查条件
- 条件 满 足 则执 行 对应 的 逻辑 。
synchronized(对象) {while(条件不满足) {对象.wait();}对应的处理逻辑 }
-
- 通知方
- 获 得 对 象的 锁
- 改 变 条件
-
通知所有等待在对象上的线程
synchronized(对象) {改变条件对象.notifyAll(); }
-
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))方法来获取同步的状态。
-
同步器支持独占或者是共享地获取锁。
- 同步器是 实现锁 (同步 组 件)的关 键 ,在 锁 的 实现中聚合同步器,利用同步器 实现锁 的 语义 。可以 这样 理解二者之 间 的关系: 锁 是面向使用者的,它定 义 了使用者与 锁交互的接口(比如可以允 许 两个 线 程并行 访问 ), 隐 藏了 实现细节 ;同步器面向的是 锁 的 实现者, 它 简 化了 锁 的 实现 方式,屏蔽了同步状 态 管理、 线 程的排 队 、等待与 唤 醒等底 层 操作。 锁和同步器很好地隔离了使用者和 实现 者所需关注的 领 域。
-
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
- 实现 自定 义 同步 组 件 时 ,将会调用同步器提供的模板方法同步器提供的模板方法基本上分 为 3 类 :独占式 获 取与 释 放同步状 态 、共享式 获 取与 释 放同步状 态 和 查询 同步 队 列中的等待 线程情况
5.2.2、AQS的实现
- 同步队列:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态
-
独占式同步状态获取与释放
-
共享式同步状态获取与释放:共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态。
-
独占式超时获取同步状态:通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false
5.3、重入锁
- 实现重进入:是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再 次成功 获 取。
- 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到 该锁 。 锁 的最 终释 放要求 锁对 于 获 取 进 行 计 数自增, 计 数表示当前 锁 被重复 获 取的次数,而锁被 释 放 时 , 计 数自减,当 计 数等于 0 时 表示 锁 已 经 成功 释 放。
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再
- 公平与非公平获取锁
- 公平锁:如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
- 非公平锁:
5.4、读写锁
- 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升;
- 一般情况下, 读 写 锁 的性能都会比排它 锁 好,因 为 大多数 场 景 读 是多于写的。在 读多于写的情况下, 读 写 锁 能 够 提供比排它 锁 更好的并 发 性和吞吐量。 Java 并 发 包提供 读 写 锁的实现是 ReentrantReadWriteLock
- 写锁的获取与释放:写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
- 读锁的获取与释放:读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
- 锁降级:锁降级指的是写锁降级成为读锁。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
5.5、LockSupport
5.6、Condition
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的实现
- 等待队列:等待队列是一个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、阻塞队列
- 当队列满时,队列会阻塞插入元素的线程,直到队列不满;在队列为空时,获取元素的线程会等待队列变为非空。
- 阻塞 队 列常用于生 产 者和消 费 者的 场 景,生 产 者是向 队 列里添加元素的 线 程,消 费者是从 队 列里取元素的 线 程。阻塞 队 列就是生 产 者用来存放元素、消 费 者用来获取元素的容器
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
- DelayQueue:一个使用优先级队列实现的无界阻塞队列
- SynchronousQueue:一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作, 否 则 不能 继续 添加元素
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
6.3.1、阻塞队列实现原理:
- 使用通知模式 实现 。所 谓 通知模式,就是当生 产 者往 满 的 队 列里添加元素 时 会阻塞住生产 者,当消 费 者消 费 了一个 队 列中的元素后,会通知生 产 者当前 队 列可用。
- ArrayBlockingQueue使用了Condition来实现,当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现
6.4、Fork/Join框架
七、java中的原子类
- 原子更新基本类型:AtomicBoolean、AtomicInteger、AtomicLong
- Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现
- 原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 原子更新引用:AtomicReference
- 原子更新属性
八、java中的并发工具
8.1、CountDownLatch
- 使用:CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。 当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法 会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个 点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个 CountDownLatch的引用传递到线程里即可
- 如果有线程处理得比较慢,我们不可能让主线程一直等待,所以可以使用另外一个带指定时间的await方法——await(long time,TimeUnit unit),这个方法等待特定时间后,就会不再阻塞当前线程
- 数器必 须 大于等于 0 ,只是等于 0 时 候, 计 数器就是零, 调 用 await 方法 时不会阻塞当前 线 程。 CountDownLatch 不可能重新初始化或者修改 CountDownLatch 对 象的内部 计数 器的 值 。一个 线 程 调 用 countDown 方法 happen-before 另外一个 线 程 调 用 await 方法。
8.2、CyclicBarrier
- 使用:CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
- 场景:可以用于多线程计算数据,最后合并计算结果的场景
- CyclicBarrier和CountDownLatch的区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景
8.3、Semaphore
8.4、Exchanger
- 应用场景:用于遗传算法
九、java中的线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
9.1、线程池的实现原理
ThreadPoolExecutor执行:
- 如果当前运行的 线 程少于 corePoolSize , 则创 建新 线 程来 执 行任 务 (注意, 执 行 这 一步 骤需要 获 取全局 锁)。
- 如果运行的 线 程等于或多于 corePoolSize , 则 将任 务 加入 BlockingQueue。
- 如果无法将任 务 加入 BlockingQueue ( 队 列已 满 ), 则创 建新的 线 程来 处 理任 务(注意,执行 这 一步 骤 需要 获 取全局 锁)。
- 如果 创 建新 线 程将使当前运行的 线 程超出 maximumPoolSize ,任 务 将被拒 绝 ,并 调 用RejectedExecutionHandler.rejectedExecution()方法
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 ( 线 程活 动 保持 时间 的 单 位)
提交任务
- execute():用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
- subimit():用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线 程一段 时间 后立即返回, 这时 候有可能任 务 没有 执 行完
关闭线程池:
- 通过调用线程池的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
线程池监控
- t askCount : 线 程池需要 执 行的任 务 数量。
- completedTaskCount : 线 程池在运行 过 程中已完成的任 务 数量,小于或等于 taskCount 。
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数
十、Executor框架
10.1、Executor
- 任 务 。包括被 执 行任 务 需要 实现 的接口: Runnable 接口或 Callable接口。
- 任 务 的 执 行。包括任 务执 行机制的核心接口 Executor ,以及 继 承自 Executor的 ExecutorService 接口。 Executor 框架有两个关 键类实现 了 ExecutorService接口 ( ThreadPoolExecutor 和 ScheduledThreadPoolExecutor)。
- 异步 计 算的 结 果。包括接口 Future 和 实现 Future 接口的 FutureTask 类 。
- Executor 是一个接口,它是 Executor 框架的基 础 ,它将任 务 的提交与任 务 的 执行分离开来。
- ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
- ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。 ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更 强大。
- Future 接口和 实现 Future 接口的 FutureTask 类 ,代表异步 计 算的 结果。
- Runnable 接口和 Callable 接口的 实现类 ,都可以被 ThreadPoolExecutor 或Scheduled和ThreadPoolExecutor 执 行。
- 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