线程总结
- 一.线程入门
- 二. 线程安全
- 三. 线程间通信知识点补充
- 四. Lock锁的使用
- 五. 并发编程中一些问题
- 六. 线程池与Executor 框架
- 1. 使用线程池的好处
- 2. Executor 框架
- 3. ThreadPoolExecutor详解
- 4. ScheduledThreadPoolExecutor详解
- 5. 各种线程池的适用场景介绍
一.线程入门
1. 进程和多线程简介
1.1 何为进程?
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
- 进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序
1.2 何为线程?
- 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。
- 与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
- 线程是在同一程序内几乎同时执行一个以上的程序段。
1.3 线程和进程的关系
- 基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
- 线程上下文的切换比进程上下文切换要快很多
- 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
- 线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
1.4 线程有哪些基本状态?这些状态是如何定义的?
- 新建(new):新创建了一个线程对象。
- 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
- 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
- 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice(时间片),暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:
- (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waiting queue)中。
- (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- (三). 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

1.5 线程的优先级
每个线程都具有各自的优先级,线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。
- 线程优先级具有继承特性比如A线程启动B线程,则B线程的优先级和A是一样的。
- 线程优先级具有随机性也就是说线程优先级高的不一定每一次都先执行完。
Thread类中包含的成员变量代表了线程的某些优先级。
- 如Thread.MIN_PRIORITY(常数1),Thread.NORM_PRIORITY(常数5),
Thread.MAX_PRIORITY(常数10)。 - 其中每个线程的优先级都在Thread.MIN_PRIORITY(常数1) 到Thread.MAX_PRIORITY(常数10) 之间。
- 在默认情况下优先级都是Thread.NORM_PRIORITY(常数5)。
1.6 何为多线程?
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。
1.7 为什么多线程是必要的?
开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能
1.8 为什么提倡多线程而不是多进程?
线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
2. 几个重要的概念
2.1 同步和异步
同步和异步通常用来形容一次方法调用。
- 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
- 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
2.2 并发(Concurrency)和并行(Parallelism)
它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。
而并行是真正意义上的"同时执行"。
- 多线程在单核CPU的话是顺序执行,也就是交替运行(并发)。
- 多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行(并行)。
2.3 高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
2.4 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。
但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。
2.5 阻塞和非阻塞
非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,而阻塞与之相反。
3. 多线程
3.1 使用多线程常见的三种方式
- 继承Thread类
- 实现Runnable接口
- 使用线程池
3.2 Java多线程分类
- 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
- 守护线程:运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 "佣人"。
特点:一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
应用:数据库连接池中的检测线程,JVM虚拟机启动后的检测线程
最常见的守护线程:垃圾回收线程
3.3 如何设置守护线程?
- 通过调用Thead类的setDaemon(true)方法设置当前的线程为守护线程
1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
2. 在守护线程中产生的新线程也是守护线程
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
4. 一些常用方法
4.1 currentThread()
返回对当前正在执行的线程对象的引用。
4.2 getId()
返回此线程的标识符
4.3 getName()
返回此线程的名称
4.4 getPriority()
返回此线程的优先级
4.5 isAlive()
测试这个线程是否还处于活动状态。
什么是活动状态呢?
活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备运行的状态。
4.6 sleep(long millis)
使当前正在执行的线程以指定的毫秒数"休眠"(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
4.7 interrupt()
中断这个线程。
4.8 interrupted() 和isInterrupted()
interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能
isInterrupted(): 测试线程Thread对相关是否已经是中断状态,但部清楚状态标志
4.9 setName(String name)
将此线程的名称更改为等于参数 name 。
4.10 isDaemon()
测试这个线程是否是守护线程。
4.11 setDaemon(boolean on)
将此线程标记为 daemon线程或用户线程。
4.12 join()
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
join()的作用是:"等待该线程终止",这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行
4.13 yield()
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU时间。注意:放弃的时间不确定,可能一会就会重新获得CPU时间片。
4.14 setPriority(int newPriority)
更改此线程的优先级
二. 线程安全
0. 锁
0.1 为什么需要锁(并发控制)?
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
典型的冲突有:
- 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
- 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
- 为了解决这些并发带来的问题。 我们需要引入并发控制机制。
0.2 并发控制机制(锁)
- 悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
- 乐观锁
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
- 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
0.3 两种锁的使用场景
- 乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
- 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
0.4 乐观锁的缺点
- ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。- 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。- 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
0.5 乐观锁与悲观锁详解
1. synchronized关键字(1)

- synchronized
Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 "重量级锁" 。
但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
1.1 变量安全性
- "非线程安全"(线程不安全) 问题存在于"实例变量"中,如果是方法内部的私有变量,则不存在"非线程安全",所得结果也就是"线程安全"的了。
- 如果两个线程同时操作对象中的实例变量,则会出现"非线程安全",解决办法就是在方法前加上synchronized关键字即可。
1.2 多个对象对个锁
synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。所以在上面的实例中,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
1.3 synchronized方法与锁对象
synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。
如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。
1.4 脏读
- 发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过。
- 代码没有做同步,虽然set方法同步,但是由于get方法一般都会忘了,导致读的值是被写过的
1.5 synchronized锁重入
- 可重入锁"概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
1.6 同步不具有继承性
- 如果父类有一个带synchronized关键字的方法,子类继承并重写了这个方法。 但是同步不能继承,所以还是需要在子类方法中添加synchronized关键字。
2. synchronized关键字(2)

2.1 synchronized方法的缺点
- 使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
可以使用synchronized同步块来解决这个问题。但是要注意synchronized同步块的使用方式,如果synchronized同步块使用不好的话并不会带来效率的提升。
2.2 synchronized(this)同步代码块的使用
public class Task {
private String getData1;
private String getData2;
public void doLongTimeTask() {
try {
System.out.println("begin task");
Thread.sleep(3000);
String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName="
+ Thread.currentThread().getName();
String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName="
+ Thread.currentThread().getName();
synchronized (this) {
getData1 = privateGetData1;
getData2 = privateGetData2;
}
System.out.println(getData1);
System.out.println(getData2);
System.out.println("end task");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 当一个线程访问一个对象的synchronized同步代码块时,另一个线程任然可以访问该对象非synchronized同步代码块。
- 不在synchronized代码块中就异步执行,在synchronized代码块中就是同步执行。
2.3 synchronized(object)代码块间使用
- 两个线程使用了同一个"对象监视器",所以运行结果是同步的。
- 两个线程使用了不同的"对象监视器",所以运行结果不是同步的了。
2.4 synchronized代码块间的同步性
- 当一个对象访问synchronized(this)代码块时,其他线程对同一个对象中所有其他synchronized(this)代码块代码块的访问将被阻塞,这说明synchronized(this)代码块使用的"对象监视器"是同一个。
- 和synchronized方法一样,synchronized(this)代码块也是锁定当前对象。
- 其他线程执行对象中synchronized同步方法和synchronized(this)代码块时呈现同步效果;
- 如果两个线程使用了同一个"对象监视器",运行结果同步,否则不同步.
2.5 静态同步synchronized方法与synchronized(class)代码块
- synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给Class类上锁,而synchronized关键字加到非静态方法上是给对象上锁。
- 静态同步synchronized方法与synchronized(class)代码块持有的锁一样,都是Class锁,Class锁对对象的所有实例起作用。
3. volatile关键字

3.1 volatile关键字简介
- volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数。
- 如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
3.2 volatile关键字的可见性
- volatile 修饰的成员变量在每次被线程访问时,都强迫从主存(共享内存)中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主存(共享内存)。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,这样也就保证了同步数据的可见性。


3.3 synchronized关键字和volatile关键字比较
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用synchronized关键字还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字用于解决变量在多个线程之间的可见性,而ynchronized关键字解决的是多个线程之间访问资源的同步性。
4. 等待/通知(wait/notify)机制
4.1 等待/通知机制介绍
为什么要使用等待/通知机制?
当两个线程之间存在生产和消费者关系,也就是说第一个线程(生产者)做相应的操作然后第二个线程(消费者)感知到了变化又进行相应的操作。
第二个语句不停过通过轮询机制来检测判断条件是否成立。如果轮询时间的间隔太小会浪费CPU资源,轮询时间的间隔太大,就可能取不到自己想要的数据。所以这里就需要我们今天讲到的等待/通知(wait/notify)机制来解决这两个矛盾。
概念
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上诉两个线程通过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
4.2 等待/通知机制的实现
- notify()执行后并不会立即释放锁
- synchronized关键字可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了等待/通知(wait/notify)机制的相关方法,它们必须用在synchronized关键字同步的Object的临界区内。
- 通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁。
- 而notify()方法可以唤醒一个因调用wait操作而处于阻塞状态中的线程,使其进入就绪状态。
- 被重新唤醒的线程会试图重新获得临界区的控制权也就是锁,并继续执行wait方法之后的代码。
4.3 notify()锁不释放
当方法wait()被执行后,锁自动被释放,但执行完notify()方法后,锁不会自动释放。必须执行完notify()方法所在的synchronized代码块后才释放。
三. 线程间通信知识点补充

1. 管道输入/输出流
- 管道输入/输出流主要用于线程之间的数据传输,而且传输的媒介为内存。
- 管道输入/输出流主要包括下列两类的实现:
- 面向字节: PipedOutputStream、 PipedInputStream
- 面向字符: PipedWriter、 PipedReader
2. Thread.join()的使用
- 在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。另外,一个线程需要等待另一个线程也需要用到join()方法。
- Thread类除了提供join()方法之外,还提供了join(long millis)、join(long millis, int nanos)两个具有超时特性的方法。这两个超时方法表示,如果线程thread在指定的超时时间没有终止,那么将会从该超时方法中返回。
- Thread.sleep(2000)不会释放锁,threadTest.join(2000)会释放锁 。
3. ThreadLocal的使用
3.1 ThreadLocal介绍
- 变量值的共享可以使用public static变量的形式,所有线程都使用一个public static变量。如果想实现每一个线程都有自己的共享变量该如何解决呢?
- JDK中提供的ThreadLocal类正是为了解决这样的问题。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
3.2 ThreadLocal方法
| 方法名称 | 描述 |
|---|---|
| get() | 返回当前线程的此线程局部变量的副本中的值。 |
| set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值 |
| remove() | 删除此线程局部变量的当前线程的值 |
| initialValue() | 返回此线程局部变量的当前线程的"初始值" |
3.3 InheritableThreadLocal
- ThreadLocal类固然很好,但是子线程并不能取到父线程的ThreadLocal类的变量,InheritableThreadLocal类就是解决这个问题的。
- 在使用InheritableThreadLocal类需要注意的一点是:如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的还是旧值。
四. Lock锁的使用

1 Lock接口
1.1 Lock接口简介
锁是用于通过多个线程控制对共享资源的访问的工具。
通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。
但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。
- 在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。
Lock接口的实现类:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
1.2. Lock的简单使用
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
- 因为Lock是接口所以使用时要结合它的实现类,另外在finall语句块中释放锁的目的是保证获取到锁之后,最终能够被释放。
- 注意: 最好不要把获取锁的过程写在try语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁无法被释放。
1.3. Lock接口的特性和常见方法
- Lock接口提供的synchronized关键字不具备的主要特性:
| 特性 | 描述 |
|---|---|
| 尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
| 能被中断地获取锁 | 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
| 超时获取锁 | 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回 |
- Lock接口基本的方法:
| 方法名称 | 描述 |
|---|---|
| void lock() | 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。 |
| void lockInterruptibly() | 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。 |
| Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
| boolean tryLock() | 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。 |
| boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false. |
| void unlock() | 释放锁。 |
2. Lock接口的实现类:ReentrantLock
- ReentrantLock和synchronized关键字一样可以用来实现线程之间的同步互斥,但是在功能是比synchronized关键字更强大而且更灵活。
2.1 ReentrantLock类常见方法
- 构造方法:
| 方法名称 | 描述 |
|---|---|
| ReentrantLock() | 创建一个 ReentrantLock的实例。 |
| ReentrantLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例 |
- ReentrantLock类常见方法(Lock接口已有方法这里没加上):
| 方法名称 | 描述 |
|---|---|
| int getHoldCount() | 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。 |
| protected Thread getOwner() | 返回当前拥有此锁的线程,如果不拥有,则返回 null |
| protected Collection getQueuedThreads() | 返回包含可能正在等待获取此锁的线程的集合 |
| int getQueueLength() | 返回等待获取此锁的线程数的估计。 |
| protected Collection getWaitingThreads(Condition condition) | 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。 |
| int getWaitQueueLength(Condition condition) | 返回与此锁相关联的给定条件等待的线程数的估计。 |
| boolean hasQueuedThread(Thread thread) | 查询给定线程是否等待获取此锁。 |
| boolean hasQueuedThreads() | 查询是否有线程正在等待获取此锁。 |
| boolean hasWaiters(Condition condition) | 查询任何线程是否等待与此锁相关联的给定条件 |
| boolean isFair() | 如果此锁的公平设置为true,则返回 true 。 |
| boolean isHeldByCurrentThread() | 查询此锁是否由当前线程持有。 |
| boolean isLocked() | 查询此锁是否由任何线程持有。 |
2.2 第一个ReentrantLock程序
- 当一个线程运行完毕后才把锁释放,其他线程才能执行,其他线程的执行顺序是不确定的。
3. Condition接口简介
- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。
- Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
- 在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例可以实现"选择性通知",这个功能非常重要,而且是Condition接口默认提供的。
- Condition接口的常见方法:
| 方法名称 | 描述 |
|---|---|
| void await() | 相当于Object类的wait方法 |
| boolean await(long time, TimeUnit unit) | 相当于Object类的wait(long timeout)方法 |
| signal() | 相当于Object类的notify方法 |
| signalAll() | 相当于Object类的notifyAll方法 |
- synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
3.1 使用Condition实现等待/通知机制
- 使用单个Condition实例实现等待/通知机制:
- 必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。
- 注意: 必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器,不然会报错。
- 使用多个Condition实例实现等待/通知机制:
- 实现"选择性通知",只会唤醒注册在指定Condition实例中的所有等待线程
- 使用Condition实现顺序执行
- 在一个线程运行完之后通过condition.signal()/condition.signalAll()方法通知下一个特定的线程运行,就这样循环往复即可。
- 注意: 默认情况下ReentranLock类使用的是非公平锁
4. 公平锁与非公平锁
- Lock锁分为:公平锁 和 非公平锁。
- 公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
- 而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先的到锁,这样可能造成某些线程一直拿不到锁,结果也就是不公平的了。
5. ReadWriteLock接口的实现类:ReentrantReadWriteLock
5.1 简介
- ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。
ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。
- 读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,
一个是写操作相关的锁也称为排他锁。
通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
- 多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的)
在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,
而进行写入操作的Thread只有在获取写锁后才能进行写入操作。
即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
5.2 ReentrantReadWriteLock的特性与常见方法
- ReentrantReadWriteLock的特性:
| 特性 | 说明 |
|---|---|
| 公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量上来看还是非公平优于公平 |
| 重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁也能够同时获取读锁 |
| 锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级称为读锁 |
- ReentrantReadWriteLock常见方法:
构造方法
| 方法名称 | 描述 |
|---|---|
| ReentrantReadWriteLock() | 创建一个 ReentrantReadWriteLock()的实例 |
| ReentrantReadWriteLock(boolean fair) | 创建一个特定锁类型(公平锁/非公平锁)的ReentrantReadWriteLock的实例 |
常见方法:
和ReentrantLock类 类似
5.3 ReentrantReadWriteLock的使用
- 读读共享
两个线程同时运行read方法,你会发现两个线程可以同时或者说是几乎同时运行lock()方法后面的代码,输出的两句话显示的时间一样。这样提高了程序的运行效率。
- 写写互斥
两个线程同时运行write方法,你会发现同一时间只允许一个线程执行lock()方法后面的代码
- 读写互斥(读写互斥)
运行两个使用同一个Service对象实例的线程a,b,线程a执行上面的read方法,线程b执行上面的write方法。你会发现同一时间只允许一个线程执行lock()方法后面的代码。
- 记住:只要出现写操作的过程就是互斥的。
五. 并发编程中一些问题

1. 多线程就一定好吗?快吗??
- 并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,
比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - 多线程就是几乎同时执行多个线程(一个处理器在某一个时间点上永远都只能是一个线程!即使这个处理器是多核的,除非有多个处理器才能实现多个线程同时运行)。CPU通过给每个线程分配CPU时间片来实现伪同时运行,因为CPU时间片一般很短很短,所以给人一种同时运行的感觉。
2. 上下文切换
- 当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
- 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
2.1 减少上下文切换
- 上下文切换又分为2种:
- 让步式上下文切换
指执行线程主动释放CPU,与锁竞争严重程度成正比,可通过减少锁竞争和使用CAS算法来避免;- 抢占式上下文切换。
指线程因分配的时间片用尽而被迫放弃CPU或者被其他优先级更高的线程所抢占,一般由于线程数大于CPU可用核心数引起,可通过适当减少线程数和使用协程来避免。
总结一下:
- 减少锁的使用。因为多线程竞争锁时会引起上下文切换。
- 使用CAS算法。这种算法也是为了减少锁的使用。CAS算法是一种无锁算法。
- 减少线程的使用。人物很少的时候创建大量线程会导致大量线程都处于等待状态。
- 使用协程。
2.2 CAS算法
CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
实现非阻塞同步的方案称为"无锁编程算法"( Non-blocking algorithm)。 属于乐观锁。
CAS算法涉及到三个操作数
- 需要读写的内存值V
- 进行比较的值A
- 拟写入的新值B
- 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
2.3 协程
协程也可以说是微线程或者说是轻量级的线程,它占用的内存更少并且更灵活。很多编程语言中都有协程。
3. 避免死锁
3.1 如何产生的死锁
- 在操作系统中,死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- 在线程中,如果两个线程同时等待对方释放锁也会产生死锁。
3.2 避免死锁的常见方法
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
3.3 解决资源限制
- 什么是资源限制?
所谓资源限制就是我们在进行并发编程时,程序的运行速度受限于计算机硬件资源比如CPU,内存等等或软件资源比如软件的质量、性能等等。举个例子:如果说服务器的带宽只有2MB/s,某个资源的下载速度是1MB/s,系统启动10个线程下载该资源并不会导致下载速度编程10MB/s,所以在并发编程时,需要考虑这些资源的限制。硬件资源限制有:带宽的上传和下载速度、硬盘读写速度和CPU处理速度;软件资源限制有数据库的连接数、socket连接数、软件质量和性能等等。
- 资源限制引发的问题
在并发编程中,程序运行加快的原因是运行方式从串行运行变为并发运行,但是如果某段程序的并发执行由于资源限制仍然在串行执行的话,这时候程序的运行不仅不会加快,反而会更慢,因为可能增加了上下文切换和资源调度的时间。
3.如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用Hadoop或者自己搭建服务器集群。
对于软件资源的限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket复用,或者在调用对方webservice接口获取数据时,只建立一个连接。另外还可以考虑使用良好的开源软件。
- 在资源限制的情况下如何进行并发编程
根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源-带宽和硬盘读写速度。有数据库操作时,设计数据库练连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。
六. 线程池与Executor 框架

1. 使用线程池的好处
- 线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息
使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2. Executor 框架
2.1 Executor 简介
Executor 框架是Java5之后引进的,在Java 5之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
this逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。
2.2 Executor 框架结构(主要由三大部分组成)
1 任务
执行任务需要实现的Runnable接口或Callable接口。Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。
两者的区别:
- Runnable接口不会返回结果,但是Callable接口可以返回结果。后面介绍Executors类的一些方法的时候会介绍到两者的相互转换。
2 任务的执行
- 包括任务执行机制的核心接口Executor ,以及继承自Executor 接口的ExecutorService接口。
- ScheduledThreadPoolExecutor和ThreadPoolExecutor这两个关键类实现了ExecutorService接口。
3 异步计算的结果
- Future接口以及Future接口的实现类FutureTask类。
- 当我们把Runnable接口或Callable接口的实现类提交(调用submit方法)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,会返回一个FutureTask对象。
2.3 Executor 框架的使用示意图
-
主线程首先要创建实现Runnable或者Callable接口的任务对象。
备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))
-
然后可以把创建完成的Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task)或ExecutorService.submit(Callable task))
执行execute()方法和submit()方法的区别是什么呢?
1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完 -
如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(我们刚刚也提到过了执行execute()方法和submit()方法的区别,到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。
-
最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
3. ThreadPoolExecutor详解
线程池实现类ThreadPoolExecutor是Executor 框架最核心的类
3.1 ThreadPoolExecutor类的四个比较重要的属性

3.2 ThreadPoolExecutor类中提供的四个构造方法
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
* @param corePoolSize 核心线程池大小
* @param maximumPoolSize 最大线程池的大小
* @param keepAliveTime 当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,
*核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
* @param unit keepAliveTime参数的时间单位
* @param workQueue 等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;
*
* @param threadFactory 执行者创建新线程时使用的工厂
* @param handler RejectedExecutionHandler类型的变量,表示线程池的饱和策略。
* 如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。
* 线程池提供了4种策略:
1.AbortPolicy:直接抛出异常,这是默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
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;
}
3.3 如何创建ThreadPoolExecutor
在《阿里巴巴Java开发手册》"并发处理"这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
为什么呢?
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者"过度切换"的问题。
另外《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
方式一:通过构造方法实现
方式二:通过Executor 框架的工具类Executors来实现
- 我们可以创建三种类型的ThreadPoolExecutor:
FixedThreadPool
SingleThreadExecutor
CachedThreadPool
底层都是调用的ThreadPoolExecutor的构造方法
3.4 FixedThreadPool详解
FixedThreadPool被称为可重用固定线程数的线程池。
/**
* 创建一个可重用固定数量线程的线程池
*在任何时候至多有n个线程处于活动状态
*如果在所有线程处于活动状态时提交其他任务,则它们将在队列中等待,
*直到线程可用。 如果任何线程在关闭之前的执行期间由于失败而终止,
*如果需要执行后续任务,则一个新的线程将取代它。池中的线程将一直存在
*知道调用shutdown方法
* @param nThreads 线程池中的线程数
* @param threadFactory 创建新线程时使用的factory
* @return 新创建的线程池
* @throws NullPointerException 如果threadFactory为null
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(),
threadFactory);
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
新创建的FixedThreadPool的corePoolSize和maximumPoolSize都被设置为nThreads。
FixedThreadPool的execute()方法运行示意图
上图说明:
- 如果当前运行的线程数小于corePoolSize,则创建新的线程来执行任务;
- 当前运行的线程数等于corePoolSize后,将任务加入LinkedBlockingQueue;
- 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;
FixedThreadPool使用无界队列 LinkedBlockingQueue(队列的容量为Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响:
- 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize;
- 由于1,使用无界队列时maximumPoolSize将是一个无效参数;
- 由于1和2,使用无界队列时keepAliveTime将是一个无效参数;
- 运行中的FixedThreadPool(未执行shutdown()或shutdownNow()方法)不会拒绝任务
3.5 SingleThreadExecutor详解
SingleThreadExecutor是使用单个worker线程的Executor。
/**
*创建使用单个worker线程运行无界队列的Executor
*并使用提供的ThreadFactory在需要时创建新线程
*
* @param threadFactory 创建新线程时使用的factory
*
* @return 新创建的单线程Executor
* @throws NullPointerException 如果ThreadFactory为空
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(),
threadFactory));
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
新创建的SingleThreadExecutor的corePoolSize和maximumPoolSize都被设置为1.
其他参数和FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Intger.MAX_VALUE)
SingleThreadExecutor使用无界队列作为线程池的工作队列会对线程池带来的影响与FixedThreadPool相同。
SingleThreadExecutor的运行示意图
上图说明:
- 如果当前运行的线程数少于corePoolSize,则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程后,将任务加入LinkedBlockingQueue
- 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;
3.6 CachedThreadPool详解
CachedThreadPool是一个会根据需要创建新线程的线程池。
/**
* 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它,
*并在需要时使用提供的ThreadFactory创建新线程。
* @param threadFactory 创建新线程使用的factory
* @return 新创建的线程池
* @throws NullPointerException 如果threadFactory为空
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue(),
threadFactory);
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
CachedThreadPool的corePoolSize被设置为空(0),maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程
极端情况下,这样会导致耗尽cpu和内存资源。
CachedThreadPool的execute()方法的执行示意图
上图说明:
- 首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤2;
- 当初始maximumPool为空,或者maximumPool中没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1将失败,此时CachedThreadPool会创建新线程执行任务,execute方法执行完成;
3.7 ThreadPoolExecutor使用示例
首先创建一个Runnable接口的实现类WorkerThread
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
//创建一个FixedThreadPool对象
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new WorkerThread("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
shutdown()VS shutdownNow()
- shutdown()方法表明关闭已在Executor上调用,因此不会再向DelayedPool添加任何其他任务(由ScheduledThreadPoolExecutor类在内部使用), 但是,已经在队列中提交的任务将被允许完成。
- shutdownNow()方法试图终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的List。
isTerminated() Vs isShutdown()
- isShutDown当调用shutdown()方法后返回为true。
- isTerminated当调用shutdown()方法后,并且所有提交的任务完成后返回为true
4. ScheduledThreadPoolExecutor详解
4.1 ScheduledThreadPoolExecutor简介
- ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。
- ScheduledThreadPoolExecutor使用的任务队列DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTask的time变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTask的squenceNumber变量小的先执行)。
4.2 ScheduledThreadPoolExecutor运行机制
ScheduledThreadPoolExecutor的执行主要分为两大部分:
- 当调用ScheduledThreadPoolExecutor的 scheduleAtFixedRate() 方法或者scheduleWirhFixedDelay() 方法时,会向ScheduledThreadPoolExecutor的 DelayQueue 添加一个实现了 RunnableScheduledFutur 接口的 ScheduledFutureTask 。
- 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下修改:
- 使用 DelayQueue 作为任务队列;
- 获取任务的方不同
- 执行周期任务后,增加了额外的处理
4.3 ScheduledThreadPoolExecutor执行周期任务的步骤
- 线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前系统的时间;
- 线程1执行这个ScheduledFutureTask;
- 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间;
- 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
4.4 ScheduledThreadPoolExecutor使用示例
创建一个简单的实现Runnable接口的类
测试程序使用ScheduledExecutorService和Executors.newScheduledThreadPool实现的java调度
4.4.1 ScheduledExecutorService scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)方法
//可以使用ScheduledExecutorService scheduleAtFixedRate方法来安排任务在初始延迟后运行,然后在给定的时间段内运行。
scheduledThreadPool.scheduleAtFixedRate(worker, 0, 10, TimeUnit.SECONDS);
4.4.2 ScheduledExecutorService scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)方法
//ScheduledExecutorService scheduleWithFixedDelay方法可用于以初始延迟启动周期性执行,然后以给定延迟执行。 延迟时间是线程完成执行的时间。
scheduledThreadPool.scheduleWithFixedDelay(worker, 0, 1, TimeUnit.SECONDS);
4.4.3 scheduleWithFixedDelay() vs scheduleAtFixedRate()
- scheduleAtFixedRate(…)将延迟视为两个任务开始之间的差异(即定期调用)
不管是否执行完 - scheduleWithFixedDelay(…)将延迟视为一个任务结束与下一个任务开始之间的差异,执行完才开始下一个。
scheduleAtFixedRate(): 在给定的初始延迟之后创建并执行,随后以给定的时间段首先启用的周期性动作; 那就是执行将在initialDelay之后开始,然后initialDelay+period ,然后是initialDelay + 2 * period ,等等。 如果任务的执行遇到异常,则后续的执行被抑制。 否则,任务将仅通过取消或终止执行人终止。 如果任务执行时间比其周期长,则后续执行可能会迟到,但不会同时执行。
scheduleWithFixedDelay() : 在给定的初始延迟之后创建并执行,首先启用的定期动作,随后在一个执行的终止和下一个执行的开始之间给定的延迟。 如果任务的执行遇到异常,则后续的执行被抑制。 否则,任务将仅通过取消或终止执行终止。
5. 各种线程池的适用场景介绍
FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器;
SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景。
CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器;
ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景,
SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。






