线程状态及转换

2024-06-04  
0条评论   252浏览

线程状态

(1)线程状态

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法

  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”

    • 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)

    • 就绪状态的线程在获得CPU时间片后变为运行中状态(running)

  • 阻塞(BLOCKED):表示线程阻塞于锁

  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回

  • 终止(TERMINATED):表示该线程已经执行完毕

(2)runnable 和 callable 区别

Runnable 接口run方法没有返回值

Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

线程中断机制

什么是中断机制

首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运,所以,Thread.stop,Thread.suspend,Thread.resume都已经被废弃了

其次,在Java中没 有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制----中断,也即中断标识协商机制

  • 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自行实现。若要中断一个线程,你需要手动调用该线程interrupt方法,该方法也仅仅是将该线程对象的中断标识设置为true,接着你需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟应该做什么需要你自己写代码实现。
  • 每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设置为true;可以在别的线程中调用,也可以在自己的线程中调用。

中断的相关API方法之三大方法说明

interrupt() 方法:
  • 这是Thread类的一个方法,用于请求中断线程。
  • 调用一个线程的interrupt()方法只是设置该线程的中断状态为true,并不会立即停止线程的执行。
  • 如果线程在调用interrupt()方法时正在等待、睡眠或执行某些阻塞操作(如Object.wait(), Thread.sleep(), Thread.join(), BlockingQueue.take(), Selector.select(), LockSupport.park()等),那么它将收到一个InterruptedException
isInterrupted() 方法:
  • 这也是Thread类的一个方法,用于检查线程的中断状态。
  • 如果线程的中断状态为true,则返回true
  • 需要注意的是,如果线程在调用isInterrupted()后收到中断请求(即interrupt()被调用),isInterrupted()将返回true,但中断状态不会被清除。这意味着你需要连续检查中断状态或捕获InterruptedException
interrupted() 方法:
  • 这是Thread类的一个静态方法,用于检查当前线程的中断状态,并清除中断状态。
  • 如果当前线程的中断状态为true,则返回true,并清除中断状态。
  • 如果线程在调用interrupted()后收到中断请求,再次调用interrupted()将返回false,因为中断状态已经被清除。

对于静态方法Thread.interrupted()和实例方法isInterrupted()区别在于:

  • 静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true)
  • 实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)
处理InterruptedException
  • 当线程在等待、睡眠或执行某些阻塞操作时收到中断请求,它将抛出InterruptedException
  • 你应该总是捕获这个异常,并适当地处理它。通常的做法是清除中断状态(通过调用Thread.currentThread().interrupt()),然后要么重新抛出异常(如果当前方法也是声明抛出InterruptedException的),要么设置一个中断标志并返回。
响应中断:
  • 线程应该定期检查中断状态(通过调用isInterrupted()或捕获InterruptedException),并在收到中断请求时采取适当的行动。
  • 响应中断的具体方式取决于线程的任务和上下文。例如,一个正在执行长时间计算的线程可能在收到中断请求时停止计算并返回结果,而一个正在等待I/O操作的线程可能在收到中断请求时取消I/O操作并抛出异常。

如何停止中断运行中的线程

通过一个volatile变量实现

/**
 * 
 * 使用volatile修饰一个标识符来决定是否结束线程
 */
public class InterruptDemo {
    static volatile boolean isStop = false; //volatile表示的变量具有可见性

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println(Thread.currentThread().getName() + " isStop的值被改为true,t1程序停止");
                    break;
                }
                System.out.println("-----------hello volatile");
            }
        }, "t1").start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            isStop = true;
        }, "t2").start();

    }
}
/**
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * t1 isStop的值被改为true,t1程序停止
 */

通过AutomicBoolean

/**
 * 
 * 使用AtomicBoolean
 */
public class InterruptDemo {
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);


    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println(Thread.currentThread().getName() + " atomicBoolean的值被改为true,t1程序停止");
                    break;
                }
                System.out.println("-----------hello atomicBoolean");
            }
        }, "t1").start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            atomicBoolean.set(true);
        }, "t2").start();

    }
}

/**
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * t1 atomicBoolean的值被改为true,t1程序停止
 */

通过Thread类自带的中断API实例方法实现

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。

/**
 * 
 * 使用interrupt() 和isInterrupted()组合使用来中断某个线程
 */
public class InterruptDemo {
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);


    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " isInterrupted()的值被改为true,t1程序停止");
                    break;
                }
                System.out.println("-----------hello isInterrupted()");
            }
        }, "t1");
        t1.start();

        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2向t1放出协商,将t1中的中断标识位设为true,希望t1停下来
        new Thread(() -> t1.interrupt(), "t2").start();

        //当然,也可以t1自行设置
        t1.interrupt();

    }
}
/**
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * t1 isInterrupted()的值被改为true,t1程序停止
 */

Thread线程中断原理

  • 当前线程的中断标识为true,是不是线程就立刻停止?

答案是不立刻停止,具体来说,当对一个线程,调用interrupt时:

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响。
  • 如果线程处于阻塞状态(例如sleep,wait,join状态等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(interrupt状态也将被清除),并抛出一个InterruptedException异常。
/**
 * 
 * 执行interrupt方法将t1标志位设置为true后,t1没有中断,仍然完成了任务后再结束
 * 在2000毫秒后,t1已经结束称为不活动线程,设置状态为没有任何影响
 */
public class InterruptDemo2 {
    public static void main(String[] args) {
        //实例方法interrupt()仅仅是设置线程的中断状态位为true,不会停止线程
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 300; i++) {
                System.out.println("------: " + i);
            }
            /**
             * ------: 298
             * ------: 299
             * ------: 300
             * t1线程调用interrupt()后的中断标志位02:true
             */
            System.out.println("t1线程调用interrupt()后的中断标志位02:" + Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();

        System.out.println("t1线程默认的中断标志位:" + t1.isInterrupted());//false

        try {
            TimeUnit.MILLISECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t1.interrupt();//true
        /**
         * ------: 251
         * ------: 252
         * ------: 253
         * t1线程调用interrupt()后的中断标志位01:true
         */
        System.out.println("t1线程调用interrupt()后的中断标志位01:" + t1.isInterrupted());//true

        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //2000毫秒后,t1线程已经不活动了,不会产生任何影响
        System.out.println("t1线程调用interrupt()后的中断标志位03:" + t1.isInterrupted());//false

    }
}
/**
 * 
 *1. 中断标志位默认为false
 * 2.t2对t1发出中断协商  t1.interrupt();
 * 3. 中断标志位为true: 正常情况 程序停止
 *     中断标志位为true  异常情况,.InterruptedException ,将会把中断状态清楚,中断标志位为false
 * 4。需要在catch块中,再次调用interrupt()方法将中断标志位设置为false;
 */
public class InterruptDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 中断标志位为:" + Thread.currentThread().isInterrupted() + " 程序停止");
                    break;
                }
                //sleep方法抛出InterruptedException后,中断标识也被清空置为false,如果没有在
                //catch方法中调用interrupt方法再次将中断标识置为true,这将导致无限循环了
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    //Thread.currentThread().interrupt(); 
                    e.printStackTrace();
                }
                System.out.println("-------------hello InterruptDemo3");

            }
        }, "t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            t1.interrupt();
        }, "t2").start();
    }
}

线程等待唤醒机制

三种让线程等待和唤醒的方法

方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程

方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类中的wait和notify方法实现线程等待和唤醒

  • wait和notify方法必须要在同步代码块或者方法里面,且成对出现使用
  • 先wait再notify才ok
/**
 * @author Guanghao Wei
 * @create 2023-04-11 12:13
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        Object objectLock = new Object();
        /**
         * t1	 -----------come in
         * t2	 -----------发出通知
         * t1	 -------被唤醒
         */
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + "\t -----------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t -------被唤醒");
            }
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
            }

        }, "t2").start();
    }
}

Condition接口中的await和signal方法实现线程的等待和唤醒

  • Condition中的线程等待和唤醒方法,需要先获取锁
  • 一定要先await后signal,不要反了
/**
 * @author Guanghao Wei
 * @create 2023-04-11 12:13
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        /**
         * t1	 -----------come in
         * t2	 -----------发出通知
         * t1	 -----------被唤醒
         */
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t -----------come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t -----------被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
            } finally {
                lock.unlock();
            }
        }, "t2").start();

    }
}

上述两个对象Object和Condition使用的限制条件

  • 线程需要先获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,其中park()和unpack()而作用分别是阻塞线程和解除阻塞线程

用于阻塞和唤醒线程的功能

一、定义和用途

定义:LockSupport是Java并发包(java.util.concurrent.locks)中的一个工具类,专门用于实现线程的阻塞和唤醒操作。

用途:LockSupport是创建锁和其他同步组件的基础工具,常用于构建复杂的线程同步和协作模式。

二、主要方法和原理

主要方法:

  • public static void park();:阻塞当前线程,直到它被其他线程通过unpark方法唤醒、线程被中断,或者已经过了一个不可预知的时间。
  • public static void park(Object blocker);:带有阻塞对象的版本,主要用于线程监控和诊断。
  • public static void parkNanos(long nanos);public static void parkNanos(Object blocker, long nanos);:阻塞当前线程,但最多只阻塞指定的纳秒数。
  • public static void parkUntil(long deadline);public static void parkUntil(Object blocker, long deadline);:阻塞当前线程,直到指定的绝对时间。
  • public static void unpark(Thread thread);:唤醒处于阻塞状态的指定线程。

原理:

  • LockSupport使用类似信号量的机制,为每个线程准备一个许可(permit)。当线程调用park方法时,如果许可可用则park方法立即返回,否则线程将被阻塞直到许可可用。
  • unpark方法用于使一个许可变为可用,但许可不能累加,永远只有一个。这意味着即使unpark发生在park之前,它也可以使下一个park操作立即返回。

三、特点

灵活性:与传统的使用synchronized关键字或Object类的wait()notify()方法不同,LockSupport提供了更灵活的线程阻塞和唤醒控制。

精确性:LockSupport提供了更精确的线程阻塞和唤醒控制,使得开发者能够更精确地控制线程的行为。

可移植性和可维护性:LockSupport具有更好的可移植性和可维护性,可以在不同的Java平台上保持一致的行为。

四、使用注意事项

  • 在调用park之前,应确保当前线程没有持有任何可能导致死锁的锁。
  • Object.wait()Object.notify()Object.notifyAll()相比,LockSupport提供了一种更灵活的线程挂起和恢复方法,但它不会释放任何锁资源。
  • 某个线程可以先被unpark(这时,该线程就获得了一个许可),然后这个线程调用LockSupport.park()时,如果发现有许可可用,则使用此许可而不会阻塞。

LockSupport在Java并发编程中是一个非常重要的工具,它提供了灵活且精确的线程阻塞和唤醒控制,使得开发者能够更好地管理线程的行为和状态。

LockSupport类中的park等待和unpark唤醒

是什么

  • LockSupport 是用于创建锁和其他同步类的基本线程阻塞原语
  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只能有一个,累加上限是1。

主要方法

  • 阻塞: Peimit许可证默认没有不能放行,所以一开始调用park()方法当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。

  • park/park(Object blocker)-------阻塞当前线程/阻塞传入的具体线程

  • 唤醒: 调用unpack(thread)方法后 就会将thread线程的许可证peimit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。

  • unpark(Thread thread)------唤醒处于阻塞状态的指定线程

/**
 * 
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        /**
         * t1	 -----------come in
         * t2	 ----------发出通知
         * t1	 ----------被唤醒
         */
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t -----------come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");
        }, "t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");
        }, "t2").start();

    }
}

重点说明(重要)

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有的方法都是静态方法,可以让线程再任意位置阻塞,阻塞后也有对应的唤醒方法。归根结底,LockSupport时调用Unsafe中的native代码

  • LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,LockSupport和每个使用它的线程都有一个许可(Peimit)关联,每个线程都有一个相关的permit,peimit最多只有一个,重复调用unpark也不会积累凭证。

  • 形象理解:线程阻塞需要消耗凭证(Permit),这个凭证最多只有一个

  • 当调用park时,如果有凭证,则会直接消耗掉这个凭证然后正常退出。如果没有凭证,则必须阻塞等待凭证可用;

  • 当调用unpark时,它会增加一个凭证,但凭证最多只能有1各,累加无效。

面试题

  • 为什么LockSupport可以突破wait/notify的原有调用顺序?

  • 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞,先发放了凭证后续可以畅通无阻。

  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

  • 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要消费两个凭证,证不够,不能放行。