文章目录
日志编号 | 说明 |
---|---|
C-2020-07-27 | 第一次创建 |
Java多线程
写在前头
本文主旨在写Java多线程开发的基础知识,基本不会提及进程,线程,CPU,内存,寄存器等这方面的知识。
多线程
线程和进程这两个名字很像,不要搞混了。举一个类比的例子,一列火车就是一个进程,火车里的单个车厢,就是线程,当有多个车厢时,就是多线程。
网上最多见的解释,进程是申请计算机资源的最小单位,线程共享当前进行申请来的资源,是执行和调度的最小单位。如果想对这部分原理性,非编码的知识进行了解学习,需要去按照写在前头这部分中涉及的知识点去进行学习。
什么时候使用多线程
在什么情况下需要用到多线程?
这个问题是贯穿多线程这个开发技能的始末,能够区分出什么情况下需要多线程,可以在实际开发中对性能提升有更好的解决思路。
列举一个吃饭的例子。
假设我现在去吃饭。有两种情况,一种是吃面,一大碗面吃完就饱了。另一种是米饭+各种炒菜。我相信在外面吃过米饭+炒菜的同志们应该都碰到过上菜的问题,不论总体上菜速度的快慢,基本不能做到同时把菜上齐,而是陆陆续续的上菜。
在吃面的时候,把味道调好之后,埋头吃饭就行。吃米饭+炒菜的时候,吃饭的整体过程不如吃面更连贯。一会儿吃个这个,一会儿吃个那个。纵观两个过程,就是单线程与多线程的区别和应用场景。
在计算密集型程序里,CPU使用率本身就高,不需要也不推荐使用多线程。多线程技术用在这里会提升线程切换的开销(切换程序上下文),起不了提升性能的作用,同时还会因为变量,方法同步产生各类不变。这就类似于吃面,只需要专注眼前的事务,并且执行起来之后,中途没什么等待。
对于I/O密集型程序,推荐使用多线程。因为I/O操作会阻塞线程(类似于上菜的过程),此时CPU使用率不高,因此在使用了多线程的情况下,当前线程阻塞,就会去执行其他线程,提升CPU使用率,继而提升效率。
多线程概念
多线程的诸多概念中,有一个概念很重要,就是线程的状态。
在java.lang.Thread中定义了一个名为State的Enum,这里面是线程对应的状态。
下面出现的内容会按照先列举状态,然后用表格的形式解释这其中涉及的方法的意义,最后再用一个线程状态跃迁图来进行描述。
多线程状态
-
NEW
官方解释:
Thread state for a thread which has not yet started.
这个就是线程被new出来,但是没有执行start()方法之前的状态。 -
RUNNABLE
官方解释:
Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
可运行线程的状态。这个可运行状态细分成两种,就绪(ready)和执行(running),就绪(ready)是在等待来自操作系统其它资源的状态,执行(running)就是正在JVM中执行的线程状态。 -
BLOCKED
官方解释:
Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait().
阻塞状态。阻塞状态是说,当一个线程等待监视器锁的时候的状态。线程等待监视器锁,是为了进入或者再次进入被 synchronized关键字修饰的方法或者代码块中。
线程本身调用了wait()方法(继承自Object)后,会先进入WAITING状态。之后执行Object.notify()或者Object.notifyAll()方法后,线程不是变成RUNNABLE状态,而是会进入同步队列,等待获取监视器锁,此时被监控到的线程状态就是BLOCKED。 -
WAITING
官方解释:
Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods: Object.wait() with no timeout, Thread.join() with no timeout, LockSupport.park().
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object.
A thread that has called Thread.join() is waiting for a specified thread to terminate.
等待状态。线程会在调用一下三个方法后被设置成WAITING状态。- Object.wait()
- Thread.join()
- LockSupport.park()
处于WAITING状态的线程,其实是在等待其他线程执行一个特定的操作。
例如,如果一个线程执行了wait()方法,它就是在等待另一个线程执行notify()或者notifyAll()方法。再比如线程Thread-A,在他的内部调用了Thread-B的join()方法,那么Thread-A会进入WATING状态,直至Thread-B变成TERMINATED状态。
-
TIMED_WAITING
官方解释:
Thread state for a waiting thread with a specified waiting time.
A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:Thread.sleep, Object.wait(long), Thread.join(long), LockSupport.parkNanos, LockSupport.parkUntil.网上很多帖子里会把这个状态称为“超时等待状态”,这里我采用了官方本身定义的名称,定时等待状态。当一个线程执行了下面的方法后,就会进入定时等待状态。
- Thread.sleep()
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos
- LockSupport.parkUntil
注,上面方法里有三个没有指明参数列表,是因为这三个方法是重载的方法。下面的方法解释时会详细讲解。
-
TERMINATED
官方解释:The thread has completed execution.
当一个线程执行完成之后,就会进入到TERMINATED状态。
多线程常用API与线程间转态的关系
上面涉及的方法与没涉及但是在线程中经常使用的方法如下表所示。
方法名 | 参数 | 作用和说明 | 对线程的影响 |
---|---|---|---|
start | - | 当一个Thread对象被创建之后,线程的状态是NEW(新建)此时,这个线程对象并不能被执行。用这个线程对象调用start()方法,使线程启动。 | 调用该方法的线程状态从NEW到RUNNABLE(按照上面的RUNNABLE中的细分来看,会是READY(就绪状态)) |
wait | - | wait方法继承自Object类,这个方法的作用是让当前调用方法的线程等待,将线程状态设置成WAITING。 | 调用这个方法的线程状态编程WAITING,并且释放同步锁。当其他拥有同步锁的线程执行了notify,那么这个WAITING的线程有可能变成BLOCKED,如果执行的是notifyAll,则会变成BLOCKED |
LockSupport.park | - | 首先这个方法是属于java.java.util.concurrent.locks.LockSupport这个类的,它是一个静态方法。在方法内部,调用了sun.misc包下的Unsafe类的park(boolean,long)方法。LockSupport.park()方法上的注释是,出于线程调度目的,禁用当前正在执行的类,除非许可证可用。如果许可证可用,那么它会将会被使用,并且立即返回。否则当前线程会被禁用并且直到发生下面三种情况才不再被禁用。1,其他的线程用当前线程作为参数,执行了unpark(Thread)方法;2,其他线程内部执行了Interrupt();3,出于未知的原因,这次调用没有成功。注意,在Unsafe类中的park方法里有两个参数,分别是boolean 和long。Boolean表示许可证是否可用,fales不可用,true可用。第二个long值,是等待时间的deadline。在 LockSupport.park() 内部调用Unsafe.park()方法时,传入的参数是(false,0L),意思是许可不可用,无限期等待 | 和wait方法一样,让当前线程进入到WAITING状态。并且这个在WAITING的线程需要等待其他线程执行特定操作,对于被park的线程来说,它要等待的特殊操作就是unpark()或者interrupt()。针对park之后的interrupt例子在下面将interrupt的部分。 |
join | - | 这个方法属于Tread类,作用是强制执行调用join()方法的线程。也就是说,在线程Thread-A中,用Thread-B调用了join()方法,则会把Thread-A的状态置成WAITING,并且执行Thread-B的内容,直到Thread-B线程的执行完。一旦Thread-B的状态是TERMINATED,就会调用notifyAll()方法,使得Thread-A状态变成BLOCKED。注意在join()方法的内部最终实际是调用了wait(long)方法,注意二之所以join()在其他线程Terminated之后,能通知当前线程,因为在线程terminated之后会触发notifyAll()方法。不推荐在线程中使用wait,notify,notifyAll这三个方法。 上面黄色的内容是官方文档中的内容,目前我并没有能理解他的精髓,只是遵照着去写相应的代码 | 在线程Thread-A中,用Thread-B调用了join()方法,则会把Thread-A的状态置成WAITING,并且执行Thread-B的内容,直到Thread-B线程的执行完。一旦Thread-B的状态是TERMINATED,就会调用notifyAll()方法,使得Thread-A状态变成BLOCKED |
Thread.sleep | long,毫秒数值 | 让当前正在执行的线程休眠指定的毫秒数 | 让当前的线程状态从RUNNABLE(Running)变成TIMED_WAITING,并且,这个方法不会释放监视器的方法 |
Thread.sleep | long,毫秒数值;int,纳秒数值 | 让当前正在执行的线程休眠指定的毫秒数+纳秒数 | 让当前的线程状态从RUNNABLE(Running)变成TIMED_WAITING,并且,这个方法不会释放监视器的方法 |
wait | long,毫秒值 | 让当前线程停止指定毫秒数,或者等到其他拥有共享锁的线程执行notify或者notifyAll方法。 | 当前线程调用了wait(long)之后,会把线程状态从RUNNABLE设置成TIMED_WAITING,之后等到指定的时间(方法中传入的参数),或者其他线程执行了notify或者notifyAll之后,才有可能变成BLOCKED |
wait | long,毫秒值,int 纳秒值 | 让调用当前方法的线程停止,停止指定的时间(毫秒值+纳秒值)。或者在此期间有其他线程执行了notify或者notifyAll方法 | 让当前线程停止指定毫秒数+纳秒值,或者等到其他拥有共享锁的线程执行notify或者notifyAll方法。 当前线程调用了wait(long,int)之后,会把线程状态从RUNNABLE设置成TIMED_WAITING,之后等到指定的时间(方法中传入的参数),或者其他线程执行了notify或者notifyAll之后,才有可能变成BLOCKED |
join | long,毫秒值 | 这个方法属于Tread类,作用是强制执行调用join(long)方法的线程。也就是说,在线程Thread-A中,用Thread-B调用了join(long)方法,则会把Thread-A的状态置成TIMED_WAITING,并且执行Thread-B的内容,直到Thread-B线程的执行完或者等待足够的时间(long)。一旦Thread-B的状态是TERMINATED,就会调用notifyAll()方法,使得Thread-A状态变成BLOCKED。 或者等待时间结束,也会调用notifyAll,这个线程也会变成BLOCKED状态。 | 在线程Thread-A中,用Thread-B调用了join(long)方法,则会把Thread-A的状态置成TIMED_WAITING,并且执行Thread-B的内容,直到Thread-B线程的执行完或者等待足够的时间(long)。一旦Thread-B的状态是TERMINATED,就会调用notifyAll()方法,使得Thread-A状态变成BLOCKED。或者等待时间结束,也会调用notifyAll,这个线程也会变成BLOCKED状态。 |
LockSupport.parkNanos | long,纳秒值 | 出于线程调度的目的,让当前线程停止特定的时间长度(传入的纳秒值)。 | 线程状态会设置成TIMED_WAITING状态,直到某个线程执行了unpark或者interrupt方法,或者等待足够的时间,在此之后,线程状态从TIMED_WAITING设置成RUNNABLE |
LockSupport.parkNanos | Object,同步锁对象;long 纳秒值 | 写在头里,在上面出现的park方法里,我只列举了无参的,但是他还有一个有参的,参数就是这个方法的第一个参数,Object。经过对源码的解读之后, 这里传入的Object会被放置在一个类似Map的容器中(之所以说是类似,因为再往下的方法是native,我没有深入进入),记录一下,KEY是当前线程,VALUE就是传入的同步对象(Object),等这个方法执行完,会再次往这个map中设置KEY是当前线程,VALUE是null。所以,这个方法就是调用了这个方法的线程会被设置成TIMED_WAITING,并且将同步对象存在一个类似MAP的地方,等方法执行完,再把Map中对应的内容设成null | 一个线程调用了这个方法后,这个线程的状态会变成TIMED_WAITING,直到等到时间结束,或者在此期间有其他线程执行了unpark或者interrupt方法,之后这个线程的状态会从TIMED_WAITING变成RUNNABLE |
LockSupport.parkUntil | long,毫秒值 | 出于线程调度的目的,让调用该方法的线程停止一段时间,直到long(传入的时间)为止。注意,这里的long是毫秒值,并且表示的是deadline | 一个线程调用了这个方法后,这个线程的状态会变成TIMED_WAITING,直到等到时间结束,或者在此期间有其他线程执行了unpark或者interrupt方法,之后这个线程的状态会从TIMED_WAITING变成RUNNABLE |
LockSupport.parkUntil | Object,同步锁对象;long 毫秒值 | 第一个参数的作用与上面提到的parkNanos里面的作用一样。当前方法整体意思是出于线程调度的目的,让调用该方法的线程停止一段时间,直到long(传入的时间)为止。注意,这里的long是毫秒值,并且表示的是deadline。 | 一个线程调用了这个方法后,这个线程的状态会变成TIMED_WAITING,直到等到时间结束,或者在此期间有其他线程执行了unpark或者interrupt方法,之后这个线程的状态会从TIMED_WAITING变成RUNNABLE |
interrupt | - | 中断线程,这个方法内容有点太长了,在表格下面进行讲解 | 中断线程 |
Thread.yield | - | 这个方法是Thread类中的静态方法,用来暂定当前执行的线程 | 当前线程状态从RUNNABLE(Running)变成RUNNABLE(Ready),重新和所有的RUNNABLE(ready)线程争夺资源 |
interrupt
在网上的很多资料中,都会出现一句话,interrupt方法是用来中断阻塞线程。这个说法我这里不打算沿用,因为在前面介绍线程状态里,有一个阻塞(BLOCKED)状态,这个状态与那些资料中的阻塞线程其实是两个事情。网上那些资料里在讲的,其实是,interrupt方法,是用来中断线程状态是WAITING或者TIMED_WAITDING的线程。
在interrupt方法的注释上说了,如果一个线程被 wait(), wait(long), wait(long, int) , join(), join(long), join(long, int), sleep(long), sleep(long, int)这些方法阻塞住,所谓的阻塞住是说这些线程在执行了上述方法,但是并没有等到特殊操作,等待足够的时间,此时,我们不想要这些线程再等下去了,就可以用interrupt方法去中断这些线程。
在上面介绍这些方法中讲到了,处于WAITING和TIMED_WAITING状态的线程在得到特殊操作,或者等待足够时间后,会变成BLOCKED,再次去争夺同步锁,继续执行。
而interrupt方法的作用可以简单的形容成,卧槽,老子不管了,不等了,这种结果就是,interrupt之后的线程就不继续执行了。
对于interrupt方法本身而言,这个方法不能中断一个正在执行的线程,interrupt适用于中断处于WAITING或者TIMED_WAITING的线程,并且在中断之后报出一个InterruptedException。
针对这个InterruptedException异常,虽然是异常,但是是正常情况,不必惊慌。
interrupt方法,会改变线程的中断状态。这种设计之所以能用于中断等待中的线程,是因为,在等待状态中的线程,会不断的检测自己的中断状态。一旦中断状态变成要进行中断,那这些等待的线程就会中断。
两个demo例子
public static void main(String[] args) throws InterruptedException {
final Thread main = Thread.currentThread();
final Thread t2 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("in t2");
Thread.sleep(100000);
System.out.println("out t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
final Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println(t2.getState());
//t2调用interrupt方法,中断t2线程
t2.interrupt();
System.out.println("out t1");
}
});
t2.start();
t1.start();
}
在上面的例子里,t2通过sleep(10000)让当前线程变成了TIMED_WAITING状态,之后通过线程t1在执行的时候,中断了t2。out t2 不会执行
public static void main(String[] args) throws InterruptedException {
final Thread main = Thread.currentThread();
final Thread t2 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("in t2");
for (int i =1;i<10000;i++){
}
System.out.println("out for");
Thread.sleep(100000);
System.out.println("out t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
final Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println(t2.getState());
t2.interrupt();
System.out.println("out t1");
}
});
t2.start();
t1.start();
}
这个例子和第一个例子中有一个明显的差异。这个例子是用来描述,假如一个正在执行中的线程被interrupt的时候,会怎么做。
在t1中有一个执行10000次的for循环,这个循环数字其实可以更大,目的是为了在一个时间片内,当前线程不会执行到sleep语句。此时,线程t2并没有变成TIMED_WAITING状态,却被t1线程里的t2.interrupt()方法进行了终止。
在这种情况下,t2还是会继续执行自己在执行的内容,因为t2对自己中断状态的检测,只会在WAITING和TIMED_WAITING或者即将WAITING和TIMED_WAITING的时候才进行。因此,只有当t2完成for循环,并且执行到sleep语句(或者上述提到其他任何一个可以让它等待的方法)时,线程发现自己马上要等待了,检查了一下中断状态,发现自己需要被中断,就不在执行下去。out t2 不会执行。
在这两个例子中,当t2被中断后,都会由t2报一个InterruptedException ,这是正常的情况。
注,这里有一个细节点。
在上方讲解park()的时候,他的等待的特殊操作是unpark()或者interrupt,所以对于被park的线程,在执行interrupt之后,不是被中断,而是会让这个线程变成BLOCKED,会继续进行。代码如下:
public static void main(String[] args) throws InterruptedException {
final Thread main = Thread.currentThread();
final Thread t2 = new Thread(new Runnable() {
public void run() {
System.out.println("in t2");
for (int i =1;i<10000;i++){
}
System.out.println("out for");
LockSupport.park();
System.out.println("out t2");
}
});
final Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println(t2.getState());
t2.interrupt();
System.out.println("out t1");
}
});
t2.start();
t1.start();
}
out t2 会执行,同样这样的情况也适用于parkUntil和parkNanos方法。
综上,Java多线程里的基础知识(线程状态,多线程常用API)已经介绍完毕。
下面是基于上面内容的一个多线程状态跃迁图例。
多线程状态跃迁图例
这个图是将上面的状态讲解,方法讲解进行整合,最终形成了上面的状态跃迁与API参照图。
线程同步
在多线程中线程同步是一个很重要的步骤。
多线程是允许多个线程并发操作共享资源,这样避免数据不一致就成了一个关键问题。
在进行线程同步的时候,有多种思路可选,比如同步代码块;同步方法;volatile关键字修饰变量;使用JUC里的ReentrantLock控制锁的创建,获取,释放;ThreadLocal创建线程本地变量;JUC里的阻塞队列等。作为基础知识,只需要掌握同步代码块和同步方法。
Java中定义同步代码块和同步方法时,需要使用关键字synchronized,被这个关键字修饰的方法或者代码块是线程安全的。从实际操作中去看,同步方法写起来更简单,与其他方法的区别就在于多了一个关键字。同步代码块在实际操作中就不像同步方法那样简单。
同步代码块,顾名思义是一个需要进行线程同步的代码块。这里面就需要提一嘴同步监视器,更多的知识会在JVM分栏里进行讲解。
在Java里,每个对象都有自己的监视器,称之为对象监视器。同理,每个类也有自己的监视器,类监视器是基于对象监视器实现的,称之为类监视器。
同步代码块或者同步方法,简单解释来看,如果当前要执行同步方法或者代码块的线程A被JVM发现已经有一个其他线程B在执行,并且线程A的监视器与线程B的监视器之间的关系满足了同步代码块或者同步方法中定义的需要互斥的策略,那A和B之间,针对这个同步方法或者同步代码块之间就是互斥策略。
注,这里所说的监视器,实则是创建线程时,传入的Runnable对象的监视器,以及通过对象监视器而实现的类监视器。
下面的代码中,在创建线程时,传入的入参不一样,他们之间应对不同的监视器,互斥策略也会不一样。
public static void main(String[] args) {
SelfTest selfTest = new SelfTest();
//不同对象
//不同对象创建线程,他们的对象监视器不一样,但都是SelfTest类,所以类监视器一样。
Thread t1 = new Thread(new SelfTest(), "Thread-1-new object");
Thread t2 = new Thread(new SelfTest(), "Thread-2-new object");
//相同对象
//相同对象创建线程,他们的对象监视器一样,并且类监视器也一样。
Thread t3 = new Thread(selfTest, "Thread-3-same object");
Thread t4 = new Thread(selfTest, "Thread-4-same object");
//如果同步代码块或者同步方法是对新对象互斥,那这4个都不互斥
// 如果是对当前对象互斥,那Thread1,Thread2不互斥,与Thread3,4也不互斥。但是Thread3与Thread4互斥
// 如果是对类监视器互斥(假如用更多是SelfTest类监视器),那这四个线程之间都互斥
t1.start();
t2.start();
t3.start();
t4.start();
}
synchronized不同写法与互斥策略关系表
类别 | 写法 | 策略 |
---|---|---|
同步代码块 | synchronized (new Object) | 同步监视器与要进入同步代码块中的每个对象监视器都不一样,因此不会有互斥存在。 |
同步代码块 | synchronized (this) | 同步监视器定义的是当前对象。也就是说在创建线程时,如果有传入相同的Runnable对象,则这些相同Runnable对象的线程之间互斥。 |
同步代码块 | synchronized(Class.class) | 同步监视器定义的是类监视器,也就是说在创建线程时,如果传入的对象与同步监视器中的类是同一个类,则这些相同类之间的线程互斥。 |
静态同步方法 | static synchronized | 静态同步方法的阻塞策略与定义了类监视器的同步代码块一样,如果创建线程时传入的Runnable对象是同一个类的话,那这些同样类的线程之间互斥。 |
非静态同步方法 | synchronized | 非静态同步方法的阻塞策略与定义了当前对象监视器的同步代码块一样,如果在创建线程的时候,传入了同一个Runnable对象,那么相同Runnable对象的线程之间互斥。 |
测试代码:
package com.phl;
import java.util.Calendar;
public class SelfTest implements Runnable {
private Calendar instance;
{
instance = Calendar.getInstance();
}
//新对象监视器
public void func1() {
//共享资源每次都是new出来的,每次new出来的新对象就有了一个新的监视器,也就是说在创建线程的时候,不论传入的是不是相同对象,都不会与这个新的监视器是同一个
// 因此对于要进入这段同步代码的线程都不会进行拦截。
synchronized (new SelfTest()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//当前对象监视器
public void func2() {
//共享资源指定了是当前类对象。意思就是说,在创建线程时,如果传入的是相同对象(Thread-3,Thread-4),这会拦截
// 因为Thread-3和Thread-4具有同一个监视器,这两个线程在执行的时候是互斥的,Thread-1和Thread-2不会拦截。
synchronized (this) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//类监视器
public void func3() {
//共享资源是SelfTest类,在下面创建线程时,都是用的SelfTest对象,他们都是SelfTest类,具有同一个类监视器。
//因此,这四个线程对于func3中的同步代码而言,是互斥的。
synchronized (SelfTest.class) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//静态同步方法,执行起来的结果与类锁的同步代码块一样,SelfTest类会被拦截
//这个也好理解,在JVM加载的过程里,还没有当前类的对象被创建,因此这种写法就规定为执行类监视器的策略
public static synchronized void func4() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//非静态同步方法,执行起来的结果与当前对象监视器的策略一样。相同对象监视器之间的对象互斥,其他不拦截。
public synchronized void func5() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void print() {
instance.setTimeInMillis(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + "-----" + instance.getTime().toString());
}
public void run() {
print();
//替换成要测试的func()即可
func5();
print();
}
public static void main(String[] args) {
SelfTest selfTest = new SelfTest();
//不同对象
//不同对象创建线程,他们的对象监视器不一样,但都是SelfTest类,所以类监视器一样。
Thread t1 = new Thread(new SelfTest(), "Thread-1-new object");
Thread t2 = new Thread(new SelfTest(), "Thread-2-new object");
//相同对象
//相同对象创建线程,他们的对象监视器一样,并且类监视器也一样。
Thread t3 = new Thread(selfTest, "Thread-3-same object");
Thread t4 = new Thread(selfTest, "Thread-4-same object");
//如果同步代码块或者同步方法是对新对象互斥,那这4个都不互斥
// 如果是对当前对象互斥,那Thread1,Thread2不互斥,与Thread3,4也不互斥。但是Thread3与Thread4互斥
// 如果是对类监视器互斥(假如用更多是SelfTest类监视器),那这四个线程之间都互斥
t1.start();
t2.start();
t3.start();
t4.start();
}
}
线程池和阻塞队列
线程池是在多线程里用于管理线程的工具,可以重复利用已经存在的线程,降低创建和销毁线程消耗。不光是线程池,像是做数据库连接的时候也有数据库连接池一样,让程序员更关注程序本身,而非重复的建立连接,断开连接。
先上总体的图。
线程池执行过程:
如上图所示,当任务提交者提交了新的任务时,在线程池里先判断核心线程池是否满了,如果没有满,就创建新的线程去执行这个任务,如果满了,就判断阻塞队列是否满了。如果没有,把这个任务添加到阻塞队列中,如果满了,再去判断线程池是否满了,如果没有则创建线程去执行这个任务,如果线程池整体满了,则按照当前线程池的饱和策略进行处理。
根据上述文字描述,体现出了两个问题。第一个是当前线程池,第二个是饱和策略。说明,线程池有多种,并且饱和策略也有多种。现在会逐一讲解线程池和饱和策略。
综述
这个小章节是把下面所有的线程池,饱和策略,阻塞队列这三个大的关注点进行综述,并用表格的形式展开,方便查阅。针对表格中出现的内容,可以从下方线程池分类,阻塞队列分类中看到更详细描述。
注,下面详细介绍中提及的每个Executors静态方法,在Executors中都提供了传入自定义工厂类作为参数的方法,详细参数列表见Executors中源码。
线程池概述表格
线程池别名 | 实际类 | 简易创建方法 | 创建出来线程池的特点 |
---|---|---|---|
CachedThreadPool | ThreadPoolExecutor | Executors.newCachedThreadPool() | 1. 线程池没有固定大小,最大可以有Integer.Max_Value这么大;2. 线程池中的线程可以重复利用,线程的默认时间1分钟;3. 当线程池中如果没有可用线程时,会创建一个新的线程。 |
FixedThreadPool | ThreadPoolExecutor | Executors.newFixedThreadPool(int nThreads) | 1. 线程池的核心线程数和线程总数一样,大小就是传入的int值;2. 线程池中的线程将一直存在,除非显示的关闭线程池;3. 如果线程池中的全部线程都处于工作状态,提交进来的任务会在阻塞队列中进行等待。 |
SingleThreadExecutor | ThreadPoolExecutor | Executors.newSingleThreadExecutor() | 1. 这个线程池里只有一个线程,并且这个值不能在被创建之后进行重新配置;2. 线程池中的线程将一直存在,除非显示的关闭线程池;3. 如果线程池中的全部线程都处于工作状态,新提交进来的任务会在阻塞队列中进行等待。 |
ScheduledThreadPool | ScheduledThreadPoolExecutor | Executors.newScheduledThreadPool(int corePoolSize) | 1. 这个线程池在创建的时候指定了核心线程数量,但是他的最大线程池是Integer.Max_Value;2. 这个线程池可以进行延迟执行或者周期执行的线程池; |
SingleThreadScheduledExecutor | ScheduledThreadPoolExecutor | Executors.newSingleThreadScheduledExecutor() | 1. 这个线程池中只有一个核心线程,并且核心线程数无法在创建之后进行重新配置;2. 这个线程池可以进行延迟执行或者周期执行的线程池;3. 当线程池中的线程都处在工作状态,提交的任务会在阻塞队列中进行等待 |
WorkStealingPool | ForkJoinPool | Executors.newWorkStealingPool(),或者,Executors.newWorkStealingPool(int parallelism) | 1. 这个线程池中的线程是按照并行执行,而非并发执行;2. 这个线程池在使用时会根据在ForkJoinTask中配置的最大任务数对大的任务进行fork,之后再对多个子任务的执行结果进行join;3. 这个线程池内实现了work-stealing策略 |
线程池使用中的基础方法
线程池别名 | 线程池基础方法 | 意义 |
---|---|---|
CachedThreadPool,FixedThreadPool ,SingleThreadExecutor | execute(Runnable command) | 给线程里提交任务 |
CachedThreadPool,FixedThreadPool ,SingleThreadExecutor | submit(Runnable/Callable command) | 给线程里提交任务 |
CachedThreadPool,FixedThreadPool ,SingleThreadExecutor | awaitTermination(long timeout, TimeUnit unit) | 1. 在任务都执行完了之后收到shutdown请求;2. 等待超过了指定的超时时间;3. 当前线程被interrupt。上述三种情况,哪个先发生,就在发生的时候停止线程池。翻译一下就是,要么所有任务都执行完并且执行了shutdown,或者超过了等待时间,或者开启线程池的线程被interrupt了,那么线程池就会进行关闭 |
CachedThreadPool,FixedThreadPool ,SingleThreadExecutor | shutdown() | 有序的关闭线程池,在此之前提交的任务还会执行,但不接受任何新任务。如果已关闭,则调用没有附加效果。此方法不会等待以前提交的任务完成执行,联合使用awaitTermination来做到这一点。 |
CachedThreadPool,FixedThreadPool ,SingleThreadExecutor | shutdownnow() | 尝试停止所有主动执行的任务,在阻塞队列中的任务会被清空,并且返回当时清空时阻塞队列中的任务。这个方法不会等待正在执行的任务执行完成,联合使用awaitTermination来做到这一点。shutdownnow方法只是尽力停止正在执行的任务,除此之外什么都保证不了。这个方法可以通过interrupt方法中断,但是所有没有响应interrupt方法而中断的任务可能永远都不会终止。 |
ScheduledThreadPool ,SingleThreadScheduledExecutor | - | ScheduledThreadPoolExecutor 中除了有上述基础方法(分别实现自Executor和ExecutorService)之外,更推荐针对可调度线程池使用如下实现自ScheduledExecutorService的方法 |
ScheduledThreadPool ,SingleThreadScheduledExecutor | schedule(Callable callable,long delay, TimeUnit unit) | 提交一个任务给线程池,并且延后delay的时间再执行 |
ScheduledThreadPool ,SingleThreadScheduledExecutor | schedule(Runnable command,long delay, TimeUnit unit) | 提交一个任务给线程池,并且延后delay的时间再执行 |
ScheduledThreadPool ,SingleThreadScheduledExecutor | scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) | 创建并执行一个定期操作,该操作在给定的初始延迟后首先启用,随后在一次执行终止和下一次执行开始之间等待给定的延迟。如果任务的任何执行遇到异常,则禁止后续执行。否则,任务只能通过取消或终止执行器来终止 |
ScheduledThreadPool ,SingleThreadScheduledExecutor | scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 创建并执行一个周期操作,该操作在给定的初始延迟后首先启用。启用之后,每次间隔period的时间就会执行一次。如果任务的任何执行遇到异常,则禁止后续执行。否则,任务只能通过取消或终止执行器来终止。如果某一次任务执行时间都超过其周期(period),则后续执行可能开始较晚,但不会同时执行。 |
WorkStealingPool | - | ForkJoinPool中除了有上述基础方法(分别实现自Executor和ExecutorService)之外,还提供了更符合自己特色的方法 |
WorkStealingPool | commonPool() | ForkJoinPool.commonPool() ,是ForkJoinPool中的静态方法,通过这个方法也可以得到一个ForkJoinPool对象 |
WorkStealingPool | execute(ForkJoinTask<?> task) | 给ForkJoinPool添加一个分治任务,通过Excute方法添加的任务是异步执行的 |
WorkStealingPool | invoke(ForkJoinTask<?> task) | 给ForkJoinPool添加一个分治任务,执行完成后返回执行结果(task.join()) |
WorkStealingPool | submit(ForkJoinTask task) | 给ForkJoinPool添加一个分治任务,执行完成后把整个task进行返回。 |
线程池分类
在Java中,通过java.util.concurrent.Executors工厂类进行线程池的简单实例化,或者直接调用目标线程池对象类型的构造方法,进行自定义实例。基于调用的不同方法和传入参数,常用的线程池分为如下几类。
注意,这部分内容编排与网上绝大多数的资料都不一样,因为网上是按照Executors中生成线程池的方法名命名,而我这里是先按照那些方法里调用了哪个类的构造方法进行分类,这样分类之后,方便将同一类型对象下的各种策略进行讲解。
ThreadPoolExecutor对象
下面出现的线程池在实例化过程里都是调用了ThreadPoolExecutor类的构造方法。
Executors.newCachedThreadPool()
也就是大家总提到的CachedThreadPool,CachedThreadPool缓存线程池是一个常用的线程池,是ThreadPoolExecutor类的对象。
CachedThreadPool线程池,会根据需要创建新线程,但当以前构造的线程可用时,将重用它们。超过60 秒未被使用的线程将被终止,并已从缓存中删除。CachedThreadPool线程池通常会提高执行许多短期异步任务的程序的性能。
在执行newCachedThreadPool()时,实际上是实例化ThreadPoolExecutor的过程。
newCachedThreadPool()方法体如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
上面的方法中调用的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
第一个参数,int corePoolSize,核心线程池大小,传的值是0;
第二个参数,int maximumPoolSize,线程池最大容量,传入的是Integer.MAX_VALUE;
第三个参数, long keepAliveTime,存活时间,传入的值是,60L;
第四个参数,TimeUnit unit,给指定时间一个时间单位,传入的是,TimeUnit.SECONDS;
第五个参数,BlockingQueue workQueue,阻塞队列,传入的是,
之所以写成代码段,我不知道<>内的内容怎么才能显示出来
new SynchronousQueue<Runnable>()
通过这些入参可以发现,CachedThreadPool,是一个核心大小为0,最大容量是Integer.MAX_VALUE,线程存活时间60秒,阻塞队列是SynchronousQueue队列的这么一个线程池。并且在实例化过程中,使用的Executors类默认的线程工厂,并且使用的ThreadPoolExecutor类默认的饱和策略对象,AbortPolicy。
默认的饱和策略具体情况如下:
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
饱和策略
在ThreadPoolExecutor类里,定义了四个静态内部类,这四个静态内部类都实现了RejectedExecutionHandler接口。实现了RejectedExecutionHandler接口的类,就是饱和策略的类。这四个内部类如下。
- AbortPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池会丢弃新加入的任务,并且抛出RejectedExecutionException异常。 - CallerRunsPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,则会在提交任务的线程里执行这个任务。如果线程池关闭了,则会丢弃这个新来的任务。 - DiscardOldestPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,会接收并执行这个新来的任务,并且丢弃队列里最旧的任务。 - DiscardPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池不会有任何反应,即不接受或者丢弃任务,也不报异常。
上面是对CachedThreadPool的基本解释,通过Executors可以简单的创建出一个线程池,同时也可以选择通过ThreadPoolExecutor类中更多种的构造方法进行更多自定义的创建线程池,并且可以在创建出线程池之后通过set方法去自定义线程池里的属性。
Executors.newFixedThreadPool(int nThreads)
也就是大家总说的FixedThreadPool。
FixedThreadPool线程池的核心线程数和最大线程数一样(传入的 nThreads)。在任何时刻,最多 nThreads 条线程都将是活动处理任务。如果当所有线程处于活动状态时提交其他任务,它们将在队列中等待,直到线程可用。如果任何线程在关闭前执行过程中发生故障而终止(TERMINATED),那么一个新线程将代替它(如果还需要执行后续任务)。FixedThreadPool线程池中的线程将一直存在,直到显式关闭(shutdown()/shutdownnow())。
newFixedThreadPool(int nThreads)和之前提到的newCachedThreadPool()一样,返回的同为ThreadPoolExecutor类的对象。
newFixedThreadPool(int nThreads)方法体如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
根据上面代码内容可以看出来,newFixedThreadPool()方法里传递的int值,被当做了ThreadPoolExecutor对象的核心线程大小,最大线程数。并且,这个方法里设置的线程可存活时间是0毫秒,同时还有一个不一样的位置,就是这里用到的阻塞队列是LinkedBlockingQueue。针对各个阻塞队列的特性,会在最下面进行整体介绍。
与newCachedThreadPool()一样,newFixedThreadPool(int nThreads)返回的也是ThreadPoolExecutor,有着同样的饱和策略。
饱和策略
在ThreadPoolExecutor类里,定义了四个静态内部类,这四个静态内部类都实现了RejectedExecutionHandler接口。实现了RejectedExecutionHandler接口的类,就是饱和策略的类。这四个内部类如下。
- AbortPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池会丢弃新加入的任务,并且抛出RejectedExecutionException异常。 - CallerRunsPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,则会在提交任务的线程里执行这个任务。如果线程池关闭了,则会丢弃这个新来的任务。 - DiscardOldestPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,会接收并执行这个新来的任务,并且丢弃队列里最旧的任务。 - DiscardPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池不会有任何反应,即不接受或者丢弃任务,也不报异常。
Executors.newSingleThreadExecutor()
也就是大家总说的SingleThreadExecutor。
SingleThreadExecutor线程池,创建使用一个只有一个线程的线程池。但是请注意,如果此单个线程在关闭前执行期间发生故障而终止,如果需要执行后续任务,新线程将代替它。保证任务按顺序执行,并且在任何给定时间不会激活超过一个任务。
与其他等效的 newFixedThreadPool(1)不同,返回的执行器保证不会重新配置以使用更多线程。简单而言,SingleThreadExecutor被创建出来之后,无法再次定义核心线程池数和线程池总大小。
newSingleThreadExecutor()方法体如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
与newCachedThreadPool(),newFixedThreadPool(int nThreads)一样,newSingleThreadExecutor()返回的也是ThreadPoolExecutor,有着同样的饱和策略。
饱和策略
在ThreadPoolExecutor类里,定义了四个静态内部类,这四个静态内部类都实现了RejectedExecutionHandler接口。实现了RejectedExecutionHandler接口的类,就是饱和策略的类。这四个内部类如下。
- AbortPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池会丢弃新加入的任务,并且抛出RejectedExecutionException异常。 - CallerRunsPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,则会在提交任务的线程里执行这个任务。如果线程池关闭了,则会丢弃这个新来的任务。 - DiscardOldestPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,会接收并执行这个新来的任务,并且丢弃队列里最旧的任务。 - DiscardPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池不会有任何反应,即不接受或者丢弃任务,也不报异常。
ScheduledThreadPoolExecutor对象
在这个分栏下面出现的方法内部,都是在实例化ScheduledThreadPoolExecutor类,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类。
从名称可以看出来,ScheduledThreadPoolExecutor类,是可调度的线程池。
Executors.newScheduledThreadPool(int corePoolSize)
也就是人们总说的ScheduledThreadPool。
ScheduledThreadPool线程池是ThreadPoolExecutor的子类,它可以安排任务在给定延迟之后运行,或定期执行。当需要多个工作线程时,或者当需要为ThreadPoolExecutor提供额外灵活性或功能时,此类比 Timer 更可取。
newScheduledThreadPool(int corePoolSize)方法体代码如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
在newScheduledThreadPool(int corePoolSize)方法中,调用了ScheduledThreadPoolExecutor的构造方法,构造方法代码如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
在ScheduledThreadPoolExecutor的构造方法中,调用了父类构造方法进行实例化,具体的参数意义如下:
第一个参数,int corePoolSize,核心线程池大小;
第二个参数,int maximumPoolSize,线程池最大容量;
第三个参数,int keepAliveTime,存活时间;
第四个参数,TimeUnit unit,对第三个参数设置一个单位;
第五个参数,阻塞队列。
从构造方法中的参数就可以直观的看出来,之所以ScheduledThreadPoolExecutor是一个可调度的线程池,究其原因是因为使用的阻塞队列与之前介绍的不一样,这里使用的阻塞队列是DelayedWordQueue。针对阻塞队列的内容,会在线程池的下面进行详细介绍。
因为ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,ScheduledThreadPoolExecutor的饱和策略使用了ThreadPoolExecutor提供的饱和策略。
饱和策略
在ThreadPoolExecutor类里,定义了四个静态内部类,这四个静态内部类都实现了RejectedExecutionHandler接口。实现了RejectedExecutionHandler接口的类,就是饱和策略的类。这四个内部类如下。
- AbortPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池会丢弃新加入的任务,并且抛出RejectedExecutionException异常。 - CallerRunsPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,则会在提交任务的线程里执行这个任务。如果线程池关闭了,则会丢弃这个新来的任务。 - DiscardOldestPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,会接收并执行这个新来的任务,并且丢弃队列里最旧的任务。 - DiscardPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池不会有任何反应,即不接受或者丢弃任务,也不报异常。
Executors.newSingleThreadScheduledExecutor()
也就是人们总说的SingleThreadScheduledExecutor。创建单线程线程池,该线程池可以安排任务在给定延迟之后运行,或定期执行。(但是请注意,如果此单个线程在关闭前执行期间发生故障而终止,如果需要执行后续任务,新线程将代替它。保证任务按顺序执行,并且在任何给定时间不会激活超过一个任务。)与其他等效的 newSchedThreadPool(1)不同,返回的线程池保证不会重新配置以使用其他线程。
SingleThreadScheduledExecutor与ScheduledThreadPool的关系,就像是,SingleThreadExecutor与FixedThreadPool的关系一样。SingleThreadScheduledExecutor同样在实例化之后,无法重新设置,去使用更多的线程数。
newSingleThreadScheduledExecutor()代码如下:
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
SingleThreadScheduledExecutor内部同样是使用DelayedWorkQueue作为阻塞队列。
饱和策略也与其他的都一致,共有四种。
饱和策略
在ThreadPoolExecutor类里,定义了四个静态内部类,这四个静态内部类都实现了RejectedExecutionHandler接口。实现了RejectedExecutionHandler接口的类,就是饱和策略的类。这四个内部类如下。
- AbortPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池会丢弃新加入的任务,并且抛出RejectedExecutionException异常。 - CallerRunsPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,则会在提交任务的线程里执行这个任务。如果线程池关闭了,则会丢弃这个新来的任务。 - DiscardOldestPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,如果线程池没有关闭,会接收并执行这个新来的任务,并且丢弃队列里最旧的任务。 - DiscardPolicy
当核心线程池满了,阻塞队列满了,线程池整体也满了,此时有新的任务过来,线程池不会有任何反应,即不接受或者丢弃任务,也不报异常。
ForkJoinPool对象
在ThreadPoolExecutor和ScheduledThreadPoolExecutor类都是比较好理解的,他们就是用多线程的形式,从一个队列中,逐个抓取任务来并发执行,但是ForkJoinPool和上述两种有显著区别。
在提ForkJoinPool显著区别之前,先顺带说一句。对于之前介绍到的ThreadPoolExecutor和ScheduledThreadPoolExecutor而言,在使用中涉及如下对象,线程池本身(ThreadPoolExecutor和ScheduledThreadPoolExecutor),添加到线程池里要执行的对象(Runnable和Callable对象),线程池里使用到的阻塞队列(LinkedBlockingQueue,DelayedWorkQueue,SynchronousQueue)以及饱和策略(AbortPolicy,DiscardPolicy,DiscardOldestPolicy和CallerRunsPolicy)。把握住这些内容的配置和使用,对于上述两大类线程池就算是把基础知识有了很好的理解。
ForkJoinPool两大区别
介绍这量大区别之前,先把三个在ForkJoinPool里面的核心类列出来。
ForkJoinPool,分治线程池;
ForkJoinTask,分治之后的任务;
ForkJoinWorkerThread,用于执行分治任务的线程;
ForkJoinPool类,从名字上看核心点在于fork和join,换做我们熟悉的说话,fork是分叉,join是合并,ForkJoin体现的是分治算法(Divide and conquer algorithm)。
对于分治算法而言,有两个核心问题需要考虑,一个是如何把一个大的任务合理的进行拆分,另一个就是如何把拆分执行的子任务结果进行合并。这一点在Java提供的API中有很好的辅助。
虽然是ExecutorService对象(ThreadPoolExecutor,ScheduledThreadPoolExecutor往上也都继承了ExecutorService类),ForkJonPool在执行任务时也可以执行Runnable和Callable,但是专门为了更好的做到分治,Java为ForkJoinPool提供了ForkJoinTask泛型类。这个泛型类对ForkJoinPool的分治任务有更好的支持(实际使用中,更多使用了ForkJoinTask的子类RecursiveTask泛型类)。
除过上述所言的分治,ForkJoinPool中还有一个明显区别与其他两大类线程池的地方,就在于work-stealing(工作窃取)。
在其他两大类线程池里,都只有一个阻塞队列,线程池里的任务都会从同一个队列中按照队列规定的逻辑抓取任务,此时不存在说某个队列里的任务为空,某个队列里堆积的任务很多。
但是,在ForkJoinPool里面,通过对大型任务的分割,分割出多个子任务,而这些子任务的内部,每个子任务都有自己的阻塞队列。针对工作窃取,这里只简单的介绍相应的效果。所谓工作窃取,通过ForkJoinPool对象动态监控线程池内fork出来的子任务执行情况。每个子任务(ForkJoinTask)内部有自己的工作队列(WorkQueue)。当存在某个子任务将自己工作队列中的任务执行完毕后,查看其它子任务里有没有未执行的工作。如果有,则会抓取其它子任务内的工作任务进行执行。
ForkJoinPool里面有一个内部WorkQueue,这个内部类在ForkJoinPool,ForkJoinTask,ForkJoinWorkerThread中都有运用到,具体的工作队列和work-stealing都是基于这个内部类来做的。详情可以看源码。
Executors.newWorkStealingPool()和Executors.newWorkStealingPool(int parallelism)
上述两个方法,是通过Executors创建ForkJoinPool的方法,也就是常说的WorkStealingPool线程池。
上述方法里唯一的区别在于是否传int parallelism,这个值表示并行数。
注,多线程大多数都是并发,唯独这里是并行,这个是因为在这个线程子任务(ForkJoinTask)中提供了fork方法,这个方法涉及很多操作系统中的这里不做详细解释。fork方法的作用是把子任务放在不同的CPU上进行并行处理。
newWorkStealingPool()代码如下:
/**
* Creates a work-stealing thread pool using all
* {@link Runtime#availableProcessors available processors}
* as its target parallelism level.
* @return the newly created thread pool
* @see #newWorkStealingPool(int)
* @since 1.8
*/
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
这里用无参方法举例,在这里会发现,如果没有显示指定并行数的话,会返回当前可用的处理器总数。
注,如果是使用了超线程技术的CPU,这个返回的值还是CPU核数,而非超线程数。
这个方法里内部调用的ForkJoinPool构造方法如下:
/**
* Creates a {@code ForkJoinPool} with the given parameters.
*
* @param parallelism the parallelism level. For default value,
* use {@link java.lang.Runtime#availableProcessors}.
* @param factory the factory for creating new threads. For default value,
* use {@link #defaultForkJoinWorkerThreadFactory}.
* @param handler the handler for internal worker threads that
* terminate due to unrecoverable errors encountered while executing
* tasks. For default value, use {@code null}.
* @param asyncMode if true,
* establishes local first-in-first-out scheduling mode for forked
* tasks that are never joined. This mode may be more appropriate
* than default locally stack-based mode in applications in which
* worker threads only process event-style asynchronous tasks.
* For default value, use {@code false}.
* @throws IllegalArgumentException if parallelism less than or
* equal to zero, or greater than implementation limit
* @throws NullPointerException if the factory is null
* @throws SecurityException if a security manager exists and
* the caller is not permitted to modify threads
* because it does not hold {@link
* java.lang.RuntimePermission}{@code ("modifyThread")}
*/
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
第一个参数,int parallelism,并行数;
第二个参数,ForkJoinWorkerThreadFactory factory,ForkJoinWorkerThread工厂类;
第三个参数,UncaughtExceptionHandler handler,未捕获异常处理器;
第四个参数,boolean asyncMode,表示是否异步;
第五个参数,String workerNamePrefix,子任务工作线程名称前缀;
于ForkJoinPool而言,他没有自己的饱和策略,不像其他线程池那样简单定义了对于各种情况的解决方案。他是通过Pool,task,workerThread动态执行和控制添加到ForkJoinPool中的任务。这里在基础知识中不作讲解,他对API熟悉程度要求太高。
线程池生命周期
线程池生命周期:
对线程池而言,并没有类似线程那样的start方法。在线程池里,创建好就算是RUNNING了,之后在运行中,各个状态之间的跃迁就如上图所示。
阻塞队列
JUC中的一个很重要的内容,就是队列。
在Java中,队列的层级关系如下图所示:
从上面的途中可以看出来,队列最顶层两个元素是很熟悉的Iterable和Collection接口。这也就从侧面描述了对于队列而言,他是一个一维的数据类型。在最下层分别是BlockingQueue和AbstractQueue,也就是说我们的队列全部实现或者继承了BlockingQueue和AbstractQueue。
在基础知识部分,先讲解几个最常见,最常用的队列。
ArrayBlockingQueue
ArrayBlockingQueue,基于数组实现的阻塞队列。在ArrayBlockingQueue中维护了一个定长数组,以便缓存队列中的对象。ArrayBlockingQueue的构造方法里有一个boolean值,如果这个值是true,则采用FIFO的顺序,如果是false则采用未指定访问顺序的形式。如果boolean不写,默认是false。
同时,ArrayBlockingQueue在初始化的时候,必须指定大小。
在ArrayBlockingQueue中生产者和消费者获取数据都是用同一个对象锁,由此也意味着两者无法真正并行运行。
ArrayBlockingQueue在插入和删除元素时,不会产生或者销毁任何额外的对象实例。
在ArrayBlockingQueue中,有好多中插入和移除内部元素的方法。这里不做过度介绍,只是列举出方法名,后续有意向的,可以自行了解。
插入 | 取出 |
---|---|
offer(E e) | poll() |
put(E e) | take() |
offer(E e, long timeout, TimeUnit unit) | poll(long timeout, TimeUnit unit) |
LinkedBlockingQueue
LinkedBlockingQueue,是基于链表实现的阻塞队列,其内部也维护着一个数据缓冲队列。
与ArrayBlockingQueue不同点在于,首先,LinkedBlockingQueue中生产者和消费者采用了两个对象锁,这也使得LinkedBlockingQueue在实际应用中可以真正得到并行处理。然后,在创建和删除队列中元素时,LinkedBlockingQueue产生了中间对象node,node的存在使得在高并发长时间处理大批量数据时,队列的增减对GC产生的压力要大于ArrayBlockingQueue。最后,在初始化过程里,LinkedBlockingQueue不必指定容量,如果没有指定容量,则容量是Integer.MAX_VALUE,同时在实例化的时候也无法指明队列是FIFO或是未指定访问顺序,LinkedBlockingQueue是一个FIFO队列。
插入 | 取出 |
---|---|
offer(E e) | poll() |
put(E e) | take() |
offer(E e, long timeout, TimeUnit unit) | poll(long timeout, TimeUnit unit) |
DelayQueue
DelayQueue最大的特点就是,DelayQueue中的元素只有在等待了指定的Delay时间之后,才能从队列中获取到该元素。
DelayQueue是一个没有大小限制的队列,因此向队列中插入数据的操作永远不会阻塞,只有在获取数据的操作中才会发生阻塞。
在实际使用中,DelayQueue的应用场景并不多见,但都比较巧妙。最常见的例子,比如使用DelayQueue去管理一个超时未响应的连接队列。
注意,在使用DelayQueue的时候,往DelayQueue中添加的任务需要是实现了Delayed接口的类。在Delayed接口中有两个核心方法,接口的方法实现可以根据自己的需要进行编写,下面给出的只是一种推荐写法:
方法名 | 方法意义 |
---|---|
getDelay(TimeUnit unit) | 将延迟结果进行返回,用运行时间-当前系统时间进行判断 |
compareTo(Delayed o) | compareTo方法的比较结果,会影响延迟队列中出队列的结果。如果没有定义好,会在出队列的时候,低延迟结果一直出不来 但是高延迟结果在队列最上方 |
在compareTo方法中,调用getDelay方法,返回延迟任务与当前系统时间的差值。按照差异大小排序。差异越小,就排前面。 |
代码块如下:
/**
* 要使用DelayQueue,需要将task实现Delayed接口,并且实现内部的getDelay和compareTo方法
*/
class DelayTask implements Delayed {
private TimeUnit time_unit;
private Long delay_time;
private Long execute_time;
private String task_name;
@Override
public String toString() {
return "DelayTask{" +
"time_unit=" + time_unit +
", delay_time=" + delay_time +
", execute_time=" + execute_time +
", task_name='" + task_name + '\'' +
'}';
}
public DelayTask(Long delay_time, String task_name) {
this.time_unit = TimeUnit.MILLISECONDS;
this.delay_time = delay_time;
this.execute_time = time_unit.convert((delay_time + System.currentTimeMillis()), this.time_unit);
this.task_name = task_name;
}
/**
* @param unit
* @return 将延迟结果进行返回,用运行时间-当前系统时间进行判断
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert((this.execute_time - System.currentTimeMillis()), this.time_unit);
}
/**
* compareTo方法的比较结果,会影响延迟队列中出队列的结果。如果没有定义好,会在出队列的时候,低延迟结果一直出不来
* 但是高延迟结果在队列最上方
* <p>
* 在compareTo方法中,调用getDelay方法,返回延迟任务与当前系统时间的差值。按照差异大小排序。差异越小,就排前面。
*
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
if (this.getDelay(this.time_unit) > o.getDelay(((DelayTask) o).time_unit)) {
return 1;
} else if (this.getDelay(this.time_unit) < o.getDelay(((DelayTask) o).time_unit)) {
return -1;
} else {
return 0;
}
}
}
队列方法还是那几个:
插入 | 取出 |
---|---|
offer(E e) | poll() |
put(E e) | take() |
offer(E e, long timeout, TimeUnit unit) | poll(long timeout, TimeUnit unit) |
PriorityBlockingQueue
PriorityBlockingQueue,基于优先级的阻塞队列(优先级的判断通过传入的对象来决定,这个对象必须能被排序)。
注意,PriorityBlockingQueue并不会阻塞数据生产者,只会在没有可消费数据时,阻塞数据的消费者。因此在使用的时候,生产者生产数据的速度绝对不能小于消费者消费数据的速度,否则时间一长,会耗尽所有的可用堆内空间。
在实现PriorityBlockingQueue时,内部控制线程同步的锁是公平锁(FIFO)。
与DelayQueue类似,DelayQueue中要求他的task需要实现Delayed接口,而PriorityBlockingQueue中要求他的任务必须是能被排序的,简单来做的话,这个任务就需要实现Comparable接口,并且实现核心方法CompareTo。
代码示例如下:
/**
* 在使用PriorityBlockingQueue的时候,和DelayQueue一样,需要将任务实现comparable接口,并且实现接口中的compareTo方法。进行优先级排序。
*
*/
class Task implements Comparable {
private Integer priority_level;
private String name;
@Override
public String toString() {
return "Task{" +
"priority_level=" + priority_level +
", name='" + name + '\'' +
'}';
}
public Task() {
}
public Task(Integer priority_level, String name) {
this.priority_level = priority_level;
this.name = name;
}
@Override
public int compareTo(Object o) {
if (this.priority_level < ((Task) o).priority_level) {
return 1;
} else if (this.priority_level > ((Task) o).priority_level) {
return -1;
} else {
return 0;
}
}
}
SynchronousQueue
这个队列有些特殊,是一种无缓存的等待队列。可以粗略的类比为原始社会中的情况,生产者生产了产品之后,自己拿到集市上去售卖给已经在集市上的消费者。如果这两者中人任何一个没有准备好,则整个过程都会进行等待。
相对于有缓存的BlockingQueue而言,少了中间缓存的过程,单个产品上的及时响应性能会上升,但是在整体的吞吐量上会下降。
在前面整篇介绍里,之后一个地方用到了SynchronousQueue,那就是CachedThreadPool。因为CachedThreadPool通过简单方法创建之后,他的最大线程数目到了一个夸张的情况,在那种情况下,对于队列中存在的任务只需要直接开辟一个线程去执行即可。
注意,在声明SynchronousQueue时有两种不同的方式,他们只在存在不太一样的行为。
代码如下:
SynchronousQueue<String> queue = new SynchronousQueue<>(true);//默认是false,使用非公平锁,队列无序
构造方法如下:
/**
* Creates a {@code SynchronousQueue} with the specified fairness policy.
*
* @param fair if true, waiting threads contend in FIFO order for
* access; otherwise the order is unspecified.
*/
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
在创建SynchronousQueue时,如果显示指明了采用公平锁,数据就会放置在一个FIFO队列中来阻塞多余的生产者和消费者,从而做到整体FIFO。
但是如果显示指明了false,或者缺省了这个参数(缺省是默认是false),数据就会放置在栈这样的LIFO的数据结构中。此时,如果生产者的处理速度有差异,则很容易出现饥渴的情况,即可能有些生产者或者消费者的数据永远都得不到处理。