Java多线程中如果每个请求到达就创建一个新线程,由于线程的创建与销毁都需要很大的开销,甚至比处理这些请求的时间和资源要多的多。因此,如果在一个JVM里要实现很多请求,每个请求对应一个线程的话,那么会增加系统的消耗和负担。为了避免这些缺点,服务器应用程序需要限制给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。这也是线程池技术存在的原因。线程池主要用来解决线程生命周期开销和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟,使得请求服务的响应更快,另外可以通过调整线程中的线程数目可以防止出现资源不足的情况。首先看一下如下代码,首先是Student类,后面介绍线程池时,都会使用该类,并且无须更改:
package person;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
import java.util.concurrent.locks.Condition;;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();//创建condition对象
public void saveMoney(Student s) {
// TODO Auto-generated method stub
try
{
lock.lock();
while(total>=3000)
{
try
{
condition.await();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
condition.signal();
}
finally
{
lock.unlock();
}
}
public void getMoney(Student s)
{
try
{
lock.lock();
while(s.total<=0)
{
try
{
condition.await();//使当前线程暂停,让出占用当前同步对象的锁,并进入等待状态
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
condition.signal();//唤醒那些暂停的线程,使其进入等待列表,类似于synchronized下的notify()方法
}
finally
{
lock.unlock();
}
}
}
下面这个程序是不使用线程池执行任务的测试程序:
package waytobuildthread;
import person.Student;
import java.util.concurrent.*;
public class RunnableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
Thread[] save=new Thread[10];
Thread[] get=new Thread[10];
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
s.getMoney(s);;
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
get[i]=t1;
}
for(int i=0;i<10;i++)
{
Thread t2= new Thread(){
public void run(){
s.saveMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
save[i]=t2;
}
//等待线程结束
for(Thread t:get)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
//等待线程结束
for(Thread t:save)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("最后Tom总共拥有"+s.total+"元");
}
}
上述代码在之前文章中已经举过例子,上述代码中每当创建一个任务,就会调用该任务的start()方法,即启动线程,所以是一个任务对应一个线程,那么当任务很多的时候,对系统会造成很大的负担。因此如果指定几个线程进行工作,任务进行排队机制。就如同去银行办业务一样,如果每来一个人来办业务,就开辟一个新的窗口的话,那么无疑大大地耗钱耗力,而按照事先开设好的窗口,人来了,到窗口排队处理业务即可,那么这样就省去了重新创建窗口的开销。其实线程池就是这个道理,下面通过自定义的线程池以及Java自带的线程池来体会线程池的优势。首先运行如上测试程序,结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom取了200当前总共拥有1000元
Tom存了400当前总共拥有1400元
Tom取了200当前总共拥有1200元
Tom存了400当前总共拥有1600元
Tom存了400当前总共拥有2000元
Tom取了200当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
最后Tom总共拥有3000元
耗时:1003ms
可以看出完成上述存钱取钱总共20个任务,如果不采用线程池需要1003ms。
前文已经测试了不使用线程池的方法执行任务队列,接下来首先讲解自定义线程池。根据线程池的特点,首先创建一个线程池类:
package threadpool;
import java.util.LinkedList;
public class ThreadPool {
// 线程池大小
int threadPoolSize;
// 任务容器
LinkedList tasks = new LinkedList();
// 试图消费任务的线程
public ThreadPool() {
threadPoolSize = 10;
// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskThread("线程 " + i).start();
}
}
}
public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}
class TaskThread extends Thread {
public TaskThread(String name) {
super(name);
}
Runnable task;
public void run() {
//System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();
}
task.run();//运行run(),因为当调用start(),表示只是处于待运行状态。所以需要run()使任务运行。
}
}
}
}
该线程池在初始化时先创建10个线程,然后有任务时,将任务添加到任务队列中,然后如果有线程空了,便会从任务队列中获取任务执行。以下是测试程序,如下所示:
package threadpool;
import person.Student;
public class TestThread {
public static void main(String[] args)throws InterruptedException {
ThreadPool pool = new ThreadPool();
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
long start=System.currentTimeMillis();
for(int i=0;i<10;i++)
{
Runnable t1= new Runnable(){
public void run(){
s.getMoney(s);;
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
pool.add(t1);//仅是将该任务添加到线程池中,而不调用start()方法,就是为了不启动该线程,而是将该任务交由已有的线程执行。
}
for(int i=0;i<10;i++)
{
Runnable t2= new Runnable(){
public void run(){
s.saveMoney(s);;
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
pool.add(t2);
}
long end=System.currentTimeMillis();
Thread.sleep(5000);
System.out.println("Tom最后总共拥有"+s.total+"元");
System.out.println("耗时:"+(end-start-5000));
}
}
运行测试程序,结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom取了200当前总共拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom存了400当前总共拥有600元
Tom存了400当前总共拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom最后总共拥有3000元
耗时:2ms
可以看出,完成所有任务只需要2ms,前面已经测试未使用线程池实现相同功能的时间是1003ms,可以得出两者相差很明显,如果任务量更大的话,则会使差距更加大,这也体现出了线程池的优势。
Java语言自带功能十分完善的线程池,而无需自己去开发,可以节省时间与精力,线程池类ThreadPoolExecutor在包java.util.concurrent下,以下便是利用Java自带线程池方式完成本文示例:
package threadpool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import person.Student;
public class ThreadTest2 {
static int k=0;
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue());
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
long start=System.currentTimeMillis();
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
s.getMoney(s);;
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
threadPool.execute(t1);
}
for(int i=0;i<10;i++)
{
Thread t2= new Thread(){
public void run(){
s.saveMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
threadPool.execute(t2);
}
Thread.sleep(5000);
long end=System.currentTimeMillis();
System.out.println("Tom最后总共拥有"+s.total+"元");
System.out.println("耗时:"+(end-start-5000)+"ms");
}
}
运行以上程序,结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom存了400当前总共拥有1600元
Tom取了200当前总共拥有1400元
Tom取了200当前总共拥有1200元
Tom取了200当前总共拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom最后总共拥有3000元
耗时:2ms
从上述结果可以看出,Java自带的线程池与自定义的线程池都能较好的减少时间以及资源的占用,一定程度上多线程因创建线程的开销大而造成的时间过多与资源不足的问题。
总之,线程池就如同一个银行,而初始化启动的线程,就如同事先设立好的窗口,一旦有事务过来就会利用已有线程进行处理,如果有很多任务,则没法执行的任务需要在进行排队。因此使用这样的技术可以很大程序提升系统性能。如果一个任务启动一个线程,那么无疑给系统造成很大的压力。以上便是多线程的线程池技术及其优势。