1 名词概念

  1. 并行:在同一时刻,有多个任务在多个CPU上同时执行。
  2. 并发:在同一时刻,有多个任务在单个CPU上交替执行。
  3. 进程:进程简单地说就是在多任务操作系统中,每个独立执行的程序,所以进程也就是“正在进行的程序”。(Windows系统中,我们可以在任务管理器中看到进程)
  4. 线程:线程是程序运行的基本执行单元。当操作系统执行一个程序时,会在系统中建立一个进程,该进程必须至少建立一个线程(这个线程被称为主线程)作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个线程。
  5. 多线程:
    1. 硬件角度:现代CPU能够同时处理多个线程任务
    2. 软件角度:一个进程,可以同时开启多个线程执行不同任务
      image.png

可以提高系统资源的利用率,及解决问题的效率。

2 线程的创建方式

java.lang.Thread 是线程类,可以用来给进程创建线程处理任务使用。要使用线程先介绍两个比较重要的方法:
- publicvoid run() : 线程执行任务的方法,是线程启动后第一个执行的方法
- publicvoid start() : 启动线程的方法,线程对象调用该方法后,Java虚拟机就会调用此线程的run方法。

线程的创建方式1:继承Thread方式

基本步骤:

  1. 创建一个类继承Thread类。
  2. 在类中重写run方法(线程执行的任务放在这里)
  3. 创建线程对象,调用线程的start方法开启线程。

代码参考:
需求 :
我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
此时主线程和启动的线程在并发执行,观察控制台打印的结果。

  1. public class MyThread01 {
  2. public static void main(String[] args) {
  3. // 3 创建线程对象,调用线程的start方法开启线程。
  4. Thread01 t1 = new Thread01();
  5. t1.start();
  6. //当主线程开启了t1线程后,不会等待线程t1执行完
  7. //继续执行后续代码
  8. for (int i = 0; i < 100; i++) {
  9. System.out.println("旺财:" + i);
  10. }
  11. }
  12. }
  13. //1 创建一个类继承Thread类。
  14. class Thread01 extends Thread {
  15. //2 在类中重写run方法(线程执行的任务放在这里)
  16. @Override
  17. public void run() {
  18. for (int i = 0; i < 100; i++) {
  19. System.out.println("小强:" + i);
  20. }
  21. }
  22. }

线程的创建方式2 : 实现Runable方式

实现步骤如下:

  1. 定义任务类实现Runnable,并重写run方法
  2. 创建任务对象
  3. 使用含有Runnable参数的构造方法,创建线程对象并指定任务。
  4. 调用线程的start方法,开启线程。

代码参考
需求 :
我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
此时主线程和启动的线程在并发执行,观察控制台打印的结果。

  1. public class MyThread02 {
  2. public static void main(String[] args) {
  3. //2 创建任务对象
  4. MyTask m1 = new MyTask();
  5. // 3 创建Thread类型的对象 , Thread类的构造方法需要接受一个Runnable实现类对象
  6. //public Thread(Runnable target) : 接受一个Runnable接口的实现类对象
  7. Thread t1 = new Thread(m1);
  8. //4 调用线程的start方法,开启线程
  9. t1.start();
  10. //使用Lambda直接实现Runnable方式
  11. Thread t2 = new Thread(()-> System.out.println("Lambda 实现Runnable "));
  12. t2.start();
  13. //主线程干活
  14. for (int i = 0; i < 100; i++) {
  15. System.out.println("旺财:" + i);
  16. }
  17. }
  18. }
  19. //1 定义任务类,实现Runnable,并重写run方法
  20. class MyTask implements Runnable {
  21. @Override
  22. public void run() {
  23. //子线程干活
  24. for (int i = 0; i < 100; i++) {
  25. System.out.println("小强:" + i);
  26. }
  27. }
  28. }
优点 缺点
实现Runnable 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能直接使用Thread类中的方法
继承Thread 编程比较简单,可以直接使用Thread类中的方法 可扩展性较差,
不能再继承其他的类

3 Thread常见方法

  1. public String getName():返回此线程的名称
  2. public void setName(String name):将此线程的名称更改为等于参数 name ,通过构造方法也可以设置线程名称
  3. public static Thread currentThread() :返回对当前正在执行的线程对象的引用
  4. public static void sleep(long time):让线程休眠指定的时间,单位为毫秒。 1s = 1000ms
  5. public void join() : 具备阻塞作用 , 等待这个线程死亡,才会执行其他线程
  6. public final void setPriority(int newPriority) 设置线程的优先级
  7. public final int getPriority() 获取线程的优先级

4 线程调度

线程有两种调度模型
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

Java使用的是抢占式调度模型

5 线程安全

概念:多个线程操作共享数据时出现的数据错乱现象,我们称为线程出现安全问题了。
原因:多个线程操作共享数据
解决方案:使用同步技术,让操作共享资源的代码互斥执行

1.同步代码块。
2.同步方法。
3.Lock锁机制。

卖票安全问题案例演示:
需求:
1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
2 在Ticket类中重写run()方法实现卖票,代码步骤如下
A:判断票数大于0,就卖票,并告知是哪个窗口卖的
B:票数要减1
C:卖光之后,线程停止
3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
A:创建Ticket类的对象
B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
C:启动线程

  1. package com.itheima.thread_safety.ticket_demo;
  2. /*
  3. 1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
  4. 2 在Ticket类中重写run()方法实现卖票,代码步骤如下
  5. A:判断票数大于0,就卖票,并告知是哪个窗口卖的
  6. B:票数要减1
  7. C:卖光之后,线程停止
  8. 3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
  9. A:创建Ticket类的对象
  10. B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
  11. C:启动线程
  12. */
  13. public class TicketDemo {
  14. public static void main(String[] args) {
  15. //卖票任务
  16. Ticket ticket = new Ticket();
  17. //让三个线程模拟三个窗口去卖票
  18. new Thread(ticket,"【窗口1】").start();
  19. new Thread(ticket,"【窗口2】").start();
  20. new Thread(ticket,"【窗口3】").start();
  21. }
  22. }
  23. class Ticket implements Runnable {
  24. //票
  25. private int ticketCount = 100;
  26. @Override
  27. public void run() {
  28. //获取当前线程的名字【窗口名】
  29. Thread thread = Thread.currentThread();
  30. String name = thread.getName();
  31. //模拟不断在卖票
  32. while (true) {
  33. if (ticketCount > 0) {
  34. System.out.println(name + "正在卖:" + ticketCount);
  35. ticketCount--;// 票少一张
  36. } else {
  37. System.out.println(name + "票卖完了");
  38. break;
  39. }
  40. //模拟每次卖票所需的时间
  41. try {
  42. Thread.sleep(50);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. }
  47. }
  48. }

执行结果,出现重票,漏票现象
【窗口1】正在卖:100
【窗口3】正在卖:100
【窗口2】正在卖:100
【窗口2】正在卖:97
【窗口1】正在卖:97
【窗口3】正在卖:97
【窗口1】正在卖:94
【窗口2】正在卖:94
【窗口3】正在卖:94

5.1 同步代码块

同步代码块就是可以将访问共享资源的代码包括起来,让其互斥执行的代码块。
语法格式:

  1. synchronized(锁对象) { //获取锁
  2. 多条语句操作共享数据的代码
  3. }//释放锁

注意:
锁对象可以是任意对象,锁对象可以是任意对象 , 但是多个线程必须使用同一把锁。
锁对象一次只能被一个线程所获取,线程间是互斥的。

默认情况锁是没有被线程占用的,只要有一个线程执行了同步代代码,就会占用锁,执行完后就可以释放锁。释放锁后其他线程就有机会获取锁资源执行了。

同步的好处和弊端
好处:解决了多线程的数据安全问题
弊端:会降低程序的运行效率

代码实践:
卖票案例,使用同步代码块进行优化

  1. public class TicketDemo {
  2. public static void main(String[] args) {
  3. //卖票任务
  4. Ticket ticket = new Ticket();
  5. //让三个线程模拟三个窗口去卖票
  6. new Thread(ticket,"【窗口1】").start();
  7. new Thread(ticket,"【窗口2】").start();
  8. new Thread(ticket,"【窗口3】").start();
  9. }
  10. }
  11. class Ticket implements Runnable {
  12. //票
  13. private int ticketCount = 100;
  14. Object lock = new Object();//任意对象
  15. @Override
  16. public void run() {
  17. //Object lock = new Object();//不行,每个线程执行都会去执行run方法,每个线程都会创建新的锁
  18. //获取当前线程的名字【窗口名】
  19. Thread thread = Thread.currentThread();
  20. String name = thread.getName();
  21. //模拟不断在卖票
  22. while (true) {
  23. synchronized (lock) {
  24. //synchronized ("锁") { //字符串常量,每个线程拿到的锁对象都是同一个
  25. if (ticketCount > 0) {
  26. System.out.println(name + "正在卖:" + ticketCount);
  27. ticketCount--;// 票少一张
  28. } else {
  29. System.out.println(name + "票卖完了");
  30. break;
  31. }
  32. }
  33. //模拟每次卖票所需的时间
  34. try {
  35. Thread.sleep(50);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41. }

5.3 同步方法

同步方法可以认为是约束范围更大的同步代码块,可以让整个方法实现同步。上一讲中的同步代码块类似与局部代码块,只能在局部区域对代码进行同步约束。

同步方法格式:

在修饰符位置上加上synchronized关键字

  1. 修饰符 synchronized 返回值类型 方法名(方法参数) {
  2. 互斥执行
  3. }

定义同步方法时,并不用手动给定锁对象

  1. 对于非static方法,同步锁就是this。
  2. 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。 Class类型的对象

同步方法和同步代码块的区别:

  1. 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
  2. 同步代码块可以指定锁对象,同步方法不能指定锁对象

卖票案例使用同步方法优化

  1. public class TicketDemo {
  2. public static void main(String[] args) {
  3. //卖票任务
  4. Ticket ticket = new Ticket();
  5. //让三个线程模拟三个窗口去卖票
  6. new Thread(ticket,"【窗口1】").start();
  7. new Thread(ticket,"【窗口2】").start();
  8. new Thread(ticket,"【窗口3】").start();
  9. /*
  10. 线程调用start方法启动后,会执行run方法
  11. run方法内部,会执行任务对象ticket的run方法
  12. ticket.run()
  13. */
  14. }
  15. }
  16. class Ticket implements Runnable {
  17. //票
  18. private static int ticketCount = 100;
  19. @Override
  20. public void run() {
  21. //获取当前线程的名字【窗口名】
  22. Thread thread = Thread.currentThread();
  23. String name = thread.getName();
  24. //模拟不断在卖票
  25. while (true) {
  26. /* if (this.sellTicket(name)){ //非静态方法
  27. break;
  28. }*/
  29. if (sellTicket2(name)){ //静态方法
  30. break;
  31. }
  32. //模拟每次卖票所需的时间
  33. try {
  34. Thread.sleep(50);
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }
  40. //难点:锁对象是 this
  41. private synchronized boolean sellTicket(String name) {
  42. if (ticketCount > 0) {
  43. System.out.println(name + "正在卖:" + ticketCount);
  44. ticketCount--;// 票少一张
  45. } else {
  46. System.out.println(name + "票卖完了");
  47. return true;
  48. }
  49. return false;
  50. }
  51. //静态方法锁对象:类名.class
  52. private static synchronized boolean sellTicket2(String name) {
  53. if (ticketCount > 0) {
  54. System.out.println(name + "正在卖:" + ticketCount);
  55. ticketCount--;// 票少一张
  56. } else {
  57. System.out.println(name + "票卖完了");
  58. return true;
  59. }
  60. return false;
  61. }
  62. }

5.4 Lock锁机制

JDK5后java.util.concurrent.locks.Lock比之前的synchronized实现同步方法,同步代码块,更具有灵活的代码结构及操作。
Lock锁是在JDK层面实现的同步,synchronized关键字实现的同步是从底层的JVM实现的。
image.png

使用方式:

  1. 使用子类ReentrantLock先实例化Lock对象,Lock lock = new ReentrentLock();
  2. 调用lock方法获取锁
  3. 调用unlock方法释放锁

语法格式:

  1. Lock l = Lock lock = new ReentrantLock();
  2. l.lock();
  3. try {
  4. //锁定访问资源的代码
  5. } finally {
  6. l.unlock();
  7. }

卖票案例使用Lock锁的优化

  1. public class TicketDemo {
  2. public static void main(String[] args) {
  3. //卖票任务
  4. Ticket ticket = new Ticket();
  5. //让三个线程模拟三个窗口去卖票
  6. new Thread(ticket, "【窗口1】").start();
  7. new Thread(ticket, "【窗口2】").start();
  8. new Thread(ticket, "【窗口3】").start();
  9. }
  10. }
  11. class Ticket implements Runnable {
  12. //票
  13. private int ticketCount = 100;
  14. //Lock锁
  15. private final Lock LOCK = new ReentrantLock();
  16. @Override
  17. public void run() {
  18. //获取当前线程的名字【窗口名】
  19. Thread thread = Thread.currentThread();
  20. String name = thread.getName();
  21. //模拟不断在卖票
  22. while (true) {
  23. LOCK.lock(); //锁定
  24. try {
  25. if (ticketCount > 0) {
  26. System.out.println(name + "正在卖:" + ticketCount);
  27. ticketCount--;// 票少一张
  28. } else {
  29. System.out.println(name + "票卖完了");
  30. break;
  31. }
  32. }finally {
  33. LOCK.unlock();//一定要保证能够执行
  34. }
  35. //模拟每次卖票所需的时间
  36. try {
  37. Thread.sleep(50);
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }
  43. }

6 死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

在多线程中如果出现锁的嵌套,出现锁资源的竞争,那就极大概率会出现死锁现象。
举例
image.png
图中是一对情侣,小明和小花。他们一起吃面,就上了一盘意大利面,一双筷子。我们把小明和小花都看做是线程。面看做是资源,筷子两根比喻为两个锁。筷子1 筷子2。
当小花同时拿到筷子1和筷子2时,能吃面。
当小明同时拿到筷子1和筷子2时,能吃面。
当小花拿到筷子1,小明拿到筷子2时,小花等着小明的筷子2,小明等着小花的筷子1。两个人相互等待,没得吃了,这就是死锁的状态。

用代码演示效果如下:

  1. public class DeadLockDemo {
  2. public static void main(String[] args) {
  3. //两个锁
  4. String 筷子A = "筷子A";
  5. String 筷子B = "筷子B";
  6. new Thread(() -> {
  7. //小明吃面
  8. while (true) {
  9. synchronized (筷子A) {
  10. System.out.println("小明拿到筷子A");
  11. //阻塞
  12. synchronized (筷子B) {
  13. System.out.println("小明拿到筷子B,开始吃面");
  14. }
  15. }
  16. }
  17. }).start();
  18. new Thread(()->{
  19. //小花吃面
  20. while (true) {
  21. synchronized (筷子B) {
  22. System.out.println("小花拿到筷子B");
  23. //阻塞
  24. synchronized (筷子A) {
  25. System.out.println("小花拿到筷子A,开始吃面");
  26. }
  27. }
  28. }
  29. }).start();
  30. }

7 线程的状态

java.lang.Thread.State这个枚举中给出了六种线程状态,
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

新建状态( NEW ):创建线程对象
就绪状态( RUNNABLE ):start方法调用
阻塞状态( BLOCKED ):无法获得锁对象
等待状态( WAITING ):wait方法
计时等待( TIMED_WAITING )wait(时间),sleep(时间)方法
结束状态( TERMINATED ):run方法运行结束,无论是正常还是非正常结束

各种状态之间的转化关系:
image.png

8 线程间通讯技术

线程间通讯就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。
等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法

  1. 等待方法

void wait() 让线程进入无限等待。
void wait(long timeout) 让线程进入计时等待
以上两个方法调用会导致当前线程释放掉锁资源。

  1. 唤醒方法

void notify() 随机唤醒在此对象监视器(锁对象)上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
以上两个方法调用不会导致当前线程释放掉锁资源。

注意:

  1. 等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中使用)。
  2. 等待和唤醒方法应该使用相同的锁对象调用。
  3. wait方法的调用会导致锁的释放

线程进入无限等待代码演示

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. new Thread(()->{
  4. synchronized ("锁A") {
  5. System.out.println("获取了锁A!模拟无限等待!");
  6. try {
  7. "锁A".wait();//进入无限等待,同时会释放锁。 代码会阻塞于此
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("被唤醒,拿到了锁A");
  12. }
  13. }).start();
  14. }
  15. }

线程进入无限等待后被唤醒

  1. public class Test2 {
  2. public static void main(String[] args) throws InterruptedException {
  3. final String LOCK = "锁";
  4. new Thread(()->{
  5. System.out.println("A线程开始执行");
  6. synchronized (LOCK) {
  7. System.out.println("A线程抢到锁,进入无限等待状态");
  8. try {
  9. LOCK.wait();//释放锁,代码进入阻塞
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println("A线程被唤醒抢到锁,继续执行");
  14. }
  15. }).start();
  16. new Thread(()->{
  17. try {
  18. Thread.sleep(2000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. System.out.println("B线程开始执行");
  23. synchronized (LOCK) {
  24. System.out.println("B线程拿到了锁!唤醒等待的线程A");
  25. LOCK.notify();// 唤醒在当前锁等待的线程,不会释放锁
  26. System.out.println("B线程执行完成!");
  27. }
  28. }).start();
  29. }
  30. }
  31. 执行结果:
  32. A线程开始执行
  33. A线程抢到锁,进入无限等待状态
  34. B线程开始执行
  35. B线程拿到了锁!唤醒等待的线程A
  36. B线程执行完成!
  37. A线程被唤醒抢到锁,继续执行

线程进入计时等待自动唤醒演示

  1. public class Test3 {
  2. public static void main(String[] args) {
  3. new Thread(()->{
  4. synchronized ("锁A") {
  5. System.out.println("获取了锁A!模拟计时等待!");
  6. try {
  7. "锁A".wait(3000);//进入计时等待,同时会释放锁。 代码会阻塞于此 3秒后主动唤醒,抢锁
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println("=========");
  12. try {
  13. Thread.sleep(3000);//不会释放锁,抱着锁睡觉
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("被唤醒,拿到了锁A");
  18. }
  19. }).start();
  20. }
  21. }

9 生产者消费者案例

使用等待唤醒机制实现生产者消费者案例

需求:厨子做汉堡,吃货吃汉堡
image.png 消费者:

  1. 判断桌子上是否有汉堡包,
  2. 如果没有就等待。
  3. 如果有就开吃
  4. 吃完之后,桌子上的汉堡包就没有了叫醒等待的生产者继续生产

生产者:

  1. 判断桌子上是否存放满10个如果有就等待,
  2. 如果没有继续才生产。
  3. 把汉堡包放在桌子上。
  4. 叫醒等待的消费者开吃。

测试类

  1. // 测试类
  2. public class Test {
  3. public static void main(String[] args) {
  4. new Thread(new Cooker()).start();//厨子线程
  5. new Thread(new Foodie()).start();//吃货线程
  6. }
  7. }

桌子

  1. import java.util.ArrayList;
  2. /*
  3. 桌子类
  4. */
  5. public class Desk {
  6. //共享资源
  7. public final static ArrayList<String> DISH = new ArrayList<>();
  8. }

生产者

  1. package com.itheima.waitnotify_demo2;
  2. /*
  3. 生产者步骤:
  4. 1,判断桌子上是否放满10个,如果放满是个停下来等吃货吃
  5. 2,把汉堡包放在桌子上
  6. 3,叫醒等待的消费者开吃
  7. 生产者 : 厨师
  8. */
  9. public class Cooker implements Runnable {
  10. @Override
  11. public void run() {
  12. int num = 0;
  13. while (true) {
  14. try {
  15. Thread.sleep(200);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. synchronized (Desk.DISH) {
  20. if (Desk.DISH.size()>=10) {
  21. //盘子已经满了,放不下了,等待
  22. System.out.println("盘子已经放满了!歇会儿!");
  23. try {
  24. Desk.DISH.wait();//等待,让吃货去吃
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. //做汉堡
  30. num++;
  31. String hamburg = "香辣鸡腿堡" + num;
  32. //将汉堡放到盘子中
  33. Desk.DISH.add(hamburg);
  34. System.out.println("盘子中还剩汉堡数量:"+Desk.DISH.size());
  35. Desk.DISH.notify();//唤醒对方去吃
  36. }
  37. }
  38. }
  39. }

消费者

  1. /*
  2. 消费者步骤:
  3. 1,判断桌子上是否有汉堡包。
  4. 2,如果没有就等待。
  5. 3,如果有就开吃
  6. 4,吃完之后,桌子上的汉堡包就没有了
  7. 叫醒等待的生产者继续生产
  8. 汉堡包的总数量减一
  9. 消费者 : 吃货
  10. */
  11. public class Foodie implements Runnable {
  12. @Override
  13. public void run() {
  14. while (true) {
  15. try {
  16. Thread.sleep(210);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. synchronized (Desk.DISH) {
  21. //1,判断桌子上是否有汉堡包。
  22. if (Desk.DISH.size() == 0) {
  23. //没有就等待。
  24. System.out.println("没有汉堡了!等待!...");
  25. try {
  26. Desk.DISH.wait();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. //有汉堡了
  32. String hamburg = Desk.DISH.remove(0);
  33. System.out.println("吃货品尝了一个汉堡:"+hamburg +", 盘子中还剩余:"+Desk.DISH.size());
  34. //叫厨子做汉堡
  35. Desk.DISH.notify();
  36. }
  37. }
  38. }
  39. }

10 线程池入门使用

  1. 线程使用存在的问题

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程销毁线程,会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。

  1. 线程池的认识

其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

使用线程池的流程如下:
1.创建线程池指定线程开启的数量
2.提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
3.线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
4.如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务。

  1. 线程池的好处

    1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建 , 就能立即执行。
    3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多内存 , 服务器死机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  2. 线程池API认识

java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。获取线程池对象我们可使用工具类java.util.concurrent.Executors的静态方:
**public static **ExecutorService newFixedThreadPool (**int **num) 指定线程池最大线程池数量获取线程池

线程池ExecutorService的相关方法:

  1. public <T> Future<T> submit(Callable<T> task) 提交执行Callable任务
  2. public Future<?> submit(Runnable task) 提交执行Runnable任务
  3. public void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
  4. 关闭线程池方法(一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)
  1. 线程池提交 Runnable任务

使用线程池模拟游泳教练教学生游泳。游泳馆(线程池)内有3名教练(线程),游泳馆招收了5名学员学习游泳(任务)。

实现步骤:
1.创建线程池指定3个线程
2.定义学员类实现Runnable,
3.创建学员对象给线程池
代码参考:

  1. package com.itheima.threadpool_demo;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. public class Test1 {
  5. public static void main(String[] args) {
  6. // 获取线程池
  7. ExecutorService threadPool = Executors.newFixedThreadPool(3);
  8. // 给线程池提交任务
  9. threadPool.submit(new Student("小花"));
  10. threadPool.submit(new Student("小明"));
  11. threadPool.submit(new Student("小黑"));
  12. threadPool.submit(new Student("小白"));
  13. threadPool.submit(new Student("大黄"));
  14. threadPool.submit(new Student("老王"));
  15. threadPool.submit(new Student("小四"));
  16. // 关闭线程池
  17. //threadPool.shutdown();
  18. //threadPool.shutdownNow();
  19. }
  20. }
  21. // 定义学生类(任务类)
  22. class Student implements Runnable {
  23. private String name;
  24. public Student(String name) {
  25. this.name = name;
  26. }
  27. @Override
  28. public void run() {
  29. //线程名字【教练名】
  30. String coachName = Thread.currentThread().getName();
  31. System.out.println(coachName + "教练,正在教" + name + "学习游泳!!");
  32. try {
  33. Thread.sleep(2000);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. System.out.println(coachName + "教练,完成对" + name + "的教学!!");
  38. }
  39. }
  1. 提交Callable任务

认识Callable接口

  1. @FunctionalInterface
  2. public interface Callable<V> {
  3. V call() throws Exception;
  4. }

对比Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Callable与Runnable的不同点:
1.Callable支持结果返回,Runnable不行
2.Callable可以 抛出异常,Runnable不行

使用步骤:
1.创建线程池
2.定义Callable任务
3.创建Callable任务,提交任务给线程池
4.获取执行结果

<T> Future<T>  submit(Callable<T> task)  提交Callable任务方法 返回值类型Future的作用就是为了获取任务执行的结果。

返回值类型Future是一个接口,里面存在一个get方法用来获取值。

比如使用线程池计算 从0~n的和,并将结果返回。

package com.itheima.threadpool_demo;

import java.util.concurrent.*;

public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        // 2 为线程池提交任务
//        threadPool.submit(Callable任务)
        Future<Integer> f1 = threadPool.submit(new CalculatorTask(100));

        Integer sum = f1.get();//获取的结果就是call方法返回的结果
        System.out.println("sum = " + sum);
    }
}

// 定义Callable任务
class CalculatorTask implements Callable<Integer> {
    private int n;

    public CalculatorTask(int n) {//注入
        this.n = n;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}