当前位置 博文首页 > RtxTitanV的博客:Java并发总结之基础

    RtxTitanV的博客:Java并发总结之基础

    作者:[db:作者] 时间:2021-07-07 10:07

    本文主要对Java并发基础知识进行简单总结。

    一、并发、并行和串行

    1.并发

    解释一:并发是指两个或多个事件在同一时间间隔发生。

    解释二:同一时刻只能有一个任务在一个CPU核心上执行,但多个任务在一个时间段内被快速地轮流交替执行,从宏观角度看具有多个进程同时执行的效果,但从微观角度看并不是同时执行的。

    2.并行

    解释一:并行是指两个或者多个事件在同一时刻发生。

    解释二:同一时刻有多个任务在多个CPU核心上同时执行。无论从微观还是从宏观角度来看,二者都是一起执行的。

    3.串行

    多个任务由一个线程按顺序执行,不存在线程不安全问题。

    二、线程与进程

    1.进程

    进程是资源分配的基本单位,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。进程控制块(Process Control Block,PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。

    2.线程

    线程是独立调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

    3.线程与进程的区别

    • 进程是操作系统资源分配的基本单位,而线程是独立调度的基本单位。
    • 一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器**、**虚拟机栈和本地方法栈。
    • 进程在创建、撤销或切换时,所付出的开销要远大于线程。
    • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
    • 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
    • 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。

    4.多线程的概念及其优劣

    多线程:指一个应用程序有多条执行路径,即在一个应用程序中可以同时运行多个不同的线程来执行不同的任务。

    优势:提高CPU的利用率。

    劣势:

    • 线程越多占用内存也越多。
    • 多线程需要协调和管理,所以需要CPU时间跟踪线程。
    • 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。

    一个 Java程序的运行是主(main)线程和多个其他线程同时运行。JVM的启动是多线程的。

    5.使用多线程的原因

    先从总体上来说:

    • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
    • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

    再深入到计算机底层来探讨:

    • 单核时代: 在单核时代多线程主要是为了提高CPU和IO设备的综合利用率。举个例子:当只有一个线程的时候会导致CPU计算时,IO设备空闲;进行IO操作时,CPU空闲。我们可以简单地说这两者的利用率目前都是50%左右。但是当有两个线程的时候就不一样了,当一个线程执行CPU计算时,另外一个线程可以进行IO操作,这样两个的利用率就可以在理想情况下达到100%了。
    • 多核时代:多核时代多线程主要是为了提高CPU利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU只会一个CPU核心被利用到,而创建多个线程就可以让多个CPU核心被利用到,这样就提高了CPU的利用率。

    6.守护线程和用户线程的区别

    • 用户(User)线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
    • 守护(Daemon)线程:运行在后台,为其他用户线程服务。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。

    注意:

    • main方法所在的主(main)线程就是一个用户线程,不能设置成守护线程,main方法启动的同时在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。main线程结束,其他线程一样可以正常运行;main线程结束,其他线程也可以立即结束,当且仅当这些子线程都是守护进程。
    • 所有用户线程结束,JVM退出,不管这个时候有没有守护线程运行。而守护线程不会影响JVM的退出。
    • setDaemon()方法可以将一个线程设置为守护线程,setDaemon(true)必须在start()方法前调用,否则会抛出 IllegalThreadStateException 异常。
    • 在守护线程中产生的新线程也是守护线程。
    • 不是所有的任务都可以分配给守护线程来执行。
    • 守护线程中不能靠finally代码块的内容来确保执行关闭或清理资源的逻辑。因为一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,finally代码块可能无法被执行。

    7.上下文切换

    多线程编程中一般线程的个数都大于CPU核心数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

    概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

    上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间,事实上,可能是操作系统中时间消耗最大的操作。

    Linux相比与其他操作系统(包括其他类Unix系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

    三、多线程的实现方式

    1.继承Thread类

    定义一个类继承Thread类,重写run()方法。创建自定义的线程类对象调用start()方法启动线程,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法。

    public class MyThread extends Thread {
        @Override
        public void run() {
            // ...
        }
    }
    
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
    

    2.实现Runnable接口

    定义Runnable接口的非抽象实现类,重写run()方法。创建Runnable接口实现类对象,以该对象作为创建线程的参数来创建线程,调用start()方法启动线程。

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            // ...
        }
    }
    
    public static void main(String[] args) {
        MyRunnable instance = new MyRunnable();
        Thread thread = new Thread(instance);
        thread.start();
    }
    

    3.实现Callable接口

    定义Callable接口的非抽象实现类,重写call()方法,与Runnable相比,Callable可以有返回值,返回值通过FutureTask进行封装,以Callable接口实现类对象为参数创建FutureTask对象,再以FutureTask对象为参数创建线程,调用start()方法启动线程。

    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() {
            return 1;
        }
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
    

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

    4.Executor

    任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。

    Executor是一个简单的接口(是线程池的顶级接口但并不是一个线程池),但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。该框架提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。通过使用Executor,可以实现各种调优、管理、监视、记录日志、错误报告和其他功能。Executor基于生产者一消费者模式,提交任务相当于生产者,执行任务的线程则相当于消费者。

    调用Executors中的静态工厂方法来创建线程池,返回的线程池都实现了ExecutorService接口。主要有newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutornewScheduledThreadPool

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyRunnable());
        }
        executorService.shutdown();
    }
    

    5.常见问题

    Runnable和Callable的区别
    • Runnable接口的run()方法无返回值,Callable接口的call()方法有返回值,是个泛型,和FutureFutureTask配合可以用来获取异步执行的结果。
    • Runnable接口的run()方法只能抛出运行时异常,且无法捕获处理,Callable接口的call()方法允许抛出异常,可以获取异常信息。
    run()和start()的区别
    • start()方法用于启动线程,run()方法用于执行线程的运行时代码。
    • start()只能调用一次,run()可以重复调用。
    • start()方法启动线程后先处于就绪状态,获取CPU时间片后进入运行状态,通过调用该线程的run()方法以完成其运行状态,run()方法运行结束, 此线程终止,然后再调度其它线程,真正实现了多线程运行。run()方法是在本线程里的只是一个普通方法,如果直接调用就是在当前线程中调用一个普通方法而已,执行路径还是只有一条,没有多线程的特征,所以在多线程执行时需要调用start()方法而不是直接调用run()方法。
    调用start()时会执行run()以及不能直接调用run()方法的原因

    新建一个线程后线程进入了新建状态,调用start()方法会启动线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。一句话总结,调用start()方法方可启动线程并使线程进入就绪状态,而run()方法只是Thread的一个普通方法调用,还是在主线程里执行。

    Callable、Future和FutureTask
    • Callable:Callable是一种更好的抽象,它认为主入口点(即call)将返回一个值并可能抛出一个异常,其中这个返回值可以被Future拿到。在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable。
    • Future:表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future用于获取结果。
    • FutureTask:表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了CallableRunnable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    六、线程调度和优先级

    1.调度模型

    • 分时调度模型:所有线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间片。
    • 抢占式调度模型:优先让可运行池中优先级高的线程使用CPU,如果可运行池中的线程优先级相同,那么会随机选择一个线程获取CPU的使用权,优先级高的线程获取的CPU时间片相对多一些。 Java采用的抢占式调度模型。

    2.调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    • 线程体中调用了yield方法让出了对CPU的使用权。
    • 线程体中调用了sleep方法使线程进入睡眠状态。
    • 线程由于IO操作受到阻塞。
    • 出现了一个更高优先级的线程。
    • 在支持时间片的系统中,该线程的时间片用完。

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

    3.线程优先级

    在Java中,每一个线程有一个优先级,新线程的优先级最初设置为等于创建线程的优先级,可以用setPriority方法更改线程的优先级,优先级的范围在MIN_PRIORITY(1)与MAX_PRIORITY(10)之间,默认值为NORM_PRIORITY(5)。默认情况下,一个线程继承它的父线程的优先级。线程调度器选择新线程时首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。

    ·注意:如果确定要使用优先级,应该避免以下错误。如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死。

    优先级相关方法如下:

    java.lang.Thread

    • public final void setPriority(int newPriority):更改线程的优先级。如果优先级不在MIN_PRIORITYMAX_PRIORITY范围内,会抛出IllegalArgumentException异常。如果当前线程不能修改此线程,会抛出SecurityException异常。
    • public final int getPriority():返回此线程的优先级。

    java.lang.ThreadGroup

    • public final void setMaxPriority(int pri):设置线程组的最大优先级。线程组中具有较高优先级的线程不受影响。如果参数priMIN_PRIORITYMAX_PRIORITY范围外,组的最大优先级保持不变。如果当前线程不能修改此线程组,会抛出SecurityException异常。
    • public final int getMaxPriority():返回此线程组的最大优先级。作为此组的一部分的线程的优先级不能高于最大优先级。

    七、线程控制和协作

    1.休眠线程

    java.lang.Thread

    • public static void sleep(long millis)
      • 使当前正在执行的线程休眠指定毫秒数时间,线程不失去任何监视器(Monitor)的所有权。
      • 如果millis值为负数,会抛出IllegalArgumentException异常。
      • 如果任何线程中断该休眠线程,该休眠线程的中断状态将被清除并抛出InterruptedException异常(sleep方法抛出)。

    2.礼让线程

    java.lang.Thread

    • public static void yield()
      • 声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。
      • 该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

    3.守护线程

    java.lang.Thread

    • public final void setDaemon(boolean on)
      • 将此(调用该方法的)线程标记为守护线程或用户线程,参数on设置为true则将此线程标记为守护线程,设置为false则标记为用户线程。
      • 当所有用户线程都结束运行时,守护线程会随着JVM退出一起结束。
      • 此方法必须在线程启动之前调用。
      • 如果该线程为alive,会抛出IllegalThreadstateException异常。
      • 如果当前线程不能修改此线程,会抛出SecurityException异常。
    • public final boolean isDaemon():判断该线程是否是守护线程,返回true表示是守护线程,false表示不是。

    4.中断线程

    java.lang.Thread

    • public void interrupt()
      • 中断此线程。
      • 调用该方法会设置线程的中断状态为true,这是每一个线程都具有的boolean标志。
      • 如果该线程调用了Object类的wait()wait(long)wait(long, int)方法,或者sleep(long)sleep(long, int)join()join(long)join(long, int)这类方法,则它的中断状态将被清除并会收到一个InterruptedException异常,从而提前结束该线程,但是不能中断I/O阻塞和synchronized锁阻塞。
    • public static boolean interrupted()
      • 测试当前线程是否被中断,返回true表示被中断,false表示没中断。
      • 该方法会清除线程的中断状态,即会将当前线程的中断状态重置为false,如果该方法连续调用两次且中间没再次中断,第二次调用将返回false。
    • public boolean isInterrupted()
      • 测试此线程是否被中断,返回true表示被中断,false表示没中断。
      • 线程的中断状态不受该方法影响。

    注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(就是线程中断后会抛出InterruptedException的方法)就是在监视线程的中断状态,一旦线程被中断,则该线程的中断状态就被置为true,然后该线程的中断状态将被清除并且这些方法会抛出中断异常。

    java.util.concurrent.ExecutorService接口:

    • void shutdown()

      • 执行平缓的关闭过程,不再接受新的任务。
      • 等待已经提交的任务执行完成,包括那些还未开始执行的任务。
      • 如果已经关闭,调用没有额外的作用。
    • List<Runnable> shutdownNow()

      • 将执行粗暴的关闭过程,不再接受新的任务。
      • 尝试取消所有运行中的任务(interrupt中断),并且不再启动队列中尚未开始执行的任务并返回等待执行的任务列表。
      • 在ExecutorService关闭后提交的任务将由“拒绝执行处理器(Rejected Execution Handler)”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException

      注意:试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,这种方法的作用有限,如果线程中没有sleepwaitCondition、定时锁等应用,interrupt()方法无法中断当前线程。所以shutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候都能立即退出。

    • boolean isShutdown():判断该executor是否已被关闭(shutdown),true表示已关闭,false表示未关闭。

    • boolean isTerminated():如果所有提交的任务在shutdown后都完成,则返回true,否则返回false。注意,除非先调用shutdownshutdownNow,否则isTerminated永远不会为true。

    • boolean awaitTermination(long timeout, TimeUnit unit)

      • 当前线程会阻塞直到shutdown请求后所有已提交的任务执行完(包括正在执行和等待队列中的任务)或者发生超时,或者当前线程被中断,这三种情况以先发生的为准。
      • timeoutTimeUnit用于设定超时时间及单位。
      • 会监测Executor是否已经终止,若终止(shutdown请求后所有已提交任务执行完成)则返回true,若终止之前已超时则返回false。该方法一般情况下会和shutdown方法组合使用。
      • 如果在等待中被中断,会抛出InterruptedException异常。

    5.加入线程

    java.lang.Thread

    • public final void join()
      • 将当前线程挂起,直到调用该方法的目标线程结束。
      • 如果任何线程中断该挂起线程,该挂起线程的中断状态将被清除并抛出InterruptedException异常(join方法抛出)。
    • public final void join(long millis)
      • 将当前线程挂起,等待调用该方法的目标线程结束,但等待时间最多为millis毫秒。
      • millis为0则意味着永久等待,此时等价于join()
      • 如果millis值为负数,会抛出IllegalArgumentException异常。
      • 如果任何线程中断该挂起线程,该挂起线程的中断状态将被清除并抛出InterruptedException异常(join方法抛出)。

    6.等待和唤醒

    java.lang.Object

    • public final void wait()
      • 导致当前线程等待,直到被另一个线程调用该对象的notify()notifyAll()方法唤醒,等同于执行wait(0)
      • 当前线程必须拥有该对象的监视器锁,然后该线程会释放该对象锁并进入等待状态等待另一个线程通过调用该锁对象的notify()notifyAll()方法来通知等待该对象锁的线程,直到该线程重新获取对象锁才能进入就绪状态。
      • 如果当前线程不是对象锁的所有者,会抛出IllegalMonitorstateException异常。
      • 如果任何线程中断等待唤醒的线程,则等待唤醒的线程的中断状态将被清除并抛出InterruptedException异常(wait方法抛出)。
    • public final void wait(long timeout)
      • 导致当前线程等待,直到被另一个线程调用该对象的notify()notifyAll()方法唤醒或者超过指定超时时间。