多线程系列五之线程的状态
提示:本文主要讲述了线程的六种状态,并详细介绍了造成阻塞的三种方法 sleep、wait和notify、join。
文章目录
一、线程状态
1.线程是操作系统调度执行的基本单位
2.状态更是线程的重要属性。我们所说的状态是用来描述该线程被调度执行的。
1) 线程共有六种状态
1.NEW:Thread对象已经创建,但是还没有调用start,在操作系统内核里还没有创建PCB。
2.Runnable:可运行的,这种状态共有两种情况,分别是正在CPU上执行的和处于就绪队列中,还没有去CPU上执行,随时准备去CPU上执行。
3.TERMINATED:操作系统的内核PCB已经执行完毕并销毁,但是Thread对象还在
三种阻塞状态:
4.TIME_WAITING 调用sleep
5.WAITING wait(notify)和join
6.BLOCKED 加锁操作synchronized
2) 线程状态运行示意图
1.TERMINATED: 此时操作系统内核里的PCB中run方法执行完后,PCB也就随之释放了,但是Thread对象还在。
2.是因为应用程序和内核态中生命周期无法保持一致。PCB释放了但是无法保证t对象也立即释放。
3.一般情况下,我们认为一个线程只能statr一次,此时t线程已经无法再使用,但是可以通过t调用一些属性和方法仍然是可以的。
二、sleep方法
1) sleep方法介绍
1.sleep方法是Thread类的类方法,因此通过使用Thread.sleep才能使用。
2.让线程休眠,本质上就是让这些线程暂时不再参与调度。
3.sleep方法需要包裹一个try catch,因为sleep可能会触发异常。
2) 打印sleep时线程的状态
运行结果
1.可以看出在Thread对象已经有了,PCB还未创建,此时线程的状态是NEW
2.在main线程中通过循环打印可以捕捉到线程的休眠状态和线程正在运行时候的状态
3.由于操作系统的调度策略是随机调度,抢占式执行,所以main线程和t线程谁先结束,我们并不能控制,此时在main线程中加入 t.join, 就是让main线程在此处代码等待t线程执行完后再继续执行
4.t线程执行完后此时线程的状态是TERMINATED
三、synchronized锁
明确:加锁是线程针对 对象 加锁。
1) synchronized修饰
1.修饰普通方法
可以放在public前面,也可以放在public的后面,当synchronized修饰普通方法的时候,此时的加锁对象是this
2.修饰静态方法
与修饰普通方法放置的位置相同,但是此时加锁的对象是类对象,也就是类名.class
3.修饰代码块
synchronized也可以修饰代码块,加锁的对象可以任意指定,进入代码块加锁,出代码块释放锁
2) 锁规则
第一条
如果两个线程针对同一个对象加锁,就会产生锁竞争,先到先得,t1线程获得锁之后,另外一个线程t2只能阻塞等待。直到t1释放锁之后,t2线程才能获取锁成功
第二条
如果两个线程针对不同的对象加锁,就不会产生锁竞争,两个线程都能获取到对方的锁。
第三条
如果一个线程加锁,另外一个线程不加锁,这个时候就不会产生锁竞争。
3) 案例演示
代码
分别搞两个线程,分别调用add方法5w次,期望count的值是10w,如果不加synchronized
运行结果:此时number的数值是不确定的
分析 ***** 重点 ****
1.首先先来介绍一下CPU寄存器
cpu中有一个重要的组成部分,叫做cpu寄存器,这个寄存器空间比较小,但访问速度很快,cpu进行的操作都是针对该cpu寄存器的数据进行的。
2.number++操作不是原子的,可以分为三步
所谓的原子,就是指不可以再分的指令。
而++操作并非是原子的,可以分为三个步骤
一、load 先把内存器的值读取到cpu寄存器中
二、add 把cpu寄存器的值进行+1操作
三、save cpu寄存器的值写回到内存中
3.图解:
1.期望的运行样子
2.!!!!!!!!!!!!!!
1.但是由于++操作并不是原子的
2.操作系统调度策略是随机调度,抢占式执行的
3.因此可能就会出现无数种情况,比如 t1 线程执行完load操作,切换 t2 线程进程,这个时候就会出现脏读。
3.出现错误的一种情形
如果代码的执行顺序如下所示,就会出现bug
1.t1 线程读到了 t2 线程还没来得及返回的数据脏数据
2.因此出现了脏读
4) 引入synchronized
1.由上述分析可以看出出现bug的原因是因为count++操作不是原子的,由三个原子指令操作组成,因为操作系统的随机调度,随时可能会切换线程
2.为了解决上述问题,我们引入了synchronized,本质上就是把不是原子的操作 “转换成” 原子的。
图解:
1.加锁产生锁竞争必须是针对同一个对象进行加锁才会产生锁竞争,如上图所示,是因为都对 myCount 对象试图加锁,因为t1先获得锁,所以t2只能等待,直到t1释放锁后 t2 才能加锁成功
2.图片中空出菱形是因为,线程加锁并不是意味着这个线程就赖在CPU上不走了,操作系统依然是随机调度线程的,只是其他线程试图对该对象加锁,只能进入阻塞等待。
四、wait 和join
1) join方法
1.线程在join中时候,线程的状态是WAITING
2.join方法需要抛出异常
运行结果:
在main线程中调用 t1.join 就意味着main线程要等线程 t1执行完后才可以继续执行,main线程进入阻塞
2) wait 和 notify
1.由于操作系统调度线程是随机调度,抢占式执行的
2.可以通过一些提供的API让线程主动放弃CPU给别的线程让路。
3.主要的三个方法是wait notify 和 notifyAll,这三个方法都是Objetc的方法
4.wait方法和notify必须成对出现。
但是不是由sleep方法和join方法嘛?为什么还要引入wait方法呢???
1.sleep方法只是指定了一段休眠时间,A线程到底干活花了多少时间,不好估计。
2.相比于wait方法,join方法只能一直阻塞等待,死等,只有某个线程执行完后,这个线程才能继续执行。而wait方法可以让A线程干活干了50% 然后去唤醒B线程更加灵活方便。
wait和notify的使用
1.wait和notify如果使用的对象不同,则notify不会唤醒wait
2.wait和notify必须加锁,且加锁的对象也必须相同,即四个使用的对象都必须相同
3.wait分为有参数的版本和无参数的版本,无参数的版本没有收到通知只能一直死等下去,有参数的版本,超过最大等待时间,就不notify也会直接重新尝试获取锁,获取锁成功之后继续执行
wait操作会有三个步骤
1.先释放锁
2.进入阻塞等待
3.在收到通知后,重新尝试获取锁,获取锁成功之后继续执行
无参数版本
运行结果:
1.notify只能唤醒同一个对象上等待的线程
2.t1 start之后需要休眠一会,防止notify后还没有wait,使得notify成为无效通知。
带参数版本
运行结果:
notifyAll
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 线程被唤醒");
});
Thread t2 = new Thread(() -> {
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 线程被唤醒");
});
Thread t3 = new Thread(() -> {
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3 线程被唤醒");
});
Thread t4 = new Thread(() -> {
synchronized (object) {
object.notifyAll();
}
});
t1.start();
t2.start();
t3.start();
Thread.sleep(500);
t4.start();
}
t1 t2 t3 是等待线程 t4 是唤醒线程 当是notifyAll的时候,可以把处在同一个对象等待的线程全部唤醒
运行结果:
当t4 notify时候会随机唤醒处于同一对象的等待线程
运行结果:
控制三个线程依次执行
创建三个线程,由于操作系统随机调度,抢占式执行,顺序并无法控制,通过notify wait可以使得三个线程按照期望依次运行
public static void main(String[] args) throws InterruptedException {
Object obj1 = new Object();
Object obj2 = new Object();
定义两个对象
t1 和 t2 wait和notify 使用obj1
t2 和 t3 wait和notify 使用obj2
Thread t1 = new Thread(() -> {
System.out.println("执行了 t1 线程");
synchronized (obj1) {
obj1.notify();
}
});
Thread t2 = new Thread(() -> {
try {
synchronized (obj1) {
obj1.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行了 t2 线程");
synchronized (obj2) {
obj2.notify();
}
});
Thread t3 = new Thread(() -> {
try {
synchronized (obj2) {
obj2.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行了 t3 线程");
});
t2.start();
t3.start();
让t1 和 t2 先创建并进入阻塞状态 防止t1 notify 成为无效通知。
Thread.sleep(500);
t1.start();
}
运行结果: