1. 前言
spring中的作用域(scope)有singleton/prototype/request/session/global session。
scope选择的原则:有状态(有成员变量)的bean可以设置为prototype作用域,而对无状态(stateless)的bean使用singleton作用域。无状态的单例是线程安全的,容器中只存在一个共享的实例,有状态(stateful)的原型模式(prototype)是线程安全的,每次会重新创建一个实例。
有状态的bean有如下方法解决多线程问题,spring源码中采用ThreadLocal进行处理:
1、方法的参数局部变量(相当于new)
2、threadlocal
3、设置bean的scope=prototype
2. 实际编程过程中的一些多线程的思考?
1、自己编写的类是否满足线程安全?
1. 实体类是有状态的bean,类中有成员变量,在dao层、service层、controller层中传递,不满足线程安全。
在项目开发中对于实体bean在多线程中的处理:
a. 对于实体bean一般通过方法参数的的形式传递(参数是局部变量),所以多线程之间不会有影响。
b. 有的地方对于有状态的bean直接使用prototype原型模式来进行解决。
c. 对于使用bean的地方可以通过new的方式来创建。
2. dao层、service层、controller层中的类默认都是单例模式,dao层中一般不包含可变的成员变量,dao层的类是通过持久化框架进行封装,经测试不会出现线程安全问题,service层中注入dao层类(相当于不变(immutable)类),所以属于线程安全 ,同理controller层中注入service层类属于线程安全。所以这些类虽然是单例模式也是线程安全。
2、ThreadLocal如何使用?
原理概念: 为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。【每个线程其实是改变的是自己线程的副本,而不是真正要改变的变量,所以效果就是每个线程都有自己的,“这其实就将共享变相为人人有份!”】
/*ThreadLocal源码*/
public class ThreadLocal{
// Map用于存储每一个线程的变量的副本
private Map values = Collections.synchronizedMap(new HashMap());
public Object get() {
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)){
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(Object newValue){
values.put(Thread.currentThread(), newValue);
}
public Object initialValue(){
return null;
}
}
2.1 使用方法一
Hibernate文档中关于ThreadLocal的使用
// 初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。如果不初始化initialvalue,则initialvalue返回null。
public static final ThreadLocal session = new ThreadLocal(); //使用ThreadLocal变量
public static Session currentSession() {
// session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接)。多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的session(数据库连接)。如果是该线程是初次访问,自然,s(数据库连接)会是null,接着创建一个Session,保存该数据库连接s到ThreadLocal中。如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。
Session s = (Session)session.get();
//open a new session,if this session has none
if(s == null){
s = sessionFactory.openSession();
session.set(s);
}
return s;
}
2.2 使用方法二
当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal进行子类化,EasyDBO中创建jdbc连接上下文就是这样做的:
public class JDBCContext{
private static Logger logger = Logger.getLogger(JDBCContext.class);
private DataSource ds;
protected Connection connection;
private boolean isValid = true;
private static ThreadLocal jdbcContext; //ThreadLocal变量
private JDBCContext(DataSource ds){
this.ds = ds;
createConnection();
}
public static JDBCContext getJdbcContext(javax.sql.DataSource ds){
if(jdbcContext==null)jdbcContext=new JDBCContextThreadLocal(ds); //new的创建,看下面的自定义方法
JDBCContext context = (JDBCContext) jdbcContext.get();
if (context == null) {
context = new JDBCContext(ds);
}
return context;
}
private static class JDBCContextThreadLocal extends ThreadLocal{
public javax.sql.DataSource ds;
public JDBCContextThreadLocal(javax.sql.DataSource ds){
this.ds=ds;
}
protected synchronized Object initialValue() {
return new JDBCContext(ds);
}
}
}
简单的实现版本
public class SimpleThreadLocal {
private Map valueMap = Collections.synchronizedMap(new HashMap());
public void set(Object newValue) {
valueMap.put(Thread.currentThread(), newValue); //①键为线程对象,值为本线程的变量副本
}
public Object get() {
Thread currentThread = Thread.currentThread();
Object o = valueMap.get(currentThread); //②返回本线程对应的变量
if (o == null && !valueMap.containsKey(currentThread)) {//③如果在Map中不存在,放到Map中保存起来。
o = initialValue();
valueMap.put(currentThread, o);
}
return o;
}
public void remove() {
valueMap.remove(Thread.currentThread());
}
public Object initialValue() {
return null;
}
}
3、ThreadLocal与synchronized之间的区别?
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。但在有些情况下,synchronized不能保证多线程对共享变量的正确读写。例如类有一个类变量,该类变量会被多个类方法读写,当多线程操作该类的实例对象时,如果线程对类变量有读取、写入操作就会发生类变量读写错误,即便是在类方法前加上synchronized也无效,因为同一个线程在两次调用方法之间时锁是被释放的,这时其它线程可以访问对象的类方法,读取或修改类变量。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。
hreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
4、Spring使用ThreadLocal解决线程问题
Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因此有状态的Bean就可以在多线程中共享了。
举例说明:
// 非线程安全
public class TopicDao {
private Connection conn;// 一个非线程安全的变量
public void addTopic(){
Statement stat = conn.createStatement();// 引用非线程安全变量
}
}
// 线程安全
public class TopicDao {
// 1、使用ThreadLocal保存Connection变量
private static ThreadLocal connThreadLocal = new ThreadLocal();
public static Connection getConnection(){
// 2、如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,并将其保存到线程本地变量中。
if (connThreadLocal. get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else{
return connThreadLocal. get();// 3、直接返回线程本地变量
}
}
public void addTopic() {
// 4、从ThreadLocal中获取线程对应的Connection
Statement stat = getConnection().createStatement();
}
}
不同的线程在使用TopicDao时,先判断connThreadLocal.是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
相关:
Spring的单例模式底层实现