Java并发编程(一)多线程基础概念

概述

  • 多线程技术:基于软件或者硬件实现多个线程并发执行的技术

    • 线程可以理解为轻量级进程,切换开销远远小于进程

    • 在多核CPU的计算机下,使用多线程可以更好的利用计算机资源从而提高计算机利用率和效率来应对现如今的高并发网络环境

  • 并发编程核心三要素
    • 原子性
      • 原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
    • 有序性
      • 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    • 可见性
      • 当多个线程访问内存中同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值
  • 并行 VS 并发
    • 并行:在同一时刻,有多个指令在多个CPU上同时执行

      Java并发编程(一)多线程基础概念_第1张图片

    • 并发:在同一时刻,有多个指令在单个CPU上交替执行

  • 同步 VS 异步 && 阻塞 VS 非阻塞

    同步和异步指的对于消息结果的获取是客户端(调用者)主动获取[同步],还是由服务端(提供者)主动通知客户端[异步];阻塞和非阻塞指的一个是客户端(调用者)等待消息处理时的本身状态是挂起[阻塞]还是继续处理其他事[非阻塞];

    • 同步阻塞:客户端主动发起获取结果,而同时一直在等待应答结果
    • 同步非阻塞:客户端主动发起获取结果,而同时不断轮询查看发起的请求是否有应答结果
    • 异步阻塞:客户端发出请求后,服务端会主动通知,而客户端同时一直在等待通知
    • 异步非阻塞:客户端发出请求后,服务端会主动通知,而客户端在获取服务端通知之前已经去处理其他事情
  • 进程 VS 线程
    • 进程:是系统运行的基本单位
      • 独立性: 进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
      • 动态性: 进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
      • 并发性: 任何进程都可以同其他进程一起并发执行
    • 线程:是进程中的单个顺序控制流,是一条执行路径
      • 单线程: 一个进程如果只有一条执行路径,则称为单线程程序
      • 多线程: 一个进程如果有多条执行路径,则称为多线程程序
  • 悲观锁 VS 乐观锁
    • 悲观锁: 每次操作前对资源加锁,操作完后释放锁,它认为所有的资源都是不安全的,随时会被其他线程操作、更改。所以操作资源前一定要加一把锁、防止其他线程访问。悲观锁包括:
      • synchronized关键字
      • 基于AQS的实现类(如ReentrantLock,ReentrantReadWriteLock,CountDownLatch)
    • 乐观锁: 它认为所有的资源都是安全的,每个线程对资源的操作都是符合预期的,所以它不需要对资源加锁,它可以通过CAS机制保证线程安全,且性能比悲观锁高。我们可以通过如下使用乐观锁:
      • 底层由乐观锁实现的类。例如:java.util.concurrent.atomic下的原子类
      • 若自己实现乐观锁,则可以参考原子类,使用同样的valotile+CAS 的方式实现
  • 线程安全

  • 在多线程环境下,由于多个线程可以同时访问和修改共享资源,如果没有采取相应的措施来保护共享资源,就可能会出现数据竞争、死锁、活锁等问题,导致程序出现不稳定或不可预期的结果或错误,这些称为"线程安全"问题;线程安全问题指的是内存的安全,在每个进程的内存空间中都会有一块共享区域称为堆内存,进程内的所有线程都可以访问到该区域,这就是造成线程安全问题的潜在原因。所以在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存储在内存的数据(主要是全局变量和静态变量)可能被别的线程修改。因此在多线程场景下,我们通常借助多种同步锁、原子操作类,如 synchronized 关键字、Lock 接口、volatile 关键字、Atomic 类、ThreadLocal 类等,来保证多线程在同时访问共享资源时的正确性和一致性,实现线程安全

  • 上下文切换:线程在执行过程中都会有自己的运行条件和状态,这些运行条件和状态我们就称之为线程上下文,这些信息例如程序计数器、虚拟机栈、本地方法栈等信息。当出现以下几种情况时线程就会从占用CPU状态中退出:

    • 线程主动让出CPU,例如调用wait或者sleep等方法
    • 线程的CPU 时间片用完 而退出CPU占用状态 (因为操作系统为了避免某些线程独占CPU导致其他线程饥饿的情况就设定的例如时间分片算法)
    • 线程调用了阻塞类型的系统中断,例如IO请求等
    • 线程被终止或者结束运行
    • 上述的前三种情况都会发生上下文切换为了保证线程被切换在恢复时能够继续执行,所以上下文切换都需要保存线程当前执行的信息,并恢复下一个要执行线程的现场。所以会占用CPU和内存资源,频繁的进行上下文切换就会导致整体效率低下
  • 死锁:指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。死锁的必要条件包括
    • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用
    • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
    • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链

线程分类

  • 普通线程:即前台线程或者用户线程;即应用程序运行的线程
  • 守护线程:即服务线程或者后台线程;优先级较低,作用是为其他线程的运行提供便利服务
    • JRE判断程序是否执行结束的标准是所有的前台执行线程执行完毕,而不管后台线程(守护线程)的状态
    • 守护线程最典型的应用就是GC
    • 除虚拟机提供的如GC守护线程外,开发者也可以设置线程为守护线程

线程生命周期(线程状态)

Java并发编程(一)多线程基础概念_第2张图片

根据sun官网可知,线程生命周期在java中有以下几种状态:初始(NEW) ,可运行(RUNNABLE)运行(Running),阻塞(BLOCKED)终止(TERMINATED)

  • 初始(NEW): 如果创建Thread类的实例但在调用start()方法之前,线程处于初始(NEW)状态。
  • 可运行(RUNNABLE): 调用start()方法后,线程处于runnable状态,但线程调度程序尚未选择它作为正在运行的线程
  • 运行(RUNNING): 如果线程调度程序已选择它,则线程处于运行状态
  • 阻塞(BLOCKED): 此时线程仍处于活动状态但当前没有资格运行的状态
  • 终止(TERMINATED): 当run()方法退出时,线程处于终止或死亡状态

线程间通信方式

很多业务场景下某个任务都是通过多个线程一起完成,此时就需要线程之间进行通信一起完成该任务,线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺;线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信
    • volatile关键字,该方式是最简单的一种实现方式,volatile修饰的变量是共享内存,保证内存可见性,让多个线程共享一个标志位,当标志位更改的时候就执行不同的线程
  • 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信
    • Object类的 wait/notify方法,执行wait()方法,该线程释放锁资源,然后执行notify() 方法,唤醒其他的线程
    • condition类的 await/signal方法,condition通过lock对象创建,await操作会立刻释放掉锁,进入阻塞状态,signal会唤醒等待队列中的头节点(失败就依次唤醒)
  • 管道流: 管道的输入和输出实际上使用的是一个循环缓冲数组来实现的

创建线程

  • 通过继承Thread,重写其中的run方法,通过调用start方法启动线程时
  • 通过实现Runnable接口,重写其中的run方法,并将自定义类的实例作为一个参数传入Thread获取线程实例,再调用start方法启动线程
  • 通过实现callable接口的,重写其中的call方法。新建Callable的实例,将Callable传入FutureTask得到FutureTask实例,再将FutureTask实例传入Thread获得Thread实例,调用Thread的start方法启动线程,并通过FutureTask的get方法来获取返回值
  • 通过线程池提前为我们准备好线程,我们需要的时候直接取用就可以了,而不需要自己创建

线程Thread常用API

  • start():在使用 new 关键字创建一个线程后(New 状态),只有在 start() 方法执行后,才表示这个线程可运行了(Runnable 状态),至于何时真正运行还要看线程调度器的调度;在线程死亡后,不要再次调用 start() 方法。只能对新建状态的线程调用且只能调用一次 start() 方法,否则将抛出 IllegalThreadStateException 异常
  • run():启动线程是 start() 方法,而不是 run() 方法。如果直接调用 run() 方法,这个线程中的代码会被立即执行,多个线程就无法并发执行了
  • join():等待该线程完成的方法,其他线程将进入等待状态(Waiting 状态),通常由使用线程的程序(线程)调用,如将一个大问题分割为许多小问题,要等待所有的小问题处理后,再进行下一步操作
  • sleep():主动放弃占用的处理器资源,该线程进入阻塞状态(Blocked 状态),指定的睡眠时间超时后,线程进入就绪状态(Runnable),等待线程调度器的调用。
  • yield():主动放弃占用的处理器资源,线程直接进入就绪状态(Runnable),等待线程调度器的调用。可能出现当线程使用yield方法放弃执行后,线程调度器又将该线程调度执行
  • interrupt():没有任何强制线程终止的方法,这个方法只是请求线程终止,这个方法并没有实际的用途,还有 isInterrupted() 方法检查线程是否被中断
  • setDaemon():设置守护进程,该方法必须在 start() 方法之前调用,判断一个线程是不是守护线程,可以使用 isDaemon() 方法判断
  • setPriority():设置线程的优先级,理论上讲线程优先级高更容易被执行。每个线程默认的优先级和父线程(如 main 线程、普通优先级)的优先级相同,线程优先级区间为 1~10,三个静态变量:MIN_PRIORITY = 1、NORM_PRIORITY = 5、MAX_PRIORITY = 10。使用 getPriority() 方法可以查看线程的优先级
  • isAlive():检查线程是否处于活动状态,如果线程处于就绪、运行、阻塞状态,方法返回 true,如果线程处于新建和死亡状态,方法返回 false

线程常见问题

sleep VS wait

  • sleep不会释放锁,只是单纯休眠一会,在给定时间后就会苏醒;而wait则会释放锁,若wait没有设定时间,只能通过notify或者notifyAll唤醒
  • wait常用于线程之间的通信或者交互,而sleep单纯让线程让出执行权。
  • sleep是Thread的方法,因为sleep要做的仅仅是让线程休眠,所以不涉及任何锁释放等逻辑,放在Thread上最合适;而wait是Object的方法,调用wait时会释放锁,并让对象进入WAITING状态,会涉及到资源释放等问题,所以我们需要将wait放在Object 类上

run VS start

  • run: 仅仅是方法,若直接通过对象调用run方法,那么该方法和普通方法没有任何差别,它仅仅是一个名字为run的普通方法
  • start: 开启线程方法,线程就会从用户态转内核态创建线程,并在获取CPU时间片的时候开始运行,然后运行run方法

Thread,Runnable,Callable区别

  • Thread 和 Runnable 区别
    • Thread 是类,而Runnable是接口;如果有其他类需要继承的话肯定是用Runnable接口了
    • Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性
  • Callable和Runnable区别
    • 相同点
      • 都是接口实现,具有良好的扩展性
    • 不同点
      • Callable可以在任务结束的时候提供一个返回值,Runnable无法提供这个功能
      • Callable的call方法可以抛出异常,而Runnable的run方法不能抛出异常

你可能感兴趣的:(#,Java,java)