type
status
date
slug
summary
tags
category
icon
password
URL

线程

进程是操作系统分配资源的最小单位,线程是操作系统调度的最小单位 一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈 和 本地方法栈
为什么要有线程
  1. 线程切换的代价远小于进程
特性
进程切换
线程切换
地址空间
需要切换
不需要切换(同一进程内)
上下文保存
程序计数器、寄存器、内存管理信息(MMU)
程序计数器、寄存器
开销
较高
较低
资源共享
不共享资源
共享同一进程的资源
适用场景
隔离性高的多任务处理
高并发、轻量级任务切换
  1. 利用多线程可以大大提升系统并发能力

JMM(Java 内存模型)

volatile

  1. 保证数据的可见性,原始的意义就是禁用 CPU 缓存
  1. 禁止指令重排
  1. 不能保证数据的原子性
 

悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候都会出现竞争的情况(比如共享数据被修改)

乐观锁

💡
乐观锁一般会使用版本号机制或 CAS 算法实现。
乐观锁总是假设最好的情况,认为共享资源每次被访问不会出现竞争的情况。
乐观锁线程不会挂起,通过CAS轮询直到成功。所以当竞争激烈时,CPU会频繁重试。大量失败重试,LongAdder以空间换时间的方式就解决了这个问题。

CAS

💡
CAS 的全称是 Compare And Swap(比较与交换) CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。单可以使用AtomicReference把多个变量放在一个对象里来进行 CAS 操作。

CAS ABA问题

一个变量 V 经历 A → B → A的过程,光通过值无法判断是否V被修改过
解决方案
在变量前面追加上版本号或者时间戳

synchronized

ReentrantLock

ReentrantLock是一个可重入且独占式的锁.不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock 里面有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer).
Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类.
ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁
 

AQS 抽象队列同步器

💡
ReentrantLock 的底层就是由 AQS 来实现的
 

Semaphore

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
可以用来控制并发度

CountDownLatch

用来控制并行度
CountDownLatch 的两种典型用法
  1. 某一线程在开始运行前等待 n 个线程执行完毕 : 将 CountDownLatch 的计数器初始化为 n (new CountDownLatch(n)),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()),当计数器的值变为 0 时,在 CountDownLatch 上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  1. 实现多个线程开始执行任务的最大并行性:注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

CyclicBarrier

加强版CountDownLatch
 

AQS 如何阻塞线程

💡
LockSupport.park()
  • 功能LockSupport.park() 方法用于使当前线程进入等待状态,直到另一个线程调用 unpark() 方法唤醒它。
  • 阻塞时机park() 方法会导致当前线程阻塞,它可以在任何时刻被调用,而不受任何限制。
  • 无时限等待park() 方法不会引入任何时限或时间限制,因此可以在等待的线程被显式唤醒之前一直持续下去。
  • 中断响应park() 方法不会抛出 InterruptedException 异常,因此线程在等待过程中不会被中断。
  • 线程状态:调用 park() 方法后,线程的状态会变为等待状态,不会持有任何锁。

LockSupport.park() vs Thread.sleep()

  • 功能Thread.sleep() 方法用于使当前线程进入休眠状态,即暂停执行一段指定的时间。
  • 阻塞时机sleep() 方法会导致当前线程阻塞,但它只能在非静态上下文中被调用,即只能由 Thread 对象调用。
  • 时限等待sleep() 方法引入了一个时间限制,即使没有其他线程调用 interrupt() 方法,也会在指定的时间后自动唤醒。
  • 中断响应sleep() 方法会在收到中断请求时抛出 InterruptedException 异常,因此可以通过捕获异常来响应中断请求。
  • 线程状态:调用 sleep() 方法后,线程的状态仍然保持为可运行状态,不会释放持有的锁。

ReentrantReadWriteLock

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全
  • 一般锁进行并发控制的规则:读读互斥、读写互斥、写写互斥。
  • 读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)。

线程持有读锁还能获取写锁吗?

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

StampedLock

  • 写锁:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。
  • 读锁 (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。
  • 乐观读:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

Condition

 

ThreadPool

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

JDK ThreadPool内置类型

  • FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
  • FixedThreadPoolSingleThreadExecutor:使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
  • ScheduledThreadPoolSingleThreadScheduledExecutor:使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数 :
  • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :拒绝策略(后面会单独详细介绍一下)。
DelayWorkQueue
根据delay的时间排序,跟PriorityQueue基于堆实现

线程池的拒绝策略有哪些?

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:
  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

不同的线程池的阻塞队列

  • 容量为 Integer.MAX_VALUELinkedBlockingQueue(无界队列):FixedThreadPoolSingleThreadExectorFixedThreadPool最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExector只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
  • SynchronousQueue(同步队列):CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。
  • DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。

线程池处理任务的流程了解吗?

execute/submit → ThreadPoolExcutor.addWorker() → t.start()
notion image

任务优先级线程池

参考DelayWorkQueue根据Delay时间排序

动态修改线程池参数

Future

Future
Callable
CompletableFuture
FutureTask
 

🤗 总结归纳

总结文章的内容

📎 参考文章

 
git hooksetcd数据迁移
Loading...