@[toc]
第三章:线程状态
线程状态概述
当线程被创建并启动以后,它既不是已启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在Java的API中java.lang.Thread.State
这个枚举中给出了六种线程状态。
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态 |
Waiting(无线等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,则该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Time Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep ,Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了rtun方法而死亡 |
Timed Waiting(计时等待)
计时等待也就是暂时在一段时间内等待,时间一过或者被其他进程唤醒就可以进入可运行状态。
案例:计数到100,没数10个就睡眠1秒
public class demo05 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i);
}
}
}, "aa").start();
}
}
其中还有些细节需要注意:
- 进入
TIMED_WAITING
状态可以在单独的线程中调用sleep
方法,不一定非要有协作关系。 sleep
方法一定要放在run方法内,这样才会使该线程睡眠- sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable状态。而且苏醒后还需要获取到处理机后次啊能执行。
BLOCKED(锁阻塞)
一个正在阻塞等待一个锁对象的线程处于这一状态。
线程A和线程B同时争抢锁,如果线程A获取到锁,线程A进入到可执行状态,线程B就需要进入到阻塞状态。
这事由Runnable状态进入Blocked状态,初次Waiting
和Time_Waiting
状态也会在某种情况下进入阻塞状态。
Waiting(无限等待)
一个正在无限期等待另一个线程执行一个唤醒操作的线程处于这一状态。
我们需要用到Object类的两个方法,wait
和notify
方法。
public class demo05 {
public static void main(String[] args) {
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁对象,调用wait方法,进入waiting状态,释放锁对象");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒了,并获取到锁对象,可以继续执行了");
}
}
}
}, "等待线程").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "等待3秒钟");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "获取到锁对象,调用notify方法,释放锁对象");
obj.notify();
}
}
}, "唤醒线程").start();
}
}
结果:
通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify()方法 或 Object.notifyAll()方法。
其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系, 多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞 争,但更多时候你们更多是一起合作以完成某些任务。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入 了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了 notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入 Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
总结
一个线程调用sleep
函数,只是表示当前线程释放处理机,不代表释放锁,如果在睡眠之前就拥有锁,那么在睡眠后只需要获取到处理机就可以再次执行。(这就相当于我虽然睡了一会,但人在还在那里不走)
而wait操作时将锁释放掉并释放处理机。(直接无限期睡眠,并且人离开了)
第四章:等待唤醒机制
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如:线程A用来生产包子,线程B用来吃包子,包子可以理解为同一资源,线程A与线程B处理的东综,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
为什么要处理线程间通信:
多个线程并发执行时,在默认情况下CPU时随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,一次来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效率利用资源
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程能有效的利用资源。这就是等待唤醒机制。
等待唤醒机制
什么是等待唤醒机制
这事多个线程间的一种协作机制,也可以说是同步机制,线程之间不仅只有争夺资源,也会相互协作。只有当一件事完成之后下一件事才能发生。
一个线程进行了规定操作之后,就进入等待状态,等待其他线程执行完他们的指定代码过后再将其唤醒,在有多个线程进行等待时,如果需要,可以使用notifyall
来唤醒所有的等待线程。
wait/notify就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用来解决线程间通信的问题的,其中包含了三个方法:
wait
:线程不再获得,不在参与调度,进入wait set
的等待队列中,因此不会浪费CPU资源,也不会去争夺锁了,这时的线程状态即是WAITING
。它还要等着别的线程执行了notify
操作来唤醒此线程,从等待队列中释放出来,重新进入调度队列中notify
: 从wait set
选取一个线程释放。notifyAll
:则释放wait set
上的所有进程。
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。
代码演示:
包子类:
public class BaoZi {
private String type;
public BaoZi() {
}
public BaoZi(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
盘子类
public class Plate {
private boolean flag = false;// 当前盘子中有包子
private BaoZi baoZi;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public BaoZi getBaoZi() {
return baoZi;
}
public void setBaoZi(BaoZi baoZi) {
this.baoZi = baoZi;
}
public Plate(boolean flag, BaoZi baoZi) {
this.flag = flag;
this.baoZi = baoZi;
}
}
顾客类:
public class Customer extends Thread {
final Plate plate;
public Customer(String name, Plate plate) {
super(name);
this.plate = plate;
}
@Override
public void run() {
while (true) {
synchronized (plate) {
if (plate.isFlag() == false) {
try {
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
plate.setFlag(false);
BaoZi baoZi = plate.getBaoZi();
System.out.println(this.getName() + "正在吃" + baoZi.getType());
plate.notify();
}
}
}
}
包子铺类
public class Shop extends Thread {
private Plate plate;
private BaoZi baoZi;
public Shop(Plate plate) {
this.plate = plate;
}
public Plate getPlate() {
return plate;
}
public void setPlate(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
while (true) {
synchronized (plate) {
if (plate.isFlag() == true) {
try {
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
plate.setFlag(true);
System.out.println("生产包子");
baoZi = new BaoZi("鲜肉");
plate.setBaoZi(baoZi);
plate.notify();
}
}
}
}
测试类
public class Demo01 {
public static void main(String[] args) {
Plate plate = new Plate(false, null);
Customer customer = new Customer("小明", plate);
Shop shop = new Shop(plate);
customer.start();
shop.start();
}
}
运行结果
通过结果我们可以清楚可到两个线程实现了同步。
第五章:线程池
线程池概念
线程池:就是一个可以容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
再上图的例子中:
任务1、2、3都获取到了线程池对象,可以执行任务
但任务4由于线程池已无空闲线程,所以只能等待其他任务释放线程。
利用线程池的好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能执行。
- 提高线程的可管理性:可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
创建Runnable接口的实现类
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
线程池测试类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
// 创建Runnable实例对象
MyRunnable myRunnable = new MyRunnable();
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(myRunnable);
service.submit(myRunnable);
service.submit(myRunnable);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
service.shutdown();
}
}
运行结果