并发编程结构化
(一)任务执行
大多数并发编程都是围绕“任务执行”来构造的。不同的任务启动不同的线程,并发执行。
在线程中执行任务有以下几个策略:
1、串行执行任务:性能低
2、显示的为任务创建线程:为每一个请求创建一个新的线程,比串行稍微快一些。但是会无限的创建线程,创建线程需要时间,并且大量的线程会消耗资源,尤其是CPU。
3、Executor框架
任务是一组逻辑工作单元,线程是使任务异步执行的机制。
任务执行的主要抽象是Executor
public interface Executor{
void execute(Runnable commmand);
}
Executor基于生产者消费者模式,提交任务相当于生产者,执行任务相当于消费者。
Executor框架的使用简介转载自:http://blog.csdn.net/qq_16811963/article/details/52161713
类似于我们熟悉的集合框架(由Collection和Map接口衍生出很多其他的接口和类),在Java多线程中,也存在一个Executor框架。等以后时间充足了,会对该框架来一波源码剖析。
简而言之,Executor框架实现了工作单元与执行单元的分离。
本文用到的程序源码请参考我的github。
一.Executor框架的两级调度模型
在HotSpot VM的线程模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会启动一个本地操作系统线程:当该JAVA线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
两级调度模型的示意图:
从图中可以看出,该框架用来控制应用程序的上层调度(下层调度由操作系统内核控制,不受应用程序的控制)。
二.Executor框架的结构
Executor主要由三部分组成:任务产生部分,任务处理部分,结果获取部分。(设计模式:生产者与消费者模式)
先来看个图:
1.任务的产生:Runnable接口和Callable接口
这2个对象属于任务对象。工具类Executors可以把一个Runnable对象封装为Callable对象。当我们拥有任务对象之后,就可以将其交给ExecutorService(Executor的一个实现接口)了,这样转入第二部分–任务处理部分。
2.任务的处理:Executor接口—>ExecutorService接口
任务的处理主要是将任务丢到线程池中,由线程池提供线程将任务“消费掉”。
线程池有2类:ThreadPoolExecutor和ScheduledThreadPoolExecutor。2种线程池类均可以通过工厂类Executors来创建。
⑴:ThreadPoolExecutor类
工厂类可以创建3种类型的ThreadPoolExecutor类:
①:FixedThreadPool:拥有固定数量线程的线程池,限制了线程的数目,适用于负载比较重的服务器。
②:SingleThreadPool:单个线程的线程池,适用于需要保证顺序的执行各个任务;任意时间点,不会有多个线程活动。
③:CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
⑵:ScheduleThreadPoolExecutor类
工厂类可以创建2种类型的SchedulePoolExecutor类:
①:ScheduleThreadPoolExecutor:包含若干线程。
②:SingleThreadScheduleExecutor:单个线程。
3.任务结果的获取:Future接口
Future接口有个实现类FutureTask,迄今为止API中返回的都是FutureTask对象,未来的JDK实现中,可能有Future对象。
关于Executor框架的架构基本就这些,下面用几个Demo实测一下:
1.下面这个程序实现了启动一个2个线程的线程池并给该线程池扔入5个任务,结果显示了他们的执行策略:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
结果:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
下面我们再来看看CachedThreadPool的实测结果,由于代码区别只是构造方法的不同,这里不浪费页面贴代码了,直接贴出结果:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
2.结合Future接口来做一个实测
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
结果:
(二)取消和关闭线程或者任务
通过中断(协作机制,使一个线程终止另一个线程的当前工作),来终止线程。
*中断
*停止基于线程的服务
*JVM关闭
1、设置某个“已请求取消”标志,任务定期的查看该标志。可能存在的问题,不可靠的取消操作将把生产者置于阻塞的操作中。
2、中断:实现取消的最合理的方式
调用interrupt不是马上中断,而是给一个状态标记,可以通过获取状态标记决定中断处理。
public class Thread{
public void interrupt(){...}
public boolean isInterrupted(){...}
public static boolean interrupted(){...}//清除当前线程中的中断状态
}
3、中断策略
中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多快的速度响应中断。
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作;尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应,即使“非所有者”代码也可以做出响应。
这就是为什么大多数可阻塞的库函数都只是抛出 InterruptedException作出中断响应。它们永远不会在某个自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果除了将 InterruptedException 传递给调用者外还需要执行其他操作,那么应该在捕获 InterruptedException 之后恢复中断状态:
Thread.currentThread().interrupt();
正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信心封装到某个合适的取消机制中,例如关闭(shutdown)方法。
4、响应中断
有两种实用策略用于处理InterruptedException:
(1)传递异常 throws InterruptedException
(2)回复中断状态,从而是调用栈中的上层代码能够对其进行处理。
捕获异常进行处理,finally中interrupt()恢复异常。
5、通过Futrue来实现取消
前面的例子都是直接使用runnable来执行本身,所以如果要取消任务的话只能使用wait join sleep与Interrupt来组合取消任务。
其实 Future 早已经提供这样的功能 ,ExecutorService.submit 将返回一个 Future 来描述任务。Future 拥有一个cancel 方法,该方法带有一个 boolean 类型的参数 mayinterruptIfRunning,表示取消操作是否成功。(这只是表示任务是否能接受中断,而不是表示任务是否能检测并处理中断。)如果 mayinterruptIfRunning 为 true 并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为 false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。
下面程序给出了另一个版本的 timedRun:将任务提交给一个 ExecutorService ,并通过一个定时的 Future.get 来获取结果。如果 get 在返回时抛出一个 TimeoutException,那么任务将通过它的 Future 来取消。如果任务在被取消前就抛出一个异常,那么该异常将被重新抛出以便由调用者来处理异常。
/**
* 通过Future 来取消任务
*
*/
public class TimedRun {
private static final ExecutorService taskExec = Executors.newCachedThreadPool();
public static void timedRun(Runnable r,
long timeout, TimeUnit unit)
throws InterruptedException {
Future<?> task = taskExec.submit(r);
try {
task.get(timeout, unit);
} catch (TimeoutException e) {
// task will be cancelled below
} catch (ExecutionException e) {
// exception thrown in task; rethrow
throw launderThrowable(e.getCause());
} finally {
// Harmless if task already completed
task.cancel(true); // interrupt if running
}
}
}
6、处理不可中断的阻塞
在java库中,许多可阻塞的方法都是通过提前返回或者抛出 InterruptedException 来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能响应中断:
•造成线程阻塞的原因:
1. java.io包中的同步Socket I/O。如套接字中进行读写操作read, write方法。
2. java.io包中的同步I/O。如当中断或关闭正在InterruptibleChannel上等待的线程时,会对应抛出ClosedByInterruptException或 AsynchronousCloseException。
3. Selector的异步I/O。如果一个线程在调用Selector.select时阻塞了,那么调用close, wakeup会使线程抛出ClosedSelectorException。
4. 获取某个锁。当一个线程等待某个锁而阻塞时,不会响应中断。但Lock类的lockInterruptibly允许在等待锁时响应中断。
/**
* 7.11 通过改写 interrupt 方法将非标准的取消操作封装在 Thread 中
* @ClassName: ReaderThread
* @author xingle
* @date 2014-10-24 上午9:05:56
*/
public class ReaderThread extends Thread{
private static final int BUFSZ = 512;
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException{
this.socket = socket;
this.in = socket.getInputStream();
}
public void interrupt(){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally{
super.interrupt();
}
}
public void run(){
byte[] buf = new byte[BUFSZ];
while(true){
try {
int count = in.read(buf);
if (count < 0){
break;
}else if(count >0 ){
processBuffer(buf, count);
}
} catch (IOException e) {
//允许线程退出
}
}
}
private void processBuffer(byte[] buf, int count) {
// TODO Auto-generated method stub
}
}
我们可以通过 newTaskFor 方法来进一步优化 ReaderThead 中封装非标准取消的技术,这是 Java 6 在 ThreadPoolExecutor 中的新增功能。当把一个 Callable 提交给 ExecutorService 时,submit 方法会返回一个 Future ,我们可以通过这个 Future 来取消任务。newTaskFor 是一个工厂方法,它将创建 Future 来代表任务。
通过定制表示任务的 Future 可以改变Future.cancel 的行为。例如,定制的取消代码可以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。通过改写 interrupt 方法,ReaderThead 可以取消基于套接字的线程。同样,通过改写任务的 Future.cancel 方法也可以实现类似的功能。
在下面的程序中,定义了一个CancellableTask 接口,该接口扩展了 Callable,并增加了一个 cancel 方法和一个 newTask 工厂方法来构造RunnableFuture 。CancellingExecutor 扩展了 ThreadPoolExecutor ,并通过改写 newTaskFor 使得 CancellableTask 可以创建自己的 Future.
/**
* 7.12 通过 newTaskFor 将非标准的取消操作封装在一个任务中
*
* @ClassName: SocketUsingTask
* @author xingle
* @date 2014-10-24 下午2:27:07
*/
public class SocketUsingTask<T> implements CancellableTask<T> {
@GuardedBy("this")
private Socket socket;
protected synchronized void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public T call() throws Exception {
//do working
return null;
}
@Override
public void cancel() {
try {
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this) {
public boolean cancel(boolean mayInterruptIfRunning) {
try {
SocketUsingTask.this.cancel();
} finally {
return super.cancel(mayInterruptIfRunning);
}
}
};
}
/**
* 通过newTaskFor将非标准的取消操作封装在任务中
*/
public class CancellingExecutor extends ThreadPoolExecutor {
/**
* @param corePoolSize
* @param maximumPoolSize
* @param keepAliveTime
* @param unit
* @param workQueue
*/
public CancellingExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
// TODO Auto-generated constructor stub
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
if (callable instanceof CancellableTask) { // 若是我们定制的可取消任务
return ((CancellableTask<T>) callable).newTask();
}
return super.newTaskFor(callable);
}
}
}
/**
* 可取消的任务接口
*/
interface CancellableTask<T> extends Callable<T> {
void cancel();
RunnableFuture<T> newTask();
}
SocketUsingTask 实现了 CancellableTask,并定义了Future.cancel 来关闭套接字和调用 super.cancel。如果 SocketUsingTask 使用自己的 Future 来取消,那么底层的套接字将被关闭并且线程将被中断。
(三)线程池的使用
1、线程饥饿死锁
只要线程池中的任务需要无限期的等待一些必须由池中其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或者执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。
2、运行较长时间的任务
限定任务等待资源的时间,等待超时,任务标识失败,终止或者重新放入队列。
3、设置线程池的大小
要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
任务的优先级:高、中、低。
任务的执行时间:长、中、短。
任务的依赖性:是否依赖其他系统资源,如数据库连接等。
性质不同的任务可以交给不同规模的线程池执行。
对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。
在这篇如何合理地估算线程池大小?文章中发现了一个估算合理值的公式
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。
并发编程网上的一个问题
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
4、ThreadPoolExecutor的通用构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
(1)基本大小:线程池的目标大小
(2)最大大小:可同时活动的线程数量的上限
(3)存活时间:如果某个线程的空闲时间超过了存活时间,将标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个超时线程将被终止。
可以直接通过显示的ThreadPoolExecutor构造函数来构造,也可以使用newCachedThreadPool、newFixedThreadPool、newScheduledThreadExecutor等工厂方法返回。
(4)阻塞队列:BlockingQueue
保存等待执行的任务。
任务的排队方法:
无界队列:任务队列可以无限制的增加,资源耗尽
有界队列:队列填满后,新的任务来使用饱和策略,队列大小和线程池大小必须同时调节
同步移交:对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间移交的机制。在newCachedThreadPool中有使用
有界队列和无界队列的使用:
当任务相互独立是才考虑用有界队列。
如果任务之间存在依赖关系,有界队列可能会造成饥饿死锁的问题,应该使用无界队列线程池newCachedThreadPool。
(5)饱和策略:RejectedExecutionHandler
JDK提供了几种不同的实现:
•AbortPolicy:默认的饱和策略,就是中止任务,该策略将抛出RejectedExecutionException。调用者可以捕获这个异常然后去编写代码处理异常。
•CallerRunsPolicy:“调用者运行”策略,实现了一种调节机制 。它不会抛弃任务,也不会抛出异常。 而是将任务回退到调用者。它不会在线程池中执行任务,而是在一个调用了Executor的线程中执行该任务。
•discardPolicy:当新提交的任务无法保存到队列中等待执行时,DiscardPolicy会稍稍的抛弃该任务
•DiscardOldestPolicy:会抛弃最旧的(下一个将被执行的任务),然后尝试重新提交新的任务。如果工作队列是那个优先级队列时,搭配DiscardOldestPolicy饱和策略会导致优先级最高的那个任务被抛弃,所以两者不要组合使用。
调用者策略实例:
/**
* 调用者运行的饱和策略
* @ClassName: ThreadDeadlock2
* TODO
* @author xingle
* @date 2014-11-20 下午4:18:11
*/
public class ThreadDeadlock2 {
ExecutorService exec = new ThreadPoolExecutor(0, 2, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),new ThreadPoolExecutor.CallerRunsPolicy());
private void putrunnable() {
for (int i = 0; i < 4; i++) {
exec.submit(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
}
}
public static void main(String[] args) {
new ThreadDeadlock2().putrunnable();
}
}
信号量(没有预定义的饱和策略):
当工作队列被填满之后,没有预定义的饱和策略来阻塞execute。通过使用 Semaphore (信号量)来限制任务的到达率,就可以实现这个功能。在下面的BoundedExecutor 中给出了这种方法,该方法使用了一个无界队列,并设置信号量的上界设置为线程池的大小加上可排队任务的数量,这是因为信号量需要控制正在执行的和正在等待执行的任务数量。
/**
* 8.4 使用Semaphore来控制任务的提交速率
* @ClassName: BoundedExecutor
* TODO
* @author xingle
* @date 2014-11-20 下午2:46:19
*/
public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore;
int bound;
public BoundedExecutor(Executor exec,int bound){
this.exec = exec;
this.semaphore = new Semaphore(bound);
this.bound = bound;
}
public void submitTask(final Runnable command) throws InterruptedException{
//通过 acquire() 获取一个许可
semaphore.acquire();
System.out.println("----------当前可用的信号量个数:"+semaphore.availablePermits());
try {
exec.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程" + Thread.currentThread().getName() +"进入,当前已有" + (bound-semaphore.availablePermits()) + "个并发");
command.run();
} finally {
//release() 释放一个许可
semaphore.release();
System.out.println("线程" + Thread.currentThread().getName() +
"已离开,当前已有" + (bound-semaphore.availablePermits()) + "个并发");
}
}
});
} catch (RejectedExecutionException e) {
semaphore.release();
}
}
}
测试程序:
public class BoundedExecutor_main {
public static void main(String[] args) throws InterruptedException{
ExecutorService exec = Executors.newCachedThreadPool();
BoundedExecutor e = new BoundedExecutor(exec, 3);
for(int i = 0;i<5;i++){
final int c = i;
e.submitTask(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" +c);
}
});
}
}
}
执行结果:
(6)线程工厂:ThreadFactory
当线程池需要创建线程时,都是通过线程工厂方法完成的。
public interface ThreadFactory{
Thread newThread(Runnable r);
}
自定义的线程工厂可以实现ThreadFactory接口,返回一个Thread线程。
5、扩展ThreadPoolExecutor
public class TimmingThreqadPool extends ThreadPoolExecutor{
//添加几个新的方法
}