Java多线程
Java 多线程
多线程是如何执行的
并发和并行同时进行的
多线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池
- 匿名类
继承Thread类
缺点:已经继承Thread类,无法再继承其他类,不利于功能的扩展
流程
1 |
|
注意事项
- 不要把主线程任务放到子线程之前,否则会先把主线程全部执行完
实现Runnable接口
流程
1 |
|
缺点
需要多创建一个Runnable 任务对象
实现Callable接口
上述的那两种方法都会有一个问题,假如线程执行完有数据要返回,它们的run方法无法返回数据,因为run方法的返回值只能是void。
而Callable接口重写的是call方法,call方法的返回值可以自己指定
实现流程:
利用 CollCollable接口
和 FutureTask类
来实现
1 |
|
匿名类
1 |
|
线程常用方法
多线程协调运行
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
线程等待和唤醒
wait()
: 当前线程等待,直到被其他线程唤醒notify()
: 随机唤醒单个线程notifyAll()
: 唤醒所有进程
线程进入等待状态 : wait方法
- 必须在synchronized块中才能调用wait()方法
因为wait()方法调用时,会释放线程获得的锁,wait()方法返回时,线程又会重新试图获得锁。
我们在while()循环中调用wait(),而不是if语句:
因为线程被唤醒时,需要再次获取this锁。多个线程被唤醒后,只有一个线程能获取this锁,此刻,该线程执行queue.remove()可以获取到队列的元素,然而,剩下的线程如果获取this锁后执行queue.remove(),此刻队列可能已经没有任何元素了,所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:
1 |
|
- 只能在锁对象上调用wait()方法
线程唤醒 :notifyAll()
如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法
内部调用了this.notifyAll()而不是this.notify()
使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
中断线程
interrupt
main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted()
设置标志位中断线程
线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。中断休眠中的线程,会提前结束睡眠
线程7大状态
NEW 新建状态
尚未启动的线程,使用new语句创建的线程处于新建状态(new Thread),仅仅在堆上分配了内存RUNNABLE 运行状态(包括就绪态和执行态)
在Java虚拟机中执行的线程处于此状态BLOCKED 阻塞状态
当线程由于缺少响应的资源而导致程序无法继续执行,就会从运行状态进入到阻塞状态比 如:锁资源,IO资源WAITING 等待状态
当线程调用了wait方法就会进入到WAITING状态,该状态只有notify等操作才能唤醒线程进入下一个状态TIMED_WAITING 睡眠状态
如果线程执行了sleep(long)/join(long)/wait(long),会触发线程进入到Time_waiting状态,只有到达设定的时间,才会脱离阻塞状态TERMINATED 终止状态
已退出的线程处于此状态,该线程结束生命周期
线程安全问题
守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
1 |
|
守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
出让线程和插入线程
Thread.yield()
:出让线程t1.join()
:插入线程
线程死锁
可重入锁
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
一个线程可以获取一个锁后,再继续获取另一个锁
1 |
|
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
避免死锁:线程获取锁的顺序要一致
1 |
|
线程同步
为了解决线程安全问题,就进行线程同步
- 同步代码块
- 同步方法
- lock锁
同步代码块
原理:把访问共享资源的 核心代码 上锁,以此保证线程安全
线程同步的常见方案:
加锁: 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能加锁进来。
锁对象规范:
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
锁住一个代码块:
1 |
|
1 |
|
同步方法
作用:把访问共享资源的 方法 进行上锁,以此保证线程安全
释放锁
以下操作会释放锁
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
当前线程当前线程在同步代码块、同步方法中执行了wait(),当前线程暂停,并释放锁
以下操作不会释放锁
- 线程执行同步方法、同步代码块时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程执行,不会释放锁
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(不推荐使用suspend()方法控制线程)
常用方法跟锁的关系
1.sleep会使当前线程睡眠指定时间,不释放锁
2.yield会使当前线程重回到可执行状态,等待cpu的调度,不释放锁
3.wait会使当前线程回到线程池中等待,释放锁,当被其他线程使用notify,notifyAll唤醒时进入可执行状态
4.当前线程调用 某线程.join()时会使当前线程等待某线程执行完毕再结束,底层调用了wait,释放锁
lock锁
Lock锁是一个接口,可以用实现类ReentrantLock
来构建锁对象,Lock中提供了手动加锁和释放锁的方法
和synchronized不同的是,ReentrantLock可以尝试获取锁,因此更安全
1 |
|
线程池
线程池是一个可以复用线程的技术
不使用线程池的问题:用户没发起一个请求,后台就需要创建一个新线程来处理。开销过大
线程池工作原理
下图工作线程类似于服务员,任务队列类似于饭店的客人。
创建线程池
- 方式一:使用ExecutorService的实现类ThreadPoolExecutor 自创建一个线程池对象
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
Executors
- 获取线程池对象
ExecutorService pol = Executors.newCachedThreadPool(int nThreads);
因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
提交任务
pol.submit(new MyRunnable());
销毁线程池
pol.shutdown();
线程池在程序结束的时候要关闭。使用shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()
会立刻停止正在执行的任务,awaitTermination()
则会等待指定的时间让线程池关闭。
ThreadPoolExecutor 线程对象参数
什么时候开始创建临时进程
新任务提交时,发现核心线程都在忙,任务队列也满了,并且还可以创建临时进程,此时才会创建临时进程
什么时候会拒绝新任务
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
任务拒绝策略
处理Runnable任务/Callable任务
ExecutorService常用方法
通过Executors 创建线程池
强烈不建议使用这种方法去创建线程池
通过工具类的静态方法返回不同特点的线程对象
线程池多大合适
CPU密集型运算
最大并行数 + 1I/O密集型运算
虚拟线程
Java 19引入的一种轻量级线程,它在很多其他语言中被称为协程、纤程、绿色线程、用户态线程