JAVASpring

Spring的线程安全问题

概述


我们知道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变量都是当前线程所绑定的。