JUC 之 ThreadPoolExecutor 的一些研究
ThreadPoolExecutor 概述:=====================================================================
构造函数:
4个构造函数, 其实最终都是调用了这个:
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < ||
maximumPoolSize <= ||
maximumPoolSize < corePoolSize ||
keepAliveTime < )
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
} 其中:
int corePoolSize, 核心线程数, 个人任务 理解为最小的工作线程数 更好。
int maximumPoolSize, 最大工作线程数, 也就是最大pool size
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler
RejectedExecutionHandler 有这么几种:
AbortPolicy 直接抛出异常
CallerRunsPolicy 使用用户线程运行, CallerRunsPolicy其实保证了 无论怎么样,即使等待了很长时间,所以的工作任务都会被执行,而绝不会被舍弃。
DiscardOldestPolicy 舍弃工作的阻塞队列头的工作任务 ,the head of this queue ,然后重新运行当前任务。 也就是舍弃最 先进入队列的任务。 也就是最老的那个。
DiscardPolicy 直接舍弃,也就是忽略当前添加的工作任务
注意:
CallerRunsPolicy 是可能会影响 当前的用户线程的运行的。
AbortPolicy 可能会抛出给 用户 线程, 故。。 但是, 对之前添加的 Worker 无影响。
主要的属性或者方法:================================================================
addWorker 方法签名:private boolean addWorker(Runnable firstTask, boolean core) { 尝试为 原始线程 新创建 工作线程。
addWorkerFailed 新创建 工作线程怎么办?
advanceRunState
afterExecute 钩子
allowCoreThreadTimeOut() 是否允许核心线程超时? 默认是false, 可以通过allowCoreThreadTimeOut方法动态设置
allowsCoreThreadTimeOut 返回 allowCoreThreadTimeOut
awaitTermination 签名:public boolean awaitTermination(long timeout, TimeUnit unit) 等待线程池的状态变为TERMINATED,timeout内返回true,否则false。 接下来怎么办, 自己根据返回值去处理。
beforeExecute 钩子
checkShutdownAccess
compareAndDecrementWorkerCount cas方式增加工作线程的数目。一般来说是用在 新建工作线程成功的时候。
compareAndIncrementWorkerCount cas方式减少工作线程的数目。一般来说是用在 工作线程退出的时候。
ctlOf
decrementWorkerCount 无论如何,都要成功减少工作一个线程的数目, 只需一个。
drainQueue
ensurePrestart 即使corePoolSize is 0, 也要至少启动一个工作线程。
execute 关键的工作执行方法
finalize
getKeepAliveTime
interruptIdleWorkers(boolean onlyOne) 中断空闲的worker
interruptIdleWorkers() 调用前者
interruptWorkers 中断所有的worker。 Interrupts all threads, even if active. Ignores SecurityExceptions
isRunning 是否运行?
isRunningOrShutdown
onShutdown
prestartAllCoreThreads 预先启动所有core 线程。 这个方法用的少。 它会导致core 线程idly wait for work,默认是starting core threads only when new tasks are executed
prestartCoreThread 预先启动一个core 线程。 这个方法也用的少
processWorkerExit 钩子
purge
reject
remove
runStateAtLeast
runStateLessThan
runStateOf
runWorker 这个是被 Worker 调用的,是真正的 。。
setKeepAliveTime
shutdown 优雅关闭线程池
shutdownNow 强制关闭线程池
terminated
toString
tryTerminate 尝试Terminate ?
workerCountOf
allowCoreThreadTimeOut 是否允许核心线程超时
CAPACITY final 常量,固定为
COUNT_BITS
ctl
defaultHandler 默认reject handler
handler
keepAliveTime core 意外的线程的超时时间。 keepAliveTime其实完全可以理解为 keepAlived timeOut
mainLock 需要一个 JUC 的Lock, 为什么。
ONLY_ONE
// 几种状态
RUNNING
SHUTDOWN
STOP
TERMINATED
TIDYING
termination
shutdownPerm
workers 这个很关键, 它线程池是内部的 一个保留Worker 的缓存Set: 签名 private final HashSet<Worker> workers = new HashSet<Worker>();
workQueue
================================ IDEA 计算出来的属性 ================================
activeCount: int 实际正在运行 Worker 线程
completedTaskCount: long 所有总共完成的 "任务"。
corePoolSize: int 核心的(其实也就是最小的)Worker 线程数
largestPoolSize: int 曾经达到的最大的 实际运行的Worker 线程数
maximumPoolSize: int (潜在的)最大的 Worker 线程数
poolSize: int 当前线程池的 线程数, 不管线程是否正在运行 还是空闲
public BlockingQueue<Runnable> getQueue() {: BlockingQueue<Runnable> 阻塞队列
rejectedExecutionHandler: RejectedExecutionHandler 拒绝执行的策略
shutdown: boolean 是否关闭?
task: Runnable
taskCount: long
terminated: boolean
terminating: boolean
threadFactory: ThreadFactory
================================ IDEA 计算出来的属性 END ================================
内部类 Worker:
它是实际的工作线程:(其实是对原始线程或Runnable的封装)。 Worker 到底是什么东东? 它是这样的:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable Worker
interruptIfStarted
lock
run
tryAcquire
tryLock
tryRelease
unlock
completedTasks
firstTask
serialVersionUID
thread
}
ThreadPoolExecutor 线程池 的个人理解 =====================================================================
线程池 最关键的两个属性: 工作线程的Set + 阻塞队列
private final HashSet<Worker> workers = new HashSet<Worker>(); // workers 的本质是保存所有的实际的工作的线程。
private final BlockingQueue<Runnable> workQueue; // workQueue 的本质是 对某个时刻 过多的任务 task 做缓冲。( 个人认为 缓冲 比缓存更合适此时的场景)
线程池本质上是 上面的两个属性:workers + workQueue 和 围绕它们的一些方法。
所以,JUC线程池,可以简单这么理解:
用户构造线程池,然后,随意的对线程池发出了各种各样的执行任务的请求,线程池检查 其本身两个属性:workers + workQueue。按照下面的规则处理任务请求:
1 如果当前pool size < corePoolSize, 则直接添加工作线程 Worker。
2 如果当前pool size >= corePoolSize,且 < maximumPoolSize,那么使用workQueue 缓冲之。 如果workQueue 无法再缓冲了,那么继续添加Worker
2 如果当前pool size >= maximumPoolSize,那么使用RejectedExecutionHandler 拒绝之。
Worker 运行完任务后,不会立即退出,而是立即从workQueue 获取task。获取也要分情况,如果当前pool size <= corePoolSize, 那么一直等待(可理解为阻塞)直到用户有新的工作任务请求发过来。 否则,当然就是pool size > corePoolSize 的情况, 那么就最多等待unit个keepAliveTime的时间,获取到了那么就立即运行,否则 就退出, 也就是工作线程退出,把 自己从workers 移除。
pool size 就是 当前Worker的总数,包括正在运行的和空闲的。
注意, 只有工作线程数已经达到了maximumPoolSize,而用户还在添加工作任务时, RejectedExecutionHandler 才会生效。
workQueue 的 缓冲能力 完全取决于workQueue的capacity,pool 能够同时接受的工作任务 == maximumPoolSize + workQueue的capacity 。
workers.size 的最大值 = maximumPoolSize;
也就是 : max pool size = maximumPoolSize + workQueue.size()
workQueue 的capacity可以是0(比如SynchronousQueue ),也就是 没有缓冲能力。 这个时候, 工作任务会直接被 之前创建的空闲Worker 执行(如果有的话) 或创建新的 Worker 执行。
keepAliveTime 直接影响了 Worker 的退出。 每个新建Worker 执行完第一个 工作任务后, 会循环去 workQueue 中拿被缓冲的 工作任务, 拿到则执行, 拿不到则等待一个 keepAliveTime 的时间。 所以, 如果 workQueue 中还有工作任务, 那么 Worker 是不会退出的。
源码分析: =====================================================================
线程从调用 ThreadPoolExecutor 的submit 方法开始, ThreadPoolExecutor 调用 AbstractExecutorService 的 submit:
public Future<?> submit(Runnable task) { // command 就 工作任务
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null); // 创建一个 RunnableFuture
execute(ftask);
return ftask;
}
然后调用 execute :
public void execute(Runnable command) { // command 的拒绝主要是在这个方法: execute完成的
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread // whether we should have added a thread 不太好理解。
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get(); // 我的理解: 这里应该是规则1, 如果工作线程数小于 核心线程数,直接增加线程。
if (workerCountOf(c) < corePoolSize) {//工作线程数 < 核心线程数 则 直接添加
if (addWorker(command, true)) // 成功则直接返回吧,但有可能添加失败,失败也是可能的,比如当前工作线程 == 核心线程数-1,同时有多个工作任务过来。
return;
c = ctl.get();
} // 这里应该是规则2, 如果工作线程数大于 核心线程数,小于最大线程数,则直接增加线程
if (isRunning(c) && workQueue.offer(command)) { //如果pool还在运行,那么尝试把 command 缓冲到workQueue 起来。
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))// recheck的意义在于: 虽然workQueue.offer 当前成功了, 但是同时,那个瞬间,pool 状态被改变为SHUTDOWN了,那么根据SHUTDOWN的语义,SHUTDOWN状态下是不应该再继续addWorker, 故先remove,然后再reject这个 command。
reject(command);
else if (workerCountOf(recheck) == ) // 如果还有工作线程,那么就不 addWorker, 因为 Worker 会自动从workQueue中拉取 工作任务; 否则就是表明没有任何工作线程了, 也就是工作线程 数 == 0, 那么 添加工作线程吧。
这种情况出现于 之前的工作任务耗时都很短, 很快运行完了, 故 没有进入 if (workerCountOf(c) < corePoolSize) 判断。。 也有可能 corePoolSize,但是workQueue有缓冲能力, 虽然,corePoolSize, 但是至少也是需要一个线程来运行 workQueue中的 工作任务的!。。
addWorker(null, false);
} // 规则3 体现在哪里呢? 如果工作线程数大于 最大线程数,则reject
else if (!addWorker(command, false))// 再次尝试 缓冲到workQueue, 注意这里的第二个参数是false。可能失败(比如workQueue已经满了。), 那么继续添加Worker吧; reject(command); // 如果工作线程数 >= 最大工作线程数,addWorker 就会成功,否则就失败, 失败则 reject 它
}
考虑一种情况:
若workQueue 没有缓冲能力, 而且corePoolSize也是0, 而且maximumPoolSize 也是0。No, 观察构造函数发现: 不可能出现 maximumPoolSize < corePoolSize
若workQueue 没有缓冲能力, 而且corePoolSize > 0,那么 工作任务其实 直接交给了工作线程处理。。
private static boolean isRunning(int c) { // 判断pool 状态是否是 小于SHUTDOWN。
return c < SHUTDOWN;
}
addWorker(null, false); 的含义:
addWorker 成功后是 运行工作线程, 然后调用下面的方法 runWorker :
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) { // getTask() 方法 会从 workQueue 中窃取工作任务。
w.lock(); // 为什么这里会有一个 lock ? 据说是为了防止其他pool 执行这个task, 匪夷所思。
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
pool 中 线程name 的由来:=====================================================================
Thread 的 toString 是:
public String toString() {
ThreadGroup group = getThreadGroup();
if (group != null) {
return "Thread[" + getName() + "," + getPriority() + "," +
group.getName() + "]";
} else {
return "Thread[" + getName() + "," + getPriority() + "," +
"" + "]";
}
}
而默认的线程工厂类是:
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
} public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
可见,默认情况下,名字是: "pool-" + poolNumber.getAndIncrement() + "-thread-" + threadNumber.getAndIncrement() 。
那么pool.submit(thread); 时给 thread 设置的名字是无效的。 因为实际显示的时候, 不会用到,我们仅仅使用 thread 的 run 方法。 其实 pool 需要submit的并不是 Thread ,而是 Runnable 或者 Callable
pool 工作状态及 生命周期:=====================================================================
pool 状态有:
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
pool 创建好之后,基本就是处于 RUNNING 了。 之后要是不执行 shutdown,shutdownNow, tryTerminate 方法, 它就一直处于 Running 状态。
shutdown 会优雅的关闭pool,它不限制时间,但最终pool 是会关闭的。
shutdownNow 立即强制的关闭 pool
tryTerminate 呢, 只是尝试关闭 pool ,而最终可能对于pool 没有影响?
AbstractExecutorService 提供了几个 重载的 submit 方法: =======================================
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
} /**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) { // 对于这个方法, 返回值其实执行run 之前就已经固定了为 result
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
} /**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
可见: submit 方法都是有返回值的, 当然, 对于Runnable 来说, 返回值只能是 null, 对于 Callable来说, 返回值类型是 其中call 的返回值类型。。
Runnable 其实也会被适配到 Callable。 实际上运行的是 Callable 的 run 方法。
submit 返回了一个Future 对象, 实际上我们还可以通过Future 对我们之前提交的任务进行一些操作, 比如检查是否执行完毕,取消它,等待执行完毕。
关于FutureTask =====================================================================
FutureTask 是RunnableFuture的一个实现:
public class FutureTask<V> implements RunnableFuture<V> {
newTaskFor(task); 返回的实际上就是 FutureTask。 FutureTask 对 Callable 和其返回值同时进行了封装,FutureTask运行的是 Callable run 方法。 FutureTask 提供get 方法, 获取Callable 的返回值。
FutureTask 的 get() 方法不会立即返回,它是异步的, 必须等待 任务运行完成了, 才会有结果,否则阻塞。
FutureTask 另外提供了一个 可超时的 get 方法: get(long timeout, TimeUnit unit):
public V get(long timeout, TimeUnit unit)
FutureTask 有7个状态:
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
pool 的runWorker方法运行的实际是 FutureTask 的run 方法,最终运行 Callable 的run 方法:
THE END =====================================================================
参考:
http://blog.csdn.net/vernonzheng/article/details/8299108 等
JUC 之 ThreadPoolExecutor 的一些研究的更多相关文章
- ThreadPoolExecutor源码解读
1. 背景与简介 在Java中异步任务的处理,我们通常会使用Executor框架,而ThreadPoolExecutor是JUC为我们提供的线程池实现. 线程池的优点在于规避线程的频繁创建,对线程资源 ...
- Java 并发编程-不懂原理多吃亏(送书福利)
作者 | 加多 关注阿里巴巴云原生公众号,后台回复关键字"并发",即可参与送书抽奖!** 导读:并发编程与 Java 中其他知识点相比较而言学习门槛较高,从而导致很多人望而却步.但 ...
- 动态线程池(DynamicTp)之动态调整Tomcat、Jetty、Undertow线程池参数篇
大家好,这篇文章我们来介绍下动态线程池框架(DynamicTp)的adapter模块,上篇文章也大概介绍过了,该模块主要是用来适配一些第三方组件的线程池管理,让第三方组件内置的线程池也能享受到动态参数 ...
- 【JUC】JDK1.8源码分析之ThreadPoolExecutor(一)
一.前言 JUC这部分还有线程池这一块没有分析,需要抓紧时间分析,下面开始ThreadPoolExecutor,其是线程池的基础,分析完了这个类会简化之后的分析,线程池可以解决两个不同问题:由于减少了 ...
- Java - "JUC线程池" ThreadPoolExecutor原理解析
Java多线程系列--“JUC线程池”02之 线程池原理(一) ThreadPoolExecutor简介 ThreadPoolExecutor是线程池类.对于线程池,可以通俗的将它理解为"存 ...
- juc线程池原理(二):ThreadPoolExecutor的成员变量介绍
概要 线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析ThreadPoolExecutor类,来了解线程池的原理. ThreadPoolExecutor数据结构 Thread ...
- JUC - Monitor监控ThreadPoolExecutor
JUC - Monitor监控ThreadPoolExecutor 一个自定义Monitor监控ThreadPoolExecutor的执行情况 TASK WokerTask class WorkerT ...
- JUC - ThreadPoolExecutor
JUC - ThreadPoolExecutor 创建一个ThreadPoolExecutor ThreadPoolExecutor( int corePoolSize, // 保留在池中的线程数,即 ...
- JUC源码分析-线程池篇(一):ThreadPoolExecutor
JUC源码分析-线程池篇(一):ThreadPoolExecutor Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池 ...
随机推荐
- ubuntu base make 未找到命令
引用:https://blog.csdn.net/fenglibing/article/details/7096556 1.先放入UBUNTU安装盘到光盘中: 2.再按顺序执行以下的命令: sudo ...
- sql clr项目注意
1.如果引用了其他第三方的dll没有在系统里注册的话会报错,需要手工引用,引用的时候可能需要不安全的使用授权,如果没有权限则使用以下语句获取 alter database Class01New_Cac ...
- ThinkPHP 3.1.2 输出和模型使用 配置项等 - 2
一.ThinkPHP 3 的输出 (重点) a.通过 echo 等PHP原生的输出方式在页面中输出 b.通过display方法输出 想分配变量可以使用assign方法 c.修改左右定界符 休 ...
- What is SolrCloud? (And how does it compare to master-slave?)
What is SolrCloud? (And how does it compare to master-slave?) SolrCloud is a set of new features and ...
- Hadoop 管理工具HUE配置-集成Unix用户和用户组
HUE安装完成之后,第一次登录的用户就是HUE的超级用户,可以管理用户,等等.但是在用的过程发现一个问题这个用户不能管理HDFS中由supergroup创建的数据. 虽然在HUE中创建的用户可以管理自 ...
- IDC:IDC(互联网数据中心)
ylbtech-IDC:IDC(互联网数据中心) IDC为互联网内容提供商(ICP).企业.媒体和各类网站提供大规模.高质量.安全可靠的专业化服务器托管.空间租用.网络批发带宽以及ASP.EC等业务. ...
- 方法 - 调试Dll方法
1.exe加载dll 2.Dll属性设置2.1运行exe生成Debug/...exe2.2属性->调试->命令-> 改成 ./Debug/调试Dll.exe ../Debug/调试D ...
- Scrapy学习篇(二)之常用命令行工具
简介 Scrapy是通过Scrapy命令行工具进行控制的,包括创建新的项目,爬虫的启动,相关的设置,Scrapy提供了两种内置的命令,分别是全局命令和项目命令,顾名思义,全局命令就是在任意位置都可以执 ...
- CDlinux 安装
镜像 CDlinux-0.9.7.1 虚拟机VMware12 1.VMware12中,新建虚拟机 2.典型安装方式 下一步 3.稍后安装操作系统 4.内核版本要选择[其他linux2.6.X内核] 5 ...
- IGMP Internet组管理协议 未完
一.IGMP Internet组管理协议 2.IGMP v2 3.IGMP三版本比较 4.1.1.4 IGMP v2 与 IGMP v1 的兼容 5.IGMP窃听(IGMP Snooping) IGM ...