1.实现多线程的第三种方式_Callable开启多线程(理解)
在java中实现多线程有三种方式:
-
继承Thread类 重写Thread类中的run方法,自定义类称为线程类
-
自定义类实现任务接口Runnable,实现Runnable中的run方法,书写任务 自定义类称为任务类
void run()
任务方法run没有返回值
-
自定义类实现任务方法Callable,该接口
Callable
接口类似于Runnable
,只是Runnable接口的run方法没有返回值,而Callable接口中的**call**()方法是有返回值的:V call() 计算结果,如果无法计算结果,则抛出一个异常。 返回值:V 就是返回的结果
-
实现多线程方式三实现
Callable
接口的步骤:-
自定义任务类实现任务接口
Callable<V>
-
在自定义任务类中实现抽象方法call()方法,书写任务
-
创建任务类对象
-
获取线程池对象,存放线程
-
使用线程池对象调用方法:
<T> Future<T> submit(Callable<T> task) 参数: task:表示要执行的任务对象 返回值:使用Future接收call方法的返回值,使用Future接口中的方法: V get() 取出结果
-
代码演示:
练习1:
package com.itheima.sh.callable_01; import java.util.concurrent.Callable; // 1.自定义任务类实现任务接口`Callable<V>` public class MyTask implements Callable<Object>{ // 2.在自定义任务类中实现抽象方法call()方法,书写任务 @Override public Object call() throws Exception { for (int i = 0; i < 10; i++) { System.out.println(i); } return null; } } package com.itheima.sh.callable_01; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /* 实现多线程方式三实现`Callable`接口的步骤: 1.自定义任务类实现任务接口`Callable<V>` 2.在自定义任务类中实现抽象方法call()方法,书写任务 3.创建任务类对象 4.获取线程池对象,存放线程 5.使用线程池对象调用方法: <T> Future<T> submit(Callable<T> task) 参数: task:表示要执行的任务对象 返回值:使用Future接收call方法的返回值, 使用Future接口中的方法: V get() 取出结果 */ public class Test01 { public static void main(String[] args) throws ExecutionException, InterruptedException { //3.创建任务类对象 MyTask mt = new MyTask(); //4.获取线程池对象,存放线程 ExecutorService es = Executors.newFixedThreadPool(3); //5.使用线程池对象调用方法: <T> Future<T> submit(Callable<T> task) Future<Object> f = es.submit(mt); //6.取出返回的数据 Object o = f.get(); System.out.println("o = " + o);//null } }
练习2:
package com.itheima.sh.callable_01; import java.util.concurrent.Callable; //1.自定义任务类实现任务接口`Callable<V>` public class MyTask2 implements Callable<Integer>{ //2.在自定义任务类中实现抽象方法call()方法,书写任务 //需求:求1-5和值 @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 5; i++) { sum = sum + i; } return sum; } } package com.itheima.sh.callable_01; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /* 实现多线程方式三实现`Callable`接口的步骤: 1.自定义任务类实现任务接口`Callable<V>` 2.在自定义任务类中实现抽象方法call()方法,书写任务 3.创建任务类对象 4.获取线程池对象,存放线程 5.使用线程池对象调用方法: <T> Future<T> submit(Callable<T> task) 参数: task:表示要执行的任务对象 返回值:使用Future接收call方法的返回值, 使用Future接口中的方法: V get() 取出结果 */ public class Test02 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 3.创建任务类对象 MyTask2 mt = new MyTask2(); // 4.获取线程池对象,存放线程 ExecutorService es = Executors.newFixedThreadPool(2); // 5.使用线程池对象调用方法:<T> Future<T> submit(Callable<T> task) Future<Integer> f = es.submit(mt); // 6.获取数据 V get() 取出结果 Integer i = f.get(); System.out.println("i = " + i); } }
-
小结:实现多线程方式三:继承Callable接口,实现方法 V call() 有返回值 只要需要返回值的就使用Callable不需要返回值就使用Runnable
-
-
2.死锁(掌握)
-
概念:就是多个线程操作共享资源时,会产生互相等待锁的现象就是死锁。
-
死锁是要避免的,我们平常使用多线程开发的时候会经常遇到死锁现象,所以一定要避免。
-
演示死锁现象:
- 如果使用同步的嵌套并且获取多把锁特别容易出现死锁现象
- 死锁现象案例描述:有两个线程 t1 t2,执行共享资源必须获取两把锁才可以执行。
- t1线程:获取锁的顺序:先获取lock_a锁 然后在获取lock_b锁
- t2线程:获取锁的顺序:先获取lock_b锁 然后在获取lock_a锁
-
代码演示:
package com.itheima.sh.death_lock_02; /* - t1线程:获取锁的顺序:先获取lock_a锁 然后在获取lock_b锁 - t2线程:获取锁的顺序:先获取lock_b锁 然后在获取lock_a锁 */ public class DeathLockTask implements Runnable { //定义成员变量 private Object lock_a = new Object(); private Object lock_b = new Object(); boolean flag = true;//定义标记让jvm执行哪个代码 @Override public void run() { //判断 //- t1线程:获取锁的顺序:先获取lock_a锁 然后在获取lock_b锁 if(flag){//假设t1执行 while(true){ //加同步 synchronized (lock_a){//t1 System.out.println(Thread.currentThread().getName()+"---if----lock_a"); synchronized (lock_b){ System.out.println(Thread.currentThread().getName()+"---if----lock_b"); } } } }else{//t2执行 // - t2线程:获取锁的顺序:先获取lock_b锁 然后在获取lock_a锁 while(true){ //加同步 synchronized (lock_b){//t2 System.out.println(Thread.currentThread().getName()+"---else----lock_b"); synchronized (lock_a){ System.out.println(Thread.currentThread().getName()+"---else----lock_a"); } } } } } } package com.itheima.sh.death_lock_02; public class Test01 { public static void main(String[] args) throws InterruptedException { //创建任务类对象 DeathLockTask dlt = new DeathLockTask(); //创建线程 Thread t1 = new Thread(dlt, "t1"); t1.start(); //让主线程休眠 Thread.sleep(10); //修改flag标记,让t2线程执行else dlt.flag = false; //创建线程 Thread t2 = new Thread(dlt, "t2"); t2.start(); } }
-
图解:
- 小结:
- 开发中我们要一定避免死锁现象,能不要同步嵌套就不嵌套,如果嵌套锁保证唯一。
3.线程状态(掌握)
在java中多线程一共有六个状态。位于枚举类Thread.State中:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典叫法) |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
1.New 新建状态 就是刚创建线程,还没有调用start启动 new Thread(任务对象) 不能被cpu运行
2.Runnable 可运行状态,就是使用线程调用start方法获取系统资源启动线程,但是不一定立刻被cpu执行,只是有可能被运行,在运行的队列中等待(可运行状态的线程具有cpu执行的资格)
3.Blocked(锁阻塞状态) :就是某个线程想要进入到同步中必须获取锁对象,此时锁对象被其他线程占有,二当前线程就是处于锁阻塞状态
4.Timed Waiting(计时等待)状态:有两种情况可以让一个正在运行的线程进入到计时等待状态:
1)线程遇到static sleep(毫秒) 此时线程就会进入到计时等待状态,进入到计时等待状态的线程不会被cpu执行,只有休眠的线程醒来之后才有可能被cpu执行(具有cpu执行的资格,在可运行的队列中等待cpu执行)
2)使用锁对象调用Object类中的等待方法:void wait(long timeout) 位于Object类中的原因是因为该方法必须使用锁对象调用,而锁对象是任意对象,能够被任意对象调用的方法肯定位于所有类的共同父类Object中。
对于带参数的wait(long timeout) 有两种醒来方式:a:时间到自然醒 b:时间未到被唤醒
从等待中醒来的线程必须获取到锁对象才可以被执行,没有锁对象即使醒来也不会执行
5.无限等待:使用锁对象调用Object类中的wait()是无参的可以让线程无限等待。处于无限等待的线程只能被唤醒,使用锁对象调用Object类中的 notify() 方法或 notifyAll() 来唤醒等待线程。cpu不会执行无限等待的线程
6.Terminated(被终止):线程停止运行。1)遇到异常,线程就会被终止 2)执行完任务
线程状态图:
4.线程之间的通信_等待唤醒机制(包子铺卖包子)掌握
等待唤醒机制,在这里我们使用生产者和消费者线程来进行演示。
-
为什么等待和唤醒的方法位于Object类中?
等待方法:void wait();
唤醒方法:
1.notify()随机唤醒单个等待的线程
2.notyfyAll() 唤醒所有等待的线程
等待和唤醒的方法位于Object类中是因为等待和唤醒机制必须依赖于锁对象,使用锁对象调用等待和唤醒的方法,而锁对象属于任意对象,能够被任意对象调用的方法肯定位于Object类中
-
由于等待和唤醒都需要使用锁对象调用,而只有在同步中才有锁对象,所以等待和唤醒机制必须在同步中完成
-
如果一个线程处于等待状态,那么会立刻释放锁对象,此时cpu永远不会执行等待的线程,只有被其他线程唤醒之后并获取到锁对象才有可能被cpu执行
-
入门代码:
package com.itheima.sh.pro_consu_03; public class MyTask1 implements Runnable{ @Override public void run() { //加同步 synchronized ("abc"){//"abc"是同步锁t1 System.out.println("A"); System.out.println("B"); try { //让当前线程等待 //释放锁对象,cpu不会执行等待线程 "abc".wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("C"); System.out.println("D"); } } } package com.itheima.sh.pro_consu_03; public class MyTask2 implements Runnable{ @Override public void run() {//t2 //加同步 synchronized ("abc"){//"abc"是同步锁 System.out.println("1"); System.out.println("2"); //唤醒等待的线程 "abc".notify(); //因为t2占有锁 System.out.println("3"); System.out.println("4"); } } } package com.itheima.sh.pro_consu_03; public class Test01 { public static void main(String[] args) throws InterruptedException { // "abc".wait(); // "abc".notify();t1 等待 其他线程打断了等待的线程或者休眠的线程此时就会抛被打断异常 sleep(2min) //创建任务对象 MyTask1 mt1 = new MyTask1(); MyTask2 mt2 = new MyTask2(); //创建线程 Thread t1 = new Thread(mt1); Thread t2 = new Thread(mt2); //启动线程 t1.start(); //休眠 Thread.sleep(5000); t2.start(); } }
图解:
-
包子案例
创建包子类:
package com.itheima.sh.pro_con_04; //创建包子类: public class BaoZi { //成员变量 String pi;//皮 String xian;//馅 //定义标记 boolean flag = false;//false表示没有包子 true 表示有包子 }
创建生产包子的任务类:
package com.itheima.sh.pro_con_04; /* 创建生产包子的任务类: 生产包子思想: 在生产包子之前先判断是否有包子,如果有包子就不生产则等待; 如果没有包子,则生产包子,唤醒吃货来吃包子。 */ public class ProduceBaoZi implements Runnable { //定义成员变量接收包子对象 BaoZi baoZi; public ProduceBaoZi(BaoZi baoZi) { this.baoZi = baoZi; } @Override public void run() { //模拟无限生产使用死循环 while (true) { //为了安全加同步 synchronized ("abc") {//t1 // 在生产包子之前先判断是否有包子,如果有包子就不生产则等待; //flag的值是true表示有包子 if (baoZi.flag) { try { //说明flag是true,表示有包子,不生产则等待; "abc".wait(); } catch (InterruptedException e) { e.printStackTrace(); } } /* 如果能够执行到这里,说明没有执行"abc".wait();说明flag是false 没有包子,生产包子 如果没有包子,则生产包子,唤醒吃货来吃包子。 */ //给包子的成员变量赋值 baoZi.pi = "白面"; baoZi.xian = "韭菜大葱"; System.out.println("生产者已经生产完毕包子"); //上述代码已经生产完包子了,修改标记 baoZi.flag = true; //唤醒吃货来吃包子。 "abc".notify(); } } } }
消费包子的任务类:
package com.itheima.sh.pro_con_04; /* 消费包子的任务类: 消费者线程(吃货):吃包子之前先判断有没有包子,如果没有包子则等待;如果有包子, 则吃包子(消费),吃完之后唤醒生产者线程生产包子 */ public class ChiHuo implements Runnable {//t2 //定义成员变量接收包子对象 BaoZi baoZi; public ChiHuo(BaoZi baoZi) { this.baoZi = baoZi; } @Override public void run() { //模拟无限吃包子使用死循环 while (true){ //为了安全加同步 synchronized ("abc"){//t2 //吃包子之前先判断有没有包子,如果没有包子则等待; //没有包子标记flag是false if(!baoZi.flag){ try { //如果没有包子则等待; "abc".wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果能够执行到这里,说明没有执行 "abc".wait(); 说明flag是true //表示有包子 //如果有包子,则吃包子(消费),吃完之后唤醒生产者线程生产包子 System.out.println("吃货吃了一个"+baoZi.pi+"皮,馅是"+baoZi.xian+"的包子"); //打印完毕之后包子被吃没了 baoZi.pi = null; baoZi.xian = null; //修改标记 包子吃没了,标记变为false baoZi.flag = false; //吃完之后唤醒生产者线程生产包子 "abc".notify(); } } } }
测试类
package com.itheima.sh.pro_con_04;
/*
测试类
*/
public class Test01 {
public static void main(String[] args) {
//1.创建包子类对象
BaoZi baoZi = new BaoZi();
//2.创建生产者任务对象
ProduceBaoZi pb = new ProduceBaoZi(baoZi);
//3.创建生产者线程
Thread t1 = new Thread(pb);
//4.启动生产者线程
t1.start();
//5.创建消费者任务对象
ChiHuo ch = new ChiHuo(baoZi);
//6.创建消费者线程
Thread t2 = new Thread(ch);
//7.启动消费者线程
t2.start();
}
}
图解:
5.定时器Timer(了解)
-
介绍: 定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件 事情。定时器类是java.util.Timer类
-
构造方法:
Timer() 创建一个新计时器。
-
方法:
1.void schedule(TimerTask task, long delay) 安排在指定延迟后执行指定的任务。 参数: task:属于TimerTask类,由 Timer 安排为一次执行或重复执行的任务的抽象类。父接口是 Runnable,该类实现了run方法,该方法也是抽象的: abstract void run() 此计时器任务要执行的操作。 我们需要自定义类继承该抽象类并重写run方法并书写定时器需要执行的任务 delay:表示过多少毫秒定时器开始执行任务task
代码演示:
/* 1.void schedule(TimerTask task, long delay) 安排在指定延迟后执行指定的任务。 参数: task:属于TimerTask类,由 Timer 安排为一次执行或重复执行的任务的抽象类。父接口是 Runnable,该类实现了run方法,该方法也是抽象的: abstract void run() 此计时器任务要执行的操作。 我们需要自定义类继承该抽象类并重写run方法并书写定时器需要执行的任务 delay:表示过多少毫秒定时器开始执行任务task */ private static void method_1() { //1.创建定时器类的对象Timer() 创建一个新计时器。 Timer t = new Timer(); //2.使用定时器类的对象调用方法执行任务 t.schedule(new TimerTask(){ @Override public void run() { System.out.println("起床了"); } },3000); }
2.void schedule(TimerTask task, long delay, long period) 安排指定的任务从指定的延迟后开始进行 重复的固定延迟执行。 参数: task:任务类 delay:过多场时间执行任务 period:每隔多场时间执行一次任务
代码演示:
/* void schedule(TimerTask task, long delay, long period) 安排指定的任务从指定的延迟后开始进行 重复的固定延迟执行。 参数: task:任务类 delay:过多场时间执行任务 period:每隔多场时间执行一次任务 */ private static void method_2() { //1.创建定时器类的对象Timer() 创建一个新计时器。 Timer t = new Timer(); //2.使用定时器类的对象调用方法执行任务 t.schedule(new TimerTask(){ @Override public void run() { System.out.println("起床了"); } },3000,1000);//3000表示3s之后自行任务 1000 表示以后每个1s执行一次 }
3.void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务。 参数: task:定时器执行的任务 time:指定的具体时间,时间一到就执行任务
代码演示:
/* void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务。 参数: task:定时器执行的任务 time:指定的具体时间,时间一到就执行任务 */ private static void method_3() throws ParseException { //1.创建定时器类的对象Timer() 创建一个新计时器。 Timer t = new Timer(); //2.使用定时器类的对象调用方法执行任务 //定义字符串 String s = "2020-05-25 14:28:00"; //创建日期格式化解析类对象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将上述字符串解析为Date Date date = sdf.parse(s); t.schedule(new TimerTask() { @Override public void run() { System.out.println("起床了"); } },date); }
4. void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。 参数: task:任务对象 firstTime:指定日期,日期时间一到就执行任务 period:每隔一段时间执行一次任务
代码演示:
/* void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。 参数: task:任务对象 firstTime:指定日期,日期时间一到就执行任务 period:每隔一段时间执行一次任务 */ private static void method_4() throws ParseException { //1.创建定时器类的对象Timer() 创建一个新计时器。 Timer t = new Timer(); //2.使用定时器类的对象调用方法执行任务 //定义字符串 String s = "2020-05-25 14:31:10"; //创建日期格式化解析类对象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //将上述字符串解析为Date Date date = sdf.parse(s); t.schedule(new TimerTask() { @Override public void run() { System.out.println("起床了"); } },date,1000);//1000表示每隔1s执行一次 }
6.Lambda(掌握)
lambda介绍
-
属于jdk8以后的新特性,为了简化代码开发的。代码写的变少了,但是完成的功能是一样的。write less do more
-
lambda引入
package com.itheima.sh.lambda_06; public class Test01 { public static void main(String[] args) { //使用匿名内部类方式完成多线程 /*Runnable r = new Runnable() { @Override public void run() { System.out.println("黑马程序员"); } }; //启动线程 new Thread(r).start();*/ //启动线程 /* new Thread(new Runnable() { @Override public void run() { System.out.println("黑马程序员"); } }).start();*/ //使用lambda简化匿名内部类方式 new Thread(()->{ System.out.println("黑马程序员");}).start(); } }
Lambda的格式
lambda由三部分组成:
-
(参数列表):一些参数,和之前的方法参数是一样的
-
->: 一个箭头,属于全新的语法,表示将小括号中的数据执向给后面的大括号
-
{}:一段代码,和我们之前学习的方法体一样
lambda由三个一组成。完成接口中的抽象方法。
举例:
Runnable接口中的抽象方法:run: public abstract void run(); 没有方法体,我们可以使用lambda完成方法
lambad代码演示:
package com.itheima.sh.lambda_06; import java.util.Arrays; import java.util.Comparator; public class Test02 { public static void main(String[] args) { //需求:使用Arrays数组工具类中的sort方法对自定义类型的数组的对象年龄进行降序排序 //1.创建数组存储Person对象 Person[] arr = {new Person("柳岩", 19), new Person("冰冰", 17), new Person("杨幂", 20)}; //2.使用数组工具类Arrays调用sort方法对上述Person对象的年龄降序排序 //static <T> void sort(T[] a, Comparator<? super T> c)根据指定比较器产生的顺序对指定对象数组进行排序。 //int compare(T o1, T o2); o1- o2 升序 o2 - o1 降序 //匿名内部类 /* Arrays.sort(arr, new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o2.age - o1.age; } });*/ //使用lambda完成 //sort方法第二个参数是一个Comparator接口,并且该接口中有一个抽象方法,此时我们可以使用lambda完成该抽象方法 //lambada:三个一:1)一些参数 2)一个箭头 3)一段代码 //int compare(T o1, T o2); Arrays.sort(arr,(Person o1,Person o2)->{return o2.age - o1.age;}); //输出 System.out.println(Arrays.toString(arr)); } }
小结:
1.lambda组成:一些参数 一个箭头 一段代码
2.使用lambda可以完成接口中的抽象方法
省略格式
格式:
(参数类型 参数名,参数类型 参数名,...)->{一段代码}
1.参数:
1)参数类型可以省略,无论多少个参数,类型都可以不写
2) 如果只有一个参数,小括号也可以省略
2.方法体中的内容,如果只有一行代码,无论是否有返回值,那么可以一起省略 : return 大括号 大括号中的分 号。方法体内容要省略都省略,要么都不能省略
package com.itheima.sh.lambda_06;
import java.util.Arrays;
import java.util.Comparator;
public class Test02 {
public static void main(String[] args) {
//需求:使用Arrays数组工具类中的sort方法对自定义类型的数组的对象年龄进行降序排序
//1.创建数组存储Person对象
Person[] arr = {new Person("柳岩", 19), new Person("冰冰", 17), new Person("杨幂", 20)};
//2.使用数组工具类Arrays调用sort方法对上述Person对象的年龄降序排序
//static <T> void sort(T[] a, Comparator<? super T> c)根据指定比较器产生的顺序对指定对象数组进行排序。
//int compare(T o1, T o2); o1- o2 升序 o2 - o1 降序
//匿名内部类
/* Arrays.sort(arr, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o2.age - o1.age;
}
});*/
//使用lambda完成
//sort方法第二个参数是一个Comparator接口,并且该接口中有一个抽象方法,此时我们可以使用lambda完成该抽象方法
//lambada:三个一:1)一些参数 2)一个箭头 3)一段代码
//int compare(T o1, T o2);
// Arrays.sort(arr,(Person o1,Person o2)->{return o2.age - o1.age;});
Arrays.sort(arr,(o1,o2)->o2.age - o1.age);
//输出
System.out.println(Arrays.toString(arr));
}
}
Lambda的前提条件
1.必须有函数式接口:只有一个抽象方法的接口,可以有默认方法,静态方法。其实我们使用lambda完成接口中的抽象方法
2.要求某个方法的形参位置的类型必须是函数式接口类型
举例:
//满足条件1
public interface Runnable {
public abstract void run();
}
//满足条件2 Thread类的构造方法的形参位置是Runnable接口类型
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
最后实现:
new Thread(()->{ System.out.println("黑马程序员");}).start();
Runnable target = ()->{ System.out.println("黑马程序员");};
补充:
匿名内部类和lambda区别:
1.匿名内部类可以是类,也可以是接口
lambda只能是一个接口
2.匿名内部类对父类或者父接口中的抽象方法个数没有要求
lambda要父接口中的抽象方法只能有一个