什么是多线程
# 线程与进程
● 进程:是指⼀个内存中运行的应用程序,每个进程都有⼀个独立的内存空间,⼀个应用程序可以同时运行多个进程;进程也是程序的⼀次执行过程,是系统运行程序的基本单位;系统运行⼀个程序即是⼀个进程从创建、运行到消亡的过程。
进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源
● 线程:线程是进程中的⼀个执行单元,负责当前进程中程序的执行,⼀个进程中至少有⼀个线程。 ⼀个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
⼀个程序运行后至少有⼀个进程,⼀个进程中可以包含多个线程。
我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:
那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢?
为使程序能并发执行,系统必须进行以下的一系列操作:
• 创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应PCB;
• 撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB;
• 进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。
可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
引入线程主要是**为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。**使OS具有更好的并发性。
简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能**【调度】**)。
# 并发与并行
● 并发:指两个或多个事件在同⼀个时间段内发生。
● 并行:指两个或多个事件在同⼀时刻发生(同时发生)。
在操作系统中,安装了多个程序,并行指的是在⼀段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每⼀时刻只能有⼀道程序执行,即微观上这些程序是分时的交替运行,只不过是给⼈的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务 并行执行,即利⽤每个处理器来处理⼀个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的 效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上 并发运行。同理,线程也是⼀样的,从宏观⻆度上理解线程是并行运行的,但是从微观⻆度上分析却是串行运行的,即⼀个线程⼀个线程的去运行,当系统只有⼀个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
# 线程生命周期
1、新建状态(New)
当线程对象对创建后,即进入了新建状态;
2、就绪状态(Runnable)
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了 start() 此线程立即就会执行;
3、运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,可运行状态(runnable)的线程获得了cpu 时间片(timeslice),即进入到运行状态。注:就绪状态是进入到运行状态的唯⼀入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
注意:java线程将操作系统中的就绪和运行两种状态笼统的称为“运行状态”
4、阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,即让出了cpu timeslice,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep() 状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、等待状态(Waiting)
进入等待状态表示当前线程需要等待其他线程做出一些特定的动作(通知或中断)。
6、超时等待(Time_Waiting)
超时等待不等同于等待状态,它是可以在指定的时间自行返回的。
7、终止状态(TERMINATED)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。 死亡的线程不可再次复生。
# 多线程状态之间的转换
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有⼀定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来 CPU仍然调度了A线程的情况。
下面就来讲解与线程生命周期相关的方法~
# 线程常用方法
# 线程的优先级
可以通过传递参数给线程的 setPriority() 来设置线程的优先级别
调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。优先级 : 只能反映线程 的 中或者是 紧急程度 , 不能决定 是否⼀定先执行行
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
//线程可以具有的最高优先级,取值为10。
static int MAX_PRIORITY
//线程可以具有的最低优先级,取值为1。
static int MIN_PRIORITY
//分配给线程的默认优先级,取值为5。
static int NORM_PRIORITY
2
3
4
5
6
class PriorityThread extends Thread{
@Override
public void run() {
for(int i=0;i<50;i++) {
System.out.println(Thread.currentThread().getName()+"============"+i);
}
}
}
public class TestPriority {
public static void main(String[] args) {
PriorityThread p1 = new PriorityThread();
p1.setName("P1线程");
PriorityThread p2 = new PriorityThread();
p2.setName("P2线程");
PriorityThread p3 = new PriorityThread();
p3.setName("P3线程");
p1.setPriority(Thread.MAX_PRIORITY);
p3.setPriority(Thread.MIN_PRIORITY);
p1.start();
p2.start();
p3.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# sleep方法
sleep()使当前线程进入阻塞状态,在指定时间内不会执行,但不会释放“锁标志”。
调用sleep方法会进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态!
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
2
3
4
5
6
# yield方法
暂停当前正在执行的线程对象,并执行其他线程。
yield()只是使当前线程重新回到可运行状态,所以执行yield()的线程有可能在进入到可运行状态后马上又被执行。
yield()只能使同优先级或更高优先级的线程有执行的机会。
使用 yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
class Task1 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 100;i++){
System.out.println("A:"+i);
}
}
}
class Task2 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 10;i++){
System.out.println("B:"+i);
//让步
Thread.yield();
}
}
}
public class TestYield {
public static void main(String[] args) {
//匿名对象,这个方法只需要使用一次
new Thread(new Task1()).start();
new Thread(new Task2()).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
从输出结果可以虽然线程B让步了,但是也不是线程A执行完了,线程B才执行。
sleep()和yield()的区别
这是一个面试的考点。
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可运行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼⼀段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在⼀个运行系统中, 如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
# join方法
join() 方法的作用是调用线程等待该线程完成后,才能继续往下运行。
join是Thread类的⼀个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这⾥需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"打印---->"+i);
}
}
}
public class TestJoin {
public static void main(String[] args) {
System.out.println("主线程开始执行.....");
JoinThread joinThread = new JoinThread("新加入的线程");
joinThread.start();
try {
joinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束执行.....");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
主线程开始执行..... 新加入的线程打印---->0 新加入的线程打印---->1 新加入的线程打印---->2 新加入的线程打印---->3 新加入的线程打印---->4 主线程结束执行.....
# 守护线程
守护线程.setDaemon(true):设置守护线程
线程有两类:用户线程(前台线程)、守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程会自动结束
垃圾回收器线程属于守护线程
守护线程有一个特点:
• 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止了。
• 也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行了
使用线程的时候要注意的地方
在线程启动前设置为守护线程,方法是setDaemon(boolean on)
使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
守护线程中产生的新线程也是守护线程
class DeamonThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("守护线程打印:"+i);
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestDeamon {
public static void main(String[] args) {
//启动一个守护线程
DeamonThread deamonThread = new DeamonThread();
deamonThread.setDaemon(true);//是守护线程
deamonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程打印:"+i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
从输出结果可以看出,主线程结束后守护线程也结束了!
# 线程中止
线程自然终止
要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
stop
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume()和stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
现在已经没有强制线程终止的方法了!
中断
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了。具体到底中断还是继续运行,应该由被通知的线程自己处理。
Thread t1 = new Thread( new Runnable(){
public void run(){
// 若未发生中断,就正常执行任务
while(!Thread.currentThread.isInterrupted()){
// 正常任务代码……
}
// 中断的处理代码……
doSomething();
}
} ).start();
2
3
4
5
6
7
8
9
10
线程通过检查自身的中断标志位是否被置为 true 来进行响应, 线程通过方法 **isInterrupted()**来进行判断是否被中断,也可以调用静态方法 **Thread.interrupted()**来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。这种情况下,使用中断会更好,因为:
一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
再次说明:调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!
interrupt线程中断还有另外两个方法(检查该线程是否被中断):
• 静态方法interrupted()-->会清除中断标志位
• 实例方法isInterrupted()-->不会清除中断标志位
public static void main(String[] args) {
Main main = new Main();
// 创建线程并启动
Thread t = new Thread(main.runnable);
System.out.println("This is 主方法");
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("In main");
e.printStackTrace();
}
// 设置中断
t.interrupt();
}
Runnable runnable = () -> {
int i = 0;
try {
while (i < 1000) {
// 睡个半秒钟我们再执行
Thread.sleep(500);
System.out.println(i++);
}
} catch (InterruptedException e) {
// 判断该阻塞线程是否还在
System.out.println(Thread.currentThread().isAlive());
// 判断该线程的中断标志位状态
System.out.println(Thread.currentThread().isInterrupted());
System.out.println("In Runnable");
e.printStackTrace();
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
This is 主方法 0 1 2 3 4 true false In Runnable java.lang.InterruptedException: sleep interrupted
执行流程是这样的:
# 线程通信
多个线程在处理同⼀个资源,但是处理的动作(线程的任务)却不相同。
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成⼀件任务,并且我们希望他们有规律的执行, 那么多线程之间需要⼀些协调通信,以此来帮我们达到多线程共同操作⼀份数据。
如何保证线程间通信有效利用资源 ?
多个线程在处理同⼀个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同⼀个变量的使用或操作。 就是多个线程在操作同⼀份数据时, 避免对同⼀共享变量的争夺。也就是我们需要通过⼀定的⼿段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
什么是等待唤醒机制
这是多个线程间的⼀种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,线程间也会有协作机制。
就是在⼀个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后再将其唤醒
(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象 上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
线程通信方法
方法 | 说明 |
---|---|
public final void wait() | 释放锁,进入等待队列 |
public final void wait(long timeout) | 在超过指定的时间前,释放锁,进入等待队列 |
public final void notify() | 随机唤醒、通知⼀个线程 |
public final void notifyAll() | 唤醒、通知所有线程 |
wait()
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用 wait()方法后,会释放对象的锁。它还要等着别的线程执行⼀个特别的动作,也即 是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进⼊到调度队列
(ready queue)中 。
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒,还没有被notify唤醒,就会自动醒来 。
notify()
通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
notifyAll()
通知所有等待在该对象上的线程。
等待和通知的标准范式
等待方遵循如下原则:
1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
2
3
4
5
6
通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
synchronized(对象){
改变条件;
对象.notifyAll();
}
2
3
4
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized 代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
notify 和 notifyAll 应该用谁 ?
尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
调用wait和notify方法需要注意的细节
1、wait方法与notify方法必须要由同⼀个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同⼀个锁对象调用的wait方法后的线程。
2、wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3、wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
注意:
哪怕只通知了⼀个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块 内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能⾯临其它线程的竞争),成功后才 能在当初调用 wait 方法之后的地方恢复执行。
public static void main(String[] args) {
Object lock = new Object();//这个就是协调者的角色,
new Thread(new Runnable() {
@Override
public void run() {
//进入等待
synchronized (lock){
System.out.println("顾客1线程:1、点餐....");
try {
lock.wait();
//lock.wait(4000);//会等待指定的时间,如果没有被唤醒,那么会自己唤醒,执行后面的代码
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("顾客1线程:3、开始吃....");
}
}
},"顾客线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
//进入等待
synchronized (lock){
System.out.println("顾客2线程:1、点餐....");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("顾客2线程:3、开始吃....");
}
}
},"顾客线程2").start();
new Thread(new Runnable() {
@Override
public void run() {
//等待2秒时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知顾客
synchronized (lock){
System.out.println("2、老板做好了,交给顾客.....");
lock.notifyAll();//通知所有等待的线程
}
}
},"老板线程").start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
顾客1线程:1、点餐.... 顾客2线程:1、点餐.... 2、老板做好了,交给顾客..... 顾客2线程:3、开始吃.... 顾客1线程:3、开始吃....