您当前的位置:首页 > 电脑百科 > 程序开发 > 算法

面试中如何答好:线程

时间:2023-10-16 12:27:47  来源:微信公众号  作者:码农本农

1.进程

是操作系统为应用程序分配资源的基本单位,比如操作系统会为一个应用程序分配独立的工作空间,硬件资源,任务调度等。一个应用程序就是一个进程。

面试中如何答好:线程

2.线程

是cpu执行的基本单位,可以理解为一个基本的执行流,一个进程中至少有一个线程。进程是线程的集合体或者载体。

3.线程模型

线程分为内核线程和用户线程。

内核线程就是操作系统自己实现的一套线程机制,实现一套线程机制并不是像JAVA一样创建一个Thread对象即可,而是除了创建线程对象,还要基于操作系统及硬件的不同(比如cpu是多核单核等),制定何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等事宜,而内核实现了这一套机制。

用户线程就是用户程序中创建的线程,和内核线程一样,线程是交给处理器执行,但是具体的执行和分配机制需要用户程序自己实现,就是说也要根据机器的不同实现一套线程运行机制才行。但是实现这样一套机制是极其复杂的,而且还要考虑不同的机器平台,所以说有一定的困难度。

那我们开发的应用程序线程是怎么实现的呢,一般的应用程序实现的线程根据其特点可以分为三种:

  • 用户线程和内核一一对应的1:1模型
  • 用户线程和进程多对一的1:N模型
  • 用户线程和内核线程多对多的M:N模型

(1) 1:1模型

应用程序创建一个线程实际上是通过内核提供的内核线程api调用与一个内核线程建立一比一映射关系,也就是说看起来应用程序创建了用户线程,但其实应用程序只是创建线程对象,通过线程对象调用了内核api创建了内核线程,对于应用程序来说就比较省事了,因为具体的线程机制都是由内核完成。

这种模型免不了用户态到内核态的切换,性能方面会被限制。

(2) 1:N模型

就是上面所说的,由用户程序自己实现线程机制,包括线程创建,线程调度等。

这种模型基本能避免用户态到内核态的切换,这就可以支持更多的线程并发,不好的地方就是实现困难。

(3) M:N模型

就是用户程序中的用户线程和内核线程不是一比一映射,而是多条用户线程对应一条内核线程或者多条内核线程。

这种模型既能够保证并发量,提高性能,又能利用内核的调度机制,但是免不了自己实现一套线程机制,依然具备很高的复杂性。

java在实现线程机制这条路上可以说对于三种模型都曾有过实现。其实采用哪种线程模型依赖于所使用的操作系统是否支持,就拿Hotspot这款虚拟机来说,它在不同操作系统上的实现就不一样,在Solaris平台的HotSpot虚拟机,由于操作系统的线程特性本来就可以同时支持 1:1及N:M

但是主流的java虚拟机hotspot在主流的操作系统win和linux,采用的都是1:1模型。即线程调度交个内核来完成,这样我们在创建线程的时候就受到了资源的很大限制,比如我们无法做到创建大批量的线程出来。

举个例子:

内核是一个国有工厂负责加工产品,cpu是加工产品的机器,一个用户程序就是一个私有企业,也就是一个进程,而线程就是私企和国企里面的工作人员。每个私企员工都会将一定的原材料送到国企进行加工。

1:1模型中,每当一个私企员工携带原材料来加工,都会有一个国企的员工接待,负责加工流程中的所有事情,直到加工完成并将私企员工送走。因为有很多私企,一个私企里面有很多私企员工,而国企员工相对较少,这样一对一的服务,如果私企的人来的太多,会把国企人员累死,整个生产线会瘫痪。

1:N模型中就是每个私企在国企里面租一台机器的使用时间,就是某个时间段,由某个私企承包,这个时间段内机器只为这个私企工作,这种情况下只要在这个时间段内,私企想派多少人去加工就派多少人去加工。

M:N模型中,某个私企大概有100人携带原材料加工,私企感觉这样效率有点低,就想了个办法,和国企达成某种协议,让国企出几个人专门服务于私企的这100个人,这样效率会提升了。

4.线程调度

多线程情况下,能够让每个线程都能有条不紊的得到运行就是线程调度。

我们知道cpu处理器是单线流水运行,线程是运行在cpu上的,一个时间点上只会有一条线程正在被cpu运行。

基于以上情况,线程要如何调度呢,一般情况下,操作系统有两种调度方式:

(1) 协同式调度

正在执行的线程自己控制自身的执行时间,并且当前线程执行完后由自身告知操作系统可以调度到其他线程上了。

这种方式相对来说实现简单,不用考虑同步的问题。

但是缺点也很明显,这种调度方式中有两个点要注意,线程多长时间可以执行完?线程一定能通知操作系统调度其他线程吗?这两点无法保证的话就会引发一些问题,比如当前线程本身运行时间很长,执行逻辑中有一些io等待操作,这就造成cpu是空闲的,资源大大浪费;再比如如果代码或者业务逻辑出了问题稍有不慎就会造成无法通知操作系统调度其他线程,这个是灾难性的。

(2) 抢占式调度

由操作系统为每个线程分配操作时间,并且由操作系统自主负责调度其他线程。

cpu处理整个过程被分成若干个极小的时间片段,每个线程可以被分配多个时间片段。然后操作系统会按照分配情况进行调度。

这种方式可以让所有的线程看起来是同时运行的,即便有一条线程出了问题,也不会影响其他线程。

主流的平台linux的内核线程都是抢占式调度,这种方式的缺点就是接下来要说的上下文切换。

4.线程上下文切换

当处理器要运行线程时,除了运行代码外还要有上下文数据的支撑。而这里说的“上下文”,以程序员的角度来看,是方法调用过程中的各种局部的变量与资源;以线程的角度来看,是方法的调用栈中存储的各类信息;而以操作系统和硬件的角度来看,则是存储在内存、缓存和寄存器中的一个个具体数值。

物理硬件的各种存储设备和寄存器是被操作系统内所有线程共享的资源,比如线程A正在运行,线程B正在挂起等待运行,此时线程A的时间片段运行完毕了,线程B就会被调度。从线程A切换到线程B去执行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,这样线程B被重新激活后才能仿佛从来没有被挂起过。这种保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中的来回拷贝,这便是上下文切换,同时这个操作不是一个轻量级的操作,你想每个时间片段是很短的,也就意味着短时间内要做大量的上下文切换,这就一定会造成很大的时间消耗。

说到这里,java开发中的线程是java线程和内核线程一比一映射的1:1线程模型,一般java运行在linux系统,所以java开发中常遇到的性能影响点是”用户态转换内核态和上下文切换“。

5.java线程

java中的线程类是Thread,它定义了java层面线程创建,线程操作的一些列方法。

java中线程实现有三种方式:

//1.继承thread
Student extends Thread
Student xiaoming = new Student("小明",punishment);
xiaoming.start();

//2.实现Runnable
Student implements Runnable
Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
xiaoming.start();

//3.任务FutureTask实现
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception{
return "hello";
}
}
public static void mAIn(String[] args) {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask ());
new Thread(futureTask).start();
try{
String result=futureTask.get();
}catch(){
e.printStackTrace();
}
}
  • 第一种方式继承Thread类,所以子类也是一个Thread类,这样子类是一个携带任务的线程类,任务和线程牢牢绑在一起不可分割,且子类继承Thread类,就再也不能继承其他类。
  • 第二种方式,Runnable是任务类,子类实现Runnable后依然是一个任务类,而且还能实现其他的类,这种方式中任务和线程隔离,一个任务可以被多个线程执行。符合java职责分离的设计原则和面向接口编程的原则。

以上两种方式均没有返回值,而第三种方式可以支持返回值,后面我们细讲FutureTask。

我们来解析一下上面的前两种方式:

Thread是java中的线程实体类,Runable是任务实体类,他们两个的关系为,Runable代表任务,Thread代表任务的载体,以上面的例子来说,Thread就是企业内的工作人员,而Runable是具体的加工任务。

public interface Runnable {
    public abstract void run();
}

Runnable是一个接口,run是任务方法,自定一个类并实现Runnable类,然后重写run方法定义具体的任务逻辑,这个类就是一个具体的任务类。

任务类自身不能运行,需要依赖载体。

public class Thread implements Runnable {

     private Runnable target;
     
     public Thread() {
     }
     
     public Thread(Runnable target) {
        this.target = target;
     }
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    
    
 public synchronized void start() {
       
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        
      group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
              
            }
        }
    }   
    
    
}

Thread线程作为任务的载体,通过实现Runnable而具备携带任务的能力,通过源码可知,这个任务逻辑可以来自于自身,也可以来自外界传进来的任务实体Runnable。

Thread线程除了可以携带任务,更重要的是能够驱动任务运行,start方法就是启动开关,其内部是本地方法,由jvm内部实现,不难想象这个方法的底层一定是调用了内核线程api,与内核线程做映射,然后jvm回调java中的run方法。

6.java线程状态

java中的线程具有六种状态。

  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
  • 等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线唤醒。一般在调用wait方法和sleep方法的时候处于这种状态。
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

7.java 线程 API

睡眠
public static native void sleep(long millis);
获取当前线程对象
public static native Thread currentThread();
让出cpu
public static native void yield();
设置优先级
public final void setPriority(int newPriority);
阻塞等待执行完成
public final void join();
设置为守护线程
public final void setDaemon(boolean on);
让可中断方法中断
public void interrupt();
获取中断位状态
public static boolean interrupted();
获取中断位状态,并重置
public boolean isInterrupted();

这些方法有一些不是很常用,所以了解即可,重点说一下join,interrupt,interrupted,isInterrupted。

join方法是一个成员方法,调用某个线程的join方法,会让主线程一直处于wait状态(join方法底层源码中通过不断调用wait方法实现),等待这个线程处理完成再继续执行下去。

interrupt是一个成员方法,这个方法会中断静止线程,而运行中的线程只能被设置为可中断标志位为true,怎么理解呢?就是说如果线程处于sleep、wait状态,这个时候调用interrupt会把这个阻塞状态唤醒并抛出异常,之后该线程继续运行,如果线程正在运行中(非Waiting状态),此时调用interrupt只会给线程的可中断状态设置为true,对线程运行不会有任何影响。

调用某个线程的interrupt方法,给线程设置可中断状态的意义是什么呢?因为线程运行过程中可能正在处理数据,这个时候如果人为中断线程是不安全的,所以要等到线程处于一个安全位置中断才比较合理,那什么时候才是安全的位置呢,这个程序员自己决定,当处于安全位置的时候给一个可中断标示,这样中断的时候判断一下可中断状态再决定是否中断即可。juc源码中随处可见该方法的应用。

上面说了中断的时候判断一下可中断的状态,interrupted这个方法就是获取线程的中断状态,而isInterrupted方法也是获取线程的可中断状态,不同点在于isInterrupted方法在获取中断状态后,会顺便把中断状态重置为false。



Tags:线程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Search: 线程  点击:(11)  评论:(0)  加入收藏
C++多线程编程:解锁性能与并发的奥秘
今天我们将深入探讨C++中的多线程编程,揭示多线程如何解锁性能潜力,提高程序的并发性能。什么是多线程?在计算机科学中,多线程是指一个进程(程序的执行实例)中的多个线程同时执行...【详细内容】
2024-02-03  Search: 线程  点击:(69)  评论:(0)  加入收藏
C# 线程本地存储为什么线程间值不一样
为什么用 ThreadStatic 标记的字段,只有第一个线程拿到了初始值,其他线程都是默认值,让我能不能帮他解答一下,尼玛,我也不是神仙什么都懂,既然问了,那我试着帮他解答一下,也给后面类...【详细内容】
2024-01-26  Search: 线程  点击:(67)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  Search: 线程  点击:(115)  评论:(0)  加入收藏
深入理解与应用多线程技术
如果synchronized​作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM​使用monitorenter和monitorexit​两个指令实现同步;如果作用synchronized​作用于方...【详细内容】
2024-01-09  Search: 线程  点击:(82)  评论:(0)  加入收藏
在 Rust 编程中使用多线程
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 1:1 线程实现,这代表程序的每一个语言级线程使用一个系统线程。1. Rust线程...【详细内容】
2024-01-07  Search: 线程  点击:(77)  评论:(0)  加入收藏
深入掌握Java线程池调度策略,优化任务执行
在Java开发中,线程池是一种重要的并发处理机制。合理地使用线程池可以提高系统性能、响应速度和资源利用率。下面将深入掌握Java线程池的调度策略,介绍线程池的原理和常用的调...【详细内容】
2023-12-29  Search: 线程  点击:(76)  评论:(0)  加入收藏
Java 21 神仙特性:虚拟线程使用指南
虚拟线程是由 Java 21 版本中实现的一种轻量级线程。它由 JVM 进行创建以及管理。虚拟线程和传统线程(我们称之为平台线程)之间的主要区别在于,我们可以轻松地在一个 Java 程序...【详细内容】
2023-12-28  Search: 线程  点击:(107)  评论:(0)  加入收藏
响应式编程又变天了?看JDK21虚拟线程如何颠覆!
命令式风格编程一直深受开发者喜爱,如 if-then-else、while 循环、函数和代码块等结构使代码易理解、调试,异常易追踪。然而,像所有好的东西一样,通常也有问题。这种编程风格导...【详细内容】
2023-12-28  Search: 线程  点击:(100)  评论:(0)  加入收藏
三分钟理解 Java 虚拟线程
虚拟线程是 Java 语言中实现的一种轻量级线程,在 Java 项目中可以减少编写、维护和调试高吞吐量并发应用程序的工作量。有关虚拟线程的背景介绍,大家可以参阅 JEP 444。https:...【详细内容】
2023-12-27  Search: 线程  点击:(153)  评论:(0)  加入收藏
▌简易百科推荐
小红书、视频号、抖音流量算法解析,干货满满,值得一看!
咱们中国现在可不是一般的牛!网上的网友已经破了十个亿啦!到了这个互联网的新时代,谁有更多的人流量,谁就能赢得更多的掌声哦~抖音、小红书、、视频号,是很多品牌必争的流量洼地...【详细内容】
2024-02-23  二手车小胖说    Tags:流量算法   点击:(12)  评论:(0)  加入收藏
雪花算法详解与Java实现:分布式唯一ID生成原理
SnowFlake 算法,是 Twitter 开源的分布式 ID 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 ID。在分布式系统中的应用十分广泛,且 ID 引入了时间戳...【详细内容】
2024-02-03   一安未来  微信公众号  Tags:雪花算法   点击:(50)  评论:(0)  加入收藏
程序开发中常用的十种算法,你用过几种?
当编写程序时,了解和使用不同的算法对解决问题至关重要。以下是C#中常用的10种算法,每个算法都伴随着示例代码和详细说明。1. 冒泡排序 (Bubble Sort):冒泡排序是一种简单的比...【详细内容】
2024-01-17  架构师老卢  今日头条  Tags:算法   点击:(44)  评论:(0)  加入收藏
百度推荐排序技术的思考与实践
本文将分享百度在推荐排序方面的思考与实践。在整个工业界的推广搜场景上,特征设计通常都是采用离散化的设计,需要保证两方面的效果,一方面是记忆,另一方面是泛化。特征都是通过...【详细内容】
2024-01-09  DataFunTalk  微信公众号  Tags:百度推荐   点击:(73)  评论:(0)  加入收藏
什么是布隆过滤器?如何实现布隆过滤器?
以下我们介绍了什么是布隆过滤器?它的使用场景和执行流程,以及在 Redis 中它的使用,那么问题来了,在日常开发中,也就是在 Java 开发中,我们又将如何操作布隆过滤器呢?布隆过滤器(Blo...【详细内容】
2024-01-05  Java中文社群  微信公众号  Tags:布隆过滤器   点击:(87)  评论:(0)  加入收藏
面向推荐系统的深度强化学习算法研究与应用
随着互联网的快速发展,推荐系统在各个领域中扮演着重要的角色。传统的推荐算法在面对大规模、复杂的数据时存在一定的局限性。为了解决这一问题,深度强化学习算法应运而生。本...【详细内容】
2024-01-04  数码小风向    Tags:算法   点击:(89)  评论:(0)  加入收藏
非负矩阵分解算法:从非负数据中提取主题、特征等信息
非负矩阵分解算法(Non-negativeMatrixFactorization,简称NMF)是一种常用的数据分析和特征提取方法,主要用于从非负数据中提取主题、特征等有意义的信息。本文将介绍非负矩阵分解...【详细内容】
2024-01-02  毛晓峰    Tags:算法   点击:(62)  评论:(0)  加入收藏
再谈前端算法,你这回明白了吗?
楔子 -- 青蛙跳台阶一只青蛙一次可以跳上一级台阶,也可以跳上二级台阶,求该青蛙跳上一个n级的台阶总共需要多少种跳法。分析: 当n=1的时候,①只需要跳一次即可;只有一种跳法,即f(...【详细内容】
2023-12-28  前端爱好者  微信公众号  Tags:前端算法   点击:(107)  评论:(0)  加入收藏
三分钟学习二分查找
二分查找是一种在有序数组中查找元素的算法,通过不断将搜索区域分成两半来实现。你可能在日常生活中已经不知不觉地使用了大脑里的二分查找。最常见的例子是在字典中查找一个...【详细内容】
2023-12-22  小技术君  微信公众号  Tags:二分查找   点击:(78)  评论:(0)  加入收藏
强化学习算法在资源调度与优化中的应用
随着云计算和大数据技术的快速发展,资源调度与优化成为了现代计算系统中的重要问题。传统的资源调度算法往往基于静态规则或启发式方法,无法适应动态变化的环境和复杂的任务需...【详细内容】
2023-12-14  职场小达人欢晓    Tags:算法   点击:(164)  评论:(0)  加入收藏
站内最新
站内热门
站内头条