Java并发3:线程

269 阅读7分钟

本文是根据《Java并发编程的艺术》以及之前的文章 https://juejin.cn/post/6844903733273313293 和 https://juejin.cn/post/6844903734556753933 共同整理而来。

进程和线程

进程

进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是从系统进行资源分配和调度的一个独立单位。

线程

线程是一个比进程更小的执行单位,可以理解成是在进程中独立运行的子任务。一个进程在执行过程中可以产生多个线程,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换时,负担比进程小得多。

多线程

多线程就是指多个线程同时运行或交替运行。单核CPU是顺序执行,也即交替运行。多核CPU,因为每个CPU有自己的运算器,所以在多个CPU可以同时运行。

为什么要使用多线程

  1. 更多的处理器核心,使得多线程能分配到多个处理器核心,减少了程序的处理时间,提升了效率
  2. 更快的响应时间:将数据一致性不强的操作派发给不同的线程,从而缩短了响应时间
  3. 更好的编程模型

相关概念

线程优先级

Java线程中,通过整形变量priority来控制优先级,范围从1-10,创建线程时可以设置,默认为5。在不同的JVM和操作系统上,线程规划存在差异,有些操作系统甚至会忽略对线程优先级的设定。

同步和异步

同步方法调用开始后,调用者必须等待,直到方法调用返回后,才能继续后序的行为。可以理解为,排队执行。

异步方法调用像是一个消息传递,当一个异步过程调用发出后,调用者可以继续后序的操作,但是调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。多线程是异步的,其调用时机是随机的。

并发和并行

并发是指一个处理器同时处理多个任务。并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。

如上图所示,并发是逻辑上是同时发生。并行是物理上的同时发生。

多线程在单核CPU的话是顺序执行,也就是交替运行(并发)。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行(并行)。

阻塞与非阻塞

阻塞是指调用结果返回之前,当前线程会被挂起,只有在得到结果后才返回。非阻塞指不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

守护线程

Java中有两种线程,一种是用户线程,另外一种是守护线程。当进程中不存在非守护线程了,守护线程自动销毁,java虚拟机会退出。典型的守护线程就是垃圾回收线程。只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束,守护线程才随着JVM一同结束工作。当JVM退出时,守护线程中的finally块不一定会执行,不能依靠finally块中的内容确保执行关闭或者清理资源的逻辑。

线程状态转换

  • 新建(New):创建后尚未启动。

  • 可运行(Runnable):线程对象创建后,在调用它的start()方法,系统为此线程分配CPU资源,使其处于可运行状态,这是一个准备运行的状态。

    • 调用sleep()方法后经过的时间超过了指定的休眠时间。
    • 线程调用的阻塞IO返回,阻塞方法执行完毕。
    • 线程获得了试图同步的监视器
    • 线程正在等待通知,且其他线程发出了通知
    • 处于挂起状态的线程调用了resume恢复方法。
  • 运行(Running):在Runnable状态下的线程获得了CPU时间片,执行程序代码。

  • 阻塞(Blocking):线程因为某种原因放弃了CPU使用权,让出了CPU时间片,暂时停止运行。包括如下五种情况:

    • 线程调用sleep()方法
    • 线程调用了阻塞IO方法,在该方法返回前,被阻塞
    • 线程试图获得同步锁,该锁被其他线程持有(同步阻塞)
    • 线程等待通知(等待阻塞)
    • 程序调用了suspend方法挂起(避免该方法,易死锁)
  • 死亡(Dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程创建及启动

在Java中,实现多线程编程的方式主要有两种,一种是继承Thread类,一种是实现Runnable接口。

在Java源码中,Thread类实现了Runnable接口,它们之间具有多态关系。使用这两种方式创建的线程在工作时性质是一样的,没有本质区别。

线程对象在初始化完成后,调用 start() 方法启动这个线程。其含义是当前线程(启动子线程的线程)告知JVM,只要线程规划器空闲,立即启动调用 start() 方法的线程。

示例代码参考:https://juejin.cn/post/6844903733273313293

线程中断

中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。线程中断是一种用于停止线程的协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作。

使用interrupt()方法将中断状态设置为ture;使用isInterrupted()判断线程的中断状态;使用Thread.interrupted()判断当前线程的中断状态,并将当前线程的中断状态设置为false。

参考:https://juejin.cn/post/6844903745109622797

线程通信

等待/通知机制

参考:https://juejin.cn/post/6844903734556753933

等待通知的经典范式:

等待方:1.获取对象锁;2.如果条件不满足,调用对象的 wait() 方法;3.被通知后检查条件,条件满足执行对应的逻辑。

synchronized(对象){
    while(条件不满足){
        对象.wait();
    }
    处理逻辑
}

通知方:1.获取对象锁;2.改变条件;3.通知等待该对象的进程

synchronized(对象){
    改变条件
    对象.notifyAll();
}

管道流

管道流用于线程之间的数据传输,其媒介为内存。 一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。通过使用管道,实现不同线程间的通信,而无须借助类似临时文件之类的东西。 JDK中提供了四个类:PipedInputStream,PipedOutputStream;PipedReader,PipedWriter。

示例代码: https://juejin.cn/post/6844903734581919752

join()方法

在线程A的代码中执行了线程B threadB.join(),含义是线程A等待线程B终止之后才继续进行线程该代码之后的工作。join() 方法还可以传入参数如join(long mills)以及join(long mills,int nanos)的超时方法,表示线程在给定时间内没有终止,将从中返回。

实例代码: https://juejin.cn/post/6844903734695165959

ThreadLocal

ThreadLocal 是存放线程变量的变量,是一个以ThreadLocal对象为key,任意对象为value的存储结构。

public class Profiler {
    private static final ThreadLocal<Long> TIME_TL=new ThreadLocal<Long>(){
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };
    private static final ThreadLocal<String> STR_TL=new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "hello";
        }
    };

    public static final void begin(){
        TIME_TL.set(System.currentTimeMillis());
    }

    public static final long end(){
        return System.currentTimeMillis()-TIME_TL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost "+Profiler.end()+" mills");
        System.out.println(STR_TL.get());
    }
}

运行结果:

Cost 1001 mills
hello