赞
踩
1、【java线程及线程池系列】java线程及线程池概念详解
2、【java线程及线程池系列】synchronized、ReentrantLock和ReentrantReadWriteLock介绍及示例
3、【java线程及线程池系列】线程池ThreadPoolExecutor的类结构、使用方式示例、线程池数量配置原则和线程池使用注意事项
本文介绍了线程与线程池的概念介绍以及线程的创建,是该系列的起步篇。
阅读本文需要对java有比较深入的了解。
本文部分图片来源于互联网。
Java 给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
进程的调度以时间片为单位进行,如果两个进程都分得一个同等长度的时间片,其中进程A只有一个线程,另一个进程B有10个线程,那么两个进程执行的时间片相同,但A的线程执行的时间是B的线程的10倍。
一般来说,线程可以分为内核级线程和用户级线程。
在java中,一般只关注用户级线程,所有java的线程,就是在java的JVM主进程下启动的各个线程。
java的各个线程并发执行,其实往往只是一种错觉,对于单核cpu而言,java各线程只是按调度策略执行一个个的时间段,所以在一个cpu中,一个时间点上只有一个线程在执行的,但可能还没执行完,就轮到下个线程执行了。对于多核cpu而言可能存在绝对意义上的线程并发(即两个线程在两个cpu中同时执行)。
在Java中,还常常遇到用户线程和守护(后台)线程的概念。
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
在Java中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
下图显示了一个线程完整的生命周期。
在生成线程对象,并且还没有调用该对象的start方法时,这是线程处于创建状态,在这种状态下,用getState()方法可以获取当前线程的状态,状态值为State.NEW。
当调用了线程对象的start方法之后,并且该线程没有被BLOCK,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。如果此时调用getState()方法,会得到State.RUNABLE;
线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。运行状态仅仅发生在处于就绪状态的线程获得了JVM的调度的情况下,所以处于运行状态的线程没有专门定义RUNNING状态,对于处于运行状态的线程,调用的getState()获得的仍然是State.RUNABLE。
线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait,join等方法都可以导致线程阻塞。
在阻塞状态下,调用getState()可能获得3种线程状态:
无论上面3种哪种阻塞状态,都只能是从运行(RUNNING,而不是RUNABLE)状态而非其它任何状态转换得来。
如果一个线程的run方法执行结束或抛出异常或者调用stop方法并完成对线程的中止之后,该线程就会死亡。此时,再调用getState()方法,得到的是State.TERMINATED。对于已经死亡的线程,无法再使用start方法令其进入就绪。
对于线程的几种状态,下图说明了状态之间的转换关系。
一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。
Java中,每个线程都有一个调用栈,即使你不在Java程序中创建任何新的线程,线程也在后台运行着(如:main线程)。一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。一旦创建一个新的线程,就产生一个新的调用栈。
Java的虚拟机内存分为几个部分,其中线程共享的是Heap和Method area,线程私有的是jvm stack,native method stack和program counter register(程序计数器)。
heap,由各个线程共享,存储的是对象的实例;
jvm stack,指的是虚拟机栈(在HotSpot虚拟机中,本地方法栈是与虚拟机栈放在一起实现的,所以这里不再专门区分),存储的是局部变量表、动态链接、方法出口。
局部变量表是什么呢?它是每个线程私有一份的,内存空间在编译时期就已经确定了,在执行时,不会改变局部变量表的大小。
其中存储的主要是下面的数据,
String有两种声明方式:
String aaa=“abcd”;
String bbb=new String(“abcd”);
第一种声明方式是直接在常量池中找一下有没有现成的"abcd"串的存在,如果有,将aaa的引用指向该串,如果没有,在常量池中新产生一个"abcd",并将aaa的引用指向该串;
第二种声明方式是现在堆中new一个String对象(只要用到new,就一定是先在堆中创建对象"abcd"),然后bbb的应用指向该对象。这个对象与常量池中的"abcd"没有关系,只有在调用bbb.intern()方法时,才能查到常量池中的"abcd"串。
无论上面哪种声明方式,String都具有不可变性,所以,虽然局部变量表中保持的是String的一份引用,但是这份引用是堆中引用的一个副本,可能出现主内存和线程local内存不同步更新的情况,因此类似于基本数据类型。
在上面的描述中,把那些非常量的 基本数据类型、String、普通对象引用 统称为线程中的“变量”。
把上面所说的堆内存中的对象和基本数据类型的备份,称为主内存(main memory),把上面所说的栈内存中用于存储变量的部分内存,称为本地内存(local memory)(或叫工作内存),这就组成了Java内存模型(JMM)。
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成。
如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
Java线程间通信,使用的是共享内存模型,而非消息传递模型,即线程间是通过write-read内存中的公共状态来进行隐式通信的,多个线程之间不能直接传递数据交互,它们之间的交互只能通过共享变量来实现。
Java的内存模型与硬件系统内存模型是相对应的,主内存相当于硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中,而每个线程的本地内存就相当于是寄存器和高速缓存。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。
为了使得处理器内部的运算单元能尽可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
重排序分成三种类型
Java线程的调度是多线程执行的核心,良好的调度策略,可以充分发挥系统的性能,并提高程序的执行效率。Java线程的执行具有一定的控制粒度,即,你编写的线程调度策略只能最大限度的控制和影响线程执行次序,而无法做到精准控制。
1、对于wait/notify/notifyAll的调用,必须在该对象的同步方法或同步代码块中
2、wait方法的调用会释放锁,而sleep或yield不会
3、当wait被唤醒或超时时,并不是直接进入运行态或就绪态,而是先进入Blocked态,抢锁成功,才能进入运行态
4、notify和notifyAll的区别在于:
notify唤醒的是对象多个锁线程中的一个线程,这个线程进入Blocked状态,开始抢锁,当这个线程执行完释放锁的时候,即使现在没有其它线程占用锁,其它处于wait状态的线程也会继续等待notify而不是主动去抢锁
notifyAll,一旦notifyAll消息发出,所有wait在这个对象上的线程都会去抢锁,抢到锁的执行,其它线程Blocked在这个锁上,当抢到锁的线程执行完成释放锁之后,其它线程自动抢锁
线程wait后的唤醒过程必须是:wait-notify-抢锁-执行-释放锁
5、notify和wait必须加循环进行保证,没有循环条件保证的话,如果有多个wait线程在等待notify,当notifyAll发出时,两个wait线程同时被唤醒,进入RUNABLE状态,如果此时他们竞争一个非锁资源,则只有一个能抢到,另一个虽然抢不到,但因为是非锁资源,所以会继续执行,就容易造成问题。
在java中,还有另一对方法suspend()和resume(),他们的作用类似于wait()/notify(),区别在于,suspend()和resume()不会释放锁,所以这两个方法容易造成死锁问题。现在这两个方法已经不再使用了
sleep方法如其名,是让线程休眠的,而且是哪个线程调用,就是哪个线程休眠。可以是调用TimeUtil.sleep(long)、Thread.sleep(long),或当前线程对象t上的t.sleep(long),其结果都是当前线程休眠。
sleep方法有两个:sleep(long timeout), sleep(long timeout,int nanos),两个方法功能相似,后一种方法能够提供纳秒级别的控制。
sleep是Thread类的方法,不是对象的,也无法通过notify来唤醒,当sleep的时间到了,自然会唤醒。
在sleep休眠期间,线程会释放出CPU资源给其它线程,但仍占有锁,而不会释放锁。
sleep()的调用使得其它低优先级、同等优先级、高优先级的线程有了执行的机会。
java线程的优先级并不绝对,它所控制的是执行的机会,也就是说,优先级高的线程执行的概率比较大,而优先级低的线程只是执行的概率相对低一些。Java线程一共有10个优先级,分别为1-10,数值越大,表明优先级越高,一个普通的线程,其优先级为5;
线程的优先级具有继承性,如果一个线程B是在另一个线程A中创建的,则B叫做A的子线程,B的初始优先级与A保持一致。
java中使用t.setPriority(n)来设置优先级,n必须为1-10之间的整数,否则会抛异常。
Java各优先级线程间具有不确定性,由于操作系统的不同,不同优先级的线程会有很大的表现上的不同,所以很难比较或统计。不过,需要注意的是编码过程中,最好不要有代码逻辑是依赖于线程优先级的,不然可能造成问题,因为在Java中,高优先级不一定比低优先级先执行,也不一定比他低优先级线程被调度到的几率大。
对于线程组ThreadGroup的优先级,具有特殊性,简单的说,就是线程组内的线程,优先级不能超过线程组的整体设置。
Java的让步使用的是Thread.yield()静态方法,功能是暂停当前线程的执行,并让步于其它同优先级线程,让其它线程先执行。
yield()仅仅是让出CPU资源,但是让给谁,是由系统决定的,是不确定的,当前线程使用yield()让出资源后,线程不会释放锁,而是回到就绪状态,等待调度执行。
yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会,如果调用yield()后发现没有同等级别的其它线程,则当前线程会立即重新进入运行态。
yield()从某种程度上与sleep()相似,但yield不能指定让步的时间。而且,sleep()让出的机会并不限制其它线程的优先级,而yield仅限于其它同优先级线程。
实际上,yield()方法对应了如下操作;先检测当前是否有相同优先级的线程处于可运行状态,如有,则把CPU的占有权交给次线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。
Java线程见合并,使用的是join方法。join()方法做的事情是将并行执行的线程合并为串行执行的,例如,如果在线程ta中调用tb.join(),则ta会停止当前执行,并让tb先执行,直到tb执行完毕,ta才会继续执行。
join方法有3个重载方法。
如果当前线程ta调用tb.join(),tb开始执行,ta进入WAITING或TIMED_WAITING状态。
如果ta调用tb.join(),则ta会释放当前持有的锁。事实上,join是通过wait/notify来实现的,当ta调用tb.join(),ta就wait在tb对象上,同时释放锁,tb对象抢锁执行,当执行完成后,tb自己发出notify通知。触发ta继续执行,注意,当tb用notify通知ta后,ta还要重新抢锁。
Java7中,出现了一个新的模式:fork()/join()模式,采用的是分而治之的思想来实现并发编程,fork用于将现场拆分成多个小块并行执行,join用于合并结果。不过这个模式容易出问题,要慎用。
如果对一个线程t,调用t. setDaemon(true),则可以将该线程设置为守护线程,JVM判断程序运行结束的标准是所有用户线程执行完成,当用户线程全部结束,即使守护线程仍在运行,或尚未开始,JVM都会结束。
不能将正在运行的线程设置为守护线程,因此t.setDaemon(true)方法必须在t.start()之前调用;如果在一个守护线程中new出来一个新线程,即使不执行setDaemon(true),新的线程也是守护线程;守护线程一般用来做GC、后台监控、内存管理等后台型任务,且这些任务即使随时被结束,也不影响整体程序的运行。
在线程中,中断是一个重要的功能。在Java线程中,中断有且只有一个含义,就是让线程退出阻塞状态,t.interrupt()只是向线程t发出一个中断信号,让该线程退出阻塞状态。
在sleep、wait、join方法中,会不断的检查中断状态的值,如果发现中断状态为true,则立即抛出InterruptedException,并尝试跳出阻塞(用尝试的原因是wait方法阻塞的线程可能需要先抢锁)
示例下面3个方法的实际效果如下
try {
//检查程序是否发生中断
while (!Thread. interrupted()) {
System.out.println( "I am running!");
//point1 before sleep
Thread.sleep( 20);
//point2 after sleep
System.out.println( "Calculating");
}
} catch (InterruptedException e) {
System.out.println( "Exiting by Exception");
}
System.out.println( "ATask.run() interrupted!" );
如果一个线程无法响应中断(比如线程的run中是一个不sleep的死循环),则可能永远无法用interrupt()方法来结束它的运行。这一点在Java线程池(ThreadPoolExecutor)中用到,因为线程池有可能调用shutdown()或shutdownNow()来结束池中线程,这两个方法都是通过interrupt来执行的,如果其中一个线程无法中断,那线程池的这个方法就可能达不到预期效果。
创建Java线程除了使用Thread类之外,还可以有Runable和Callable两种,这两者都是可以实现并发线程的接口
1,Runnable是JDK1.1中就出现的,属于包java.lang,而Callable是在JDK1.5才提供的,属于java.util.concurrent;
2,Runnable中要实现的是void run()方法,没有返回值,而Callable要实现的是V call()方法,返回一个泛型V的返回值(通过Future.get()方法获取);
3,Runnable中抛出的异常,在线程外是无法捕获的,而Callable是可以抛出Exception;
4,Runnable和Callable都可以用于ExecutorService,而Thread类只支持Runnable,当然,可以用FutureTask对Callable进行封装,并用Thread类才能运行;
5,运行Callable可以得到一个Future对象,用于表示异步计算的结果,类似于CallBack,而Runable不行;
运行Runnable和Callable的代码示例如下
class SomeRunnable implements Runnable
{
public void run()
{
//do something here
}
}
Runnable oneRunnable = new SomeRunnable ();
Thread oneThread = new Thread(oneRunnable);
oneThread.start();
class SomeCallable implements Callable <String>
{
public String call() throws Exception {
// do something
return "" ;
}
}
Callable<String> oneCallable = new SomeCallable();
FutureTask<String> oneTask = new FutureTask<String>(oneCallable);
Thread twoThread = new Thread (oneTask);
twoThread.start();
Future是对Runnable或Callable的任务结果进行查询、获取结果、取消等操作的异步管理类(Future对Runnable的管理是通过FutureTask实现的)
Future与Callable一样位于java.util.concurrent包下,是一个泛型接口。
提供如下方法:
//该方法用于取消任务,如果取消成功,返回true,如果取消失败,返回false。mayInterruptIfRunning参数表示的是是否允许取消正在执行且没有完毕的任务,true表示可以取消正在执行的任务;如果任务已经执行完成,无论参数是什么,该方法都返回false;如果任务正在执行:若参数为true,则取消成功的话返回true;若参数为false,则直接返回false;若任务尚未执行,则无论参数是什么,都在取消成功后返回true;
boolean cancel(boolean mayInterruptIfRunning);
//该方法判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true;
boolean isCancelled();
//该方法判断任务是否正常完成,如果是,返回true;
boolean isDone();
// 该方法用于获取执行结果,调用该方法后,会产生阻塞,调用者会一直阻塞直到任务完毕返回结果才继续执行;
V get() throws InterruptedException, ExecutionException;
//该方法与get相似,只不过,有超时限制,如果到了指定时间还没有得到结果,则返回null;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
Future是一个接口,无法用于直接创建对象,而且Runnable也无法直接用Future,所以就有了FutureTask,FutureTask也位于java.util.concurrent包,
FuntureTask的实现如下:
public class FutureTask<V> implements RunnableFuture<V>
就是说,FutureTask实现了RunnableFuture接口,而RunnableFuture接口是怎么回事呢?
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可见,FutureTask实际上同时实现了Future接口和Runnable接口,所以它既可以作为Runnable被Thread线程执行,也可以作为Future得到Callable的返回值;
FutureTask提供了两个构造器:
public FutureTask(Callable<V> callable) {}
public FutureTask(Runnable runnable, V result) {}
用这两个构造器,FutureTask可以对callable和runnable的做出实现,并且由于FutureTask实现了Future接口,所以可以实现对于callable和runnable的管理。
线程池存在的目的在于提前创建好需要的线程,减少临时创建线程带来的资源消耗。而且每个ThreadPoolExecutor线程池还维护者一些统计数据,如完成的任务数,可以方便的进行统计,同时该类还提供了很多可调整的参数和扩展的钩子(hook)。java.util.concurrent中,关于线程池提供了很多接口和类,这些接口和类的关系如下:
execute和submit的区别在于:
execute是定义在Executor中,并在ThreadPoolExecutor中具体实现,没有返回值,其作用就是向线程池提交一个任务并执行;
submit是定义在ExecutorService中,并在AbstractExecutorService中具体实现,且在ThreadPoolExecutor中没有对其进行重写,submit能够返回结果,其内部实现,其实还是在调用execute(),不过,它利用Future&FutureTask来获取任务结果
ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。可以关闭ExecutorService,这将导致其拒绝新任务。
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态。
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
在任务提交到线程池并且执行完毕之前,需要先了解ThreadPoolExecutor中的几个重要成员变量:
private final BlockingQueue<Runnable> workQueue;
//任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock= new ReentrantLock();
//线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers= new HashSet<Worker>();
//用来存放工作集
private volatile long keepAliveTime;
//线程存活时间
private volatile boolean allowCoreThreadTimeOut;
//是否允许为核心线程设置存活时间
private volatile int corePoolSize;
//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize;
//线程池最大能容忍的线程数
private volatile int poolSize;
//线程池中当前的线程数
private volatile RejectedExecutionHandler handler;
//任务拒绝策略
private volatile ThreadFactory threadFactory;
//线程工厂,用来创建线程
private int largestPoolSize;
//用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount;
//用来记录已经执行完毕的任务个数
其中,corePoolSize指的是核心池大小,如果线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列,如果缓存队列也放不下了,则会考虑扩展线程池中的线程数,一直扩展到maximumPoolSize; maximumPoolSize是线程池最大能容忍多少线程数;
上面两个参数遵循下面的规则:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理; 如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
通过 setCorePoolSize()和setMaximumPoolSize()可以动态设置线程池容量的大小。
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线程
在前面多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue,基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue,基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
Executors类为创建ExecutorServ]Cice提供了便捷的工厂方法。
推荐使用Executors 工厂方法:
这些工厂方法为大多数场景预定义了参数设置。如果不用这些配置,需要自己设置比较麻烦;
在Java线程相关的类中,有一些特殊的类并不经常用,但在特定场景下,会非常重要,比如:
CycliBarrier和CountDownLatch
这些类都有其特定的应用场景,在Java编程思想中也有相关介绍,这里就不再赘述
主要的线程分析工具有Jstatck,这个在网上已经有很多教程
简单的创建一个线程有三种方式,即继承Thread、实现Runnable和Callable接口。其中Thread和Runnable可以直接集成或者实现其接口,然后通过start方法启动即可。但Callable接口不但需要实现还需要在线程池中使用,且需要返回值时只能通过线程池的submit方法进行调用,使用Future进行接收。
Thread和Runnable的实现run接口,但是没有返回值,也不能捕获异常处理。Callable实现其call接口,可以返回值。
以下是线程的四种实现方式,其实就是两种的不同变种,即继承Thread和实现Runnable接口,线程的实现还有一种就是实现Callable接口
//第一种
MyThread myThread = new MyThread();//继承了Thread的类
myThread.start();
//第二种
new Thread(){
@Override
public void run() {
System.out.println("MyThread2 running");
}
}.start();
//第三种
Thread thread = new Thread(new MyRunnable());//实现了Runnable的接口
thread.start();
//第四种
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("MyRunnable2 running");
}
};
Thread thread2= new Thread(runnable);
thread2.start();
Runnable是执行工作的独立任务,但是它不返回任何值。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表的是从方法call()中返回的值,并且必须使用ExecutorServices.submit()方法调用它,下面是一个简单示例。
public class CallableTest {
public static void main(String[] args) {
ExecutorService exec=Executors.newCachedThreadPool();
List<Future<String>> results=new ArrayList<Future<String>>();
for(int i=0;i<5;i++) {
results.add(exec.submit(new TaskWithResult(i)));
}
for(Future<String> fs :results) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id=id;
}
@Override
public String call() throws Exception {
return "result of TaskWithResult "+id;
}
}
在创建线程池时需要能明确的知道创建的线程池的corePoolSize、maximumPoolSize以及缓冲队列的长度。一般而言,线程池的数量是cpu的2倍(针对是计算任务)或者5-10倍(针对时IO任务,线程数量cpus/1-0.8或者cpus/1-0.9),缓冲队列数量设置为maximumPoolSize的5倍。
计算型的任务,尽可能的减少在运算过程中进行cpu的切换,所以有的推荐线程数是cpu数量+1;
IO型的任务,由于IO任务在运行过程中总是独占的,所以设置多一些线程处理其他的业务,一般推荐是cpu数量的5-10倍,也有推荐是cpu数量的2倍的。
缓冲队列长度的设置,按照一般的理解应该设置成maximumPoolSize的5倍,因为单个线程在不切换cpu的情况下是单个cpu运算一个线程,corePoolSize设置成和cpu一样的数量,maximumPoolSize设置成cpu的2倍,避免cpu处于空闲状态则直接加入一旦cpu空闲理解执行的线程数量,同时由于cpu的速度较快以及有请求执行的线程加入缓冲队列中减少cpu等待时间,故将缓冲队列设置大一些。但缓冲队列也是较为消耗资源的。
注意:
如果使用LinkedBlockingQueue缓冲队列,且设置了大小了,同样超出了长度会拒绝接收新的任务,和使用ArrayBlockingQueue就没有区别了,但ArrayBlockingQueue会比的读取更快,同样频繁的移除LinkedBlockingQueue效率更高,至于两者如何选择视具体情况而定。即如果是短时的任务,使用LinkedBlockingQueue更适合,如果是长时任务ArrayBlockingQueue更合适,当然如果存在长时任务且进行任务切换的ArrayBlockingQueue也是更合适的。一般情况下,线程任务都是短时且频繁的移除,所以推荐使用LinkedBlockingQueue。
以上关于数字设置需要经过实际的测试而定。
public class CallableTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(1);
return 123;
} catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
};
ExecutorService executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1024),
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
// ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(task);
System.out.println("future done? " + future.isDone());
Integer result = future.get();
System.out.println("future done? " + future.isDone());
System.out.print("result: " + result);
}
}
以上,完成了线程与线程池的概念介绍以及线程的创建,是该系列的起步篇。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。