Java线程的6种状态(二)

132 阅读4分钟

一、线程的六种状态

1.1、New 新创建的

new表示线程被创建但尚未启动的状态

Thread thread=new Thread();

1.2、Runnable 可运行的

当线程调用start方法,线程就处于Runnable状态。

thread.start();

Java中Runnable状态对应操作系统线程状态:Running和Ready

Java中处于Runnable状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配CPU资源

1.3、阻塞状态

阻塞状态又包含三种状态:

blocked与waiting的区别:blocked是等待其他线程释放monitor锁,而waiting是等待某个条件

  1. Blocked 被阻塞的

    Runnable状态进入blocked状态只有一种可能,就是进入synchronized保护的代码时没有抢到monitor锁。(无论是进入synchonized代码块,还是synchronized方法,都是一样的)

    当处于blocked的线程抢到了monitor锁,就会从Blocked状态回到Runnable状态

  2. Waiting 等待

    有三种情况,会进入waiting状态:

    1. 没有设置timeout参数的Object.wait()方法
    2. 没有设置timeout参数的Thread.join()方法
    3. LockSupport.park()方法

    从waiting重新进入runnable状态

    1. LockSupport.unpark()
    2. join的线程运行结束或者被中断

    从waiting进入其他状态

    1. o.notifyAll()
    2. o.notify()

    因为唤醒waiting线程的线程如果调用notify或者notifyAll()。要求必须首先持有该monitor锁。所以处于waiting状态的线程被唤醒时拿不到该锁,就会进入block状态。拿到了锁了就进入runnable状态

  3. Timed_waiting 计时等待

    Timed Waiting与Waiting是相似的,Timed Waiting会等待超时。系统会自动唤醒,或者在超时时间前被唤醒信号唤醒

    1. 设置了时间参数的Thread.sleep(long millis)
    2. 设置了时间参数的Object.wait(long timeout)
    3. 设置了时间参数的Thread.join(long millis)
    4. 设置了时间参数的LockSupport.parkNanos(long nanos)、LockSupport.partUtil(long deadline)

    从Timed_waiting重新进入runnable状态

    1. LockSupport.unpark()

    从Timed_waiting重新进入其他状态

    1. o.notifyAll()
    2. o.notify()

    因为唤醒Timed_waiting线程的线程如果调用notify或者notifyAll()。要求必须首先持有该monitor锁。所以处于waiting状态的线程被唤醒时拿不到该锁,就会进入block状态。拿到了锁了就进入runnable状态

1.4、Terminated 被终止的
  1. run方法执行完毕,线程正常退出
  2. 出现一个没有捕获的异常,终止了run方法,最终导致以外终止

二、线程转换的注意点

  1. 线程的状态是需要按照箭头方向来走的,比如线程从new状态是不可以直接进入blocked状态的,它需要先经历Runnable状态
  2. 线程生命周期不可逆:一旦进入runnable状态就不能会到new状态。一旦被终止就不可能再有任何状态变化。所以一个线程只能一次NewTerminated状态,只有处于中间才可以相互转换

三、Java线程的状态的流程图扭转

Java并发编程1.drawio

四、wait、notify、notifyAll方法的使用注意事项

4.1、为什么wait必须在synchronized保护的同步代码中使用?
/**
 * Thrown to indicate that a thread has attempted to wait on an
 * object's monitor or to notify other threads waiting on an object's
 * monitor without owning the specified monitor.
 *
 * @author  unascribed
 * @see     java.lang.Object#notify()
 * @see     java.lang.Object#notifyAll()
 * @see     java.lang.Object#wait()
 * @see     java.lang.Object#wait(long)
 * @see     java.lang.Object#wait(long, int)
 * @since   JDK1.0
 */
public
class IllegalMonitorStateException extends RuntimeException {

其实意思就是说,也就是当前的线程不是此对象监视器的所有者。也就是要在当前线程锁定对象,才能用锁定的对象此行这些方法,需要用到synchronized ,锁定什么对象就用什么对象来执行 notify(), notifyAll(),wait(), wait(long), wait(long, int)操作,否则就会报IllegalMonitorStateException异常。

也就是在说,就是需要在调用wait()或者notify()之前,必须使用synchronized语义绑定住被wait/notify的对象。

所以运行下面这段代码是报错的

package com.strivelearn.concurrent.chapter02;
​
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
​
/**
 * @author strivelearn
 * @version BlockingQueue.java, 2022年12月24日
 */
public class BlockingQueue {
    Queue<String> buffer = new LinkedList<>();
​
    /**
     * 负责往buffer中添加数据
     * 添加完之后执行notify方法
     */
    public void give() {
        buffer.add(UUID.randomUUID().toString());
        notify();
    }
​
    /**
     * 负责检查整个buffer是否为空
     * 如果为空就进入等待,如果不为空就remove队列第一个数据
     * @return
     */
    public String take() {
        while (buffer.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return buffer.remove();
    }
​
    public static void main(String[] args) {
        BlockingQueue queue = new BlockingQueue();
        Thread producer = new Thread(queue::give);
        Thread consumer = new Thread(queue::take);
        consumer.start();
        producer.start();
​
    }
}

正确的写法:

package com.strivelearn.concurrent.chapter02;
​
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;
​
/**
 * @author strivelearn
 * @version BlockingQueue.java, 2022年12月24日
 */
public class BlockingQueueRight {
    Queue<String> buffer = new LinkedList<>();
​
    /**
     * 负责往buffer中添加数据
     * 添加完之后执行notify方法
     */
    public void give() {
        synchronized (this) {
            buffer.add(UUID.randomUUID().toString());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            notify();
        }
    }
​
    /**
     * 负责检查整个buffer是否为空
     * 如果为空就进入等待,如果不为空就remove队列第一个数据
     * @return
     */
    public String take() {
        synchronized (this) {
            while (buffer.isEmpty()) {
                try {
                    System.out.println("等待中...");
                    wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("等待结束");
            return buffer.remove();
        }
    }
​
    public static void main(String[] args) {
        BlockingQueueRight queue = new BlockingQueueRight();
        Thread producer = new Thread(queue::give);
        Thread consumer = new Thread(queue::take);
        consumer.start();
        producer.start();
​
    }
}
  1. 这样可以确保notify方法永远不会在buffer.isEmpty()、wait()之间被调用,提升程序的安全性
  2. 另外,wait方法会释放monitor锁,这也要求我们必须首先进入synchorized内持有这把锁
4.2、为什么wait、notify、notifyAll被定义在Object类中而sleep定义在Thread类中

因为Java中每个对象都有一把称为monitor监视器的锁,由于每个对象可以都上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait、notify、notifyAll 也都是锁级别的操作,它们的锁属于对象。所有把它们定义在Object类中最合适,因为Object类是所有对象的父类

如果把wait、notify、notifyAll方法定义在Thread类中,会带来很大的局限性。比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时wait方法定义在Thread类中,如何实现让一个线程持有多把锁?如何明确线程等待的是哪把锁?

我们让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程

4.3、wait、notify和sleep方法的异同
相同点:
  1. 它们都可以让线程阻塞
  2. 它们都可以响应interrupt中断,在等待的过程中如果收到中断信号,都可以进行响应,并抛出InterruptedException异常
不同点:
  1. wait方法必须在synchronized保护的代码中使用,而sleep方法并没有这个要求
  2. 在同步代码中执行sleep方法时,并不会释放monitor锁,但是执行wait方法时会主动释放monitor锁
  3. sleep方法中要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的wait方法而言,意味着永久等待,直到被中断或者被唤醒才能恢复,它并不会主动恢复
  4. wait/notify是object类的方法,而sleep是Thread类的方法

五、如何使用wait、notify、condition、blockingQueue实现生产者消费者模式

生产者消费者模式是程序设计中非常常见的一种设计模式,被广泛运用在解耦、消息队列等场景

Java并发编程-生产者消费者模式.drawio

什么时候阻塞线程需要被唤醒呢?
  1. 当消费者看到阻塞队列为空时,开始进入等待,这时生产者一旦往队列中放入数据,就会通知所有的消费者,唤醒阻塞的消费者线程
  2. 如果生产者发现队列已经满了,也会被阻塞,而一旦消费者获取数据之后就相当于队列空了一个位置,这时消费者就会通知所有正在阻塞的生产者进行生产
package com.strivelearn.concurrent.chapter02;
​
import java.util.LinkedList;
import java.util.UUID;
​
/**
 * @author strivelearn
 * @version MyBlockingQueue.java, 2022年12月24日
 */
public class MyBlockingQueue {
    private int                maxSize;
​
    private LinkedList<String> storage;
​
    public MyBlockingQueue(int size) {
        this.maxSize = size;
        this.storage = new LinkedList<>();
    }
​
    public synchronized void put() throws InterruptedException {
        // 当前仓储容量达到上限的时候,阻塞线程
        while (storage.size() == maxSize) {
            wait();
        }
        // 当未达到仓储容量上限时,生产数据
        storage.add(UUID.randomUUID().toString());
        // 唤醒阻塞的消费者线程(认为仓储容器为0的线程)开始消费
        notifyAll();
    }
​
    public synchronized void take() throws InterruptedException {
        // 当仓储容量为0时,阻塞消费者线程,等待生产者线程进行消费
        while (storage.size() == 0) {
            wait();
        }
        System.out.println("消费:" + storage.remove());
        // 唤醒阻塞的生产者线程(认为仓储容量满的线程)开始生产
        notifyAll();
    }
}