spring单例与线程并发

spring与线程概述

spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这在多线程开发的时候尤其注意。
单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程。(线程不是Spring分配的,是Servlet初始化的时候,就已经分配了的, 单实例,多线程->线程池)这时多个线程会并发的执行该请求所对应的业务逻辑(成员方法),如果该处理逻辑中有对成员变量的修改(临界资源),则必须考虑线程同步的问题。
一般的web应用划分为展现层、服务层和持久层,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般的情况下,从接收请求到返回响应所经过的所有程序调用都属于同一个线程。用完后再释放到线程池

何时需要考虑同步

(当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单例Bean状态的修改(体现为该单例Bean的成员属性),则必须考虑线程同步问题。)

临界资源的多线程修改-线程同步机制

在同步机制中,通过对对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等复杂的问题

临界资源的多线程修改-threadlocal

threadlocal从另一个角度来解决多线程的并发访问。threadlocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal
如果是使用了ThreadLocale,线程执行完毕后要自行remove,否则后续请求分配到以往请求的线程,会拿到上次设置和操作过的值。remove的时机:使用spring拦截器:HandlerInterceptor,在方法执行成功后调用删除remove:afterCompletion

总结:对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

spring使用Threadlocal解决线程安全问题

一般情况下,只有无状态的Bean才可以在多线程环境下共享(无状态会话Bean是没有能够标识它的目前状态的属性的Bean;状态会话Bean至少有一个属性来标识它目前的状态).在Spring中,对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因次有状态的Bean就可以在多线程中共享了。
在开发和设计系统的时候注意下一下三点:

  1. 自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
  2. 对线程环境下,对每一个共享的可变变量都要注意其线程安全性
  3. 我们的类和方法在做设计的时候,要尽量设计成无状态的

线程安全案例

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
public class ConcurrentDateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
//这里重写threadlocal的初始化方法,当get获取为空的时候,自动初始化该值
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
}
或者
public class ThreadLocalDateUtil {
private static final String date_format = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
public static DateFormat getDateFormat()
{
DateFormat df = threadLocal.get();
if(df==null){
df = new SimpleDateFormat(date_format);
threadLocal.set(df);
}
return df;
}
public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}
public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}
}

说明:使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。 —后面要考虑工具类是否是线程安全的

threadlocal部分源码解析

对于每个Thread而言,都有个存放数据的变量threadLocals:

1
2
3
4
5
ThreadLocal.ThreadLocalMap threadLocals = null;
//threadlocalMap的定义,里面实际上是一个Entry,key就为threadlocal,value为存放的值
ThreadLocalMap {
Entry<threadlocal,value>;
}

threadlocal对于共享变量拷贝的副本,实际上是存储在每个Thread的本地变量threadLocals中的。不同的线程维护自己的threadLocals,来达到线程安全隔离

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
//设置值
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//从当前线程的threadLocals中取出值
if (map != null)//如果已存在值,则替换为新值
map.set(this, value);
else
createMap(t, value);//不存在则创建新的值
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;//返回的实际上是当前线程内的数据变量
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);//这里构建新的值
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//从当前线程的threadLocals中取出值
if (map != null) {//如果有值,则取出值,key都为当前的threadlocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//如果无值,则初始化一个值,所以创建threadlocal时可以重新初始化方法,默认是返回空值
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}

###有状态与无状态

有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。     无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。     无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。     有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的Bean实例。

参考:Spring单例与线程安全小结