创建多线程有四种方式:
一、继承Thread类创建线程类
实现步骤:
1)定义实现了Thread类的子类
2)重写run方法,该run方法的方法体就代表了该线程需要完成的任务
3)创建Thread类的实例,即创建了线程对象
4)调用线程的start方法来启动线程
public class MyThread extend Thread{
private int i;
@Override
public void run() {
for(; i<10; i++) {
System.out.println(getName()+"\t"+i);
}
}
}
public class Test{
public static void main(String[] args){
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+"\t"+i);
if(i==5){
MyThread t1 = new MyThread();
t1.start();
new MyThread().start();
}
}
}
}
二、实现Runnable接口
实现步骤:
1)定义实现了Runnable接口的类
2)重写run方法,run方法同样是该线程的执行体
2)创建该实现类的实例
3)将此实例作为Thread的参数创建一个Thread对象,该Thread对象才是真正的线程对象
4)调用start方法启动该线程
//1、
public class MyThread implements Runnable(
private int i;
@Override
public void run(){
for(; i<20; i++){
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
}
}
public class Test{
public staic void main(String[] args){
for(int i=0; i<20; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==5){
//2、
MyThread t = new MyThread();
//3、
Thread thread1 = new Thread(t, "线程1");
Thread thread2 = new Thread(t, "线程2");
//4、
thread1.start();
thread2.start();
}
}
}
}
结论:采用Runnable接口方式创建多个线程可以共享线程类的实例变量,这是因为在这种方式下,程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个实例变量,在一些情境中,使用实现了Runnable接口的方式较为方便,比如三个窗口共同售卖100张票。
三、使用callable和future创建线程
有返回值、call()方法可以抛出异常。Java5提供了Future接口来代表Callable接口的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,也实现了Runnable接口——可以作为Thread的target
实现步骤:
1)创建实现了Callable接口的类
2)实现call()方法,该call()方法会成为线程执行体,并且call()方法具有返回值
3)创建该实现类的对象
4)使用FutureTask类来包装Callable对象,该FutureTask封装call()方法的返回值
5)调用start方法启动线程
public class MyThread implements Callable<Integer>(
int i=0;
@Override
public Integer call() throws Exception{
for(; i<20; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
public class Test{
public staic void main(String[] args){
MyThread t = new MyThread();
Futuretask<Integer> ft = new futureTask<>(t);
ThreeThread tt2 = new Thread(ft, "新线程");
tt2.start();
try{
System.out.println(ft.get()); //get方法获取Call的返回值
}catch(Exception e){
}
}
}
结论:采用Runnable、Callable的优势在于——线程类只是实现了Runnable或Callable接口,还可以继承其他类;在这种方法下,多个线程可以共享一个target对象,因此非常适合多个相同线程处理同一份资源的情况,从而将CPU、代码和数据分开,形参清晰的模型,体现了面对对象的编程思想。劣势在于编程复杂度略高。
四、使用线程池创建多线程
定义:线程池,其实就是一个可以容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
4.1 为什么要使用线程池:
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
4.2 创建线程池的常用方法:
newFixedThreadPool(int threads):创建一个固定数目的线程池
newCachedThreadPool():创建一个可缓存的线程池,调用execute方法将重用以前创建的线程,如果没有可用线程则创建一个新的线程并添加到池中。终止并移除那些已经存在60s未被使用的线程。
newSingleThreadPoolExcutor():创建一个单线程化的Excutor
newScheduledThreadPool(int corePoolSize):创建一个定时及周期性执行任务的线程池。
4.3 源码分析:
可以看到源码中创建线程池是通过实例化ThreadPoolExecutor类来实现的,阻塞队列类型是LinkedBlockingQueue
核心线程池(corePoolSize):
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
最大线程池(maximumPoolSize):
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
保持存活时间(keepAliveTime):
线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
线程活动保持时间的单位(TimeUnit):
可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
阻塞队列:java中的阻塞队列有四种:
ArrayBlockingQueue 以数组设计的有界队列,必须设置大小,按照先进先出(FIFO)进行排序
LinkedBlockingQueue 以链表实现的有界阻塞队列,容量可以选择设置,如果不设置的话就是无界的,最大长度为Integer.MAX_VALUE,按照先进先出(FIFO)进行排序
SynchronousQueue 不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
PriorityBlockingQueue 具有优先级得无限阻塞队列
对于一个无边界队列来说是可以向其中无限添加任务的,这种情况下可能由于任务数太多而导致内存溢出。
4.4 向线程池提交任务
使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
4.5 线程池的关闭
通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。