未处理异常
以我们通常的经验,如果线程执行过程中抛出了未处理异常(没有用try-catch),那么我们的APP就会崩溃,并且我们可以从Error Log中看到出错的异常堆栈信息。那么我们有没有方法,在异常抛出之前对该异常进行处理呢?
Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,该接口只有一个方法:void uncaughtException(Thread t,Throwable e),该方法中的t代表出错的异常线程,e代表该线程抛出的异常。
public class Thread {
static interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
}
那么怎么设置UncaughtExceptionHandler呢?
(1)线程设置UncaughtExceptionHandler:
//为该线程类的所有线程实例设置默认的异常处理器
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh);
//为指定的线程实例设置异常处理器
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh);
(2)线程组设置UncaughtExceptionHandler:
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以只需要重写它的uncaughtException方法即可。
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e) {
...
}
}
异常处理过程:
- JVM首先检查该线程实例的异常处理器(setUncaughtExceptionHandler)有没有设置,有则调用该异常处理器处理异常。
- 如果该线程组有父线程组,则调用父线程组的异常处理器处理该异常。
- 调用线程组自身的异常处理器处理该异常。
- 如果该线程实例所属的线程类有默认的异常处理器(setDefaultUncaughtExceptionHandler),则调用该默认异常处理器处理异常。
- 否则将异常跟踪栈信息打印到System.err错误输出流,并结束该线程。
下面演示了APP崩溃时将异常跟踪栈保存到文件中:
public class MyApplication extends Application {
private MyUncaughtExceptionHandler uncaughtExceptionHandler;
@Override
public void onCreate() {
super.onCreate();
initErrorLog();
}
private void initErrorLog() {
// 程序崩溃时触发
uncaughtExceptionHandler = new MyUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
//创建服务用于捕获崩溃异常
private class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
ex.printStackTrace();
// 保存错误日志
saveCatchInfo2File(ex);
}
};
/**
* 保存错误信息到文件中
*
* @return 返回文件名称
*/
private String saveCatchInfo2File(Throwable ex) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String sb = writer.toString();
try {
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
String time = formatter.format(new Date());
String fileName = time + ".txt";
System.out.println("fileName:" + fileName);
errorPath = getApplicationContext().getExternalFilesDir("error").getAbsolutePath();
FileOutputStream fos = new FileOutputStream(errorPath + "/" + fileName);
fos.write(sb.getBytes());
fos.close();
return fileName;
} catch (Exception e) {
System.out.println("an error occured while writing file..." + e.getMessage());
}
return null;
}
}
线程池
使用线程池原因
- (1)当同时并发多个网络线程时,引入线程池技术会极大地提高APP的性能。
- (2)显著减少了创建线程的数目(系统创建一个新线程的成本是比较高的,因为涉及与操作系统的交互)。
- (3)防止内存过度消耗。控制活动线程的数量,防止并发线程过多。
线程池在系统启动时即创建大量空闲线程,程序将一个Runnable对象或一个Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,方法执行完后该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待下一次任务的到来。
四种线程池
(1)newCachedThreadPool()
缓存型线程池,先查看池中有没有以前建立的线程,如果有,就reuse,如果没有,就建一个新的线程加入池中。能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s, 超过这个IDLE时长,线程实例将被终止并移出池。缓存型池子通常用于执行一些生存期很短的异步型任务 。线程池的大小上限为Integer.MAX_VALUE,可看做是无限大。
(2)newFixedThreadPool(int nThread)
fixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但是不能随时创建新的线程。其独特之处:任意时间点,最多只能有固定数目的活动线程存在,如果此时有新的线程要建立,只能放在另外的队列中(LinkedBlockingQueue队列)等待,直到当前的线程中某个线程终止。和cacheThreadPool不同:fixedThreadPool池线程数固定,但是0秒IDLE(无IDLE),这也就意味着创建的线程会一直存在。所以fixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器。线程池的大小设置,可参数Runtime.getRuntime().availableProcessors()。
(3)newSingleThreadExecutor()
单例线程,任意时间池中只能有一个线程,0秒IDLE(无IDLE)。 相当于newFixedThreadPool(1);
(4)newScheduledThreadPool()
调度型线程池。这个池子里的线程可以按schedule依次delay执行,或周期执行 。0秒IDLE(无IDLE)。
schedule(Runnable command, long delay, TimeUnit unit) //延迟一定时间后执行Runnable任务
schedule(Callable callable, long delay, TimeUnit unit)//延迟一定时间后执行Callable任务;
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)//延迟一定时间后,以间隔period时间的频率周期性地执行任务;
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)//与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。
方法对比:
下面通过一个demo来演示一下四种线程池的用法:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button CachedThreadPool;
private Button FixedThreadPool;
private Button SingleThreadExecutor;
private Button ScheduledThreadPool;
/** 总共多少任务(根据CPU个数决定创建活动线程的个数,这样取的好处就是可以让手机承受得住) */
// private static final int count = Runtime.getRuntime().availableProcessors() * 3 + 2;
/** 总共多少任务 */
private static final int count = 2;
/** 所有任务都一次性开始的线程池 */
private static ExecutorService mCacheThreadExecutor = null;
/** 每次执行限定个数个任务的线程池 */
private static ExecutorService mFixedThreadExecutor = null;
/** 创建一个可在指定时间里执行任务的线程池,亦可重复执行 */
private static ScheduledExecutorService mScheduledThreadExecutor = null;
/** 每次只执行一个任务的线程池 */
private static ExecutorService mSingleThreadExecutor = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CachedThreadPool = (Button) findViewById(R.id.CachedThreadPool);
FixedThreadPool = (Button) findViewById(R.id.FixedThreadPool);
SingleThreadExecutor = (Button) findViewById(R.id.SingleThreadExecutor);
ScheduledThreadPool = (Button) findViewById(R.id.ScheduledThreadPool);
CachedThreadPool.setOnClickListener(this);
FixedThreadPool.setOnClickListener(this);
SingleThreadExecutor.setOnClickListener(this);
ScheduledThreadPool.setOnClickListener(this);
mCacheThreadExecutor = Executors.newCachedThreadPool();
mFixedThreadExecutor = Executors.newFixedThreadPool(count);
mScheduledThreadExecutor = Executors.newScheduledThreadPool(count);
mSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.CachedThreadPool:
ExecutorServiceThread(mCacheThreadExecutor);
break;
case R.id.FixedThreadPool:
ExecutorServiceThread(mFixedThreadExecutor);
break;
case R.id.SingleThreadExecutor:
ExecutorServiceThread(mSingleThreadExecutor);
break;
case R.id.ScheduledThreadPool:
ExecutorScheduleThread(mScheduledThreadExecutor);
break;
}
}
private void ExecutorServiceThread(ExecutorService executorService) {
Log.i("huaxun", "-------------------------------------");
for (int i = 0; i < 3; ++i) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("huaxun", "Thread:" + Thread.currentThread().getId() + " activeCount:" + Thread.activeCount() + " index:" + index);
}
});
}
}
private void ExecutorScheduleThread(ScheduledExecutorService scheduledExecutorService) {
Log.i("huaxun", "-------------------------------------");
for (int i = 0; i < 3; ++i) {
final int index = i;
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("huaxun", "Thread:" + Thread.currentThread().getId() + " activeCount:" + Thread.activeCount() + " index:" + index);
}
},2, TimeUnit.SECONDS);
}
}
}
(1)60s内点击两次CachedThreadPool按钮(两次点击时间大于2s),第二次点击之后超过60s再次点击一下。
一共三次点击。仔细对比可以发现:第一次点击开启了3个线程。没有复用任何线程,60s内第二次点击,全部复用了第一次点击开启的线程。60s后第三次点击,由于IDLE机制,原来开启的线程被自动终止,重新开启了3个新线程。
(2)60s内点击两次FixedThreadPool按钮(两次点击时间大于2s),第二次点击之后超过60s再次点击一下。
FixedThreadPool我们设置的最大线程数是2,表示同时最多只会运行2个线程。从输出的log可以看出第一次创建了390、391两个线程。而在这之后,无论多长时间再次点击FixedThreadPool按钮,都在复用已经创建了的2个线程。无IDLE,所以即使60s后也不会被回收。
(3)60s内点击两次SingleThreadExecutor按钮(两次点击时间大于2s),第二次点击之后超过60s再次点击一下。
Log显示这种线程池从创建392线程之后就一直复用这个线程。newSingleThreadExecutor线程池只能装下一个线程,唯一的用处是保证所有任务按照FIFO(First In First Out)顺序执行。
(3)60s内点击两次ScheduledThreadPool按钮(两次点击时间大于2s),第二次点击之后超过60s再次点击一下。
这个和mFixedThreadExecutor一样,区别是首次创建Thread有个启动延迟时间,本Demo是2s。0秒IDLE(无IDLE)。
几种类型线程池优缺点:
FixedThreadPool
是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。CachedThreadPool
特点是在线程池空闲时,即线程池中没有可运行任务时,它会释放工作线程,从而释放工作线程所占用的资源。但是,但当出现新任务时,又要创建一新的工作线程,又要一定的系统开销。并且,在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
submit()和execute()的区别:
JDK5往后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,它们的区别是:
- execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
- submit(Runnable x) 返回一个future。因为run()没有返回值,所以future对象返回null,但可以调用future对象的isDone()、isCancelled()来判断任务是否成功完成。
- submit(Callable x) 返回一个future。future代表call()方法的返回值。
线程池原理
Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。这些方法最终都是通过ThreadPoolExecutor类来完成的,这里强烈建议大家直接使用Executors类提供的四个便捷的工厂方法,能完成绝大多数的用户场景,当需要更细节地调整配置,需要先了解每一项参数的意义。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明:
- corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
- maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
- keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,线程的空闲时间超过keepAliveTime则会终止。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天);
- workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互: 如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。 如果运行的线程数等于或多于 corePoolSize,则Executor 始终首选将请求加入队列,而不添加新的线程。如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
- threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号);
handler(线程饱和策略):当线程池和队列都满了,则表明该线程池已达饱和状态。
- ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常
- RejectedExecutionException。(默认策略)
- ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
- ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
- ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。
排队有三种通用策略:
直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
- 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
线程池关闭
调用线程池的shutdown()或shutdownNow()方法来关闭线程池:
- shutdown:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- shutdownNow:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
合理地配置线程池
需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。
- 对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数;
- 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率;
- 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。
线程池监控
利用线程池提供的参数进行监控,参数如下:
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数。
通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。
ThreadLocal类
ThreadLocal概述
ThreadLocal是线程局部变量的意思,它的功能非常简单,就是为每一个使用该变量的线程都提供一个该变量值的副本,而不会和其他线程的副本冲突,从线程角度来看,就好像每一个线程都完全拥有该变量一样。
先了解一下ThreadLocal类提供的几个方法:
public T get(); //返回该线程局部变量中当前线程的副本值
public void remove(); //删除该线程局部变量中当前线程的值
public void set(T value); //设置该线程局部变量中当前线程的副本值
protected T initialValue(); //设置该线程局部变量的初始值
总结一下:
- (1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals变量中的;
- (2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
- (3)在进行get之前,必须先set,否则会报空指针异常,如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
注:ThreadLocal原理见threadlocal原理及常用应用场景这篇文章。
public class Test03 {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {
protected Long initialValue() {
return Thread.currentThread().getId();
};
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>() {
protected String initialValue() {
return Thread.currentThread().getName();
};
};
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test03 test = new Test03();
//test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread() {
public void run() {
//test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
}
}
运行结果:
1
main
8
Thread-0
ThreadLocal的应用场景
最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。如:
(1)数据库连接
先来看一个例子:
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close();
}
}
假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。
通常建议:如果多个线程之间需要共享资源,以达到线程之间通信的目的,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
使用ThreadLocal修改代码:
class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection();
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
}
(2)Session管理
class SessionManager {
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
}