在现在的项目中遇到的一个问题。
我所做的短信平台,原来只是单线程发送短信的,但是由于公司的应用范围的扩大,短信的发送量成倍的增多,一批插入的短信量达到5W数据,如果按照以前的方式,发送过程十分缓慢。因为我们所用的第三方短信提供商只提供给我们10个并发的限制,所以我们采用10条线程进行读取。一次发10条,等10条发送完成以后再发送另外10条。
以下是程序:
private void sendSmsLoop() throws Exception {
for (int i=0; i< threads.length; i++){
threads[i] = null;
}
String sql =""
+"select top 10 "
+"smsSend.id "
+",smsSend.phoneNumber "
+",smsSend.content "
+",smsSend.subcode "
+",smsSend.clientID "
+",smsSend.SID "
+"from "
+"smsSend "
+"where "
+"(smsSend.handleFlag = 0 "
+"and datediff(minute, smsSend.registerDate, getDate()) < 30 "
+"and left(smsSend.phoneNumber,3) in ('130','131','132','155','156','186','134','135','136','137','138','139','150','151','152','154','157','158','159','187','188')) or (ClientID='1' and sendDate is NULL)";
//System.out.println(sql);
Stmt = DBConn.createStatement();
Rs = Stmt.executeQuery(sql);
int inc = 0;
while(Rs.next())
{
String ID = Rs.getString("id");
String phoneNumber = Rs.getString("phoneNumber");//手机号
String content = StrUtil.replaceIllegalString(Rs.getString("content")); //信息
String subcode = Rs.getString("subcode");
String clientID = Rs.getString("clientID");
String sid = Rs.getString("SID");
//在这里开始开启线程,因为我所选的是TOP10,所以一次可以启动10条线程
SmsThread st = new SmsThread(ID,phoneNumber,content,subcode,clientID,sid);
threads[inc] = st;
threads[inc].start();
inc++;
}
//循环判断10个线程的FLAG信息,判断FLAG必须都为TRUE了。才可以进行下一轮扫描。【判断为TRUE的结果就是只要该线程执行完毕后FLAG就赋值为TRUE】
while(true)
{
boolean _flag = false;
for (int i=0; i< threads.length; i++){
if (threads[i] != null){
SmsThread st = (SmsThread) threads[i];
if(!st.flag)
{
_flag = true;
break;
}
}
}
if (_flag){
Thread.sleep(1000);
}else{
break;
}
}
}
class SmsThread extends Thread
{
public boolean flag = false;
public String ID;
public String phoneNumber;
public String content;
public String subcode;
public String clientID;
public String sid;
public SmsThread()
{
}
public SmsThread(String ID,String phoneNumber,String content,String subcode,String clientID,String sid)
{
this.ID = ID;
this.phoneNumber =phoneNumber;
this.content=content;
this.subcode = subcode;
this.clientID = clientID;
this.sid = sid;
}
public void run()
{
try {
sendSmsByStrategy(ID, phoneNumber, content, subcode, clientID,
sid);
} catch (Exception e) {
e.printStackTrace();
}
//每个线程执行完成以后都会将自己的FLAG设置为TRUE,所以我们在循环取数据的时候要保证所有的线程方法都执行了,才开始下一次的数据读取。
flag = true;
}
}
private void sendSmsByStrategy(String id, String phoneNumber, String content,String SubCode,String clientID,String sid) throws Exception
{
SmsSendBatch send = new SmsSendBatch(DBConn);
send.sendSms(id, phoneNumber, content, SubCode);
}
----------------------------------------------------
对以上方法的改良版本V1.0。上面程序存在的一个性能问题就是,每次发完10条信息都会要重新去数据库取出一次数据,这样对性能也会造成一定的影响,现在对齐进行改良,因为我们根据每次并发8条线程对于发送短信是最快的【主要是短信提供商那边提供给我们虽然有10 个并发,但是我们自己测试8个并发是最好的,10个并发会有连接异常发生】。我改变读取策略,一次从数据库中读取80条信息,放在集合中,然后用8个线程分别每天处理10条信息进行发送,等发送完毕这80条信息,然后再去取另外的80条信息。为什么不一次性取多点数据呢,因为我们还有其他的数据任务在操作短信表,如果数据量一次取太大,会对数据库造成死锁,所以取的够用就可以了。
具体代码实现:
private void sendSmsLoop() throws Exception {
threads.clear();
smslist.clear();
String sql =""
+"select top 80 "
+"smsSend.id "
+",smsSend.phoneNumber "
+",smsSend.content "
+",smsSend.subcode "
+",smsSend.clientID "
+",smsSend.SID "
+"from "
+"smsSend_temp_tuping as smsSend "
+"where "
+"(smsSend.handleFlag = 0 "
//+"and datediff(minute, smsSend.registerDate, getDate()) < 30 "
//+"and left(smsSend.phoneNumber,3) in ('130','131','132','155','156','186','134','135','136','137','138','139','150','151','152','154','157','158','159','187','188')) or (ClientID='1' and sendDate is NULL) order by Priority desc,registerdate asc";
+"and left(smsSend.phoneNumber,3) in ('130','131','132','155','156','186','134','135','136','137','138','139','150','151','152','154','157','158','159','187','188')) or (ClientID='1' and sendDate is NULL) order by registerdate asc";
// System.out.println(sql);
Stmt = DBConn.createStatement();
Rs = Stmt.executeQuery(sql);
int inc = 0;
while(Rs.next())
{
String ID = Rs.getString("id");
String phoneNumber = Rs.getString("phoneNumber");//手机号
String content = StrUtil.replaceIllegalString(Rs.getString("content")); //信息
String subcode = Rs.getString("subcode");
String clientID = Rs.getString("clientID");
String sid = Rs.getString("SID");
SmsBean sb = new SmsBean();
sb.setID(ID);
sb.setPhoneNumber(phoneNumber);
sb.setContent(content);
sb.setSubcode(subcode);
sb.setClientID(clientID);
sb.setSid(sid);
smslist.add(sb);
inc++;
}
for(int i=0;i<smslist.size();i++)
{
SmsBean sb = (SmsBean)smslist.get(i);
SmsThread ut = new SmsSendClientBatchNew().new SmsThread(sb.getID(),sb.getPhoneNumber(),sb.getContent(),sb.getSubcode(),sb.getClientID(),sb.getSid());
threads.add(ut);
ut.start();
//判断每开启8个线程就开始等待,等待到该8个线程执行完毕,然后将该8个线程清空,然后再开启后面的8个。
if(inc>=10)
{
System.out.println("信息发送量估计会大于10条信息,接着发!");
}
else
{
System.out.println("信息发送量小于10条信息,休息5秒!");
Thread.sleep(5000);
}
}
该方法比第一种方法的效率要高点,同时也减少了对数据库的操作。
------------------------------------------------------
对以上方法的改良,上一种方式也存在缺点,比如如果有24条数据,第一条线程处理8条,第2条处理8条,第三条处理6条,如果第一条线程提比后面两条处理的都快很多,那么先处理完成的线程将被浪费,我们可以采用线程池的概念。
与数据库连接池类似的是,线程池会启动大量的空闲线程,程序将一个RUNNABLE对象的线程给线程池,线程池就会启动一挑线程来执行该对象的RUN方法,当RUN方法执行结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个RUNABLE对象的RUN方法。
JDK1.5提供了一个EXECUTORS工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池:
1、newCachedThreadPool(): 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
2.newFixedThreadPool(int n):创建一个可重用的具有固定线程数的线程池。
3.newSingleThreadExecutor():创建一个只有单线程的线程池,他相当于newFixedThreadPool方法时传入参数为1
4.newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,他可以再制定延迟后执行线程任务,corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
5.newSingleThreadScheduledExecutor()创建一个只有一条线程的线程池,它可以再制定延迟后执行线程任务。
以上5个方法前三个返回一个 ExecutorService对象,该对象代表一个线程池,他可以执行RUNNABLE对象或CALLABLE对象所代表的线程。
EXECUTORSERVICE代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个RUNNABLE或CALLABLE对象提交给线程池即可,该线程池就会尽快执行该任务。他提供了三个方法:
1. Future<?> submit(Runnable task):将一个RUNNABLE对象提交给指定的线程池,线程池将在有空闲线程时执行对象所代表的任务,其中FUTURE对象代表RUNNABLE人物的返回值,但RUN方法没有返回值,所以FUTURE对象将在RUN方法执行结束后返回NULL,但可以调用FUTURE的isDone(),isCancelled()方法获得 runnable对象的执行状态。
2.Future<T> submit(Runnable task,T result):将一个RUNNABLE对象提交给指定的线程池,线程池将在空闲线程时候执行对象的任务,RESULT显示指定线程执行结束后的返回值,所以FUTURE对象将在RUN方法执行结束后返回RESULT。
3.Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池,线程池将在空闲线程时候执行对象的任务,FUTURE代表将在RUN方法执行结束后返回值。
代码:
首先开启具有8个固定线程的线程池。
public ExecutorService pool = Executors.newFixedThreadPool(8);
private void sendSmsLoop() throws Exception {
threads.clear();
smslist.clear();
String sql =""
+"select top 80 "
+"smsSend.id "
+",smsSend.phoneNumber "
+",smsSend.content "
+",smsSend.subcode "
+",smsSend.clientID "
+",smsSend.SID "
+"from "
+"smsSend_temp_tuping as smsSend "
+"where "
+"(smsSend.handleFlag = 0 "
//+"and datediff(minute, smsSend.registerDate, getDate()) < 30 "
+"and left(smsSend.phoneNumber,3) in ('130','131','132','155','156','186','134','135','136','137','138','139','150','151','152','154','157','158','159','187','188')) or (ClientID='1' and sendDate is NULL) order by registerdate asc";
// System.out.println(sql);
Stmt = DBConn.createStatement();
Rs = Stmt.executeQuery(sql);
int inc = 0;
while(Rs.next())
{
String ID = Rs.getString("id");
String phoneNumber = Rs.getString("phoneNumber");//手机号
String content = StrUtil.replaceIllegalString(Rs.getString("content")); //信息
String subcode = Rs.getString("subcode");
String clientID = Rs.getString("clientID");
String sid = Rs.getString("SID");
SmsBean sb = new SmsBean();
sb.setID(ID);
sb.setPhoneNumber(phoneNumber);
sb.setContent(content);
sb.setSubcode(subcode);
sb.setClientID(clientID);
sb.setSid(sid);
smslist.add(sb);
inc++;
}
for(int i=0;i<smslist.size();i++)
{
SmsBean sb = (SmsBean)smslist.get(i);
SmsThread ut = new SmsSendClientBatchPool().new SmsThread(sb.getID(),sb.getPhoneNumber(),sb.getContent(),sb.getSubcode(),sb.getClientID(),sb.getSid());
//开启一个线程,并将该线程放入到线程池中,然后将该线程的返回值放入到一个List<Future> 的结合中,用来保存所开启线程的返回结果。
if(inc>=10)
{
System.out.println("信息发送量估计会大于10条信息,接着发!");
}
else
{
System.out.println("信息发送量小于10条信息,休息5秒!");
Thread.sleep(5000);
}
}
用了线程池以后,如果一次取80条数据,原来每次要用8个线程来发,但是线程池每次也许只用5-6个就可以用来发送8条信息了。因为有可能第一个用完的线程在发送第7条信息的时候又被拿出来用。