Java 从多线程到并发编程(三)——线程的状态 线程的停止 stop

前言 ´・ᴗ・`

-前面两篇文章主要了解了进程线程的概念以及如何创建多线程的三种方式 demo用的最多应该是Runnable,而真正业务场景中常用的应该是Callable

在这一节,我们会讨论线程的状态,会讨论如何优雅的停止线程,还会讨论过去停止(stop) 挂起(suspend)线程的方式有哪些问题,讨论的途中对多线程有更加深入的认识。

线程状态

之前讲过进程的状态有:
就绪(ready)运行(Running)与阻塞(blocked)

再经过前面简单的例子 我们应该大致感觉出来线程具有哪些状态了:

  1. 初始(NEW):新创建了一个线程对象,在内存中待着,但还没有调用start()方法。
  2. 运行(RUNNABLE):线程中将类似进程的就绪(runnable)和运行中(running)两种状态 笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(runnable)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回Thread.sleep正是这种状态
  6. 终止(TERMINATED):线程执行完毕

这里线程能否得到CPU的垂青 其机制与进程几乎相同 尤其是关键的 就绪态 阻塞态 一样具有就绪队列

详细的分析建议看这篇博客

值得注意的是,我个人不太喜欢官方这种对线程状态的分类,主要是Runnable这个状态,粒度太大了,中间可能是真的获得了CPU的时间片得以执行,也可能只是Runnable,即还处于一种就绪态,在等待CPU的垂青,还可能,比如在BIO模型中,线程其实被网络IO阻塞,正在等待,但你检查他的线程状态,也显示这Runnable。

总之,这个状态 尤其是Runnable状态 并不那么精确。

线程状态demo

package com.base.threadlearning.threadState;

/**
 * new
 * runnable
 * blocked
 * wait
 * time wait
 * terminated
 *
 */
public class ThreadState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 2; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程run方法跑完");
        });

        // 线程启动前
        System.out.println(thread.getState());

        // 线程启动时 进入run 然后进入thread sleep
        thread.start();
        System.out.println(thread.getState());


        // 线程终止
        // 一旦进入死亡状态 比如运行完成并不挂起 或者被中断 那么再也不能启动 除非开启新的线程
            while (thread.getState()!=Thread.State.TERMINATED){
            Thread.sleep(100);
            System.out.println(thread.getState());
            // 每隔100ms更新状态
        }

    }


}

运行结果如下:

NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
线程run方法跑完
TERMINATED

这里提一个小问题,你说我要获取线程的状态,是线程自己说,还是通过别的线程来获取?
你想一下,如果线程被锁blocked阻塞了,你说它可能告诉你 他被阻塞了嘛(指的是直接返回阻塞状态),显然他不能,他不但被某个临界资源卡住了,而且也没办法获得CPU,怎么可能会返回状态,因此一般是通过另一个线程来获取。

优雅的线程停止方式

既然我们学习了线程的三种状态,那么下面我们开始实现线程的停止。
总所周知,利用stop实现的外部强制的停止都是不推荐的 因为很可能触发数据安全问题

这里提供一个简单的思路:利用一个内在的flag 能够实现停止接口

接下来我们看个简单的demo

package com.base.threadlearning.threadVSrunnable;



public class RunnableTicket implements Runnable{
    private static final int TICKETS=10000;
    private int tickets;
    private boolean flag;

    RunnableTicket(){
        tickets=TICKETS;
        flag=true;
    }
    public void setFlag(Boolean flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        while(flag){
            if(tickets<=0) break;

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" get the "+tickets--+" tickets");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        RunnableTicket ticket_station = new RunnableTicket();
        String[] Robot_array = {"Robot A","Robot B","Robot C"};

        for(String i : Robot_array){
            new Thread(ticket_station,i).start();
        }
        Thread.sleep(4000);
        TicketController ticketController = new TicketController(ticket_station);
        new Thread(ticketController).start();
    }
}

这个demo的本意也很简单 三个抢票机器人 如何实现抢票时间一定后就停止呢 比如我就抢4秒

这里我采用主线程sleep的方式 (也就是让主线程处于TIME_WAIT模式4秒)同时3个robot还在挖票

4秒后我调用TicketController 实质上就是调用了robot内在的setFlag函数接口 实现内部的线程停止

TicketController代码如下:


package com.base.threadlearning.threadVSrunnable;

/**
 * @ClassName: ticketContrller
 * @Description
 * @Author Ryan
 * @Date 2021.2.18 17:04
 * @Version 1.0.0-Beta
 **/
public class TicketController implements Runnable{
    RunnableTicket ticket_station;
    TicketController(RunnableTicket ticket_station){
        this.ticket_station = ticket_station;
    }

    @Override
    public void run() {
        ticket_station.setFlag(false);
    }
}

其实这里的实现还可以使用后面我们所说的守护进程的方式 而且可以通过守护进程 轮询判断robot的状态来控制

抛出异常 interrupt 停止一个被阻塞的线程

不知道大家发现了没,前面的方式很简单,而且甚至在嵌入式开发中都非常常用,但是有个致命的问题,那就是如果你想停止一个被阻塞的线程,比如正在等待IO资源而被阻塞的线程,由于此线程被阻塞,拿不到CPU 因此不可能运行到标志位然后自我退出。

可能你觉得,WHY?他反正阻塞完成,还是会run到标志位然后自我退出啊,没啥问题
其实我们之所以让线程退出,一方面是避免CPU资源的继续消耗,还有呢?IO资源不算资源嘛?如果能够实现立刻停止,其实他压根不用等待IO,也不用导致其他线程的IO阻塞,而是干脆的退出,这样相对来说更好。

那么该怎么实现?有个巧妙地方式就是,抛出异常
我们看一个经典的案例,停止一个自我阻塞(sleep)的线程:

package com.base.threadlearning.threadState;

/**
 * @ClassName: ThreadInterrupt
 * @Description
 * @Author Ryan
 * @Date 2021.7.15 9:31
 * @Version 1.0.0-Beta
 **/
public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                super.run();
                System.out.println("Thread start");
                try{
                	// 原定要自我阻塞10秒钟
                    Thread.sleep(1000*10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread terminated");
                
            }
        };
        thread.start();
        // main Thread 自我阻塞1秒后 将我们的线程 thread interrupt
        Thread.sleep(1000);
        thread.interrupt();
        
    }
}


这里我们的线程会自我阻塞 利用sleep 阻塞10秒,而我们的主线程会在一秒后强制让我们的线程停止

打印结果如下
在这里插入图片描述
其实这里我是因为打印了Interrupt Exception才会这样的 如果不打印 那就好像无事发生一样 如下:
在这里插入图片描述
当然,这个interrupt只能停止自我阻塞这种情况,那IO阻塞呢?其实道理相同,抛出IO相关的异常,比如IOException即可
还记得我们下载图片的案例嘛?我们的Downloader 有这么一段代码:

	try {
	  	 FileUtils.copyURLToFile(new URL(url),new File(name));
	} catch (IOException e) {
		e.printStackTrace();
		System.out.println("download failed");
	}

这里就是为了实现,网络传输超时 自动停止线程的操作,直接抛出IOException 这样就能避免IO资源的占用

Stop

过去有种强制停止线程的方法叫stop,我们可以看看他的说明:
在这里插入图片描述
为啥deprecated?
首先我们得知道stop的机制,他是会抛出一个ThreadDeath异常,然后直接结束执行。这听起来没啥问题啊?我们刚刚的方式不也是抛出异常嘛?我们知道,重要的不是方法本身,而是如何运用,错的不是我,而是这个世界 ,抛出异常本身没有问题,关键是,我们抛出异常“有理有据”:要不是自己内部确定了当前状态,比如网络问题,阻塞了,所以抛出异常来结束,或者采用标志位,一般while循环一轮,数据处理完成了才会触发。

但是如果你从外部,直接强制停止,那么线程将terminated,而且释放掉所有的临界区资源的锁,如果这样,那和之前就完全不是一回事了——你不知道内部发生了什么,可能临界区的数据,也就是会被保护,会上锁的数据才加工了一半,你停了线程,其他线程拿到锁,拿到的数据就会是错误的数据,而且因为完全随机,bug很难debug,因此会成为巨大的隐患。

同样的道理,如果你并不确定内部运行状态,瞎interrupt,那效果我认为也是和stop相同的! 因此最好是内部来停止,如果实在需要外部停止,一定要判断好内部的状态。

总结 ´◡`

我们从线程的几种状态开始学起,然后提到如何优雅的让线程停止,还有过去stop方式的弊端,

可能有读者想到,我们可以停止线程,那是不是也可以阻塞(或者你认真的话可以说 挂起)线程呢?其实阻塞是门学问,我们先按捺一下好奇心,耐心往下学。其实在后面几节都会涉及到阻塞,等到一定程度我会总结出阻塞相关的系统知识。

下一节 Java 从多线程到并发编程(四)——线程礼让yield 线程强制执行join 守护线程 线程优先级

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值