Java知识点整理 2 — 多线程

一. 什么是进程和线程

  • 进程是系统运行程序的基本单位,是程序的一次执行过程,是动态的。比如可以通过Windows的任务管理器查看系统当前运行的进程(.exe文件的运行)。
  • 线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程,与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。因此系统产生一个线程或者在多个线程间切换,负担要比进程小得多,线程也称为轻量级进程

一个Java程序的运行是main线程和多个其它线程共同运行。

二. 进程与线程的区别

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于,基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,而进程正相反。

扩展知识点:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的,而堆和方法区是共享的?

  • 程序计数器的主要作用是:一是通过程序计数器来依次读取指令,实现代码的流程控制;二是在多线程环境中,程序计数器用来记录线程的执行位置,使得线程被切换回来后能知道上次运行到哪里了。因此其私有主要是为了线程切换后能够恢复到正确的执行位置。
  • 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 
  • 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

三. 并发与并行,同步与异步

  • 并发是指两个及两个以上作业在同一时间段内执行;并行是指两个及两个以上作业在同一时刻执行。
  • 同步是指发出一个调用之后在没有得到结果之前,该调用不得返回,需要一直等待;异步是指发出一个调用后无需等待结果,可以直接返回。

四. 为什么要使用多线程

从计算机底层来看,线程可以比作轻量级进程,是程序的最小执行单位,系统对线程的切换与调度管理的成本比进程要小得多。并且在多核CPU时代,允许多个线程同时运行,减小了线程上下文切换的开销。

五. 使用多线程可能带来的问题

可能会造成内存泄露、死锁、线程不安全等问题。

线程安全与否是指在多线程环境下,对同一份数据的访问,能否保证其正确性和一致性的描述。

六. 如何创建线程

创建线程有多种方式,比如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池等。但严格来说,这些都是在Java中使用多线程的方法,真正创建线程只有new Thread() . start()创建。

七. 线程的生命周期和状态

  • NEW: 初始状态,线程被创建出来但没有被调用start( ) 。
  • RUNNABLE: 运行状态,线程被调用了start( ) 等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

八. 什么是内存泄露、上下文切换

内存泄露是指程序中已分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致系统运行速度减慢甚至系统崩溃。

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器,栈信息等。当出现如下情况时,线程会从占用 CPU 的状态中退出:

  • 主动让出 CPU,比如调用了 sleep(), wait() 等
  • 时间片用完,因为操作系统要防止一个线程长时间占用 CPU 导致其他线程饿死
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞
  • 被终止或结束运行

在发生线程切换时,需要保存当前线程的上下文,留着线程下次占用 CPU 时恢复现场,并加载下一个将要占用 CPU 的线程上下文。这就是 上下文切换

【上下文切换是操作系统的基本功能,每次需要保存信息、恢复信息,会占用 CPU、内存等系统资源,也就意味着效率会有损耗,如果频繁切换就会造成效率低下。】

九. 死锁

死锁是指多个线程同时竞争一个资源,并处于互相等待的状态。

产生死锁的四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:多个线程之间形成一种头尾相接的循环等待资源关系。

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件:一次性申请所有的资源。
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件:按序申请,反序释放。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。

十. sleep()和wait()方法对比

共同点:两者都可以暂停线程的执行。

区别

  • sleep()方法没有释放锁,而wait()方法释放了锁
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是Thread类的静态本地方法,wait() 则是Object类的本地方法。

十一. 为什么wait()方法不定义在Thread类中?

wait() 是让获得对象锁的线程等待,会自动释放当前线程占有的对象锁。操作的是对象。

每个对象都有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,就要操作相应的对象而非当前线程(Thread)。

sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

十二. 可以直接调用Thread类的run方法吗?

不可以,因为直接执行run方法并不会以多线程的方式执行,会将其当成是main线程下的普通方法去执行,所以这并不是多线程工作。而真正的多线程工作是,new一个Thread,线程进入新建状态,然后调用start()方法,会启动一个线程并进入就绪状态,当分配到时间片后就可以运行。start()会执行相应的线程准备工作,并自动运行run()方法。


十三. volatile关键字

三个特性:

  • 保证变量可见性
  • 禁止指令重排序
  • 无法保证原子性

(1)如果我们将变量声明为volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

(2)指令重排序是指在编译器或者处理器优化程序执行过程中,对指令的执行顺序进行重新排序。在单线程环境下,这种重排序是没有问题的,因为最终的执行结果是一样的。但在多线程环境下,如果指令重排序被允许,可能会导致多个线程之间出现数据不一致的情况。

(3)原子性是指一个操作或者一系列操作要么全部执行成功并且不可中断,要么都不执行。

十四. synchronized关键字

synchronized,即同步。主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。早期,它属于重量级锁,效率低,之后引入大量优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁的开销,提升了效率。

(1)如何使用?

  • 修饰实例方法:给当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁。
  • 修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁
  • 修饰代码块:对指定对象(synchronized(object))/类(synchronized(类.class))加锁,进⼊同步代码块前要获得给定对象/Class的锁。

synchronized关键字加到static静态方法和synchronized(class)代码块上都是对class类加锁,加到实例方法上是给对象实例加锁。尽量不要使用synchronized(String a),因为JVM中字符串常量池具有缓存功能(实际上锁定的是字符串常量池中的对象,而非字符串本身)。

(2)底层原理

synchronized关键字底层原理属于JVM层面的东西。

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置

synchronized 修饰的方法并没有这两个指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

两者的本质都是对对象监视器 monitor 的获取。

(3)JDK1.6之后对synchronized的优化

1.6对锁的实现引入了大量优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化来减少锁开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

(4)synchronized和volatile的区别:

synchronized 关键字和 volatile 关键字是互补的。

  • volatile是线程同步的轻量级实现,它性能比synchronized关键字好。但 volatile 关键字只能用于变量,而 synchronized关键字可以修饰方法及代码块。
  • volatile能保证数据的可见性,但不保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile主要解决变量在多个线程间的可见性,synchronized解决多个线程访问资源的同步性。

十五. ReentrantLock

  • ReentrantLock 是一个可重入、独占式的锁,实现了 Lock 接口,和 synchronized 关键字类似。但比它更灵活强大,增加了轮询、超时、中断、公平锁和非公平锁等功能。
  • ReentrantLock 里有一个内部类 Sync,Sync 继承 AQS(Abstract Queued Synchronizer),添加和释放锁的大部分操作都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。
  • ReentrantLock 默认使用非公平锁,也可以通过构造器显式指定使用公平锁。

十六. 公平锁与非公平锁

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

十七. synchronized和ReentrantLock的区别

(1)两者都是可重入锁。

可重入锁也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁时还是可以获取的,如果是不可重入锁的话,就会造成死锁。JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。

(2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。

synchronized依赖于JVM实现的,1.6对它进行了优化,但这些优化都在虚拟机层面实现,并没有暴露给我们。

ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)。

(3)ReentrantLock 比 synchronized 增加了一些高级功能。

  • 等待可中断 : ReentrantLock提供了能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。正在等待的线程可选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定公平锁或非公平锁。synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法结合可以实现等待/通知机制。ReentrantLock类可以借助Condition接口与newCondition()方法实现。

十八. 乐观锁和悲观锁

(1)悲观锁:

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

(2)乐观锁:

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

(3)应用场景:

  • 悲观锁多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。(不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也可以考虑使用乐观锁的,视实际情况而定)

乐观锁多于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。(不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类))

十九. 乐观锁的实现

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。

  • 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version值相等时才更新,否则重试更新操作,直到更新成功。举例:操作员AB同时操作银行账户。

  • CAS算法

Compare And Swap,比较与交换。用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。预期值是你认为它当前应该是多少,而不是要更新成的那个值。

举例:

线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
  2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

二十. 乐观锁存在的问题

(1)ABA问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其它线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其它值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳

(2)循环时间长,开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

(3)只能保证一个共享变量的原子操作


二十一. ThreadLocal

(1)什么是ThreadLocal

用于实现每个线程都有自己专属的本地变量。ThreadLocal类主要解决的是让每个线程绑定自己的值,可以将ThreadLocal类比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果创建了一个ThreadLocal变量,那访问变量的每个线程都会有这个变量的本地副本。(本地副本指的是每个线程独立保存的该变量的拷贝,每个线程对该变量的操作仅会影响自己的本地副本,不会影响其他线程的副本)

可以用get和set方法来获取默认值或将其值改为当前线程所存的副本值,从而避免线程安全问题。

再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

(2)ThreadLoacl原理

  • 通过查看Thread源代码,可以知道 Thread 类中有两个变量 threadLocals 和 inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,可以把ThreadLocalMap理解为ThreadLocal类实现的定制化的HashMap。

  • 变量放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal 上,ThreadLocal 可理解为ThreadLocalMap的封装,传递了变量值。

  • 每个Thread都有一个自己的ThreadLocalMap,可以简单地将它的key视作ThreadLocal,value就是 ThreadLocal对象调用set方法设置的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现线程隔离。

(3)ThreadLocal内存泄露问题

ThreadLocalMap使用的key是ThreadLocal的弱引用,value是强引用,如果ThreadLocal没有被外部强引用的话,在垃圾回收时key会被清理掉,而value不会被清理掉,这样ThreadLocalMap中就出现key为null的Entry,假如不做任何处理的话,value永远不会被GC回收,这时就产生内存泄漏了。ThreadLocalMap考虑到这种情况,在调用set、get、remove方法时,会清理掉key为null的记录,使用完ThreadLocal方法后,最好也手动调用remove方法。

二十二. Java的四种引用类型

  • 强引用:new出来的对象就是强引用类型,只要强引用存在,即使内存不足,GC也不会回收被引用的对象。
  • 软引用:使用 SoftReference 修饰的对象,内存不足时,软引用指向的对象会被GC。
  • 弱引用:使用 WeakReference 修饰的对象,只要发现,就会被回收。
  • 虚引用:最弱的引用,使用 PhantomReference 定义。唯一作用是用队列接收对象即将死亡的通知。

二十三. 线程池的定义和使用

线程池就是用来管理一系列线程的资源池,当需要处理任务时,直接从线程池中调用线程,处理完任务后线程不会被立即销毁,而是等待下一个任务。

使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以有效管理调度线程。

如何创建线程池:

  1. 通过ThreadPoolExecutor构造函数创建(推荐)。
  2. 通过Executor框架的工具类Executors创建。

通过工具类 Executors 可以创建四种类型的 ThreadPoolExecutor:

  • FixedThreadPool:创建固定线程数量的线程池。当有一个新任务提交时,线程池中如有空闲线程,就立即执行。如果没有,新的任务会暂存在任务队列中,等有线程空闲时,就处理任务队列中的任务。
  • SingleThreadExecutor:创建只有一个线程的线程池。如果多于一个任务提交到线程池,任务会被保存在任务队列中,等线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool:线程数量随着任务增加而增加。如果线程任务执行完且空闲了一段时间则会被回收。
  • ScheduledThreadPool:该方法创建用来在给定延迟后运行任务或定期执行任务的线程池【可用作定时器】。

二十四. 线程池的参数

public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               )

7个参数,分别是:

  • corePoolSize:系统能同时工作的核心线程数。
  • maximumPoolSize:极限情况下,线程池允许的最大线程数。
  • workQueue:任务队列。
  • keepAliveTime:临时线程的存活时间。
  • TimeUnit:临时线程的存活时间单位。
  • ThreadFactory:线程工厂,用来创建线程,一般默认。
  • RejectExecutionHandler:拒绝策略。

拒绝策略有哪些:

当系统已经达到最大线程数并且任务队列也满,触发拒绝策略。

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。默认策略。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。【主线程直接调用任务的run()方法从而绕过线程池执行,由老板亲自负责处理新任务
  • ThreadPoolExecutor.DiscardPolicy不处理新任务,直接丢弃任务。但是不抛出异常,这是不推荐的做法。
  • ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。

常用阻塞队列有哪些:

新任务到来时会先判断当前线程数是否达到核心线程数,如果达到,则将新任务放到任务队列。不同的线程池会选用不同的阻塞队列,有三类:无界队列、同步队列、延迟阻塞队列。

  • LinkedBlockingQueue(无界队列):用于FixedThreadPool 和 SingleThreadExector 。由于队列永远不会被放满,FixedThreadPool最多只能创建核心线程数的线程【不会创建临时线程】【容量为 Integer.MAX_VALUE】任务较多时,会导致 OOM(内存溢出)。
  • SynchronousQueue(同步队列) :用于CachedThreadPool 。同步队列没有容量,不存元素,目的是保证对于提交的任务,如果有空闲线程,就用空闲线程来处理,否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,导致 OOM。
  • DelayedWorkQueue(延迟阻塞队列):用于ScheduledThreadPool。延迟阻塞队列的内部元素不是按照放入的时间排序,而是按照延迟的时间长短,(内部采用堆,保证每次出队的任务都是当前队列中执行时间最靠前的) 队列元素满了之后会自动扩容原来容量的 1/2,永远不会阻塞,最大可扩容Integer.MAX_VALUE,最多只能创建核心线程数的线程。

二十五. 线程池的工作机制

  • 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  • 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入任务队列里等待执行。
  • 如果任务队列已经满了,但是当前运行的线程数是小于最大线程数的,就新建一个临时线程来执行任务。
  • 如果当前运行的线程数已经等于最大线程数了,那新任务会被拒绝,使用拒绝策略处理。

举个例子:可以把线程池理解为公司,任务队列是备忘录,核心线程数是正式员工数量,假设为2,最大线程数可以是核心员工加临时员工的数量,假设为4,任务队列长度为2。

最初是空的,当新来一个任务1时,正式员工1去处理,又来一个任务2,正式员工2去处理。又来一个任务3,此时corePoolSize满了,但maximumPoolSize没满,将任务3放到任务队列。又来一个任务4,同样放到任务队列,此时workQueue也满了。又来一个任务5,正式员工和任务队列都满了,但最大线程数没满,还可以招临时员工,因此让临时员工1处理任务5,又来一个任务6,同样招另一个临时员工2处理任务6,此时最大线程数也慢了。但又来一个任务7,已经无法处理,因此调用拒绝策略,默认拒绝任务并抛出异常。如果后续没有任务了,并且当前线程数量超过corePoolSize,等keepAliveTime时间到达后,就可释放临时线程,也就是解雇临时员工。

二十六. 线程池大小的设定

CPU密集型任务:占用大量CPU资源,比如视频处理、逻辑计算、图像处理等。

IO密集型任务:占用带宽/内存/硬盘的读写资源,比如网络读取、文件读取等。

  • CPU密集型的任务:核心线程数量 = CPU的核数 + 1
  • IO密集型的任务:核心线程数量 = CPU的核数 * 2

线程池参数动态化是美团提出的一个新型解决方案。

二十七. Executor工具类常用方法

Executors工具类提供了很多静态方法,通过调用静态方法返回不同特点的线程池对象。这些方法的底层都是通过线程池ExecutorService的实现类ThreadPoolExecutor创建的线程池对象

工具类中的常用方法:

  • ExecutorService newFixedThreadPool
  • newSingleThreadExecutor
  • newCachedThreadPool
  • newScheduledThreadPool

《阿里巴巴开发手册》不允许使用 Executors 创建线程池,推荐通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让开发者更加明确线程池的运行规则,规避资源耗尽的风险。【建议使用ThreadPoolExecutor来指定线程池参数】

二十八. ExecutorService接口的常用方法

  • void execute(Runnable target) 执行任务,没有返回值,一般用来执行 Runnable 任务。
  • Future<T> submit(Callable<T> task) 执行任务,返回Future任务对象,获取线程结果,一般用来执行 Callable 任务。
  • void shutdown() 任务执行完关闭线程池。
  • List<Runnable> shutdownNow() 立即关闭,停止正在执行的任务,并返回队列中未执行的任务列表。

两者对比:

  • execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值。

二十九. Future类的作用

Future类是异步思想的典型应用,它提供了一种检查异步计算是否完成的机制,以及在计算完成时获取计算结果的方法。主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。

在Java中,Future类是一个泛型接口,主要包括以下功能:

  • 取消任务(cancel);
  • 判断任务是否被取消(isCancelled);
  • 判断任务是否已经执行完成(isDone);
  • 获取任务执行结果(get)。

三十. AQS

Abstract Queued Synchronizer,抽象队列同步器。AQS 就是一个抽象类,主要用来构建锁和同步器。使用 AQS 能简单且高效地构造出应用广泛的大量的同步器。

核心思想:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH(Craig,Landin,and Hagersten)队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

AQS 使用 int 成员变量state表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作。

资源共享方式:

AQS有两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

基于AQS的常见同步类工具:

  • Semaphore(信号量)
  • CountDownLatch(倒计时器)
  • CyclicBarrier(循环栅栏)

三十一. Atomic原子类

 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。原子类说简单点就是具有原子/原子操作特征的类。

基本数据类型原子类的优势:不需要加锁也可以实现线程安全。

AtomicInteger线程安全原理解析:主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

原子类有哪些类别:

根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类

基本类型:使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

数组类型:使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来
  • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

三十二. Java常见并发容器(并发包)

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnWriteArrayList:线程安全的List,在读多写少的场合性能非常好,远远好于Vector。
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的LinkedList,这是一个非阻塞队列。
  • BlockingQueue:这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap:跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。

这部分目前先到这儿,后续可能会更新修改。

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Phoenixxxxxxxxxxxxx

感谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值