一. 线程生命周期(常见面试题)

1.1生命周期介绍

线程的声明周期就是线程从创建到线程销毁的过程。
通过学习线程生命周期也可以清楚的知道线程中有哪些状态以及哪些可用方法

1.2 生命周期图

17_线程生命周期_线程通信 - 图1

线程生命周期从新建到死亡共包含五种状态:
新建状态、就绪状态、运行状态、阻塞状态、死亡状态

1.2.1 新建状态

当实例化Thread类对象后, 线程就处于新建状态. 这时线程并没有执行
17_线程生命周期_线程通信 - 图2

1.2.2 就绪状态

只要在代码中启动了线程,就会从新建状态,变为就绪状态
17_线程生命周期_线程通信 - 图3
就绪状态属于一种临时状态。处于就绪状态的线程会去抢占CPU,只要抢占成功就会切换到运行状态
线程抢占CPU的场景和超市中大妈早上去抢菜的场景是一样的。
https://v.qq.com/x/page/d3212manm00.html

1.2.3 运行状态

运行状态就是开始执行线程的功能。具体点就是执行run方法
17_线程生命周期_线程通信 - 图4
在代码执行过程中分为三种情况:
1. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态
2. 如果调用yield()方法或失去CPU执行权限会切换为就绪状态
3. 如果run方法成功执行完成,或出现问题或被停止(中断)会切换为死亡状态

1.2.4阻塞状态

阻塞状态时,线程停止执行。让出CPU资源。
处于阻塞状态的线程需要根据情况进行判断是否转换为就绪状态:
1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态
2. 如果是因为wait()变为阻塞状态,需要调用notify()或notifyAll()手动切换为就绪状态
3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态
4. (已过时)如果是因为suspend()暂停的线程,需要通过resume()激活线程

1.2.5死亡状态

死亡状态即线程执行结束

二. stop 和 interrupt

2.1 stop介绍(已过时)

stop()可以停止一个线程。让线程处于死亡状态, stop()已经过时

2.2 stop弃用的原因

stop()本身就是不安全的,强制停止一个线程,可能导致破坏线程内容。导致结果的异常。但是程序还没有任务异常。应该使用interrupt停止一个长时间阻塞的线程。
stop()太绝对了,什么情况下都能停,并没有任何的提示信息, 可能导致混乱结果。

2.3 stop代码演示

2.3.1停止并结束阻塞状态的线程

  1. public class TestA {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread thread = new Thread(() -> {
  4. for (int i = 0; i < 10; i++) {
  5. try {
  6. System.out.println("run方法执行第"+i+"次");
  7. Thread.sleep(500);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. });
  13. thread.start();
  14. Thread.sleep(1000);
  15. thread.stop();
  16. }
  17. }

17_线程生命周期_线程通信 - 图5

2.3.2停止并结束运行状态的线程

  1. public class TestB {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread thread = new Thread(() -> {
  4. while (true){
  5. System.out.println("run方法正在运行");
  6. }
  7. });
  8. thread.start();
  9. Thread.sleep(500);
  10. thread.stop();
  11. }
  12. }

17_线程生命周期_线程通信 - 图6

2.4 interrupt介绍

interrupt()作为stop()的替代方法。可以实现中断线程, 并结束该线程
interrupt()只能中断当前线程状态带有InterruptedException异常的线程, 当程序执行过程中,如果被强制中断会出现Interrupted异常
interrupt() 负责打断处于阻塞状态的线程。防止出现死锁或长时间wait(等待)

2.5 interrupt 代码演示

2.5.1停止并结束阻塞状态的线程

  1. public class TestC {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread thread = new Thread(() -> {
  4. for (int i = 0; i < 10; i++) {
  5. try {
  6. System.out.println("run方法执行第"+i+"次");
  7. Thread.sleep(500);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. });
  13. thread.start();
  14. Thread.sleep(1000);
  15. thread.interrupt();
  16. }
  17. }

17_线程生命周期_线程通信 - 图7
17_线程生命周期_线程通信 - 图8

2.5.2停止并结束阻塞状态的线程

运行状态没有抛出InterruptException, 所以不能中断
子线程中没有可以抛出异常的sleep,所以不能中断,run中没有异常

public class TestD {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true){
                System.out.println("run方法正在运行");
            }
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();

    }
}

17_线程生命周期_线程通信 - 图9
17_线程生命周期_线程通信 - 图10

三. suspend和resume

3.1 suspend介绍(已过时)

suspend()可以挂起、暂停线程,让线程处于阻塞状态, 是一个实例方法,
挂起时, 不会释放锁
17_线程生命周期_线程通信 - 图11

3.2 resume介绍(已过时)

resume()可以让suspend()的线程唤醒, 变成运行状态, 已过时
17_线程生命周期_线程通信 - 图12

3.3 代码实现: suspend()和resume()

**

public class TestE {
    public static void main(String[] args) throws InterruptedException {
        //新建状态
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                try {
                    System.out.println("run方法执行第"+i+"次");
                    Thread.sleep(200);//子线程阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("子线程就绪");
        thread.start();
        Thread.sleep(2000);
        System.out.println("子线程将被挂起");
        thread.suspend();//子线程线程挂起
        Thread.sleep(10000);
        System.out.println("子线程将被唤醒");
        thread.resume();//子线程线程唤醒
    }
}

3.4 被弃用的原因

死锁:当一个线程持有锁,因为各种原因,不释放锁。其他线程又想拿到这个锁,但拿不到,这时这个锁就称为死锁
但由于这两个已经是过时方法, 容易产生死锁, 目前已经很少使用了
官方解释:

已弃用
此方法已被弃用,因为它本质上容易死锁。 如果目标线程在挂起时保护关键系统资源的监视器上持有锁,则在目标线程恢复之前,没有线程可以访问该资源。 如果将恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。 这种死锁通常表现为“冻结”进程

解释说明:
如果线程A持有锁(假设锁叫做L),对线程A做了suspend,让线程A挂起。在线程A没有resume之前,线程B无论如何也是无法获得锁的,也就出现了死锁。因为suspend时没有释放锁

3.5 代码演示: 死锁

public class TestF {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        synchronized (TestF.class){
            for (;;){
                System.out.println(Thread.currentThread().getName());
            }
        }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (TestF.class){
                for (;;){
                    System.out.println(Thread.currentThread().getName());
                }
            }
        });

        try {
            thread.start();//子线程1就绪状态
            Thread.sleep(1000);//主线程阻塞1秒
            thread.suspend();//线程一阻塞
            Thread.sleep(2000);//主线程阻塞2秒
            thread2.start();//子线程2就绪状态
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

出现死锁,程序不会停止,下图有误,请注意
17_线程生命周期_线程通信 - 图13

四. 线程通信

4.1 什么是线程通信

需要多个线程配合完成一件事情, 如何让多个线程能够合理的切换就是线程通信需要考虑的问题. 重点在于配合

4.2 生产者消费者模式

生产者和消费者模式为最经典的线程通信案例
需求:
1. 商品具有库存数
2. 如果商品的库存满了, 可以让用户进行购买/消费这个商品
3. 如果库存为0, 商品需要进货, 补充库存, 库存满了之后才能继续消费
实现思路:
1. 提供成员变量, 代表库存数
2. 如果库存为10, 执行一个线程减少该变量的值
3. 如果库存为0. 执行一个线程增加该变量的值

/**生产者和消费者模式为最经典的线程通信案例
        需求:
        1. 商品具有库存数
        2. 如果商品的库存满了, 可以让用户进行购买/消费这个商品
        3. 如果库存为0, 商品需要进货, 补充库存, 库存满了之后才能继续消费
        实现思路:
        1. 提供成员变量, 代表库存数
        2. 如果库存为10, 执行一个线程减少该变量的值
        3. 如果库存为0. 执行一个线程增加该变量的值*/
public class Product {
    static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Producer producer = new Product().new Producer();
        Consumer consumer = new Product().new Consumer();
        producer.start();
        consumer.start();
    }
    class Producer extends Thread{  //生产者线程
        @Override
        public void run() {
            while (true){
                synchronized (Product.class){
                    System.out.println("生产者----争抢锁");
                    if (count==0){
                        for (int i = 0; i < 10; i++) {
                            try {
                                count++;
                                System.out.println("生产者生产了:"+count+"个");
                                Thread.sleep(500);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    class Consumer extends Thread{//消费者线程
        @Override
        public void run() {
            while (true){
                synchronized (Product.class){
                    System.out.println("消费者----争抢锁");
                    if (count==10){
                        for (int i = 0; i< 10 ; i++) {
                            try {
                                System.out.println("消费者消费了第:"+count+"个");
                                count--;
                                Thread.sleep(500);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
}

该方式的缺点:
因为两个线程一直处于运行状态和就绪状态
两个线程一直在阻塞和就绪状态之间切换。 抢到锁的就可以执行,而没有抢到锁的线程一直在抢锁, 所以对系统性能损耗较大, 不推荐使用这种方式

4.3 线程通信的几种方式(面试题)

wait()和notify() | notifyAll() 方式
join()方式
Condition 方式
PipedInputStream和PipedOutputStrem 管道
今天我们先研究前两种, 后两种方式在后面讲解

五. wait()和notify | notifyAll

5.1 介绍

wait() 是Object中的方法。调用wait()后会让线程从运行状态变为阻塞状态。
在Object类中提供了wait的方法重载
17_线程生命周期_线程通信 - 图14
wait()虽然是让线程变为阻塞,但底层是阻塞的同时会释放锁。所以wait必须要求被等待的线程持有锁,调用wait()后会把锁释放,其他线程竞争获取锁。当其他线程竞争获取到锁以后,如果达到某个条件后可以通过notify唤醒,如果有多个wait的线程,系统判断唤醒其中一个。如果多个处于wait的线程可以使用notifyAll全部唤醒。唤醒后线程处于就绪状态。需要注意的是:一个线程唤醒其他线程时,要求当前线程必须持有锁
最简易结论:
1. 使用wait()和notify() | notifyAll()要求必须有锁。
2. wait()、notify()、notifyAll() 都是放入锁的代码中。
3. wait()和notify() | notifyAll() 配合使用。

5.2 代码演示

这种实现方式比while轮询方式优点在于,当执行完自己任务后,当前线程处于阻塞状态,在没有notify之前,一直是阻塞状态,不会去竞争锁。
当其他线程唤醒了wait线程时会立即让自己wait,整体没有过多性能损耗

public class Production1 {
    int totalCount = 0;
    String str = "库存锁";
    /* 生产者 */
    public class Producer extends Thread{
        @Override
        public void run() {
            synchronized (str){
                for (;;) {
                    totalCount++;
                    System.out.println("生产了"+totalCount+"个产品");
                    try {
                        Thread.sleep(500);
                        //库存为10
                        if(totalCount==10){
                            try {
                                //唤醒一个线程, Producer线程并没有等待,所以会继续执行
                                str.notify();
                                //Producer线程等待, 释放锁. 线程等待后续代码就不会继续执行,直到被唤醒
                                str.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    /* 消费者 */
    public class Consumer extends Thread{
        @Override
        public void run() {
            synchronized (str){
                //存库为0, Consumer线程等待(不等待, Consumer线程先获得到锁会出现负消费)
                if(totalCount==0){
                    try {
                        str.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (;;) {
                    totalCount--;
                    System.out.println("消费了"+(totalCount+1)+"个产品");
                    try {
                        Thread.sleep(500);
                        //库存为10, 线程1等待, 释放锁
                        System.out.println(totalCount);
                        if(totalCount==0){
                            try {
            //唤醒一个线程, Consumer线程并没有等待,所以会继续执行
                                str.notify();
            //Consumer线程等待, 释放锁. 线程等待后续代码就不会继续执行,直到被唤醒
                                str.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        Production1 production1 = new Production1();
        Consumer consumer = production1.new Consumer();
        consumer.start();
        Producer producer = production1.new Producer();
        producer.start();
    }
}

5.3 wait()和sleep()区别(常见面试题)

  1. 所属类不同
    wait() 是Object中方法
    sleep()是Thread的方法。
    2. 唤醒机制不同。
    wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()。
    sleep是到指定时间自动唤醒。
    3. 锁机制不同。
    wait()释放锁
    sleep()只是让线程休眠,不会释放锁。
    4. 使用位置不同。
    wait()必须持有对象锁
    sleep()可以使用在任意地方。
    5. 方法类型不同。
    wait()是实例方法
    sleep()是静态方法。

    六. join()

    6.1 介绍

    join() 把线程加入到另一个线程中。在哪个线程内调用join(),就会把对应的线程加入到当前线程中
    join()后,会让当前线程挂起,变成阻塞状态,直到新加入的线程执行完成。当前线程才会继续执行

    6.2 代码演示


6.3 需求

有两个子线程 t1, t2. t1, t2 都进行10次for循环. 当t2线程循环到第三次时, 执行t1的循环, t1循环结束后, 再继续执行t2的循环

public class Demo06 {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println("t1: "+i);
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println("t2: "+i);
                        Thread.sleep(1000);
                        if(i==3){
                            t1.start();
                            t1.join();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t2.start();
    }
}