多线程及线程安全问题详解(全)

2022-10-04 13:46:35

学前小故事

深入线程

Java并发集合

深入锁机制

Java线程池

cpu与核心

高并发解决方案

学前小故事

1.一切要从CPU说起

2.从CPU到操作系统

3.从单核到多核,如何充分利用多核

4.从进程到线程

5.线程与内存

6.线程的使用

7.从多线程到线程池

8.线程池是如何工作的

9.线程池中线程的数量

一切要从CPU说起

你可能会有疑问,讲多线程为什么要从CPU说起呢?原因很简单,在这里没有那些时髦的概念,你可以更加清晰的看清问题的本质。

CPU并不知道线程、进程之类的概念。
CPU只知道两件事:

1. 从内存中取出指令
2. 执行指令,然后回到1
在这里插入图片描述
你看,在这里CPU确实是不知道什么进程、线程之类的概念。

接下来的问题就是CPU从哪里取出指令呢?答案是来自一个被称为Program Counter(简称PC)的寄存器,也就是我们熟知的程序计数器,在这里大家不要把寄存器想的太神秘,你可以简单的把寄存器理解为内存,只不过存取速度更快而已。

PC寄存器中存放的是什么呢?这里存放的是指令在内存中的地址,什么指令呢?是CPU将要执行的下一条指令。
在这里插入图片描述
那么是谁来设置PC寄存器中的指令地址呢?

原来PC寄存器中的地址默认是自动加1的,这当然是有道理的,因为大部分情况下CPU都是一条接一条顺序执行,当遇到if、else时,这种顺序执行就被打破了,CPU在执行这类指令时会根据计算结果来动态改变PC寄存器中的值,这样CPU就可以正确的跳转到需要执行的指令了。

聪明的你一定会问,那么PC中的初始值是怎么被设置的呢?

在回答这个问题之前我们需要知道CPU执行的指令来自哪里?是来自内存,废话,内存中的指令是从磁盘中保存的可执行程序加载过来的,磁盘中可执行程序是编译器生成的,编译器又是从哪里生成的机器指令呢?答案就是我们定义的函数。
在这里插入图片描述

注意是函数,函数被编译后才会形成CPU执行的指令,那么很自然的,我们该如何让CPU执行一个函数呢?显然我们只需要找到函数被编译后形成的第一条指令就可以了,第一条指令就是函数入口。

现在你应该知道了吧,我们想要CPU执行一个函数,那么只需要把该函数对应的第一条机器指令的地址写入PC寄存器就可以了,这样我们写的函数就开始被CPU执行起来啦。

你可能会有疑问,这和线程有什么关系呢?

从CPU到操作系统

上节中我们明白了CPU的工作原理,我们想让CPU执行某个函数,那么只需要把函数对应的第一条机器执行装入PC寄存器就可以了,这样即使没有操作系统我们也可以让CPU执行程序,虽然可行但这是一个非常繁琐的过程,我们需要:

* 在内存中找到一块大小合适的区域装入程序

* 找到函数入口,设置好PC寄存器让CPU开始执行程序

这两个步骤绝不是那么容易的事情,如果每次在执行程序时程序员自己手动实现上述两个过程会疯掉的,因此聪明的程序员就会想干脆直接写个程序来自动完成上面两个步骤吧。
在这里插入图片描述

机器指令需要加载到内存中执行,因此需要记录下内存的起始地址和长度;同时要找到函数的入口地址并写到PC寄存器中,想一想这是不是需要一个数据结构来记录下这些信息:

struct***{void* start_addr;int len;void* start_point;...};

接下来就是起名字时刻。

这个数据结构总要有个名字吧,这个结构体用来记录什么信息呢?记录的是程序在被加载到内存中的运行状态,程序从磁盘加载到内存跑起来叫什么好呢?干脆就叫进程(Process)好了,我们的指导原则就是一定要听上去比较神秘,总之大家都不容易弄懂就对了,我将其称为“弄不懂原则”。

就这样进程诞生了。

CPU执行的第一个函数也起个名字,第一个要被执行的函数听起来比较重要,干脆就叫main函数吧。

完成上述两个步骤的程序也要起个名字,根据“弄不懂原则”这个“简单”的程序就叫操作系统(Operating System)好啦。

就这样操作系统诞生了,程序员要想运行程序再也不用自己手动加载一遍了。
现在进程和操作系统都有了,一切看上去都很完美。

从单核到多核,如何充分利用多核

人类的一大特点就是生命不息折腾不止,从单核折腾到了多核。
在这里插入图片描述
这时,假设我们想写一个程序并且要分利用多核该怎么办呢?

有的同学可能会说不是有进程吗,多开几个进程不就可以了?听上去似乎很有道理,但是主要存在这样几个问题:

* 进程是需要占用内存空间的(从上一节能看到这一点),如果多个进程基于同一个可执行程序,那么这些进程其内存区域中的内容几乎完全相同,这显然会造成内存的浪费

计算机处理的任务可能是比较复杂的,这就涉及到了进程间通信,由于各个进程处于不同的内存地址空间,进程间通信天然需要借助操作系统,这就在增大编程难度的同时也增加了系统开销

该怎么办呢?

从进程到线程

让我再来仔细的想一想这个问题,所谓进程无非就是内存中的一段区域,这段区域中保存了CPU执行的机器指令以及函数运行时的堆栈信息,要想让进程运行,就把main函数的第一条机器指令地址写入PC寄存器,这样进程就运行起来了。
在这里插入图片描述
进程的缺点在于只有一个入口函数,也就是main函数,因此进程中的机器指令只能被一个CPU执行,那么有没有办法让多个CPU来执行同一个进程中的机器指令呢?

聪明的你应该能想到,既然我们可以把main函数的第一条指令地址写入PC寄存器,那么其它函数和main函数又有什么区别呢?

答案是没什么区别,main函数的特殊之处无非就在于是CPU执行的第一个函数,除此之外再无特别之处,我们可以把PC寄存器指向main函数,就可以把PC寄存器指向任何一个函数。

当我们把PC寄存器指向非main函数时,线程就诞生了。
在这里插入图片描述

至此我们解放了思想,一个进程内可以有多个入口函数,也就是说属于同一个进程中的机器指令可以被多个CPU同时执行。

注意,这是一个和进程不同的概念,创建进程时我们需要在内存中找到一块合适的区域以装入进程,然后把CPU的PC寄存器指向main函数,也就是说进程中只有一个执行流。
在这里插入图片描述
但是现在不一样了,多个CPU可以在同一个屋檐下(进程占用的内存区域)同时执行属于该进程的多个入口函数,也就是说现在一个进程内可以有多个执行流了。
在这里插入图片描述

总是叫执行流好像有点太容易理解了,再次祭出”弄不懂原则“,起个不容易懂的名字,就叫线程吧。

这就是线程的由来。

操作系统为每个进程维护了一堆信息,用来记录进程所处的内存空间等,这堆信息记为数据集A。

同样的,操作系统也需要为线程维护一堆信息,用来记录线程的入口函数或者栈信息等,这堆数据记为数据集B。

显然数据集B要比数据A的量要少,同时不像进程,创建一个线程时无需去内存中找一段内存空间,因为线程是运行在所处进程的地址空间的,这块地址空间在程序启动时已经创建完毕,同时线程是程序在运行期间创建的(进程启动后),因此当线程开始运行的时候这块地址空间就已经存在了,线程可以直接使用。这就是为什么各种教材上提的创建线程要比创建进程快的原因(当然还有其它原因)。

值得注意的是,有了线程这个概念后,我们只需要进程开启后创建多个线程就可以让所有CPU都忙起来,这就是所谓高性能、高并发的根本所在。
在这里插入图片描述
很简单,只需要创建出数量合适的线程就可以了。
另外值得注意的一点是,由于各个线程共享进程的内存地址空间,因此线程之间的通信无需借助操作系统,这给程序员带来极大方便的同时也带来了无尽的麻烦,多线程遇到的多数问题都出自于线程间通信简直太方便了以至于非常容易出错。

出错的根源在于CPU执行指令时根本没有线程的概念,多线程编程面临的互斥与同步问题需要程序员自己解决,关于互斥与同步问题限于篇幅就不详细展开了,大部分的操作系统资料都有详细讲解。

最后需要提醒的是,虽然前面关于线程讲解使用的图中用了多个CPU,但不是说一定要有多核才能使用多线程,在单核的情况下一样可以创建出多个线程,原因在于线程是操作系统层面的实现,和有多少个核心是没有关系的,CPU在执行机器指令时也意识不到执行的机器指令属于哪个线程。

即使在只有一个CPU的情况下,操作系统也可以通过线程调度让各个线程“同时”向前推进,方法就是将CPU的时间片在各个线程之间来回分配,这样多个线程看起来就是“同时”运行了,但实际上任意时刻还是只有一个线程在运行。

线程与内存

在前面的讨论中我们知道了线程和CPU的关系,也就是把CPU的PC寄存器指向线程的入口函数,这样线程就可以运行起来了,这就是为什么我们创建线程时必须指定一个入口函数的原因。无论使用任何编程语言,创建一个线程大体相同:

// 设置线程入口函数DoSomething
thread=CreateThread(DoSomething);// 让线程运行起来thread.Run();

那么线程和内存又有什么关联呢?

我们知道函数在被执行的时产生的数据包括函数参数、局部变量、返回地址等信息,这些信息是保存在栈中的,线程这个概念还没有出现时进程中只有一个执行流,因此只有一个栈,这个栈的栈底就是进程的入口函数,也就是main函数,假设main函数调用了funA,funcA又调用了funcB,如图所示:
在这里插入图片描述
那么有了线程以后了呢?

有了线程以后一个进程中就存在多个执行入口,即同时存在多个执行流,那么只有一个执行流的进程需要一个栈来保存运行时信息,那么很显然有多个执行流时就需要有多个栈来保存各个执行流的信息,也就是说操作系统要为每个线程在进程的地址空间中分配一个栈,即每个线程都有独属于自己的栈,能意识到这一点是极其关键的。
在这里插入图片描述
同时我们也可以看到,创建线程是要消耗进程内存空间的,这一点也值得注意。

线程的使用

现在有了线程的概念,那么接下来作为程序员我们该如何使用线程呢?
从生命周期的角度讲,线程要处理的任务有两类:长任务和短任务。

1,长任务,long-lived tasks

顾名思义,就是任务存活的时间很长,比如以我们常用的word为例,我们在word中编辑的文字需要保存在磁盘上,往磁盘上写数据就是一个任务,那么这时一个比较好的方法就是专门创建一个写磁盘的线程,该写线程的生命周期和word进程是一样的,只要打开word就要创建出该写线程,当用户关闭word时该线程才会被销毁,这就是长任务。

这种场景非常适合创建专用的线程来处理某些特定任务,这种情况比较简单。
有长任务,相应的就有短任务。

2,短任务,short-lived tasks

这个概念也很简单,那就是任务的处理时间很短,比如一次网络请求、一次数据库查询等,这种任务可以在短时间内快速处理完成。因此短任务多见于各种Server,像web server、database server、file server、mail server等,这也是互联网行业的同学最常见的场景,这种场景是我们要重点讨论的。

这种场景有两个特点:一个是任务处理所需时间短;另一个是任务数量巨大。

如果让你来处理这种类型的任务该怎么办呢?

你可能会想,这很简单啊,当server接收到一个请求后就创建一个线程来处理任务,处理完成后销毁该线程即可,So easy。
这种方法通常被称为thread-per-request,也就是说来一个请求就创建一个线程:
在这里插入图片描述
如果是长任务,那么这种方法可以工作的很好,但是对于大量的短任务这种方法虽然实现简单但是有这样几个缺点:

1. 从前几节我们能看到,线程是操作系统中的概念(这里不讨论用户态线程实现、协程之类),因此创建线程天然需要借助操作系统来完成,操作系统创建和销毁线程是需要消耗时间的

2. 每个线程需要有自己独立的栈,因此当创建大量线程时会消耗过多的内存等系统资源

这就好比你是一个工厂老板(想想都很开心有没有),手里有很多订单,每来一批订单就要招一批工人,生产的产品非常简单,工人们很快就能处理完,处理完这批订单后就把这些千辛万苦招过来的工人辞退掉,当有新的订单时你再千辛万苦的招一遍工人,干活儿5分钟招人10小时,如果你不是励志要让企业倒闭的话大概是不会这么做到的,因此一个更好的策略就是招一批人后就地养着,有订单时处理订单,没有订单时大家可以闲呆着。

这就是线程池的由来。

从多线程到线程池

线程池的概念是非常简单的,无非就是创建一批线程,之后就不再释放了,有任务就提交给这些线程处理,因此无需频繁的创建、销毁线程,同时由于线程池中的线程个数通常是固定的,也不会消耗过多的内存,因此这里的思想就是复用、可控。

线程池是如何工作的

可能有的同学会问,该怎么给线程池提交任务呢?这些任务又是怎么给到线程池中线程呢?

很显然,数据结构中的队列天然适合这种场景,提交任务的就是生产者,消费任务的线程就是消费者,实际上这就是经典的生产者-消费者问题。
在这里插入图片描述
现在你应该知道为什么操作系统课程要讲、面试要问这个问题了吧,因为如果你对生产者-消费者问题不理解的话,本质上你是无法正确的写出线程池的。

限于篇幅在这里博主不打算详细的讲解生产者消费者问题,参考操作系统相关资料就能获取答案。这里博主打算讲一讲一般提交给线程池的任务是什么样子的。

一般来说提交给线程池的任务包含两部分:1) 需要被处理的数据;2) 处理数据的函数

struct task{void* data;// 任务所携带的数据
    handler handle;// 处理数据的方法}

(注意,你也可以把代码中的struct理解成class,也就是对象。)

线程池中的线程会阻塞在队列上,当生产者向队列中写入数据后,线程池中的某个线程会被唤醒,该线程从队列中取出上述结构体(或者对象),以结构体(或者对象)中的数据为参数并调用处理函数:

while(true){
  struct task=GetFromQueue();// 从队列中取出数据
  task->handle(task->data);// 处理数据}

以上就是线程池最核心的部分。
理解这些你就能明白线程池是如何工作的了。

线程池中线程的数量

现在线程池有了,那么线程池中线程的数量该是多少呢?
在接着往下看前先自己想一想这个问题。

如果你能看到这里说明还没有睡着。

要知道线程池的线程过少就不能充分利用CPU,线程创建的过多反而会造成系统性能下降,内存占用过多,线程切换造成的消耗等等。因此线程的数量既不能太多也不能太少,那到底该是多少呢?

回答这个问题,你需要知道线程池处理的任务有哪几类,有的同学可能会说你不是说有两类吗?长任务和短任务,这个是从生命周期的角度来看的,那么从处理任务所需要的资源角度看也有两种类型,CPU密集型和I/O密集型。

1,CPU密集型

所谓CPU密集型就是说处理任务不需要依赖外部I/O,比如科学计算、矩阵运算等等。在这种情况下只要线程的数量和核数基本相同就可以充分利用CPU资源。
在这里插入图片描述

2,I/O密集型

这一类任务可能计算部分所占用时间不多,大部分时间都用在了比如磁盘I/O、网络I/O等。
在这里插入图片描述

这种情况下就稍微复杂一些了,你需要利用性能测试工具评估出用在I/O等待上的时间,这里记为WT(wait time),以及CPU计算所需要的时间,这里记为CT(computing time),那么对于一个N核的系统,合适的线程数大概是N * (1 + WT/CT),假设I/O等待时间和计算时间相同,那么你大概需要2N个线程才能充分利用CPU资源,注意这只是一个理论值,具体设置多少需要根据真实的业务场景进行测试。

当然充分利用CPU不是唯一需要考虑的点,随着线程数量的增多,内存占用、系统调度、打开的文件数量、打开的socker数量以及打开的数据库链接等等是都需要考虑的。

因此这里没有万能公式,要具体情况具体分析。

线程池不是万能的

线程池仅仅是多线程的一种使用形式,因此多线程面临的问题线程池同样不能避免,像死锁问题、race condition问题等等,关于这一部分同样可以参考操作系统相关资料就能得到答案,所以基础很重要呀老铁们。

线程池使用的最佳实践

线程池是程序员手中强大的武器,互联网公司的各个server上几乎都能见到线程池的身影,使用线程池前你需要考虑:

* 充分理解你的任务,是长任务还是短任务、是CPU密集型还是I/O密集型,如果两种都有,那么一种可能更好的办法是把这两类任务放到不同的线程池中,这样也许可以更好的确定线程数量

* 如果线程池中的任务有I/O操作,那么务必对此任务设置超时,否则处理该任务的线程可能会一直阻塞下去

* 线程池中的任务最好不要同步等待其它任务的结果

话题总结

我们从CPU开始一路来到常用的线程池,从底层到上层、从硬件到软件。注意,这里通篇没有出现任何特定的编程语言,线程不是语言层面的概念(依然不考虑用户态线程),但是当你真正理解了线程后,相信你可以在任何一门语言下用好多线程,你需要理解的是道,此后才是术。

深入线程

线程的实现方式

线程的生命周期

线程的执行顺序

线程的停止方法

判断线程是否中断

线程的安全问题

Java实现线程安全的方式

线程的内部存储

深入Callable接口

6.Future

7.FutureTask

线程的实现方式

在Java中,实现线程的方式大体上分为三种,通过继承Thread类、实现Runnable接口,实现Callable接口。

  1. 继承Thread类

    publicclassThreadDataPro{publicstaticvoidmain(String[] args){Ticket ticket=newTicket();//创建四个线程对象Thread t1=newThread(ticket,"窗口1");Thread t2=newThread(ticket,"窗口2");Thread t3=newThread(ticket,"窗口3");Thread t4=newThread(ticket,"窗口4");
    	
    	    t1.start();
    	    t2.start();
    	    t3.start();
    	    t4.start();}staticclassTicketextendsThread{privateint tickets=10;//设置线程任务@Overridepublicvoidrun(){//TODO 在此写在线程中执行的业务逻辑while(true){if(tickets>0){String name=Thread.currentThread().getName();//(由于i--不是原子操作(先获取i的值,让后再减一,再把结果赋给i),所以输出的值会有重复的情况,比如4 4 2)System.out.println(name+"正在发售第"+tickets--+"张票");}else{break;}}}}}
  2. 实现Runnable接口

    publicclassRunnableTestimplementsRunnable{@Overridepublicvoidrun(){//TODO 在此写在线程中执行的业务逻辑}}

    实现Runnable接口的方法避免了使用Thread单继承的局限性,并且实现了解耦,任务可以被多个线程共享,任务和线程是独立的。

  3. 实现Callable接口的代码

    publicclassCallableTestimplementsCallable<String>{@OverridepublicStringcall()throwsException{//TODO 在此写在线程中执行的业务逻辑 return null;}}

线程的生命周期

一个线程从创建,到最终的消亡,需要经历多种不同的状态,而这些不同的线程状态,由始至终也构成了线程生命周期的不同阶段。线程的生命周期可以总结为下图。

在这里插入图片描述
其中,几个重要的状态如下所示。

  • New :初始状态,线程被构建,但是还没有调用start()方法

  • RUNNABLE :可运行状态,可运行状态可以包括:运行中状态和就绪状态。状态为runnable的线程正在Java虚拟机中执行,但是它可能正在等待来自操作系统的其他资源,例如处理器。

  • BLOCKED :阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized

  • WAITING :等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。由于调用以下方法之一,线程处于等待状态: Object#wait()、Thread#join()、LockSupport#park()

  • TIMED_WAITING :超时等待状态。可以在一定的时间自行返回。

  • TERMINATED :终止状态,当前线程执行完毕。

线程的执行顺序

调用Thread的start()方法启动线程时,线程的执行顺序是不确定的。也就是说,在同一个方法中,连续创建多个线程后,调用线程的start()方法的顺序并不能决定线程的执行顺序。这里看一个简单的示例程序,如下所示。

/**
* @author ferao
* @version 1.0.0 
* @description 线程的顺序,直接调用Thread.start()方法执行不能确保线程的执行顺序 
*/publicclassThreadSort{publicstaticvoidmain(String[] args){Thread thread1=newThread(()->{System.out.println("thread1");});Thread thread2=newThread(()->{System.out.println("thread2");});Thread thread3=newThread(()->{System.out.println("thread3");}); 
		thread1.start(); 
		thread2.start(); 
		thread3.start();}}

在ThreadSort类中分别创建了三个不同的线程,thread1、thread2和thread3,接下来,在程序中
按照顺序分别调用thread1.start()、thread2.start()和thread3.start()方法来分别启动三个不同的线程。

那么,问题来了,线程的执行顺序是否按照thread1、thread2和thread3的顺序执行呢?运行
ThreadSort01的main方法,结果如下所示。

  • thread1
  • thread3
  • thread2

多次运行可以看到,每次运行程序时,线程的执行顺序可能不同。线程的启动顺序并不能决定线程的执行顺序。

在实际业务场景中,有时,后启动的线程可能需要依赖先启动的线程执行完成才能正确的执行线程中的业务逻辑。此时,就需要确保线程的执行顺序。那么如何确保线程的执行顺序呢?

可以使用Thread类中的join()方法来确保线程的执行顺序。例如下面的测试代码

/**
 * @author ferao
 * @version 1.0.0
 * @description 线程的顺序,Thread.join()方法能够确保线程的执行顺序
 */publicclassThreadSort02{publicstaticvoidmain(String[] args)throwsInterruptedException{Thread thread1=newThread(()->{System.out.println("thread1");});Thread thread2=newThread(()->{System.out.println("thread2");});Thread thread3=newThread(()->{System.out.println("thread3");});
        thread1.start();//实际上让主线程等待子线程执行完成
        thread1.join(); 
        thread2.start(); 
        thread2.join(); 
        thread3.start(); 
        thread3.join();}}

可以看到,ThreadSort02类比ThreadSort类,在每个线程的启动方法下面添加了调用线程的join()方法。此时,运行ThreadSort02类,结果如下所示。

thread1
thread2
thread3

可以看到,每次运行的结果都是相同的,所以,使用Thread的join()方法能够保证线程的先后执行顺序。

既然Thread类的join()方法能够确保线程的执行顺序,我们就一起来看看Thread类的join()方法到底是个什么鬼。[注意join是一个同步方法]进入Thread的join()方法,如下所示。

publicfinalvoidjoin()throwsInterruptedException{join(0);}

可以看到join()方法调用同类中的一个有参join()方法,并传递参数0。继续跟进代码,如下所示。

publicfinalsynchronizedvoidjoin(long millis)throwsInterruptedException{long base=System.currentTimeMillis();long now=0;if(millis<0){thrownewIllegalArgumentException("timeout value is negative");}if(millis==0){while(isAlive()){wait(0);}}else{while(isAlive()){long delay= millis- now;if(delay<=0){break;}wait(delay);
            now=System.currentTimeMillis()- base;}}}

可以看到,有一个long类型参数的join()方法使用了synchroinzed修饰,说明这个方法同一时刻只能被一个实例或者方法调用。由于,传递的参数为0,所以,程序会进入如下代码逻辑。

if(millis==0){while(isAlive()){wait(0);}}

首先,在代码中以while循环的方式来判断当前线程是否已经启动处于活跃状态,如果已经启动处于活跃状态,则调用同类中的wait()方法,并传递参数0。继续跟进wait()方法,如下所示。

publicfinalnativevoidwait(long timeout)throwsInterruptedException;

可以看到,wait()方法是一个本地方法,通过JNI的方式调用JDK底层的方法来使线程等待执行完成。

需要注意的是,调用线程的wait()方法时,会使主线程处于等待状态,等待子线程执行完成后再次向下执行。也就是说,在ThreadSort02类的main()方法中,调用子线程的join()方法,会阻塞main()方法的执行,当子线程执行完成后,main()方法会继续向下执行,启动第二个子线程,并执行子线程的业务逻辑,以此类推。

[拓展]由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
1.用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
2.内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

```java
用户态与内核态的切换

	所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等.

	而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.

	这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令

	这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)
```

线程的停止方法

  1. 线程自己执行完后自动终止

  2. stop强制终止,不安全

  3. 使用interrupt方法

    线程对象有一个boolean变量代表是否有中断请求,interrupt方法将线程的中断状态设置会true,但是并没有立刻终止线程,就像告诉你儿子要好好学习一样,但是你儿子怎么样关键看的是你儿子。

    publicstaticvoidmain(String[] args){try{MyThread thread=newMyThread();
    		thread.start();Thread.sleep(200);
    		thread.interrupt();}catch(
  • 作者:Ferao
  • 原文链接:https://blog.csdn.net/qq_21561501/article/details/90637859
    更新时间:2022-10-04 13:46:35