一、概述
-
新建(NEW):新创建了一个线程对象。
-
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
-
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
-
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种: (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。 (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。 (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
-
死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
二、同步阻塞(synchronized)
下面用代码来展示一下线程同步问题。
public class ThreadSynchronized {
public static void main(String[] args) {
final Outputter output = new Outputter();
new Thread() {
@Override
public void run() {
output.output("abcd");
}
}.start();
new Thread() {
@Override
public void run() {
output.output("efgh");
}
}.start();
}
}
class Outputter {
public void output(String name) {
// TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
显然输出的字符串被打乱了,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,使用synchronized修饰的方法或者代码块可以看成是一个原子操作。
- 将synchronized加在需要互斥的方法上。
public synchronized void output(String name) {
// TODO 线程输出方法
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
}
三、等待阻塞(wait)
使用wait()、notify()和notifyAll()时需要首先对调用对象加锁
调用wait()方法后,线程状态会从RUNNING变为WAITING,并将当线程加入到lock对象的等待队列中(会释放当前的锁,然后让出CPU,进入等待状态)
调用notify()或者notifyAll()方法后,等待在lock对象的等待队列的线程不会马上从wait()方法返回,必须要等到调用notify()或者notifyAll()方法的线程将lock锁释放(执行完),等待线程才有机会从等待队列返回。这里只是有机会,因为锁释放后,等待线程会出现竞争,只有竞争到该锁的线程才会从wait()方法返回,其他的线程只能继续等待
notify()方法将等待队列中的一个线程移到lock对象的同步队列,notifyAll()方法则是将等待队列中所有线程移到lock对象的同步队列,被移动的线程的状态由WAITING变为BLOCKED
wait()方法上等待锁,可以通过wait(long timeout)设置等待的超时时间
public class WaitNotifyThread {
//条件是否满足的标志
private static boolean flag = true;
//对象的监视器锁
private static Object lock = new Object();
//日期格式化器
private static DateFormat format = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
Thread waitThread = new Thread(new WaitThread(), "WaitThread");
waitThread.start();
Thread notifyThread = new Thread(new NotifyThread(), "NotifyThread");
notifyThread.start();
}
/**
* 等待线程
*/
private static class WaitThread implements Runnable {
public void run() {
//加锁,持有对象的监视器锁
synchronized (lock) {
//只有成功获取对象的监视器才能进入这里
//当条件不满足的时候,继续wait,直到某个线程执行了通知
//并且释放了lock的监视器(简单来说就是锁)才能从wait
//方法返回
while (flag) {
try {
System.out.println(Thread.currentThread().getName() + " flag is true,waiting at "
+ format.format(new Date()));
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//条件满足,继续工作
System.out.println(Thread.currentThread().getName() + " flag is false,running at "
+ format.format(new Date()));
}
}
}
/**
* 通知线程
*/
private static class NotifyThread implements Runnable {
public void run() {
synchronized (lock) {
//获取lock锁,然后执行通知,通知的时候不会释放lock锁
//只有当前线程退出了lock后,waitThread才有可能从wait返回
System.out.println(Thread.currentThread().getName() + " holds lock. Notify waitThread at "
+ format.format(new Date()));
lock.notifyAll();
flag = false;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " holds lock again. NotifyThread will sleep at "
+ format.format(new Date()));
}
}
}
}
返回结果
WaitThread flag is true,waiting at 09:52:39
NotifyThread holds lock. Notify waitThread at 09:52:39
NotifyThread holds lock again. NotifyThread will sleep at 09:52:44
WaitThread flag is false,running at 09:52:44
四、其他阻塞
1.线程中断:interrupt()
interrupt()方法用于中断线程,通常的理解来看,只要某个线程启动后,调用了该方法,则该线程不能继续执行。
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread("MyThread");
t.start();
Thread.sleep(5000);// 睡眠100毫秒
t.interrupt();// 中断t线程
}
}
class MyThread extends Thread {
int i = 0;
public MyThread(String name) {
super(name);
}
public void run() {
while(!isInterrupted()) {// 当前线程没有被中断,则执行
System.out.println(getName() + getId() + "执行了" + ++i + "次");
}
}
}
interrupt()方法并不是中断线程的执行,而是为调用该方法的线程对象打上一个标记,设置其中断状态为true,通过isInterrupted()方法可以得到这个线程状态。
2.线程睡眠:Thread.sleep()
线程睡眠的过程中,如果是在synchronized线程同步内,是持有锁(监视器对象)的,也就是说,线程是关门睡觉的,别的线程进不来。
public class SleepTest {
public static void main(String[] args) {
// 创建共享对象
Service service = new Service();
// 创建线程
SleepThread t1 = new SleepThread("t1", service);
SleepThread t2 = new SleepThread("t2", service);
// 启动线程
t1.start();
t2.start();
}
}
class SleepThread extends Thread {
private Service service;
public SleepThread(String name, Service service) {
super(name);
this.service = service;
}
public void run() {
service.calc();
}
}
class Service {
public synchronized void calc() {
System.out.println(Thread.currentThread().getName() + "准备计算");
System.out.println(Thread.currentThread().getName() + "感觉累了,开始睡觉");
try {
Thread.sleep(10000);// 睡10秒
} catch (InterruptedException e) {
return;
}
System.out.println(Thread.currentThread().getName() + "睡醒了,开始计算");
System.out.println(Thread.currentThread().getName() + "计算完成");
}
}
返回结果
t1准备计算
t1感觉累了,开始睡觉
t1睡醒了,开始计算
t1计算完成
t2准备计算
t2感觉累了,开始睡觉
t2睡醒了,开始计算
t2计算完成
3.线程让步:Thread.yield()
线程让步用于正在执行的线程,在某些情况下让出CPU资源,让给其它线程执行。
public class YieldTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象
YieldThread t1 = new YieldThread("t1");
YieldThread t2 = new YieldThread("t2");
// 启动线程
t1.start();
t2.start();
// 主线程休眠100毫秒
Thread.sleep(10);
// 终止线程
t1.interrupt();
t2.interrupt();
}
}
class YieldThread extends Thread {
int i = 0;
public YieldThread(String name) {
super(name);
}
public void run() {
while(!isInterrupted()) {
System.out.println(getName() + "执行了" + ++i + "次");
if(i % 10 == 0) {// 当i能对10整除时,则让步
Thread.yield();
}
}
}
}
输出结果略,从输出结果可以看到,当某个线程(t1或者t2)执行到10次、20次、30次等时,就会马上切换到另一个线程执行,接下来再交替执行,如此往复。注意,如果存在synchronized线程同步的话,线程让步不会释放锁(监视器对象)。
3.线程合并:join()
线程合并是优先执行调用该方法的线程,再执行当前线程
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Long st = System.currentTimeMillis();
JoinThread t1 = new JoinThread("t1");
JoinThread t2 = new JoinThread("t2");
t1.start();
t2.start();
t1.join();
t2.join();
Long et = System.currentTimeMillis();
System.out.println("主线程开始执行!");
Long durtime = et - st;
System.out.println("共耗时" + durtime + "毫秒");
}
}
class JoinThread extends Thread {
public JoinThread(String name) {
super(name);
}
public void run() {
try {
//准备工作
Thread.sleep(8000);
//开始工作
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + "执行了" + i + "次");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
t1和t2都执行完才继续主线程的执行(根据总耗时结果,看出t1和t2是同时执行的),所谓合并,就是等待其它线程执行完,再执行当前线程,执行起来的效果就好像把其它线程合并到当前线程执行一样。
五、番外篇CountDownLatch与join的区别
首先,我们来看一个应用场景1:
假设一条流水线上有三个工作者:worker0,worker1,worker2。有一个任务的完成需要他们三者协作完成,worker2可以开始这个任务的前提是worker0和worker1完成了他们的工作,而worker0和worker1是可以并行他们各自的工作的。
如果我们要编码模拟上面的场景的话,我们大概很容易就会想到可以用join来做。当在当前线程中调用某个线程 thread 的 join() 方法时,当前线程就会阻塞,直到thread 执行完成,当前线程才可以继续往下执行。补充下:join的工作原理是,不停检查thread是否存活,如果存活则让当前线程永远wait,直到thread线程终止,线程的this.notifyAll 就会被调用。
我们首先用join来模拟这个场景:
Worker:
public class Worker extends Thread {
//工作者名
private String name;
//工作时间
private long time;
public Worker(String name, long time) {
this.name = name;
this.time = time;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
System.out.println(name+"开始工作");
Thread.sleep(time);
System.out.println(name+"工作完成,耗费时间="+time);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
Test类如下:
public class Test {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000));
Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000));
Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000));
worker0.start();
worker1.start();
worker0.join();
worker1.join();
System.out.println("准备工作就绪");
worker2.start();
}
}
运行test,观察控制台输出的顺序,我们发现这样可以满足需求,worker2确实是等worker0和worker1完成之后才开始工作的。
worker1开始工作
worker0开始工作
worker1工作完成,耗费时间=3947
worker0工作完成,耗费时间=4738
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=4513
除了用join外,用CountDownLatch 也可以完成这个需求。
Worker:
public class Worker extends Thread {
//工作者名
private String name;
//工作时间
private long time;
private CountDownLatch countDownLatch;
public Worker(String name, long time, CountDownLatch countDownLatch) {
this.name = name;
this.time = time;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
System.out.println(name+"开始工作");
Thread.sleep(time);
System.out.println(name+"工作完成,耗费时间="+time);
countDownLatch.countDown();
System.out.println("countDownLatch.getCount()="+countDownLatch.getCount());
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
Test:
public class Test {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
CountDownLatch countDownLatch = new CountDownLatch(2);
Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);
worker0.start();
worker1.start();
countDownLatch.await();
System.out.println("准备工作就绪");
worker2.start();
}
}
我们创建了一个计数器为2的 CountDownLatch ,让Worker持有这个CountDownLatch 实例,当完成自己的工作后,调用countDownLatch.countDown() 方法将计数器减1。countDownLatch.await() 方法会一直阻塞直到计数器为0,主线程才会继续往下执行。
worker1开始工作
worker0开始工作
worker0工作完成,耗费时间=3174
countDownLatch.getCount()=1
worker1工作完成,耗费时间=3870
countDownLatch.getCount()=0
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=3992
countDownLatch.getCount()=0
CountDownLatch与join的区别:
应用场景2:
假设worker的工作可以分为两个阶段,work2 只需要等待work0和work1完成他们各自工作的第一个阶段之后就可以开始自己的工作了,而不是场景1中的必须等待work0和work1把他们的工作全部完成之后才能开始。
试想下,在这种情况下,join是没办法实现这个场景的,而CountDownLatch却可以,因为它持有一个计数器,只要计数器为0,那么主线程就可以结束阻塞往下执行。我们可以在worker0和worker1完成第一阶段工作之后就把计数器减1即可,这样worker0和worker1在完成第一阶段工作之后,worker2就可以开始工作了。
worker:
public class Worker extends Thread {
//工作者名
private String name;
//第一阶段工作时间
private long time;
private CountDownLatch countDownLatch;
public Worker(String name, long time, CountDownLatch countDownLatch) {
this.name = name;
this.time = time;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
System.out.println(name+"开始工作");
Thread.sleep(time);
System.out.println(name+"第一阶段工作完成");
countDownLatch.countDown();
Thread.sleep(2000); //这里就姑且假设第二阶段工作都是要2秒完成
System.out.println(name+"第二阶段工作完成");
System.out.println(name+"工作完成,耗费时间="+(time+2000));
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
Test:
public class Test {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
CountDownLatch countDownLatch = new CountDownLatch(2);
Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);
Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);
worker0.start();
worker1.start();
countDownLatch.await();
System.out.println("准备工作就绪");
worker2.start();
}
}
观察控制台打印顺序。
worker0开始工作
worker1开始工作
worker1第一阶段工作完成
worker0第一阶段工作完成
准备工作就绪
worker2开始工作
worker1第二阶段工作完成
worker1工作完成,耗费时间=5521
worker0第二阶段工作完成
worker0工作完成,耗费时间=6147
worker2第一阶段工作完成
worker2第二阶段工作完成
worker2工作完成,耗费时间=5384
最后,总结下CountDownLatch与join的区别:调用thread.join() 方法必须等thread 执行完毕,当前线程才能继续往下执行,而CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕。
六、番外篇Callable异步获取结果
编写多线程程序一般有三种方法,Thread,Runnable,Callable.
(1)Callable规定的方法是call(),Runnable规定的方法是run().。
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得。
(3)call方法可以抛出异常,run方法不可以。
(4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。Future.get()方法可能会阻塞当前线程的执行。
创建一个挂起任务类
public class TaskCallable implements Callable<String> {
public int id;
TaskCallable(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
System.out.println("开始执行挂起任务");
Thread.sleep(8000);
System.out.println("结束执行挂起任务");
return id + " 睡了8s,并完成了挂起任务";
}
}
创建一个主任务类
public class CallableTest {
public static void main(String[] args) {
Long start = System.currentTimeMillis();
//创建一个线程池大小为4个
ExecutorService threadPool = Executors.newFixedThreadPool(4);
//启动4个线程去调用,将返回结果存入集合
List<Future<String>> list = new ArrayList<>();
for (int i = 0; i < 4; i++) {
Future<String> future = threadPool.submit(new TaskCallable(i));
list.add(future);
}
try {
System.out.println("主线程逻辑开始");
Thread.sleep(10000);
System.out.println("主线程逻辑结束");
//获取异步结果
for (Future<String> stringFuture : list) {
System.out.println("看看挂起的线程执行结果" + stringFuture.get());
}
System.out.println("所有线程逻辑结束");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
Long end = System.currentTimeMillis();
System.out.println("总耗时" + (end - start) / 1000 + "s");
}
}
根据执行结果可以看到,我们的四个挂起线程并发执行完消耗的时间一样,总消耗时间只是主主线程的时间(如果挂起线程消耗时间大于主线程消耗时间,那么主线程会一直阻塞,直到挂机线程执行完毕)。
七、番外篇ReentrantLock的使用
1.效果和synchronized一样,都可以同步执行,lock方法获得锁,unlock方法释放锁
public class MyService {
private Lock lock = new ReentrantLock();
public void testMethod() {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
lock.unlock();
}
}
Condition类的awiat方法和Object类的wait方法等效
Condition类的signal方法和Object类的notify方法等效
Condition类的signalAll方法和Object类的notifyAll方法等效
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void testMethod() {
try {
lock.lock();
System.out.println("开始wait");
condition.await();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println("唤醒signal");
condition.signal();
} finally {
lock.unlock();
}
}
}
2.Java并发编程——锁与可重入锁
简单锁:
public class Counter{
private Lock lock = new Lock();
private int count = 0;
public int inc(){
lock.lock();
this.count++;
lock.unlock();
return count;
}
}
上面的程序中,由于this.count++这一操作分多步执行,在多线程环境中可能出现结果不符合预期的情况,这段代码称之为 临界区 ,所以需要使用lock来保证其原子性。
Lock的实现:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){ //不用if,而用while,是为了防止假唤醒
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
说明:当isLocked为true时,调用lock()的线程在wait()阻塞。 为防止该线程虚假唤醒,程序会重新去检查isLocked条件。 如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。当线程完成了临界区中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且唤醒 其中一个 处于等待状态的线程。
锁的可重入性:
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
outer中调用了inner,outer先锁住了lock,这样inner就不能再获取lock。其实调用outer的线程已经获取了lock锁,但是不能在inner中重复利用已经获取的锁资源,这种锁即称之为 不可重入 。通常也称为 自旋锁 。相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
可重入锁的基本实现:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
//获取当前线程的引用
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
lockBy:保存已经获得锁实例的线程,在lock()判断调用lock的线程是否已经获得当前锁实例,如果已经获得锁,则直接跳过while,无需等待。
lockCount:记录同一个线程重复对一个锁对象加锁的次数。否则,一次unlock就会解除所有锁,即使这个锁实例已经加锁多次了。
3.Java中常用的锁的属性
synchronized:可重入锁;
java.util.concurrent.locks.ReentrantLock:可重入锁;
八、番外篇ThreadLocal的使用
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。
demo:
public class ThreadLocalTest implements Runnable{
ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextInt(100);
System.out.println(currentThreadName + " is set age: " + age);
Studen studen = getStudent(); //通过这个方法,为每个线程都独立的new一个student对象,每个线程的的student对象都可以设置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent() {
Studen studen = studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Studen{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
result:
Thread A is running...
Thread B is running...
Thread B is set age: 25
Thread A is set age: 65
Thread B is first get age: 25
Thread A is first get age: 65
Thread A is second get age: 65
Thread B is second get age: 25
九、线程池
- FixedThreadPool 重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
在FixedThreadPool中,有一个固定大小的池,如果当前需要执行的任务超过了池大小,那么多于的任务等待状态,直到有空闲下来的线程执行任务,而当执行的任务小于池大小,空闲的线程也不会去销毁。
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i < 5; i++) {
final int taskID = i;
threadPool.execute(new Runnable() {
public void run() {
for (int j = 1; j < 4; j++) {
try {
Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
}
}
});
}
threadPool.shutdown();// 任务执行完毕,关闭线程池
}
}
初始化线程池大小为3,运行4个线程,每个线程执行3次任务; 前3个线程首先执行完,然后空闲下来的线程去执行第4个任务;
pool-1-thread-2第2次任务的第1次执行
pool-1-thread-1第1次任务的第1次执行
pool-1-thread-3第3次任务的第1次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-3第3次任务的第2次执行
pool-1-thread-2第2次任务的第2次执行
pool-1-thread-3第3次任务的第3次执行
pool-1-thread-2第2次任务的第3次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-2第4次任务的第1次执行
pool-1-thread-2第4次任务的第2次执行
pool-1-thread-2第4次任务的第3次执行
- CachedThreadPool创建一个根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来,如果线程有可用的,就使用之前创建好的线程,如果没有可用的,就新创建线程,终止并且从缓存中移除已有60秒未被使用的线程。
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 1; i < 5; i++) {
final int taskID = i;
threadPool.execute(new Runnable() {
public void run() {
for (int j = 1; j < 4; j++) {
try {
Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
}
}
});
}
threadPool.shutdown();// 任务执行完毕,关闭线程池
}
}
运行4个线程,每个线程执行3次任务;4个线程交替执行完成;类似Executors.newFixedThreadPool(4);
pool-1-thread-1第1次任务的第1次执行
pool-1-thread-3第3次任务的第1次执行
pool-1-thread-2第2次任务的第1次执行
pool-1-thread-4第4次任务的第1次执行
pool-1-thread-3第3次任务的第2次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-2第2次任务的第2次执行
pool-1-thread-4第4次任务的第2次执行
pool-1-thread-2第2次任务的第3次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-3第3次任务的第3次执行
pool-1-thread-4第4次任务的第3次执行
- SingleThreadExecutor创建一个根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
SingleThreadExecutor得到的是一个单个的线程,这个线程会保证你的任务执行完成,如果当前线程意外终止,会创建一个新线程继续执行任务,这和我们直接创建线程不同,也和newFixedThreadPool(1)不同。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
for (int i = 1; i < 5; i++) {
final int taskID = i;
threadPool.execute(new Runnable() {
public void run() {
for (int j = 1; j < 4; j++) {
try {
Thread.sleep(200);// 为了测试出效果,让每次任务执行都需要一定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "第" + taskID + "次任务的第" + j + "次执行");
}
}
});
}
threadPool.shutdown();// 任务执行完毕,关闭线程池
}
}
结果
pool-1-thread-1第1次任务的第1次执行
pool-1-thread-1第1次任务的第2次执行
pool-1-thread-1第1次任务的第3次执行
pool-1-thread-1第2次任务的第1次执行
pool-1-thread-1第2次任务的第2次执行
pool-1-thread-1第2次任务的第3次执行
pool-1-thread-1第3次任务的第1次执行
pool-1-thread-1第3次任务的第2次执行
pool-1-thread-1第3次任务的第3次执行
pool-1-thread-1第4次任务的第1次执行
pool-1-thread-1第4次任务的第2次执行
pool-1-thread-1第4次任务的第3次执行
4.线程池配置参数
线程池基本大小 int corePoolSize
线程池最大大小 int maximumPoolSize
保持活动时间 long keepAliveTime
保持活动时间单位 TimeUnit unit
工作队列 BlockingQueue workQueue
线程工厂 ThreadFactory threadFactory
驳回策略 RejectedExecutionHandler handler
当线程池大小 >= corePoolSize 且 队列未满时,这时线程池使用者与线程池之间构成了一个生产者-消费者模型。线程池使用者生产任务,线程池消费任务,任务存储在BlockingQueue中,注意这里入队使用的是offer,当队列满的时候,直接返回false,而不会等待。
当线程处于空闲状态时,线程池需要对它们进行回收,避免浪费资源。但空闲多长时间回收呢,keepAliveTime就是用来设置这个时间的。默认情况下,最终会保留corePoolSize个线程避免回收,即使它们是空闲的,以备不时之需。但我们也可以改变这种行为,通过设置allowCoreThreadTimeOut(true)。
当队列满 且 线程池大小 >= maximumPoolSize时会触发驳回,因为这时线程池已经不能响应新提交的任务,驳回时就会回调这个接口rejectedExecution方法,JDK默认提供了4种驳回策略,代码比较简单,直接上代码分析,具体使用何种策略,应该根据业务场景来选择,线程池的默认策略是AbortPolicy。
并不是先加入任务就一定会先执行。假设队列大小为 4,corePoolSize为2,maximumPoolSize为6,那么当加入15个任务时,执行的顺序类似这样:首先执行任务 1、2,然后任务3~6被放入队列。这时候队列满了,任务7、8、9、10 会被马上执行,而任务 11~15 则会抛出异常。最终顺序是:1、2、7、8、9、10、3、4、5、6。当然这个过程是针对指定大小的ArrayBlockingQueue<Runnable>来说,如果是LinkedBlockingQueue<Runnable>,因为该队列无大小限制,所以不存在上述问题。
ThreadPoolExecutor:
<bean id="commonExecutor" class="java.util.concurrent.ThreadPoolExecutor"
destroy-method="shutdown">
<!-- int corePoolSize -->
<constructor-arg value="8" />
<!-- int maximumPoolSize, -->
<constructor-arg value="48" />
<!-- long keepAliveTime, -->
<constructor-arg value="60" />
<!-- TimeUnit unit, -->
<constructor-arg value="SECONDS" />
<!-- BlockingQueue<Runnable> workQueue -->
<constructor-arg>
<bean class="java.util.concurrent.LinkedBlockingQueue">
<!-- int capacity -->
<constructor-arg value="100" />
</bean>
</constructor-arg>
</bean>
ThreadPoolTaskExecutor:
<!-- spring线程池 -->
<bean id="threadPoolTaskExecutor"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数,默认为1 -->
<property name="corePoolSize" value="10" />
<!-- 最大线程数,默认为Integer.MAX_VALUE -->
<property name="maxPoolSize" value="50" />
<!-- 队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE -->
<property name="queueCapacity" value="300" />
<!-- 线程池维护线程所允许的空闲时间,默认为60s -->
<property name="keepAliveSeconds" value="120" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 -->
<property name="rejectedExecutionHandler">
<!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常 -->
<!-- CallerRunsPolicy:主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 -->
<!-- DiscardOldestPolicy:抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
<!-- DiscardPolicy:抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 -->
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy" />
</property>
</bean>