线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State 这个枚举中给出了线程生命周期的六种状态:
Timed Waiting(计时等待)
在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就
强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。sleep方法的使用需要记住下面几点:
- 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协
作关系。 - 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程
中会睡眠 - sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
//实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串
package ThreadStatusTeat;
public class StatusWaiting {
public static void main(String[] args) {
Runnable r= new Runnable() {
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if (i%10==0){
System.out.println("----------"+i);
}
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
new Thread(r).start();
}
}
LOCKED(锁阻塞)
Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态
Waiting(无限等待)
Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
package ThreadStatusTeat;
public class WaitingTest {
public static Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (obj){
try {
System.out.println("获取到锁对象。调用wait方法,进入waiting状态,释放锁对象");
obj.wait(3000);//计时等待,三秒时间到自动醒来
obj.wait();//无限等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"从waiting状态醒来,获取到锁对象,继续执行了");
}
}
}
},"等待线程").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+"---等待三秒钟");
try {
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(锁阻塞状态)。
线程状态图如下:
等待唤醒机制
线程间通信
概念:多个线程处理同一个资源,但是处理的动作(线程的任务)却不相同
比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个
是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
线程间通信目的:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们
希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就
是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效
的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制
概念:
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将
其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时
的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象
上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中 - notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先
入座。 - notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而
此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调
用 wait 方法之后的地方恢复执行。
总结如下:
如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对
象调用的wait方法后的线程。 - wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继
承了Object类的。 - wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方
法。
生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
包子铺线程生产包子**,吃货线程消费包子。
1、当包子没有时(包子状态为false),吃货线程等待,
2、包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。
3、接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。
涉及包子资源类**、吃货线程类及包子铺线程类,代码演示:
包子资源类:
package ThreadStatusTeat;
public class Baozi {
String pier;
String xianer;
boolean flag=false;
}
吃货线程类:
package ThreadStatusTeat;
public class ChiHuo extends Thread{
private Baozi bz;
public ChiHuo(Baozi bz,String name){
super(name);
this.bz=bz;
}
@Override
public void run() {
while (true){
synchronized (bz){
if (bz.flag==false){ //没包子
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag=false;
bz.notify();
}
}
}
}
包子铺线程类:
package ThreadStatusTeat;
public class BaoZiPu extends Thread{
private Baozi bz;
public BaoZiPu(Baozi bz,String name){
super(name);
this.bz =bz;
}
@Override
public void run() {
int count=0;
while (true){
synchronized (bz){
if (bz.flag==true){ //包子资源存在
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("包子铺正在做包子");
if (count%2==0){
bz.pier="冰皮";
bz.xianer="五仁";
}else{
bz.pier="薄皮";
bz.xianer="牛肉大葱";
}
count++;
bz.flag=true;
System.out.println("包子做好了:"+bz.pier+bz.xianer);
System.out.println("吃货来吃吧");
//唤醒等待线程
bz.notify();
}
}
}
}
测试类:
package ThreadStatusTeat;
public class BaoZiDemo {
public static void main(String[] args) {
Baozi bz =new Baozi();
ChiHuo ch = new ChiHuo(bz,"吃货");
BaoZiPu bzp = new BaoZiPu(bz,"包子铺");
ch.start();
bzp.start();
}
}
线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,
无需反复创建线程而消耗过多资源
线程池的作用
Java里面线程池的顶级接口是java.util.concurrent.Executor ,但是严格意义上讲Executor 并不是一个线程
池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService 。
在java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官
方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法:
public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线
程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法:
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
1、创建线程池对象
2、创建Runnable接口子类对象(task)
3、提交Runnable接口子类对象(take task)
4、关闭线程池(一般不做)
Runnable实现类代码:
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了:"+Thread.currentThread().getName());
System.out.println("教我游泳,教完后,教练回到了游泳池");
}
}
线程池测试类:
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
//1、创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
//2、创建Runnable实例对象
MyRunnable r = new MyRunnable();
//之前的做法
Thread t =new Thread(r);
t.start();
//3、从线程池中取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//再从线程池中取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//再从线程池中取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//注意submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭
//将使用完的线程又归还到了线程池
//service.shutdown();关闭线程池
}
}
Lambda表达式
函数式编程思想
面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
Lambda的更优写法
传统写法
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable 接口来定义任务内容,并使用
java.lang.Thread 类来启动该线程。代码如下:
public class Demo01Runnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
@Override
public void run() { // 覆盖重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start(); // 启动线程
}
}
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable 接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
Lambda的更优写法
借助Java 8的全新语法,上述Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() ‐> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担
语义分析
仔细分析上述代码中的语义, Runnable 接口只有一个run 方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
无参数:不需要任何条件即可执行该方案。
无返回值:该方案不产生任何结果。
代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda 语法中,要更加简单:
()->System.out.println("多线程任务执行!")
前面的一对小括号即run 方法的参数(无),代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码。
Lambda标准格式
Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数
一个箭头
一段代码
Lambda表达式的标准格式为:
(参数类型 参数名称) ‐> { 代码语句 }
格式说明:
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。
练习:使用Lambda标准格式(无参无返回)
给定一个厨子Cook 接口,内含唯一的抽象方法makeFood ,且无参数、无返回值。如下:
public interface Cook {
void makeFood();
}
下面的代码中,通过Lambda的标准格式调用invokeCook 方法,打印输出“吃饭啦!”字样:
package LambdaTest;
public class InvokeCookDemo {
public static void main(String[] args) {
/*Cook cook = new Cook() {
@Override
public Cook makeFood() {
System.out.println("吃饭啦!");
return null;
}
}.makeFood();*/
invokeCook(()->{
System.out.println("吃饭啦!");
});
}
private static void invokeCook(Cook cook) {
cook.makeFood();
}
}
练习:使用Lambda标准格式(有参有返回)
给定一个计算器Calculator 接口,内含抽象方法calc 可以将两个int数字相加得到和值:
package LambdaTest;
//给定一个Calculate接口,内含抽象方法calc可以将两个int 数字相加得到和值
public interface Calculate {
public abstract int calc(int a, int b) ;
}
在下面的代码中,通过Lambda的标准格式调用invokeCalc 方法,完成120和130的相加计算:
package LambdaTest;
public class InvokeCalculateDemo {
public static void main(String[] args) {
/*new Calculate() {
@Override
public void calc(int a, int b) {
int result = a+b;
System.out.println(result);
}
}.calc(100,200);*/
invokeCalc(120,130,(int a, int b)->{
return a+b;
});
}
private static void invokeCalc(int a,int b,Calculate calculate){
int result = calculate.calc(a,b);
System.out.println("结果是:"+result);
}
}
Lambda省略格式
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可
以使用Lambda的省略写法:
public static void main(String[] args) {
invokeCalc(120, 130, (a, b) ‐> a + b);
}
省略规则:
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
eg:仍然使用前文含有唯一makeFood 抽象方法的厨子Cook 接口,使用Lambda的省略格式调用
invokeCook 方法,打印输出“吃饭啦!”字样:
public static void main(String[] args) {
invokeCook(() ‐> System.out.println("吃饭啦!"));
}
Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable 、Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一
时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。