Controller 是否线程安全

案例1- Contoller对象属性操作

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private int count = 0;

    @GetMapping(value = "/index")
    public String index() {
        count++;
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-01-02 07:33:03.749  INFO 3183 --- [nio-8080-exec-3] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:2
2021-01-02 07:33:03.749  INFO 3183 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:3
2021-01-02 07:33:03.750  INFO 3183 --- [nio-8080-exec-2] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:5
2021-01-02 07:33:03.750  INFO 3183 --- [nio-8080-exec-8] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:6
2021-01-02 07:33:03.751  INFO 3183 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:7
2021-01-02 07:33:03.751  INFO 3183 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:8
2021-01-02 07:33:03.750  INFO 3183 --- [nio-8080-exec-6] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:4
2021-01-02 07:33:03.752  INFO 3183 --- [nio-8080-exec-4] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:9
......
......
2021-01-02 07:33:03.867  INFO 3183 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:92
2021-01-02 07:33:03.867  INFO 3183 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:92
2021-01-02 07:33:03.867  INFO 3183 --- [nio-8080-exec-6] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:94
2021-01-02 07:33:03.867  INFO 3183 --- [nio-8080-exec-2] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:92
2021-01-02 07:33:03.868  INFO 3183 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:95
2021-01-02 07:33:03.869  INFO 3183 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:96
2021-01-02 07:33:03.870  INFO 3183 --- [nio-8080-exec-3] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:97
2021-01-02 07:33:03.873  INFO 3183 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

案例2-Contoller对象属性加volatile

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private volatile int count = 0 ;

    @GetMapping(value = "/index")
    public String index() {
        count++;
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ab -n 100 -c 10 http://localhost:8080/unsafe/index
1
2021-01-02 10:57:15.117  INFO 5680 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:86
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-6] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:89
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-5] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:90
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:88
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-3] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:91
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:92
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-4] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:93
2021-01-02 10:57:15.119  INFO 5680 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:94

1
2
3
4
5
6
7
8
9

volatile关键字能保证可见性,可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。所以最终输出并不是100.

案例3-Contoller对象属性: volatile + integer

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private  volatile Integer count = 0 ;

    @GetMapping(value = "/index")
    public  String index() {
        count++;
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ab -n 100 -c 10 http://localhost:8080/unsafe/index
1
2021-01-02 10:58:34.622  INFO 5711 --- [nio-8080-exec-6] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:89
2021-01-02 10:58:34.623  INFO 5711 --- [nio-8080-exec-2] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:90
2021-01-02 10:58:34.624  INFO 5711 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:91
2021-01-02 10:58:34.624  INFO 5711 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:92
2021-01-02 10:58:34.624  INFO 5711 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:93
2021-01-02 10:58:34.623  INFO 5711 --- [nio-8080-exec-3] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:90
2021-01-02 10:58:34.625  INFO 5711 --- [nio-8080-exec-8] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:94
1
2
3
4
5
6
7

同样Integer也不是线程安全的方法,不能保证最终结果是100。

案例4-synchronized 关键字

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private  int count = 0 ;

    @GetMapping(value = "/index")
    public synchronized String index() {
        count++;
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-01-02 10:49:11.664  INFO 5450 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:94
2021-01-02 10:49:11.665  INFO 5450 --- [nio-8080-exec-3] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:95
2021-01-02 10:49:11.668  INFO 5450 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:96
2021-01-02 10:49:11.669  INFO 5450 --- [nio-8080-exec-4] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:97
2021-01-02 10:49:11.669  INFO 5450 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:98
2021-01-02 10:49:11.670  INFO 5450 --- [nio-8080-exec-2] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:99
2021-01-02 10:49:11.670  INFO 5450 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:100
1
2
3
4
5
6
7

案例5-synchronized修饰代码段

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private  Integer count = 0 ;

    @GetMapping(value = "/index")
    public  String index() {
        synchronized(count) {
            count++;
        }
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ab -n 1000 -c 10 http://localhost:8080/unsafe/index
1
2021-01-02 10:53:28.234  INFO 5609 --- [nio-8080-exec-2] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:998
2021-01-02 10:53:28.234  INFO 5609 --- [nio-8080-exec-4] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:997
2021-01-02 10:53:28.234  INFO 5609 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:997
2021-01-02 10:53:28.234  INFO 5609 --- [nio-8080-exec-8] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:999
2021-01-02 10:53:28.235  INFO 5609 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : Unsafe Controller count should be:1000

1
2
3
4
5
6

案例6-使用AtomicInteger

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);

    private AtomicInteger count = new AtomicInteger(0);

    @GetMapping(value = "/index")
    public String index() {
        count.addAndGet(1);
        logger.info("Unsafe Controller count should be:" + count);
        return "unsafe";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ab -n 10000 -c 50 http://localhost:8080/unsafe/index
1

使用Spring托管的Service Bean是否线程安全

我们常说Spring托管的bean是singleton的,那么如果把逻辑放入一个bean,然后在controller引用这个bean,会解决并发,保证原子性吗?

答案是否定的。

@Service
public class SumBean {
    private int cnt = 0;

    public void plus(){
        this.cnt++ ;
    }

    public int getCnt(){
        return cnt;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13

案例7-Service Bean

我们新建一个contoller方法


    @GetMapping(value = "/sum")
    @ResponseBody
    public String sum() {
        sum.plus();
        logger.info("Sum Bean count should be:" + sum.getCnt());
        return "sum";
    }

1
2
3
4
5
6
7
8
9
 ab -n 100 -c 10 http://localhost:8080/unsafe/sum
1
2021-01-02 11:44:09.006  INFO 6606 --- [nio-8080-exec-5] c.f.s.t.controller.UnsafeController      : Sum Bean count should be:92
2021-01-02 11:44:09.006  INFO 6606 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : Sum Bean count should be:93
2021-01-02 11:44:09.007  INFO 6606 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : Sum Bean count should be:94
2021-01-02 11:44:09.007  INFO 6606 --- [nio-8080-exec-6] c.f.s.t.controller.UnsafeController      : Sum Bean count should be:95
2021-01-02 11:44:09.009  INFO 6606 --- [nio-8080-exec-4] c.f.s.t.controller.UnsafeController      : Sum Bean count should be:96
1
2
3
4
5

残念ですが

程序并没有输出我们期待的100。其实与放在controller里一样.Spring mvc是一个多线程的环境,线程之间修改共享的对象或者属性,是非常危险的。

Spring容器会初始化一个DispatcherServlet实例。容器同样管理着一个线程池用于响应HTTP连接。当一个请求来的时候,容器从线程池中拿出一个线程,执行service()方法,把请求转发给正确的@Controller实例。

所以,如果我们保证Sumbean的原子性操作

案例8-synchronized

@Service
public class SumBean {
    private int cnt = 0;
    private  final Logger logger = LoggerFactory.getLogger(SumBean.class);

    public synchronized void plus(){
        this.cnt++ ;
        logger.info("Sum Bean count should be:" + cnt);

    }

    public int getCnt(){
        return cnt;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

案例9-AtomicInteger

@Service
public class SumBean {
    //private int cnt = 0;
    private AtomicInteger cnt = new AtomicInteger(0);

    private  final Logger logger = LoggerFactory.getLogger(SumBean.class);

    public  void plus(){
        this.cnt.incrementAndGet() ;
        logger.info("Sum Bean count should be:" + cnt);

    }

    public int getCnt(){
        return cnt.get();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

案例10-模拟数据库访问

在这个案例中,我们模拟一个数据库应用,扣减库存。

  • 向Contoller发起请求,每次请求扣减1个库存。系统初始化为10000.
  • Service负责计算库存,并调用DAO保存。 我们预想中使用的库存和可用库存的总数量应该为10000.
@Component
/**
*模拟DAO
*/
public class InventoryDAO {
	//库存初始化为10000
    private Integer availableStock=10000;
    private Integer usedStock = 0;

    public Integer getStock(){
        return this.availableStock;
    }

    public void setStock(Integer qty){
        this.availableStock = qty;
    }

    public void setUsedStock(Integer qty){
        this.usedStock = this.usedStock + qty;
    }

    public Integer getUsedStock(){
        return this.usedStock;
    }
}

@Service
/*
*业务逻辑
*/
public class Inventory {
    @Autowired
    InventoryDAO inventoryDAO;

    private  final Logger logger = LoggerFactory.getLogger(Inventory.class);

    //扣减库存
    public  void allocateStock(int qty){
        Integer newQty;
        if(inventoryDAO.getStock()>= qty){
            newQty = inventoryDAO.getStock() - qty;
            inventoryDAO.setStock(newQty);
            inventoryDAO.setUsedStock(qty);
            logger.info("Stock left/Stock used: "+ inventoryDAO.getStock() + "/" + inventoryDAO.getUsedStock());

        }
        else{
            logger.info("Stock not Available");
        }
    }

    public int getStock(){
        return inventoryDAO.getStock().intValue();
    }

    public int getUsedStock(){
        return inventoryDAO.getUsedStock().intValue();
    }
}

@Controller
@RequestMapping("/unsafe")
public class UnsafeController {
    private static final Logger logger = LoggerFactory.getLogger(UnsafeController.class);
    @Autowired
    private Inventory inventory;

    @GetMapping(value = "inv")
    @ResponseBody
    //每次扣减1个
    public  String inv(){
        int qty = 1;
        inventory.allocateStock(1);
        logger.info("used stock should be: " + inventory.getUsedStock());
        return "inv";
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

总共发起100个请求,并发为10

ab -n 100 -c 10 http://localhost:8080/unsafe/inv
1

按照预期,最终使用的使用库存数和库存余数和应该为10000,使用库存应该为100,预存余数应该为9900

2021-01-02 16:01:41.466  INFO 10680 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : used stock should be: 84
2021-01-02 16:01:41.466  INFO 10680 --- [io-8080-exec-10] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9914/86
2021-01-02 16:01:41.466  INFO 10680 --- [nio-8080-exec-8] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9915/85
2021-01-02 16:01:41.466  INFO 10680 --- [nio-8080-exec-5] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9913/87
2021-01-02 16:01:41.466  INFO 10680 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : used stock should be: 87
2021-01-02 16:01:41.466  INFO 10680 --- [nio-8080-exec-8] c.f.s.t.controller.UnsafeController      : used stock should be: 87
2021-01-02 16:01:41.466  INFO 10680 --- [nio-8080-exec-5] c.f.s.t.controller.UnsafeController      : used stock should be: 87
1
2
3
4
5
6
7

多次执行,我们发现,使用库存数几乎都不是100.

如何保证线程安全

与之前的例子一样,我们可以使用synchronized方式来改造上述代码。

除此以外还可以使用ReentranLock。

@Service
public class Inventory {
    @Autowired
    InventoryDAO inventoryDAO;

    private  Lock lock;

    public Inventory(){
        this.lock = new ReentrantLock();
    }

    private  final Logger logger = LoggerFactory.getLogger(Inventory.class);
    public  void allocateStock(int qty){
        Integer newQty;
        lock.lock();
        try {
            if (inventoryDAO.getStock() >= qty) {
                newQty = inventoryDAO.getStock() - qty;
                inventoryDAO.setStock(newQty);
                inventoryDAO.setUsedStock(qty);
                logger.info("Stock left/Stock used: " + inventoryDAO.getStock() + "/" + inventoryDAO.getUsedStock());

            } else {
                logger.info("Stock not Available");
            }
        }finally {
            lock.unlock();
        }
    }

    public int getStock(){
        return inventoryDAO.getStock().intValue();
    }

    public int getUsedStock(){
        return inventoryDAO.getUsedStock().intValue();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

执行测试

ab -n 100 -c 10 http://localhost:8080/unsafe/inv
1
2021-01-02 20:47:16.057  INFO 13090 --- [nio-8080-exec-7] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9903/97
2021-01-02 20:47:16.057  INFO 13090 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : used stock should be: 97
2021-01-02 20:47:16.057  INFO 13090 --- [nio-8080-exec-8] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9902/98
2021-01-02 20:47:16.057  INFO 13090 --- [nio-8080-exec-8] c.f.s.t.controller.UnsafeController      : used stock should be: 98
2021-01-02 20:47:16.059  INFO 13090 --- [nio-8080-exec-9] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9901/99
2021-01-02 20:47:16.059  INFO 13090 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : used stock should be: 99
2021-01-02 20:47:16.059  INFO 13090 --- [io-8080-exec-10] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9900/100
2021-01-02 20:47:16.059  INFO 13090 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : used stock should be: 100
......
 2021-01-02 20:47:24.693  INFO 13090 --- [nio-8080-exec-5] c.f.s.t.controller.UnsafeController      : used stock should be: 197
2021-01-02 20:47:24.695  INFO 13090 --- [io-8080-exec-10] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9802/198
2021-01-02 20:47:24.695  INFO 13090 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : used stock should be: 198
2021-01-02 20:47:24.695  INFO 13090 --- [nio-8080-exec-7] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9801/199
2021-01-02 20:47:24.695  INFO 13090 --- [nio-8080-exec-7] c.f.s.t.controller.UnsafeController      : used stock should be: 199
2021-01-02 20:47:24.697  INFO 13090 --- [nio-8080-exec-1] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9800/200
2021-01-02 20:47:24.697  INFO 13090 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : used stock should be: 200
......
2021-01-02 20:47:29.751  INFO 13090 --- [nio-8080-exec-1] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9702/298
2021-01-02 20:47:29.751  INFO 13090 --- [nio-8080-exec-1] c.f.s.t.controller.UnsafeController      : used stock should be: 298
2021-01-02 20:47:29.751  INFO 13090 --- [io-8080-exec-10] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9701/299
2021-01-02 20:47:29.751  INFO 13090 --- [io-8080-exec-10] c.f.s.t.controller.UnsafeController      : used stock should be: 299
2021-01-02 20:47:29.753  INFO 13090 --- [nio-8080-exec-9] c.f.sample.threadsafe.service.Inventory  : Stock left/Stock used: 9700/300
2021-01-02 20:47:29.753  INFO 13090 --- [nio-8080-exec-9] c.f.s.t.controller.UnsafeController      : used stock should be: 300
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public synchronized void method() {
    method body
}

1
2
3
4

与下面这段代码等价。

public void method() {
    this.intrinsicLock.lock();
    try {
        method body
    }
    finally { 
        this.intrinsicLock.unlock(); 
    }
}
1
2
3
4
5
6
7
8
9

另外一般情况下(例如单体应用,一个JVM),考虑Core Java一书给出的建议

  • It is best to use neither Lock/Condition nor the synchronized keyword. In many situations, you can use one of the mechanisms of the java.util.concurrent package that do all the locking for you. For example, in Section 12.5.1, “Blocking Queues,” on p. 781, you will see how to use a blocking queue to synchronize threads that work on a common task. You should also explore parallel streams—see Chapter 1 of Volume II.
  • If the synchronized keyword works for your situation, by all means, use it. You’ll write less code and have less room for error. Listing 12.5 shows the bank example, implemented with synchronized methods.
  • Use Lock/Condition if you really need the additional power that these constructs give you.

多JVM/集群环境/分布式

synchronized也好或者AtomicInteger也好,只能保证在同一个JVM中的线程安全。

如果在分布式环境下,要读取操作同样一份数据,就要更妥善的考虑。