Java中多线程详解之多线程机制,多线程队列及多线程控制类分

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

基本概念

  • Java多线程内容:
    • Object类中的wait(),notify() 等接口
    • Thread类中的接口
    • synchronized关键字
  • JUC是指java.util.concurrent
  • 进程: 计算机中已经运行的程序
    • 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
    • 在面向进程设计的系统中,进程是程序的基本执行实体
    • 在面向线程设计的系统中,进程是线程的容器
    • 程序本身只是指令,数据和自身组织形式的描述名称,进程才是程序的真正运行实例
  • 线程: 操作系统能够进行运算调度的最小单位
    • 线程包含在进程中,是进程中的实际运行单位
    • 一条线程表示的是进程中的一个单一顺序的控制流
    • 一个进程中可以并发多个线程,每条线程并行执行不同的任务
  • 多线程: 一个进程运行时产生不止一个线程
  • 并行: 多个CPU实例或者多台机器同时执行一段处理逻辑,是真正的同时进行
  • 并发: 通过CPU的调度算法,让处理逻辑看上去是同时执行,实际从CPU操作层面上来说不是真正的同时进行
  • 线程安全: 在并发的情况下,代码经过多线程的使用,线程的调度顺序不影响处理结果
    • 线程安全的情况下,只需要关注系统内存,CPU是否够用
    • 线程不安全的情况下,意味着线程的调度顺序会影响最终的处置结果,除了要关注系统内存,CPU是否够用外,还需要关注线程的调度顺序
  • 同步: 通过控制和调度,保证共享资源的多线程访问是线程安全的,来保证结果的准确性
    • 在保证结果准确的同时,提高性能,才是优秀的程序
    • 线程安全的优先级高于性能需求的优先级

线程状态图

  • Java中线程的状态分为6种: 新建状态New, 运行状态Runnable( 包括就绪状态ready和运行状态running), 阻塞状态Blocked, 等待状态WAITING, 超时等待状态 (TIMED_WAITING), 终止状态TERMINATED(消亡状态Dead)

在这里插入图片描述

  • 新建状态NEW: 新创建了一个线程对象,但还没有调用start() 方法
    • 线程对象被创建后,就进入了新建状态
    • 实现Runnable接口和继承Thread可以得到一个线程类 ,new一个实例出来,线程就进入了初始状态
    • 比如 : Thread thread = new Thread();
  • 运行状态RUNNABLE: Java线程中将就绪状态Ready和运行状态Running统称为运行状态Runnable
    • 就绪状态Ready: 线程对象创建后,其余线程调用该对象的start() 方法.该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态
      • 调用线程的start() 方法,此线程进入就绪状态
      • 当前线程sleep() 方法结束,其余线程join() 结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态
      • 当前线程时间片用完了,调用当前线程的yield() 方法,当前线程进入就绪状态
      • 锁池里的线程拿到对象锁后,进入就绪状态
      • 就绪状态只是表示线程可以运行,,如果调度程序调度到线程,线程就永远处于就绪状态
    • 运行状态Running: 就绪状态的线程在获得CPU时间片后变为运行状态
      • 线程调度程序从可运行池中选择一个就绪状态Ready的线程进行调度
      • 这是线程进入运行状态的唯一方式
  • 阻塞状态BLOCKED: 线程阻塞于锁
    • 线程阻塞在进入synchronized关键字修饰的方法或代码块获取锁时的状态
  • 等待状态WAITING: 进入该状态的线程需要等待其余线程做出一些特定动作,比如通知或中断
    • 处于这种状态的线程不会被分配CPU执行时间
    • 等待被显式地唤醒,否则会处于无限期等待的状态
  • 超时等待状态TIMED_WAITING: 该状态不同于WAITING, 可以在指定的时间后自行返回
    • 处于这种状态的线程不会被分配CPU执行时间
    • 不需要无限期等待被其余线程显式地唤醒,在达到一定时间后会自动唤醒
  • 终止状态TERMINATED: 线程已经执行完毕
    • 当线程的run() 方法完成时,或者主线程的main() 方法完成时,就认为线程终止
    • 线程对象也许是活的,但是已经不是一个单独执行的线程.线程一旦终止,就不能复生
    • 在一个终止的线程上调用start() 方法,会抛出java.lang.IllegalThreadStateException异常

多线程机制

Monitor

  • Monitor监视器应用于同步问题的线程调度工具
  • Java中的每个对象都有一个Monitor监视器,用来监测并发代码的重入
    • 非多线程编码时监视器不发挥作用
    • synchronized范围内,监视器发挥作用

在这里插入图片描述

synchronized

  • wait()notify() 必须存在于synchronized块中,这三个关键字针对的是同一个对象监视器. 也就是说,在wait() 之后,其余线程可以进入同步块中执行
  • 如果某块代码没有持有Monitor监视器的使用权时而去进行wait()notify() 操作,会抛出java.lang.IllegalMonitorStateException
  • synchronized代码块中调用另一个对象的wait()notify() 操作,由于对象的Monitor监视器不同,会抛出java.lang.IllegalMonitorStateException
  • 代码块使用synchronized:
    • 在多线程环境中 ,synchronized块中的方法获取某个对象实例的Monitor监视器
    • 如果对象实例相同,那么就是同一个Monitor监视器,同一时间只有一个线程可以执行synchronized块中的方法
  • 方法中使用synchronized:
    • 实际获取的是类的Monitor监视器
    • 修饰static方法时,则锁定的是这个类的所有实例

volatile

  • 多线程的内存模型:
    • 主存: main memory
    • 线程栈: working memory
    • 处理数据时,线程会将数据从主存main memory中加载load到本地栈working memory, 完成操作以后,再保存save回主存main memory
    • volatile的作用: 在这里的作用是,每次对变量操作,都会激发一次load and save

在这里插入图片描述

  • volatile:
    • 多线程的变量如果不使用volatile或者final进行修饰,会造成不可预知的后果. 比如某个变量被一个线程修改后,另一个线程获取到的是修改之前的值
    • 原因在于同一个实例的同一属性本身应该只有一个副本,但是多线程中会对值进行缓存
    • volatile就是不使用缓存,直接取值
    • 在线程安全的情况下,使用volatile会影响性能

等待队列

  • 调用objwait(),notify() 方法前,必须获得obj锁.即wait()notify() 方法必须写在synchronized(obj) 代码内

在这里插入图片描述

  • 线程1获取对象的锁,正在使用对象A
  • 线程1调用对象Await() 方法
  • 线程1释放对象A的锁,进入等待队列
  • 同步队列的线程争抢对象锁
  • 线程5获取对象的锁,使用对象A
  • 线程5调用A对象的notifyAll() 方法唤醒所有等待线程
  • notifyAll() 方法所在的synchronized结束,线程5释放对象A的锁
  • 同步队列的线程争抢对象锁

Object类

  • wait()
  • notify()
  • notifyAll()

Thread类

  • 线程休眠函数 : sleep()
  • 线程中断函数 : interrupt()
  • 获取线程名称 : getName()

synchronized关键字

  • 分为synchronized代码块和synchronized方法
  • 作用: 使得线程获取对象的同步锁

同步队列

  • 当前线程调用对象A的同步方法时,发现对象A的锁被其余线程占用,此时当前线程进入同步队列. 即同步队列中的线程都是准备争抢对象锁的线程
  • 当一个线程1被另外一个线程2唤醒时,线程1进入同步队列,去争抢对象锁
  • 同步的环境下才会有同步队列.一个对象对应一个同步队列
  • 超时等待线程到了等待时间或者等待线程被notify() 方法或者notifyAll() 方法唤醒,会进入同步队列中去竞争锁:
    • 如果获得锁,则进入运行状态RUNNABLE
    • 如果没有获得锁,则进入阻塞状态BLOCKED等待获取锁

Thread中的方法

Thread.sleep(long millis)

  • 作用: 给其余线程执行的机会
  • 一定是当前线程调用此方法
  • 当前线程进入超时等待状态TIMED_WAITING, 不释放对象锁, millis后线程自动苏醒进入就绪状态

Thread.yield()

  • 作用:
    • 使得相同优先级的线程轮流执行,但是并不保证相同优先级的线程一定会轮流执行
    • 实际中无法保证yield() 达到让步目的,因为让步的线程还有可能会被线程调度程序再次选中
    • Thread.yield() 不会导致线程阻塞,该方法与sleep() 类似,只是不能由用户指定暂停多长时间
  • 一定是当前线程调用此方法
  • 当前线程放弃获取的CPU时间片,不释放资源,由运行状态Running转变为就绪状态Ready, 重新选择执行线程

thread.join

  • 包括 : thread.join()thread.join(long millis)
  • 当前线程调用其余线程的join方法,当前线程进入等待状态WAITING或者超时等待状态TIMED_WAITING. 当前线程不会释放已经持有的对象锁
  • 调用的其余线程执行完毕或者超时等待时间millis时间结束,当前线程进入RUNNABLE状态,也可能进入BLOCKED状态

obj.wait()

  • 当前线程调用对象的wait() 方法,当前线程释放对象锁,进入等待队列
  • 进入等待队列的线程依靠notify() 或者notifyAll() 唤醒
  • 如果是调用对象的超时等待方法wait(long timeout), 则超时等待时间timeout时间结束自动唤醒

obj.notify()

  • 唤醒此对象监视器上等待的单个线程,选择是任意性的

obj.notifyAll()

  • 唤醒在此对象监视器上等待的所有线程

LockSupport

  • 包括LockSupport.park(), LockSupport.parkNanos(long nanos), LockSupport.parkUntil(long deadlines)
  • 当前线程进入等待状态WAITING或者超时等待状态TIMED_WAITING
  • 不需要获得锁就可以让线程进入等待状态WAITING或者超时等待状态TIMED_WAITING
  • 需要通过LockSupport.unpark(Thread thread) 方法唤醒线程

多线程控制类

  • Java的多线程包 : java.util.concurrent中提供很多高效的多线程控制类,用于Java中多线程的控制

ThreadLocal类

  • 作用: 保存线程的独立变量
    • 对于一个继承自Thread的线程类,使用ThreadLocal维护变量时,ThreadLocal会为每个使用这个变量的线程提供独立的变量副本
    • 这样每一个线程都可以独立的改变自己拥有的变量副本,不会影响其余线程的变量副本
  • 实现方式:
    • 每一个线程Thread持有一个ThreadLocalMap类型的变量
    • ThreadLocalMap是一个轻量级的Map, 功能和Map一样,区别在于桶中存放的是entry不是entry链表
    • 线程本身作为key, 目标变量作为value
    • 主要方法有set()get()
      • set():ThreadLocalMap中维护threadLocal和变量的关系
      • get(): 将目标变量返回
    • ThreadLocal是一个特殊的容器
  • 应用场景:
    • 用户登录控制中记录session信息

原子类

  • 如果使用原子包装类,比如AtomicInteger. 或者使用自定义的方式保证原子操作,本质上和synchronized类似
  • 对于某些对象出现属性丢失的情况:
    • 对象oldObject == currentObject
    • oldObject.getProperty() != currentObject.getProperty()
    • 这时,可以使用AtomicReference中的AtomicStampedReference为对象加上版本号

Lock类

  • 作用: 为了解决同步问题,处理资源争用. 主要目的和synchronized类似
  • Locksynchronized比较:
    • Lock:
      • Lock更加灵活,可以灵活定义多把锁的加锁和解锁顺序
      • 提供多种加锁方案.比如阻塞式lock, 无阻塞式trylock, 打断式lockInterruptily, 带超时时间版本的trylock
      • 可以和Condition类联合使用
      • Lock锁的性能比synchronized更高
    • synchronized:
      • synchronized中的锁按照先加后解的顺序定义
  • Lock锁的三种实现:
    • ReentrantLock
    • ReentrantReadWriteLock.ReadLock
    • ReentrantReadWriteLock.WriteLock
  • ReentrantLock:
    • 可重入锁
    • 可重入是指持有锁的线程可以继续持有,并且要释放对等的次数才能真正释放锁
  • ReentrantReadWriteLock:
    • 可重入读写锁
    • 可重入读写锁中的读锁ReadLock和写锁WriteLock都有lock()方法和unlock()方法
    • 写写互斥,写读互斥
    • 读读不互斥
    • 可以实现高效的线程安全的并发读

容器类

  • BlockingQueue:
    • 阻塞队列
      • 队列Queue是一个单向队列,可以在队头添加元素和队尾删除或者取出元素
      • 类似一个管道,适用于一些先进先出策略的应用场景
      • 普通的Queue接口的主要实现有优先队列PriorityQueue
      • 阻塞队列BlockingQueue在队列的基础上添加了多线的协作的功能
    • 除了Queue中的方法外 ,BlockingQueue中提供了阻塞接口put()take(), 带有超时功能的阻塞接口offer()poll()
      • put: 在队列满时阻塞,直到有空间才会被唤醒
      • take: 队列空时阻塞,直到有元素才会被唤醒
    • 适用于生产者-消费者模型的应用场景
  • 常见的阻塞队列:
    • ArrayListBlockingQueue
    • LinkedListBlockingQueue
    • DelayQueue
    • SynchronousQueue
  • ConcurrentHashMap:
    • 高线的线程安全的HashMap, 功能和HashMap类似

线程管理类

  • 线程管理类: 用于管理线程,本身可能并不是多线程的,但提供一些机制对多线程工具进行封装来实现对多线程的管理
  • 线程管理类主要有以下两种:
    • ThreadPoolExecutor
    • ThreadMXBean(JMX)
  • ThreadPoolExecutor中构造参数说明:
    • corePoolSize: 线程的初始值.也是最小值.空闲状态时,也会保持这个数目的线程
    • maximumPoolSize: 线程的最大值. 线程的数目始终不会超过这个值
    • keepAliveTime: 当线程池内的线程数目高于corePoolSize时,经过这个时间后多余的线程才会被回收,回收前处于wait状态
    • unit: 时间参数的单位. 可以使用TimeUnit实例
    • workQueue: 待入任务Runnable的等待队列. 这个参数主要会影响调度策略,比如公平策略,是否产生starving线程
    • threadFactory: 线程工厂类. 默认的参数实现,如果需要自定义实现,需要实现ThreadFactory接口并作为参数传入