前言
一、线程基础知识
线程创建的四种方式
1.继承Thread类,实现run方法
通过分析Thread源码可知,Thread类是通过实现Runnable接口,重写Runnable的run方法创建线程:
for (int i = 0; i < 100000; i++){
//这是创建了10万个线程,来执行代码。 10万个线程需要在cpu里面进行切换,消耗性能
Thread thread = new Thread(){
//重写里面的run方法. Thread实现了Runnable接口。
@Override
public void run(){
list.add(random.nextInt());
}
};
thread.start();
thread.join();
}
通过函数式编程创建线程:参数是Runnable。
2.实现Runnable接口,重写run方法
class MyTask implements Runnable{
int i = 0;
public MyTask(int i){
this.i = i;
}
@Override
public void run() {
//打印执行当前任务的线程名称
System.out.println(Thread.currentThread().getName()+"--"+i);
try {
//模拟处理业务逻辑的耗时操作,如查询数据库
Thread.sleep(1000L);
}catch (Exception e){
}
}
3.实现Callable接口
4.使用线程池
线程的状态
● NEW,新建
● RUNNABLE,运行
● BLOCKED,阻塞
● WAITING,等待
● TIMED_WAITING,超时等待
● TERMINATED,终结
这些生命状态在Java中被定义:
状态切换如下图所示:
yield()
:让当前【正在运行】的线程,让出CPU的使用权,重新和其他线程竞争CPU; -AQS中使用这个方法等待状态WAITING
:让【正在运行】的线程进入等待状态,具体等待多长时间-不确定,需要人手动唤醒;超时等待状态TIMED_WAITING
:让【正在运行】的线程进入等待状态,具体等待多长时间-1.人工唤醒2.超时结束自动唤醒;
>以上两种状态,都涉及到【Java线程在CPU中上下文切换】。
-- 提示:
1.Java【线程模型】使用的是【内核线程模型】,并不是说:Java线程就是内核线程。
2.内核线程模型包括:用户线程、内核线程。而且,在内核空间的【线程表】存储着用户线程与内核线程一对一的关系。
3.**内核线程模型 不等于 内核线程。**
阻塞BLOCKED
:多线程竞争锁资源,没有抢到锁的线程进入阻塞状态。
>该状态,可能会有【线程在CPU中上下文切换】。由于锁优化手段:【自适应自旋】,当处于自旋状态中获取到了锁,就不会切换CPU状态;如果获取不到,则会切换。
如何优雅的中断一个线程
面试题:在JAVA中如何优雅的停止一个线程?
方案1:设置标志位
我们可以设置一个已关闭的标志位,当任务或者线程运行的时候先判断标志位的状态,如果是已经关闭那个这个任务或者线程就直接结束,不过这个标志位需要用volatile关键字修饰,否则可能其他线程已经修改了任务可能仍然在运行。
这种方法可以解决一部分问题,但是当任务可能会被阻塞的时候就会出现问题,就像之前的生产者、消费者模式,如果生产者通过循环往队列里面加元素,在每次循环之前都要判断中断标志位,如果结束了就不往队列中put数据了,当消费者在某些情况下可能不在消费数据所以会设置标志位为已结束。此时如果阻塞队列是满的,而刚好生产者在put阻塞中,由于消费者不在消费,生产者线程就会永远处于阻塞状态。
方案2:用Thread.stop()
建议:千万不要那么用,Thread.stop是@deprecated的方法,它注释中有一句话 Forces the thread to stop executing. 比如,当线程刚刚获取了一个锁资源,直接把线程停掉,锁未释放,意味着其他线程都无法获取锁资源了,造成了很严重的死锁问题;再比如任务未完成,直接把线程停掉,会带来脏数据的很多问题。
方案3:调用suspend()和resume()方法
废弃原因(再写具体一点):太过暴力,可能会导致一些清理工作不会完成
调用后会直接释放锁,可能会导致数据不同步的问题https://www.jianshu.com/p/e0ff2e420ab6
方案4:线程中断机制
答案是 :用线程中断机制。那么什么是‘线程中断机制’呢?
我们先来看一个例子:
package thread;
public class ThreadInterruptTest {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
i ++;
System.out.println(i);
//100毫秒之后才能捕获到中断状态
if (Thread.interrupted()) {
System.out.println("======");
break; // 需要主动中断,break
}
}
}
});
t1.start();
try {
//让主线程休眠100毫秒之后,再去让t1线程发出中断信号。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发送中断线程信号
t1.interrupt();
}
}
输出结果
...
17917
17918
17919
======
解释:t1.start(); 启动线程,t1.interrupt(); 发送中断线程信号,Thread.interrupted() 检测到信号,break循环主动中断。
再来看一个例子:
package thread;
public class ThreadInterruptTest {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public synchronized void run() {
while (true) {
i ++;
System.out.println(i);
if (Thread.interrupted()) {
System.out.println("======");
}
if (i == 10) {
break;
}
}
}
});
// 启动线程
t1.start();
// 发送中断线程信号
t1.interrupt();
}
}
输出结果:
begin
1
======
2
3
4
5
6
7
8
9
10
解释:t1.interrupt();设置了中断标志为true,但Thread.interrupted()执行后,会将中断标志设回false,因此只输出了一行‘======’。
再来看一个例子:
package thread;
public class ThreadInterruptTest {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public synchronized void run() {
while (true) {
i ++;
System.out.println(i);
if (Thread.currentThread().isInterrupted()) {
System.out.println("======");
//break; // 需要主动中断,break
}
if (i == 10) {
break;
}
}
}
});
// 启动线程
t1.start();
// 发送中断线程信号
t1.interrupt();
}
}
输出结果:
begin
1
======
2
======
3
======
4
======
5
======
6
======
7
======
8
======
9
======
10
======
解释:t1.interrupt();设置了中断标志为true,Thread.interrupted()执行后,中断标志依然为true,不会清空,因此输出了10行======
Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断
。
线程中断机制详讲
线程重要方法介绍
yield和join
yield:放弃CPU的使用权,重新竞争CPU;
join:只有等子线程执行结束,主线程才能结束;线程篇 - join的作用以及如何使用
sleep和wait的区别
1、对于 sleep()方法是属于 Thread 类中的,可以在任意地方使用
;而 wait()方法,则是属于 Object 类中的,只能在同步方法或者同步代码块中使用
。
2、
● sleep()的过程中, 线程不会释放对象锁
,程序暂停执行指定的时间,让出 cpu
给其他线程,但是当指定的时间到了又会自动恢复运行状态。
● wait()方法的时候,线程会放弃对象锁
,进入等待队列
(等待锁定池),只有此对象调用 notify()方法后本线程才会获取对象锁进入运行状态。
3、
● sleep和wait都会感应到线程中断状态
,并且清除中断状态。
来看一个例子:
package thread;
public class ThreadInterruptTest {
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public synchronized void run() {
while (true) {
i ++;
System.out.println(i);
try {
// 会感知到中断信号,且清除中断标志位(会把标志位设为false)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 会感知到中断信号,且清除中断标志位(会把标志位设为false)
wait(1000); //wait必须结合synchronized锁使用,所以11行加了锁
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread.interrupted() 清除中断标志位
// Thread.currentThread().isInterrupted() 不会清除中断标志位
if (Thread.interrupted()) {
System.out.println("======");
break; // 需要主动中断,break
}
if (i == 10) {
break;
}
}
}
});
// 启动线程
t1.start();
// 发送中断线程信号
t1.interrupt();
}
}
输出结果:
begin
1
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.ThreadInterruptTest$1.run(ThreadInterruptTest.java:18)
at java.lang.Thread.run(Thread.java:748)
2
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at thread.ThreadInterruptTest$1.run(ThreadInterruptTest.java:25)
at java.lang.Thread.run(Thread.java:748)
3
4
5
6
7
8
9
10
解释:Thread.sleep 和 wait(time) 都可以感知到中断信号,且会清除中断标志位(都没有进入到Thread.interrupted()代码块)。
补充一个小知识点:System.out 是有缓冲区的,System.err没有缓冲区。
二、线程安全知识
线程通信/同步的方式
加锁的方式:
- synchronized:wait和notify
- lock:park和unpark
- 线程同步工具:CountDownLatch、CyclicBarrier、Semaphore
不加锁的方式:
- CAS
- volatile
- 内存队列(比如京东自研的内存队列)
- threadlocal:线程内共享
ThreadLocal讲解
ThreadLocal 的作用是提供线程内的局部变量, 这种变量在线程的生命周期内起作用, 减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度 。每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
更多的ThreadLocal相关知识,请参考:线程安全之 - ThreadLocal
线程两大等待唤醒机制
● 基于monitor机制的:Object类中的wait和notify方法。
–synchronized
● 基于线程的:LockSupport类中的park和unpark方法。
–juc包下的
park和unpark示例
场景描述:
程序一旦启动,当遇到 LockSupport.park();的时候会被阻塞;
5s后,遇到 LockSupport.unpark(t0);,再次被唤醒,然后继续执行代码,最后结束。
@Slf4j
public class Juc01_Thread_LockSupport {
public static void main(String[] args) {
Thread t0 = new Thread(new Runnable() {
@Override
public void run() {
Thread current = Thread.currentThread();
log.info("{},开始执行!",current.getName());
for(;;){//spin 自旋
log.info("准备park住当前线程:{}....",current.getName());
LockSupport.park();
log.info("当前线程{}已经被唤醒....",current.getName());
}
}
},"t0");
t0.start();
try {
Thread.sleep(5000);
log.info("准备唤醒{}线程!",t0.getName());
LockSupport.unpark(t0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
11:15:21.402 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - t0,开始执行!
11:15:21.408 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备park住当前线程:t0....
11:15:26.446 [main] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备唤醒t0线程!
11:15:26.446 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 当前线程t0已经被唤醒....
11:15:26.446 [t0] INFO com.yg.edu.lock.Juc01_Thread_LockSupport - 准备park住当前线程:t0....
unpark与notify最大的区别
unpark可以指定唤醒哪个线程
;
notify是随机唤醒线程
; - - synchronized的两个对象:entrylist和waitset
三、线程并发工具
线程并发工具类以及代码案例
CountDownLatch、Semaphore、CyclicBarrier
CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤
CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对
CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中
排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通
过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()
⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤
醒,直到没有空闲许可。
四、线程实战篇
让前4个线程先执行,第五个线程后执行如何设计更好
1、前四个线程进行阻塞
Java阻塞线程的方法:sleep、wait、join、park。其中wait和park都涉及到锁
- sleep。
join
。 只有等子线程执行结束,主线程才能结束; 线程篇 - join的作用以及如何使用
● join可以让线程按照顺序跑:t1.start(),t1.jon(),t2.start(),t2.join()。 // 效率低
● join可以让线程乱跑:t1.start(),t2.start(),t1.jon()。 // 效率高- 线程池的
submit
方法有返回值。 前四个线程submit然后future.get(),第五个线程最后submit - 国储项目
● 除了newSingleThreadExecutor是单线程,其他几个线程池并行执行,效率较高。 - 单线程化线程池(newSingleThreadExecutor)😗*优点,串行执行所有任务:通过调用submit方法
- 使用CountDownLatch、CyclicBarrier、Sephmore强制让线程依次按照顺序执行
线程顺序执行的几种方式
同上。顺序执行,无效率可言,顺序执行。
参考:java顺序执行线程的几种方式
两个线程交替打印奇偶数
两个线程模拟死锁
小红书面试:手写死锁。
获取子线程执行结果
[Java并发与多线程](二十一)获取子线程的执行结果(*)【来而不往非礼也】
Java多线程获取执行结果
//( maximumPoolSize + queueSize) >= tokenSize (目的:如果有5个token,那么可以直接从线程池拿到5个线程来处理;)
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10,
200,
1L,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1)
);
public static void main(String[] args) throws InterruptedException {
// tokenSize 必须小于 (maximumPoolSize + queueSize)
// 从配置文件中取
int tokenSize = 5;
// 存放结果数据
List<String> resultList = new ArrayList<>();
// 线程计算器
final CountDownLatch countDownLatch = new CountDownLatch(tokenSize);
// 假设有x个不同的token(token数量 == 计算器countDownLatch数量)
for(int i =1; i<=tokenSize; i++) {
int num = i;
String token = "token-00" + i;
// 提交任务
threadPoolExecutor.submit(()->{
try {
System.out.println(new Date() + "开始处理任务:" + token + ", threadName:" + Thread.currentThread().getName());
resultList.add(token);
try{
// api
}catch (Exception e){
System.out.println("api 网络错误");
}
try {
Thread.sleep(1000 * num);
// Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(new Date() + "结束处理任务:" + token);
}catch (Exception e) {
System.out.println("本地异常error");
}finally{
// 计算器减1
countDownLatch.countDown();
}
});
}
System.out.println("主线程在等待");
// 阻塞
countDownLatch.await();
System.out.println("resultList:" + resultList);
}