栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

初识Java多线程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

初识Java多线程

文章目录
  • 一、进程与线程
  • 二、线程模型
    • 2.1、一对一模型
    • 2.2、多对一模型
    • 2.3、多对多模型
  • 三、内核态与用户态
  • 四、线程的创建函数
  • 五、线程的状态
    • 5.1、start() 与 run() 方法
    • 5.2、sleep() 与 yield() 方法
    • 5.3、join() 方法
    • 5.4、setDaemon() 守护线程
  • 六、线程的优先级

一、进程与线程

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。

进程是程序在计算机上的一次执行活动。当运行了一个程序,就启动了一个进程。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。


线程是CPU进行调度的最小单位,必须依赖于进程而存在,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程无处不在,任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程,Java Web开发里面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制,远程访问接口RMI等,任何一个监听事件,onclick的触发事件等都离不开线程和并发的知识。


二、线程模型

在Java中经常说的线程(Thread)实际上应该叫作“用户线程”,而对应到操作系统,还有另外一种线程叫作“内核线程”。 用户线程和内核线程之间必然存在某种关系,多对一模型、一对一模型和多对多模型。

2.1、一对一模型

即一个用户线程对应一个内核线程,内核负责每个线程的调度。

  • 优点: 比如JVM几乎把所有对线程的操作都交给了内核,实现线程模型的容器(JVM)简单,所以我们经常听到在Java中使用线程一定要慎重就是这个原因。
  • 缺点: 对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换;内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。
2.2、多对一模型

多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。

  • 优点:
    用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换,使线程的创建、调度、同步等非常快。
  • 缺点:
    由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行;内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等。
2.3、多对多模型

又叫作两级线程模型,它充分吸收前面两种线程模型的优点且尽量规避它们的缺点。在此模型下用户线程与内核线程是多对多(M : N,通常M >= N)的映射模型。

  • 优点:
    兼具多对一模型的轻量,并且由于对应了多个内核线程,则一个用户线程阻塞时,其他用户线程仍然可以执行,可以实现较完整的调度、优先级等。
  • 缺点:
    实现比较复杂。

三、内核态与用户态

Intel X86 中将 CPU 指令权限划分为了 4 个执行级别 Ring0-Ring3,Linux只使用了其中的 Ring0级 和 Ring3 级分别来表示内核态和用户态,所谓的内核态和用户态其实仅仅是CPU的一个权限而已。


例如在内存分配中,有一部分内存是仅为内核态使用的,用户态是不允许访问那些内存地址,每个进程只允许访问自己申请到的内存,且不允许访问外围设备。大多数时间各类程序都是执行在用户态下。

当用户态的程序需要向操作系统申请更高权限的服务时,就通过系统调用向内核发起请求。内核自然也会提供很多的接口来供调用,应用程序可以通过库函数来调用内核的提供的接口,而库函数就会发起系统调用,发起了系统调用之后,用户态就会切换成内核态去执行对应的内核方法。


用户态切换到内核态的3种方式:

  • 系统调用
  • 异常(这个异常不是Java当中的异常)
  • 外设中断

用户态相较于内核态有较低的执行权限,很多操作是不被操作系统允许的,原因简单来说就是用户态出现问题(程序中的bug),也不能影响操作系统的运行。


其实站在Java程序员的角度只需要关注系统调用,因为系统调用可以认为是用户进程主动发起的,比如调用线程的 park() 方法会对应到一个OS的一个函数,从而使当前线程进入了内核态;再比如遇到 synchronized 关键字,如果是重量锁则会调用 pthread_mutex_lock 这样我们的线程也会切换到内核态;当执行完系统调用切换到用户态。


四、线程的创建函数

通过上述线程模型的介绍,知道Java是采用的一对一模型,所以Java代码中通过 Thread.start() 方法启动一个线程(让一个线程进入就绪状态等待CPU调度),其底层也是通过调用 JNI 去实现的。


可以看到这个方法最核心的就是调用了一个 start0() 方法,而该方法又是一个 native 方法,其实在Hotspot源码中是调用了 pthread_create 函数来启动线程的,所以说Java级别的线程其实就是操作系统级别的线程。


这里我们可以执行 man pthread_create 来查看下该命令的作用,如下:

根据man配置的信息可以得 pthread_create 会创建一个线程,这个函数是linux系统的函数,可以用C或者C++直接调用,上面信息也告诉我们该函数在 中, 该函数有四个参数:

参数名称参数定义参数解释
pthread_t *thread传出参数,调用之后会传出被创建线程的ID定义 pthread_t pid; 继而取地址 &pid
const pthread_attr_t *attr线程属性,关于线程属性是linux的知识一般传null,保持默认属性
void (start_routine) (void *)线程的启动后的主体函数需要你定义一个函数,然后传函数名即可
void *arg主体函数的参数没有可以传null

五、线程的状态

线程是有多种状态,其关系主要如下图所示:

这里先简单介绍如下几个方法,其余方法后续会进行详细介绍。

5.1、start() 与 run() 方法
  • start() 方法让一个线程进入就绪队列等待CPU调用,分配到CPU后才调用实现的 run() 方法,start() 方法不能重复调用,如果重复调用会抛出异常(会校验 threadStatus 的值)。

  • run() 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用,同步执行。

5.2、sleep() 与 yield() 方法
  • sleep() 方法会使线程进入阻塞状态,等待休眠时间达到,再进入就绪状态等待CPU调用,需要注意的是 sleep() 是不会释放锁资源的。

  • yield() 方法会立即进入就绪状态,有可能在进入到就绪状态后会被操作系统再次选中马上又被运行,同样 yield() 也不会释放锁资源。

5.3、join() 方法

join() 方法的作用,是等待这个线程结束。


多个线程之间是没有先后顺序的,但是如果我们希望线程A执行结束之后,我们线程B再执行,这时就可以用 join() 方法来解决。比如说可以在 main() 主线程内,等待其它线程完成再结束 main() 主线程。


5.4、setDaemon() 守护线程

守护线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。可以通过调用 thread.setDaemon(true) 将线程设置为守护线程。


当我们启动一个main()方法时,就会创建很多的守护线程,可以通过 ThreadMXBean 接口打印出来,其中除main线程外都是守护线程(包括GC线程),都会随我们的main线程的终止而终止。

public class App {
    public static void main(String[] args) {
        // ThreadMXBean虚拟机线程管理的接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(threadInfo.getThreadId() + "..." + threadInfo.getThreadName());
        }
    }
}


在我们的使用守护线程时,也是一个比较重要的点,需要注意是,finally() 方法不一定会被执行,如下:


六、线程的优先级

在Java线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int) 方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

但是我们非常要注意的问题时,我们在设置了线程的优先级时,是不可靠的,比如我们把一个线程的优先级设置最高时,它不一定每次都是优先执行的,它只是优先执行的概率比较高,并不一定的,所以不能依赖线程的优先级来进行设置。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/930472.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号