绕不过的并发编程--Java线程基础

239 阅读14分钟

简单介绍

什么是进程?

什么是进程?

「进程」是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。

系统运行一个程序即是一个进程从创建,运行到消亡的过程。

什么是线程?

什么是线程?

「线程」是操作系统能够进行运算调度的最小单位。

「线程」被包含在进程之中,是进程中的实际运作单位。

(可以把进程当作车间,线程当作工人,是一对多的关系)

并发和并行

并发

并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间(宏观上同时发生,微观上交替发生),且这几个程序都是在同一个处理机上运行。

举个例子:

我们知道一个程序必须放入内存,并给它分配CPU才能运行。

比如在单核计算机中某用户打开了「QQ」,「网易云音乐」,「Google Chrome」...

这个场景是真实的,单核计算机确实可以通过并发来达到这样的效果。那么操作系统是如何实现并发性的呢?

  • 为什么单核操作系统能支持并发

    靠着虚拟处理器技术,实际上只有一个CPU,但是在用户视角有多个CPU在为他服务。

    依靠着虚拟技术中的「时分复用技术」,处理机在微小的时间片内交替分配给各个进程。(抽象意义上可以直接理解成CPU分配给进程时间片)

并行

并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

并行场景出现在多核物理机上,有多个CPU才能出现真正的『同时发生』。

并行和并发的区别关键就在于是否是同一时刻发生的。

同步和异步

同步

发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。

「同步」就是阻塞模式

异步

调用在发出之后,不用等待返回结果,该调用直接返回。

「异步」就是非阻塞模式

临界区

什么是临界区?

「临界区」用来表示一种公共资源或者说是共享数据,可以被多个线程使用。

但是每个线程使用时,一旦「临界区」资源被一个线程占有,那么其他线程必须等待。

阻塞和非阻塞

什么是阻塞和非阻塞?

阻塞和非阻塞通常用来形容多线程间的相互影响

比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是「阻塞」

而「非阻塞」就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行

多线程

为什么需要多线程?

这个问题需要参考多方面来回答。

从总体上来说

  • 从计算机底层来说

    线程可以比作是轻量级的进程,是程序执行的最小单位。

    另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

    (多个线程才能实现让一个应用程序同时能播放音乐和运行游戏)

    并且最重要的线程间的切换和调度的成本远远小于进程。

  • 从当代互联网发展趋势来说

    现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

从计算机本身考虑

  • 单核计算机

    在单核计算机多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。

    假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。

    当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。

  • 多核计算机

    多核计算机多线程主要是为了提高进程利用多核 CPU 的能力。

    举个例子:

    假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。

    创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

Java和多线程

Java程序天生就是多线程程序

即Java进程中往往不只一个主线程。为了证明这一点,我们准备了一段程序来验证。

我们通过JMX来查看你一个Java程序有哪些线程,代码如下:

 public class MultiThread {
     public static void main(String[] args) {
         // 获取 Java 线程管理 MXBean
         ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
         // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
         ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
         // 遍历线程信息,仅打印线程 ID 和线程名称信息
         for (ThreadInfo threadInfo : threadInfos) {
             System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
         }
     }
 }

运行结果:

 [6] Monitor Ctrl-Break // idea中特有的,而且还是用run启动方式
 [5] Attach Listener // 添加事件
 [4] Signal Dispatcher // 分发处理给 JVM 信号的线程
 [3] Finalizer // 调用对象 finalize 方法的线程
 [2] Reference Handler // 清除 reference 线程
 [1] main // main线程,程序入口

这说明Java程序的执行是多个线程一起配合的。

  • 什么是JMX

    JMXJava Management Extensions(Java管理扩展)的缩写,是一个为应用程序植入管理功能的框架。

    用户可以在任何Java应用程序中使用这些代理和服务实现管理,用来管理和监测Java程序。

    最常用到的就是对于JVM的监测和管理,比如 JVM 内存、CPU 使用率、线程数、垃圾收集情况等等。

    另外,还可以用作日志级别的动态修改,比如 log4j 就支持 JMX 方式动态修改线上服务的日志级别。最主要的还是被用来做各种监控工具,比如文章开头提到的 Spring Boot ActuatorJConsoleVisualVM 等。

多线程应用场景

我们知道了Java支持多线程,那么多线程一般在什么时候运用呢?

  • 文件下载

    可以使用多线程实现

  • 后台任务

    如定时向大量(100W以上)的用户发送邮件

  • 异步处理

    记录日志

  • 多步骤的任务处理

    可根据步骤特征选用不同个数和特征的线程来

  • 协作处理

    多任务的分割,由一个主线程分割给多个线程完成

线程的生命周期

Java线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态,每一个状态对应一个枚举值。

这些枚举值都定义在java.lang.Thread.State中:

image.png

  • NEW: 初始状态

    线程被创建出来但没有被调用 start()

  • RUNNABLE: 运行状态

    线程已经触发start()方式调用。线程处于运行中状态。

  • BLOCKED :阻塞状态

    表示线程阻塞,等待锁释放。

  • WAITING:等待状态

    表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒。

    如通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法

    通过join()方法进行等待的线程等待目标线程运行结束而唤醒

    一旦通过相关事件唤醒线程,线程就进入了RUNNABLE状态继续运行。

  • TIME_WAITING:超时等待状态

    可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

  • TERMINATED:终止状态

    表示线程执行完毕后,进行终止状态。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

线程状态切换

线程状态变迁图

这里我直接找了一张网上的图,总结的很好:

image.png

  • 这里的READYRUNNING在「Java线程的生命周期和状态」中为什么没有?

    在操作系统层面,线程有 READYRUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态

  • 为什么不区分这两种状态?

    现在的时分多任务操作系统架构下,时间分片很小,一个线程一次最多只能在 CPU 上运行比如 10-20 ms 的时间(处于 RUNNING 状态),时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(回到 READY 状态)。

    线程切换的如此之快,区分这两种状态就没什么意义了。

贴一张图不证明怎么行呢?接着就上代码对上面的图画的进行证明。

代码分析

接着我们看看Java程序的线程随着代码状态是如何变化的。

  • new Thread()时的状态

    状态为:NEW

    测试代码:

     @Test
     public void testNewThread(){
         Thread t1 = new Thread(()->{
             System.out.println("t1执行任务");
         });
         System.out.println("new Thread() 时 t1 的状态为 "+t1.getState());
     }
    

    运行结果:

     new Thread() 时 t1 的状态为 NEW
    
  • thread.start()时的状态

    状态为:RUNNALE

    测试代码:

     @Test
     public void testThreadStart() throws InterruptedException {
         Thread t1 = new Thread(()->{
             System.out.println("t1执行任务");
             int count=0;
             while(true){
                 count++;
             }
         });
         t1.start();
         Thread.sleep(1000L);
         System.out.println("t1.start() 时 t1的状态:"+t1.getState());
     }
    

    运行结果:

     t1执行任务
     t1.start() 时 t1的状态:RUNNABLE
    
  • thread.sleep(time)时的状态

    状态为:TIMED_WAITING

    (定时等待📆)

    测试代码:

     static volatile boolean running = true;
     ​
     @Test
     public void testThreadSleep() throws InterruptedException {
         Thread t1=new Thread(()->{
             try{
                 while (running){
                     System.out.println("t1 任务开始执行,并且即将 sleep");
                     Thread.sleep(10000L);
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
         t1.start();
         Thread.sleep(2000L);
         running=false;
         Thread.sleep(2000L);
         System.out.println("t1.sleep() 时 t1 的状态:"+t1.getState());// timed_waiting
     }
    

    运行结果:

     t1 任务开始执行,并且即将 sleep
     t1.sleep() 时 t1 的状态:TIMED_WAITING
    

    解释:

    线程 t1 开始执行任务,然后睡眠,主线程抢占时间片,获取t1状态为TIMED_WAITING

  • thread.join()thread.join(time)时的状态

    thread.join() 状态为:WAITING

    (傻等😂)

    thread.join(time) 状态为:TIMED_WAITING

    (定时等待📆)

    测试代码:

     @Test
     public void testThreadJoin() throws InterruptedException {
         Thread t1 = new Thread(() -> {
             try {
                 Thread.sleep(10000L);// 上来就睡,并且时间超过t2中执行t1.join(5000L)的时间
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
         Thread t2 = new Thread(() -> {
             try {
                 System.out.println("t2 中执行 t1.join(5000L)");
                 t1.join(5000L); //t2等待t1 5s
                 System.out.println("t2 中执行 t1.join()");
                 t1.join(); //t2等待t1执行完
                 System.out.println("t2 执行完成");
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
         t1.start();
         t2.start();
         Thread.sleep(1000L); // 想办法将时间片交给t2
         // 此时t2正好在执行t1.join(5000L)
         System.out.println("t2 的状态: " + t2.getState());
         Thread.sleep(5000L);
         // 此时t2正好在执行t1.join()
         System.out.println("t2 的状态: " + t2.getState());
     }
    

    运行结果:

     t2 中执行 t1.join(5000L)
     t2 的状态: TIMED_WAITING
     t2 中执行 t1.join()
     t2 的状态: WAITING
    

    解释:

    线程t1执行睡眠10s,此时进入线程t2,并执行t2中执行t1.join(5000L)

    接下来t1会抢占时间片,t1还在睡,此时回到主线程,打印出来t2在执行t1.join(5000L)的状态为TIMED_WAITING

    主线程又睡眠,t2开始执行t1.join(),此时t2的状态为WAITING

  • 线程synchronized时的状态

    状态为:BLOCKED

    (抢不到锁,线程处于阻塞状态,并且这个并不是定时的等待,干等也不一定能等到,主要要靠CPU分配,该状态就是阻塞🔒)

    测试代码:

     @Test
         public void testSynchronizedThread() throws InterruptedException {
             Thread t1 = new Thread(() -> {
                 synchronized (TestThreadState.class) {
                     System.out.println("t1抢到锁");
                 }
             });
             synchronized (TestThreadState.class) {
                 t1.start();
                 Thread.sleep(1000L);
                 System.out.println("t1抢不到锁的状态: " + t1.getState());
             }
         }
     ​
    

    运行结果:

     t1抢不到锁的状态: BLOCKED
     t1抢到锁
    

    解释:

    主线程启动,先抢到锁。此时执行t1.start()启动了t1线程。

    然后主线程睡眠,锁还没有释放。此时的t1状态为BLOCKED

  • 线程wait时的状态

    lock.wait(time) 状态为:TIMED_WAITING

    (定时等待📆)

    lock.wait() 状态为:WAITING

    (傻等😂一个notify来唤醒它)

    测试代码:

     @Test
     public void testThreadWait() throws InterruptedException {
         Object object = new Object();
         Thread t1 = new Thread(() -> {
             synchronized (object) {
                 try {
                     System.out.println("t1将wait(1000L)");
                     object.wait(1000L);
                     System.out.println("t1将wait");
                     object.wait();
                     System.out.println("t1将执行完");
                     Thread.sleep(200L);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
         t1.start();
         Thread.sleep(800L);
         synchronized (object) {
             System.out.println("t1的状态:" + t1.getState());
             object.notify();
             Thread.sleep(1000L);
             System.out.println("t1的状态:" + t1.getState());
         }
         Thread.sleep(3000L);
         System.out.println("t1的状态:" + t1.getState());
         Thread.sleep(1000L);
         synchronized (object) {
             object.notify();
             Thread.sleep(200L);
             System.out.println("t1的状态:" + t1.getState());
         }
         Thread.sleep(200L);
         System.out.println("t1的状态:" + t1.getState());
         Thread.sleep(1000L);
         System.out.println("t1的状态:" + t1.getState());
     }
     ​
    

    运行结果:

     t1将wait(1000L)
     t1的状态:TIMED_WAITING
     t1的状态:BLOCKED
     t1将wait
     t1的状态:WAITING
     t1的状态:BLOCKED
     t1将执行完
     t1的状态:RUNNABLE
     t1的状态:TERMINATED
    

    解释:

    主线程启动,执行t1.start(),进入t1,打印「t1将wait(1000L)」。此时t1让出锁。在t1超时等待(wait(time))的同时,主线程睡眠。

    之后主线程抢到锁,t1的状态为TIMED_WAITING。这时主线程执行object.notify(),但这时锁还没有释放,t1还没有获取到锁,所以t1状态BLOCKED

    之后主线程释放锁,t1获得锁,执行object.wait(),这时t1的状态WAITING

    然后回到主线程,并获得锁,执行object.notify(),但这时锁还没有释放,t1还没有获取到锁,所以t1状态BLOCKED

    接着主线程释放锁,并让t1获取到锁就把控制权交给主线程,此时t1线程被唤醒并处于运行状态RUNNABLE

    最后t1执行完成,状态为TERMINATED

  • 线程park时的状态

    park状态为:WAITING

    (调用一次park,线程被阻塞了,傻等😂一个unpark

    unpark状态为:RUNNABLE

    测试代码:

     @Test
     public void testThreadPark() throws InterruptedException {
         Thread t1 = new Thread(()->{
             LockSupport.park();
             try {
                 Thread.sleep(200L);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         });
         t1.start();
         Thread.sleep(1000L);
         System.out.println("t1 park后的状态:"
                            + t1.getState());
         LockSupport.unpark(t1);
         Thread.sleep(200L);
         System.out.println("t1 unpark后的状态:"
                            + t1.getState());
     }
    

    运行结果:

     t1 park后的状态:WAITING
     t1 unpark后的状态:RUNNABLE
    

Java如何创建线程

如何通过Java代码创建线程?

  • 继承Thread

    Thread类实现了Runnable接口,因此我们需要重写run方法来定制线程对象执行的任务。

     public class MyThread extends Thread {
         public void run() {
             // ...
         }
     }
    

    当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

     @Test
     public void testThread(){
         MyThread myThread=new MyThread();
         myThread.start();
     }
    
  • 实现Runnable接口

    实现Runnable接口中的run方法

     public class MyRunnable implements Runnable {
         @Override
         public void run() {
             // ...
         }
     }
    

    使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。

     @Test
     public void testRunnable(){
         MyRunnable myRunnable=new MyRunnable();
         Thread thread=new Thread(myRunnable);
         thread.start();
     }
    
  • 实现Callable接口

    Runnablerun方法相比 ,Callablecall方法可以有返回值,返回值通过FutureTask进行封装

     public class MyCallable implements Callable<String> {
         public String call() {
             return "你好";
         }
     }
    
     @Test
     public void testCallable() throws ExecutionException, InterruptedException {
         MyCallable myCallable = new MyCallable();
         FutureTask<String> futureTask = new FutureTask<>(myCallable);
         Thread thread = new Thread(futureTask);
         thread.start();
         System.out.println(futureTask.get());
     }
    

    可能有同学对Callable的了解比较少,所以这边我们介绍下

    • 什么是CallableFutureFutureTask

      我们直接继承Thread,实现Runnable接口的创建线程的方式的缺陷在于:执行完任务后无法获取执行结果。如果一定需要获取执行结果,就必须手动使用共享变量或者线程通信的方式来达到效果,这样就比较麻烦了。

      于是在JDK1.5开始,提供了CallableFutureFutureTask。通过它们在执行完任务后可以得到任务执行结果。

      其中我们上面执行的get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

实现 RunnableCallable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的

并且创建线程的方式中实现接口的方式好过继承Thread,因为Java不支持多继承,继承了Thread就无法继承其它类。类可能只要求可执行就行,继承整个Thread类开销过大。

小结

本章是并发系列文章的打头文章,结合操作系统知识介绍了线程的基本概念,接着运用Java代码分析了线程的生命周期和创建线程工作的过程。并发编程是面试中重要的考点,需要我们理解透彻。

本章参考: