参考资料:
线程池之ThreadPoolExecutor概述
一文读懂多线程
java3y
一、基础概念
1、进程
- 在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:
-
- 进程的定义:
- 进程是程序的一次运行。
- 进程是具有独立功能的程序在一个数据集合上运行的过程。
- 进程是系统进行资源分配和调度的独立单位。每个进程都有自己的内存空间和系统资源。
2、线程
- 为使程序能并发进行,系统必须进行一系列操作:
- 创建进程:
- 系统在创建一个进程时,必须为它分配其所必须的、除虚拟机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB;
- 撤销进程:
- 系统在撤销进程时,必须先对其所占有的资源进行回收操作,然后再撤销PCB。
- 进程切换:
- 对进程进行上下文切换时,需要保存当前进程的CPU环境,设置新选中进程的CPU环境,因此需花费不少处理时间。
- 创建进程:
-
- 引入线程主要是**为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。**使OS具有更好的并发性
- 简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能调度)。
3、进程线程总结
-
进程作为资源分配的基本单位
-
线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
-
线程有3个基本状态:
- 执行、就绪、阻塞
-
线程有5种基本操作:
- 派生、阻塞、激活、 调度、 结束
-
线程的属性:
- 1)轻型实体;
- 2)独立调度和分派的基本单位;
- 3)可并发执行;
- 4)共享进程资源。
-
线程有两个基本类型:
-
- 用户级线程:
- 管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。
-
- 系统级线程(核心级线程):
- 由操作系统内核进行管理。
- 操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行以及撤消线程。
-
-
值得注意的是:
- 多线程的存在,不是提高程序的执行速度。
- 其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。
- 多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权
4、并行与并发
- 并行:
- 并行性是指同一时刻内发生两个或多个事件。
- 并行是在不同实体上的多个事件。
- 并发:
- 并发性是指同一时间间隔内发生的两个或多个事件。
- 并发是在同一实体上的多个事件。
- 由此可见:并行是针对进程的,并发是针对线程的。
5、线程生命周期
-
线程的生命周期可以通过getstate()获得,Thread.state类型分为:
- 创建new、可运行runnable、阻塞Blocking、等待Waiting、死亡Terminaled
-
(1)新建(new)
- 新建状态,创建了线程对象,在Start启动前的状态
- 创建后尚未启动
(2)可运行(runnable)
-
可能正常运行,也可能正在等待CPU时间片
-
包含了操作系统线程状态中的Running和Ready。
- Ready状态,表示该线程可以被资源调度器进行调度。
- Running状态,表示该线程正在执行,
-
如果用yieid方法可以把RUNNING状态转化为READY状态
(3)阻塞(Blocking)
- 等待获取一个排他锁,如果其他线程释放了锁就会结束此状态,
- 线程执行了wait()、thread.join() 方法会把线程转化为Waiting等待状态,
- 执行object.notify()方法,或者加入的线程执行完毕,当前线程会转化为RUNNABLE状态。
(4)无限期等待(Waiting)
等待其他线程显式的唤醒,否则不会被分配CPU时间片。
进入方法 | 退出方法 |
---|---|
没有设置Timeout参数的Object.wait()方法 | Object.notify()/Object.notifyAll() |
没有设置Timeout参数的Object.join()方法 | 被调用的线程执行完毕 |
LockSupport.park()方法 | - |
(5)限期等待Timed waiting
-
无需等待其他线程显式的唤醒,在一定时间后会被系统自动唤醒。
-
调用Thread.sleep( ) 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
-
调用Thread.wait()方法时线程进入限期等待或者无限等待时,常常用“挂起一个线程”进行描述。
-
睡眠和挂起是用来描述行为,而阻塞和等待时描述状态。
-
阻塞和等待的区别在于,阻塞式被动的,它是在等待获取一个排他锁,而等待是被动的,通过调用Thread.sleep()和Object.wait()等方法进入。
进入方法 | 退出方法 |
---|---|
Thread.sleep() | 时间结束 |
设置了Timeout参数的Object.wait()方法 | 时间结束/Object.notify()/Object.notifyAll() |
设置了Timeout参数的Object.join()方法 | 时间结束/被调用的线程执行完毕 |
LockSupport.parkNanos()方法 | - |
LockSupport.parkUtil()方法 | - |
(6)死亡Terminaled
线程结束任务之后自己结束,或者产生了异常自己结束。
6、死锁
(1)概念
- 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
- 线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
(2)死锁必要条件
- 互斥条件:
- 在一段时间内某资源只由一个进程占用。
- 如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。
- 占有且等待条件:
- 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不可抢占条件:
- 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待条件:
- 若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)
(3)避免死锁
1、避免一个线程同时获得多个锁
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
二、多线程基础
1、Thread类的顶部
- jvm允许多个线程并发执行
- 线程有优先级,优先级高的先执行
- 线程可能会有守护线程(后台线程)
- 初始化的时候,优先级平等
- jvm启动时,main线程先启动,无守护线程
- 线程的结束有两种情况:
- 1、执行退出exit方法
- 2、run方法执行完毕或者抛出了异常
- 两个方法创建新的线程
- 继承thread类,重写run方法
- 实现Runnable方法,重写run方法
- 实现Callable接口,重写run方法
- 每个线程都有自己的名字,如果没有,默认指定一个。
2、继承Thread类
-
创建:
- 继承Thread类,重写里面的Run方法,因为Thread类也实现了Runnable接口。
-
启动:
- 创建子类对象,调用start方法
-
缺点:
- 不建议使用,继承了一个类,就不能继承其他父类了
-
code:
public class MyThread extends Thread {
@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
public static void main(String[] args) {
// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
3、实现Runnable接口
-
创建:
- 实现Runnable接口 重写Run方法
-
启动:
- 创建实现类对象、Thread对象 调用Start方法
-
推荐:
- 避免单继承的局限性,优先使用接口
-
code:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
demo:
建立三个用户模仿抢票:
public class RabbitClass extends RunnableThread {
private int num=99;
@Override
public void run() {
while (true) {
if(num<0)
break;
try {
Thread.sleep(200);//模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+num--);
}
}
public static void main(String[] args) {
RabbitClass rabbitClass=new RabbitClass();
new Thread(rabbitClass,"one").start();//用户一
new Thread(rabbitClass,"two").start();//用户二
new Thread(rabbitClass,"three").start();//用户三
}
}
4、实现Callable接口
与Runnble相比,Callable可以有返回值,返回值通过FutureTask进行封装。
public class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
return 123;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
/**
* Future 是获取异步计算结果,对任务执行的结果进行获取
* FutureTask 除了实现了Future接口外还实现了Runnable接口
*/
FutureTask<Integer> ft = new FutureTask<>(callable);
Thread thread = new Thread();
thread.start();
System.out.println(ft.get());
}
}
- 补充:
- Callable就是Runnable的扩展。
- Runnable没有返回值,不能抛出受检查的异常,而Callable可以!
- Future一般我们认为是Callable的返回值,但他其实代表的是任务的生命周期(当然了,它是能获取得到Callable的返回值的)
5、实现多线程注意细节
(1)run()和start()方法区别:
run()
:仅仅是封装被线程执行的代码,直接调用是普通方法start()
:首先启动了线程,然后再由jvm去调用该线程的run()方法。
(2)jvm虚拟机的启动是单线程的还是多线程的?
- 是多线程的。不仅仅是启动main线程,还至少会启动垃圾回收线程的,不然谁帮你回收不用的内存~
6、Thread线程类API
(1)setName()/getName()
- Thread.setName()/getName()
- 获取当前线程名Thread.currentThread().getName()
- new Thread(myThread, “关注公众号Java3y”)
- 创建线程时没有设置线程名会给个默认明
- 通过设置线程名称有助于程序调试,提高可读性,建议为每一个线程设置一个可以体现线程功能的名称。
(2)Daemon守护线程
-
守护线程是为其他线程服务的
- 垃圾回收线程就是守护线程
-
守护线程特点
- 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。
- 也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行了
-
注意点:
- 在线程启动前设置为守护线程,方法是
setDaemon(boolean on)
- 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
- 守护线程中产生的新线程也是守护线程
- 在线程启动前设置为守护线程,方法是
-
main()属于非守护进程。
-
设置守护线程。
-
public static void main(String[] args) { Thread thread = new T