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 中开始执行,但从系统来看,该线程可能真的在运行,也可能在等待别的资源

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,096评论 0 23
  • 线程 操作系统线程理论 线程概念的引入背景 进程 之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有...
    go以恒阅读 1,637评论 0 6
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,114评论 0 23
  • 文/tangsl(简书作者) 原文链接:http://www.jianshu.com/p/2b993a4b913e...
    西葫芦炒胖子阅读 3,755评论 0 5
  • 立志夷简,神清龆龀之年,体拔浮华之世。凝情定室,匿迹幽巖。
    林间风月阅读 735评论 0 0