概述
我们知道Spring IoC 容器中只会存在一个 bean 的实例,无论一次调用还是多次调用,始终指向的都是同一个 bean 对象。
对于单实例来说,所有线程都共享同一个 bean 实例,自然就会发生资源的争抢,从而导致线程不安全。
举例:
新增服务类ThreadUnSafeService
@Service public class ThreadUnSafeService { public int i; public void add() { i++; } public void sub() { i--; } public int getValue() { return i; } }
启动十个线程,每个线程各做一千次加减:
public class TestSpring { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( new String[] { "applicationContext.xml" }); for (int j = 0; j < 10; j++) { new Thread(new Runnable() { @Override public void run() { ThreadUnSafeService service = (ThreadUnSafeService) context.getBean("threadUnSafeService"); for (int i = 0; i < 1000; i++) { service.add(); } for (int i = 0; i < 1000; i++) { service.sub(); } System.out.println(Thread.currentThread().getName() + "-" + service.getValue()); } }).start(); } } }
输出结果:
Thread-2-0 Thread-3-0 Thread-8-1201 Thread-10-663 Thread-1-1443 Thread-9-1380 Thread-7-1846 Thread-6-488 Thread-4-2507 Thread-5-2856
从结果可以看出,运行结果都不是 0,这明显的是线程不安全!!
因为 10 个线程获取的 ThreadUnSafe 实例都是同一个,并且 10 个线程都对同一个资源 i 发生了争抢,所以才会导致线程安全问题的发生。
解决方案一:scope 的值改为 prototype
@Service @Scope("prototype") public class ThreadSafeService { public int i; public void add() { i++; } public void sub() { i--; } public int getValue() { return i; } }
输出结果:
Thread-3-0 Thread-1-0 Thread-7-0 Thread-2-0 Thread-4-0 Thread-6-0 Thread-5-0 Thread-8-0 Thread-9-0 Thread-10-0
prototype 作用域下,每次获取的 ThreadUnSafe 实例都不同,所以自然不会有线程安全的问题。
解决方案二:将bean改为无状态的
无状态bean就是没有共享变量,所以就不会产生线程安全问题。
@Service public class ThreadSafeService { public void getValue() { int val = 0; for (int i = 0; i < 1000; i++) { val++; } for (int i = 0; i < 1000; i++) { val--; } System.out.println(val); } }
调用:
public class TestSpring { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( new String[] { "applicationContext.xml" }); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { ThreadSafeService service = (ThreadSafeService) context.getBean("threadSafeService"); service.getValue(); } }).start(); } } }
输出结果:
0 0 0 0 0 0 0 0 0 0
解决方案三:加锁
既然是线程安全问题,那就加锁。
public class TestSpring { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( new String[]{"applicationContext.xml"}); for (int j = 0; j < 10; j++) { new Thread(new Runnable() { @Override public void run() { ThreadUnSafeService service = (ThreadUnSafeService) context.getBean("threadUnSafeService"); synchronized (service) { for (int i = 0; i < 1000; i++) { service.add(); } for (int i = 0; i < 1000; i++) { service.sub(); } System.out.println(Thread.currentThread().getName() + "-" + service.getValue()); } } }).start(); } } }
运行结果为 0。
毫无疑问加锁确实可以,但是加锁会增加性能上的开销。
解决方案四:ThreadLocal
ThreadLocal 在自己线程内创建一个变量的副本,规避了线程安全问题。
@Service public class ThreadUnSafeService { public static ThreadLocal<Integer> i = new ThreadLocal<>(); public void add() { Integer integer = i.get(); if (null == integer){ integer = 0; } integer++; i.set(integer); } public void sub() { Integer integer = i.get(); if (null == integer){ integer = 0; } integer--; i.set(integer); } public int getValue() { return i.get(); } }
运行结果为 0。
但是ThreadLocal为每个线程创建变量的副本,带来了空间上的开销。
总之,解决线程安全问题思路分为两种。 第一种,消除共享变量,是线程不安全问题不存在,方法有将bean改为无状态的、scope 的值改为 prototype、ThreadLocal。 第二种,就是加锁。
Spring自身方案
无状态的bean
在Spring中,绝大部分bean都是无状态的,因此即使这些bean默认是单例的,也不会出现线程安全问题的。比如controller、service、dao这些类,这些类里面通常不会含有成员变量,因此它们被设计成单例的。如果这些类中定义了实例变量,就线程不安全了,所以尽量避免定义实例变量。
有状态的bean
Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象” 采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。
这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。