文章目录
一、线程状态
1、线程状态(生命周期)概述
当线程被创建并启动后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程生命周期中,在APIjava.lang.Thread.State
这个枚举中给出了6种线程状态 (始于JDK1.5):
线程状态 | 线程状态发生条件 |
---|---|
New(新建) | 尚未启动的线程处于此状态。还未调用start方法 |
Runnable(可运行) | 在Java虚拟机中执行的线程处于此状态,可能正在运行自己的代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程视图获取一个对象锁,而该对象锁被其他线程拥有,这时就会进入阻塞状态;当该线程持有锁时,就会进入Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)特定动作时,该线程进入waiting状态,通过锁对象调用Object.wait()。进入这个状态后是不能自动唤醒,必须等待另一个线程通过锁对象调用notify或者notifyAll方法才能被唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入TimedWaiting状态。这个状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep(long timeout)、Object.wait(long timeout) |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获异常终止了run方法而死亡。 |
2、Timed Waiting(计时等待)
Timed Waiting状态在线程安全的卖票案例中就已经使用了,待用sleep方法,就时让线程进入计时等待状态,在该状态下就是让线程休眠,不执行任何代码,以”减慢线程“。
实现一个计时器,计数100,在每个数字之间停1秒,每隔10个数字输出一个字符串。
public class TimedWaiting implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
try {
System.out.println(i);
if(i % 10 == 0){
System.out.println("--------");
}
Thread.sleep(1000); // 进入休眠状态,Timed Waiting
} catch (InterruptedException e) {
System.out.println("中断异常");
}
}
}
public static void main(String[] args) {
new Thread(new TimedWaiting()).start();
}
}
3、BLOCKED (阻塞状态)
当多个线程抢夺cpu执行权,当一个线程抢到了cpu执行权,执行run方法,run方法种有同步锁,当该同步锁被其他线程夺走了,那么该线程就不能执行同步代码,所以就会进入blocked阻塞状态。
另外waiting状态和timedwaiting状态被唤醒之后,也不能拿到锁对象,因此也会进入blocked阻塞状态。
直到拿到锁对象,才会进入runnable状态去执行同步代码。
4、Waiting (无限等待状态)
一个线程在等待另一个线程执行一个(唤醒)特定动作时,该线程进入waiting状态,通过Object调用wait方法进入waiting状态。进入这个状态后是不能自动唤醒,必须等待另一个线程Object调用notify或者notifyAll方法才能被唤醒。也就时线程之间通信,告知你该醒了。
案例 顾客与老板包子
实现一个顾客需要买包子,告知老板我要卖包子,顾客进入等待状态,老板花5秒时间去做包子,做好了之后告知顾客,顾客得到告知,开始吃包子。
代码实现:
// 为什么这里顾客线程永远先拿到锁?
// 因为顾客线程先执行,先start(),所以会先拿到锁去执行自己的类中的run方法
public class WaitingTest {
public static void main(String[] args) {
Object obj = new Object(); // 创建一个锁对象,两个线程通过同一个锁对象进行通信交流
// 创建一个匿名内部类,顾客
new Thread(){
@Override
public void run() {
synchronized (obj){
try {
System.out.println("老板,我要买包子。");
// 进入无限等待状态,这时cpu执行权就让出去了,并将锁对象释放,必须等待被唤醒,才可以继续执行
obj.wait();
// 顾客线程接收到了老板的通知,被唤醒
System.out.println("谢谢老板,你的包子真好吃!");
} catch (InterruptedException e) {
System.out.println("wait()中断异常");;
}
}
}
}.start();
// 创建一个匿名内部类,老板
new Thread(){
@Override
public void run() {
synchronized (obj){
try {
// 拿到锁对象,获得cpu执行权,线程进入休眠状态,这过程中不会释放锁对象,花5秒时间做包子,5秒后自动唤醒
sleep(5000);
System.out.println("顾客,你的包子做好了。");
obj.notify(); // 5秒后,告知顾客线程,你的包子好了,不会释放锁
} catch (InterruptedException e) {
System.out.println("sleep()中断异常");
}
}
}
}.start();
}
}
5、Object类中的方法
wait()与sleep()区别
都来自不同的类
wait ==》 Object类中
sleep ==》 Thread类中
public final void wait(long timeout) throws InterruptedException
Object锁对象调用该方法。导致当前线程等待,直到另一个线程调用此对象的notify()
方法或notifyAll()
方法,或指定的时间已过则会自动唤醒。进入Timed Waiting状态。该方法必须在同步代码块中使用。会释放资源。会释放锁。public void sleep(long timeout) throws InterruptedException
线程调用该方法 ,进入Timed Waiting状态,时间一过会自动唤醒线程。任何地方使用。不释放资源。不会释放锁。
wait()和wait(long timeout)区别
- wait()方法会释放资源释放锁,进入Waiting状态
- wait(long timeout)方法会释放资源释放锁,进入TimedWaiting状态
sleep()和sleep(long timeout)区别
- sleep()方法不会释放锁,也不会释放资源,处于runnable的暂停状态
- sleep(long timeout)方法会释放锁,会释放资源,进入timedWaiting状态,时间内会被notify或者notyall唤醒,超过时间会自动唤醒
notify()和notifyAll()区别
会让进入waiting状态的线程唤醒过来,进入到blocked阻塞状态,等待线程执行完代码之后,释放锁,才会有机会去抢到锁而执行。因此notify和notifyAll是不会立刻释放锁的,只是通知线程你被唤醒,你可以抢夺cpu执行权。只有代码执行完了,该线程才会释放锁。
-
public final void notify()
会随机唤醒等待中的一个线程,先唤醒等待时间久的。 -
public final void notifyAll()
唤醒所有等待Waiting中的线程。
二、线程等待唤醒机制
1、线程之间通信
线程通信也就是线程之间的合作。就如同顾客与老板的案例。
为什么要处理线程间通信:
多个线程是并发执行的,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同完成一件任务时,并且有规律的去执行任务,那么多线程之间就必须得有一些协调通信。
通过等待唤醒机制保证线程间通信有效利用资源。
2、等待唤醒机制
等待唤醒机制是多个线程的一种协作机制。就是一个线程进行了规定操作之后,就进入了等待状态(wait()),等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待的线程。
等待唤醒中的方法
等待唤醒机制就是用于解决线程通信的问题,使用到以下3个方法:
- wait:线程不再活动,不再参与调度,进入wait set(等待集合)中,释放锁对象,因此不会浪费CPU资源,也不会去竞争锁,这时的线程状态就是WAITING。他还要的等待别的线程执行一个特别的动作,也就是“通知notify”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中。
- notify:则选取所通知对象的wait set中的一个线程释放。先唤醒等待时间久的。
- notifyAll:则释放所有通知对象的wait set上的全部线程。
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻锁对象已经被其他线程占有,所以它需要再次尝试获取锁对象(很可能面临其他线程的竞争),成功之后才能开始恢复执行当初调用wait方法之后的代码。
总结:
- 如果能获取锁,线程就能从WAITING状态变为RUNNABLE状态
- 如果不能获取锁,从wait set出来,进入 entry set(进入集合) ,线程从WAITITNG状态又变成BLOCKED状态
notify和wait方法的使用注意事项:
- notify和wait方法都必须使用同一个锁对象,因为notify只能唤醒同一个锁对象的线程。
- notify和wait方法都必须在同步代码块或同步方法中调用,可以保证锁对象唯一。
- notify和wait方法必须是Object类中的方法。因此所有的类对象都可以成为锁对象。
练习:生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
包子铺线程生产包子,吃货线程消费包子。当包子没有时,吃货线程等待,包子铺线程生产包子,
并通知吃货线程,因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进
一步执行取决于锁的获取情况。如果吃货获得锁,那么就执行吃包子的动作,包子吃完了,并通
知包子铺线程,吃货线程进入等待,包子铺线程能否进一步执行则取决于锁的获取情况。
Baozi.java
public class Baozi {
private String pi; // 包子皮
private String xian; // 包子馅
private boolean zhuangTai = false; // 判断是否有包子,初始化没有包子
public String getPi() {
return pi;
}
public void setPi(String pi) {
this.pi = pi;
}
public String getXian() {
return xian;
}
public void setXian(String xian) {
this.xian = xian;
}
public boolean isZhuangTai() {
return zhuangTai;
}
public void setZhuangTai(boolean zhuangTai) {
this.zhuangTai = zhuangTai;
}
}
Shengchan.java
public class Shengchan implements Runnable{
// 创建一个锁对象
private Baozi baozi;
public Shengchan(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
int count = 0; // 定义一个变量,用于生产不同的包子
while (true){
synchronized (baozi){
if(baozi.isZhuangTai()){ // 如果有包子,就等待
try {
baozi.wait();
} catch (InterruptedException e) {
System.out.println("生产wait()有异常!");
}
}
// 如果没有包子就生产包子
if(count % 2 == 0){
baozi.setPi("薄皮");
baozi.setXian("三鲜馅");
}else {
baozi.setPi("厚皮");
baozi.setXian("大葱牛肉馅");
}
count++;
System.out.println("请稍等,正在为你生产包子...");
try {
Thread.sleep(5000); // 让线程等待5秒钟,假设用5秒在生产包子
} catch (InterruptedException e) {
System.out.println("生产sleep()有异常!");
}
System.out.println("你的"+ baozi.getPi() + baozi.getXian() + "包子,已为你生产完成,请享用!");
// 将包子的状态设置为有包子,并通知等待的线程
baozi.setZhuangTai(true);
baozi.notify();
}
}
}
}
Xiaofei.java
public class Xiaofei implements Runnable {
// 创建一个锁对象
private Baozi baozi;
public Xiaofei(Baozi baozi) {
this.baozi = baozi;
}
@Override
public void run() {
while (true){
synchronized (baozi){
if(baozi.isZhuangTai()){ // 如果为true就是有包子,就吃包子
System.out.println("正在吃" + baozi.getPi() + baozi.getXian() + "包子...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("消费sleep()有异常!");
}
baozi.setZhuangTai(false); // 将包子设为false表示没有包子了
System.out.println("包子已吃完,老板我还要包子。");
System.out.println("-------------------------------------");
try {
baozi.notify(); // 通知等待的生产线程,你要准备生产把包子了
baozi.wait();
} catch (InterruptedException e) {
System.out.println("消费wait()有异常!");
}
}else {
baozi.notify(); // 通知等待的生产线程,你要准备生产把包子了
}
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
Baozi baozi = new Baozi(); // 创建一个包子类
// 将包子类给两个线程,保证了锁对象一致,两个线程也可以对同一资源(同包子实例)进行处理
new Thread(new Shengchan(baozi)).start();
new Thread(new Xiaofei(baozi)).start();
}
}
三、线程正确停止
方法:
- 利用次数,不建议死循环
- 建议使用标志位,如建立flag通过判断true,false
- 不建议使用JDK的stop方法,interrupt方法,destroy放等一些过时的方法
public class CallableTest implements Runnable {
private boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println("线程run");
}
}
public void stop(){
flag = false;
}
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
new Thread(callableTest).start();
for (int i = 0; i < 10000; i++) {
if(i == 5000){
callableTest.stop();
System.out.println("线程被停止了");
}
System.out.println(i);
}
}
}
四、线程优先级
线程优先级用数字表示,范围1~10
注意:
不是优先级高的就能够被优先执行,执行结果还是的看cpu的调度。
优先级高了其实就是增加了权重,被执行的几率就大了很多。
Thread.MAX_PRIORITY = 10;
Thread.MIN_PRIORITY = 1;
Thread.NORM_PRIORITY = 5;
线程调用方法获取和设置优先级:
getPriority()
setPriority(int num)
超过最大值、最小值会报异常
五、守护(daemon)线程
-
线程分为守护线程(后端线程)和用户线程(前端线程)
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕,如:后台记录日志,监控内存,垃圾回收(GC)等
用户线程执行完了,程序就结束了。不用管守护线程。
自己创建的正常线程,main线程都是用户线程。
调用方法设置守护线程:
setDaemon(boolean flag)
传值为true,则设置为守护线程。默认为false。