Spring中线程安全问题

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的单例模式底层实现

参考:
https://blog.csdn.net/cs408/article/details/48972653

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值