1. 线程状态
-
新建状态(New):
当用new操作符创建一个线程时, 例如new Thread( r ),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码。 -
就绪状态(Runnable):
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。 -
运行状态(Running):
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法. -
阻塞状态(Blocked):
线程运行过程中,可能由于各种原因进入阻塞状态:
(1) 线程通过调用sleep方法进入睡眠状态;
(2) 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
(3) 线程试图得到一个锁,而该锁正被其他线程持有;
(4) 线程在等待某个触发条件;所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
-
死亡状态(Dead):
有两个原因会导致线程死亡:
(1) run方法正常退出而自然死亡。
(2) 一个未捕获的异常终止了run方法而使线程猝死。为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。
1.1 等待唤醒案例分析
等待唤醒案例: 线程之间的通信
Object类中的方法:
- void wait() 在其他线程调用此对象的notify()方法或notifyAll()方法前,使当前线程等待;
- void notify() 唤醒在此对象监视器上等待的单个线程,会继续执行wait()方法之后的代码
代码实现:
- 创建一个顾客线程:告知老板要的包子种类和数量,调用wait方法,放弃cpu执行,进入到WAITING状态(无限等待);
- 创建一个老板线程:花了5秒做包子,做好包子后,调用notify方法,唤醒顾客吃包子。
注意: 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行,同步使用的锁对象必须保证唯一,只有锁对象才能调用wait和notify方法。
public class WaitAndNotify {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj = new Object();
//创建一个顾客线程
new Thread(){
@Override
public void run(){
//保证等待和唤醒的线程只能有一个在执行,使用同步技术
synchronized (obj) {
System.out.println("告知老板要的包子种类和数量");
//调用wait方法,放弃cpu执行,进入到WAITING状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后继续执行wait后的代码
System.out.println("开始吃包子");
}
}
}.start();
//创建一个老板线程
new Thread(){
@Override
public void run() {
try {
Thread.sleep(5000); //花五秒做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("做好包子后,唤醒顾客吃包子");
obj.notify();
}
}
}.start();
}
}
1.2 Objects类中wait带参方法和notifyAll方式
进入到TimeWaiting(计时等待)有两种方法:
- 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
- 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
- void notify() 唤醒在此对象监视器上等待的单个线程
- void notifyAll() 唤醒在此对象监视器上等待的所有线程
1.3 线程间通信
概念: 多个线程在处理一个资源,但是处理的动作(线程的任务)却不相同。
为什么要处理线程间通信:
多个线程并发执行时,在默认情况下CPU是随即切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,用来帮助我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。这种手段就是——等待唤醒机制。
1.4 等待唤醒机制概述
等待唤醒中的方法:
- wait: 线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态即使WAITING。他还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中。
- notify/notifyAll: 即选取所通知对象的wait set中的一个(全部)线程释放。
1.5 吃包子 —— 代码实现
包子类:
public class BaoZi {
boolean flag = false;
String pi;
String xian;
}
包子铺类:
public class BaoZiPu extends Thread{
// 1. 需要创建一个包子变量
private BaoZi bz;
// 2. 使用带参构造方法,为这个包子变量赋值
public BaoZiPu(BaoZi bz){
this.bz = bz;
}
// 3. 设置线程任务:生产包子
@Override
public void run() {
int count = 0;
//使用同步技术保证两个线程只能有一个正在执行
synchronized (bz) {
//对包子状态进行判断
if(bz.flag == true) {
//包子铺调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后执行,生产包子,交替生产两种包子
if(count % 2 == 0){
bz.pi = "薄皮";
bz.xian = "三鲜馅";
} else {
bz.pi = "冰皮";
bz.xian = "豆沙馅";
}
count ++;
System.out.println("包子铺正在生产" + bz.pi + bz.xian + "包子");
//生产包子需要3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子铺生产好了包子,改包子的状态为tree(有包子)
bz.flag = true;
//唤醒吃货线程吃包子
bz.notify();
System.out.println("包子已经生产好了,吃货可以开始吃了");
}
}
}
吃货类:
public class ChiHuo extends Thread {
private BaoZi bz;
public ChiHuo(BaoZi bz){
this.bz = bz;
}
//设计线程任务:吃包子
@Override
public void run() {
synchronized (bz) {
//对包子的状态进行判断
if(bz.flag == false) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,吃包子
System.out.println("正在吃包子");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//吃完后,修改包子的状态为false
bz.flag = false;
//唤醒包子铺线程生产包子
bz.notify();
System.out.println("吃货已经吃完包子了,开始生产包子");
}
}
}
测试类:
public class DemoBaozi {
public static void main(String[] args) {
BaoZi bz = new BaoZi();
new BaoZiPu(bz).start();
new ChiHuo(bz).start();
}
}
2. 线程池
概念: 线程池是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。JDK1.5后可直接使用
- 当程序第一次启动的时候,创建多个线程,保存到一个集合中
- 当我们想要使用多线程的时候,就可以从集合中取出来线程使用
- 当我们使用完毕线程,需要把线程归还给线程池
2.1 线程池代码实现
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程的线程池
参数: int nThreads,创建线程池中包含的线程数量;
返回值: ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)。
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task)提交一个Runnable任务用于执行
关闭/销毁线程池的方法: void shutdown()。
//2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class DemoThreadPool {
public static void main(String[] args) {
//1. 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool产生一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new RunnableImpl());
//线程会一直开启,使用完了线程,会把线程归还线程池,线程可以继续使用
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
//4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
es.shutdown();
}
}
3. Lambda表达式
面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。
函数式编程思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。
3.1 冗余的Runnable代码
使用实现Runnable接口的方式来实现多线程程序:
public class Demo01Runnable {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类
Thread t = new Thread(run);
//调用start方法开启新线程,执行run方法
t.start();
//简化代码,使用匿名内部类,实现多线程程序
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(r).start();
//继续简化
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}
对于Runnable的匿名内部类用法,可以分析出:
Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务的核心;为了指定run的方法体,不得不需要Runnable接口的实现类;为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;为了覆盖重写抽象run方法,所以方法名称,方法参数,方法返回值不得不再写一遍,且不能写错;而实际上,方法体才是关键所在。
3.2 Lambda的更优写法
JDK1.8中加入了Lambda表达式的新特性
public class DemoLambda {
public static void main(String[] args) {
//使用Lambda表达式实现多线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}
).start();
}
}
3.3 Lambda标准格式
Lambda标准格式由三部分组成:一些参数,一个箭头,一段代码。
格式: (参数列表) -> {一些重写方法的代码}
格式说明:
- ():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数之间使用逗号隔开;
- ->:传递的意思,把参数传递给方法体{}
- {}:重写接口中的抽象方法的方法体。
3.4 Lambda表达式的练习
无参数无返回值的情况下:
- 给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数,无返回值;
- 使用Lambda的标准格式调用invokeCook方法,打印出“吃饭啦”的字样。
public interface Cook {
public abstract void makeFood();
}
public class DemoCook {
public static void main(String[] args) {
invokeCook(new Cook() {
@Override
public void makeFood() {
System.out.println("吃饭啦");
}
});
//使用Lambda表达式
invokeCook(() -> {
System.out.println("吃饭啦");
}
);
}
public static void invokeCook(Cook cook) {
cook.makeFood();
}
}
有参数无返回值的情况下:
- 使用数组存储多个Person对象
- 对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
public class Person {
String name;
int age;
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class DemoArrays {
public static void main(String[] args) {
Person[] arr = {
new Person("猪猪侠", 15),
new Person("小菲菲", 14),
new Person("超人强", 16)
};
Arrays.sort(arr, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
//使用Lambda表达式
Arrays.sort(arr, (Person o1, Person o2) -> {
return o1.getAge() - o2.getAge();
}
);
for (Person person : arr) {
System.out.println(person);
}
}
}
有参数有返回值的情况下:
- 给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数组相加得到和值;
- 使用Lambda的标准格式调用invokeCalc方法,计算1+2。
public interface Calculator {
public abstract int calc(int a, int b);
}
public class DemoCalculator {
public static void main(String[] args) {
invokeCalc(1, 2, new Calculator() {
@Override
public int calc(int a, int b) {
return a + b;
}
});
//使用Lambda表达式
invokeCalc(1, 2, (int a, int b) -> {
return a + b;
}
);
}
public static void invokeCalc(int a, int b, Calculator c){
int sum = c.calc(a, b);
System.out.println(sum);
}
}
3.5 Lambda省略格式和使用前提
Lambda表达式:凡是可以推导出来的,都可以省略;
可以省略的内容:
- 参数列表:括号中参数列表的数据类型,可以省略不写;
- 参数列表:括号中的参数如果只有一个,那么类型和()都可以省略;
- 一些代码:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号),{}、return、分号必须一起省略。
例如上面代码中分别可以省略成:
invokeCook(() -> System.out.println("吃饭啦"));
Arrays.sort(arr, (o1, o2) -> o1.getAge() - o2.getAge());
invokeCalc(1, 2, (a, b) -> a + b);
使用前提:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法;
- 使用Lambda必须具有上下文推断,也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才可以使用Lambda作为该接口的实例。