2021SC@SDUSC
多线程
1.什么是进程?什么是线程?
进程是一个应用程序。
线程是一个进程中的执行场景/执行单元
一个进程可以有多个线程
2.java程序的进程
对于java程序而言,当执行一个java程序时,会先启动JVM,JVM就是一个进程
JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中有两个线程并发。
3.进程
在java语言中:
线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
- 堆内存用来存放对象
- 非堆内存存储类的元数据、方法、常量、属性等
具体可以查看:https://blog.csdn.net/lingbo229/article/details/82586822
关于堆内存和栈内存:
- 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
- 堆内存用于存放new创建的对象和数组
在main执行完以后,主线程的栈已经清空,但是子线程仍然可能在进行操作
4.对于单核的CPU,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没有问题的。
单核的CPU不能做到真正的多线程并发,但是能通过快速的在不同的线程之间频繁切换来实现多线程操作
5.实现多线程
/*
实现线程的第一个方法:
编写一个类,直接继承java.lang.Thread
new 该类,并且调用其start方法
*/
start方法的作用是:启动分支线程,在JVM中开辟一个新的栈空间
run方法在这个新栈的底部
注意,如果调用Thread的run方法,并没有开辟新的栈空间,所以并没有启动分支线程!!
/*
实现线程的第二种方式:
编写一个类,实现Runnable接口,重写run方法,作用是创建一个可运行对象
然后new Thread(new ...());
*/
/*
匿名类:
new Thread(new Runnable(){
void run(){
//...
}
})
*/
6.获取线程名字
setName("...")
设置线程名字
String getName()
获取线程名字
线程的默认名字是 Thread-[线程的次序]
如第一个创建的子线程的名字为 Thread-0 , 第二个为Thread-1,以此类推
主线程的名字是main
static Thread currentThread()
【静态方法】获取当前线程
7.sleep
*sleep需要try-catch
static void sleep(millis:)
传入毫秒数,可以让当前线程阻塞,不占用CPU时间片
注意,sleep是静态方法,与哪个对象调用没有关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQvQTXud-1638862794558)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227074008219.png)]
对于继承的方法,子类不能比父类抛出更多的异常
中断睡眠方式:调用线程的interrupt()
方法,通过抛出异常触发包围sleep方法的try-catch来打断sleep
8.终止线程的执行
旧方法(已经废弃,不建议使用):stop()
,缺点是很容易丢失数据
现在方法:
- 使用布尔标记,通过改变线程的布尔标记来控制线程的执行,这样就可以自己来处理线程结束操作。
9.线程的调度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NhEzJ2Tk-1638862794559)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227075813998.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EOQGy4dp-1638862794560)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227080345227.png)]
interrupt
方法:
interrupt方法会将一个线程终止。特殊情况下,假如这个线程处于等待状态,则会抛出异常,可以通过捕获异常的方式终止进程
10.多线程安全问题
-
存在安全问题的原因
可能多个线程同时对于一个共享资源资源进行修改操作,导致操作出现错误
-
解决方法
线程同步:即线程不能并发了,必须排队执行,会牺牲一部分效率
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xPDxvqzj-1638862794561)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227081209247.png)]
线程同步代码块:
/// ()中写什么
/// 如果你有t1,t2,t3,t4,t5五个线程
/// 你只希望t1,t2,t3排队
/// 你要在()中写一个t1,t2,t3共享的对象,而4,5不共享
synchronized(...){
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DXn6YAz-1638862794562)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227082025970.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7S6GH32M-1638862794563)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227082635787.png)]
synchronized的作用范围越小,效率越高。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCBivAtN-1638862794565)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227083020794.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idUPAOLI-1638862794566)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210227083107836.png)]
11.死锁
多线程的时候可能出现下列情况:
现在有两个对象,obj1与obj2
有两个线程,t1与t2
t1线程先去Synchronized obj1对象,然后在该同步中执行了一个obj2的方法锁方法,
t2线程先去Synchronized obj2对象,然后在该同步中调用obj1的synchronized方法,
这时候,由于t1锁住了obj1,t2锁住了obj2,t1在执行obj2方法的时候还没有归还obj1的锁,t2在执行obj1方法的时候还没有归还obj2的锁,
这就会导致t1等t2,t2等t1,程序锁死并且不报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKcwb580-1638862794568)(C:\Users\11049\AppData\Roaming\Typora\typora-user-images\image-20210304122829179.png)]
所以synchronized在项目中不要嵌套使用
并且synchronized会降低效率,需要减少使用
12.volatile
由于Java的内存模型限制,如果某一线程访问了一个变量,首先会将该变量复制一个副本到自己的内存中工作内存中,如果对其进行修改,虚拟机会在某一时刻将这一修改同步回主内存中,但是,这个时间不固定
这时,如果我们用volatile关键字修饰变量,则相当于要求:
- 每次访问变量时,总是获得主内存的最新值
- 每次修改变量后,立刻写回到主内存
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile
关键字,运行上述程序,发现效果和带volatile
差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。
13.守护线程
当我们退出JVM时,我们希望所有的子线程已经结束,或者不需要管他,他会自动结束,对于后者,我们引入守护线程的概念。
当JVM退出时,不必关心守护线程是否已结束
Thread t = **new* MyThread();
t.setDaemon(true);
t.start();
14.线程池
Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
如果可以复用一组线程:
┌─────┐ execute ┌──────────────────┐
│Task1│─────────>│ThreadPool │
├─────┤ │┌───────┐┌───────┐│
│Task2│ ││Thread1││Thread2││
├─────┤ │└───────┘└───────┘│
│Task3│ │┌───────┐┌───────┐│
├─────┤ ││Thread3││Thread4││
│Task4│ │└───────┘└───────┘│
├─────┤ └──────────────────┘
│Task5│
├─────┤
│Task6│
└─────┘
...
那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
Java标准库提供了ExecutorService
接口表示线程池,它的典型用法如下:
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
因为ExecutorService
只是接口,Java标准库提供的几个常用实现类有:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
我们观察执行结果,一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务。
线程池在程序结束的时候要关闭。使用shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()
会立刻停止正在执行的任务,awaitTermination()
则会等待指定的时间让线程池关闭。
如果我们把线程池改为CachedThreadPool
,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。
如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()
方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
因此,想创建指定动态范围的线程池,可以这么写:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
ScheduledThreadPool
还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool
。放入ScheduledThreadPool
的任务可以定期反复执行。
创建一个ScheduledThreadPool
仍然是通过Executors
类:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
我们可以提交一次性任务,它会在指定延迟后只执行一次:
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
如果任务以固定的每3秒执行,我们可以这样写:
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
如果任务以固定的3秒为间隔执行,我们可以这样写:
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
注意FixedRate和FixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:
│░░░░ │░░░░░░ │░░░ │░░░░░ │░░░
├───────┼───────┼───────┼───────┼────>
│<─────>│<─────>│<─────>│<─────>│
而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:
│░░░│ │░░░░░│ │░░│ │░
└───┼───────┼─────┼───────┼──┼───────┼──>
│<─────>│ │<─────>│ │<─────>│
因此,使用ScheduledThreadPool
时,我们要根据需要选择执行一次、FixedRate执行还是FixedDelay执行。
细心的童鞋还可以思考下面的问题:
- 在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?
- 如果任务抛出了异常,后续任务是否继续执行?
Java标准库还提供了一个java.util.Timer
类,这个类也可以定期执行任务,但是,一个Timer
会对应一个Thread
,所以,一个Timer
只能定期执行一个任务,多个定时任务必须启动多个Timer
,而一个ScheduledThreadPool
就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool
取代旧的Timer
。