基础知识
1、进程和线程的对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2、并行与并发
- 并发(concurrent)是同一时间应对多件事情的能力
- 并行(parallel)是同一时间动手做多件事情的能力
3、异步与同步
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异
Java线程
1、创建线程
方法一:使用 T h r e a d \color{red}{Thread} Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方法一:使用 R u n n a b l e \color{red}{Runnable} Runnable配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
原理之 Thread 与 Runnable 的关系:
- 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三: F u t u r e T a s k \color{red}{FutureTask} FutureTask配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return null;
}
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t = new Thread(task);
t.start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
2、线程运行原理
每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
3、线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
4、常见方法
5、run和start
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
6、sleep与yield
- Sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(CPU闲,让也让不出去)
区别:
- Runnable还是有机会被调用的,但任务调度器不会把时间片分配给Timed Waiting
7、程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
8、join方法
等待某个进程的运行结果
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
输出:0
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出
- 而主线程一开始就要打印 r 的结果,所以只能打印出r=0
就想得到10,怎么办?
可以使用join,等待t1的返回结果
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
9、interrupt方法
打断 sleep,wait,join 的线程 ,这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态(变成false),以 sleep 为例
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
sleep(1);
}, "t1");
t1.start();
}
//main方法中
sleep(0.5);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted())
打断标记:
该线程是不是被其他的线程所干扰,打断过。
打断正常线程时,其实并没有打断,只是改变打断标记(不会清空打断状态),线程还会继续运行,需要我们手动利用打断标记的布尔值去判断进行打断
private static void test2() throws InterruptedException {
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
log.debug(" 打断状态: {}", interrupted);
break;
}
}
},
"t2");
t2.start();
sleep(0.5);
t2.interrupt();
}
10、两阶段终止模式
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
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、不推荐使用的方法
这些方法已过时,容易破坏同步代码块,造成线程死锁
12、主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
线程的状态
1、从操作系统层面看
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状
2、从Java API层面来描述
根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用start() 方法
- RUNNABLE 当调用了start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED ,WAITING ,TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
共享模型之管程
1、共享问题
最后可能会出现正数或者负数,原因在于上下文的切换:
-
出现负数:
-
出现整数:
2、临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界
竞态条件 Race Condition
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
3、synchronized解决方案
使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
4、方法上的synchronized
不加 synchronized 的方法
- 不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
5、变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
6、常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
它们的每个方法是原子的
但注意它们多个方法的组合不是原子的
如:
不可变类线程安全性
- String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
7、Monitor概念
Monitor,在操作系统领域一般翻译为“管程”,在Java邻域就是“对象锁”。
Java对象头,以32位虚拟机为例
其中Klass word指定该对象的类型,即指向一个class对象。
Mark Word的不同状态对应不同的结构,如下:
其中Normal表示该对象是正常状态,hashcode表示每个对象不同的哈希码,age表示分代的年龄,垃圾回收算法可以根据此将其放入老年区。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁,对应上图的Heavyweight Locked,该对象头的Mark Word中的ptr_to_heavyweight_monitor就会指向Monitor。
以一段代码为例:
synchronized(obj){
//临界资源
}
- 当线程Thread2执行synchronized(obj),就会将MarkWord中的指针指向Monitor
- 刚开始,Monitor中的Owner为null,Thread会将Monitor的所有者Owner设置为Thread2,Owner只能指向一个线程
- 当别的线程Thread1执行synchronized(obj),会检查obj有没有关联Monitor,然后检查关联的Monitor有没有主人,即Owner
- 此时,Thread1就会进入EntryList,可以理解成堵塞队列,然后线程Thread1变成BLOCKED状态
- 当Thread2执行完毕,就会让出Owner的位置。然后从Monitor的EntryList中唤醒一个线程,成为该Monitor的下一个主人。
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联 monitor ,不遵从以上规则
8、synchronized原理进阶
轻量级锁
使用场景:
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized
举例子:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
-
当method1执行到synchronized,会创建锁记录(Lock Record)对象,每个栈帧都会包含一个锁记录的结构:
- Object reference指向锁对象,并尝试用cas替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- Object reference指向锁对象,并尝试用cas替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
-
如果cas替换成功,对象头中的Mark Word存储了锁记录地址和状态00,表示该线程给对象加锁。此时图示如下:
-
cas失败的两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。(如代码执行到上述method2的synchronized部分)
即要进行cas交换时,发现Mark Word中的状态是00,且锁记录指向的是本线程。此时会添加一条锁记录,且将锁记录中存储Mark Word的部分设置为null
-
当退出synchronized如果有取值为null的锁记录,表示有重入。此时,将重置锁记录,表示重入计数减一
-
当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
-
当Thread1进行轻量级加锁,Thread0已经加了轻量级锁
-
此时,Thread1加锁失败,进入锁膨胀流程
- 为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后将自己加入到Monitor的EntryList,进入BLOCKED状态
-
当Thread0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败(因为此时存储的是Monitor的地址)。然后进入重量级解锁流程,按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(阻塞会造成上下文切换)。
偏向锁
轻量级锁在没有竞争时,每次重入任需要执行CAS判断。
因此引入偏向锁做进一步优化:
- 只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID就是自己就表示没有竞争,不用重新CAS。
- 以后只要不发生竞争,这个对象就归该线程所有
回忆对象头格式:
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
注意
- 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
撤销偏向锁:
- 调用锁对象的hashCode:
偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销(因为没有位置存储hashCode了)- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
- 其它线程使用对象:
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
批量重偏向
若对象被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
例子:
输出
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; 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));
}
}, "t2");
t2.start();
}
- 第一次cas会启用偏向锁,偏向t1
- 因为没有竞争,因此不会发生撤销偏向锁
- 当进入t2时,标识为撤销锁偏向001,再次申请锁会将偏向锁升级为轻量级锁
- 在后面几次循环中会保持轻量级锁的申请模式
- 当撤销锁偏向达到某个阈值,会将轻量级锁转变成偏向锁偏向t2进程
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
9、wait、notify
-
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程有机会获取对象的锁。无限制等待,直到notify 为止
- wait(long n) 有时限的等待, 到 n 毫秒后结束等待继续执行后续代码,或是被 notify
-
obj.notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒
-
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
wait和sleep的区别
- sleep是Thread类的方法,wait是Object的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用,在获取对象锁之后,才能使用wait
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们状态都是TIMED_WAITING
wait、notify的使用格式
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
wait、notify原理
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味立即获得锁,仍需进入EntryList重新竞争
10、Park & Unpark
它们都是LockSupport类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
原理
每个线程都有自己的一个Parker对象,由三部分组成_counter
、_cond
、_mutex
线程就像个旅人,Parker就像携带的背包:
- 条件变量
_cond
好比背包中的帐篷 _counter
就好比背包中的干粮(0为耗尽、1为充足)_mutex
保证线程在修改 counter 等共享资源时的安全性,避免数据竞争。
调用park,会查看是否需要停下来休息:
- 如果 counter 为 1(有干粮),线程会消耗干粮(counter 设置为 0),然后继续运行。
- 如果 counter 为 0(干粮耗尽),线程会进入“帐篷”休息(即挂起,等待条件满足)。
调用unpark,相当于补充干粮:
- 如果线程在帐篷,就唤醒让其继续前进
- 如果线程在运行,那么下次调用park时,仅是消耗干粮,不停留继续前进
- 背包有限,多次调用unpark仅会补充一份干粮
调用顺序
- 先park再unpark
Thread t1 = new Thread(() -> {
log.debug("start..."); // 线程启动时输出
sleep(1); // 线程休眠 1 秒
log.debug("park..."); // 线程准备挂起
LockSupport.park(); // 挂起线程
log.debug("resume..."); // 线程被唤醒后继续执行
}, "t1");
t1.start(); // 启动线程 t1
sleep(2); // 主线程休眠 2 秒
log.debug("unpark..."); // 主线程输出
LockSupport.unpark(t1); // 唤醒 t1 线程
输出:
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...
线程会被暂停1s
- 先unpark再park
Thread t1 = new Thread(() -> {
log.debug("start..."); // t1线程启动,输出 "start..."
sleep(2); // t1线程休眠2秒
log.debug("park..."); // 2秒后,t1线程输出 "park..."
LockSupport.park(); // t1线程进入等待(被park,挂起)
log.debug("resume..."); // 当被unpark后,t1线程恢复,输出 "resume..."
}, "t1");
t1.start(); // 启动 t1 线程
sleep(1); // 主线程休眠1秒
log.debug("unpark..."); // 主线程输出 "unpark...",此时 t1 仍在休眠
LockSupport.unpark(t1); // 唤醒 t1 线程,即使此时 t1 还没有到达 park 位置
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
并没有暂停
11、重新理解线程转换状态
-
情况1 NEW→RUNNABLE:
调用t.start()
方法时 -
情况2 RUNNABLE↔WAITTING
- t线程使用了synchronized(obj)获得对象锁后,调用
obj.wait()
会让t线程从RUNNABLE→WAITING- 调用
obj.notify()
、obj.notifyAll()
、t.interrupt()
后:- 若竞争锁成功,t从WAITTING→RUNNABLE
- 若竞争锁成功,t从WAITTING→BLOCKED
- 调用
- 当前线程调用
t.join()
,当前线程在t 线程对象的监视器上等待,会让当前线程从RUNNABLE→WAITING- t进程结束,或调用当前线程的
interrupt()
,t从WAITTING→RUNNABLE
- t进程结束,或调用当前线程的
- t线程调用
LockSupport.park()
会让t线程从 RUNNABLE → WAITING- 调用
LockSupport.unpark(t)
或调用了线程的
interrupt()
,会让t线程从WAITTING→RUNNABLE
- 调用
- t线程使用了synchronized(obj)获得对象锁后,调用
-
情况3 RUNNABLE↔TIMED_WAITTING
- 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 - 当前线程调用
t.join(long n)
,当前线程在t 线程对象的监视器上等待,会让当前线程从RUNNABLE→TIMED_WAITING- t进程等待时间超过n毫秒,或运行结束,或调用当前线程的
interrupt()
,t从TIMED_WAITING→RUNNABLE
- t进程等待时间超过n毫秒,或运行结束,或调用当前线程的
- 当前线程调用Thread.sleep(long n),当前线程从RUNNABLE→TIMED_WAITING
- t线程调用
LockSupport.parkNanos(long nacos)
或LockSupport.parkUntil(long millis)
会让t线程从 RUNNABLE → TIMED_WAITING- 调用
LockSupport.unpark(t)
或调用了线程的
interrupt()
,或者是超时等待 ,会让t线程从TIMED_WAITING→RUNNABLE
- 调用
- t线程使用了synchronized(obj)获得对象锁后,调用
-
情况4 RUNNABLE↔BLOCKED
t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE→BLOCKED -
情况5 RUNNABLE↔TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
12、ReentrantLock
相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
12.1 可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
实例:
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
输出:
17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method3
12.2 可打断
实例:
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
public class InterruptibleLockExample {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
// 主线程先获取锁
lock.lock();
try {
log.debug("主线程获得了锁");
// 启动线程 t1
t1.start();
// 主线程睡眠1秒
sleep(1000);
// 打断 t1 线程
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
输出:
18:02:40.520 [main] c.TestInterrupt - 获得了锁
18:02:40.524 [t1] c.TestInterrupt - 启动...
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
izer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17)
at java.lang.Thread.run(Thread.java:748)
18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断
若是不可中断模式,即使使用了interrupt 也不会让等待中断
lock.lockInterruptibly();
可中断模式lock.lock
不可中断模式
12.3 锁超时
实例:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
public class TimedTryLockExample {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
// 尝试获取锁,等待 1 秒
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
log.debug("线程被中断");
e.printStackTrace();
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
// 主线程先获取锁
lock.lock();
try {
log.debug("主线程获得了锁");
// 启动线程 t1
t1.start();
// 主线程睡眠 2 秒
sleep(2000);
} finally {
lock.unlock();
}
}
}
输出:
18:19:40.537 [main] c.TestTimeout - 获得了锁
18:19:40.544 [t1] c.TestTimeout - 启动...
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回
lock.tryLock(1, TimeUnit.SECONDS)
:尝试获取锁1s,失败后返回lock.tryLock()
:尝试获得锁,获取不到立刻返回
12.4 公平锁
ReentrantLock默认是不公平的
- 公平锁指的是线程在获取锁时遵循先来先得的原则,也就是按照线程请求锁的顺序来分配锁资源。
- 非公平锁不保证锁的获取顺序,线程可以在任意时刻尝试获取锁,不管是否有其他线程在等待
设置公平锁的方式:
ReentrantLock lock = new ReentrantLock(true);
12.5 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的
synchronized是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
// 可重入锁
static ReentrantLock lock = new ReentrantLock();
// 两个条件队列:一个用于等待香烟,一个用于等待早餐
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
// 两个标志,表示是否有香烟和早餐
static volatile boolean hasCigarette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) throws InterruptedException {
// 线程1:等待香烟的到来
new Thread(() -> {
try {
lock.lock(); // 获取锁
// 当没有香烟时,进入等待状态
while (!hasCigarette) {
try {
waitCigaretteQueue.await(); // 线程在等待香烟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了香烟"); // 香烟到达后,执行逻辑
} finally {
lock.unlock(); // 释放锁
}
}).start();
// 线程2:等待早餐的到来
new Thread(() -> {
try {
lock.lock(); // 获取锁
// 当没有早餐时,进入等待状态
while (!hasBreakfast) {
try {
waitBreakfastQueue.await(); // 线程在等待早餐
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了早餐"); // 早餐到达后,执行逻辑
} finally {
lock.unlock(); // 释放锁
}
}).start();
// 主线程模拟送早餐
Thread.sleep(1000); // 等待1秒
sendBreakfast();
// 主线程模拟送香烟
Thread.sleep(1000); // 再等待1秒
sendCigarette();
}
// 模拟送香烟
private static void sendCigarette() {
lock.lock(); // 获取锁
try {
log.debug("送香烟来了"); // 日志:香烟送达
hasCigarette = true; // 更新状态
waitCigaretteQueue.signal(); // 唤醒等待香烟的线程
} finally {
lock.unlock(); // 释放锁
}
}
// 模拟送早餐
private static void sendBreakfast() {
lock.lock(); // 获取锁
try {
log.debug("送早餐来了"); // 日志:早餐送达
hasBreakfast = true; // 更新状态
waitBreakfastQueue.signal(); // 唤醒等待早餐的线程
} finally {
lock.unlock(); // 释放锁
}
}
// 模拟日志输出工具
static class log {
public static void debug(String message) {
System.out.println(Thread.currentThread().getName() + ": " + message);
}
}
}
共享模型之内存
1、Java内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
2、可见性
不可见的情况:
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变量都是直接操作主存
3、有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
指令重排是编译器或处理器为了优化性能而对指令执行顺序进行调整的一种现象。在代码中,ready = true 可能会在 num = 2 之前执行,从而导致线程1在检查 ready 时看到的是 true,但 num 还没有被赋值为 2,这会导致线程1使用了错误的值。
解决方法:
volatile修饰的变量,可以禁用指令重排
int num = 0;
volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
4、原理之volatile
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结:
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
/*
- 由于 x 是 volatile 变量,t2 应该能立即看到 t1 对 x 的修改。
- volatile 保证了可见性:当 t1 修改 x 的值时,这个修改会被立即写入到主存,而不是存储在线程的本地缓存中。于是,t2 读取 x 时会看到最新的值。
*/
volatile static int x;
public static void main(String[] args) {
new Thread(() -> {
x = 10; // 线程 t1 对 x 进行写操作
}, "t1").start();
new Thread(() -> {
System.out.println(x); // 线程 t2 对 x 进行读操作
}, "t2").start();
}
- 线程对volatile变量的写,对接下来其它线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
//线程启动之前对共享变量的所有写操作对该线程启动后是可见的
//由于主线程在启动 t2 之前执行了 x = 10,因此 t2 将会读取到 x 的最新值 10。
输出结果将是 10。
static int x;
public static void main(String[] args) {
x = 10; // 主线程对 x 进行写操作
new Thread(() -> {
System.out.println(x); // 线程 t2 读取 x
}, "t2").start(); // 启动 t2 线程
}
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
/*
- join() 保证了线程的执行顺序:主线程会等到子线程 t1 完成后才会打印 x 的值。
- 由于 t1 修改了 x 并且已经结束,主线程读取到的 x 值是 10。
*/
static int x; // 静态变量 x
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
x = 10; // 子线程 t1 对 x 进行写操作
}, "t1");
t1.start(); // 启动 t1 线程
t1.join(); // 等待 t1 线程执行完毕
System.out.println(x); // 主线程打印 x 的值
}
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
/*
线程 t1 对 t2 的打断操作发生在 t2 检测到它被打断之前。因此,线程 t1 在打断前对 x 的写操作,对线程 t2 是可见的。
/*
static int x = 0;
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 线程 t2 持续运行,直到被打断
}
System.out.println("Thread t2 interrupted. x = " + x);
}, "t2");
t2.start();
Thread t1 = new Thread(() -> {
x = 10; // 线程 t1 对 x 进行写操作
t2.interrupt(); // 线程 t1 打断 t2
}, "t1");
t1.start();
}
共享模型之无锁
1、 CAS与volatile
CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
线程不安全的余额扣款和存款:
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
}
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
}
因为指令交错的原因,会导致拿到balance的旧值,导致线程不安全。
无锁方式实现线程安全:
class AccountSafe implements Account {
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) {
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-amount);
}
}
这里借助了 AtomicInteger
来实现无锁的共享变量安全问题。
其中的关键是compareAndSet
,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
其实 CAS 的底层是
lock cmpxchg
指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。工作方式如下:
- cmpxchg(Compare-and-Exchange)指令:这是比较和交换指令,比较寄存器中的值与目标内存位置的值,如果相等则将新值写入该位置,否则将内存中的值加载到寄存器中。
- lock 前缀:在多核 CPU 下,lock 前缀用于锁定总线,确保当前 CPU 核对内存的修改是原子的,其他 CPU 无法同时访问该内存位置,直到该操作完成。
这意味着即便在多核环境下,lock cmpxchg 指令也可以防止多个处理器同时修改同一块内存,保证了原子性操作。
2、 为什么无锁效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
- 打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
- 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换
3、 CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
- CAS 体现的是无锁并发、无阻塞并发
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
4、原子整数
Java 的 java.util.concurrent并发包提供了一系列原子类,主要用于在多线程环境下安全地执行基本的数值操作,而无需使用显式的锁。以下是一些常用的原子类:
- AtomicBoolean
- AtomicInteger
- AtomicLong
以AutomicInteger为例:
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));
使用原子类的优点:
- 无锁操作:原子类使用底层的硬件原子操作,避免了加锁带来的性能开销。
- 简化代码:通过提供原子操作方法,减少了显式的同步代码,提升了代码的可读性和可维护性。
- 性能优势:在多线程场景下,原子类通常能提供比传统锁机制更高的性能,尤其是在高竞争的环境下。
5、原子引用
原子引用类型的引入,主要是为了确保在多线程环境中对对象引用的原子性操作和可见性。
常见原子引用:
- AtomicReference
AtomicReference是一个用于原子性操作对象引用的类。它允许对对象的引用进行安全的更新,同时提供一些原子操作的方法。
常用方法:- get(): 获取当前的对象引用。
- set(newValue): 设置新的对象引用。
- compareAndSet(expectedValue, newValue): 如果当前的引用是预期的值,则原子地将其设置为新值。这是原子引用类型最常用的方法,用于确保对对象的安全更新。
AtomicReference<YourObjectType> atomicRef = new AtomicReference<>(initialValue);
- AtomicMarkableReference
AtomicMarkableReference 是一个扩展了 AtomicReference 的类,除了存储对象引用外,还可以存储一个布尔标记。它可以用于需要跟踪对象引用状态的场景,例如跟踪对象是否被标记为“已删除”。
常用方法:- get(boolean[] marked): 获取当前的对象引用和标记状态。
- compareAndSet(expectedReference, newReference, expectedMark, newMark): 如果当前引用和标记与预期值匹配,则原子地将其更新为新值和新标记。
AtomicMarkableReference<YourObjectType> atomicMarkableRef = new AtomicMarkableReference<>(initialValue, initialMark);
- AtomicStampedReference
AtomicStampedReference 也是一个原子引用类,但它的设计用于解决 ABA 问题。它通过引入一个整型“戳”来跟踪对象引用的版本。
常用方法:- get(int[] stamp): 获取当前的对象引用和戳值。
- compareAndSet(expectedReference, newReference, expectedStamp, newStamp): 如果当前的引用和戳值与预期匹配,则原子地将其更新为新值和新戳。
6、原子数组
java.util.concurrent.atomic包提供了一些原子数组类,允许对数组中的元素进行原子性操作。
原子数组类
- AtomicIntegerArray:支持对整数数组元素的原子操作。
- AtomicLongArray:支持对长整型数组元素的原子操作。
- AtomicReferenceArray:支持对对象数组元素的原子操作。
例子:
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class ArrayDemo {
/**
* 执行多线程操作的示例方法。
*
* @param arraySupplier 提供数组,可以是线程不安全数组或线程安全数组
* @param lengthFun 获取数组长度的方法
* @param putConsumer 自增方法,回传 array 和 index
* @param printConsumer 打印数组的方法
* @param <T> 数组的类型
*/
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get(); // 获取数组实例
int length = lengthFun.apply(array); // 获取数组长度
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % length); // 自增操作
}
}));
}
ts.forEach(Thread::start); // 启动所有线程
ts.forEach(t -> {
try {
t.join(); // 等待所有线程结束
} catch (InterruptedException e) {
e.printStackTrace();
}
});
printConsumer.accept(array); // 打印数组
}
}
- 不安全的数组
demo(
()->new int[10],
(array)->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
);
输出:
[9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698]
- 安全的数组
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
输出:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
7、字段更新器
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
Java 提供的原子类,专门用于更新对象字段的原子性。这些更新器类可以确保在多线程环境中安全地更新对象的字段:
- AtomicReferenceFieldUpdater :
用于更新对象中某个字段的引用类型。它通过反射实现,对字段的访问和更新都具有原子性。
例子:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
class User {
volatile String name; // volatile 确保可见性
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
User user = new User("Alice");
// 创建 AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater<User, String> updater =
AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
// 更新字段
boolean updated = updater.compareAndSet(user, "Alice", "Bob");
System.out.println("Updated: " + updated); // 输出: Updated: true
System.out.println("Current Name: " + user.getName()); // 输出: Current Name: Bob
}
}
- AtomicIntegerFieldUpdater:
用于更新对象中某个整型字段。与 AtomicReferenceFieldUpdater 类似,它也通过反射实现原子更新。
例子:
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
class Counter {
volatile int count; // volatile 确保可见性
public Counter(int initialCount) {
this.count = initialCount;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter(0);
// 创建 AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater<Counter> updater =
AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");
// 更新字段
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
updater.incrementAndGet(counter); // 原子自增
}
}).start();
}
// 等待所有线程完成
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + counter.getCount());
}
}
- AtomicLongFieldUpdater:
用于更新对象中某个长整型字段。用法与 AtomicIntegerFieldUpdater 类似。
7、原子累加器
原子累加器(Atomic Accumulators)是 Java 提供的一种高效并发工具,位于 java.util.concurrent.atomic 包中。它们用于在高并发环境下处理数值累加操作,类似于 AtomicInteger 和 AtomicLong,但专门用于处理大量并发操作场景下的累加操作,避免性能瓶颈。
常见的原子累加器包括:
- LongAdder
- DoubleAdder
- LongAccumulator
- DoubleAccumulator
相比 AtomicLong 和 AtomicDouble,LongAdder 和 DoubleAdder 在高并发下表现更好。这是因为它们通过分段累加的方式,减少了线程竞争:
- LongAdder 和 DoubleAdder 使用多个变量(Cell)来分别保存值,当多个线程并发更新时,每个线程可以更新不同的变量,从而降低锁竞争。
- 最终通过累加所有分段的值,得到正确的结果。
import java.util.concurrent.atomic.LongAdder;
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder longAdder = new LongAdder();
Thread[] threads = new Thread[10];
// 启动10个线程,每个线程对 longAdder 加 1000 次
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
longAdder.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完
for (Thread thread : threads) {
thread.join();
}
// 输出最终结果
System.out.println("Final Value: " + longAdder.sum());
}
}
相比 LongAdder 和 DoubleAdder,LongAccumulator 和 DoubleAccumulator 提供了更多的灵活性,允许用户定义累加规则。它们不仅限于加法操作,可以支持任意的自定义操作,如最大值、最小值等。
LongAccumulator 和 DoubleAccumulator 需要两个参数:
- 一个是函数(如 LongBinaryOperator),用于定义累加规则。
- 另一个是初始值。
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.LongBinaryOperator;
public class LongAccumulatorDemo {
public static void main(String[] args) throws InterruptedException {
// 创建 LongAccumulator,操作为 max 取最大值,初始值为 0
LongBinaryOperator maxFunction = (x, y) -> Math.max(x, y);
LongAccumulator accumulator = new LongAccumulator(maxFunction, 0);
Thread[] threads = new Thread[10];
// 启动10个线程,每个线程对 accumulator 累加随机数
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
accumulator.accumulate((long) (Math.random() * 1000));
}
});
threads[i].start();
}
// 等待所有线程执行完
for (Thread thread : threads) {
thread.join();
}
// 输出最终结果(最大值)
System.out.println("Final Max Value: " + accumulator.get());
}
}
共享模型之不可变
1、不可变对象
如果一个对象的内部状态(属性)无法被修改,那么它就是线程安全的,因为不存在并发修改的风险。这样的对象称为不可变对象(Immutable Object)。
的 String 类也是不可变的,以它为例,说明一下不可变设计的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// Constructor, methods, and other implementation details...
}
final的使用
发现该类、类中所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
疑问:String也可以赋值,为什么线程还是安全的?
- String 在 Java 中是线程安全的,原因在于它是 不可变对象。虽然你可以给 String 变量重新赋值,但这是变量的引用变化,而不是 String 对象本身的变化
- 一旦一个 String 对象被创建,它的内容就不能被改变。换句话说,String 对象的值是不可变的。例如,当你创建一个 String 对象 “Hello”,这个对象在内存中的内容是永远固定的,任何对它的操作都不会修改这个对象。
- 你可以改变指向某个 String 对象的引用,即可以让同一个变量指向不同的 String 对象,但这并不会修改原来的 String 对象。
- 修改引用不会导致线程不安全的原因在于 引用本身的修改 和 对象的修改 是两件不同的事情。引用的修改只是在当前线程的栈帧中改变了变量指向的内存地址,属于局部操作,和其他线程是独立的,不会引发线程安全问题。
2、享元模式
享元模式(Flyweight Pattern)是一种结构性设计模式,用于减少程序中对象的数量,以节省内存并提高性能。享元模式通过共享相同的对象来实现对象的复用,尤其是在大量相似对象的情况下。
如,在JDK中Boolean,Byte,Short,Integer,Long,Character等包装类提供了 valueOf 方法,例如 Long 的valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int) l + offset];
}
return new Long(l);
}
3、享元模式实现线程池
- 自定义拒绝策略接口
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
- 自定义任务队列
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 阻塞队列实现
*
* @param <T> 队列中存储的任务类型
*/
class BlockingQueue<T> {
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2. 锁
private ReentrantLock lock = new ReentrantLock();
// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5. 容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
/**
* 带超时的阻塞获取
*
* @param timeout 超时时间
* @param unit 时间单位
* @return 队列中的任务,或 null 如果超时
*/
public T poll(long timeout, TimeUnit unit) {
lock.lock(); // 获取锁
try {
// 将 timeout 统一转换为纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) { // 当队列为空时
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null; // 超时,返回 null
}
nanos = emptyWaitSet.awaitNanos(nanos); // 等待
} catch (InterruptedException e) {
e.printStackTrace(); // 打印异常信息
}
}
T t = queue.removeFirst(); // 移除并返回队列头部的任务
fullWaitSet.signal(); // 唤醒等待添加任务的线程
return t;
} finally {
lock.unlock(); // 释放锁
}
}
/**
* 阻塞获取
*
* @return 队列中的任务
*/
public T take() {
lock.lock(); // 获取锁
try {
while (queue.isEmpty()) { // 当队列为空时
try {
emptyWaitSet.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace(); // 打印异常信息
}
}
T t = queue.removeFirst(); // 移除并返回队列头部的任务
fullWaitSet.signal(); // 唤醒等待添加任务的线程
return t;
} finally {
lock.unlock(); // 释放锁
}
}
/**
* 阻塞添加任务
*
* @param task 要添加的任务
*/
public void put(T task) {
lock.lock(); // 获取锁
try {
while (queue.size() == capacity) { // 当队列已满时
try {
System.out.println("等待加入任务队列 " + task + " ..."); // 打印等待信息
fullWaitSet.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace(); // 打印异常信息
}
}
System.out.println("加入任务队列 " + task); // 打印添加信息
queue.addLast(task); // 将任务添加到队列尾部
emptyWaitSet.signal(); // 唤醒等待获取任务的线程
} finally {
lock.unlock(); // 释放锁
}
}
/**
* 带超时时间的阻塞添加任务
*
* @param task 要添加的任务
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return true 如果成功添加,false 如果超时
*/
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock(); // 获取锁
try {
long nanos = timeUnit.toNanos(timeout); // 转换超时时间为纳秒
while (queue.size() == capacity) { // 当队列已满时
try {
if (nanos <= 0) {
return false; // 超时,返回 false
}
System.out.println("等待加入任务队列 " + task + " ..."); // 打印等待信息
nanos = fullWaitSet.awaitNanos(nanos); // 等待
} catch (InterruptedException e) {
e.printStackTrace(); // 打印异常信息
}
}
System.out.println("加入任务队列 " + task); // 打印添加信息
queue.addLast(task); // 将任务添加到队列尾部
emptyWaitSet.signal(); // 唤醒等待获取任务的线程
return true; // 成功添加
} finally {
lock.unlock(); // 释放锁
}
}
/**
* 获取当前队列大小
*
* @return 当前队列的任务数量
*/
public int size() {
lock.lock(); // 获取锁
try {
return queue.size(); // 返回队列大小
} finally {
lock.unlock(); // 释放锁
}
}
/**
* 尝试添加任务,如果队列已满则根据拒绝策略进行处理
*
* @param rejectPolicy 拒绝策略
* @param task 要添加的任务
*/
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock(); // 获取锁
try {
// 判断队列是否满
if (queue.size() == capacity) {
rejectPolicy.reject(this, task); // 执行拒绝策略
} else { // 有空闲
System.out.println("加入任务队列 " + task); // 打印添加信息
queue.addLast(task); // 将任务添加到队列尾部
emptyWaitSet.signal(); // 唤醒等待获取任务的线程
}
} finally {
lock.unlock(); // 释放锁
}
}
}
- 自定义线程池
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
/**
* 自定义线程池实现
*/
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
// 拒绝策略
private RejectPolicy<Runnable> rejectPolicy;
/**
* 构造方法
*
* @param coreSize 核心线程数
* @param timeout 超时时间
* @param timeUnit 时间单位
* @param queueCapacity 队列容量
* @param rejectPolicy 拒绝策略
*/
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity,
RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
/**
* 执行任务
*
* @param task 要执行的任务
*/
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if (workers.size() < coreSize) { // 当前工作线程数小于核心线程数
Worker worker = new Worker(task); // 创建新线程
System.out.println("新增 worker " + worker + ", " + task); // 打印新增信息
workers.add(worker); // 添加新线程到集合
worker.start(); // 启动新线程
} else {
// 根据拒绝策略尝试将任务放入任务队列
taskQueue.tryPut(rejectPolicy, task);
}
}
}
/**
* 工作线程类
*/
class Worker extends Thread {
private Runnable task; // 当前线程要执行的任务
public Worker(Runnable task) {
this.task = task; // 初始化任务
}
@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
System.out.println("正在执行... " + task); // 打印执行信息
task.run(); // 执行任务
} catch (Exception e) {
e.printStackTrace(); // 打印异常信息
} finally {
task = null; // 清空任务
}
}
synchronized (workers) {
System.out.println("worker 被移除 " + this); // 打印移除信息
workers.remove(this); // 从工作线程集合中移除当前线程
}
}
}
}
4、ThreadPoolExecutor
- 线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
这些信息存储在一个原子变量 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<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时起个好名字
- handler 拒绝策略
- 工作方式:
- 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
- 如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize - corePoolSize 数目的线程来救急。
- 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现:
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
- 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由
keepAliveTime 和 unit 来控制。
根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
- newFixedThreadPool
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
- 适用于任务量已知,相对耗时的任务
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, // 核心线程数
nThreads, // 最大线程数
0L, // 线程保持存活的时间
TimeUnit.MILLISECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>() // 使用的任务队列
);
}
- newCachedThreadPool
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味
- 着全部都是救急线程(60s 后可以回收)
- 救急线程可以无限创建
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交
货) - 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, // 核心线程数,表示无核心线程
Integer.MAX_VALUE, // 最大线程数,允许创建任意数量的线程
60L, // 线程保持存活的时间
TimeUnit.SECONDS, // 时间单位,表示为秒
new SynchronousQueue<Runnable>() // 使用的任务队列,支持无缓冲的任务队列
);
}
- newSingleThreadExecutor
- 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1, // 核心线程数,表示只有一个线程
1, // 最大线程数,也为1,确保只有一个线程
0L, // 线程保持存活的时间
TimeUnit.MILLISECONDS, // 时间单位,表示为毫秒
new LinkedBlockingQueue<Runnable>() // 使用的任务队列,支持阻塞的任务队列
)
);
}