重点
存在线程安全问题必须满足三个条件:
1.有共享变量, 有共享变量, 有共享变量(重要的事情说三边)
2.处在多线程环境下
3.共享变量有直接修改(读和写)操作或者每次修改结果都不一样。
并发的风险和优势
Java与线程
线程的实现
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。目前线程是Java里面进行处理器资源调度的最基本单位
实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现)
内核线程实现
内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核(Multi-Threads Kernel)
轻量级进程 --- java使用的方式
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LightWeight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型, 对应了下图轻量级线程和内核线程的关系
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。轻量级进程也具有它的局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
但是轻量级进程也是有缺点的, 它是用户层的函数, 实现于内核线程, 所以他的每次函数调用, 都是系统调用, 需要在用户态和内核态之间来回切换, 这代码是非常大的, 并且因为轻量级线程需要和内核线程1 1配对, 需要消耗一定量的内核资源, 所以是有数量的限制的, 还有它的调度代价也相当的大, 这也是当前java存在的一个问题
用户线程实现
用户线程实现的方式被称为1:N实现。广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程(User Thread,UT)的一种,因此从这个定义上看,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型,下图是进程和用户线程 1:N 的关系
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。因为使用用户线程实现的程序通常都比较复杂,除了有明确的需求外(譬如以前在不支持多线程的操作系统中的多线程程序、需要支持大规模线程数量的应用),一般的应用程序都不倾向使用用户线程。Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如Golang、Erlang等,使得用户线程的使用率有所回升
混合实现
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为N:M实现。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量
比是不定的,是N:M的关系, 下图就是用户线程与轻量级进程之间M:N的关系
许多UNIX系列的操作系统,如Solaris、HP-UX等都提供了M:N的线程模型实现。在这些操作系统上的应用也相对更容易应用M:N的线程模型
Java线程的实现
java 1.2之前使用的是绿色线程(说白了就是用户线程)实现, 但是到了jdk1.3后改成了使用基于系统线程实现的java线程
以hotspot为例, 它的每个线程都是直接映射到系统原生线程的, 中间没有额外的间接结构, 这样做的好处就是jvm不需要去管理线程的调度, 全权交给底下的操作系统去处理, 所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的
说白了就是操作系统是怎样的线程, 一定程度上影响了jvm使用了什么线程
Java线程调度
分为两种, 一种是协同式调度(线程运行多久由线程自己决定, 当线程运行完毕, 会告诉系统让它切换到另一个线程)另一种是抢占式调度(线程调度由系统决定)
协同式调度会出现cpu一直卡在线程这里一直运行的情况, 可能导致系统崩溃
抢占式调度线程只能让出自己的执行权限, 想要抢占cpu时间片确是不可能了, 但是可以另辟蹊径, 使用线程优先级来做, 但是说到线程优先级却无法完全保证线程优先运行, 比如: window 就存在"优先级推进器(Priority Boosting)"的功能, 即使线程优先级最高也没用, 这个功能的大致作用是一个线程被执行的特别频繁时, window可能会放弃正在运行的高优先级线程,转而去运行这个频繁被执行的线程
并且java的线程优先级也存在一个问题, 那就是如果系统的优先级等级数量比较多, 那么java的优先级不会出现很多问题, 但是如果系统的优先级只有1个(极端一点), 那么java的所有优先级就一点用都没有了
状态转换
线程的五种状态
系统层面分为五种状态
初始状态: 线程刚刚创建, 这个时候它只是对象, 没有执行start函数
可运行状态: 线程执行了start函数, 但是还未获得时间片
运行状态: 线程获得了时间片
阻塞状态: 线程读取文件或者IO操作, 该线程不会实际使用到cpu, 会导致上下文切换, 进入阻塞状态
终止状态: 线程结束, 生命周期已经结束
线程六种状态
根据Thread.State枚举分为六种状态(java层面分析线程状态)
new(新建): 新建状态, 线程创建, 但是未调用 start() 方法
runnable(运行): 运行状态, 线程正在执行也有可能线程正在等待系统为它分配执行时间
waiting(无限期等待) : 处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object::wait()方法;
- 没有设置Timeout参数的Thread::join()方法;
- LockSupport::park()方法
Time Waiting(限期等待): 处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
■Thread::sleep()方法;
■设置了Timeout参数的Object::wait()方法;
■设置了Timeout参数的Thread::join()方法;
■LockSupport::parkNanos()方法;
■LockSupport::parkUntil()方法。
Blocked(阻塞): 线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态
Terminated(结束):已终止线程的线程状态,线程已经结束执行。
下图是线程状态相互转化的方法
下图是RUNNABLE状态当前可能存在的状态
这六种状态的方式, 主要用于调试线程查看线程状态错误
@Slf4j
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") { // NEW
@Override
public void run() {
log.debug("running ... ");
}
};
Thread t2 = new Thread("t2") { // RUNNABLE
@Override
public void run() {
while (true) {
}
}
};
t2.start();
Thread t3 = new Thread("t3") { // TERMINATED
@Override
public void run() {
log.debug("running ... ");
}
};
t3.start();
Thread t4 = new Thread("t4") { // TIMED_WAITING
@Override
public void run() {
synchronized (TestState.class) {
try {
TimeUnit.MILLISECONDS.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") { // WAITING
@Override
public void run() {
try {
t2.join();
} catch (Exception e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") { // BLOCKED
@Override
public void run() {
synchronized (TestState.class) {
try {
TimeUnit.MILLISECONDS.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
t6.start();
TimeUnit.MILLISECONDS.sleep(500);
log.debug("{} state {}", t1.getName(), t1.getState()); // NEW
log.debug("{} state {}", t2.getName(), t2.getState()); // RUNNABLE
log.debug("{} state {}", t3.getName(), t3.getState()); // TERMINATED
log.debug("{} state {}", t4.getName(), t4.getState()); // TIMED_WAITING
log.debug("{} state {}", t5.getName(), t5.getState()); // WAITING
log.debug("{} state {}", t6.getName(), t6.getState()); // BLOCKED
}
}
Java与协程
内核线程的局限
通过一个具体场景来解释目前Java线程面临的困境。今天对Web应用的服务要求,不论是在请求数量上还是在复杂度上,与十多年前相比已不可同日而语,这一方面是源于业务量的增长,另一方面来自于为了应对业务复杂化而不断进行的服务细分。现代B/S系统中一次对外部业务请求的响应,往往需要分布在不同机器上的大量服务共同协作来实现,这种服务细分的架构在减少单个服务复杂度、增加复用性的同时,也不可避免地增加了服务的数量,缩短了留给每个服务的响应时间。这要求每一个服务都必须在极短的时间内完成计算,这样组合多个服务的总耗时才不会太长;也要求每一个服务提供者都要能同时处理数量更庞大的请求,这样才不会出现请求由于某个服务被阻塞而出现等待。
Java目前的并发编程机制就与上述架构趋势产生了一些矛盾,1:1的内核线程模型是如今Java虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。以前处理一个请求可以允许花费很长时间在单体应用中,具有这种线程切换的成本也是无伤大雅的,但现在在每个请求本身的执行时间变得很短、数量变得很多的前提下,用户线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。
传统的Java Web服务器的线程池的容量通常在几十个到两百之间,当程序员把数以百万计的请求往线程池里面灌时,系统即使能处理得过来,但其中的切换损耗也是相当可观的。现实的需求在迫使Java去研究新的解决方案,同大家又开始怀念以前绿色线程的种种好处,绿色线程已随着Classic虚拟机的消失而被尘封到历史之中,它还会有重现天日的一天吗?
总结:
下来就是 java线程使用的是轻量级进程(使用的是系统原生线程, 后面都叫LWP吧), 这种LWP对于系统来说开销很大, 比如: 用户态到内核态的切换代价大; 线程的调度代价大; 系统可容纳的LWP也是有限的, 而现在我们追求的是微服务拆分, 通常一次客户请求需要用到多个微服务的服务组合到一起完成一次请求, 这样就要求我们的每个服务效率要很快, 但是LWP无法做到这点, 这这样的话, 只能找回绿色线程(用户线程)了吗?(绿色线程 jdk1.2 版本使用的线程实现方式)
协程的复苏
不要以为系统调用(切换, 或者叫中断)的不太影响效率, 用户线程切换到内核线程的过程中, 用户线程需要保存用户线程切换的上下文数据, 然后还需要把寄存器, 内存分页等恢复到内核线程中, 这样内核线程才能够获取到充足的资料, 这样内核线程才能够重新激活, 这种保护和恢复现场的操作免不了涉及各种寄存器, 缓存的来回拷贝, 这可不是什么轻量级操作
但是如果使用用户线程就能避免掉这些么??? 其实不然, 但是如果使用了用户线程, 上面这些过程不就由我们控制了么???这样我们就能够使用我们的方法来减少这些开销
从dos时代开始用户就自发的模拟出多线程, 自己保护和恢复场景的工作模式, 大致就是在内存中找出一个栈结构, 模仿调用栈, 只要线程压栈和出栈的过程遵循规则, 并且这块内存不被破坏, 这样就成功模拟出了多线程, 到后来操作系统出现了多线程了, 这种模拟多线程的方式变少了, 但是还未消失, 以用户线程的形式继续存在, 由于最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling)的,所以它有了一个别名——“协程”(Coroutine)。又由于这时候的协程会完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”(Stackfull Coroutine),起这样的名字是为了便于跟后来的“无栈协程”(Stackless Coroutine)区分开(无栈协程本质上是一种有限状态机,状态保存在闭包里,自然比有栈协程恢复调用栈要轻量得多,但功能也相对更有限)。
协程的优势
协程的优势主要在于它比LWP要轻的多, 在64位Linux上HotSpot的线程栈容量默认是1MB,此外内核数据结构(Kernel Data Structures)还会额外消耗16KB内存, 而一个协程的栈通常在几百个字节到几KB之间,所以Java虚拟机里线程池容量达到两百就已经不算小了,而很多支持协程的应用中,同时并存的协程数量可数以十万计
协程当然也有它的局限,需要在应用层面实现的内容(调用栈、调度器这些)特别多, 除此之外,协程在最初,甚至在今天很多语言和框架中会被设计成协同式调度,这样在语言运行平台或者框架上的调度器就可以做得非常简单, 但是这种方法存在很严重的问题, 详解前面的协同式调度
并且具体到Java语言,还会有一些别的限制,譬如HotSpot这样的虚拟机,Java调用栈跟本地调用栈是做在一起的。如果在协程中调用了本地方法,还能否正常切换协程而不影响整个线程?另外,如果协程中遇传统的线程同步措施会怎样?譬如Kotlin提供的协程实现,一旦遭遇synchronize关键字,那挂起来的仍将是整个线程
Java的解决方案
对于有栈协程,有一种特例实现名为纤程(Fiber),这个词最早是来自微软公司,后来微软还推出过系统层面的纤程包来方便应用做现场保存、恢复和纤程调度。OpenJDK在2018年创建了Loom项目,这是Java用来应对本节开篇所列场景的官方解决方案,根据目前公开的信息,如无意外,日后该项目为Java语言引入的、与现在线程模型平行的新并发编程机制中应该也会采用“纤程”这个名字,不过这显然跟微软是没有任何关系的。从Oracle官方对“什么是纤程”的解释里可以看出,它就是一种典型的有栈协程
Loom项目背后的意图是重新提供对用户线程的支持,但与过去的绿色线程不同,这些新功能不是为了取代当前基于操作系统的线程实现,而是会有两个并发编程模型在Java虚拟机中并存,可以在程序中同时使用。新模型有意地保持了与目前线程模型相似的API设计,它们甚至可以拥有一个共同的基类,这样现有的代码就不需要为了使用纤程而进行过多改动,甚至不需要知道背后采用了哪个并发编程模型。Loom团队在JVMLS 2018大会上公布了他们对Jetty基于纤程改造后的测试结果,同样在5000QPS的压力下,以容量为400的线程池的传统模式和每个请求配以一个纤程的新并发处理模式进行对比,前者的请求响应延迟在10000至20000毫秒之间,而后者的延迟普遍在200毫秒以下,具体结果如图
在新并发模型下,一段使用纤程并发的代码会被分为两部分——执行过程(Continuation)和调度器(Scheduler)。执行过程主要用于维护执行现场,保护、恢复上下文状态,而调度器则负责编排所有要执行的代码的顺序。将调度程序与执行过程分离的好处是,用户可以选择自行控制其中的一个或者多个,而且Java中现有的调度器也可以被直接重用。事实上,Loom中默认的调度器就是原来已存在的用于任务分解的Fork/Join池(JDK 7中加入的ForkJoinPool)。
Loom项目目前仍然在进行当中,还没有明确的发布日期,上面笔者介绍的内容日后都有被改动的可能。如果读者现在就想尝试协程,那可以在项目中使用Quasar协程库[1],这是一个不依赖Java虚拟机的独立实现的协程库。不依赖虚拟机来实现协程是完全可能的,Kotlin语言的协程就已经证明了这一点。Quasar的实现原理是字节码注入,在字节码层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖Java虚拟机的现场保护虽然能够工作,但很影响性能,对即时编译器的干扰也非常大,而且必须要求用户手动标注每一个函数是否会在协程上下文被调用,这些都是未来Loom项目要
解决的问题。
如同JDK 5把Doug Lea的dl.util.concurrent项目引入,成为java.util.concurrent包,JDK 9时把Attila Szegedi的dynalink项目引入,成为jdk.dynalink模块。Loom项目的领导者Ron Pressler就是Quasar的作者
线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的 --- Brian Goetz
不可变
不可变(Immutable)的对象一定是线程安全的, 使用 final 修饰的基础类型数据就是绝对安全的, 还有很多如 List.of() Map.of(), Set.of产生的集合都是线程安全的
绝对线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
上面这种线程安全是很难实现的, 所以java绝对线程安全是没有的
例如Vector, 说是线程安全的, 但是Vector存在add和remove还有get一起使用的情况下是线程不安全的
相对线程安全
对象单次的操作是线程安全的, 就是相对线程安全
java语言中存在很多这样的集合比如 Vector, Hashtable还有使用上Collections的synchronizedCollection方法的包装集合都是相对线程安全的
线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等
线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁了。也正是这个原因,suspend()和resume()方法都已经被声明废弃了。常见的线程对立的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
线程安全的实现方法
互斥同步
互斥同步(Mutual Exclusion & Synchronization)是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式。因此在“互斥同步”这四个字里面,互斥是因,同步是果;互斥是方法,同步是目的
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(BlockStructured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
在jdk1.5之前synchronized关键字效率是非常慢的, 所以引入了可重入锁ReentrantLock, 这种方法和synchronized 相似, 但是在那个时候Lock的方法效率比synchronized效率要高的高, 单核处理器下两种锁的吞吐量对比如下图
多核下比较:
并且ReentrantLock还提供了很多功能
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可
但是它也是有问题的, lock需要记得在finally中调用unlock方法
在jdk1.6甚至到jdk8+后Lock和synchronized的效率是差不多的, 所以前面的此时在现在来看是无所谓的
前面说了这么多的区别, 现在考虑下到底用那种方法?
lock的方法比较灵活, 还提供了公平锁, 锁绑定多个条件, 等待可中断等方法, 所以在复杂的情况下推荐使用这种方式, 但是要记得在finally中调用unlock方法
使用synchronized的话, 代码比较简洁清晰, 合适在比较简单的环境下使用
非阻塞同步
互斥同步使线程阻塞和唤醒, 也叫阻塞同步, 是悲观锁, 这种方案需要控制线程阻塞和唤醒都需要用到中断也就是系统调用, 而且共享变量不论是否安全, 都会上锁, 这种方法其实还是有比较不好的地方, 随着硬件指令集的发展,我们已经有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free)编程
为什么笔者说使用乐观并发策略需要“硬件指令集的发展”?因为我们必须要求操作和冲突检测这两个步骤具备原子性。靠什么来保证原子性?如果这里再使用互斥同步来保证就完全失去意义了,所以我们只能靠硬件来实现这件事情,硬件保证某些从语义上看起来需要多次操作的行为可以只通过一条处理器指令就能完成,这类指令常用的有:
测试并设置(Test-and-Set);
获取并增加(Fetch-and-Increment);
交换(Swap);
比较并交换(Compare-and-Swap,下文称CAS);
加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC;
其中,前面的三条是20世纪就已经存在于大多数指令集之中的处理器指令,后面的两条是现代处理器新增的,而且这两条指令的目的和功能也是类似的。在IA64、x86指令集中有用cmpxchg指令完成的CAS功能,在SPARC-TSO中也有用casa指令实现的,而在ARM和PowerPC架构下,则需要使用一对ldrex/strex指令来完成LL/SC的功能。因为Java里最终暴露出来的是CAS操作,所以我们以CAS指令为例进行讲解
CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断
上面可能不太懂, 我总结下
cas指令有三个操作数, 其一线程工作内存; 其二主内存; 其三新值
判断工作内存的变量是否等于主存? 是 更新, 否 补偿(一般补偿方法是循环, 获取工作主存的值作为工作内存再次判断直到成功位置)
它存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”, 虽然java提供了个类AtomicStampedReference解决这个问题, 不过这个ABA问题其实不影响程序的正确性, 所以AtomicStampedReference很鸡肋
线程上下文切换
cpu不可能一直都在一个线程上运行程序, 而是会切换到另一个进程上的线程上运行, 所以这中间需要保存当前线程的场景, 等到下次回到这个线程中执行的时候, 恢复场景使用
而每次切换都需要备份和重新载入这是需要消耗性能资源的
程序计数器+栈帧+局部变量+操作数栈+返回地址等信息需要恢复和备份
线程启动
一般调用start方法后, 线程不一定执行了, 也不一定没执行, 它是不确定的, 可能线程正处于就绪状态, 而且一个线程的start方法只能被调用一次, 多次调用会抛出异常IllegalThreadStateException
判断线程是否启动需要由程序调度器进行判断
线程新建默认状态是 NEW
线程start() 之后状态是 RUNNABLE
线程sleep() 后是 TIME_WAITING
线程阻塞之后是 BLOCKED
线程结束后是 TERMINATED
线程调用join() 方法后是WAITING
sleep方法和yield方法还有wait方法
sleep方法调用之后未必马上sleep, 如果sleep完毕之后线程状态为 TIME_WAIT 状态
sleep后的线程可以被唤醒, 但是会抛出异常
sleep和wait的区别在于, sleep不会释放锁和管程, wait会
线程中断Interrupt
注意Interrupt中的两个方法区别
isInterrupted 和 Interrupted 这两个方法都是判断是否终止了
但是前者不会消除中断标记, 后者会
这种消除中断标记和不消除中断标记的方式主要用在park的时候使用
如果标记没被消除, 该线程下次还是会被park住, 如果被消除, 则下次调用park无效
@Slf4j
public class ParkDemo {
@Test
public void test() throws Exception {
Thread thread = new Thread(() -> {
log.debug("park...");
// park
LockSupport.park();
log.debug("unpark ...");
// 显示打断标记
// log.debug("打断不清除标记: {}", Thread.currentThread().isInterrupted());
log.debug("打断清除标记: {}", Thread.interrupted());
LockSupport.park(); // Thread.interrupted() 清除标记后, 线程会在这里阻塞
log.debug("unpark ...");
});
thread.start();
// 打断线程
thread.interrupt();
thread.join();
}
}
两阶段终止模式
线程结束以前使用 stop() 但这是存在问题的, 比如线程A被stop掉了, 但是它的锁还在没被解除掉, 这样另一个等待这个锁的线程就永远阻塞了, 但是如果使用的是interrupt方法, 不仅仅线程被终止, 而且该线程的锁资源会被释放, 并且使用interrupt方法结束的线程还能够监控到自己被终止( isInterrupted 和 interrupted)而做出相对应的反应
利用 isInterrupted
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行
class TPTInterrupt {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
current.interrupt();
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
调用
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
结果
11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
利用停止标记
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
调用
TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
结果
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
主线程和守护线程
一般情况下, 进程需要等待所有的线程退出后才会退出, 但是守护线程不会, 进程不会等待守护线程, 直接强制退出
我们jvm的垃圾回收gc就是守护线程
管程 --- 悲观锁(阻塞)
临界区
一段代码块对共享资源存在多个线程的读写操作, 我们就叫它临界区
临界区存在多个线程竞争共享资源的问题, 由于分时系统, 我们的cpu不能一直负责一条线程的执行, 所以在cpu切换的时候需要保存当时的场景, 之后cpu回来后需要恢复场景再次执行代码, 我们简称这个过程为线程上下文切换
而线程竞争共享资源的这个过程中, 由于执行序列不同而导致的结果无法预测, 我们称之为竞态条件
synchronized(重量级锁)
synchronized重量级锁我们称之为[对象锁]方案, 意味着这个方案的时候需要一个对象, 使用这个方案我们就再也不需要担心线程上下文切换导致的问题了, 在这个代码块中永远只会有一个线程执行, 直到线程释放了这个对象锁
官方版: 对象锁保证了临界区的原子性
注意: 同时我发现锁方案不能规避代码重排的问题, 前文懒汉模式对象的volatile关键字就是因为这个原因
synchronized修饰方式:
修饰在普通方法或者使用this作为对象锁的对象 ----- 都表示把this类对象当作对象锁的对象
修饰在静态方法或者类.class方法进行修饰 ----- 表示将类名作为对象锁的对象
@Slf4j
public class Demo01 {
private static int counter = 0;
private static Object lock = new Object();
@Test
public void test04() throws Exception {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("res counter = {}", counter);
}
}
注意: 对象锁的对象是非常重要的, 对象就是一把钥匙, 被线程1拿走了, 线程2只能等线程1用完了还回来继续用
其底层使用的是monitor锁的概念
Monitor概念
对象头结构
以32位虚拟机为例
Klass World(代表类型) 指针指向了Class对象的地址; age: 幸存区年龄, 一次自增一次, 直到15次, 直接送到老年区
对象头只对象才会有, 如果是基本类型的话, 则没有
比如 int 是4 Integer 则是12
Monitor(锁) --- synchronized底层原理
是什么???
monitor被翻译成监视器或者管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级锁)之后, 该对象头的Mark Monitor 中被设置指向Monitor对象的指针
Monitor结构如下:
java源码:
@Slf4j
public class SynchronizedDemo {
final Object lock = new Object();
int counter = 0;
@Test
public void test() {
synchronized (lock) {
counter++;
}
}
}
字节码:
public void test();
Code:
0: aload_0
1: getfield #3 // lock 引用(synchronized开始)
4: dup
5: astore_1 // lock引用 -> slot 1
6: monitorenter // 将lock对象MarkWord置为Monitor指针
7: aload_0
8: dup
9: getfield #4 // i
12: iconst_1 // 准备常数 1
13: iadd // +1
14: putfield #4 // i
17: aload_1 // lock 引用
18: monitorexit // 将 lock 对象 MarkWord 重置, 唤醒 EntryList
19: goto 27
22: astore_2 // e -> slot2, 将异常放入 slot2
23: aload_1 // lock 引用
24: monitorexit // 将 lock 对象 MarkWord 重置, 唤醒 EntryList
25: aload_2 // slot2 (e)
26: athrow // throw e
27: return
Exception table:
from to target type
7 19 22 any // 监控异常 7~19行只要发生错误跳转到 22 行处理
22 25 22 any
Synchronized底层原理进阶
可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它, 转而去执行另一段代码, 而在控制权限返回之后, 重新回到这个方法后不会有任何的影响, 这种代码就是可重入代码
可重入代码有一些共同的特征,例如,不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等。我们可以通过一个比较简单的原则来判断代码是否具备可重入性:如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题
案例: ThreadLocal web服务器, 一个请求一个线程
小故事
故事角色
老王 - JVM
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包。
java对象头
在32位虚拟机前提下:
首先要了解 JVM 对象结构, hotspot 虚拟机的对象头结构分成两个部分
第一部分用于存储对象运行时数据, 如hashcode, gc 分代年龄等等, 另一部分主要存储这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键, 另外一部分用于存储指向方法区对象类型数据的指针
如果是数组对象,还会有一个额外的部分用于存储数组长度
由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。
64 位虚拟机 Mark Word:
轻量级锁
是什么???
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅_将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功_,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
为什么要用轻量级锁???
为了防止某个时间段内不存在多线程竞争的环境使用上重量级锁浪费资源, 所以java引入了轻量级锁这个概念
什么时候使用轻量级锁?
如果一个对象虽然有多线程访问, 但是多线程的访问时间是错开的(也就是没有竞争关系), 那么就能够使用轻量级锁来优化, 轻量级锁对使用者是透明的, 关键字就是synchronized
怎么使用的轻量级锁?
假设存在两个同步块, 利用同一个对象加锁
@Slf4j
public class LightweightLockDemo {
private static final Object obj = new Object();
private static void method01() {
synchronized (obj) {
method02();
}
}
private static void method02() {
synchronized (obj) {
System.out.println("method02");
}
}
@Test
public void test() throws Exception {
method01();
}
}
加锁过程
(1) 在线程栈里面创建一个锁记录(Lock Record)对象, 每个线程的栈帧都会存在一个锁记录(Lock Record)的结构, 内部可以存储对象的Mark Word
(2) 让锁记录中的Object reference指向锁对象的Object引用, 并尝试使用cas替换掉Object锁对象的Mark Word, 并将Mark Word 的值存入锁记录(交换)
(3) 如果cas替换成功, 对象头中存储了锁记录地址和状态00, 表示由该线程对象加锁, 这时图示如下:
(4) 如果cas失败, 有两种情况:
-
如果是其他线程已经持有了该Object的轻量级锁, 这时表明有竞争, 进入说膨胀过程
-
如果时自己执行了 synchronized 锁重入, 那么再添加一条 Lock Record 作为重入的计数
解锁过程
(1) 当退出 synchronized 代码块(解锁时)如果有取值为null的锁记录, 表示有重入, 这时重置锁记录, 表示重入计数减一
(2) 当退出 synchronized 代码块(解锁时)锁记录的值不为null, 这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功, 则解锁成功
- 失败, 说明轻量级锁进行了锁膨胀或已经升级为重量级锁, 进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中, cas操作无法成功, 这时一种清凉就是有其他线程为此对象加上了轻量级锁(有竞争), 这时需要进行锁膨胀, 将轻量级锁变为重量级锁
private static final Object obj = new Object();
private static void method01() {
synchronized (obj) {
// 同步代码块
}
}
(1) 当线程1进行轻量级锁加锁时, 发现线程0已经对该对象加上了轻量级锁
(2) 这时线程1加轻量级锁失败, 进入锁膨胀流程
-
即为Object对象申请Monitor锁, 让Object指向重量级锁地址
-
然后自己进入Monitor的EntryList BLOCKED
当线程0退出同步块解锁时, 使用cas将Mark Word 的值恢复给对象头, 失败. 这时会进入重量级解锁流程, 即按照Monitor地址找到Monitor对象, 设置Owner为null, 唤醒EntryList中BOCKED线程
优缺点
如果不存在竞争的话, 那么cas操作避免了互斥锁的重量级操作, 如果存在竞争的话, 那么这个操作不仅仅需要使用互斥量本身的开销之外, 还需要cas操作的开销, 比直接使用互斥量的开销还大
自旋锁与自适应自旋(适合多核CPU)
是什么???
重量级锁竞争的时候, 还可以使用自旋来进行优化, 如果当前线程自旋成功(即这时候持锁线程已经退出了同步块, 释放了锁), 这时当前线程就可以避免阻塞
线程阻塞之前做一个忙循环(自旋), 默认是10次循环, 可以使用参数-XX:PreBlockSpin来自行更改, 有前提, 要是多核cpu
下面是自旋重试成功的情况:
下面是自旋重试失败的情况:
jdk6自动开启自旋锁, jdk7后不再默认开启
为什么要使用自旋锁?
线程的阻塞是需要挂起和恢复线程的, 两个操作需要转入内核态来完成, 所以效率比较低, 所以引入了一个忙循环的过程, 在这个过程中也许前面获得锁的线程已经结束释放了锁, 这时忙循环的线程就能够直接获得这个锁, 不需要转入内核态了
有什么优缺点?
优点
是减少了线程转入内核态所需要的时间
缺点
如果前一个获得锁的线程没有在十次循环的时间内结束, 则这个循环所需要的cpu资源却耗费了
如果使用的系统是单核系统, 不论如何自旋都是浪费时间的
什么是自适应自旋???
自适应代表了自旋的次数不再是固定的10次, 而是可变的
为什么使用自适应自旋???
自适应自旋是为了减少'不可能或者相对较少'进入自旋的锁, 用更多的时间放在经常成功进入自旋的锁上, 这样加快了性能
缺点:
自适应自旋锁的时间过高或者过低, 那么自适应自旋锁就无法适应到合适的范围了
锁消除
是什么?
锁消除就是消除那些根本不存在共享对象竞争却用到锁的代码块上的锁
什么时候使用锁消除?
通过逃逸分析判断来确认一段代码是否需要锁消除
逃逸分析: 一个对象在方法中产生, 如果被当作其他方法的参数, 这种叫方法逃逸, 如果被其他线程访问到, 则叫线程逃逸
有些时候程序员并没有主动调用到互斥同步方法, 它还是被动被调用了, 例如:
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
上面这段代码在jdk1.5之前会被翻译成StringBuffer来完成字符串对象连接, 在jdk1.5之后会被优化为StringBuilder
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
假设上面的方法就是jdk1.5之前的反汇编代码, 这个代码已经用到了同步互斥的功能了, 但是程序员看到的却是 s1 + s2 + s3 , 根本无法被判断, 但是逃逸分析分析出来了这里的代码并未发生逃逸现象, 这里的锁是 sb 对象, 但是 sb 是线程单独的对象(不是共享资源), 不会被多线程影响到, 还有 s1 s2 s3 都是不变对象, sb在连接字符串时sb的返回结果是不变的, 所以这也是一个可重入方法, 也是线程安全的(当然这一切都建立在 sb 不是共享资源的前提下, 如果sb是共享资源, 则这个方法是不安全的, append方法将被多次执行, 那就不是 s1 + s2+ s3这么简单了)
锁粗化
是什么? 为什么?
加锁的范围尽量小, 只在共享变量的修改操作范围上加锁就好, 但是如果锁的外面就是循环的话, 锁最好放在循环外面, 即使修改的只有循环内的共享资源, 这样便会减少锁过多的进行加锁释放阻塞恢复等操作, 如果虚拟机检测到一直出现对同一个对象进行多次加锁操作的代码块, 虚拟机便会把锁粗化到这些操作的范围上, 只需要一把锁就好, 这便是锁粗化
偏向锁
是什么???
轻量级锁在没有竞争时(就自己这个线程), 每次重入仍然需要执行cas操作
jdk6中引入了偏向锁来做进一步优化: 只有第一次使用cas将线程ID设置到对象的Mark Work头, 之后发现这个线程ID是自己的就表示没有竞争, 不用重新cas, 以后只要不发生竞争, 这个对象就归该线程所有
作比较:
细心的读者看到这里可能会发现一个问题:当对象进入偏向状态的时候,Mark Word大部分的空间(23个比特)都用于存储持有锁的线程ID了,这部分空间占用了原有存储对象哈希码的位置,那原来对象的哈希码怎么办呢?
在Java语言里面一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载hashCode()方法按自己的意愿返回哈希码),否则很多依赖对象哈希码的API都可能存在出错风险。而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的是对象的一致性哈希码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码
偏向状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、 age 都为0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=e来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为0x01即最后3位为001,这时它的hashcode、age
都为0,第一次用到hashcode时才会赋值
测试禁用
在上面测试代码运行时在添加VM参数-XX:-UseBiasedLocking禁用偏向锁
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 40 80 10 00 (01000000 10000000 00010000 00000000) (1081408)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.zhazha.sync.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 30 e1 8f 52 (00110000 11100001 10001111 01010010) (1385161008)
4 4 (object header) 21 00 00 00 (00100001 00000000 00000000 00000000) (33)
8 4 (object header) 40 80 10 00 (01000000 10000000 00010000 00000000) (1081408)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.zhazha.sync.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 40 80 10 00 (01000000 10000000 00010000 00000000) (1081408)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
为什么???
偏向锁就是在无竞争环境下, 消除同步使用互斥量, 把整个临界区的同步都消除掉, 甚至连cas都不需要
有什么优点和缺点???
偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
撤销 - 调用对象 hashCode
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁
被撤销
■轻量级锁会在锁记录中记录hashCode
■重量级锁会在Monitor中记录hashCode
在调用hashCode后使用偏向锁,记得去掉-XX: -UseBiasedLocking
撤销-其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
撤销-调用wait/notify
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
static Thread t1,t2,t3;
public static void main(String[] args) throws InterruptedException {
test4();
}
private static void test4() throws InterruptedException {
Vector list = new Vector<>();
int loopNumber = 38;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
}
class Dog {}
如果切换过多(可能是39次), jvm可能会觉得这个临界区不适合使用偏向锁, 转而膨胀成轻量级锁
wait/notify
小故事-为什么需要wait
●由于条件不满足,小南不能继续进行计算
●但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
●于是老王单开了一间休息室(调用wait方法) ,让小南到休息室( WaitSet )等着去了,但这时锁释放开,
其它人可以由老王随机安排进屋
●直到小M将烟送来,大叫--声你的烟到了(调用notify方法)
●小南于是可以离开休息室,重新进入竞争锁的
●此时小南要和其他线程重新抢夺进门的权限了(时间片)
wait和notify底层实现
■Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
■BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
■BLOCKED线程会在Owner线程释放锁时唤醒
■WAITING 线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争
API介绍
■obj.wait() 让进入 object 监视器的线程到 waitSet 等待
■obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
■obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
虚假唤醒
obj.notify(); // 随机唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
上面的情况需要考虑到虚假唤醒的情况, 不管是上面的notify唤醒还是下面的notifyAll唤醒, 都存在错误唤醒的问题, 比如: notify随机唤醒了相同锁上面的另一个锁, 虽然这个锁被唤醒了, 但是不是notify想要唤醒的那个线程, 所以它被虚假唤醒了
再比如: 使用notifyAll, 全部线程都被唤醒了, 这样子不需要唤醒的线程也被唤醒了, 这里也存在虚假唤醒
wait notify 的正确姿势
sleep(long n) 和 wait(long n) 的区别
- sleep是Thread方法,而wait是Object的方法
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized 一起用
- sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
- 它们状态TIMED_ WAITING
模式之保护性暂停
同步模式之保护性暂停
是什么?
即Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
■有一个结果需要从一个线程传递到另一个线程, 让他们关联同一个GuardedObject
■如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者消费者)
■JDK中,join的实现、Future的实现,采用的就是此模式
■因为要等待另一方的结果, 因此归类到同步模式
@Slf4j
public class GuardedObjectDemo {
@Test
public void test() throws Exception {
CountDownLatch latch = new CountDownLatch(2);
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
try {
log.debug("等待结果...");
Optional> optional = Optional.ofNullable((List) guardedObject.getWaitTime(10000));
List list = optional.orElse(new ArrayList<>());
log.debug("结果大小: {}", list.size());
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
log.debug("开始下载... ");
try {
List list = Downloader.download();
guardedObject.complete(list);
} catch (IOException e) {
e.printStackTrace();
}
latch.countDown();
}, "t2").start();
latch.await();
}
class GuardedObject {
// 结果
private Object response;
/**
* 获取结果
*
* @return
*
* @throws InterruptedException
*/
public Object get() throws InterruptedException {
synchronized (this) {
while (null == response) {
this.wait();
}
}
return response;
}
/**
* 获取结果, 设置最长超时时间
*
* @param timeOutTime
* 最长超时时间
*
* @return
*
* @throws InterruptedException
*/
public Object getWaitTime(long timeOutTime) throws InterruptedException {
synchronized (this) {
LocalDateTime start = LocalDateTime.now();
long passTime;
long waitTime = 0;
while (response == null) {
passTime = timeOutTime - waitTime;
if (passTime <= 0) {
break;
}
this.wait(passTime);
Duration duration = Duration.between(start, LocalDateTime.now());
waitTime = duration.toMillis();
}
}
return response;
}
/**
* 产生结果
*
* @param response
*/
public void complete(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
}
异步模式之生产者和消费者
要点
■与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一- 对应
■消费队列可以用来平衡生产和消费的线程资源
■生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
■消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
■JDK中各种阻塞队列,采用的就是这种模式
@Slf4j
public class MessageQueueDemo {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int tmp = i;
new Thread(() -> queue.put(new Message(tmp, "message " + tmp)), "生成者" + i).start();
}
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.task();
}
}, "消费者").start();
}
}
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
private final LinkedList messageList = new LinkedList<>();
private Integer capcity;
public MessageQueue(Integer capcity) {
this.capcity = capcity;
}
/**
* 获取消息
*
* @return
*/
public Message task() {
synchronized (messageList) {
Message message;
try {
while (messageList.isEmpty()) {
try {
log.debug("队列为空, 消费者线程等待");
messageList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message = messageList.removeFirst();
log.debug("消费了消息message = {}", message);
} finally {
messageList.notifyAll();
}
return message;
}
}
/**
* 存入消息
*
* @param message
*/
public void put(Message message) {
synchronized (messageList) {
try {
while (capcity == messageList.size()) {
try {
log.debug("消息队列满了, 线程等待消息被消费后再次存入消息");
messageList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
messageList.add(message);
log.debug("生产了消息 message = {}", message);
} finally {
messageList.notifyAll();
}
}
}
}
@Slf4j(topic = "c.Message")
class Message {
private Integer id;
private String message;
@Override
public String toString() {
return "Message{" + "id=" + id + ", message='" + message + '\'' + '}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Message(Integer id, String message) {
this.id = id;
this.message = message;
}
}
Park && Unpark
它们是LockSupport类中的方法
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("线程{}运行了, 并且即将调用park", Thread.currentThread().getName());
LockSupport.park();
log.debug("线程{} unpark成功", Thread.currentThread().getName());
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
LockSupport.unpark(t1);
log.debug("解锁了");
}
特点
与Object的wait & notify相比
■wait, notify 和notifyAll必须配合Object Monitdr一起使用,而unpark不必
■park & unpark是以线程为单位来[阻塞]和[唤醒]线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么[精确]
■park & unpark可以先unpark,而wait & notify不能先notify
park/unpark原理
每个线程都有自己的一个Parker对象,由三部分组成 _counter, _cond 和 _mutex 打个比喻
■线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
■调用park就是要看需不需要停下来歇息
■如果备用干粮耗尽,那么钻进帐篷歇息
■如果备用干粮充足,那么不需停留,继续前进
■调用unpark, 就好比令干粮充足
■如果这时线程还在帐篷,就唤醒让他继续前进
■如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
■因为背包空间有限,多次调用unpark仅会补充一 份备用干粮
如果先调用unpark再调用park呢?
重新理解线程状态转换
假设有线程 Thread t
情况 1 NEW --> RUNNABLE
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2 RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
-
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 WAITING --> RUNNABLE
竞争锁失败,t 线程从 WAITING --> BLOCKED
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 断点
}
},"t2").start();
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notifyAll(); // 唤醒obj上所有等待线程 断点
}
}
}
情况 3 RUNNABLE <--> WAITING
-
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
注意是当前线程在t 线程对象的监视器上等待
-
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4 RUNNABLE <--> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
情况 5 RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
-
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
情况 6 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
TIMED_WAITING --> RUNNABLE
情况 7 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <--> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE
情况 9 RUNNABLE <--> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A 对象锁,接下来想获取 B 对象的锁 t2 线程 获得 B 对象锁,接下来想获取 A 对象的锁
例如:
@Slf4j
public class DeadLockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 先 lock1 后 lock2
synchronized (lock1) {
log.debug("{}上锁lock1", Thread.currentThread().getName());
synchronized (lock2) {
log.debug("{}上锁lock2", Thread.currentThread().getName());
}
}
}, "t1").start();
new Thread(() -> {
// 先 lock2 后 lock1 和上面的加锁方向相反, 很容易发生死锁问题
synchronized (lock2) {
log.debug("{}上锁lock2", Thread.currentThread().getName());
synchronized (lock1) {
log.debug("{}上锁lock1", Thread.currentThread().getName());
}
}
}, "t2").start();
}
}
定位死锁
(1) 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
(2) 还可以使用jvisualvm检测死锁
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
@Slf4j
public class PhilosopherDemo {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("筷子①");
Chopstick c2 = new Chopstick("筷子②");
Chopstick c3 = new Chopstick("筷子③");
Chopstick c4 = new Chopstick("筷子④");
Chopstick c5 = new Chopstick("筷子⑤");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
/**
* 哲学家
*/
@Slf4j(topic = "c.Philosopher")
@EqualsAndHashCode(callSuper = false)
@Data
@NoArgsConstructor
class Philosopher extends Thread {
// 左手筷子
private Chopstick left;
// 右手筷子
private Chopstick right;
Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
synchronized (left) {
synchronized (right) {
eat();
}
}
}
}
}
/**
* 筷子类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
class Chopstick {
private String name;
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
活锁
活锁就是两个线程相互谦让拿到了cpu时间片但是又把执行权限丢给对方, 一直互相谦让就成活锁了
还有一种情况就是出现在两个线程互相改变对方的结束条件,最后谁也无法结束
说白了, 对 i ++和对 i -- 线程无限停留都不满足自己的条件, 无限循环
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
但是这种方式却导致了饥饿问题
// 这种方式是饥饿
// new Philosopher("阿基米德", c1, c5).start();
可打断
可打断锁
@Slf4j
public class LockInterruptorDemo {
public static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("上锁了{}", Thread.currentThread().getName());
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等待锁过程中被打断...");
return;
}
try {
log.debug("上锁成功{}", Thread.currentThread().getName());
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("主线程上锁了");
t1.start();
t1.interrupt();
log.debug("主线程释放了 {} 线程", t1.getName());
}
}
锁超时
饥饿状态可以使用锁超时方式解决
立刻超时
@Slf4j
public class TryLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("子线程尝试上锁");
if (!lock.tryLock()) {
log.debug("获得锁失败");
return;
}
try {
log.debug("获得锁成功");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("主线程上锁了");
t1.start();
}
}
公平锁
ReentrantLock 默认是不公平的
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解
条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j(topic = "c.CorrectPostureDemo")
public class CorrectPostureDemo {
private static final ReentrantLock ROOM = new ReentrantLock();
private static Condition waitCigarette = ROOM.newCondition();
private static Condition waitTakeout = ROOM.newCondition();
// 是否抽烟
private static Boolean hasCigarette = false;
// 是否点外卖
private static Boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖到了没?[{}]", hasTakeout);
while (!hasTakeout) {
try {
log.debug("没到, 先休息会儿");
waitTakeout.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖到了, 可以工作了");
} finally {
ROOM.unlock();
}
}, "小女").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没??[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟, 先休息会儿");
try {
waitCigarette.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟, 可以干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
Sleeper.sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
log.debug("{} 外卖送到了[{}]", Thread.currentThread().getName(), hasTakeout);
waitTakeout.signal();
} finally {
ROOM.unlock();
}
}, "外卖小哥1").start();
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
log.debug("{} 烟送到了[{}]", Thread.currentThread().getName(), hasCigarette);
waitCigarette.signal();
} finally {
ROOM.unlock();
}
}, "外卖小哥2").start();
}
}
顺序控制
面试中经常会有的问题
@Slf4j
public class LockOrderControlDemo {
private final static ReentrantLock lock = new ReentrantLock();
private final static Condition cond = lock.newCondition();
private volatile static int flag = 0;
public static void main(String[] args) {
Thread a = new Thread(() -> {
while (true) {
try {
lock.lock();
if (0 == flag) {
System.err.print(Thread.currentThread().getName() + " -> ");
flag = 1;
cond.signalAll();
}
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "A");
Thread b = new Thread(() -> {
while (true) {
try {
lock.lock();
if (1 == flag) {
System.err.print(Thread.currentThread().getName() + " -> ");
flag = 2;
cond.signalAll();
}
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "B");
Thread c = new Thread(() -> {
while (true) {
try {
lock.lock();
if (2 == flag) {
System.err.print(Thread.currentThread().getName() + " -> ");
flag = 3;
cond.signalAll();
}
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "C");
Thread d = new Thread(() -> {
while (true) {
try {
lock.lock();
if (3 == flag) {
System.err.println(Thread.currentThread().getName());
flag = 0;
cond.signalAll();
}
cond.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "D");
a.start();
b.start();
c.start();
d.start();
}
}
CPU 缓存结构原理
CPU 缓存结构
查看 cpu 缓存
速度比较
查看 cpu 缓存行
cpu 拿到的内存地址格式是这样的
CPU 缓存读
根据低位,计算在缓存中的索引
判断是否有效
-
0 去内存读取新数据更新缓存行
-
1 再对比高位组标记是否一致
一致,根据偏移量返回缓存数据
不一致,去内存读取新数据更新缓存行
CPU 缓存一致性
MESI 协议
- M(修改,Modified):本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有);
- E(专有,Exclusive):缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
- S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝;
- I(无效,Invalid):缓存行失效, 不能使用。
状态 | 触发本地读取 | 触发本地写入 | 触发远端读取 | 触发远端写入 |
---|---|---|---|---|
M状态(修改) | 本地cache:M 触发cache:M 其他cache:I |
本地cache:M 触发cache:M 其他cache:I |
本地cache:M→E→S 触发cache:I→S 其他cache:I→S 同步主内存后修改为E独享,同步触发、其他cache后本地、触发、其他cache修改为S共 |
本地cache:M→E→S→I 触发cache:I→S→E→M 其他cache:I→S→I 同步和读取一样,同步完成后触发cache改为M,本地、其他cache改为I |
E状态(独享) | 本地cache:E 触发cache:E 其他cache:I |
本地cache:E→M 触发cache:E→M 其他cache:I 本地cache变更为M,其他cache状态应当是I(无效) |
本地cache:E→S 触发cache:I→S 其他cache:I→S 当其他cache要读取该数据时,其他、触发、本地cache都被设置为S(共享) |
本地cache:E→S→I 触发cache:I→S→E→M 其他cache:I→S→I 当触发cache修改本地cache独享数据时时,将本地、触发、其他cache修改为S共享.然后触发cache修改为独享,其他、本地cache修改为I(无效),触发cache再修改为M |
S状态(共享) | 本地cache:S 触发cache:S 其他cache:S |
本地cache:S→E→M 触发cache:S→E→M 其他cache:S→I 当本地cache修改时,将本地cache修改为E,其他cache修改为I,然后再将本地cache为M状态 |
本地cache:S 触发cache:S 其他cache:S |
本地cache:S→I 触发cache:S→E→M 其他cache:S→I 当触发cache要修改本地共享数据时,触发cache修改为E(独享),本地、其他cache修改为I(无效),触发cache再次修改为M(修改) |
I状态(无效) | 本地cache:I→S或者I→E 触发cache:I→S或者I →E 其他cache:E、M、I→S、I本地、触发cache将从I无效修改为S共享或者E独享,其他cache将从E、M、I 变为S或者I |
本地cache:I→S→E→M 触发cache:I→S→E→M 其他cache:M、E、S→S→I |
既然是本cache是I,其他cache操作与它无关 | 既然是本cache是I,其他cache操作与它无关 |
下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态。
M | E | S | I | |
---|---|---|---|---|
M | × | × | × | √ |
E | × | × | × | √ |
S | × | × | √ | √ |
I | √ | √ | √ | √ |
举个栗子来说:
假设cache 1 中有一个变量x = 0的cache line 处于S状态(共享)。
那么其他拥有x变量的cache 2、cache 3等x的cache line调整为S状态(共享)或者调整为 I 状态(无效)。
-
初始:一开始时,缓存行没有加载任何数据,所以它处于 I 状态。
-
本地写(Local Write):如果本地处理器写数据至处于 I 状态的缓存行,则缓存行的状态变成 M。
-
本地读(Local Read):如果本地处理器读取处于 I 状态的缓存行,很明显此缓存没有数据给它。此时分两种情况:
- (1)其它处理器的缓存里也没有此行数据,则从内存加载数据到此缓存行后,再将它设成 E 状态,表示只有我一家有这条数据,其它处理器都没有;
- (2)其它处理器的缓存有此行数据,则将此缓存行的状态设为 S 状态。(备注:如果处于M状态的缓存行,再由本地处理器写入/读出,状态是不会改变的)
-
远程读(Remote Read):假设我们有两个处理器 c1 和 c2,如果 c2 需要读另外一个处理器 c1 的缓存行内容,c1 需要把它缓存行的内容通过内存控制器 (Memory Controller) 发送给 c2,c2 接到后将相应的缓存行状态设为 S。在设置之前,内存也得从总线上得到这份数据并保存。
-
远程写(Remote Write):其实确切地说不是远程写,而是 c2 得到 c1 的数据后,不是为了读,而是为了写。也算是本地写,只是 c1 也拥有这份数据的拷贝,这该怎么办呢?c2 将发出一个 RFO (Request For Owner) 请求,它需要拥有这行数据的权限,其它处理器的相应缓存行设为 I,除了它自已,谁不能动这行数据。这保证了数据的安全,同时处理 RFO 请求以及设置I的过程将给写操作带来很大的性能消耗。
RFO(Read Or Ownership)请求:cpu需要获取缓存行的所有权需要先发送 RFO 请求
什么情况下会发生RFO请求?
-
- 线程的工作从一个处理器移到另一个处理器, 它操作的所有缓存行都需要移到新的处理器上。此后如果再写缓存行,则此缓存行在不同核上有多个拷贝,需要发送 RFO 请求了。
-
- 两个不同的处理器确实都需要操作相同的缓存行
缓存行是什么?
请看:伪共享和缓存行
缓存系统中是以缓存行(cache line)为单位存储的。缓存行通常是 64 字节(译注:本文基于 64 字节,其他长度的如 32 字节等不适本文讨论的重点),并且它有效地引用主内存中的一块地址。一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。所以,如果你访问一个 long 数组,当数组中的一个值被加载到缓存中,它会额外加载另外 7 个,以致你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的(如链表),你将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中。
如果存在这样的场景,有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?。没错,伪共享(False Sharing)问题就发生了!有张 Disruptor 项目的经典示例图,如下:
上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。
内存屏障 --- Memory Barrier(Memory Fence)
可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
java线程的内存模型
原子性、可见性与有序性
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、
CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
(1) 原子性(Atomicity)
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个, 我们大致可以认为,基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子性 协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。
如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。
(2) 可见性(Visibility)
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized和final
在执行synchronized最后需要unlock掉, 但是unlock之前需要完成store, write操作, 所以是可见的
而final关键字的功能是修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通 过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看见final字段的值
导致共享变量在线程间不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
JMM关于synchronized的两条规定
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意加锁与解锁是同- -把锁)
主存与工作内存
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理 硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。每条线程 还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保 存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实 现时必须保证下面提及的每一种操作都是原子的、不可再分的
线程持有主存变量的副本, 在没有特殊处理的前提下线程的所有操作都是针对副本, 而后再由副本同步到主存中, 下面就是java主存和副本的操作过程图示:
下图是对每个操作的详细说明:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。
如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从 工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作 必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令 的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此 之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内 存不接受,或者工 作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存
中。 - 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量 前,需要重新执行load或assign操作以初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
案例一: 利用 i++来分析内存间交互操作的详细过程
主体来说就是这么一个步骤:
// 多线程操作 a++;
① 将主存中的变量a锁定为一条线程独占状态 lock --- 不允许新变量直接在工作内存中产生, 只能在主存中产生读取和加载到工作内存
② 将主存读取的变量a读取出来 read
③将读取出来的变量加载到工作内存中 load --- ②③不允许独自执行, 必须按照顺序执行, 但不保证连续执行
④ 线程操作变量a自增 use; load操作可能同时给多个线程进行
⑤ 将线程执行完毕后的结果赋值给工作内存 assign --- 不允许线程的丢弃assign操作, 即工作变量变化了, 必须同步到主存, 同时如果没有assign操作也不允许工作内存私自同步到主存
⑥ 将工作内存中的变量存储到主存空间 store
⑦ 将store的变量写入到主存中 write --- ⑥⑦步骤不允许独自运行, 必须按照顺序执行, 但不保证连续执行
⑧ 解除独占模式, 释放变量, 之后该变量才可以被其他线程锁定 unlock
注意: 上面加粗的两句话, 这就是为啥要使用volatile修饰变量的原因了
案例二: 退不出的循环
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
为什么呢?分析一下:
- 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
-
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
-
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量
的值,结果永远是旧值
那如何解决呢???
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
volatile关键字
volatile处理可见性问题
第一项是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的, 对于普通变量来说是需要重新读取变量才能够获取到最新线程改变的变量
注意: volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般
如下图是下了线程断点的例子:
当前线程是
count 的大小是 2, 准备自增 count, 突然它失去了时间片
变成了线程 12, 直接执行到 count++ 的下面一行, 自增了值, 变成了 3
然后我们切换到线程 10 再看看这个 count 的值是多少
[Thread-10] DEBUG com.xxx.ExerciseTransfer - count = 3
发现打印出来的是 3, 说明了, 线程12的修改在其他线程是能够立即被反应出来的
这个就是volatile的第一个作用
而普通变量是不行的, 比如:
线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对 主内存进行读取操作,新变量值才会对线程B可见。
它需要其他线程重新读取时才会获取到新的值
Java里面的运算操作符并非原子操作, 这导致volatile变量的运算在并发下一样是不安全
如下分析i++和i--线程不安全问题
我们知道 i++ 或者 i-- 是线程不安全的, 但是为什么呢???
先上代码:
@Slf4j
public class Demo01 {
private int counter = 0;
@Test
public void test() throws Exception {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
this.counter++;
}
log.debug("{} = {}", Thread.currentThread().getName(), counter);
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
this.counter--;
}
log.debug("{} = {}", Thread.currentThread().getName(), counter);
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("res counter = {}", counter);
}
}
我们发现打印的结果并不是绝对正确的
注意: 上面这段代码确实是线程不安全的, 但是由于线程比较少, 所以可能需要多试几次才会出现问题, 当然你也可以把创建线程的代码放在for循环下面, 让for循环创建500+个线程自增, 500+线程自减, 让线程多做几次上下文切换, 就会表现出问题了(工作环境不要抱有侥幸心理, 不要因为没有看到问题而不去理会)
首先我们了解 counter++ 和 counter-- 在jvm字节码上是怎么运行的?
counter++
getfield #获取字段
iconst_1 #给定一个为1的常量
iadd # 相加
putfield # 赋值
counter--
getfield #获取字段
iconst_1 #给定一个为1的常量
isub # 相减
putfield # 赋值
实事求是地说,使用字节码来分析并发问题仍然是不严谨的,因为即使编译出来只有一条字节码指令,也并不意味执行这条指令就是一个原子操作。一条字节码指令在解释执行时,解释器要运 行许多行代码才能实现它的语义。如果是编译执行,一条字节码指令也可能转化成若干条本地机器码 指令。此处使用-XX:+PrintAssembly参数输出反汇编来分析才会更加严谨一些,但是考虑到阅读的方便性,并且字节码已经能很好地说明问题,所以此处使用字节码来解释。
下图就显示着这个过程存在的问题
volatile处理指令重排序优化的问题
volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致
volatile的使用场景
(1) 这个修饰符比较合适在修饰单个变量单纯的读取时的使用方法上, 举个例子比较简单
Map configOptions;
char[] configText;
// 此变量必须定义为volatile
volatile boolean initialized = false;
// 假设以下代码在线程A中执行
// 模拟读取配置信息,当读取完成后
// 将initialized设置为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// 假设以下代码在线程B中执行
// 等待initialized为true,代表线程A已经把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();
(2) volatile 还能使用在一个线程写, 其他线程读的情况下
总结: 记住这句话, volatile两个作用: 保持共享变量可见性和禁止指令重排序, synchronized关键字只能保证那一块代码只有一个线程运行, 不能禁止指令重排
volatile只能保证可见性不能保证原子性
volatile底层原理
如何保证可见性
volatile修饰了ready
写屏障(sfence) 保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true;
// 写屏障, 在这之前的代码所有变量都会被同步到主存
}
而读屏障(fence) 保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// 在读之后的变量都会被同步到主存
if (ready) {
r.r1 = num + num;
}
else {
r.r1 = 1;
}
}
如果保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) {
num = 2;
ready = true; // 写屏障, 在这之前的代码不会被jit重排到这行之后
}
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
if (ready) { // 读屏障, 在这之后的代码都不会被jit重排到这行之前
r.r1 = num + num;
}
else {
r.r1 = 1;
}
}
volatile内存语义的实现
volatile重排序规则表
举例来说,第三行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或
写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从表3-5我们可以看出。
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来
禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总
数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图,如下图所示
上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图,如下图所示
上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码进行说明。
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; // 普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; // 第二个 volatile写
}
… // 其他方法
}
针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化。
注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障
(3) 有序性(Ordering)
有序性: 线程的执行始终跟着代码的顺序运行
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象
线程内似表现为串行的语义: 就是单线程看的话, 我们的指令表现的是一条一条的串行执行完毕的
工作内存与主内存同步延迟: 工作内存不能够即时的和主内存进行相互的更新
举个例子:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧
指令级并行原理
鱼罐头的故事
加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工...
可以将每个鱼罐头的加工流程细分为 5 个步骤:
-
去鳞清洗 10分钟
-
蒸煮沥水 10分钟
-
加注汤料 10分钟
-
杀菌出锅 10分钟
-
真空封罐 10分钟
即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会影响对第二条鱼的杀菌出锅...
指令重排序优化
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段
术语参考:
- instruction fetch (IF)
- instruction decode (ID)
- execute (EX)
- memory access (MEM)
- register write back (WB)
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
支持流水线的处理器
现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率
提示:
奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃
诡异的结果
创建maven项目
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
boolean ready = false;
@Actor
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
}
else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
pom.xml
4.0.0
com.zhazha
ordering
1.0-SNAPSHOT
jar
3.0
org.openjdk.jcstress
jcstress-core
${jcstress.version}
UTF-8
0.5
1.8
jcstress
org.apache.maven.plugins
maven-compiler-plugin
3.1
${javac.target}
${javac.target}
org.apache.maven.plugins
maven-shade-plugin
2.2
main
package
shade
${uberjar.name}
org.openjdk.jcstress.Main
META-INF/TestList
经过控制态测试
java -jar jcstress -v
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
4 matching test results.
[OK] com.zhazha.ConcurrencyTest
(JVM args: [-XX:+UnlockDiagnosticVMOptions, -XX:+StressLCM, -XX:+StressGCM])
Observed state Occurrences Expectation Interpretation
0 276 ACCEPTABLE_INTERESTING !!!!
1 59,999,454 ACCEPTABLE ok
4 28,530,221 ACCEPTABLE ok
[OK] com.zhazha.ConcurrencyTest
(JVM args: [-XX:-TieredCompilation, -XX:+UnlockDiagnosticVMOptions, -XX:+StressLCM, -XX:+StressGCM])
Observed state Occurrences Expectation Interpretation
0 104 ACCEPTABLE_INTERESTING !!!!
1 67,885,202 ACCEPTABLE ok
4 29,954,445 ACCEPTABLE ok
[OK] com.zhazha.ConcurrencyTest
(JVM args: [-XX:-TieredCompilation])
Observed state Occurrences Expectation Interpretation
0 3,889 ACCEPTABLE_INTERESTING !!!!
1 67,195,281 ACCEPTABLE ok
4 32,530,071 ACCEPTABLE ok
[OK] com.zhazha.ConcurrencyTest
(JVM args: [])
Observed state Occurrences Expectation Interpretation
0 2,099 ACCEPTABLE_INTERESTING !!!!
1 85,847,970 ACCEPTABLE ok
4 26,918,422 ACCEPTABLE ok
*** All remaining tests
Tests that do not fall into any of the previous categories.
2 matching test results.
[OK] com.zhazha.ConcurrencyTest
(JVM args: [-XX:TieredStopAtLevel=1])
Observed state Occurrences Expectation Interpretation
0 0 ACCEPTABLE_INTERESTING !!!!
1 49,062,963 ACCEPTABLE ok
4 22,903,528 ACCEPTABLE ok
[OK] com.zhazha.ConcurrencyTest
(JVM args: [-Xint])
Observed state Occurrences Expectation Interpretation
0 0 ACCEPTABLE_INTERESTING !!!!
1 2,089,147 ACCEPTABLE ok
4 1,663,694 ACCEPTABLE ok
经过测试发现上面确实存在指令重排序
0 3,889 ACCEPTABLE_INTERESTING !!!!
0 2,099 ACCEPTABLE_INTERESTING !!!!
但是加上了 volatile 之后这种情况消失了
volatile boolean ready = false;
double-checked-locking双重检测原则
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (null == instance) {
synchronized (SingletonLazy.class) {
if (null == instance) {
instance = new SingletonLazy();
}
}
}
return instance;
}
0: getstatic #2 // Field INSTANCE:Lcom/zhazha/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class com/zhazha/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcom/zhazha/n5/Singleton;
14: ifnonnull 27
17: new #3 // class com/zhazha/n5/Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field INSTANCE:Lcom/zhazha/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcom/zhazha/n5/Singleton;
40: areturn
其中
- 17 表示创建对象,将对象引用入栈 // new Singleton
- 20 表示复制一份对象引用 // 引用地址
- 21 表示利用一个对象引用,调用构造方法
- 24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取
INSTANCE 变量的值
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初
始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效
加上volatile之后的指令集虽然还是看不出问题, 但是在实际作用中还是体现出了效果
为什么synchronized能够保证有序性却无法保证指令重排序的顺序?
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
虽然说synchronized是有序的, 但不是真正的有序, 它只不过是保证了在临界区只有一个线程运行, 所以即使发生了指令重排, 对于这个块来说是不影响的, 但是不在这个块的呢???
借助双重验证方式的单例详解区别
// 如果这个字段没有volatile关键字则会出现指令重排序
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (null == instance) { // ①
synchronized (SingletonLazy.class) { // 线程2 阻塞
if (null == instance) {
instance = new SingletonLazy(); // 线程1 运行中 ...
}
}
}
return instance;
}
如果instance不添加volatile则会出现这样一个过程:
- jvm分配内存
- 初始化内存对象(引用内存, 对象的内存, 堆)
- 栈变量指向引用对象内存
但是这个过程如果重排序成这样对于一个线程来说结果也是一样的
- jvm分配内存
- 栈变量指向引用对象内存
- 初始化内存对象(引用内存, 对象的内存, 堆)
满足所谓的 as-if-serial(说白了就是不管怎么重排, 只要不影响这个线程运行的结果就行)
那么上面代码线程2阻塞的位置, 就会出现不同的效果了,
如果是第一种方式, 未经过指令重排序, 则结果是正常的, 返回有非null的instance,
但是如果是第二种方式, 先初始化了栈变量后, 线程2发现这个栈变量发生了变化, 直接返回这个栈变量, 但是此时栈变量对应的对象堆内存还未初始化, 此时将会发生错误(这种情况发生的比较少, 我在实测中没发现问题, 但是synchronized是不能处理指令重排序的, 可能jvm做了优化吧)
当然要让sync保证指令重排的方法其实也有, 那就是让共享变量全部都在代码块中
总结下来就是: sync可以保证原子性, 可见性和有序性
先行发生原则(happens-before)
是什么?
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
详细内容
(1) 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(() -> {
synchronized (m) {
x = 10;
}
},"t1").start();
new Thread(() -> {
synchronized (m) {
System.out.println(x);
}
},"t2").start();
(2) 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(() -> {
x = 10;
}, "t1").start();
new Thread(() -> {
System.out.println(x);
}, "t2").start();
(3) 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(() -> {
System.out.println(x);
},"t2").start();
(4) 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(() -> {
x = 10;
}, "t1");
t1.start();
t1.join();
System.out.println(x);
(5) 线程打断前的写, 对线程打断后的其他线程可见
static int x;
public static void main (String[]args){
Thread t2 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
}, "t2");
t2.start();
new Thread(() -> {
sleep(1);
x = 10;
t2.interrupt();
}, "t1").start();
while (!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
(6) 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
(7) 具有传递性,线程1对x的修改在线程2中x是可见的, 并且这种可见也包括y变量
volatile static int x;
static int y;
new Thread(() -> {
y = 10;
x = 20; // x 写前面的所有变量都是有序的
}, "t1").start();
new Thread(() -> {
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
}, "t2").start();
对加锁范围的理解
public class TestVolatile {
volatile boolean initialized = false;
void init() {
if (initialized) {
return;
}
doInit();
initialized = true;
}
private void doInit() {
}
}
确认 initialized 是共享变量, 并且 doInit 方法只能被执行一次
我们围绕着共享变量来进行加锁
首先发现共享变量存在读写操作所以我们围绕着读写加上锁, 但是要上this锁还是Class锁, 根据出题条件判断, 这里我们上this锁, 保证一个 TestVolatile 对象只能调用一次 doInit 方法
volatile boolean initialized = false;
void init() {
synchronized (this) {
if (initialized) {
return;
}
doInit();
initialized = true;
}
}
这里发现 volatile 其实可加可不加
单例模式
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题
实现1:
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
private Singleton() {
}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
实现2:(推荐)
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
实现3:
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
实现4:DCL
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
实现5: (推荐)
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
无锁 --- 乐观锁(非阻塞)
共享模式之无锁
- CAS 与 volatile
- 原子整数
- 原子引用
- 原子累加器
- Unsafe
CAS无锁方案
CAS无锁主要看重三个变量: 预估值 + 修改值 + 主存值
预估值: 主要用来判断是否等于主存值, 是则将修改值修改到主存值中
修改值: 即将需要修改的新值, 修改到主存中
主存值: 实际值
class AccountSafe implements Account {
// 主存值, 其内部的 value 使用的就是 volatile
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
// 希望值
int prev = balance.get();
// 修改值
int next = prev - amount;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
public static void main(String[] args) {
Account.demo(new AccountSafe(10000));
}
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
**注意: **
- 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交
换】的原子性 - 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再
开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子
的。
CAS 与 volatile
为什么需要volatile? 其实很简单, 就是借助 volatile 关键字获取主存中的值, 如果使用, 则获取的值可能不是主存中的值
public class ConcurrentcyAtomicInteger {
// 请求总数
public static final int clientTotal = 5000;
// 高并发数量
public static final int threadTotal = 200;
public static volatile int count = 0;
// public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(threadTotal);
CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
add();
semaphore.release();
});
countDownLatch.countDown();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
System.out.println("count = " + count);
}
private synchronized static void add() {
count++;
}
}
volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
为什么需要无锁???
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时
候,发生上下文切换,进入阻塞。打个比喻 - 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,
等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大 - 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑
道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还
是会导致上下文切换
总结:
上下文切换代价比线程循环代价来得大, 所以选择了CAS
CAS合适竞争条件不激烈的环境, 如果激烈的话, 会出现很多线程 '死循环' 的情况
原子整数
AtomicInteger
AtomicBoolean
AtomicLong
以 AtomicInteger 为例
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
原子引用
为什么需要原子引用类型?
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
有如下方法
public interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
不安全实现
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
安全实现-使用锁
class DecimalAccountSafeLock implements DecimalAccount {
private final Object lock = new Object();
BigDecimal balance;
public DecimalAccountSafeLock(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
synchronized (lock) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
}
安全实现-使用 CAS
class DecimalAccountSafeCas implements DecimalAccount {
AtomicReference ref;
public DecimalAccountSafeCas(BigDecimal balance) {
ref = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return ref.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = ref.get();
BigDecimal next = prev.subtract(amount);
if (ref.compareAndSet(prev, next)) {
break;
}
}
}
}
测试代码:
DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000")));
DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal("10000")));
DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal("10000")));
运行结果
4310 cost: 425 ms
0 cost: 285 ms
0 cost: 274 ms
ABA 问题及解决
ABA是什么?
cas存在一种情况就是在对比过程前, 主存的值被瞬间做了两次修改, 恰巧第二次修改的值和修改前的值相同, 这样导致回到对比过程时, cas无法发现它已经被修改过了
AtomicStampedReference
AtomicMarkableReference
@Slf4j
public class AtomicStampedReferenceDemo {
static AtomicStampedReference ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws Exception {
log.debug("ref = {}", ref.getReference());
String prev = ref.getReference();
int stamp = ref.getStamp();
other();
Sleeper.sleep(1000);
log.debug("ref A -> B {}", ref.compareAndSet("A", "B", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
int stamp = ref.getStamp();
log.debug("ref A -> B {}", ref.compareAndSet("A", "B", stamp, ++stamp));
}).start();
new Thread(() -> {
int stamp = ref.getStamp();
log.debug("ref B -> A {}", ref.compareAndSet("B", "A", stamp, ++stamp));
}).start();
}
}
@Slf4j
public class AtomicMarkableReferenceDemo {
public static void main(String[] args) throws Exception {
GarbageBag garbageBag = new GarbageBag("装满了垃圾的垃圾袋");
AtomicMarkableReference reference = new AtomicMarkableReference<>(garbageBag, true);
log.debug("主线程 start...");
GarbageBag prev = reference.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
garbageBag.setDesc("空垃圾袋");
while (!reference.compareAndSet(garbageBag, garbageBag, true, false)) {
}
log.debug(garbageBag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = reference.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(reference.getReference().toString());
}
}
原子数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
@Slf4j
public class AtomicIntegerArrayDemo {
public static void main(String[] args) throws Exception {
demo(() -> new int[10], ints -> ints.length, (ints, integer) -> ints[integer]++, ints -> System.out.println(Arrays.toString(ints)));
demo(() -> new AtomicIntegerArray(10), AtomicIntegerArray::length, AtomicIntegerArray::getAndIncrement, System.out::println);
}
private static void demo(Supplier arraySupplier, Function lengthFun, BiConsumer putConsumer, Consumer printConsumer) {
List ts = new ArrayList<>();
T array = arraySupplier.get();
Integer len = lengthFun.apply(array);
for (int i = 0; i < len; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % len);
}
}));
}
ts.forEach(Thread::start);
ts.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
printConsumer.accept(array);
}
}
字段更新器
- AtomicReferenceFieldUpdater // 域 字段
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
@Slf4j
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) throws Exception {
Student student = new Student("zhazha");
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
System.err.println(updater.compareAndSet(student, "zhazha", "heihei"));
}
static class Student {
private volatile String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + '}';
}
public Student(String name) {
this.name = name;
}
}
}
原子累加器
累加器性能比较
@Slf4j
public class AtomicLongLongAdderDemo {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
AtomicLongLongAdderDemo.demo(() -> new AtomicLong(0), AtomicLong::getAndIncrement);
}
System.out.println("---------------------------------------------------------------");
for (int i = 0; i < 5; i++) {
AtomicLongLongAdderDemo.demo(LongAdder::new, LongAdder::increment);
}
}
private static void demo(Supplier adderSupplier, Consumer action) {
T adder = adderSupplier.get();
List ts = new ArrayList<>();
Instant start = Instant.now();
// 四核系统, 所以我们这里使用4条线程 突破到 cas 最快的速度
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(Thread::start);
ts.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println(adder + " cost: " + duration.toMillis());
}
}
2000000 cost: 34
2000000 cost: 23
2000000 cost: 33
2000000 cost: 26
2000000 cost: 39
---------------------------------------------------------------
2000000 cost: 50
2000000 cost: 25
2000000 cost: 13
2000000 cost: 16
2000000 cost: 20
毫无疑问, LongAdder效率铁定比AtomicLong快
在存在高度竞争的条件下,LongAdder的性能会远远好于AtomicLong,不过会消耗更多空间。高度竞争当然是指在多线程条件下。(cas推荐在cpu多少核就创建多少来实现累加)
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
LongAdder底层原理
@Slf4j
public class LockCasDemo {
public static void main(String[] args) throws Exception {
LockCas lockCas = new LockCas();
new Thread(() -> {
log.debug("{}运行了", Thread.currentThread().getName());
try {
lockCas.lock();
log.debug("{} lock...", Thread.currentThread().getName());
Sleeper.sleep(1000);
} finally {
lockCas.unlock();
}
}).start();
new Thread(() -> {
log.debug("{}运行了", Thread.currentThread().getName());
try {
lockCas.lock();
log.debug("{} lock...", Thread.currentThread().getName());
} finally {
lockCas.unlock();
}
}).start();
}
/**
* 生产环境最好别这样用
*/
static class LockCas {
private AtomicInteger state = new AtomicInteger(0);
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
log.debug("unlock ...");
state.set(0);
}
}
}
18:51:37.250 [Thread-1] DEBUG c.lockCasDemo - Thread-1运行了
18:51:37.275 [Thread-1] DEBUG c.lockCasDemo - Thread-1 lock...
18:51:37.275 [Thread-1] DEBUG c.lockCasDemo - unlock ...
18:51:37.250 [Thread-0] DEBUG c.lockCasDemo - Thread-0运行了
18:51:37.276 [Thread-0] DEBUG c.lockCasDemo - Thread-0 lock...
18:51:38.279 [Thread-0] DEBUG c.lockCasDemo - unlock ...
伪共享和缓存行
伪共享问题的表现是:并发的修改在一个缓存行中的多个独立变量,看起来是并发执行的,但实际在CPU处理的时候,是串行执行的,并发的性能大打折扣。
伪共享的原因就是 CPU 在 Invalid
的时候,是会直接废除一行的!
如果 两个变量 (a,b)
同时在一个 Cache Line
中,处理器A
修改了变量a
,那么处理器B中,这个 CacheLine
失效了,这个时候如果处理器B
修改了变量b
的话,就必须先提交处理器A
的缓存,然后处理器B
再去主存中读取数据!这样就出现了问题,a
和b
在两个处理器上被修改,本应该是一个并行的操作,但是由于缓存一致性,却成为了串行!这样会严重的影响并发的性能!
// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要代码
}
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
- Core-0 要修改 Cell[0]
- Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加
Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的
padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
累加主要调用下面的方法
源码分析:
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
但是缺点也能多
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
它不能保证sum在高并发情况下的正确性
Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
public class UnsafeAccessor {
private static final Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Unsafe getUNSAFE() {
return unsafe;
}
}
Unsafe CAS 操作
@Slf4j
public class GetUnsafeObjectDemo {
public static void main(String[] args) throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
unsafe.compareAndSwapInt(student, idOffset, 0, 1);
unsafe.compareAndSwapObject(student, nameOffset, null, "zhazha");
System.out.println(student);
}
@Data
static class Student {
private int id;
private String name;
}
}
使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现
public interface Account {
int getBalance();
void withdraw(int amount);
static void doSomething(Account account) {
List ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("余额: " + account.getBalance());
}
}
public class MyAtomicIntegerDemo {
public static void main(String[] args) {
Account.doSomething(new MyAtomicInteger(10000));
}
}
class MyAtomicInteger implements Account {
private volatile int value;
private final static long valueOffset;
private final static Unsafe UNSAFE;
static {
UNSAFE = UnsafeAccessor.getUNSAFE();
try {
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public MyAtomicInteger(int value) {
this.value = value;
}
@Override
public int getBalance() {
return value;
}
@Override
public void withdraw(int amount) {
while (true) {
int prev = value;
int next = prev - amount;
if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
break;
}
}
}
}
变量的线程安全分析
成员变量和静态成员变量是否线程安全????
如果它没有共享, 则线程安全
如果它共享了, 又根据它是否存在修改分为两类
- 只有对它有读操作, 则线程安全
- 如果有读写操作, 则这段代码是临界区, 需要考虑线程安全问题
局部变量是否线程安全???
局部变量是线程安全的(局部变量是只有线程自己有的资源, 不共享就没有线程安全问题)
但是局部变量的引用对象则未必
- 如果该对象没有逃离方法的作用域是线程安全的
- 如果逃出了方法的作用域, 则线程不安全
日期转换的问题
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的
@Slf4j(topic = "c.DateFormatSafeDemo")
public class DateFormatSafeDemo {
public static void main(String[] args) {
// 线程不安全
unsafeFunc();
}
private static void dateTimeFormatterFunc() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor parse = formatter.parse("2020-10-10");
log.debug("parse = {}", parse);
}).start();
}
}
private static void unsafeFunc() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", dateFormat.parse("1999-02-02"));
} catch (ParseException e) {
log.debug("{}", e);
}
}).start();
}
}
}
具体分析是否线程安全
上面那个list是线程不安全的, 首先满足共享资源, 其次这个list存在 add 和 remove
上面的list是线程安全的, 多个线程调用method1方法, 但是这个方法的list在每次调用的时候都是新的list对象, 而且method2和3方法都是private的, 外部多线程只能调用method1方法, 所以它是线程安全的
这个局部变量的list就是线程不安全的, 即时它是局部变量, 但是它传递搞了其他public修饰的方法上, 如果这两个public的方法被多线程调用则, list也将不安全, 简单点就是局部变量的引用暴露给其他线程
常见的线程安全类
但是他们的组合却不是线程安全的, 虽然他们单个都是原子性的, 但是组合使用就是两个原子性操作是不安全的
比如这样就不是线程安全的
不可变类线程安全性
为什么String是线程安全的???
public final class String
implements java.io.Serializable, Comparable, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
/** Cache the hash code for the string */
private int hash; // Default to 0
查看字段 coder是线程安全的, hash它会在对象头生成的时候生成出来, 所以也是线程安全的, 现在就value数组不敢保证线程安全性, 即使它被 volatile修饰(volatile只能修饰引用变量不能修饰引用变量所指向的内存空间)
final 的使用
发现该类、类中所有属性都是 final 的
属性用 final 修饰保证了该属性是只读的,不能修改
类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是
如何实现的,就以 substring 为例:
// beginIndex 局部变量 --- 线程安全
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// length线程安全, String是常量的, 所以他的长度也是固定的
int subLen = length() - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
if (beginIndex == 0) {
return this;
}
// 根据判断, 这个函数也是线程安全的, 不管false还是true, 都是new一个新的String对象
return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
: StringUTF16.newString(value, beginIndex, subLen);
}
我们现在要关注的是是否修改了value数组
public static String newString(byte[] val, int index, int len) {
return new String(Arrays.copyOfRange(val, index, index + len),
LATIN1);
}
发现其内部还是没有对value数组存在修改, 只有新建一个新的String对象, 把引用地址丢给string的引用变量, 前面的String对象直接丢弃
构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
final底层原理
public class TestFinal {
final int a = 20;
}
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: return
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到
它的值时不会出现为 0 的情况
无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这
种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】
实例分析
前提: 如果doGet方法中存在对上述共享资源的改变
线程不安全; 线程安全; 线程安全; 线程不安全; 线程不安全
线程不安全, MyServlet 方法是单例模式, 所以 userService 对象会被多个线程共享(产生了共享资源)但如果不追究userService对象的源码话, 那么它暂时是线程安全的, 而对象中的 count 对象也是线程共享资源, 并且 count 还存在修改(这个修改本身就是线程不安全的), 所以它也是线程不安全的
线程不安全, MyAspect 是单例的, 所以存在多线程场景, start 是共享资源并且 start 对象存在修改而且每次结果都不一样
线程安全; 线程安全; 线程安全;
首先MyServlet是单例, 所以存在多线程, userService 是共享资源, 但是没有修改, 找 userDao 是共享资源, 但是没有修改, 发现 UserDaoImpl 是线程安全的, 里面全部都是局部变量
从上到下: 线程安全, 线程安全, 线程不安全, 所以前面的全部变成变成不安全
答案: 线程不安全; 线程不安全; 线程不安全
详解: 首先 MyServlet 是单例, 存在多线程环境, user Service是线程共享资源, 但是没有修改, 所以是安全的, userDao是线程共享资源, 不考虑底层的话, 是线程安全的, 但是考虑底层的话. 那个 conn 是线程不安全的, 首先 conn 是共享变量, conn 存在变化并且每次修改都不一样
不存在线程安全问题; 一个线程一个 userDao
上面这种用法就和我们的 String 类为啥要使用 final 的原因了
抽象方法的实现有可能是线程不安全的, 这个 foo 方法可能被多个线程运行, 而参数 sdf 是线程不安全的, 不能保证一个线程, 一个 sdf 对象, 所以在这个例子中 sdf 是共享资源, sdf 可能被其中一个子类进行了修改, 所以无法保证 foo 的 sdf 是绝对线程安全的
模拟售票系统出现的线程安全问题
@Slf4j
public class exerciseSell {
@Test
public void test() throws Exception {
// 模拟存在多少张票
TicketWindow ticketWindow = new TicketWindow(2000);
CountDownLatch latch = new CountDownLatch(2000);
// 记录每次售出多少张票
List amountList = new Vector<>();
// List amountList = new ArrayList<>(); // 线程不安全的
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
int amount = ticketWindow.sell(randomAmount());
amountList.add(amount);
latch.countDown();
});
thread.start();
}
latch.await();
log.debug("余票: {}", ticketWindow.getCount());
log.debug("卖出的票数: {}", amountList.stream().mapToInt(value -> value).sum());
}
private Random random = new Random();
private int randomAmount() {
return random.nextInt(5) + 1;
}
}
/**
* 售票窗口
*/
class TicketWindow {
// private int count;
// 防止指令重排
// (1) 共享资源
private volatile int count;
TicketWindow(int count) {
this.count = count;
}
int getCount() {
return count;
}
/**
* 售票
*
* @param amount
*
* @return
*/
synchronized int sell(int amount) {
// int sell(int amount) { // 线程不安全
// (2) 读写共享资源
if (this.count >= amount) {
this.count -= amount;
return amount;
}
else {
return 0;
}
}
}
多线程行使转账的案例
@Slf4j
public class ExerciseTransfer {
@Test
public void test() throws Exception {
CountDownLatch latch = new CountDownLatch(200);
Account a = new Account(10000);
Account b = new Account(10000);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
a.transfer(b, randomAmount());
latch.countDown();
}).start();
}
for (int i = 0; i < 100; i++) {
new Thread(() -> {
b.transfer(a, randomAmount());
latch.countDown();
}).start();
}
latch.await();
log.debug("total: {}", a.getMoney() + b.getMoney());
}
private static Random random = new Random();
private static int randomAmount() {
return random.nextInt(100) + 1;
}
}
/**
* 账户
*/
class Account {
// 防止指令重排问题
private volatile int money;
int getMoney() {
return money;
}
private void setMoney(int money) {
this.money = money;
}
Account(int money) {
this.money = money;
}
/**
* 转账
*
* @param target
* @param money
*/
void transfer(Account target, int money) {
if (this.money >= money) {
// 对象锁是类名
synchronized (Account.class) {
if (this.money >= money) {
this.setMoney(this.getMoney() - money);
target.setMoney(target.getMoney() + money);
}
}
}
}
}
线程池
自定义线程池
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义线程池
*/
@Slf4j(topic = "c.TestMyPool")
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(2, 10, 1000, TimeUnit.MILLISECONDS, (queue, task) -> {
while (!queue.tryPut(task, 1000, TimeUnit.MILLISECONDS)) {
log.debug("拒绝策略重试添加任务到队列");
}
});
for (int i = 0; i < 1000; i++) {
int finalI = i;
pool.execute(() -> log.debug("任务执行...{}", finalI));
}
}
}
/**
* 拒绝策略
*
* @param
*/
@FunctionalInterface
interface MyRejectPolicy {
void reject(MyBlockingQueue queue, T task);
}
/**
* 线程池
*/
class MyThreadPool {
// 工作线程池
private final HashSet workers = new HashSet<>();
// 线程核心大小
private long coreSize;
// 阻塞队列
private final MyBlockingQueue taskQueue;
// 时间
private long timeout;
// 时间单位
private final TimeUnit timeUnit;
// 拒绝策略
private final MyRejectPolicy rejectPolicy;
public MyThreadPool(long coreSize, long capcity, long timeout, TimeUnit timeUnit, MyRejectPolicy rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectPolicy = rejectPolicy;
taskQueue = new MyBlockingQueue<>(capcity);
}
public void execute(Runnable task) {
synchronized (workers) {
if (workers.size() >= coreSize) {
taskQueue.tryPut(this.rejectPolicy, task);
}
else {
Worker worker = new Worker(task);
workers.add(worker);
worker.start();
}
}
}
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 这里的代码很关键,是个循环,而且第一次执行完毕新的任务后,旧的任务还能够设置到task中,再次运行
while (null != task || (task = taskQueue.tryTask(timeout, timeUnit)) != null) {
try {
task.run();
} finally {
task = null;
}
}
synchronized (workers) {
workers.remove(this);
}
}
}
}
/**
* 阻塞任务
*/
class MyBlockingQueue {
/**
* 字段:
* 1. 需要存访任务的队列
* 2. 队列大小
* 方法:
* 获取任务 删除任务
* 添加任务
* 获取队列大小
*/
/**
* 任务的队列
*/
private Deque queue = new ArrayDeque<>();
/**
* 队列大小
*/
private long capcity;
/**
* 锁
*/
private final ReentrantLock lock = new ReentrantLock();
/**
* 队列空
*/
private Condition emptyWwaitSet = lock.newCondition();
/**
* 队列满
*/
private Condition fullWwaitSet = lock.newCondition();
public MyBlockingQueue(long capcity) {
this.capcity = capcity;
}
/**
* 获取任务
*
* @return
*/
public T task() {
try {
lock.lock();
while (queue.isEmpty()) {
// 等待
try {
emptyWwaitSet.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWwaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
/**
* 尝试获取任务
*
* @param timeout
* @param unit
*
* @return
*/
public T tryTask(long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);
try {
lock.lock();
while (queue.isEmpty()) {
// 等待
try {
if (nanos <= 0) {
return null;
}
nanos = emptyWwaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWwaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
/**
* 添加任务
*
* @param task
*/
public void put(T task) {
try {
lock.lock();
while (queue.size() >= this.capcity) {
// 满了, 不需要生产了
try {
fullWwaitSet.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(task);
emptyWwaitSet.signal();
} finally {
lock.unlock();
}
}
/**
* 带时间的任务添加
*
* @param task
* @param timeout
* @param unit
*
* @return
*/
public boolean tryPut(T task, long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);
try {
lock.lock();
while (queue.size() >= this.capcity) {
// 满了, 不需要生产了
try {
if (nanos <= 0) {
return false;
}
nanos = fullWwaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(task);
emptyWwaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
/**
* 任务添加, 带拒绝策略
*
* @param rejectPolicy
* @param task
*/
public void tryPut(MyRejectPolicy rejectPolicy, T task) {
try {
lock.lock();
if (queue.size() >= capcity) {
rejectPolicy.reject(this, task);
}
else {
queue.addLast(task);
emptyWwaitSet.signal();
}
} finally {
lock.unlock();
}
}
/**
* 获取大小
*
* @return
*/
public long size() {
try {
lock.lock();
return this.queue.size();
} finally {
lock.unlock();
}
}
}
扩展自定义链接池
@Slf4j(topic = "c.ConnectionDemo")
public class ConnectionDemo {
public static void main(String[] args) {
Pool.INSTANCE.init(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection connection = null;
try {
connection = Pool.INSTANCE.borrow();
} finally {
Pool.INSTANCE.free(connection);
}
}).start();
}
}
}
/**
* 自定义线程池
*/
@Slf4j(topic = "c.Pool")
enum Pool {
INSTANCE(10);
private int poolSize;
private Connection[] connections;
private AtomicIntegerArray states;
Pool(int poolSize) {
init(poolSize);
}
/**
* 这个函数都是 new 所以是线程安全的, 它可能被无数次的new出来,但是最后一次new的值才是最终我们用的到的, 前面无数次的new最后都会被gc回收
* @param poolSize
*/
public void init(int poolSize) {
this.poolSize = poolSize;
states = new AtomicIntegerArray(new int[poolSize]);
connections = new Connection[poolSize];
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + i);
}
}
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("thread {} get connection {}", Thread.currentThread().getName(), connections[i]);
return connections[i];
}
}
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.notifyAll();
}
}
}
}
public void free(Connection connection) {
Optional opt = Optional.ofNullable(connection);
for (int i = 0; i < poolSize; i++) {
if (connections[i].equals(opt.orElse(new MockConnection("...")))) {
states.set(i, 0);
log.debug("thread {} free connection {}", Thread.currentThread().getName(), connections[i]);
synchronized (this) {
this.notifyAll();
}
}
}
}
}
/**
* 自定义connection
*/
class MockConnection implements Connection {
private String name;
@Override
public String toString() {
return "MockConnection{" + "name='" + name + '\'' + '}';
}
public MockConnection(String name) {
this.name = name;
}
// ...
}
ThreadPoolExecutor
线程池的组成主要是
核心线程 -- 核心线程是线程池遇到任务默认让核心线程执行(corePoolSize)
急救线程 -- 当核心线程数量不足, 我们救要使用上急救线程(maximumPoolSize - corePoolSize) 但是他存在keepAliveTime 保持时间, 如果急救线程空闲时间超过, 则线程消失
阻塞队列 -- 当任务多于maximumPoolSize , 线程池中的线程 全忙 , 多出任务丢到阻塞队列
任务 -- 线程执行的任务
线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量5
状态名 | 高 3 位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | |
SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
TERMINATED | 011 | - | - | 终结状态 |
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 创建线程工厂类, 可以在类内部主动创建一个自己想要的线程属性, 比如如下几个属性
异常处理, 优先级, 是否守护者线程, 名字, 加载器
- handler 拒绝策略
工作方式:
-
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
-
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排
队,直到有空闲的线程。 -
如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线
程来救急。 -
如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它
著名框架也提供了实现(1) AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
(2) CallerRunsPolicy 让调用者运行任务
(3) DiscardPolicy 放弃本次任务
(4) DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
(5) Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
(6) Netty 的实现,是创建一个新线程来执行任务(7) ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
(8) PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
-
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。
根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
固定线程池 -- FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
核心线程数 == 最大线程数 ==> 无急救线程 ===> 无超时时间
队列无界, 随意存放
评价
合适明确数量的耗时任务
急救线程池 -- CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
核心数 == 0 ==> 急救线程数无限创建(最大Integer.MAX_VALUE个线程同时存在)
队列无界
空闲保留时间60s
评价
合适任务多, 单任务耗时少
单例线程池-- SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
永远保证线程池中只有一个线程, 如果意外终止了这个线程, 这会抛弃掉这个线程, 再创建一个新的线程
队列无界
使用包装设计模式, FinalizableDelegatedExecutorService, 可以保证这个ThreadPoolExecutor类内部的一些类似set的方法不会被特殊方法直接调用修改, 也无法被强转成ThreadPoolExecutor, 防止内部的方法暴露
任务调度线程池 -- ScheduledThreadPool
在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
Timer定时器的缺陷
/**
* Timer的缺点: 一旦出现异常, 这无法再次使用
*/
private static void func1() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.debug("task 1");
Sleeper.sleep(2);
}
}, 1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
}, 1000);
log.debug("start ... ");
}
调度器的使用
/**
* 调度器使用
*/
private static void func2() {
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(1);
service.schedule(() -> {
log.debug("task 1");
int i = 1 / 0;
}, 1000, TimeUnit.MILLISECONDS);
service.schedule(() -> {
log.debug("task 2");
}, 1000, TimeUnit.MILLISECONDS);
}
定时器周期执行任务
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
/**
* 相隔 2 秒
* 程序未执行完毕, 定时间早就开始定时
* 时间可能按照周期算, 也可以按照程序执行时间算
* 19:32:08 FixedRate running
* 19:32:10 FixedRate running
* 19:32:12 FixedRate running
*/
threadPool.scheduleAtFixedRate(() -> {
log.debug("FixedRate running");
Sleeper.sleep(20_00);
}, 1, 1, TimeUnit.SECONDS);
/**
* 相隔 3 秒, 这里相隔 2 + 1 == 3
* 程序执行完毕, 定时器才开始定时
* 19:32:52 FixedDelay running
* 19:32:55 FixedDelay running
* 19:32:58 FixedDelay running
*/
// threadPool.scheduleWithFixedDelay(() -> {
// log.debug("FixedDelay running");
// Sleeper.sleep(2000);
// }, 1, 1, TimeUnit.SECONDS);
}
评价
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务。
线程池正确处理执行任务异常
直接在代码内部捕捉异常
threadPool.schedule(() -> {
try {
log.debug("task 1");
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}, 1000, TimeUnit.MILLISECONDS);
使用future的get方法获取异常
此方法适用于submit方法提交任务的方法
Future future = threadPool.submit(() -> {
log.debug("task 1");
int i = 1 / 0;
return true;
});
try {
if (future.get()) {
System.out.println("true");
}
else {
System.out.println("false");
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
自定义异常处理方案
此方法适用于线程池execute方法的使用, 如果使用submit函数的话,是无效的
详解:submit和execute方法各区别
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("-----------execption-----------");
System.out.println("线程信息: " + t.toString());
System.out.println("异常信息: " + e.getMessage());
}
}
ExecutorService threadPool = Executors.newCachedThreadPool(r -> {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return thread;
});
threadPool.execute(() -> {
log.debug("zhazha");
int i = 1 / 0; // 代码在这里就会报错
});
-----------execption-----------
线程信息: Thread[Thread-0,5,main]
异常信息: / by zero
不过上面这种方式优缺点,发现线程池默认的 defaultfactory 内部使用了很多的其他功能,不仅仅是setUncaughtExceptionHandler,还有线程名字等等
所以完整代码应该是这样
static class MyThreadFacorty implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
MyThreadFacorty() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "pool-" + POOL_NUMBER.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return t;
}
}
static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("-----------execption-----------");
System.out.println("线程信息: " + t.toString());
System.out.println("异常信息: " + e.getMessage());
}
}
public void test02() throws Exception {
CountDownLatch latch = new CountDownLatch(1); // 这是测试方案 防止主线程直接就退出
ExecutorService threadPool = Executors.newCachedThreadPool(new MyThreadFacorty());
threadPool.execute(() -> {
log.debug("zhazha");
int i = 1 / 0;
latch.countDown();
});
latch.await();
}
submit和execute方法各区别
Future> submit(Runnable task);
void execute(Runnable command);
他们都是提交任务到线程池的方法,但是其中的区别还是有的,其中最重要的区别在于:
submit方法的异常如果没有使用返回值Future的get方法异常无法被捕获, 即使使用前面的
thread.setUncaughtExceptionHandler(handler);
方案想要捕获异常也是无效的,上面这种方法只适用于execute方法当你想要捕获异常的时候使用
其中底层原理很简单
execute方法底层它把异常抛给线程池,只要我们使用thread.setUncaughtExceptionHandler设置自己的异常捕获机制就能够捕获出来
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
但是submit底层原理就不是这样了
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
他会把异常捕获放到它的outcome成员变量中
而要想知道是否出现异常必须使用Future的get方法体现出来
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
其中的 report 方法就是把异常抛出的
@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
// 把捕获的异常放入 x变量判断是否是异常, 这个 s 参数就是是否是异常的判断
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
// 抛出异常
throw new ExecutionException((Throwable)x);
}
提交任务
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
Future submit(Callable task);
// 提交 tasks 中所有任务
List> invokeAll(Collection extends Callable> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
List> invokeAll(Collection extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
T invokeAny(Collection extends Callable> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
T invokeAny(Collection extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
关闭线程池
shutdown
/*
线程池状态变为 SHUTDOWN
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态
advanceRunState(SHUTDOWN);
// 仅会打断空闲线程
interruptIdleWorkers();
onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
tryTerminate();
}
shutdownNow
/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List shutdownNow();
public List shutdownNow() {
List tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态
advanceRunState(STOP);
// 打断所有线程
interruptWorkers();
// 获取队列中剩余任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 尝试终结
tryTerminate();
return tasks;
}
其它方法
// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();
// 线程池状态是否是 TERMINATED
boolean isTerminated();
// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事
情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
异步模式之工作线程
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式
例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)
注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率
例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工
线程池饥饿
固定大小线程池会有饥饿现象
例如: 饭店里只有两个人, 这两个人既能服务客人也能做饭, 而且每个人只能服务一个客人 , 突然店铺来个两个客人, 饭店两个人都去服务各自选中的客人, 现在没有人做饭了, 程序在这里无法再次运行下去了, 也就是饥饿了
@Slf4j(topic = "c.TestHungerDemo")
public class TestHungerDemo {
private static final List MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
private static final Random RANDOM = new Random();
private static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(() -> {
log.debug("处理点餐服务....");
Future future = pool.submit(() -> {
log.debug("做菜中....");
return cooking();
});
try {
log.debug("菜做完了. 上菜{}", future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
pool.execute(() -> {
log.debug("处理点餐服务....");
Future future = pool.submit(() -> {
log.debug("做菜中....");
return cooking();
});
try {
log.debug("菜做完了. 上菜{}", future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
// 这句话是不可以的, 会报错, 拒绝运行错误, 说白了就是在执行 Future future = pool.submit(() -> {
// 这一行的时候 pool.shutdown(); 已经执行完毕了, 这个时候再去执行 pool.submit 方法就会出现错误
// pool.shutdown();
}
}
15:07:17.930 [pool-1-thread-1] DEBUG c.TestHungerDemo - 处理点餐服务....
15:07:17.930 [pool-1-thread-2] DEBUG c.TestHungerDemo - 处理点餐服务....
上面的代码就存在饥饿问题, 没有线程去煮饭了
解决方案
不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
log.debug("处理点餐服务....");
Future future = cookPool.submit(() -> {
log.debug("做菜中....");
return cooking();
});
try {
log.debug("菜做完了. 上菜{}", future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
log.debug("处理点餐服务....");
Future future = cookPool.submit(() -> {
log.debug("做菜中....");
return cooking();
});
try {
log.debug("菜做完了. 上菜{}", future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
创建多少线程池合适
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存
CPU 密集型运算
通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费
I/O 密集型运算
CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。
经验公式如下
线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8
例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8
例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40
tomcat线程池
- LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor 只负责【接收新的 socket 连接】
- Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
-
如果总线程数达到 maximumPoolSize
这时不会立刻抛 RejectedExecutionException 异常
而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
源码 tomcat-7.0.42
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue) super.getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.");
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
Thread.interrupted();
throw new RejectedExecutionException(x);
}
}
else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
TaskQueue.java
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if (parent.isShutdown())
throw new RejectedExecutionException("Executor not running, can't force a command into the queue");
return super.offer(o, timeout, unit); //forces the item onto the queue, to be used if the task
is rejected
}
Connector 配置
Executor 线程配置
jdk线程池源码分析
线程池在Exectors中存在几个线程池方案,不过这些线程池方案都存在问题,ali开发手册上不让用
主要原因是:
CacheTheradPool和FiedThreadPool的阻塞方案不对劲,最大数量是Integer.Max_value数量级,容易出现溢出
还有他的拒绝阻塞策略也是不太完美的,需要我们重新设计
我们现在来分析源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
从上面的源码来看, 调用了个构造方法,传入了核心线程数,最大线程数,线程空闲等待时间,时间单位,工作等待队列,线程创建工厂类,队列满拒绝策略
其中创建线程工厂类方法主要就是创建线程,并给线程赋予名字和判断是否daemon线程
然后线程队列拒绝策略就是如果队列满了,还往队列中添加,这抛出异常消息
上面分析到这里就已经完成了,现在我们需要分析的师如何执行任务了
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取标志位(其中包含线程的几种状态和线程队列的数量)
int c = ctl.get();
// 判断数量是否小于核心线程
if (workerCountOf(c) < corePoolSize) {
// 将任务添加到线程队列中
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果线程是运行状态,且添加任务到队列中
if (isRunning(c) && workQueue.offer(command)) {
// 再次检测获取
int recheck = ctl.get();
// 不属于运行状态且删除当前状态
if (! isRunning(recheck) && remove(command))
// 拒绝策略
reject(command);
// 线程队列中没有线程
else if (workerCountOf(recheck) == 0)
// 添加一个新的线程
addWorker(null, false);
}
// 如果任务无法添加到线程队列中,则创建非coreSize核心线程,创建急救线程
else if (!addWorker(command, false))
// 如果创建失败。使用拒绝策略
reject(command);
}
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get();;) {
// 检测队列是否为空
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
// 如果 core 为 true 则 返回核心线程数 corePoolSize 判断工作线程数是否超出核心线程数, 超出,返回false,添加任务失败
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
// 自增线程数量标记
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建Worker线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 上锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
// 如果线程池正在运行
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
// 线程正在运行
if (t.isAlive())
throw new IllegalThreadStateException();
// 添加线程到线程池中
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 直接运行
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 添加失败,直接返回 false,(通过这里workerStarted一定为false)
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
public boolean offer(E e) {
// 如果发现任务为空则抛出异常
if (e == null) throw new NullPointerException();
// 获取数量
final AtomicInteger count = this.count;
// 判断数量是否为极限值
if (count.get() == capacity)
return false;
final int c;
// 创建节点
final Node node = new Node(e);
// 可重入锁
final ReentrantLock putLock = this.putLock;
// 上锁
putLock.lock();
try {
// 如果任务数量和总数相同,则无法添加
if (count.get() == capacity)
return false;
// 添加节点
enqueue(node);
// 自增数量
c = count.getAndIncrement();
// 判断如果数量是否操作了总数
if (c + 1 < capacity)
// 队列超出等待后如果判断未超出队列,则释放队列
notFull.signal();
} finally {
// 解锁
putLock.unlock();
}
// 如果数量为 0 则释放不空锁
if (c == 0)
signalNotEmpty();
return true;
}
分3个步骤进行。
-
如果小于corePoolSize线程的运行数量,尝试用给定的命令启动一个新线程作为它的第一个任务。对addWorker的调用会原子化地检查runState和workerCount,因此,通过返回false来防止在不应该添加线程的情况下添加线程的错误警报。
-
如果一个任务可以成功排队,那么我们仍然需要重复检查是否应该添加一个线程(因为上次检查后已有的线程死了),或者是进入这个方法后池子关闭了。所以我们要重新检查状态,如果停止了,我们就重新检查状态,必要时回滚查询,如果没有,就启动一个新的线程。
-
如果我们无法排队任务,那么我们就尝试添加一个新的线程。如果失败了,我们知道我们已经关机或者饱和了,所以拒绝这个任务。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 如果task不为空则执行,如果为空则从队列中获取任务再去执行
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 执行前处理
beforeExecute(wt, task);
try {
task.run();
// 执行后处理
afterExecute(task, null);
} catch (Throwable ex) {
// 错误执行后处理
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
fork/join任务拆分
Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务
@Slf4j(topic = "c.TestForkJoinDemo")
public class TestForkJoinDemo {
public static void main(String[] args) {
int num = 1000;
// 简单的拆分
forkjoinFunc(num);
// 拆分优化版
forkJoinFuncPro(num);
}
private static void forkJoinFuncPro(int num) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println("统计:" + pool.invoke(new MyTaskPro(1, num)));
System.out.println("线程池数量:" + pool.getPoolSize());
}
private static void forkjoinFunc(int num) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println("统计:" + pool.invoke(new MyTask(num)));
System.out.println("线程池数量:" + pool.getPoolSize());
}
/**
* 进阶优化版
*/
static class MyTaskPro extends RecursiveTask {
private final int start;
private final int end;
public MyTaskPro(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public String toString() {
return start + "," + end;
}
@Override
protected Integer compute() {
if (start == end) {
// log.debug("start == end {}", start);
return start;
}
if ((end - start) == 1) {
// log.debug("end - start == 1");
return end + start;
}
int mid = start + (end - start) / 2;
MyTaskPro t1 = new MyTaskPro(start, mid);
MyTaskPro t2 = new MyTaskPro(mid + 1, end);
t1.fork();
t2.fork();
// log.debug("fork() t1 = {} , t2 = {}", t1, t2);
int res = t1.join() + t2.join();
// log.debug("join() {} + {} = {}", t1, t2, res);
return res;
}
}
/**
* 这个方案容易出现栈溢出
*/
static class MyTask extends RecursiveTask {
private final int n;
public MyTask(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + '}';
}
@Override
protected Integer compute() {
if (n == 1) {
return n;
}
MyTask myTask = new MyTask(n - 1);
myTask.fork();
// log.debug("fork() {} + {}", n, myTask);
Integer res = n + myTask.join();
// log.debug("join() res = {}", res);
return res;
}
}
}
容易出现栈溢出的版本, 整个过程
优化进阶版本
J.U.C
AQS 原理
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取
锁和释放锁- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
获取锁的姿势
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 park unpark
}
释放锁的姿势
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}
结点状态waitStatus
这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
实现不可重入锁
@Slf4j
public class TestAQS {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
sleep(1000);
} finally {
log.debug("unlocking...");
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
}, "t2").start();
}
static class MyLock implements Lock {
public MyLock() {
}
private final Sync sync = new Sync();
@Override
public void lock() {
while (true) {
if (sync.tryAcquire(1)) {
break;
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.tryRelease(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 尝试加锁
*
* @param arg
*
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
if (1 == arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
/**
* 尝试释放
*
* @param arg
*
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if (1 == arg) {
// if (getState() == 0) {
// throw new IllegalMonitorStateException();
// }
setExclusiveOwnerThread(null);
// 后写入state,因为state是volatile所以写入volatile变量方式,volatile修饰的变量前面的所有变量都需要从主存读取一次,防止可能影响到state的写入
// 这样就能保证 setExclusiveOwnerThread 方法的写入也是volatile的
setState(0);
return true;
}
return false;
}
/**
* 状态
*
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 设置条件
*
* @return
*/
protected Condition newCondition() {
return new AbstractQueuedSynchronizer.ConditionObject();
}
}
}
}
心得
AQS 要实现的功能目标
- 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时的等待机制
设计
AQS 的基本思想其实很简单
获取锁的逻辑
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队
释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
要点
- 原子维护 state 状态
- 阻塞及恢复线程
- 维护队列
state 设计
- state 使用 volatile 配合 cas 保证其修改时的原子性
- state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
阻塞恢复设计
- 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
- 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
队列设计
-
使用了 FIFO 先入先出队列,并不支持优先级队列
-
设计时借鉴了 CLH 队列,它是一种单向无锁队列
队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态入队伪代码,只需要考虑 tail 赋值的原子性
do {
// 原来的 tail
Node prev = tail;
// 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))
出队伪代码
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;
CLH 好处:
- 无锁,使用自旋
- 快速,无阻塞
AQS 在一些方面改进了 CLH
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列中还没有元素 tail 为 null
if (t == null) {
// 将 head 从 null -> dummy
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将 node 的 prev 设置为原来的 tail
node.prev = t;
// 将 tail 从原来的 tail 设置为 node
if (compareAndSetTail(t, node)) {
// 原来 tail 的 next 设置为 node
t.next = node;
return t;
}
}
}
}
主要用到 AQS 的并发工具类
ReentrantLock 原理
非公平锁实现原理
加锁流程分析
先从构造器开始看,默认为非公平锁实现
public ReentrantLock() {
// 这是一个不公平锁
sync = new NonfairSync();
}
NonfairSync 继承自 AQS
没有竞争时
第一个竞争出现时
然后再看lock方法
public void lock() {
sync.acquire(1);
}
进入
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们开始分析代码非公平上锁方案
分析这三个方法开始
(1)tryAcquire
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// acquires == 1
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
下面就是我们需要分析的代码
@ReservedStackAccess
// acquires == 1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state状态
int c = getState();
// 如果状态为 0
if (c == 0) {
// cas 设置 state 状态,如果预期值为0(未上锁),则将其上锁为 1
if (compareAndSetState(0, acquires)) {
// 设置当前拥有独占访问权的线程
setExclusiveOwnerThread(current);
// 返回为 true 表示当前线程获取了独占权限
return true;
}
}
// 如果当前线程 == 独占线程,说明这是同一个线程,表示当前线程独占
else if (current == getExclusiveOwnerThread()) {
// c不等于0,c + 1,如果第二次的话,c==1 nextc 最终等于2
int nextc = c + acquires;
// 如果下一个状态为负数则抛出错误,说明int state 溢出了(超出了Integer.MAX_VALUE值)
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 将 state 设置为 2
setState(nextc);
// 本线程线程独占,返回true
return true;
}
// 线程获取独占状态错误
return false;
}
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
- 接下来进入 addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
(2)addWorker
private Node addWaiter(Node mode) {
// 这里内部将当前线程绑定到指定的Node
Node node = new Node(mode);
for (;;) {
// 获取双向链表尾节点
Node oldTail = tail;
// 尾节点不为空
if (oldTail != null) {
// 将node.prev的前一个节点设置为原链表的最后一个节点(线程安全cas)
node.setPrevRelaxed(oldTail);
// 将尾节点设置为node(线程安全cas)
if (compareAndSetTail(oldTail, node)) {
// 如果设置成功,则原链表尾节点的next等于node节点
oldTail.next = node;
// 返回现在的尾节点
return node;
}
} else {
// 尾节点为空表示队列不存在,初始化队列的head和tail
initializeSyncQueue();
}
}
}
(2)acquireQueued
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 直接调用中断方法 Thread.currentThread().interrupt();
selfInterrupt();
}
这个方法主要是将node添加到链表中,而后根据中断状态来判断是否执行selfInterrupt方法
// node == Node.EXCLUSIVE, arg == 1
final boolean acquireQueued(final Node node, int arg) {
// 中断标志
boolean interrupted = false;
try {
for (;;) {
// 获取双向链表中 node 的前一个节点是否存在
final Node p = node.predecessor();
// 如果前一个节点刚好是头节点(这里头节点可能是null null==null),则尝试再次获取独占模式(因为这个时候内部大概率没有多余的节点抢夺独占状态)
if (p == head && tryAcquire(arg)) {
// 上锁成功,设置这个节点为头节点
setHead(node);
// 前一个节点设置为空,防止可能存在的多余引用
p.next = null; // help GC
// 不需要中断
return interrupted;
}
// 上面的加锁失败了,说明存在head的双向链表,所以需要把node挂到没被取消的节点上
if (shouldParkAfterFailedAcquire(p, node))
// park,并且通过调用interrupted方法判断是否被中断
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
// 取消掉mode的独占状态
cancelAcquire(node);
// 如果parkAndCheckInterrupt检测到了中断,把中断功能传递出来
if (interrupted)
// 调用中断
selfInterrupt();
throw t;
}
}
下面就是将 p 挂到节点上的方法
分析下面这个方法其实很简单
这个向分析这个方法的返回值
true表示会执行 parkAndCheckInterrupt 方法,这个方法代表着阻塞
false表示不会阻塞
然后再以参数来分析
pred 是前一个节点的意思
node是当前节点的意思(这里也存在pred和node是同一个节点)
小总结:
现在可以分析这个函数借助 前一个节点(pred)和 当前节点(node)通过一段代码得到 true or false 阻塞或者不阻塞当前节点的结果
现在我们分析在哪里为true 哪里为false
小心得:可以借助if{} == 正方形来辅助分析源码
if (ws == Node.SIGNAL) 如果前一个节点为SIGNAL状态的话,可以阻塞当前节点所绑定的线程
其他的if判断全是不阻塞的
if (ws > 0) { 之后的功能就是找到前面取消的节点,全部删除掉,将node挂到没取消的节点上
} else { 这个else就是使用cas尝试设置SIGNAL到我们pred节点上
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 第一次进来 prev.waitStatus == 0,走 else
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 第1+n次调用会进入这个判断, 只要compareAndSetWaitStatus设置成功,则返回true
// 这个节点多次无法获取独占状态,那么还不如阻塞,但是需要前一个节点来释放后一个节点的阻塞状态,这个时候需要SIGNAL了
return true; // 阻塞
// 大于 0 取消状态
if (ws > 0) {
// 找到前面的未被取消的节点,将node挂在到双向链表上
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前一个节点必须为SINGAL状态才能够去释放当前节点, 第一次执行代码走这里,想让waitStatus设置为SIGNAL状态, 只有设置为这个状态才能够阻塞
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false; // 不阻塞
}
当前线程进入 acquireQueued 逻辑
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
(3)selfInterrupt
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
这样就已经不需要再次详解了
上面的三个方法执行完毕之后会将当前多次获取独占模式失败的线程阻塞,只有以获取独占线程调用了tryRelease函数之后才会被环境
现在我们代码开始分析
解锁流程分析
public void unlock() {
sync.release(1);
}
// arg == 1
public final boolean release(int arg) {
// 尝试释放独占模式
if (tryRelease(arg)) {
// 获取双向链表头节点
Node h = head;
// 判断是否不存在头节点并且头节点的waitStatus状态是否不等于0(存在等待状态)
if (h != null && h.waitStatus != 0)
// unpark方法
unparkSuccessor(h);
return true;
}
return false;
}
下面我们分析这几个函数
(1)tryRelease
@ReservedStackAccess
// releases == 1
protected final boolean tryRelease(int releases) {
// 把state和我们需要释放的 releases 相减
int c = getState() - releases;
// 当前线程和独占线程不相同,则抛出异常,这个判断以后的代码就是线程安全的了
if (Thread.currentThread() != getExclusiveOwnerThread())
// 抛出异常
throw new IllegalMonitorStateException();
// 判断是否跳出了重入锁的环境
boolean free = false;
// 表示重入锁重入完毕
if (c == 0) {
// 表示可以真的释放锁了
free = true;
// 设置独占状态为null,现在其他线程可以重新拿到独占线程了
setExclusiveOwnerThread(null);
}
// 把计算完毕的 c 的值复制给 state
setState(c);
return free;
}
Thread-0 释放锁,进入 tryRelease 流程,如果成功
-
设置 exclusiveOwnerThread 为 null
-
state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
回到 Thread-1 的 acquireQueued 流程
如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
(2)unparkSuccessor
// node == head 头节点
private void unparkSuccessor(Node node) {
// 获取头节点等待状态
int ws = node.waitStatus;
// 如果头节点等待状态小于0
if (ws < 0)
// 修改为正确的模式
node.compareAndSetWaitStatus(ws, 0);
// 获取头节点的下一个节点
Node s = node.next;
// 如果下一个节点为null,或者下一个节点的等待状态大于0
if (s == null || s.waitStatus > 0) {
// 下一个节点赋值为 null
s = null;
// 这个循环主要是为了从尾巴开始找符合p.waitStatus <= 0条件的node
// 判断尾节点是否不等于node并且也不等于null
for (Node p = tail; p != node && p != null; p = p.prev)
// 尾节点的状态小于等于0
if (p.waitStatus <= 0)
// 赋值给s
s = p;
}
// 找到了head节点之后的那个节点
if (s != null)
// 解锁这个头节点之后的这个节点所绑定的线程
LockSupport.unpark(s.thread);
}
可重入锁原理
final boolean nonfairTryAcquire(int acquires) {
// ...
int c = getState();
if (c == 0) {
// ...
}
else if (current == getExclusiveOwnerThread()) {
// 这里就是可重入的原理
int nextc = c + acquires;
setState(nextc);
}
}
protected final boolean tryRelease(int releases) {
// 可重入原因
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 可重入原因, 如果满足计数器为 0 则标志为释放 free == true, 在外围的方法就会被unpark掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
public final boolean release(int arg) {
if (tryRelease(arg)) { // true
Node h = head;
if (h != null && h.waitStatus != 0)
// 释放 unpark
unparkSuccessor(h);
return true;
}
return false;
}
可打断原理
不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
// 产生interrupt但是仍然再for循环
interrupted |= parkAndCheckInterrupt();
}
可打断模式
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 产生中断,直接抛出异常
throw new InterruptedException();
}
公平锁实现原理
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 state
int c = getState();
// 判断 state 是否为 0
if (c == 0) {
// hasQueuedPredecessors 判断存在不存在前节点,如果没有才会去尝试使用cas竞争
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置当前线程独占
setExclusiveOwnerThread(current);
// 设置成功获取独占成功
return true;
}
// 否则上锁失败直接返回 false
}
// 如果刚好是当前线程自己独占
else if (current == getExclusiveOwnerThread()) {
// 设置 state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置 state
setState(nextc);
return true;
}
// 上锁失败
return false;
}
}
public final boolean hasQueuedPredecessors() {
Node h, s;
if ((h = head) != null) {
if ((s = h.next) == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始往前找
for (Node p = tail; p != h && p != null; p = p.prev) {
// 节点不是取消状态的,就把节点放入到s变量
if (p.waitStatus <= 0)
s = p;
}
}
// 找到了不是取消状态的节点,判断这个节点是否就是当前线程的节点
if (s != null && s.thread != Thread.currentThread())
// 是,这返回true,外面函数直接取反,最终导致无法上锁
return true;
}
// 存在前节点,新节点无法直接启用,需要进入等待队列
return false;
}
public final void acquire(int arg) {
// 上面的代码直接返回false取反满足为 true,则会进入 addWaiter将当前节点放入等待队列
if (!false &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
条件变量实现原理(Condition)
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
源码建议配合上面的图片一起看
public final void await() throws InterruptedException {
// 判断线程是否中断
if (Thread.interrupted())
// 中断抛出异常
throw new InterruptedException();
// 添加一个新节点到等待队列中
Node node = addConditionWaiter();
// 释放独占模式,修改 state
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断这个节点是否在队列上
while (!isOnSyncQueue(node)) {
// 不在的话直接上锁
LockSupport.park(this);
// 表示线程被唤醒的操作:确定中断模式并退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 前面分析过了 成功获取独占锁后,并判断 interruptMode 的值
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除非condition
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
// 根据前面的 mode 做对应的事情
// 若为 THROW_IE 则抛出异常中断线程
// 若为 REINTERRUPT 则设置线程中断标记位
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
// 如果同步不只对当前线程持有,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node t = lastWaiter;
// 如果 lastWaiter 不是condition状态,则删除掉这个节点
if (t != null && t.waitStatus != Node.CONDITION) {
// 删除掉非condition节点
unlinkCancelledWaiters();
// 把新的尾节点给 t
t = lastWaiter;
}
// 创建一个新的Node节点并且绑定当前线程
Node node = new Node(Node.CONDITION);
// 如果尾节点不存在的话,直接把新的节点丢给firstwaiter
if (t == null)
firstWaiter = node;
else
// 如果末尾节点还存在,则将新的节点赋值给末尾节点的下一个节点字段next
t.nextWaiter = node;
// 记录末尾节点为新节点
lastWaiter = node;
return node; // 返回新节点
}
// node == 新节点
final int fullyRelease(Node node) {
try {
// 获取 state
int savedState = getState();
// 释放 state(其实就是加减state)
if (release(savedState))
// 如果成功,则返回剩余的 state
return savedState;
// 否则抛出异常
throw new IllegalMonitorStateException();
} catch (Throwable t) {
// 上面代码抛出了异常,给新节点添加取消状态
node.waitStatus = Node.CANCELLED;
// 把异常抛出去
throw t;
}
}
public final boolean release(int arg) {
// 尝试释放独占模式
if (tryRelease(arg)) { // tryRelease 前面已经有源码分析
// 成功,获取当前头节点
Node h = head;
// 当前头节点存在并且头节点的waitStatus状态不为初始化状态
if (h != null && h.waitStatus != 0)
// 解锁 unpark
unparkSuccessor(h); // 前面存在源码分析
// 解锁成功
return true;
}
// 解锁失败
return false;
}
以上是await函数的底层分析
signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1
Thread-1 释放锁,进入 unlock 流程,分析过了
下面是源码级分析
public final void signal() {
// 如果同步不只对当前线程持有,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取第一个节点
Node first = firstWaiter;
// 如果第一个节点不为空
if (first != null)
// 分析代码
doSignal(first);
}
private void doSignal(Node first) {
do {
// 如果下一个节点为空的话
if ( (firstWaiter = first.nextWaiter) == null)
// 标记尾节点为null
lastWaiter = null;
// 让下一个节点为空
first.nextWaiter = null;
// 将节点从条件队列移动到同步队列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 设置节点为COndition状态 如果成功则直接返回false,如果失败返回true
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
// 将节点从条件队列移动到同步队列
Node p = enq(node);
int ws = p.waitStatus;
// 如果节点为取消状态或者设置SIGNAL状态失败,则上锁
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 否则表示转移成功
return true;
}
读写锁 -- ReentrantReadWriteLock
读写锁允许 多读少写的引用场景
除了读和读锁其他情况都会存在阻塞问题
import java.util.concurrent.locks.ReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;
import static com.zhazha.utils.Sleeper.sleep;
@Slf4j(topic = "c.demo")
public class Demo {
public static void main(String[] args) {
CacheData data = new CacheData();
// 读取和读取之间不存在阻塞问题
// new Thread(data::read, "r1").start();
new Thread(data::read, "r2").start();
// 数取和写入之间存在阻塞问题
new Thread(data::write, "w1").start();
// 写入和写入之间存在安全问题
// new Thread(data::write, "w2").start();
}
static class CacheData {
private Object data;
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock r = rw.readLock();
private final ReentrantReadWriteLock.WriteLock w = rw.writeLock();
/**
* 读
*/
public void read() {
log.debug("获取读锁。。。");
r.lock();
try {
sleep(1000);
log.debug("读取。。。");
} finally {
log.debug("释放读锁。。。");
r.unlock();
}
}
/**
* 写
*/
public void write() {
log.debug("获取写锁。。。");
w.lock();
try {
sleep(1000);
log.debug("写入。。。");
} finally {
log.debug("释放写锁");
w.unlock();
}
}
}
}
注意事项
读锁不支持条件变量
重入时升级不支持:即持有读锁时,如果再次获取写锁则会被永远阻塞
r.lock();
try {
// ...
w.lock(); // 程序会永远阻塞在这里
try {
// ...
} finally {
w.unlock();
}
} finally {
r.unlock();
}
重入降级:即持有写锁的情况下去获取读锁
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
new Thread(() -> {
rw.writeLock().lock(); // 正常情况下写锁遇到写锁会被阻塞
rw.readLock().lock();
log.debug("在这里写锁会降级为读锁");
Sleeper.sleep(1000);
rw.writeLock().unlock();
rw.readLock().unlock();
}, "t1").start();
new Thread(() -> {
rw.writeLock().lock(); // 正常情况下写锁遇到写锁会被阻塞
rw.readLock().lock();
log.debug("在这里写锁会降级为读锁");
Sleeper.sleep(1000);
rw.writeLock().unlock();
rw.readLock().unlock();
}, "t2").start();
}
主要目标是为了在写锁解锁前立即上读锁
高并发缓存方案
缓存更新策略
这里共享变量就是缓存
把 缓存 当作共享变量,那么对共享变量的操作存在,清空、写入、查询三个操作,这三个操作在高并发场景下,无法保证顺序
such as 先写入,突然被清空,然后再查询
对共享变量 数据库 的修改和查询操作在高并发下无法保证顺序,所以也存在线程安全问题,但是在这个过程中也就有可能会出现查询延迟问题,所以无所谓了
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestGenericDao {
public static void main(String[] args) {
GenericDao dao = new CacheGenericDao();
System.out.println("============> 查询");
String sql = "select * from emp where empno = ?";
int empno = 2;
Emp emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
System.out.println("============> 更新");
dao.update("update emp set sal = ? where empno = ?", 800, empno);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
}
}
class CacheGenericDao extends GenericDao {
/**
* 缓存
*/
private final Map cache = new HashMap<>();
private final GenericDao dao = new GenericDao();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public List queryList(Class beanClass, String sql, Object... args) {
return dao.queryList(beanClass, sql, args);
}
public T queryOne(Class beanClass, String sql, Object... args) {
SqlPair sqlPair = new SqlPair(sql, args);
rw.readLock().lock();
try {
T t = (T) cache.get(sqlPair);
if (null != t) {
return t;
}
} finally {
rw.readLock().unlock();
}
rw.writeLock().lock();
try {
T value = (T) cache.get(sqlPair);
if (null == value) {
value = dao.queryOne(beanClass, sql, args);
cache.put(sqlPair, value);
}
return value;
} finally {
rw.writeLock().unlock();
}
}
public int update(String sql, Object... args) {
SqlPair sqlPair = new SqlPair(sql, args);
rw.writeLock().lock();
try {
cache.remove(sqlPair);
return dao.update(sql, args);
} finally {
rw.writeLock().unlock();
}
}
private class SqlPair implements Serializable {
private final String sql;
private final Object[] args;
@Override
public String toString() {
return "SqlPair{" + "sql='" + sql + '\'' + ", args=" + Arrays.toString(args) + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
SqlPair sqlPair = (SqlPair) o;
return Objects.equals(sql, sqlPair.sql) && Arrays.equals(args, sqlPair.args);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(args);
return result;
}
public SqlPair(String sql, Object[] args) {
this.sql = sql;
this.args = args;
}
}
}
补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询
这种情况很少见,但是会出现
注意
- 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 适合读多写少,如果写操作比较频繁,以上实现性能低
- 没有考虑缓存容量
- 没有考虑缓存过期
- 只适合单机
- 并发性还是低,目前只会用一把锁
- 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
- 乐观锁实现:用 CAS 去更新
读写锁底层原理
图解流程
t1 w.lock,t2 r.lock
1)t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示
-1 表示失败
0 表示成功,但后继节点不会继续唤醒
正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
3)这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
5)如果没有成功,在 doAcquireShared 内 for (; 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (; 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (; 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
源码分析
写锁上锁流程
static final class NonfairSync extends Sync {
// ... 省略无关代码
// 外部类 WriteLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
// 尝试获得写锁失败
!tryAcquire(arg) &&
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
// 进入 AQS 队列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryAcquire(int acquires) {
// 获得低 16 位, 代表写锁的 state 计数
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (
// c != 0 and w == 0 表示有读锁, 或者
w == 0 ||
// 如果 exclusiveOwnerThread 不是自己
current != getExclusiveOwnerThread()) {
// 获得锁失败
return false;
}
// 写锁计数超过低 16 位, 报异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
setState(c + acquires);
return true;
}
if (
// 判断写锁是否该阻塞, 或者
writerShouldBlock() ||
// 尝试更改计数失败
!compareAndSetState(c, c + acquires)) {
// 获得锁失败
return false;
}
// 获得锁成功
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
}
写锁释放流程
static final class NonfairSync extends Sync {
// ... 省略无关代码
// WriteLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.release(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放写锁成功
if (tryRelease(arg)) {
// unpark AQS 中等待的线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc);
return free;
}
}
读锁上锁流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquireShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireShared(int arg) {
// tryAcquireShared 返回负数, 表示获取读锁失败
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
return -1;
}
int r = sharedCount(c);
if (
// 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且
!readerShouldBlock() &&
// 小于读锁计数, 并且
r < MAX_COUNT &&
// 尝试增加计数成功
compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// AQS 继承过来的方法, 方便阅读, 放在此处
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (; ; ) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
else if (readerShouldBlock()) {
// ... 省略不重要的代码
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
// 成功
if (r >= 0) {
// ㈠
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (
// 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
// 进入 ㈡
doReleaseShared();
}
}
}
// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析
for (; ; ) {
Node h = head;
// 队列还有节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
读锁释放流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.releaseShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代码
for (; ; ) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
return nextc == 0;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (; ; ) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
// 锁升级 - 读锁
log.debug("updating to read lock... {}", stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}
测试 读-读
可以优化
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
输出结果,可以看到实际没有加读锁
15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1
测试 读-写
时优化读补加读锁
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(100);
}, "t2").start();
}
输出结果
15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
15:57:00.717 c.DataContainerStamped [t2] - write lock 384
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
15:57:02.719 c.DataContainerStamped [t1] - read lock 513
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
Semaphore
基本使用
[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。
生动形象:
Semaphore 就是用水瓢装水到桶里拿着桶一瓢一瓢的浇水的过程,一瓢一瓢水的往桶里装水,满了拿着桶去浇水,也是一瓢一瓢的浇水
Semaphore 也是一台公共汽车(规定好限载量49),当不满49人,乘客可以随时上下车,当满了49人,只能下车了
@Slf4j(topic = "c.TestSemaphoreDemo")
public class TestSemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
log.debug("running...");
sleep(1000);
log.debug("end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
Semaphore 实现
- 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
- 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
@Slf4j(topic = "c.TestSemaphoreThreadPoolDemo")
public class TestSemaphoreConnectPoolDemo {
public static void main(String[] args) {
Pool pool = new Pool(10);
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>();
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 25; j++) {
Connection connection = pool.borrow();
copyOnWriteArrayList.add(connection);
}
});
}
while (true) {
for (int i = 0; i < copyOnWriteArrayList.size(); i++) {
Connection connection = copyOnWriteArrayList.get(i);
pool.free(connection);
copyOnWriteArrayList.remove(connection);
}
sleep(100);
}
}
@Slf4j(topic = "c.Pool")
static class Pool {
private final int poolSize;
private final Connection[] connections;
private final AtomicIntegerArray states;
private final Semaphore semaphore;
public Pool(int poolSize) {
this.poolSize = poolSize;
connections = new Connection[poolSize];
for (int i = 0; i < connections.length; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
states = new AtomicIntegerArray(poolSize);
semaphore = new Semaphore(poolSize);
}
/**
* 借出
*
* @return
*/
public Connection borrow() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
return null;
}
/**
* 释放连接
*
* @param connection
*/
public void free(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i].equals(connection)) {
states.compareAndSet(i, 1, 0);
log.debug("free {}", connection);
semaphore.release();
break;
}
}
}
}
}
单位时间内限流
guava 实现
@RestController
public class TestController {
private RateLimiter limiter = RateLimiter.create(50);
@GetMapping("/test")
public String test() {
// limiter.acquire();
return "ok";
}
}
Semaphore 原理
1. 加锁解锁流程
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
这时 Thread-4 释放了 permits,状态如下
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
源码分析
构造方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
// permits 即 state
super(permits);
}
// Semaphore 方法, 方便阅读, 放在此处
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 尝试获得共享锁
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// Sync 继承过来的方法, 方便阅读, 放在此处
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (
// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptibly
remaining < 0 ||
// 如果 cas 重试成功, 返回正数, 表示获取成功
compareAndSetState(available, remaining)
) {
return remaining;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次尝试获取许可
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功后本线程出队(AQS), 所在 Node设置为 head
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
// r 表示可用资源数, 为 0 则不会继续传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// Semaphore 方法, 方便阅读, 放在此处
public void release() {
sync.releaseShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}
为什么要有 PROPAGATE
早期有 bug
- releaseShared 方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- doAcquireShared 方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 这里会有空档
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- setHeadAndPropagate 方法
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
// 有空闲资源
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
// 下一个
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
- 假设存在某次循环中队列里排队的结点情况为 head(-1)->t1(-1)->t2(-1)
- 假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4
正常流程
产生 bug 的情况
修复前版本执行流程
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
- T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个head),不满足条件,因此不调用 unparkSuccessor(head)
- T1 获取信号量成功,调用 setHeadAndPropagate 时,因为不满足 propagate > 0(2 的返回值也就是 propagate(剩余资源量) == 0),从而不会唤醒后继结点, T2 线程得不到唤醒
bug 修复后
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
doReleaseShared();
}
}
}
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
- T3 调用 releaseShared(),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
- T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
- T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2
CountdownLatch
用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
生动形象:
CountdownLatch 就是搞前10名奖励大型家电的活动,只有前十名的客人才能获得奖励,超过10个人后,这个活动奖品没了,活动举办完毕,一直等待的快递员把快递送到客人家里
@Slf4j(topic = "c.TestCountDownLatchDemo")
public class TestCountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
log.debug("线程:{}", Thread.currentThread().getName());
latch.countDown();
}, "线程" + i).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("主线程等待完毕。。。");
}
}
CyclicBarrier
[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
生动形象:
CyclicBarrier 是客运车,限载量49人,乘客上车直到满人了,才发车,到目的地后乘客下车,重新等待乘客上车
@Slf4j(topic = "c.TestCyclicBarrierDemo")
public class TestCyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
log.debug("客车准备发车。。。");
for (int i = 0; i < 4; i++) {
new Thread(() -> {
log.debug("{}上车。。。", Thread.currentThread().getName());
try {
cyclicBarrier.await();
log.debug("客车满员了。。。{}叫:发车。。。", Thread.currentThread().getName());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
Sleeper.sleep(1000);
log.debug("{}到站,下车", Thread.currentThread().getName());
}, "A乘客" + i).start();
}
Sleeper.sleep(2000);
log.debug("客车准备发车。。。");
/**
* 可重入的证明
*/
for (int i = 0; i < 2; i++) {
new Thread(() -> {
log.debug("{}上车等待中。。。", Thread.currentThread().getName());
try {
cyclicBarrier.await();
log.debug("车辆满了,{}叫:发车。。。", Thread.currentThread().getName());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
log.debug("{}到站,下车", Thread.currentThread().getName());
Sleeper.sleep(1000);
}, "B乘客" + i).start();
}
}
}
线程安全集合类概述
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如
Hashtable
,Vector
- 使用
Collections
装饰的线程安全集合,如:Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
java.util.concurrent.*
重点介绍java.util.concurrent.*
下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking、CopyOnWrite、Concurrent- Blocking 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite 之类容器修改开销相对较重
- Concurrent 类型的容器
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
历,这时内容是旧的 - 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException
,不再继续遍历
ConcurrentHashMap
练习:单词计数
@Slf4j(topic = "c.TestWordCountDemo")
public class TestWordCountDemo {
private static final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
@Test
public void testReadFiles() throws Exception {
Map map = demo(ConcurrentHashMap::new, (stringMap, words) -> words.forEach(word -> {
LongAdder longAdder = stringMap.computeIfAbsent(word, s -> new LongAdder());
longAdder.increment();
}));
System.out.println(map);
/**
* 这种方式存在线程安全问题
*/
// Map map = demo(HashMap::new, (stringTMap, words) -> {
// words.forEach(word -> {
// Integer counter = stringTMap.get(word);
// int newValue = counter == null ? 1 : counter + 1;
// stringTMap.put(word, newValue);
// });
// });
// System.out.println(map);
}
/**
* 统计每个单词出现的数量
*
* @param supplier
* 提供Map
* @param biConsumer
* 提供 Map 和 List 把 List 的值累加到 Map 中
* @param
*/
public Map demo(Supplier
JDK 7 HashMap 并发死链
死链复现
注意
- 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
- 以下测试代码是精心准备的,不要随便改动
public static void main(String[] args) {
// 测试 java 7 中哪些数字的 hash 结果相等
System.out.println("长度为16时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 16 == 1) {
System.out.println(i);
}
}
System.out.println("长度为32时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 32 == 1) {
System.out.println(i);
}
}
// 1, 35, 16, 50 当大小为16时,它们在一个桶内
final HashMap map = new HashMap();
// 放 12 个元素
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
map.put(6, null);
map.put(7, null);
map.put(8, null);
map.put(9, null);
map.put(10, null);
map.put(16, null);
map.put(35, null);
map.put(1, null);
System.out.println("扩容前大小[main]:" + map.size());
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-0]:" + map.size());
}
}.start();
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-1]:" + map.size());
}
}.start();
}
final static int hash(Object k) {
int h = 0;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
调试工具使用 idea
在 HashMap 源码 590 行加断点
int newCapacity = newTable.length;
断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来
newTable.length==32 &&
(
Thread.currentThread().getName().equals("Thread-0")||
Thread.currentThread().getName().equals("Thread-1")
)
断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行
运行代码,程序在预料的断点位置停了下来,输出
长度为16时,桶下标为1的key
1
16
35
50
长度为32时,桶下标为1的key
1
35
扩容前大小[main]:12
接下来进入扩容流程调试
在 HashMap 源码 594 行加断点
Entry next = e.next; // 593
if (rehash) // 594
// ...
这是为了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点(条件 Thread.currentThread().getName().equals("Thread-0"))
这时可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object
查看节点状态
e (1)->(35)->(16)->null
next (35)->(16)->null
在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成
newTable[1] (35)->(1)->null
扩容后大小:13
这时 Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为
e (1)->null
next (35)->(1)->null
为什么呢,因为 Thread-1 扩容时链表也是后加入的元素放入链表头,因此链表就倒过来了,但 Thread-1 虽然结
果正确,但它结束后 Thread-0 还要继续运行
接下来就可以单步调试(F8)观察死链的产生了
下一轮循环到 594,将 e 搬迁到 newTable 链表头
newTable[1] (1)->null
e (35)->(1)->null
next (1)->null
下一轮循环到 594,将 e 搬迁到 newTable 链表头
newTable[1] (35)->(1)->null
e (1)->null
next null
再看看源码
e.next = newTable[1];
// 这时 e (1,35)
// 而 newTable[1] (35,1)->(1,35) 因为是同一个对象
newTable[1] = e;
// 再尝试将 e 作为链表头, 死链已成
e = next;
// 虽然 next 是 null, 会进入下一个链表的复制, 但死链已经形成了
源码分析
HashMap 的并发死链发生在扩容时
// 将 table 迁移至 newTable
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
// 1 处
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 2 处
// 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 next
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
假设 map 中初始元素是
原始链表,格式:[下标] (key,next)
[1] (1,35)->(35,16)->(16,null)
线程 a 执行到 1 处 ,此时局部变量 e 为 (1,35),而局部变量 next 为 (35,16) 线程 a 挂起
线程 b 开始执行
第一次循环
[1] (1,null)
第二次循环
[1] (35,1)->(1,null)
第三次循环
[1] (35,1)->(1,null)
[17] (16,null)
切换回线程 a,此时局部变量 e 和 next 被恢复,引用没变但内容变了:e 的内容被改为 (1,null),而 next 的内
容被改为 (35,1) 并链向 (1,null)
第一次循环
[1] (1,null)
第二次循环,注意这时 e 是 (35,1) 并链向 (1,null) 所以 next 又是 (1,null)
[1] (35,1)->(1,null)
第三次循环,e 是 (1,null),而 next 是 null,但 e 被放入链表头,这样 e.next 变成了 35 (2 处)
[1] (1,35)->(35,1)->(1,35)
已经是死链了
小结
- 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
- JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
ConcurrentHashMap源码分析
jdk8 ConcurrentHashMap 源码解析
重要属性和内部类
// sizeCtl:默认为0,用来控制table的初始化和扩容操作.它的数值有以下含义
// -1: 代表table正在初始化,其他线程应该交出CPU时间片,退出
// -N: 表示正有N-1个线程执行扩容操作
// >0: 如果table已经初始化,代表table容量,默认为table大小的0.75,如果还未初始化,代表需要初始化的大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node implements Map.Entry {}
// hash 表
transient volatile Node[] table;
// 扩容时的 新 hash 表
private transient volatile Node[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode extends Node {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode extends Node {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin extends Node {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode extends Node {}
重要方法
// 获取 Node[] 中第 i 个 Node
static final Node tabAt(Node[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final boolean casTabAt(Node[] tab, int i, Node c, Node v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final void setTabAt(Node[] tab, int i, Node v)
构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get 流程
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
// spread 方法能确保返回结果是正数, 并且借助hashCode计算新的 h 码为了尽量确保节点散列分布
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头结点已经是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
put 流程
以下数组简称(table),链表简称(bin)
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
// 其中 spread 方法会综合高位低位, 具有更好的 hash 性
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table; ; ) {
// f 是链表头节点
// fh 是链表头结点的 hash
// i 是链表在 table 中的下标
Node f;
int n, i, fh;
// (1)要初始化 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
tab = initTable();
// 如果hash对应的位置为空,则使用cas创建节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头使用了 cas, 无需 synchronized
if (casTabAt(tab, i, null, new Node(hash, key, value, null)))
break;
}
// (2)帮忙扩容, sizeCtl为负数,表示正在扩容table
else if ((fh = f.hash) == MOVED)
// 帮忙之后, 进入下一轮循环
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 锁住链表头节点
synchronized (f) {
// 再次确认链表头节点没有被移动
if (tabAt(tab, i) == f) {
// 链表
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node e = f; ; ++binCount) {
K ek;
// 找到相同的 key
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
// 更新
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
// 已经是最后的节点了, 新增 Node, 追加至链表尾
if ((e = e.next) == null) {
pred.next = new Node(hash, key, value, null);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
if ((p = ((TreeBin) f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 释放链表头节点的锁
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加 size 计数
addCount(1L, binCount);
return null;
}
// (1)要初始化 table
private final Node[] initTable() {
Node[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl 小于 0 ,表示正在创建初始化 table
if ((sc = sizeCtl) < 0)
Thread.yield();
// 尝试将 sizeCtl 设置为 -1(表示初始化 table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node[] nt = (Node[]) new Node, ?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// 帮忙扩容
// (1) tab = table (2) f = tabAt(tab, i = (n - 1) & hash)
final Node[] helpTransfer(Node[] tab, Node f) {
Node[] nextTab; int sc;
// 判断是否为 ForwardingNode , 是则获取下一个节点
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
CounterCell[] as;
long b, s;
if (
// 已经有了 counterCells, 向 cell 累加
(as = counterCells) != null ||
// 还没有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a;
long v;
int m;
boolean uncontended = true;
if (
// 还没有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 还没有 cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 创建累加单元数组和cell, 累加重试
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 获取元素个数
s = sumCount();
}
if (check >= 0) {
Node[] tab, nt;
int n, sc;
while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
break;
// newtable 已经创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,这时 newtable 未创建
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
size 计算流程
size 计算实际发生在 put,remove 改变集合元素的操作之中
- 没有竞争发生,向 baseCount 累加计数
- 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
- counterCells 初始有两个 cell
- 如果计数竞争比较激烈,会创建新的 cell 来累加计数
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 将 baseCount 计数与所有 cell 计数累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
- 初始化,使用 cas 来保证并发安全,懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可
注意:
-
- forwardingNode 节点用于标记是否转移完毕,如果节点存在ForwardingNode则表示转移完毕请到另一个table去查找
JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
构造器分析
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默认是 32 - 4 = 28
this.segmentShift = 32 - sshift;
// segmentMask 默认是 15 即 0000 0000 0000 1111
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建 segments and segments[0]
Segment s0 =
new Segment(loadFactor, (int)(cap * loadFactor),
(HashEntry[])new HashEntry[cap]);
Segment[] ss = (Segment[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
构造完成,如下图所示
可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
put 流程
public V put(K key, V value) {
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 计算出 segment 下标
int j = (hash >>> segmentShift) & segmentMask;
// 获得 segment 对象, 判断是否为 null, 是则创建该 segment
if ((s = (Segment)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,
// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
s = ensureSegment(j);
}
// 进入 segment 的put 流程
return s.put(key, hash, value, false);
}
segment 继承了可重入锁(ReentrantLock),它的 put 方法为
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试加锁
HashEntry node = tryLock() ? null :
// 如果不成功, 进入 scanAndLockForPut 流程
// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
scanAndLockForPut(key, hash, value);
// 执行到这里 segment 已经被成功加锁, 可以安全执行
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
// 更新
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 新增
// 1) 之前等待锁时, node 已经被创建, next 指向链表头
if (node != null)
node.setNext(first);
else
// 2) 创建新 node
node = new HashEntry(hash, key, value, first);
int c = count + 1;
// 3) 扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 将 node 作为链表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
rehash 流程
发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全
private void rehash(HashEntry node) {
HashEntry[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry[] newTable =
(HashEntry[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry e = oldTable[i];
if (e != null) {
HashEntry next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry lastRun = e;
int lastIdx = idx;
// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用
for (HashEntry last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// 剩余节点需要新建
for (HashEntry p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry n = newTable[k];
newTable[k] = new HashEntry(h, p.key, v, n);
}
}
}
}
// 扩容完成, 才加入新的节点
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
// 替换为新的 HashEntry table
table = newTable;
}
附,调试代码
public static void main(String[] args) {
ConcurrentHashMap map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
int hash = hash(i);
int segmentIndex = (hash >>> 28) & 15;
if (segmentIndex == 4 && hash % 8 == 2) {
System.out.println(i + "\t" + segmentIndex + "\t" + hash % 2 + "\t" + hash % 4 +
"\t" + hash % 8);
}
}
map.put(1, "value");
map.put(15, "value"); // 2 扩容为 4 15 的 hash%8 与其他不同
map.put(169, "value");
map.put(197, "value"); // 4 扩容为 8
map.put(341, "value");
map.put(484, "value");
map.put(545, "value"); // 8 扩容为 16
map.put(912, "value");
map.put(941, "value");
System.out.println("ok");
}
private static int hash(Object k) {
int h = 0;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
int v = h ^ (h >>> 16);
return v;
}
get 流程
get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
public V get(Object key) {
Segment s; // manually integrate access methods to reduce overhead
HashEntry[] tab;
int h = hash(key);
// u 为 segment 对象在数组中的偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// s 即为 segment
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
size 计算流程
- 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
- 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
// 超过重试次数, 需要创建所有 segment 并加锁
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
问题一:
java.util.concurrent.ConcurrentHashMap#computeIfAbsent
看下面源码引发的几个问题:
对临时变量加对象锁?
synchronized对象后头的第一个if方法不是cas操作么?为什么还要加上 synchronized
此处如果不加上 synchronized 的话,哪个共享变量会出问题?(话说它会发生问题么?每个线程进来全是单独的 r 对象)
答: 对临时变量加锁确实有这种应用场景,但要根据上下文判断,
英文:ReservationNode 翻译为预留节点
主要功能是存访预留节点,然后对预留节点上锁,此时通过cas设置预留节点进tab数组的 i 位置,这样,其他线程在设置 tab 的 i 位置时,会发现预留节点已经上锁,别的线程就无法修改这个位置,等到我们的 computeIfAbsent 执行完 setTabAt 方法将预留节点位置替换成新节点后,此时预留节点还是不是上锁状态已经不重要了
LinkedBlockingQueue 原理
1. 基本的入队出队
public class LinkedBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
static class Node {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/
Node next;
Node(E x) { item = x; }
}
}
初始化链表 last = head = new Node
Dummy 节点用来占位,item 为 null
当一个节点入队 last = last.next = node;
再来一个节点入队 last = last.next = node;
出队
Node h = head;
Node first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
h = head
first = h.next
h.next = h
head = first
E x = first.item;
first.item = null;
return x;
2. 源码分析
高明之处在于用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是
- head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
put 操作
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了等待
while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
c = count.getAndIncrement();
// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素, 叫醒 take 线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}
take 操作
public E take() throws InterruptedException {
final E x;
final int c;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 数量是空的
while (count.get() == 0) {
// 他空的,就让他等待
notEmpty.await();
}
// 删除头节点的下一个节点
x = dequeue();
// 数量 - 1
c = count.getAndDecrement();
if (c > 1)
// 他不是空的,释放
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull();
return x;
}
由 put 唤醒 put 是为了避免信号不足
offer 方法
这个方法主要是根据时间判断是否能够完成添加操作,不能够则返回false,否则true
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
final int c;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 上锁
putLock.lockInterruptibly();
try {
// 判断是否满了
while (count.get() == capacity) {
if (nanos <= 0L)
// 等待的时间已经结束,直接返回false
return false;
nanos = notFull.awaitNanos(nanos);
}
// 将节点添加到队列中
enqueue(new Node(e));
// 添加数量
c = count.getAndIncrement();
// 判断是否超出总数
if (c + 1 < capacity)
// 未超出总数,释放不满信号
notFull.signal();
} finally {
// 解锁
putLock.unlock();
}
if (c == 0)
// 如果数量为空,释放不满信号
signalNotEmpty();
return true;
}
remove 方法
public boolean remove(Object o) {
if (o == null) return false;
// 上锁,上put锁和task查询锁
fullyLock();
try {
// for循环遍历单向链表
for (Node pred = head, p = pred.next;
p != null;
pred = p, p = p.next) {
// 找到节点
if (o.equals(p.item)) {
// 删除掉节点(1)
unlink(p, pred);
return true;
}
}
return false;
} finally {
// 解锁 takeLock 和 putLock
fullyUnlock();
}
}
void unlink(Node p, Node pred) {
// 要删除的节点数据置为null
p.item = null;
// 拿出要删除节点的下一个节点,给上上一个节点的.next节点,这样中间的那个节点就删除了
pred.next = p.next;
// 如果要删除的节点是last尾节点
if (last == p)
// 则最后节点指向前面,那么pred就是最后一个节点了
last = pred;
// 如果当前数量 等于总数量,那么现在可以释放不满信号了,然后count - 1
if (count.getAndDecrement() == capacity)
notFull.signal();
}
如果满足则删除 removeIf()
这是一个批量删除的方法,主要借助二进制做标记删除链表上节点的方法
public boolean removeIf(Predicate super E> filter) {
Objects.requireNonNull(filter);
return bulkRemove(filter);
}
private boolean bulkRemove(Predicate super E> filter) {
// 标记删除与否的变量
boolean removed = false;
Node p = null, ancestor = head;
Node[] nodes = null;
int n, len = 0;
do {
// 获取一个64位大小的数组,并且记录了个 n 变量,n 表示数组长度
// 设计一个 64 bit 的数组,为了我们直接使用索引的速度比较快
fullyLock();
try {
if (nodes == null) {
// 从这里能看到,第一个节点不存放任何数据
p = head.next;
// 计算 len 的长度
for (Node q = p; q != null; q = succ(q))
if (q.item != null && ++len == 64)
break;
// new 一个这个大的节点数组
nodes = (Node[]) new Node>[len];
}
// 将链表中的节点全部放入到数组中
for (n = 0; p != null && n < len; p = succ(p))
nodes[n++] = p;
} finally {
fullyUnlock();
}
// 利用 deathRow 的算法设计了个标记作用的变量
// 如果标记位为 1 则表示需要删除的,否则无须操作
// 由于 long 是 64 bit, 所以它最多能够标识 64 位的数据
// 不理解的可以看看 1001 0100 这里这个数组大小为8 ,标记为 1 的数组都需要删除掉
long deathRow = 0L;
for (int i = 0; i < n; i++) {
final E e;
if ((e = nodes[i].item) != null && filter.test(e))
deathRow |= 1L << i;
}
if (deathRow != 0) {
fullyLock();
try {
// 遍历数组
for (int i = 0; i < n; i++) {
final Node q;
// 获取 deathRow 标记数组的索引 && 判断数组在该索引的位置下判断元素是否不为 null
if ((deathRow & (1L << i)) != 0L
&& (q = nodes[i]).item != null) {
// findPred 这个方法将q的前一个节点找到
ancestor = findPred(q, ancestor);
// 删除 q 节点, ancestor 就是 q 的前一个节点,ancestor.next = q.next, 然后 q 节点就没了
unlink(q, ancestor);
removed = true;
}
// 节点虽然从链表上删除了,但是还需要在数组上也删除
nodes[i] = null; // help GC
}
} finally {
fullyUnlock();
}
}
} while (n > 0 && p != null);
return removed;
}
字段分析
- capacity:容器可以存放的总数量
- count:当前容器中的数量
- head:链表头
- last:链表尾
- takeLock:可重入锁
- notEmpty:队列不为空条件变量,队列为空则阻塞,队列不为空释放
- putLock:添加节点锁,方式并发问题
- notFull:满了就阻塞,没满就释放(capacity)
构造方法分析
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}
- 如果构造器没写容量,默认为
Integer.MAX_VALUE
大小 - 初始化了 capacity 容量大小
- 将last和head头节点和尾节点都指向一个空Node节点上
3. 性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
ConcurrentLinkedQueue 原理
1. 模仿 ConcurrentLinkedQueue
初始代码
public class Test3 {
public static void main(String[] args) {
MyQueue queue = new MyQueue<>();
queue.offer("1");
queue.offer("2");
queue.offer("3");
System.out.println(queue);
}
}
class MyQueue implements Queue {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Node p = head; p != null; p = p.next.get()) {
E item = p.item;
if (item != null) {
sb.append(item).append("->");
}
}
sb.append("null");
return sb.toString();
}
public MyQueue() {
head = last = new Node<>(null, null);
}
private volatile Node last;
private volatile Node head;
private E dequeue() {
/*Node h = head;
Node first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;*/
return null;
}
static class Node {
volatile E item;
public Node(E item, Node next) {
this.item = item;
this.next = new AtomicReference<>(next);
}
AtomicReference> next;
}
}
ConcurrentLinkedQueue 源码分析
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 cas 来实现
事实上,ConcurrentLinkedQueue 应用还是非常广泛的
例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了
ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
类族分析
属性分析
head:头节点
tail:尾节点
内部类分析
static final class Node {
volatile E item;
volatile Node next;
Node(E item) {
ITEM.set(this, item);
}
Node() {}
void appendRelaxed(Node next) {
NEXT.set(this, next);
}
boolean casItem(E cmp, E val) {
return ITEM.compareAndSet(this, cmp, val);
}
}
上面的Node内部类主要做了cas处理
cas属性源码
private static final VarHandle HEAD;
private static final VarHandle TAIL;
static final VarHandle ITEM;
static final VarHandle NEXT;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
HEAD = l.findVarHandle(ConcurrentLinkedQueue.class, "head",
Node.class);
TAIL = l.findVarHandle(ConcurrentLinkedQueue.class, "tail",
Node.class);
ITEM = l.findVarHandle(Node.class, "item", Object.class);
NEXT = l.findVarHandle(Node.class, "next", Node.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
构造函数分析
public ConcurrentLinkedQueue() {
head = tail = new Node();
}
head ---> tail ---> Node
增加
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
final Node newNode = new Node(Objects.requireNonNull(e));
// 拿出尾节点
for (Node t = tail, p = t;;) {
// 获得尾节点的下一个节点
Node q = p.next;
// 如果尾节点的.next节点为空,表示它是最后一个节点了
if (q == null) {
// 修改尾节点的下一个节点
if (NEXT.compareAndSet(p, null, newNode)) {
if (p != t)
// 这儿允许设置tail为最新节点的时候失败,因为添加node的时候是根据p.next是不是为null判断的
TAIL.weakCompareAndSet(this, t, newNode);
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// 虽然q是p.next,但是因为是多线程,在offer的同时也在poll,如offer的时候正好p被poll了,那么在poll方法中的updateHead方法会将head指向当前的q,而把p.next指向自己,即:p.next == p
// 这个时候就会造成tail在head的前面,需要重新设置p
// 如果tail已经改变,将p指向tail,但这个时候tail依然可能在head前面
// 如果tail没有改变,直接将p指向head
p = (t != (t = tail)) ? t : head;
else
// tail已经不是最后一个节点,将p指向最后一个节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
CopyOnWriteArrayList
CopyOnWriteArraySet
是它的马甲 底层实现采用了 写入时拷贝
的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:
public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}
其它读操作并未加锁,例如:
public void forEach(Consumer super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
适合『读多写少』的应用场景
get 弱一致性
迭代器弱一致性
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}
不要觉得弱一致性就不好
- 数据库的 MVCC 都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡
高并发模拟测试工具
postman
apache bench(AB)
JMeter
(1) 使用postman创建并发模拟测试
据说这种方式的并发测试看看就行了, 没用, 使用的是串行不是并发
填入并发和延迟, 基本完成了
(2) apache bench(ab)
需要安装apache
通过上面的方法可以基本入门ab的使用方法, 但是还是不太合适推荐使用这个方法
(3) JMeter
下载之后直接进入bin目录下找到
这个bat直接双击运行, 在选项中可选择中文简体
多线程安全问题是什么?
问题: 多线程在修改共享资源的时候, 会出现主存和线程工作内存之间的交流是有延迟性的, 不能够即时的更新数据, 也不能够实时监控主存的数据是否被修改, 所以线程经常得到了旧的值, 或者线程已经算好了值, 但是没能即时更新到主存
解决方案:
①线程如果能够即时的读取到最新的主存值和线程能够即时的更新自己修改的值到主存, 那么解决了一个问题, 就是可见性问题;
②但是却无法解决线程A和线程B'同时'读取到主存的值, 然后进行操作, '同时'更新到主存的问题, 这个时候总有一个值的操作被替换了(这里的同时并不是真的同时, 只不过是线程上下文切换导致的线程读取到一个旧值), 产生了错误的值, 所以需要进行线程的串行化处理和原子性化的问题, 实际解决方案就是加锁;
③但是还存在问题, 就是指令重排序的问题, jvm在实际使用过程中存在指令优化问题, 所以还是需要使用内存屏障防止指令重排问题也就是有序性, 实际解决方案就是加上volatile关键字;
举个问题案例的例子:
Z A B 都有一根火柴共有三根, Z 不知道 A B 有火柴, A和B都知道Z有一根火柴, 但是 A B 不知道对方有火柴, Z 想知道到底有几根火柴
Z 手持一根火柴, 在自己的纸张上写了个数字 1
A 发现 Z 的纸张上写了 1 后, 抄了下来, 发现自己也有一根火柴, 然后把 1 + 1 并且算出结果 2 , 然后出去玩了下
B 发现 Z 的纸张上写了 1 后, 抄了下来, 发现自己也有一根火柴, 然后把 1 + 1 并且算出结果 2 , 然后出去玩了下
这个时候 A 告诉 Z 说, 火柴共有 2 根
B 也这样说
然后 Z 在自己的纸条上写了 2
至此, 错误出现了
然后使用上我们的解决方案的例子:
首先串行化线程执行
其次解决可见性问题和防止指令重排问题
之后
Z A B 都有一根火柴共有三根, Z 不知道 A B 有火柴, A和B都知道Z有一根火柴, 但是 A B 不知道对方有火柴, Z 想知道到底有几根火柴, AB需要排队
Z 手持一根火柴, 在自己的纸张上写了个数字 1
A 发现 Z 的纸张上写了 1 后, 抄了下来, 发现自己也有一根火柴, 然后把 1 + 1 并且算出结果 2, 马上告诉 Z 让他更新, 此时B还在排队中...
B 发现 Z 的纸张上写了 2 后, 抄了下来, 发现自己也有一根火柴, 然后把 2 + 1 并且算出结果 3 马上告诉 Z 让他更新
多线程问题需要满足哪些条件?
存在线程安全问题必须满足三个条件:
1.有共享变量
2.处在多线程环境下
3.共享变量有修改(读和写)操作或者每次修改结果都不一样(修改不是创建)。
只要不满足一点一个条件, 就不存在线程安全问题
自己总结的判断线程不安全方法
首先条件铁定要满足基本条件
多线程
共享资源
修改共享资源
然后这个时候就可以借助原子性操作不可分割的方法对我们的方法调用进行画方框, 每个方框代表一个原子性操作(必须由同一个线程执行完毕后释放才允许换个线程进行执行的代码块)
如果在临界区出现多个方框则表示线程不安全
例如:
假设add和remve都是线程安全的, 但是存在两个方框, 则线程不安全
即时上面的remove方法去掉了剩下一个add方法也是线程不安全的
即使没有错误但是情况结果
100 * 500 = 50000这么多次, 但是len才只有4w左右
还是使用的假设法
前提就是 modCount 不是常量资源共享, 共享资源有改变
elementData不是常量, 共享资源有改变
size也不是常量, 共享资源add方法内部有改变
即便modCount不管也不是线程安全的
多个线程假定同时读取 s 都加上 1 , 这里就会丢失一次加1的机会
还有一种确认线程安全的方法
既然时多线程的, 那么找到共享资源, 判断如果线程执行到了这里, 突然失去时间片, 切换到另一个线程, 此时共享资源是否被前一个线程即时更新了, 如果没有则线程不安全, 否则线程安全
还有一种方法是通过共享资源判断
判断函数的参数是否存在线程不安全的共享资源