线程池 JMM 内存模型
线程池 & JMM 内存模型
文章目录
- 线程池 & JMM 内存模型
- 线程池
- 线程池的创建
- ThreadPoolExecutor 七大参数
- 饱和策略
- ExecutorService 提交线程任务对象执行的方法:
- ExecutorService 关闭线程池的方法:
- 线程池最大线程数如何确定?
- volatile - 多线程之间共享变量的可见性
- JMM 内存模型
- 八种操作
- JMM 对这八种指令的使用,制定了如下规则:
线程池
使用线程池可以实现线程的复用,减少频繁创建和销毁线程对象造成的资源消耗。
好处:
-
降低资源消耗:减少了创建和销毁线程的次数;
-
提高响应速度:不需要频繁的创建线程;
-
提高线程的可管理性:线程池可以约束系统中最多的线程数;
线程池的创建
java.util.concurrent.Executors
提供了一系列静态方法创建线程池对象,线程池在 Java 中表现为 ExecutorService
接口。
-
使用
ThreadPoolExecutor
构造方法; -
使用
Executor
框架的⼯具类Executors
:实际也是调用了ThreadPoolExecutor
构造方法,阿里巴巴开发手册强烈不允许使用该种方式。
FixedThreadPool
: 该方法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
: 方法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先入先出的顺序执⾏队列中的任务。CachedThreadPool
: 该方法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使用可复⽤的线程。若所有线程均在⼯作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复⽤。
推荐创建线程池方式:ThreadPoolExecutor
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, // 核心线程数5, // 最大线程数,超出阻塞队列时才会触发最大线程数3, // 超出核心线程数时,终止空闲线程的等待时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue<Runnable>(3), // 阻塞队列Executors.defaultThreadFactory(), // 默认创建线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
ThreadPoolExecutor 七大参数
-
corePoolSize
:核心线程数,保留在线程池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
; -
maximumPoolSize
:最大线程数,当工作队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最⼤线程数; -
keepAliveTime
:当线程数大于核心线程数时,这是多余空闲
线程在终止前等待新任务的最长时间; -
unit:keepAliveTime
参数的时间单位; -
workQueue
:工作队列,超出核心线程数的任务会保存在任务队列, 这个队列将只保存 execute 方法提交的 Runnable任务; -
threadFactory
:执行程序创建新线程时使用的工厂; -
handler
:饱和策略(拒绝策略),执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量;
饱和策略
-
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务(使用提交该任务的线程执行)。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
ExecutorService 提交线程任务对象执行的方法:
-
Future<?> submit(Runnable task):提交一个 Runnable 的任务对象给线程池执行;
-
Future<?> submit(Callable task):提交一个 Callable 的任务对象给线程池执行,可以通过 get() 得到返回结果。
-
public void execute(Runnable command):不返回结果使用 execute;
ExecutorService 关闭线程池的方法:
-
shutdown()
:等待任务执行完毕以后才会关闭线程池; -
shutdownNow()
:立即关闭线程池的代码,无论任务是否执行完毕,相当于给每个线程调用了 intercept() 方法。
线程池提交线程任务对象会自动启动线程执行。
ExecutorService pools = Executors.newFixedThreadPool(3);// 实现 Callable 接口
Future<String> result= pools.submit((Callable<String>) () -> {});
// 获取线程执行返回的结果
String r = result.get()// 实现 Runnable 接口
pools.submit(() -> {});
线程池最大线程数如何确定?
-
CPU 密集型:定义为机器 CPU 核数,
Runtime.getRuntime().availableProcessors()
-
IO 密集型:定义为 IO 密集线程的 2 倍;
线程池主要执行流程
volatile - 多线程之间共享变量的可见性
引入:A 线程修改了共享变量的值,但是在主线程中读取到的还是之前的值,修改后的值无法读取到。
解释:根据 JMM 内存模型,线程在本地内存中会有共享变量的副本,A 线程修改了其本地内存的变量更新到主内存中,当主线程操作共享变量时,还是读取的主线程中本地内存的共享变量副本,此时还没有跟主内存共享变量同步,导致无法读取到修改后的值,这个现象称为线程之间共享变量的不可见性。
解决方案:
- 加锁:
a. 线程获得锁;
b. 清空本地工作内存;
c. 从主内存中拷贝最新值到工作内存;
- 使用 volatile 关键字修饰共享变量:使用 volatile 关键字修饰的变量被修改后,主内存更新后会将其他线程本地工作内存中的变量副本失效,重新读取主内存的最新值,从而保证了可见性。
区别:volatile 关键字不保证原子性。
volatile 是 Java 虚拟机提供轻量级的同步机制。
特点:
-
保证可见性
-
不保证原子性
-
禁止指令重排
JMM 内存模型
JMM
(Java Memory Model),Java 内存模型。是 Java 虚拟机规范中所定义的一种内存模型。 Java 内存模型(Java Memory Model)描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的底层细节。 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
八种操作
内存交互操作有 8 种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read 和 write 操作在某些平台上允许例外)
-
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
-
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
-
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load动作使用;
-
load (载入):作用于工作内存的变量,它把 read 操作从主存中变量放入工作内存中;
-
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
-
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
-
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用
-
write (写入):作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中
JMM 对这八种指令的使用,制定了如下规则:
-
不允许 read 和 load、store 和 write 操作之一单独出现。即使用了read 必须 load,使用了store 必须 write
-
不允许线程丢弃他最近的 assign 操作,即工作变量的数据改变了之后,必须告知主存
-
不允许一个线程将没有 assign 的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施 use、store 操作之前,必须经过 assign 和 load 操作
-
一个变量同一时间只有一个线程能对其进行 lock。多次 lock 后,必须执行相同次数的 unlock 才能解锁
-
如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load 或 assign 操作初始化变量的值
-
如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量
-
对一个变量进行 unlock 操作之前,必须把此变量同步回主内存