1、Spring线程安全吗?
不安全
2、为什么
Spring对bean的作用域默认是单例的,bean(包含Controller, Service, DAO, PO, VO)在使用过程中,如果使用方式为无状态的(无状态即bean中只有方法,无成员变量,只有方法里面的局部变量,局部变量都在栈中,而栈是线程私有的),那么就是安全的。
但是当bean成为了有状态的,如在service的成员变量中定义了vo,那么此vo则为不安全的。
为什么成员变量不安全:因为成员变量没有在栈上,栈上存的是局部变量表。成员变量作为类的信息,存在堆内存上,堆是线程共享的。线程私有的只有栈和程序计数器。
3、不安全的场景重现一下?
我假设MyThread就是一个service,ticket作为成员变量(当然也可以是一个vo bean,如User/Teacher之类)
1 class MyThread extends Thread { 2 private int ticket = 10; 3 4 public void run() { 5 while (ticket > 0) { 6 System.out.print(Thread.currentThread().getName()+"thread:" + ticket--+","); 7 } 8 } 9 }
这时候假设是spring的默认作用域为单例(所以MyThread为单例),来了三个请求(即三个线程)
如下Test,则因为成员变量ticket没有被锁保护,则其变量的增加或减少的操作是会窜数据的。
public class Test { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); } }
4、解决办法
a.需要的时候创建新实例:改变Spring的作用域为Prototype,这样每个请求就会创建一个Service等bean,保证了安全,如下
public class Test { public static void main(String[] args) throws Exception {
MyThread tr1 = new MyThread();
Thread td1 = new Thread(tr1,"aa");
MyThread tr2 = new MyThread();
Thread td2 = new Thread(tr2,"bb");
td1.start();
td2.start();
}
}
每次都是一个新的MyThread,这样每次成员变量都是一个类的,不存在窜数据,用空间换时间。
不过针对变量,就都是每个bean自己的了,需要每个线程自己处理自己的,结果:
bbthread:10,aathread:10,bbthread:9,bbthread:8,bbthread:7,bbthread:6,aathread:9,bbthread:5,aathread:8,bbthread:4,bbthread:3,bbthread:2,bbthread:1,aathread:7,aathread:6,aathread:5,aathread:4,aathread:3,aathread:2,aathread:1,
b. 使用同步:同步成员变量(如ticket)【加了synchronized 关键字】
package com.bjsxt.test; public class Test { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); } } class MyThread extends Thread { private int ticket = 10; public synchronized void run() { while (ticket > 0) { System.out.print(Thread.currentThread().getName()+"thread:" + ticket--+","); } } }
用时间换了空间,也保证了安全。适用于共享成员变量的时候
结果:
Thread-0thread:10,Thread-0thread:9,Thread-0thread:8,Thread-0thread:7,Thread-0thread:6,Thread-0thread:5,Thread-0thread:4,Thread-0thread:3,Thread-0thread:2,Thread-0thread:1,
c、使用ThreadLocal:
原理:
Theradlocal是空间换时间的又一种方法,他用浅拷贝(深浅copy: 浅c就是指针指向同一个对象,深c,则是重新开一块内存。),保证了每个线程有自己的变量。
示例:
package com.bjsxt.test; public class Test { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt,"aa").start(); new Thread(mt,"bb").start(); new Thread(mt,"cc").start(); } } class MyThread extends Thread { private int ticket_Default = 10; private ThreadLocal<Integer> threadLocalticket = new ThreadLocal<Integer>(); public Integer getThreadLocalticket() { Integer curticket = threadLocalticket.get(); if(curticket==null){ threadLocalticket.set(ticket_Default); } return threadLocalticket.get(); } public void run() { int ticket = getThreadLocalticket(); while (ticket > 0) { System.out.print(Thread.currentThread().getName()+"thread:" + ticket--+","); } } }
运行结果:
bbthread:10,aathread:10,ccthread:10,aathread:9,bbthread:9,aathread:8,ccthread:9,aathread:7,bbthread:8,aathread:6,ccthread:8,aathread:5,bbthread:7,aathread:4,ccthread:7,aathread:3,bbthread:6,aathread:2,aathread:1,ccthread:6,ccthread:5,ccthread:4,ccthread:3,ccthread:2,ccthread:1,bbthread:5,bbthread:4,bbthread:3,bbthread:2,bbthread:1,
和3.a一样的结果
4,题外话
4.1 Struts2是基于4.a的方法做的,会把每个controller都创建一个,创建又需要回收,非常浪费内存和性能。另外spring是根据方法进行aop拦截的,所以创建的很多,回收的也需要很多。。
4.2 因为Threadlocal是浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数
更多Theadlocal见参考文章一
4.3 Servlet是线程安全的吗?见参考文章五
5、参考
<一>聊一聊 Spring 中的线程安全性+Theadlocal
http://www.importnew.com/27440.html
<二>Spring框架中的单例Beans是线程安全的么
https://blog.csdn.net/u011202334/article/details/51585648
<三>Spring单例与线程安全小结
https://www.cnblogs.com/doit8791/p/4093808.html
<四>深浅拷贝解析
https://blog.csdn.net/u011420067/article/details/52460183
<五>深入理解Servlet线程安全问题
https://blog.csdn.net/lcore/article/details/8974590
https://blog.csdn.net/qq_24145735/article/details/52433096
<六>
Servlet 工作原理解析
https://www.ibm.com/developerworks/cn/java/j-lo-servlet/