Java 线程池

4 篇文章 0 订阅

线程状态

在说明线程池之前,觉得有必要了解下线程有哪些状态,这对后续了解线程池的原理会比较有帮助。

在这里插入图片描述

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

在 Java 线程的状态分为六种,上图是将线程更细化了一些:

  • NEW:线程被创建出来了,但是还没有调用 start() 启动线程

  • RUNNABLE:可运行状态,在图中这个状态拆分成了两部分 READY 准备状态和 RUNNING 运行状态

  • READY:只能说明线程有资格运行但等待调度

  • RUNNING:线程被调度处于运行状态

  • BLOCKED:阻塞状态,线程在进入 synchronized 关键字修饰的方法或代码块(获取锁)时的状态

  • WAITING:等待状态,线程不会被分配 CPU,线程要等待被唤醒,否则会一直等待

  • TIMED_WAITING:超时等待状态的线程不会被分配 CPU,在达到一定时间后它们会自动唤醒

  • TERMINATED:终止状态,顾名思义,线程执行结束了

异步模式之工作线程

让有限的工作线程轮流异步处理无限多的任务,也可以将其归类为分工模式,它的典型实现就是线程池。

例如,服务员(线程)会轮流处理每位顾客的点餐(任务),如果每位顾客都配一个专属服务员,则成本太过高昂。

在线程分配时有一个注意事项:避免线程饥饿现象。

线程饥饿现象即有关联的多个任务混合在同一个线程池中,相互影响出现类似死锁的问题。

public static void main(String[] args) {
	ExecutorService service = Executors.newFixedThreadPool(2);

	for (int i = 0; i < 10; i++) {
		service.submit(() -> {
			System.out.println("上菜");
		});	
	}
	
	for (int i = 0; i < 10; i++) {
		service.submit(() -> {
			System.out.println("做菜");
		});
	}
}

上面的例子中,假设上菜和做菜是有关联的任务,我们想先做菜再上菜,但 CPU 线程是抢占式的,如果让上菜先执行又没有释放,做菜就无法执行,形成类似死锁的现象。这就是线程饥饿现象。

所以,为了避免线程饥饿现象,在使用线程池时需要注意任务的颗粒度问题,有相互关联的相关任务不在同一线程池处理,而是单独开辟线程池,且效率上得到合理分配。

线程数量与 CPU 核心数的关系

线程核心数是一种执行资源,资源数量就是 CPU 核的个数,线程数就是服务请求数,而操作系统需要调配有限的资源来服务更多的请求。一般情况下,线程会 “相对公平” 的分配到 CPU 核上运行,并且在时间片上轮流使用,这就是所谓的并发执行。

比如系统有 4 个 CPU 核,如果有 3 个线程,分配到 3 个 CPU 核上运行;有 8 个线程,每个 CPU 核分配两个线程执行;有 10 个线程,有些 CPU 核跑三个有些跑两个。

所以,并非线程数量越大速度就越快,线程数量太过于庞大会导致各种内存问题,因为一个线程的开辟还会涉及到线程上下文的应用。

创建多少个线程合适?

  • CPU 密集型运算(高并发、任务执行时间短):通常采用 “CPU 核心数 + 1” 能够实现最优的 CPU 利用率,+ 1 是保证当线程由于页缺失故障(操作系统)或其他原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 始终周期不被浪费;减少线程上下文切换

  • I/O 密集型运算(并发不高、任务执行时间长):CPU 不总是处于繁忙状态,例如当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 IO 操作时或者远程 RPC 调用包括进行数据库操作等,这个时候 CPU 会闲下来,你可以利用多线程提高它的利用率

简单总结下:

  • CPU 密集型运算就是会使用到 CPU 资源计算的操作,配置最大线程数量为 “CPU 核心数 + 1” 时线程最优利用率

  • I/O 密集型就是在 CPU 空闲下来时的操作,具体要使用多少线程需要根据你的业务需要调整加大线程数量,不让 CPU 闲下来能处理更多业务。一般核心线程数量设置为 0,最大线程数量如果是中小型应用业务较简单的设置 64 个即可;如果是大型应用业务多且复杂的设置 128 个

为什么要使用线程池?

在知道线程池之前,我们启动一个线程不外乎两种方式:

// 方式1
Thread thread = new Thread() {
	@Override
	public void run() {}
};
thread.start();

// 方式2
Runnable runnable = new Runnable() {
	@Override
	public void run() {}
};
Thread thread = new Thread(runnable);
thread.start();

那么这两者有什么区别呢?进去 Thread 的源码看一下:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    Thread parent = currentThread();
    if (g == null) {
        g = parent.getThreadGroup();
    }

    g.addUnstarted();
    this.group = g;

    this.target = target; // 把Runnable给了Thread的一个成员target
    this.priority = parent.getPriority();
    this.daemon = parent.isDaemon();
    setName(name);

    init2(parent);

    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;
    tid = nextThreadID();
}

@Override
public void run() {
	// 有Runnable就执行Runnable的run(),没有就执行Thread自己重写的run()
    if (target != null) {
        target.run();
    }
}

另外说一个比较少用的一种线程 Callable

// 可以当成是有返回值的Runnable
Callable<String> callable = new Callable<String>() {
	@Override
	public String call() {
		try {
			Thread.sleep(1500);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "Done!";
	}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
try {
	// 看到这里或许会感到奇怪,为什么我把耗时操作放在Callable了你还卡我主线程?
	// 正常预想情况是主线程早就执行完输出了result: null
	// 当我们想要后台执行结果能够立即返回(future.get()),又想要主线程不被阻塞,这其实是java api的一种取舍
	// java api将这种处理抛给我们处理通过不断检查去处理其他任务
	while (!future.isDone()) {
		// 在等待future返回结果前我们自己去处理其他事情
	}
	String result = future.get(); // 会阻塞等待线程执行完成返回结果
	System.out.println("result: " + result); // result: Done!
} catch (InterruptedException | ExecutionException e) {
	e.printStackTrace();
}

但是当我们需要使用大量线程的时候,使用上面的方式是非常繁琐的,而且频繁创建销毁线程也是耗费较大资源。

线程池可以提前创建好若干个线程放到容器中,如果有任务需要处理,则将任务直接分配给线程池中的线程来执行,任务处理完成以后这个线程不会被销毁(其实就是用到享元模式),而是等待后续分配任务。

使用线程池有如下好处:

  • 降低创建线程和销毁线程的性能开销

  • 提高响应速度,当有新任务需要执行时不需要等待创建线程就可以立马执行

  • 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题

线程池

在这里插入图片描述
在日常的项目使用我们都会自定义 ThreadPoolExecutor 创建线程池。

构造方法:

public TheadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
  • corePoolSize:核心线程数即常驻线程,默认情况下,核心线程会一直存活不会被回收(最多保留的线程数)

  • maximumPoolSize:最大容纳线程数,如果任务队列满了还继续提交任务,则创建新的线程(救急线程)执行任务,前提是线程数小于 maximumPoolSize

  • keepAliveTime:救急线程能运行的时间,超过指定时长救急线程会被回收。当设置allowsCoreThreadTimeOut(true),keepAliveTime 同样会作用于核心线程

  • unit:指定 keepAliveTime 参数的时间单位,是一个枚举。分别有 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECOND(秒)、TimeUnit.MINUTES(分钟)等

  • workQueue:线程池的任务队列(一般是设置阻塞队列),当提交的任务数量大于 corePoolSize 时,会将任务添加到队列

  • threadFactory:线程工厂,为线程池提供创建新线程的功能(可以为线程创建时起个好名字)

  • handler:拒绝策略。当线程数量大于 maximumPoolSize,执行 RejectedExecutionHandler。默认情况下会抛出异常 RejectedExecutionException

线程池执行流程

  • 当线程池中线程数量 < corePoolSize,会直接启动一个核心线程执行任务

  • 当线程池中线程数量 > corePoolSize,任务会被插入到任务队列中排队等待执行

  • 当线程池中线程数量 > corePoolSize,且任务队列已满,这时如果线程数量 < maximunPoolSize,会直接启动非核心线程(救急线程)执行任务

  • 当线程池中线程数量 > corePoolSize,任务队列已满且大于 maximumPoolSize,执行 RejectedExecutionHandler

流程图如下:

在这里插入图片描述
在这里插入图片描述

线程池工作图解如下:

在这里插入图片描述

图中线程池设置 corePoolSize = 2,maximunPoolSize = 3,阻塞队列设置大小也是 2。

核心线程在线程池中分别执行任务 1 和 任务 2;此时阻塞队列也有两个任务再等待执行,此时又添加了任务 5 进来,maximunPoolSize 设置了 3 个,可以 启动一个救急线程执行任务 5,这样能够提升执行性能。但救急线程会根据 keepAliveTime 设定的时间被释放。

线程池使用细节:等待队列要设置队列长度

我们在自定义线程池时都会按照参数主要提供核心线程数(常驻线程数)、最大线程数、等待队列、救急线程存活时间这几个参数,但是等待队列这个地方有一个细节需要注意:

private static final int CPU_COUNT = Runtimes.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUN_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 30private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128); // 需要注意提供队列大小

public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
                                      KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

上面的代码是 AsyncTask 自定义线程池的源码,可以发现 LinkedBlockingQueue 提供了队列大小。自定义线程池提供的等待队列一定要提供队列长度,很多时候如果我们直接创建 LinkedBlockingQueue,它默认的大小是 Integer.MAX_VALUE:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

如果我们不提供队列大小,用默认的设置成了 Integer.MAX_VALUE 会出现的问题是,你设置的最大线程数量相当于无效,一直在使用的常驻线程,如果常驻线程有在使用的线程,其他线程就在队列一直等待。

在这里插入图片描述

写个案例复现这个问题:

private static final int KEEP_ALIVE = 30// 设置常驻线程是 0,在首次运行时只会创建一个线程出来运行
// 其他线程执行全部会走到等待队列等线程空出来,等待队列大小也是 Integer.MAX_VALUE
// 设置的最大线程池数量无效
Executor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
executor.execute(() -> {
	System.out.println(1);
	try {
		Thread.sleep(10 * 1000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
});
executor.execute(() -> {
	System.out.println(2);
});
executor.execute(() -> {
	System.out.println(3);
});

输出结果:
1 // 其他的线程要执行,就到等待队列等着它执行完成才轮到
2 // 等待 10s 后才开始打印
3 // 第二个打印执行完后才才开始打印

所以在自定义线程池时,要特别留意等待队列要根据业务具体设置队列长度。

RejectedExecutionHandler

RejectedExecutionHandler 有四种可选策略:

  • AbortPolicy:默认的拒绝策略,直接抛出异常,也不执行提交的任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
	// 抛出异常
	throw new RejectedExecutionException("Task " + t.toString() + " rejected from " + e.toString());
}
  • DiscardPolicy:什么都没干
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
	// 空实现
}
  • DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的任务,再把这个新任务加入队列
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
	if (!e.isShutdown()) {
		// 移除队列最旧的任务
		e.getQueue().poll();
		// 将这个任务添加到队列
		e.execute(r);
	}
}
  • CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池所在的线程去执行被拒绝的任务。这个策略的缺点是可能会阻塞主线程
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
	if (!e.isShutdown()) {
		r.run(); // 在所在线程中运行,如果是在主线程添加到线程池可能会阻塞主线程
	}
}

线程池的状态模型

线程池的状态管理基于 CAS,可以看到源码是使用的 AtomicInteger ctrl 用于控制状态,同时也用 ctrl 计算线程数量:

@ReachabilitySensitive
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 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;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

可以看到线程池中,它使用 ctl 将状态控制和线程数量合并到一个变量中,具体步骤:

  • 拆分短位与长位数值:短位=常量,即高 3 位用于记录线程池状态;长位=动态值,即 低 29 位用于记录线程池的线程数量

  • 打包算法:长位 &~ mask(对应源码的 runStateOf()),短位 & mask(对应源码的 workerCountOf())

mask 为分界位,如一个整数值短位占用 3 位,则长位占用 29 位,mask 作用是用来对于短位数值进行换算,所以,如果短位需要占用 3 位则需要用 111 < 29 位来占据前三位作为等值。

线程池具体的状态及说明如下表:

状态名高 3 位接收任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011--终结状态

Executors

  • FixedThreadPool:Fixed的意思是固定,就是线程数量固定只存在核心线程。比较适合于集中处理操作
// corePoolSize 和 maximumPoolSize 一样大小,也就是没有救急线程,且 keepAliveTime = 0
// LinkedBlockingQueue()默认创建大小是 Integer.MAX_VALUE 阻塞队列大小没有限制
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
  • CachedThreadPool:线程数量任意只存在救急线程,比较适合执行大量耗时较少的任务
// corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,keepAliveTime = 60s
// 只有救急线程
public static ExecutorService newCacheThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
  • ScheduledThreadPool:核心线程数量固定而非核心线程数量任意,非核心线程闲置会被立即回收。主要用于执行定时任务和具有固定周期的重复任务
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
  • SingleThreadExecutor:只有一个核心线程,使用场景上适合任务一个个排队执行
// corePoolSize = maximumPoolSize = 1,只有一个核心线程,阻塞队列大小 Integer.MAX_VALUE
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, 
                   new LinkedBlockingQueue<Runnable>()));
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值