Java线程

本文深入探讨了线程和进程的基础知识,包括它们的定义、区别、通信方式以及线程同步互斥的机制。详细阐述了进程的五种状态,线程的创建、启动、调度以及Java线程的实现原理和生命周期。还介绍了Java线程的中断机制、等待唤醒机制以及线程间的通信方法,如volatile、wait/notify和LockSupport。
摘要由CSDN通过智能技术生成

线程基础知识

线程和进程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中 还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程。
  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。

线程

  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。

进程与线程的区别

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication)不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

进程间通信的方式

  • 管道(pipe)及有名管道(named pipe)
  • 信号(signal)信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
  • 消息队列(message queue)
  • 共享内存(shared memory)
  • 信号量(semaphore)主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
  • 套接字(socket)

线程的同步互斥

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
四种线程同步互斥的控制方法:
  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
  2. 互斥量:为协调共同对一个共享资源的单独访问而设计的。
  3. 信号量:为控制一个具有有限数量用户资源而设计。
  4. 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:

  1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
  2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
  3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

上下文切换只能在内核模式下发生。

内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序) 最初在用户模式下运行,但它们可以 通过系统调用运行部分内核代码
内核模式(Kernel Mode)
执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪。
 用户模式(User Mode)
在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。 在您的计算机上运行的大多数代码将在用户模式下执行。
应用程序一般会在以下几种情况下切换到内核模式:
  1.  系统调用。
  2.  异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
  3. 设备中断。

CAS不涉及到用户态到内核态切换,没有系统调用,CAS是比较与交换

上下文切换通常是计算密集型的。
就CPU时间而言,上下文切换对系统来说是一个巨大的成本,实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是 尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。
通过命令查看CPU上下文切换情况
vmstat 1 (其中cs列就是CPU上下文切换的统计。)
查看某一个线程\进程的上下文切换
使用pidstat pid
从进程的状态信息中查看
通过命令 cat /proc/pid/status 查看进程的状态信息

操作系统层面线程生命周期

初始状态、可运行状态、运行状态、休眠状态和终止状态。

Java 语言里则把可运行状态和运行状态合并了

查看进程线程的方法

windows

任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程

 linux

ps -ef 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程top 
按大写 H 切换是否显示线程
top -H -p <PID> 查看某个进程(PID)的所有线程

Java

jps 命令查看所有 Java 进程
jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

Java线程详解

Java线程的实现方式

  1. 使用 Thread类或继承Thread类
  2. 实现 Runnable 接口配合Thread
  3. 使用有返回值的 Callable
    class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            return 1;
        }}

  4. 使用 lambda

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

 Java线程实现原理

Thread#start()源码

线程的创建和启动总结:

  1. 使用new Thread()创建一个线程,然后调用start()方法进行java层面的线程启动
  2. 初始化时会执行静态代码块,执行本地方法添加对应表
  3. 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动
  4. 调用new JavaThread(&thread_entry,sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::creat_thread(thread),方法进行线程创建
  5. 新创建的线程状态为Initialized,调用了sync->wait()方法进行等待,等到被唤醒才继续执行thread->run();
  6. 调用Thread::start(native_thread);方法进行线程启动,此时线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式
  7. 线程启动之后状态设置为RUNNABLE,并唤醒等待的线程,接着执行thread->run()方法
  8. javaThread::run()方法会回调第一步new Thread中复写的方法 

Java线程属于内核级线程

依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

用户级线程

操作系统内核不知道应用线程的存在。

协程

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

Java线程的调度机制

Java线程调度就是抢占式调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程的生命周期

Java 语言中线程共有六种状态,分别是:

  • NEW(初始化状态)
  • RUNNABLE(可运行状态+运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态)

Thread常用方法

sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

yield方法

  1. yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
  2. 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
  3. 具体的实现依赖于操作系统的任务调度器

join方法

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

Java线程的中断机制

Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
interrupt(): 将线程的中断标志位设置为true,不会停止线程
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle

sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
sleep 期间的线程被中断, 线程是可以感受到中断信号的,并且会抛出 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

Java线程间通信

volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信

等待唤醒(等待通知)机制

wait和notify

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。

缺陷:只能在synchronized同步代码块里面使用,notify()只会唤醒一个等待线程,唤醒全部需要使用notifyAll()

LockSupport(AQS使用唤醒同步队列的线程)

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
优点:可以唤醒指定线程,任意场景都可使用,可以提前调用unpark 后续的park都会获取结果 不会阻塞

Thread.join

join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值