脚本宝典收集整理的这篇文章主要介绍了Java并发编程,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
进程
线程
二者对比
单核 cpu 下,线程实际还是 串行执行
的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行
的 。总结为一句话就是: 微观串行,宏观并行
一般会将这种 线程轮流使用 CPU
的做法称为并发
引用 Rob Pike 的一段描述:
一:使用 Thread (继承Thread或匿名内部类重写run方法)
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@override
// run 方法内实现了要执行的任务
public void run() {
LOG.debug("hello");
}
};
t1.start();
二:使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Java8以后可以使用lambda精简代码
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
Thread 与 Runnable
public class Thread implements Runnable {
/* Make sure registerNatives is the First thing <clinit> does. */
PRivate static native void registerNatives();
static {
registerNatives();
}
private volatile String name;
private int priority;
private Thread threadQ;
private long eetop;
}
小结:
三:FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
栈与栈帧 Java 虚拟机栈
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
线程上下文切换 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadstateException | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 |
|
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
start 与 run
结果都是执行了run方法中的代码,但是调用的线程不同
注意:
sleep 与 yield
sleep
yield
线程优先级
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
// Thread.sleep();
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();
join
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
分析如下
interrupt
打断 sleep,wait,join 的线程并重置中断状态为 false
测试发现
LockSupport.park()
阻塞也会受中断状态的影响,即 interrupt 也能打断 park 中的线程,但是区别是其并不会重置中断状态为 false ,这就会导致打断一次后不使用interrupted
来重置状态的话我们的park/unpark
就没用了
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
sleep(1);
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
}
输出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcast.n2.util.SleePEr.sleep(Sleeper.java:8)
at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
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();
}
输出
20:57:37.964 [t2] c.TestInterrupt - 打断状态: true
打断 park 线程
打断 park 线程, 不会清空打断状态
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
输出
21:11:52.795 [t1] c.TestInterrupt - park...
21:11:53.295 [t1] c.TestInterrupt - unpark...
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true
如果打断标记已经是true,则park会失效
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
输出
21:13:48.783 [Thread-0] c.TestInterrupt - park...
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.812 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
可以使用
Thread.interrupted()
清除打断状态,不然我们打断一次后park/unpark
就没用了
不推荐方法
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
例:
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
输出
08:26:38.123 [main] c.TestDaemon - 开始运行...
08:26:38.213 [daemon] c.TestDaemon - 开始运行...
08:26:39.215 [main] c.TestDaemon - 运行结束...
注意:
垃圾回收器线程就是一种典型的守护线程
毕竟,用户程序都运行结束了,还回收垃圾干嘛
Java API层面对应线程有六种状态
NEW
线程刚被创建,但是还没有调用 start()
方法RUNNABLE
当调用了 start()
方法之后,注意,Java API 层面的 RUNNABLE
状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO
导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分TERMINATED
当线程代码运行结束反正这里需要注意的事 BLOCKED 和我们的两个 WAITING 状态分别对应的是 Monitor 中的 EntryList 和 WaitSet,也就是说处于
wait
或者sleep
状态的线程在 WaitSet 中,是没资格争夺锁的,但是 BLOCKED 状态的线程是可以去争夺锁的顺带说一句,synchronized 是非公平锁,也就是说不存在先来后到之说,只要有锁空闲,那么 EntryList 里面的线程谁抢到算谁的,当然,TIMED_WAITING 时间一到或者是
WAITING
被唤醒那么第一时间也是去抢锁,抢不到就去 EntryList 里等下一次锁空闲
操作系统使用信号量解决并发问题,Java选择使用管程(Monitor)解决并发问题。信号量和管程是等价的,可以使用信号量实现管程,也可以使用管程实现信号量。
管程就是指管理共享变量,以及对共享变量的相关操作。具体到 Java 语言中,管程就是管理类的成员变量和方法,让这个类是线程安全的。
问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++
而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
Java内存模型JMM
Java内存模型中规定所有共享变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对共享变量的操作(读取赋值等)必须在工作内存中进行,首先要将共享变量从主内存拷贝的自己的工作内存空间,然后对其进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
首先我们使用阻塞式的解决方案 synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
如果synchronized加在一个类的普通方法上,那么相当于synchronized(this),即对对象加锁。
如果synchronized加载一个类的静态方法上,那么相当于synchronized(Class对象),即对类加锁。
成员变量和静态变量是否线程安全?
局部变量是否线程安全?
常见线程安全类
这里说它们是线程安全的是指多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的,但注意它们多个方法的组合不是原子的。
String、Integer等不可变类,由于其内部存放数据的属性都定义为final,因此是不可修改的,也就是说每次修改其实就是重新创建替换,因此是线程安全的。
Java中的每个对象都与一个monitor(管程)关联,线程可以
lock
或unlock
monitor。 一次只能有一个线程在monitor上持有锁。
HotSpot对象的内存布局
即:普通对象的对象头包含Mark Word和类型指针,如果是数组那就多一个数组长度这一项
至于Mark Word的结构的话,则是对应着不同锁状态有着不同内容
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权,抢到了就是成功获取锁了;执行 monitorexit 指令则是释放了Monitor的所有权。
Monitor 结构如下
wait
或 sleep
等休眠的注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
WaitSet和EntryList区别?
- WaitSet 中的线程在没有被唤醒之前是没有权利去争夺锁的使用权的,被唤醒后可以去争夺锁,没争取到就待在 EntryList
- EntryList 中的线程每次都能去争夺线程使用权,其实就是尝试成为 Monitor 的 owner(因为synchronized是非公平锁)
ObjectMonitor
在HotSpot虚拟机中,Monitor是基于C++的ObjectMonitor类实现的,其主要成员包括:
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
对应字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用 (synchronized开始)
3: dup
4: aStore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
所以用人话解释一下synchronized
的底层原理:
monitorenter
线程执行monitorenter
指令时尝试获取monitor
的所有权
如果monitor
的记录数(ObjectMonitor的_recursions
字段)为0,则该线程进入monitor
,然后将记录数置为1,该线程即为monitor
的所有者。
如果线程已经占有该monitor
(_owner
指向当前线程),只是重新进入,则进入monitor
的记录数加1。
如果其他线程已经占用了monitor
,则该线程进入阻塞状态(进入EntryList),直到monitor
的进入数为0,再重新尝试获取monitor
的所有权。
monitorexit
执行monitorexit
的线程必须是monitor
持有者
指令执行时,monitor
的记录数减1,如果减1后记录数为0,那线程退出monitor
,不再是这个monitor
的持有者。其他被这个monitor
阻塞的线程可以尝试去获取这个 monitor
的所有权。
注意: 方法级别的
synchronized
不会在字节码指令中有所体现,方法的同步并没有通过指令monitorenter
和monitorexit
来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor
,获取成功之后才能执行方法体,方法执行完后再释放monitor
, 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
小故事 故事角色:
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的 但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢? 小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式 后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。 于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字 后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
首先,还未加锁之前:
对象头中的 MarkWord 还是老样子,同时每个线程的栈帧中存在一个锁记录结构,此时两个是互无关系的两个结构
执行synchronized代码加锁:
让锁记录中 Object reference 指向锁对象,并尝试用 CAS自旋 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
CAS成功:
对象头的MarkWord为锁记录地址和状态00
栈帧中的锁记录为原来的MarkWord(HashCode ...01)和指向锁对象的引用
CAS失败:
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
注意,到这里的话,我们对 synchronized的可重入 就有三种解释(其实是两种,偏向和轻量是一样的)了:
重量级锁可重入原理就是加锁时如果线程已经占有该
monitor
(_owner
指向当前线程),则锁重入,即monitor
的记录数字段加1。如果是轻量锁可重入,则是加锁时CAS失败(因为 MarkWord 被换走了)并且指向自己,因此重入时在添加一条锁记录作为计数,新添加的锁记录取值为 null(注意这个重入锁记录取值为null很关键,这是我们判断是否还处于重入状态的依据)。
如果是偏向锁可重入,线程第一次成功加锁时,会在对象头和线程的栈帧中的锁记录中存储所偏向的线程id,重入时直接锁记录增加即可。
可以发现,偏向锁和轻量锁都要使用锁记录的,那么既然感觉差不多,偏向锁到底优化了啥?
前面我们说了,轻量锁每次都需要 CAS 操作来尝试替换MarkWord 再根据成功与否来决定是否添加锁记录,但是偏向锁尽管也有锁记录,却不需要再CAS替换了,只需要判断是否线程ID是自己,所以相对而言轻松很多
就这么看我也没感觉出重量锁相较于这两个有啥特别重的地方,那么重量锁到底重在哪?
我们知道重量锁和Monitor管程有关,其提供了各种复杂的操作,例如可以控制线程的唤醒阻塞、线程的阻塞队列等等,而我们的轻量和偏向从实现方式来看貌似不具有这种能力,当然,重量锁能够如此强大,自然是需要消耗更多的性能,因为需要借助系统的内核功能,同时维护这么多东西也需要消耗性能!
可是为什么我反正都是加的synchronized关键字,还是能用上重量锁的全部功能例如阻塞、唤醒呢?
废话,你喵的锁用那些方法的时候就说明有线程出现竞争,这时候锁已经是膨胀成重量锁了
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
注意 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
一个对象创建时:
偏向锁撤销:
wait/notify
时,证明已经不是一个线程在使用锁了,当然会锁膨胀,撤销偏向锁批量重定向:
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销:
当撤销偏向 锁阈值超过 40 次后,Jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除就是字面意思,虚拟机会根据自己的代码检测结果取消一些加锁逻辑。虚拟机通过检测会发现一些代码中不可能出现数据竞争,但是代码中又有加锁逻辑,为了提高性能,就消除这些锁。如果一段代码中,在堆上的所有数据都不会被其他线程访问到,那就可以把它们当成线程私有数据,自然就不需要同步加锁了。
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这里不是锁膨胀那种改变锁,而是改变范围
原代码
public void method(String s1, String s2){
synchronized(this){
System.out.println("s1");
}
synchronized(this){
System.out.println("s1");
}
}
锁粗化
public void method(String s1, String s2){
synchronized(this){
System.out.println("s1");
// }
// synchronized(this){
System.out.println("s1");
}
}
API 介绍:
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n)
有时限的等待, 到 n 毫秒后结束等待,或是被 notify
原理:
这么看来,WAITING状态还是要比BLOCKED低一点
sleep(long n) 和 wait(long n) 的区别:
LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
可以先park再unpark
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
也可以先unpark再park
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
特点 与 Object 的 wait/notify 相比
_mutex
)原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter
标志 , _cond
阻塞队列 和 _mutex
互斥锁
_counter
是不是0,是0,就阻塞在_cond
里,并再赋值一下0给_counter
,
调用unpark
,将_counter
赋值为1,并唤醒_cond
里的线程,然后再把_counter
置为0,线程恢复运行。_counter
是1,不需要阻塞,继续运行,并把_counter
置为0。Thread.interrupt() 方法: 作用是改变线程的中断状态位,即设置为 true。
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。
如果线程被 Object.wait
Thread.join
和 Thread.sleep
三种方法之一阻塞,此时调用该线程的 interrupt()
方法,那么该线程将抛出一个 InterruptedException
中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到 wait()
sleep()
join()
时,才马上会抛出 InterruptedException
。
注意这里的 interrupt() 只是改变中断状态位,而我们的
wait
join
sleep
会感知这个状态位,一旦变为 true 就抛出异常,并把状态为该回false
从而导致对应的阻塞状态终止,同时wait
是与锁有关的,被终端不意味着马上就能执行接下来的代码,还是要去monitor
的EntryList
里抢夺锁
this.interrupted():测试当前线程是否已经中断(静态方法)并且将状态位置为 false
this.isInterrupted():测试线程是否已经中断,但是不能清除状态标识。
NEW --> RUNNABLE
t.start()
方法时,由 NEW --> RUNNABLERUNNABLE <--> WAITING
线程用 synchronized(obj)
获取了对象锁后
obj.wait()
方法时,t 线程从 RUNNABLE --> WAITING,线程进入WaitSetobj.notify()
, obj.notifyAll()
, t.interrupt()
时
当前线程调用 join()
方法时,当前线程从 RUNNABLE --> WAITING
线程运行结束,或调用了当前线程的 interrupt()
时,当前线程从 WAITING --> RUNNABLE
当前线程调用 LockSupport.park()
方法会让当前线程从 RUNNABLE --> WAITING
RUNNABLE <--> TIMED_WAITING
synchronized(obj)
获取了对象锁后
obj.wait(long n)
方法时,线程从 RUNNABLE --> TIMED_WAITINGobj.notify()
、obj.notifyAll()
, t.interrupt()
时
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITINGThread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLERUNNABLE <--> BLOCKED
线程用 synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于互斥锁的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
相对于 synchronized 它具备如下特点
可中断
t1.interrupt()
可以设置超时时间
lock.tryLock(1, TimeUnit.SECONDS)
可以设置为公平/非公平锁
默认非公平,可设置公平
ReentrantLock lock = new ReentrantLock(false);
支持多个条件变量 ReentrantLock 支持多个条件变量的,即我们可以定义多个条件变量,类似多个WaitSet,这样我们就能对锁住的线程分开唤醒了
与 synchronized 一样,都支持可重入 基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
JMM 即Java Memory Model
,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
Java内存模型说白了就是一个规范:
规定了所有共享变量都存储在主内存(堆内存),主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存(栈内存)中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
原子性问题:首先我们需要指导,一条程序指令的原子性是不需要我们担心的,即它不会受到多线程或者其它条件的干扰,但是代码块或 i++ 这样的执行了多条指令的代码的原子性就无法保证了,我们可以使用 synchronized 来保证,但是有一说一,太重了。
可见性问题:当两个线程都在使用同一个变量的时候,由于 JIT 有时会将频繁使用的变量值缓存到自己的工作内存中减少对主存中变量的访问,因此在另一个线程修改了变量之后,可能导致当前线程没有能够及时获取到变量的最新取值。
解决方法:volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
注意,我们使用System.out.println()
之后其实就和加上了 volatile 关键字一样的效果。此外,synchronized 也是可以保证可见性的。
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
上面代码,无论是先执行i = xx
还是j = xx
都没啥影响,因此最终底层对这两段代码的执行顺序是不确定的!
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。
现代处理器会设计将指令划分为了更小的阶段,例如
取指令
读指令
等,在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
解决方法:还是他喵的 volatile 这个关键字结合 CAS 简直不要太好用
前面说了 volatile 可以保证代码的有序性和可见性,注意没有原子性哈,那个是要靠加锁来保证的 volatile底层原理:内存屏障
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结。包含了以下规则,同时离开了下列规则的话,Jvm 并不能保证一个共享变量的写对另一个共享变量可见
程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized
就是管程的实现)
volatile变量规则:就是如果一个线程先去写一个volatile
变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()
规则
线程中断规则:对线程interrupt()
方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断
传递性规则:这个简单的,就是happens-before
原则具有传递性,即hb(A, B)
, hb(B, C)
,那么hb(A, C)
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before
它的finalize()
方法。
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance
)时的线程安全,并思考注释中的问题
饿汉式:类加载就会导致该单实例对象被创建 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
实现一:
// 问题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;
}
}
实现二:
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
实现三:
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;
}
}
实现四: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;
}
}
}
实现五:
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;
}
}
问题:
我们有一个 Amount
金额类,其中有一个 withdraw
扣款方法,该方法调用时执行 balance -= amount
命令,咋一看没问题,毕竟也就一行代码,但是多线程场景下是会有问题的!
我们对该行代码的字节码指令进行分析,首先需要将 balance
和 amount
装载进操作数栈,运算完成后再写回局部变量表。
了解了字节码指令就能看出什么问题了,多线程下,肯定会发生指令交错执行的情况,这样最终得到的数据结果就会有异常。
解决思路:
CAS 全称是 compare and swap
,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 -- 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值相比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。在 Java 中,Java 并没有直接实现 CAS,而是通过 C++ 内联汇编的形式实现的。然后Java通过本地方法栈来调用。
volatile 前面不是讲过了么?为什么这里又提到了?因为我们的 CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
synchronized
会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。 CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,再重试。 synchronized 是基于悲观锁的思想:防着其它线程来修改共享变量,使用时不允许任何线程访问
CAS 体现的是无锁并发、无阻塞并发
J.U.C 并发包提供了:AtomicBoolean
AtomicInteger
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
我们需要解决并发问题的操作不只是对整数的修改,有时我们的对象修改读取也是要进行原子操作的,例如我们的数值封装类或者 BigDecimal
因此我们可以使用原子引用
示例:
AtomicReference<BigDecimal> ref = new AtomicReference<>();
ref.COMpareAndSet(new BigDecimal(1),new BigDecimal(2));
问题:什么叫 ABA 问题?顾名思义,由 A 到 B 再到 A ,我们前面说过,CAS 本质是比较再替换,那么这里的 A 已经被人修改两次了,我们直接 CAS 是感知不到的。
解决方法:对要修改的值添加一个版本号,每次修改就让版本号同时改变,AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A - B - A - C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference
原子数值:AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
字段更新器:AtomicReferenceFieldUpdater
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
Exception in thread "main" java.lang.IllegalargumentException: Must be volatile type
示例:
public class Test5 {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
Test5 test5 = new Test5();
fieldUpdater.compareAndSet(test5, 0, 10);
// 修改成功 field = 10
System.out.println(test5.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(test5, 10, 20);
System.out.println(test5.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(test5, 10, 30);
System.out.println(test5.field);
}
}
阿里《Java开发手册》嵩山版提到过这样一条建议:
【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。 说明:如果是
count++
操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1);
如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
以上内容共有两个重点:
count++
这种非一写多读的场景使用 volatile 解决不了并发问题;LongAdder
而非 AtomicLong
来替代 volatile
,因为 LongAdder
的性能更好。LongAdder与AtomicLong分析 此处省略
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0
累加 Cell[0]
,而 Thread-1
累加Cell[1]
... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
问题:
下面的代码在运行时,由于 SimpleDateFormat
不是线程安全的
SimpleDateFormat sDF = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
有很大几率出现 java.lang.NumberForMATException
或者出现不正确的日期解析结果,因为指令并不是原子性的,多线程指令交错会出现问题,其它非线程安全的数据结构的并发问题也多是此类原因
解决:
我们熟知的 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
// ...
}
可以看见,String
内部是一个使用 final
修饰的字符数组(Jdk 9变成了 byte
数组)
final 的使用
final
修饰保证了该属性是只读的,不能修改final
修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性保护性拷贝:
我们使用 String
的时候还是用上了很多修改的方法,可是前面明明说了 String
是不可变的,那是怎么回事?其实在构造新字符串对象时,会生成新的 char[] value
,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝】
final 关键字底层原理
final
变量的设置与获取原理和 volatile
关键字类似,都会添加上读屏障写屏障
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】
线程池的优势:
如何创建线程池:
Java 中创建线程池有以下两种方式:
ThreadPoolExecutor
类创建(推荐)Executors
类创建其实这两种方式在本质上是一种方式,都是通过 ThreadPoolExecutor
类的方式创建,因为 Executors
类调用了 ThreadPoolExecutor
类的方法。
创建一个线程池:
public class MyThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(20), new ThreadPoolExecutor.CallerRunspolicy());
for (int i = 1; i 5; i++) {
// 创建WorkerThread对象
Runnable worker = new Runnable(()->{});
// 执⾏任务
executor.execute(worker);
}
}
}
构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
核心线程数目 (最多保留的线程数)
maximumPoolSize
最大线程数目
keepAliveTime
生存时间 - 针对救急线程
unit
时间单位 - 针对救急线程
workQueue
阻塞队列,无剩余线程时,新加入的任务进入此队列等待
常用的阻塞队列:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
threadFactory
线程工厂 - 一般使用默认的线程创建工厂的方法 Executors.defaultThreadFactory()
来创建线程。
handler
拒绝策略,阻塞队列已满并且无法创建新线程,就执行对应的拒绝策略
工作方式:
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
当线程数达到 corePoolSize
并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue
队列排队,直到有空闲的线程。
如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize
数目的线程来救急。
如果线程到达 maximumPoolSize
仍然有新任务这时会执行拒绝策略。拒绝策略 Jdk 提供了 4 种实现方式。
ThreadPoolExecutor.AbortPolicy:
丢弃任务并抛出异常。ThreadPoolExecutor.DiscardPolicy:
丢弃任务但不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy:
丢弃队列最前面的任务,然后重新尝试执行任务ThreadPoolExecutor.CallerRunsPolicy:
由调用线程处理该任务当高峰过去后,超过corePoolSize
的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime
和 unit
来控制。
核心线程是不会自动释放的
提交任务:
// 执行任务
void execute(Runnable command)
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task)
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
关闭线程池:
/*
线程池状态变为 SHUTDOWN
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();
/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
什么?不是说
interrupt
只是改变中断状态位吗?一般wait
sleep
join
都会感知该状态位(其实park
也可以,但是只有它检测到状态位改变后不会给人改成false
),所以使用interrupt
能够中断这些操作,但是为什么还能中断正在执行的线程?前面说过,interrupt
的出现能够让我们实现体面的停止线程,即我们可以一直检测状态位,如果状态位改变了就执行对应的善后工作再手动break
;
查看 ThreadPoolExecutor
的执行任务的代码方法 runWorker
可以看出,的确是在一个 while
循环中检测我们的线程池的调度线程(对于线程池来说就是主线程)的状态位,如果调度线程被 interrupt
打断了,确保其中的任务线程都处于打断状态
newFixedThreadPool
定长线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:
评价:适用于任务量已知,相对耗时的任务
newCachedThreadPool
无限长线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点:
Integer.MAX_VALUE
,救急线程的空闲生存时间是 60s评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
单线程执行
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景: 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入阻塞队列排队。任务执行完毕,这唯一的线程也不会被释放,这样可以保证所有的任务按序执行。
newScheduledThreadPool
定长定时周期任务
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
定长线程池,用来执行一些定时任务、周期任务
newWorkStealingPool
这个是创建的 ForkJoinPool
概念
Fork/Join
是 JDK 1.7
加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join
在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join
默认会创建与 cpu 核心数大小相同的线程池
使用
提交给 Fork/Join
线程池的任务需要继承 RecursiveTask
(有返回值)或 RecursiveAction
(没有返回值)
例如下面定义了一个对 1~n 之间的整数求和的任务
class AddTask1 extends RecursiveTask<Integer> {
int n;
public AddTask1(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + '}';
}
@Override
protected Integer compute() {
// 如果 n 已经为 1,可以求得结果了
if (n == 1) {
log.debug("join() {}", n);
return n;
}
// 将任务进行拆分(fork)
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
log.debug("fork() {} + {}", n, t1);
// 合并(join)结果
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}
然后提交给 ForkJoinPool
来执行
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask1(5)));
}
参考教程
黑马程序员-Java并发编程
以上是脚本宝典为你收集整理的Java并发编程全部内容,希望文章能够帮你解决Java并发编程所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。