相关概念
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。
一个进程是一个独立的运行环境,它可以被看作一个程序或者一个应用。而**线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类和程序的单一进程。**线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。
多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存(heap memory),因此创建多个线程去执行一些任务会比创建多个进程更好。举个例子,Servlets比CGI更好,是因为Servlets支持多线程而CGI不支持。
这里所谓的多个线程“同时”执行是人的感觉,实际上,是多个线程轮换执行。
线程调度器(ThreadScheduler)是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片(Time Slicing)是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
多线程的优点:
进程之间不能共享内存,但线程之间共享内存很容易;
系统创建进程时需要为该进程重分配系统资源,但创建线程代价小很多。因此,用多线程实现多任务并发比多进程的效率高;
java内置多线程功能支持,不是单纯地作为底层操作系统的调度方式。
线程的生命周期
线程的生命周期有五个状态。
新建(New):线程对象已经创建,还没有在其上调用start()方法;
**就绪(Runnable):**当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入就绪状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
**运行(Running):**线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
**阻塞(Blocked):**这是线程有资格运行时它所处的状态,线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
阻塞的情况分三种:
1、**等待阻塞:**运行的线程执行wait()方法,JVM会把该线程放入等待池中。
2、**同步阻塞:**运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
3、**其他阻塞:**运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
终止:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
各状态间的转换条件如下图所示:
守护线程
守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。
可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。该方法必须在启动线程前调用。该方法首先调用该线程的 checkAccess方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
线程的创建
有三种方法可以创定义、创建线程:
(一)继承Thread类创建线程类;(二)实现Runnable接口创建线程类;(三)使用Callable和Future创建线程。
方式一代码:但是要注意–使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
// 通过继承Thread类来创建线程类
public class TestThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + " ");
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 创建、并启动一条线程.所有总共有两条线程。
new TestThread().start();
}
}
}
}
1、继承java.lang.Thread类
一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。
run()方法就是Thread实例的一个成员方法,如果我们直接调用,就跟调用其他成员方法一样,不会由于线程的的调度而产生阻塞、执行的状态,而会一直执行完毕,完毕之前后面的程序不会执行。先执行threadA.run(),完毕后再执行threadB.run(),完毕后再执行threadC.run()。所以它并不是一个多线程程序
虽然start()也是调用run()方法来执行相关任务的,但是start()方法只是让线程进入可执行状态(就绪状态),等待cpu分配给它时间片,并不一定会立刻执行。这时可能有多个线程处在可执行状态,线程调度器轮流分配给它们时间片。所以它是一个多线程程序。
2、实现java.lang.Runnable接口
// 通过实现Runnable接口来创建线程类
public class RunnableThread implements Runnable
{
private int i ;
// run方法同样是线程执行体
public void run()
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName()
+ " " );
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
//创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象。
RunnableThread st = new RunnableThread();
// 通过new Thread(target , name)方法创建新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}
Runnable接口中的run()方法,跟Thread类中的run()方法一样,线程执行的任务需要写在run()方法中。
Thread的构造方法可接受一个Runnable的实例,用Runnable的run()方法覆盖掉Thread类的run()方法。
多个线程同时对同一实例中的统一数据进行了读取操作造成的。为了避免这种情况,使共享数据在同一时刻只能有一个线程进行读取,这就是线程的同步控制。
方式三代码:与实现Runnable接口基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常。
public class CallableThread
{
public static void main(String[] args)
{
// 创建Callable对象
CallableThreadrt = new CallableThread();
// 先使用Lambda表达式创建Callable<Integer>对象
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" );
// call()方法可以有返回值
return i;
});
for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
3、补充
1、一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。
2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。
3、获取当前线程的对象的方法是:Thread.currentThread();
4、在上面的代码中,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
5、当线程目标run()方法结束时该线程完成。
6、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
7、**线程的调度是JVM的一部分,**在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。
8、尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列成个一个队列的事实。
9、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式,比如设置优先级,以及调用Thread.sleep(),wait(),yield()等方法。
补充:三种方式的比较:
实现Runnable和Callable接口的方式:
优点:
1.只是实现了接口,还可以继承其他类;
2.这种情况下,多个线程可共享同一个target对象,适合多个相同线程来处理同一份资源。从而可将CPU、代码和数据分开,形成清晰模型。
缺点:编程稍复杂,如需访问当前线程,必须用Thread.currentThread方法。
继承Thread类的方式:
优点:编写简单,如需访问当前线程,只需使用this获得当前线程。
缺点:因为已经继承了Thread类,所以不能继承其他类了。
线程的状态装换
sleep()方法
Thread.sleep(long millis)和Thread.sleep(long millis,int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。
为了让其他线程有机会执行,将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会休眠。
1、线程休眠是帮助所有其他线程获得运行机会的最好方法;
2、线程休眠到期自动苏醒,并返回到就绪状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程休眠到期后就开始执行;
3、sleep()是静态方法,只能控制当前正在运行的线程。
yield()方法
线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。
要理解yield(),必须了解线程的优先级的概念。线程总是存在优先级,优先级范围在1~10之间。JVM线程调度程序是基于优先级的抢先调度机制。在大多数情况下,当前运行的线程优先级将大于或等于线程池中任何线程的优先级。但这仅仅是大多数情况。
注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,优先级越高只能代表它获取cpu资源的概率比较大。只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。
当线程池中线程都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能:一是选择一个线程运行,直到它阻塞或者运行完成为止。二是时间分片,为池内的每个线程提供均等的运行机会。
设置线程的优先级:线程默认的优先级是创建它的执行线程的优先级。可以通过setPriority(int newPriority)更改线程的优先级。例如:
Thread t = new MyThread();
t.setPriority(8);
t.start();
线程优先级为110之间的正整数,JVM从不会改变一个线程的优先级。然而,110之间的值是没有保证的。一些JVM可能不能识别10个不同的值,而将这些优先级进行每两个或多个合并,变成少于10个的优先级,则两个或多个优先级的线程可能被映射为一个优先级。
线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
static int MAX_PRIORITY 线程可以具有的最高优先级。
static int MIN_PRIORITY 线程可以具有的最低优先级。
static int NORM_PRIORITY 分配给线程的默认优先级。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到就绪状态,但有可能没有效果。
join()方法
join() 方法主要是让调用该方法的thread完成run方法里面的东西后,再执行join()方法后面的代码。
public class ThreadInstance extends Thread{
public static int count = 5 ;
@Override
public void run() {
for(int i=0;i<5;i++){
count--;
System.out.print(count+",");
}
}
public static void main(String[] args) {
Thread threadA = new ThreadInstance();
threadA.start();
System.out.println(ThreadInstance.count);
}
}
打印结果如下:
5
4,3,2,1,0,
就是说,System.out.println(ThreadInstance.count)这条语句打印出的结果为“5”,而不是“0”。
这是因为在上面的程序中存在两个线程,一个是主线程main,一个是子线程threadA。两个线程并发执行,threadA.start()只是让threadA进入就绪状态,并不一定会立即执行。同时main主线程不会等待threadA执行完毕,而是执行后面的语句,此时静态变量count还没有被threadA改变,打印出的结果是“5”。
如果想保证threadA执行完毕之后再执行后面的语句,就需要用到join()方法了。将程序修改如下:
public class ThreadInstance extends Thread{
public static int count = 5 ;
@Override
public void run() {
for(int i=0;i<5;i++){
count--;
System.out.print(count+",");
}
System.out.println();
}
public static void main(String[] args)throws InterruptedException {
Thread threadA = new ThreadInstance();
threadA.start();
threadA.join();
System.out.println(ThreadInstance.count);
}
}
打印结果如下:
4,3,2,1,0,
0
可见,join()方法保证了threadA执行完毕之后采取执行后面的语句。
在上例中,main线程是执行threadA的线程,join从字面上理解是“加入”的意思,就是表示把该线程加入到调用该线程的线程,保证其执行完毕再进行下一步的工作。
另外,join()方法还有带超时限制的重载版本。例如threadA.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。
interrupt()方法
java的中断机制:java的中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
当调用interrupt()方法的时候,只是设置了要中断线程的中断状态,而此时被中断线程可以通过非静态方法isInterrupted()或者是静态方法interrupted()方法判断当前线程的中断状态是否标志为中断。
class ATask implements Runnable{
private double d = 0.0;
public void run() {
//死循环执行打印"I am running!" 和做消耗时间的浮点计算
while (true) {
System.out.println("I am running!");
for (int i = 0; i < 900000; i++){
d = d + (Math.PI + Math.E) / d;
}
}
}
}
public class InterruptTaskTest {
public static void main(String[] args)throws Exception{
//将任务交给一个线程执行
Thread t = new Thread(new ATask());
t.start();
//运行一断时间中断线程
Thread.sleep(100);
System.out.println("****************************");
System.out.println("InterruptedThread!");
System.out.println("****************************");
t.interrupt();
}
}
运行这个程序,我们发现调用interrupt()后,程序仍在运行,如果不强制结束,程序将一直运行下去,如下所示:
I am running!
I am running!
I am running!
I am running!
InterruptedThread!
I am running!
I am running!
I am running!
I am running!
I am running!
…
interrupt()只是改变中断状态而已。**interrupt()不会中断一个正在运行的线程。这一方法实际上完成的是,给受阻塞的线程抛出一个中断信号,**这样受阻线程就得以退出阻塞的状态。更确切地说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
**如果线程没有被阻塞,这时调用interrupt()将不起作用;**否则,若线程处于阻塞状态,线程就将得到InterruptedException异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。
抛出InterruptedException和用Thread.interrupted()检查是否发生中断,下面分别看一下这两种方法:
1、在阻塞操作时如Thread.sleep()时被中断会抛出InterruptedException(注意,进行不能中断的IO操作而阻塞和要获得对象的锁调用对象的synchronized方法而阻塞时不会抛出InterruptedException),线程被中断
线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。java语言提供了专门的机制来解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
在具体的Java代码中需要完成一下两个操作:
1、把竞争访问的资源变量标识为private;
2、同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。
当然这不是唯一控制并发安全的途径。
同步
Java中每个对象都有一个内置锁。
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。\
要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。显而易见,因为静态方法、静态变量都是与类绑定的,而不是与某个特定的对象绑定。
如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种锁池中,必须在那里等待,直到其锁被释放,该线程再次变为可运行或运行为止。
当考虑阻塞时,一定要注意哪个对象正被用于锁定:
1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。
3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。
死锁
死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。
public class DeadlockRisk {
private static class Resource {
public int value;
}
private Resource resourceA =new Resource();
private Resource resourceB =new Resource();
public int read() {
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value +resourceA.value;
}
}
}
public void write(int a,int b) {
synchronized (resourceB) {
synchronized (resourceA) {
resourceA.value = a;
resourceB.value = b;
}
}
}
}
假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。
实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。
就算我们费尽心机去写一个故意死锁的程序,也不见会发生死锁。但是,无论代码中发生死锁的概率有多小,一旦发生死锁,程序就死掉。
volatile关键字
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中
当我们使用volatile关键字去修饰变量的时候,所有线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。
volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。
Java 语言中的 volatile变量可以被看作是一种“程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。
之所以要单独提出volatile这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
只能在有限的一些情形下使用volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1、对变量的写操作不依赖于当前值。
2、该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而volatile 不能提供必须的原子特性。实现正确的操作需要使 x的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。
比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入。
同样,对于一个long型数据,做了个赋值操作,在32位系统下需要经过两步才能完成,先修改低32位,然后修改高32位。
假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
下面看一个例子,我们实现一个计数器,每次线程启动的时候,会调用计数器inc方法,对计数器进行加一。
public class Counter {
public static int count = 0;
public static void inc() {
//这里延迟1毫秒,使得结果明显
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行i++计算,看看实际结果
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
//这里每次运行的值都有可能不同,可能为1000
System.out.println("运行结果:Counter.count="+ Counter.count);
}
}
运行结果:Counter.count=995
实际运算结果每次可能都不一样,本机的结果为:运行结果:Counter.count=995,可以看出,在多线程的环境下,Counter.count并没有期望结果是1000
很多人以为,这个是多线程并发问题,只需要在变量count之前加上volatile就可以避免这个问题,那我们在修改代码看看,看看结果是不是符合我们的期望
public class Counter {
public volatile static int count = 0;
public static void inc() {
//这里延迟1毫秒,使得结果明显
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行i++计算,看看实际结果
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
//这里每次运行的值都有可能不同,可能为1000
System.out.println("运行结果:Counter.count="+ Counter.count);
}
}
read and load 从主存复制变量到当前工作内存
use andassign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现
但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6。
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6。
导致两个线程尽管使用了volatile关键字,还是会存在并发的情况。
总之,个人建议,volatile能不用就不用,非高手不能驾驭。
补充
1、对于同步,要时刻清醒在哪个对象上同步,这是关键。
2、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步,也就是说在哪个对象上同步。
3、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6、线程睡眠时,它所持的任何锁都不会释放。
7、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9、synchronized关键字是不能继承的,也就是说,基类的方法synchronizedf(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
锁:
synchronized的不足
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2、线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,很影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
注意:
1、lock不是java语言内置的,sychronized是java语言的关键词,因此是内置特性。lock是一个类,通过这个类可以实现同步访问。
2、lock和sychronized 有一点很大的不同,采用sychronized不需要用户手动释放锁,当sychronized方法或者sychronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
3、通过lock可以知道线程有没有成功获取到锁,这个是sychronized无法做到的。
在java5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,同来控制对竞争资源并发访问的控制,这些内容主要集中在java.util.concurrent.locks包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock。
Lock
Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
锁时实现对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可以允许对共享资源的并发访问,如:ReadWriteLock 的读取锁。
synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的独占访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时相同的此法范围内释放所有的锁。
虽然synchronized 方法和语句的范围机制使得使用监视器锁编程方便了许多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 “hand-over-hand” 或 “chainlocking”:获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。lock接口的实现允许在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个所,从而支持使用这种技术。
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = …;
l.lock();
try {
// access the resource protected bythis lock
} finally {
l.unlock();
}
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。
lock包括trylock(非结构块的获取锁尝试)、lockinterruptibly(可中断锁的尝试)、try(long,timeUnit)获取超时失效锁的尝试
还可以提供:保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。
注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。
Lock接口除了lock()方法,还有两种获取锁的方法:
tryLock()方法表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time,TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
ReadWriteLock
在上例中使用了Lock接口以及对象,使用它,很优雅的控制了竞争资源的安全访问,但是这种锁不区分读写,称这种锁为普通锁。为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,在一定程度上提高了程序的执行效率。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private int counter = 0;
public void write(){
//获取写入锁
lock.writeLock();
System.out.println(Thread.currentThread().getName()+"获取写入锁");
for(int i=0;i<10;i++){
counter = counter + 1;
System.out.println(Thread.currentThread().getName()+"修改数据,counter:"+counter);
try {
Thread.sleep(500);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"写入完毕");
}
public void read(){
//获取读取锁
lock.readLock();
System.out.println(Thread.currentThread().getName()+"获取读出锁");
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次读数据,counter:"+counter);
try {
Thread.sleep(200);
} catch(InterruptedException e) {
// TODOAuto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"读取数据完毕");
}
public static void main(String[] args)throws InterruptedException {
final Test test = new Test(); //不加final关键字,在下面的run()方法内是不能引用该实例的
Runnable readRun = new Runnable(){
public void run(){
test.read();
}
};
Runnable writeRun = new Runnable(){
public void run(){
test.write();
}
};
Thread threadA = new Thread(writeRun,"threadA");
Thread threadB = new Thread(readRun,"threadB");
Thread threadC = new Thread(readRun,"threadC");
//读与写同时开始
threadA.start();
threadB.start();
threadC.start();
}
}
Condition
任何一个java对象,都拥有一组监视器方法(定义在Object类上),主要包括wait、wait(time)、notify、notifyall,这些方法和sychronized关键词配合使用,实现等待通知模式。Condition接口也提供类似的Object的监视器方法,与lock配合可以实现等待通知模式,
Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得Condition 实例,使用其newCondition() 方法。
线程的交互
(1)传统线程通信;(2)使用Condition控制线程通信;(3)使用阻塞队列(BlockingQueue)控制线程通信。
(1)传统线程通信:
方法:借助Object类提供的wait、notify、notifyAll三个方法。这三个方法不属于Thread类,且三个方法必须由同步监视器对象来调用。
情况分析:
1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可在同步方法中直接调用这三个方法
2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用三个方法
传统线程通信三大方法详解:
wait:
作用:导致当前线程等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。(调用wait方法的当前线程会释放对该同步监视器的锁定)。
使用形式:
notify:
作用:唤醒在此同步监视器上等待的单个线程。
注意:如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait方法),才可执行被唤醒的线程
notifyAll:
作用:唤醒在此同步监视器上等待的所有线程。
注意:只有当前线程放弃对该同步监视器的锁定后,才可执行被唤醒的线程。
//计算输出其他线程锁计算的数据
public class ThreadA {
public static void main(String[] args) {
ThreadB b = new ThreadB();
//启动计算线程
b.start();
//线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者
synchronized (b) {
try {
System.out.println("等待对象b完成计算。。。");
//当前线程A释放对象b的锁,放到对象b的等待队列中,直到收到对象b发出notify()或者notifyAll()的信号。
//注意,b.wait()并不是让b等待,而是让当前线程等待b
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b对象计算的总和是:"+ b.total);
}
}
}
//计算1+2+3 ...+100的和
public class ThreadB extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
//(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒
notify();
}
}
}
在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。
//计算1+2+3 ...+100的和
public class Calculator extends Thread {
int total;
public void run() {
synchronized (this) {
for (int i = 0; i < 101; i++) {
total += i;
}
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
notifyAll();
}
}
}
public class TestThread extends Thread{
private Calculator calculator;
public TestThread(Calculator c){
this.calculator = c;
}
public void run(){
synchronized (calculator) {
try {
System.out.println(Thread.currentThread() + "等待计算结果。。。");
calculator.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "计算结果为:"+ calculator.total);
}
}
public static void main(String[] args) {
Calculator c = new Calculator();
TestThread threadA = new TestThread(c);
TestThread threadB = new TestThread(c);
TestThread threadC = new TestThread(c);
threadA.start();
threadB.start();
threadC.start();
c.start();
}
}
必须在同步环境内调用wait()、notify()、notifyAll()方法。只有在线程拥有该对象的锁时,才能调用这三个方法。
对比
初看wait()与notify()方法与 suspend()和resume()方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
2)使用Condition控制线程通信:
定义:
直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能用wait、notify、notifyAll三个方法了,就是显示使用Lock对象来充当同步监视器,而使用Condition对象来暂停、唤醒指定线程的做法。Condition作用:
1.可让那些得到Lock对象却无法继续执行的线程释放Lock对象;
2.可唤醒其他处于等待的线程。
细节注意:
1.Condition将同步监视器方法(wait等等)分解成不同的对象,通过这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set),这种情况下,Lcok替代了同步方法或同步代码块,Condition替代了同步监视器;
2.Condition实例被绑定在一个Lock对象上。
三大方法详解:
1.await
作用:导致当前线程等待,直到其他线程调用该Condition的signal方法或signalAll方法来唤醒。
注意:有许多使用形式。
2.signal
作用:唤醒在此Lock对象等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。
注意:只有当前线程放弃对该同步监视器的锁定后(使用await方法),才可执行被唤醒的线程。
3.sinalAll
作用:换U型在此Lock对象上等待的所有线程。
注意:只有当前线程放弃对该Lock的锁定后,才可执行被唤醒的线程。
(3)使用阻塞队列(BlockingQueue)控制线程通信
定义:
BlockingQueue是queue的子接口,作为线程同步的工具。特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则线程被阻塞。
BlockingQueue提供的方法:
1.put(E e)。尝试把E元素放入BlockingQueue中,如果队列元素已满,则阻塞该线程。 2.take()。尝试从BlockingQueue的头部取出元素,如果队列的元素已空,则阻塞该线程。
也可用Queue接口提供的方法:
1.在队列尾部插入元素:有add(E e),offer(E e)和put(E e),当队列满的时候,三个方法分别会抛出异常、返回false、阻塞队列。
2.在队列头部删除并返回删除的元素:有remove()、poll()和take(),当队列已空时,三个方法分别抛出异常、返回false、阻塞队列。
3.在队列头部取出但不删除元素:有element()、peek(),当队列已空,这两个方法分别抛出异常、返回false。
有了这样的功能,就为多线程的排队等候的模型实现开辟了便捷通道。
除了阻塞队列,还有阻塞栈java.util.concurrent.BlockingDeque接口。不同点在于栈是“后入先出”的结构,每次操作的是栈顶,而队列是“先进先出”的结构,每次操作的是队列头。
四、线程组与线程池:
(1)***线程组(ThreadGroup)***:可对一批线程进行分类管理。
注意:默认情况下,子线程和创建他的福线程处于同一线程组;
一旦某线程加入到指定线程组后,giant线程就一直属于giant线程组了,,直到该线程死亡。
可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。
在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。在 Java 中,除系统线程组外的每个线程组又隶属于另一个线程组,可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。
Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。
(2)线程池:
原因:
启动一个新线程成本高,尤其是当程序需要大量生存期短的线程就更需要线程池。
做法:
程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run或call方法,当run或call方法执行完后,该线程不会死亡,而是再次返回线程池成为空闲状态。而且还可以控制系统中并发线程的数量。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
Java5的线程池分好多种:固定尺寸的线程池、单任务线程池、可变尺寸连接池、延迟线程池等。
在使用线程池之前,必须知道如何去创建一个线程池,在Java5中,需要了解的是java.util.concurrent.Executors类的API,这个类提供大量创建连接池的静态方法,很有用。
线程池相关类:
Executor工厂类的静态工厂方法产生线程池:
固定大小线程池:线程池并不保证线程加入池中的顺序来执行。**Executors.newFixedThreadPool()**创建一个可重用固定线程数的线程池,以共享的无界队列来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务。在某个线程被显式地关闭之前,池中的线程将一直存在。
单任务线程池可保证顺序地执行各个人物,并且在任意给定的时间不会有多个线程时活动的。与其他等效的newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他程序。
可变尺寸的线程池:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用他们。对于执行很多短期异步任务的程序而言,这些线程池通常可以提高性能。调用execute将重用以前构造的线程(如果线程可用的情况下)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并存缓存中移除那些已有60秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意:可以使用threadpoolexecutor构造方法创建具有类似属性但细节不同的线程池。
可调度线程池:可安排线程在给定延迟后运行命令或定期地执行,
其中前三个方法返回一个ExecutorService对象,对象代表一个线程池。中间两个方法返回一个ScheduledExecutorService线程池,属于ExecutorService的子类,可在指定延迟后执行线程任务。最后两个方法:Java8新增,可利用多CPU并行能力。两个方法生产work stealing池,相当于后台线程,如果前台线程都死亡,则work stealing池也自动死亡。
ExecutorService代表尽快执行线程的线程池。
ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。提供了四个方法。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Test{
public static void main(String[] args){
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
//注意,返回的事ScheduledExecutorService接口,而非ExecutorService,ScheduledExecutorService是ExecutorService的子接口
ScheduledExecutorService pool =Executors.newScheduledThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 = new Thread(new MyRunnable("ThreadA"));
Thread t2 = new Thread(new MyRunnable("ThreadB"));
Thread t3 = new Thread(new MyRunnable("ThreadC"));
Thread t4 = new Thread(new MyRunnable("ThreadD"));
//将线程放入池中进行执行
pool.execute(t1);
//使用延迟执行的方法:使线程t2延迟2秒再执行
pool.schedule(t2, 2000,TimeUnit.MILLISECONDS);
//使用周期执行的方法:使线程t3延迟两秒执行,然后每隔5秒执行一次
pool.scheduleAtFixedRate(t3, 2000,5000,TimeUnit.MILLISECONDS);
//使用周期延迟的方法:使线程t4延迟两秒执行,然后,在每一次执行终止和下一次执行开始之间都存在给定的5秒延迟
pool.scheduleWithFixedDelay(t4,2000, 5000, TimeUnit.MILLISECONDS);
//如关闭线程池,则周期性的方法只会执行一次
//pool.shutdown();
}
}
class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("正在执行的线程:"+name);
}
}
注意:
一、shutdown方法–用完一个线程池后,应该调用该线程池的shutdo
wn方法,启动线程池的关闭序列,线程池不再接收新任务,但会先将以前所有已提交的线程池任务执行完成。
二、shutdownNow()方法–关闭线程池,方法停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
使用线程池来执行线程任务的步骤:
一、调用Executors类的静态工厂方法创建一个ExecutorService对象
二、创建Runnable实现类或Callable实现类的实例,作为线程执行任务
三、调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例
四、当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。