Java 线程(1)- 创建与销毁 - Go语言中文社区

Java 线程(1)- 创建与销毁


Java 采用 thread-per-task 的线程模型,即一个任务(一段代码)对应一个 Java 线程(thread),而一个 Java 线程对应一个操作系统线程,所以了解一些操作系统进程的管理知识可以更好的了解 Java 线程,下面以 Liunx 为例来分析 Java 线程

Liunx 的进程管理

Linux 中的进程(Process/Task)是正在执行的程序代码集合,是内核的最小资源调度单位,各个进程之间的资源相互隔离,Linux 通过虚拟处理器和内存,使得进程感觉自己在独享处理器和内存。而线程(Thread)是最小的计算单位,各个线程之间共享资源,理论上线程由进程产生,一个进程由若干线程组成。

Linux 中,内核不区分进程与线程,只不过使用不同的创建方式,进程由 fork()exec() 系统调用创建,而线程由 clone() 系统调用创建,并且通过指定 clone 参数,与父进程共享内存空间,文件句柄,信号处理等(其本质还是进程)

POSIX thread (pthread) 的实现,在 2.6之前,Linux kernel 没有真正的 thread 支持,线程库都是通过 clone() 实现的。2.6 之后,由 NPTL(Native POSIX Thread Library) 实现,NPTL 除了使用 clone() 外,在内核中增加了 futex(fast userspace mutex) 用于支持处理线程之间的 sleep 与 wake。futex 是一种高效的对共享资源互斥访问的算法。NPTL是一个1×1 的线程模型,即一个用户线程对应一个内核进程。其他一些操作系统比如 Solaris 则是 MxN 的,M 对应创建的线程数,N 对应内核可以运行的实体。NPTL 创建线程的效率非常高。一些测试显示,基于 NPTL 的内核创建 10 万个线程只需要 2 秒,而没有 NPTL 支持的内核则需要长达 15 分钟

进程的数据结构

一个进程由结构 task_struct 表示,各进程通过双向列表联系起来,创建进程时由内核为 task_struct 分配内存(32bit 下为 1.7KB ),Linux 使用 PID 标识进程 ,其最大值决定了某一时刻系统最大的并发进程的数量

进程的状态迁移

process state

进程的创建与销毁

fork() // or exec(), clone()
wait4()
exit()

无论是 fork() 还是 exec(),都是通过调用 clone() 实现的,clone() 会调用 do_fork() 完成进程的创建,clone() 可以接受一系列参数来指明需要共享的资源

Linux 中的内核线程(ps 命令中中括号标识的进程)由内核直接创建,没有独立的地址空间,只在内核空间运行。 而用户进程(执行一个可执行文件)先由 fork() 通过复制当前进程创建一个子进程,调用 fork() 的进程称为父进程,新产生的进程称为子进程。当 fork() 调用结束,在返回点上父进程恢复执行,子进程开始执行,fork() 采用写时复制,初始时父进程与子进程只有 PID 不同,然后由 exec() 读取可执行文件并将其载入地址空间开始运行(替换子进程),父进程可以通过 wait4() 系统调用(wait/waitpid)查询子进程是否终止。

如果父进程在子进程之前结束(没有 wait 其子进程),子进程就可能变为僵尸进程,导致进程描述符所占的空间无法释放,现在的内核(2.6)中如果子进程的父进程提前结束,内核会为该子进程查找一个养父进程,内核先在同组进程中找,如果找不到则由 init 进程充当,然后由其养父进程释放该进程,因为 init 进程一定会 wait 其子进程,所以可以认为该进程最终一定会被释放而避免产生僵尸进程

与进程相关的内核参数

ulimit -u # max_user_processes
/proc/sys/vm/max_map_count # max_map_count
/proc/sys/kernel/threads-max # max_threads
/proc/sys/kernel/pid_max # pid_max

查看系统的线程信息

/proc/<pid>/status
/proc/<pid>/sched
/proc/<pid>/task

# ruser 用户 ID
# lwp (light weight process) 线程ID
# psr 为 CPU 的序号
ps -eo ruser,pid,ppid,lwp,psr -L 

创建 Java 线程

当我们调用 new Thread() 时,JVM 并不会立即创建一个与其对应的系统线程,而是当调用了 start() 方法之后,JVM 才会通过系统调用 clone 来创建一个与其对应的系统线程(参考 pthread_create()),因为 Java 线程最终被映射为系统线程,所以当我们需要创建线程时,尤其是需要大量线程时,我们需要注意:

  • 操作系统对线程的数量的限制
  • 创建、调度和终止线程的系统开销
  • 线程本身对系统资源的消耗(尤其是内存,JVM 需要为每个线程维护一个独立的线程栈 -Xss<size>

如果 JVM 无法创建线程,会抛出 java.lang.OutOfMemoryError: unable to create new native thread 异常

由于线程不可以无限制的使用,所以利用线程池(Executor Framework)对线程进行复用和管理是常见的使用线程的方式

go 语言则采取了另外一种方式构建线程:goroutine,goroutine 构建在系统线程的基础之上,与系统线程的关系是 m:n,即在 n 个系统线程(GOMAXPROCS)上多工的调度 m 个 goroutines,因此 goroutine 使用了非常小的调用栈(2KB)并且缩短了线程之间的调度(切换)时间

创建本地进程(native process)

Java 通过 ProcessBuilder.start() (推荐)或 Runtime.exec 方法创建本地进程(即执行外部程序)并返回 Process 实例,该实例可用于控制进程并获取进程的相关信息,同时通过该实列还可以操作进程的输入输出、等待进程完成、检查进程的退出状态以及销毁(终止)进程。因为 Java 本地进程是和平台相关的,因此在使用时需要注意的地方包括:

  • 即使没有任何对当前本地进程 Process 对象的引用,本地进程也不会被终止,而是继续异步的执行
  • 在本地进程退出之前,无法获取进程的退出状态,需要通过 Process.waitFor() 来等待外部程序的退出
  • 创建的本地进程(子进程)没有自己的终端或控制台(Java7 之后可以通过 ProcessBuilder.inheritIO() 将子进程的标准 I/O 和当前的 Java 进程设置成一样),本地进程的所有标准 I/O(stdinstdoutstderr)操作以流的方式被重定向到当前 Java 进程(父进程),在当前 Java 进程中可以通过 Process.getOutputStream()Process.getInputStream()Process.getErrorStream() 来获得这些流,然后父进程使用这些流向子进程提供输入并从子进程获取输出。由于某些本地平台仅为标准输入和输出流提供有限的缓冲区大小,因此无法及时写入输入流或读取输出流,这有可能导致子进程阻塞甚至死锁,因此需要立即处理来自本地进程的输入和输出,即在 ProcessBuilder.start() 之后,立即启动线程处理标准 I/O
  • 不能像命令行一样在本地进程中使用管道,如果需要使用管道,需要用 shell 对其包装,如:new ProcessBuilder("/bin/sh", "-c", "ls -l| grep foo");

终止 Java 线程

Java 中,一个线程没有办法直接终止另一个线程,只能通过发送中断请求(或信号)来请求其它线程终止,而接收到请求的线程可能立即退出,也可能不做任何响应,所以一个线程只会在以下情况下退出

  • 线程的 run 方法退出,包括正常退出和异常退出
  • 线程响应了中断请求(或终止信号),退出 run 方法
  • 线程所在的 JVM 关闭

对于异常退出的线程,记录异常信息对以后的调试和分析都非常重要,因此要尽可能的记录线程的异常信息

public void run() { 
    Throwable thrown = null; 
    try { 
        while (!isInterrupted()) 
            runTask(getTaskFromWorkQueue()); 
    } catch (Throwable e) { //cath unexpected exception
        thrown = e; 
    } finally { 
        // To call uncaughtException method to record the exception
        threadExited(this, thrown); 
}

public void uncaughtException(Thread t, Throwable e) { 
    Logger logger = Logger.getAnonymousLogger(); 
    logger.log(Level.SEVERE,  t.getName(), e); 
}

中断(Interrupt)机制

Java 的中断机制是一种通知机制,通过中断并不能直接终止另一个线程,而只是给另一线程发送了中断请求(改变线程的中断状态),该线程自己处理该中断请求,可能立即退出,也可能不做任何响应。

中断状态

Java 每个线程都维护着一个中断状态(interrupted status),该状态代表着是否有中断请求,该状态可以被任意线程设置,包括被中断的线程本身(除了线程本身正在中断中)

最佳实践:线程的中断状态应该由线程的创建者来设置

线程中与中断状态相关的方法:

中断使用的场景

  • 某个操作超过了一定的执行时间限制需要中止时
  • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时
  • 一组线程中的一个或多个出现错误导致整组都无法继续时
  • 当一个应用或服务需要停止时

中断的处理

想要处理中断,首先需要监测中断状态,可以不断的通过调用 interruptedisInterrupted 来测试线程的中断的状态,但需要根据实际情况来决定测试频率,频繁的测试可能会使程序执行效率下降,相反,测试的较少又可能导致中断请求得不到及时响应。

当监测到中断状态后,需要选择在合适的时机处理中断,合适的时机并不一定是立即处理终端退出程序,该时机也需要根据实际情况来决定,主要是要避免线程所处理对象处于不一致状态。

当时机合适就可以处理中断,比如:回滚当前事务、清理资源、清理中断标志,抛出 InterruptedException 或立即退出。

InterruptedException 的处理

Java API 中的阻塞方法一般都会声明抛出 InterruptedException 异常(代表该方法是可中断的,即会响应中断请求),Java 的阻塞线程通过清除中断状态并抛出 InterruptedException 来响应中断

如果应用程序捕获到了 InterruptedException 异常,则说明当前的线程调用的阻塞方法发生了中断,当前线程可以:

  • 选择退出或结束

  • 继续向方法调用栈的上层抛出该异常

  • 捕获可中断方法的 InterruptedException 并设置中断状态,表明当前线程已经中断

    /*Non-cancelable Task that Restores Interruption Before Exit.*/
    public Task getNextTask(BlockingQueue<Taskgt; queue) { 
        boolean interrupted = false; 
        try { 
            while (true) { 
                try { 
                    return queue.take(); 
                } catch (InterruptedException e) { 
                    interrupted = true; 
                    // fall through and retry 
                } 
            } 
        } finally { 
            if (interrupted) 
                Thread.currentThread().interrupt(); 
        } 
    } 
    

关闭 JVM

JVM 的关闭方式包括:

  • 通过调用 System.exit() 或键入 Ctrl-C(等同于 kill -2,向 JVM 发送 SIGINT 信号)的标准关闭
  • 通过 kill 命令的其它参数的关闭(abrupt shutdown)

在 JVM 关闭的时候可以通过 Shutdown Hooks (Runtime.addShutdownHook)来执行一些清理工作,在使用 addShutdownHook 时需要注意:

  • 如果有多个 hook,其执行是并发的,即无法确定 hook 的执行顺序
  • 使用 abrupt shutdown 时,hook 不会执行

Java 中有两类线程:用户线程 (User Thread) 和守护线程 (Daemon Thread),守护线程是指在程序运行时在后台提供一种通用服务的线程,比如垃圾回收线程,这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反之,只要任何非守护线程还在运行,程序就不会终止。用户线程和守护线程几乎没有区别,只是在 JVM 关闭的时候,JVM 不会为守护线程做更多的工作,如:守护线程剩下的代码不会执行

Java 线程的生命周期

thread state

线程的 runnable 状态仅代表线程在 JVM 中开始执行,但从系统来看,该线程可能真的在运行,也可能在等待别的资源

参考

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/70b1f6ed8ced
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-12 12:57:44
  • 阅读 ( 993 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢