Java线程

  • 线程是比进程更轻量级的调度执行单位,CPU调度的基本单位就是线程。
  • 线程的引入,将一个线程的资源分配和执行调度分开。
  • 各个线程既可以共享进程资源(内存地址、文件I/O等),又可独立调度。

概念

线程的生命周期状态

通用的线程生命周期

首先,通用的线程生命周期模型将线程的状态分为了以下五种:

  • 初始状态

    • 线程仅仅在编程语言层面被创建,在操作系统中并没有被创建,因此还不能被分配CPU资源。
    • 相当于现在只是在Java中new了个Thread对象,还没调用start()方法。
  • 可运行状态

    • 真正的操作系统线程此时已经成功被创建,线程已经可以被分配CPU资源了。
  • 运行状态

    • 当有空闲的CPU资源时,操作系统会将其分配给一个处于可运行状态的线程,可运行状态的线程一旦被分配的CPU,它的状态将变为运行状态。
  • 休眠状态

    • 运行状态的线程如果调用了某个阻塞式API(如以阻塞方式读文件),那么这个线程将变为休眠模式,并放弃自己的CPU使用权;
    • 当它的阻塞状态结束了,它的状态会变为可运行状态,等待再次被分配CPU资源。
  • 终止状态

    • 当线程执行完成或出现异常,它就会进入终止状态,这是一个终态(只进不出的饕餮状态),就是挂了。

Java中线程的生命周期

主要有三条链路:

  • RUNNABLE -> BLOCKED/WAITING/TIME_WAITING
  • NEW -> RUNNABLE
  • RUNNABLE -> TERMINATEd

线程的生命周期状态转换

可运行/运行的状态 -> 休眠状态

  • RUNNABLE -> BLOCKED

    • 线程等待synchronized的隐式锁时,触发该状态转换。
  • RUNNABLE -> WAITING

    • 已获取synchronized隐式锁的线程,调用无参数的Object.wait()方法。

    • 调用无参数的Thread.join()方法。

      • 因为Thread.join()其实就是通过调用线程对象本身的wait(0)方法实现的。
    • 调用LockSupport.park()方法。

  • RUNNABLE -> TIME_WAITING

    • 调用带超时参数的Thread.sleep(long millis)方法。
    • 获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法。
    • 调用带超时参数的Thread.join(long millis)方法。
    • 调用带超时参数的LockSupport.parkNanos(Object blocker, long deadline)方法。
    • 调用带超时参数的LockSupport.parkUntil(long deadline)方法。

LockSupport对象说明:

Java并发包中的锁,都是基于该对象实现的,使用方法如下:

  • 调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。

初始状态 -> 可运行/运行的状态

Java刚创建出来的Thread thread对象就是NEW状态,调用了thread.start()方法后,线程就进入了RUNNABLE状态。

可运行/运行的状态 -> 终止状态

  • 线程执行完run()方法后,会自动切换到TERMINATED状态。
  • 线程执行完run()方法后,有异常抛出,线程也会被终止。

如何强制中断run()方法的执行?

当run()方法中调用了一个耗时很长的方法时,我们等的不耐烦了,此时我们需要强制中断run()方法的执行。

在Java的Thread中,倒是给我们提供了一个stop()方法,不过该方法已经被标记为@Deprecated的了。不推荐它的原因是因为它太危险了,stop()方法不会给线程任何处理后事的机会,直接就杀掉线程,如果此时线程正好持有ReentrantLock锁,它被干掉后会导致这个锁永远不会被释放。

一种优雅的方式是中断,即调用interrupt()方法,这个方法并没有做什么实质上的事情,它相当于只是给线程打上了一个标记,而后我们通过一些手段(如调用Thread.interrupted方法)来检测当前线程是否被打上了中断标记,来决定如何终止线程。

scss 复制代码
if (Thread.interrupted()) { // Clears interrupted status!
    // do something like lock.unlock()
    throw new InterruptedException();
}

使用

如何在Java中使用多线程

继承Thread类

scala 复制代码
// 自定义线程对象
class MyThread extends Thread { 
    public void run() { 
        // 线程需要执行的代码
    }
}

// 创建线程对象并启动线程
MyThread myThread = new MyThread();
myThread.start();

Java中的Thread类中,所有关键方法都是native的,说明这些方法无法使用平台无关的手段实现。

实现Runnable接口

java 复制代码
// 实现 Runnable 接口
class Runner implements Runnable {
    @Override
    public void run() {
        // 线程需要执行的代码
    }
}

// 创建线程对象并启动线程
Thread thread = new Thread(new Runner());
thread.start();

实现Callable接口

Callable的使用需要搭配线程池,放在后续介绍线程池部分。

线程数配置原则

性能一般指:延迟吞吐量,目标是降低延迟,提高吞吐量。

  • 延迟:发出请求到收到响应这个过程的时间。
  • 吞吐量:单位时间内能处理请求的数量。

一般常用的手段:

  • 优化算法
  • 将硬件的性能发挥到极致

在并发编程领域,提升性能本质上就是提升硬件的利用率。也就是说,我们的目标是让CPU时刻保持着100%的利用率,一刻也不停歇的工作着。

然而,线程也不是越多越好的,当一个CPU上同时有多个线程运行时,我们所看到的多个线程的并行运行其实是一种伪并行,在同一时刻,真正运行的线程其实只有一个,只不过CPU在多个线程的运行之间不停的切换,让我们看起来好像是这些个线程在同时运行罢了。然而,线程运行的切换不是没有代价的,每次切换时,我们首先需要保存当前线程的上下文,然后再将下一个线程的上下文设置好。这个过程也是要消耗CPU时间的,如果CPU将大量的时间都花在了切换线程上,而非执行线程的任务上,那就得不偿失了。

在线程切换中,上下文一般指CPU寄存器和程序计数器中的内容。

一般任务有以下两种类型:CPU密集型的任务I/O密集型的任务。本质区别为:

  • CPU密集型的任务:最佳线程数 = CPU核数 + 1

    • 大多数时间里,只要在运行就有产出。
    • 因此希望一个任务一直运行到底再运行下一个,而不是将时间耗费到线程的切换上(即上下文切换)。
    • 后面的"+1"是为了一旦线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,以保证CPU的利用率。
  • I/O密集型的任务:最佳线程数 = CPU核数 * [ 1 + (I/O耗时 / CPU耗时)]

    • 一个任务从开始到完成的时间可能很长,但其间真正在干活(使用CPU)的时间可能很短,大部分时间都在等待,如等待网络发来的数据包,或等待写入或读取磁盘上的数据等。

    • 因此希望在没有产出的等待时间里,CPU不是闲呆着,而是去做其他事情。

    • 示例:如CPU计算和I/O操作的耗时是1:2,那多少个线程合适呢?答:3个线程。

      • 理想情况下,CPU在线程A、B、C之间按如下方式切换,理论上实现100%的CPU利用率。

线程间的通讯方式

选择通信

  • synchronized和volatile关键字

    • 这两个关键字可以保障线程对变量访问的可见性。
  • 等待/通知机制

  • Thread#join()

    • 如果一个线程A执行里threadA.join(),那么只有当线程A执行完之后,threadA.join()之后的语句才会继续执行,类似于创建A的线程要等待A执行完后才继续执行。

    • 使用join方法中线程被中断的效果 == 使用wait方法中线程被中断的效果,即会抛出 interruptedException。因为join方法内部就是用wait方法实现的。

    • join还有一个带参数的方法:join(long),这个方法就是等待传入的参数的毫秒数,如果计时过程中等待的方法执行完了,就接着往下执行,如果计时结束等待的方法还没有执行完,就不再继续等待,而是往下执行。

      • join(long)和sleep(long)的区别

        • 如果等待的方法提前结束,join(long)不会再计时了,而sleep(long)一定要等待够足够的毫秒数。
        • join(long)会释放锁,sleep(long)不会释放锁,原因是join(long)方法内部是用wait(long)方法实现的。
  • 管道流:PipedInputStream & PipedOutputStream

csharp 复制代码
 public class PipedStreamDemo {
     public static PipedInputStream in = new PipedInputStream();
     public static PipedOutputStream out = new PipedOutputStream();
 
     public static void send() {
         new Thread() {
             @Override
             public void run() {
                 byte[] bytes = new byte[2000];
                 while (true) {
                     try {
                         out.write(bytes, 0, 2000);
                         System.out.println("Send Success");
                     } catch (IOException e) {
                         System.out.println("Send Failed");
                         e.printStackTrace();
                     }
                 }
             }
         }.start();
     }
 
     public static void receive() {
         new Thread() {
             @Override
             public void run() {
                 byte[] bytes = new byte[100];
                 int len = 0;
                 while (true) {
                     try {
                         len = in.read(bytes, 0, 100);
                         System.out.println("len = " + len);
                     } catch (IOException e) {
                         System.out.println("Receive Failed");
                         e.printStackTrace();
                     }
                 }
             }
         }.start();
     }
 
     public static void main(String[] args) {
         try {
             in.connect(out);
         } catch (IOException e) {
             e.printStackTrace();
         }
         receive();
         send();
     }
 }

选择不通信

也可以选择不通信,将变量封闭在线程内部,使用ThreadLocal可以实现这一效果。

原理

线程的调度

  • 协同式线程调度:线程的执行时间由线程本身来实现控制,线程执行完自己的任务之后,主动通知系统切换到另一个线程。

    • 优点:实现简单,没有线程同步的问题。
    • 缺点:线程执行时间不可控,如果一个线程编写有问题一直无法结束,程序会一直阻塞在那里。
  • 抢占式线程调度:每个线程由系统分配执行时间,系统决定切不切换线程。

    • Java使用的线程调度方式就是这种。

线程的实现原理

三种线程的实现方式

使用内核线程实现

内核线程(KLT),就是直接由操作系统支持的线程,不过当然不是我们的程序可以直接去操作系统的进程,而是程序可以通过调用内核线程的一种高级接口------轻量级进程(LWP),来操作内核进程。也就是说,LWP和KLT之间是1:1的关系,因此我们也称这种模型为一对一的线程模型。

这类似于一种代理模式,LWP就是代理对象,而KLT则是被代理对象,我们把任务请求发送给代理人LWP,然后LWP会通过调用真实具备执行能力的被代理人KLT去执行任务。

  • 优点:

    • 每个LWP都是一个独立的调度单元,即使有一个LWP在调用过程中阻塞了,也不会影响到整个进程继续工作,系统的稳定性会比较好。
    • 线程的调度和各种操作都委托给了操作系统,所以实现简单。
  • 缺点:

    • 各种线程操作(创建、析构、同步等)都需要进行系统调用,而系统调用的代价较高,需要在用户态和内核态中来回切换,这会消耗掉一些时间。
    • 每个LWP都需要一个KLT支持,也就是说,每个LWP都会消耗掉一部分内部资源(内核线程和栈空间),因此系统可以支持的LWP数量是有限的。

使用用户线程实现

狭义上,用户线程(UT)指的是完全建立在用户空间的线程,即操作系统是感知不到线程的存在的,它只知道那个掌管这些UT的进程P。因此,进程和UT之间的比例为1:N。

  • 优点:

    • UT的创建、同步、销毁、调度都是在用户态完成的,完全不需要切换到内核态,因此各种线程操作可以是非常快速和低消耗的。
    • 由于进程和UT之间的比例为1:N,所以可以支持更大规模的UT数量。
  • 缺点:

    • 由于没有系统内核的支持,所以所有的线程操作都需要自己实现,这就使得UT的实现程序一般都比较复杂,而且事实证明,我们很难实现的比操作系统好。

      • 现在使用UT的程序越来越少,Java和Ruby等语言都曾使用过UT,最后都放弃了。

使用用户线程加轻量级进程

这种模式下,即存在用户线程,也存在轻量级进程。

  • UT还是只存在于用户空间,因此线程的创建、同步、销毁的消耗依旧很小,同时也可以支持很多线程并发。
  • 对应线程的调用,则通过LWP作为UT和KLT之间的桥梁,这样就可以使用操作系统提供的线程调度功能和处理器映射了。
  • UT的系统调用要通过LWP完成,大大降低了整个进程被完全阻塞的风险。
  • UT和LWP之间的比例是不确定的,即为N:M的关系。

Java线程的实现

JDK1.2之前,Java线程是基于名为Green Thread的用户线程实现的,JDK1.2之后,被替换为基于操作系统原生线程模型来实现。对于目前的JDK版本,这将取决于操作系统支持怎样的线程模型,虚拟机规范中并没有规定Java线程必须是要哪种线程模型来实现。

相关推荐
hanbarger7 分钟前
mybatis框架——缓存,分页
java·spring·mybatis
cdut_suye15 分钟前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
苹果醋327 分钟前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行28 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园31 分钟前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10431 小时前
java web springboot
java·spring boot·后端
smile-yan1 小时前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风1 小时前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~1 小时前
Maven极简安装&配置-241223
java·maven
皮蛋很白1 小时前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea