开始在在多线程的坑中摸爬滚打。。

让电脑做更多的事

Java学习到了后面,线程部分是相对来说难以理解和掌握的。初识线程时,知道它是电脑实现多任务的一种机制。然而在单核的CPU中并不是真正的同一时刻执行多个任务,而是不停地在线程间进行切换。好像多核的CPU可以真正的实现并发,但是自己还未涉及到。总的来说使用多线程可以让我们更好地利用系统资源去完成更多的事。

开启一个线程

Thread继承了一个接口Runnable,这个接口里只有一个方法run(),要让线程做的事也就写在run()里面。开启一个线程的方式为:new Thread(param).start(),param为实现了run()方法的Thread或Runnable对象。这时候线程就进入了就绪状态,等待CPU的调用了。

同步与中断

多线程为我们带来高效的执行效果时,也同时增加了程序的复杂度,我们需要管理这些线程,并对程序中的数据进行安全化的处理。这时候线程间的同步和通信就显得尤为重要,Java里也提供了不同的机制让我们更好的使用多线程,有以下方法:
Object:

  • wait() 线程等待,释放锁
  • notify() 线程唤起,释放锁

Thread:

  • sleep() 线程睡眠,不释放锁
  • interrupt() 通知线程中断

Thread中还有方法:stop,suspend,resume,不过都不建议使用了,因为一个线程应该由自己来关闭。这里的interrupt()也不是真的中断了线程,而是将interrupt status设置为true,通知线程要停止操作了。线程需要自己扫描状态位,然后自己决定要做什么事,结束或是进行一些清理工作。

synchronized关键字;可以为代码块或是方法块加上同步锁,保证了操作的原子性。

JDK5 之后的并发包

以上的都是传统多线程处理方法。jdk5之后增加了线程池、Callable和Future、锁对象Lock,可以让我们更轻松地去使用多线程来完成我们想要做的事。

线程的三大特性

原子性 可见性 有序性

这几个特性在线程的安全问题上必须要考虑周全了,缺一不可,不然就可能导致结果执行不正确。
原子性
即一个操作或者多个操作,为最小的不可分割的执行单位,执行的过程不会被任何因素打断。
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性
程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

JDK中的线程池

jdk在5之后加入了concurrent包,提供了很多并发环境下实用的类,其中包括线程池ExecutorService。利用池化技术可以节省资源,提高性能,但是使用的不得当也会导致OOM,此篇说明线程池的核心参数和使用时的注意事项。

线程池核心参数

在使用线程池的过程中,很多人会使用Executors的静态方法去更便捷的创建一些线程池,主要有以下几种:

  • newSingleThreadExecutor()
  • newFixedThreadPool(int nThreads)
  • newCachedThreadPool()
  • newScheduledThreadPool(int corePoolSize)

阿里曾在他的开发手册中禁止开发人员使用Executors去创建线程池,而要使用ThreadPoolExecutor,为的应该是让开发人员多了解线程池的核心参数,避免使用不当。ThreadPoolExecutor也就是上面几个方法最后会调用的基础方法,其中的参数较多,如下所示:

1
2
3
4
5
6
7
8
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

  1. corePoolSize 核心线程数,表示线程池中一般工作时保持的线程数量。注意线程池在创建的时候并不会启动核心线程,而是等到有任务提交时才去创建线程,除非主动调用prestartCoreThread或者prestartAllCoreThreads,所以在任务不充足的情况下线程池的大小也不一定是corePoolSize。
  2. maximumPoolSize 线程池内的最大线程数量,表示线程池中允许存在的最大线程数量,包含核心线程和非核心线程。
  3. keepAliveTime 多余闲置线程存活时间,“多余”指的是超过核心线程数量的非核心线程,当它们执行完任务后,超过存活时间就会被销毁。
  4. unit 存活时间单位
  5. workQueue 任务队列,暂存还没有被执行的任务,只会保存通过execute方法提交的任务。
  6. threadFactory 线程工厂,创建线程时用的线程工厂,默认是Executors.defaultThreadFactory()。
  7. handler 任务拒绝策略,当任务队列满了并且线程池内线程数达到最大时,对新加任务的处理策略。默认是AbortPolicy,即丢弃新加的任务并抛出异常RejectedExecutionException。

线程池的整个工作流程如下入所示

线程池工作逻辑

使用Executors的利弊

好处当然是使用更加便捷,Executors不需要去管那么多的核心参数,能立马上手创建线程池用于任务执行。但是Executors里的一些静态方法创建的线程池,其线程池和任务队列的大小是没有限制的,有OOM的风险。

newSingleThreadExecutor()

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

new LinkedBlockingQueue()的队列长度为Integer.MAX_VALUE(0x7fffffff),基本是个无界队列,所以可以往队列中无限地添加任务,有可能发生OOM。

newFixedThreadPool(int nThreads)

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

newFixedThreadPool与SingleThreadExecutor类似,区别就是核心线程数不同,使用的也是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常。

newCachedThreadPool()

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

核心线程数为0,最大线程数为Integer.MAX_VALUE,可以看作无限创建线程。而SynchronousQueue是一个不存储元素的队列,所以newCachedThreadPool每次将会创建非核心线程去执行任务。无限创建线程是一个很危险的动作,资源不足时会发生OOM。