在我们开发的过程中常常会碰到多线程的问题,对于多线程的实现方式主要有两种:实现Runnable接口、继承Thread类。对于这两种多线程的实现方式也是有着一些差异。既然实现了多线程那必然离不开管理这些线程,当问题比简单时一个或者几个线程就OK了,也涉及不到效率问题。一旦线程数量多起来的时候,必然躲不过这些线程的创建与销毁,而往往这是很浪费时间的。这时就需要利用线程池来进行管理,既免去了我们创建线程和销毁线程的代码,也提高了程序的效率。下面针对以上问题做出相关讲解。
一、Runnable、Thread比较
首先阐述实现Runnable的好处:
- java不允许多继承,因此实现了Runnable接口的类可以再继承其他类。
- 方便资源共享,即可以共享一个对象实例???(从很多博客中看到这样描述,但是此处有疑问,例子如下)
下面来通过具体代码来解释上述优点,网上很流行的买票系统,假设有10张票,首先通Thread来进行购买。代码如下:
public class TicketThread extends Thread{
private int ticket = 10;
public void run(){
for(int i =0;i<10;i++){
synchronized (this){
if(this.ticket>0){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] arg){
TicketThread t1 = new TicketThread();
new Thread(t1,"线程1").start();
new Thread(t1,"线程2").start();
//此处网上有各种写法,很多写法都是自圆其说,举一些特殊例子来印证自己的观点,然而事实却不尽如此。
}
}
输出:
线程1卖票—->10
线程1卖票—->9
线程1卖票—->8
线程2卖票—->7
线程2卖票—->6
线程1卖票—->5
线程1卖票—->4
线程2卖票—->3
线程2卖票—->2
线程1卖票—->1
实现Runnable接口:
package threadTest;
public class TicketRunnable implements Runnable{
private int ticket = 10;
@Override
public void run() {
for(int i =0;i<10;i++){
//添加同步快
synchronized (this){
if(this.ticket>0){
try {
//通过睡眠线程来模拟出最后一张票的抢票场景
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] arg){
TicketRunnable t1 = new TicketRunnable();
new Thread(t1, "线程1").start();
new Thread(t1, "线程2").start();
}
}
输出:
线程1卖票—->10
线程1卖票—->9
线程1卖票—->8
线程1卖票—->7
线程2卖票—->6
线程2卖票—->5
线程2卖票—->4
线程2卖票—->3
线程2卖票—->2
线程2卖票—->1
从这两个例子可以看出,Thread也可以资源共享啊,为什么呢,因为Thread本来就是实现了Runnable,包含Runnable的功能是很正常的啊!!至于两者的真正区别最主要的就是一个是继承,一个是实现;其他还有一些面向对象的思想,Runnable就相当于一个作业,而Thread才是真正的处理线程,我们需要的只是定义这个作业,然后将作业交给线程去处理,这样就达到了松耦合,也符合面向对象里面组合的使用,另外也节省了函数开销,继承Thread的同时,不仅拥有了作业的方法run(),还继承了其他所有的方法。综合来看,用Runnable比Thread好的多。
针对本例再补充一点,在以上程序中,如果去掉同步代码块,则会出现其中一人购买第0张票的情况,所以我们在做多线程并行的时候一定要时刻考虑到边界值的问题,在关键代码处必须要做好同步处理。
二、线程池
创建线程池主要有三个静态方法供我们使用,由Executors来进行创建相应的线程池:
public static ExecutorSevice newSingleThreadExecutor()
public static ExecutorSevice newFixedThreadPool(int nThreads)
public static ExecutorSevice newCachedThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
- newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。
- newFixedThreadPool返回一个包含指定数目线程的线程池,如果任务数量多于线程数量,那么没有执行的任务必须等待,直到有任务完成为止。
- newCachedThreadPool根据用户的任务数创建相应的线程来处理,该线程池不会对线程数目加以限制,完全依赖于JVM能创建线程的数量,可能引起内存不足。
- newScheduledThreadPool创建一个至少有n个线程空间大小的线程池。此线程池支持定时以及周期性执行任务的需求。
我们只需要把实现了Runnable的类的对象实例放入线程池,那么线程池就自动维护线程的启动、运行、销毁。我们不需要自行调用start()方法来开启这个线程。线程放入线程池之后会处于等待状态直到有足够空间时会唤醒这个线程。
private ExecutorService threadPool = Executors.newFixedThreadPool(5);
threadPool.execute(socketThread);
//至少维护5个线程容量的空间
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
//函数意义:一个线程开始之后和下一个线程开始的时间间隔
//第一个时间参数表示初始化执行延迟1000毫秒,第二个时间参数表示每隔1000毫秒执行一次
//第二个线程必须等到第一个线程执行完成才能继续执行,尽管时间间隔小于线程执行时间
threadPool.scheduleAtFixedRate(socketThread, 1000, 1000, TimeUnit.MILLISECONDS);
//基本参数和上面的类似,函数意义不一样:一个线程结束之后和下一个线程开始的时间间隔
threadPool.scheduleWithFixedDelay(socketThread, 1000, 1000, TimeUnit.MILLISECONDS);
//线程池不接收新加的线程,但是执行完线程池内部的所有线程
threadPool.shutdown();
//立即关闭线程池,停止线程池内还未执行的线程并且返回一个未执行的线程池列表
threadPool.shutdownNow();