并发编程——线程

线程

介绍

对于计算机来说每一个任务就是一个进程(Process),在每一个进程内部至少有一个线程(Thread)是在运行中,有时线程也称之为轻量级进程。

每一个线程都有自己的局部变量表、程序计数器(指向正在执行的指令指针)以及生命周期。

启动一个程序:

  1. main线程
  2. 守护线程(垃圾回收线程以及RMI线程等)

生命周期

线程的生命周期

  1. NEW
  2. RUNNABLE
  3. RUNNING
  4. BLOCKED
  5. TERMINATED

详解:

NEW状态:当我们用new关键字创建一个Thread对象时,当前不处于执行状态,因为没有调用start方法,准确的说,他只是一个Thread的一个对象,与普通new出来的对象没什么区别。

NEW状态需要通过start方法进入到RUNNABLE状态。

RUNNABLE状态:线程进入到RUNNABLE状态之后,一定会执行么,不一定,因为线程是否运行需要等待CPU的调度。

RUNNABLE只能意外终止或者是进入RUNNING状态。

RUNNING状态:一旦CPU通过轮询或者其他方式从任务可执行队列中选中线程,此时才真正开始执行自己的逻辑代码,需要注意的一点是一个正在RUNNING状态的线程可以称之为RUNNABLE,但是反过来不成立。

RUNNING状态转换:

  1. 直接进入到TERMINATED状态,通过调用JDK不推荐的stop方法和判断某个逻辑标识。
  2. 进入BLOCKED状态,调用sleep、wait方法,加入到waitSet中。
  3. 进行某个阻塞IO的操作,比如因为网络数据的读写进入BLOCKED状态。
  4. 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态。
  5. 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态。
  6. 线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态。

BLOCKED状态

BLOCKED状态转换:

  1. 直接进入TERMINTED状态,比如调用了JDK不推荐的stop方法或者意外死亡(JVM Crash)
  2. 线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态。
  3. 线程完成了指定时间的休眠,进入到RUNNABLE状态。
  4. Wait中的线程被其他线程notify/norifyall唤醒,进入RUNNABLE状态。
  5. 线程获取了某个锁资源,进入RUNNABLE状态。
  6. 线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态。

TERMINTED状态:最终状态,不会切换到其他状态,进入到该状态,意味着整个线程的生命周期结束了。

  1. 线程运行正常结束,结束生命周期。
  2. 线程运行出错意外结束。
  3. JVM Crash,导致所有线程都结束。

模板设计模式在Thread中的应用

Thread 重写run方法,不难看出,线程的真执行逻辑是在run方法中,通常,我们把run方法称为线程的执行单元。

Thread的run和start是一个典型的模板设计模式,父类编写算法结构代码,子类实现逻辑细节。

实现好处:程序结构由父类控制,并且是final修饰的,不允许被重写,子类只需要实现想要的逻辑任务即可。

Runnable

咱们经常提到的是创建线程是有四种方式

  1. 构造一个Thread
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 线程池

但是这种说法不严谨,在JDK中代表线程的只有Thread这个类,线程的执行单元是run方法,所以严格意义上来说,创建线程只有一种方式就是构造Thread类,而实现线程的执行单元有多种方式:重写Thread的run方法,实现Runable接口的run方法,并且将Runable实例用作构造Thread参数。

注意:Thread类的run方法时不能够共享的;Runnable接口则很容易通过不同的实例实现。

Thread构造函数

线程的父子关系

线程的最初状态为NEW。没有执行start之前,它仅仅是一个Thread实例,并不意味着一个新的线程被创建,在Thread构造方法中,不难发现,都会存在一个Thread parent = currentThread()方法,因此currentThread()代表的将会是创建它的那个线程,所以可以得到如下结论:

  1. 一个线程的创建肯定是由另一个线程完成的。
  2. 被创建线程的父线程是创建它的线程。

比如:main函数所在的线程是由JVM创建的,也就是main线程,那就意味着在main方法中的线程,其父线程都是main线程。

注意:如果想要理解程序的核心创建,不妨在看到一段代码之后,想一想它是由谁创建的,能够更好的理解有些代码的逻辑。

Thread和ThreadGroupo

在Thread构造函数的源码中:

if (g == null) {
    /* Determine if it's an applet or not */

    /* If there is a security manager, ask the security manager
       what to do. */
    if (security != null) {
        g = security.getThreadGroup();
    }

    /* If the security doesn't have a strong opinion of the matter
       use the parent thread group. */
    if (g == null) {
        g = parent.getThreadGroup();
    }
}

通过源码分析,可以看到,在Thread构造函数的源码中,并没有显示的创建ThreadGroup,子线程通常是被加入到父线程所在的线程组。

如果在构造函数的时候添加ThreadGroup组,则不会默认位父线程所在的线程组

  1. main线程所在的ThreadGroup称之为main
  2. 构造一个线程的时候如果没有显示地指定ThreadGroup,那么它将会和父线程同属于一个ThreadGroup。

当然,在默认设置中,除了父子线程同属于一个Gruop之外,还会有相同的优先级,同样的deamon。

Thread与JVM虚拟机栈

Thread与Stacksize

一般情况下,创建线程的时候不会手动的指定栈内存的地址空间字节数组,统一通过xss参数进行设置即可,jdk官方文档的描述,stacksize越大则代表着正在线程内方法调用递归的深度就越深,越小则代表创建线程数量越多,当然这个参数对平台的依赖性比较高,不同的操作系统、不同的硬件都会有所影响。而且某些平台下,该参数不会起到任何作用。

JVM内存结构

jvm在执行java程序的时候会把对应的物理内存划分为不同的内存区域,每个区域存放着不同的数据,也有不同的创建和销毁时机。

1 程序计数器

无论什么语言,最终的形式都是由操作系统通过控制总线向CPU发送机器指令。

程序计数器的作用:用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。

任何时候,一个处理器只执行其中的一个线程中的指令,为了能够在CPU时间片乱转切换上下文之后顺利的回到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之前互相不影响,因此jvm将此块内存区域设计成了线程私有的。

2 java虚拟机栈

java虚拟机栈也是线程私有的,它的生命周期与线程相同。

jvm运行时所创建的,在线程中,方法在执行的时候都会创建一个名为栈帧(stack frame)的数据结构。

栈帧主要用于存放:

  1. 局部变量表
  2. 操作栈
  3. 动态链接
  4. 方法出口等信息

方法的调用对应着栈帧在虚拟机栈中的压栈和弹栈过程。

每个线程在创建的时候,jvm都会为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-xss来配置。

也就是说,虚拟机栈的大小是固定的,那么如果局部变量表等信息占用的内存越小则可被压入的栈帧就会越多。相反,就会越少。

一般将栈帧的内存的大小称之为宽度,栈帧的数量称之为虚拟机栈的深度。

3 本地方法栈

java中提供了调用本地方法的接口(Java Natvice Interface),也就是C/C++程序。

JVM为本地方法所划分的内存区域便是本地方法栈,这块内存区域的自由度非常高,完全靠不同的JVM厂商来实现。

java虚拟机栈也是线程的私有内存区域。

4 堆内存

堆内存是java中最大的一块内存,被所有的线程所共享,java运行期间创建的所有对象几乎都在该内存区域,该内存区域也是垃圾回收期重点照顾的区域,有些时候堆内存也被称之为"GC堆"。

堆内存一般分为新生代和老年代,更细致的划分为Eden区、From Survivor区和To Survivor区。

5 方法区

方法区也是被多个线程所共享的内存区域,主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器(JIT)编译后的代码等数据。

方法区划分为持久代和代码缓存区。

代码缓存区只要存储编译后的本地代码(和硬件相关)以及JIT(Just In Time)编译器生成的代码,对于不同的JVM会有不同的实现。

6 java8 元空间

持久代内存被彻底删除,取而代之的是元空间。

jstat查看细节

1.7中的持久代内存区域,在1.8中,该内存被Meta Space取而代之了,元空间同样是堆内存的一部分,JVM为每个类加载器分配一块内存块列表,进行线性分配。

块的大小取决于类加载器的类型,sun、反射、代理对应的类加载器块会小一些,之前的版本会单独卸载回收某个类,现在的GC过程中发现某个类加载器已经具备回收的条件,则会将整个类加载器相关的元空间全部回收,减少内存碎片,节省GC扫描和压缩的时间。

Threa与虚拟机

在jvm中,可以创建多少个线程,与堆内存、栈内存的大小有直接的关系,只不过栈内存更加明显一些。

线程数量 = (最大地址空间(MaxProcessMemory)) - JVM堆内存 - ReservedOsMemeory) / ThreadStackSize(XSS)

守护线程

简述

守护线程是一类比较特殊的线程,一般用于处理后台的工作,比如jdk的垃圾回收线程。

设置守护线程的方式,调用setDaemon方法即可,true代表守护,false代表正常线程,必须线程启动之前设置。isDaemon是判断是否为守护线程。

线程是否是守护线程和它的父线程有很大关系,如果父线程是正常线程,则子线程也是,反之,同样。

如果一个jvm进程中没有一个非守护线程,那么jvm就会退出。也就是说守护线程具备自动结束生命周期的特性。

作用

用于执行一些后台任务,有时也被称之为后台线程,当你希望关闭某些线程的时候,或者退出JVN进程的时候,一些线程能够自动关闭,这个时候就可以考虑守护线程为你完成。

学习自:
《Java高并发编程详解——多线程与架构设计》 汪文君

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值