springboot单例模式注入对象_SpringBoot 单例Bean中实例变量线程安全研究-Go语言中文社区...

本文探讨了SpringBoot中单例模式的Bean存在的线程安全问题,如全局变量导致的问题,并提供了多种解决方案,包括使用AtomicInteger确保原子性、使用volatile关键字的限制、以及通过ThreadLocal实现线程隔离。此外,还提到了原型模式在多例场景下的应用作为另一种选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先,让我们弄清楚各种变量的区别: 成员变量、全局变量、实例变量、类变量、静态变量和局部变量的区别

Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,单例模式的意思就是只有一个实例当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题

一.单例模式有全局变量存在的问题

目前我们的系统中,Bean都是采取的单例模式,也就是在使用有 @Service 注解的类时都是采用 @Autowired。如果在 Bean 中 有全局变量,则由于该Bean 只有一个实例,当多用户访问统一接口时,Spring 采用多线程的方式去操作这个Bean ,这个全局变量也就可能存在线程不安问题。

1.1 线程不安全测试

Service

@Service

public class ConcService {

private int i;

public void add() {

i++;

}

public int getI() {

return i;

}

}

Controller

@RestController

@RequestMapping("conc")

public class ConcController {

@Autowired

private ConcService concService;

@GetMapping("/addi")

public void addI() {

concService.add();

}

@GetMapping("/geti")

public int getI() {

return concService.getI();

}

}

concService是否是单例测试

输出concService的hashcode值,发现是一样的,也就是c对象一直没变,为单例(注意:如果ConcService类上配置了lombok的 Data注解,则会发现不一样)

对实例变量i的线程安全测试

使用jmeter并发测试,并发访问 localhost:8080/conc/addi 接口 500次,如果是线程安全,则i应该为500,结果表明不是。

1.2 单例模式下保证线程安全

如果如果多个线程并发访问的对象实例只允许,也只能创建一个,那就只能采取同步措施,对于常用的 synchronized 和 lock ,这里暂不讲解 。

1.2.1. 原子类保障线程安全

这里的实例变量 i 是基本类型int,因此用它的原子类 AutomicInteger 应该就是线程安全的了。service代码如下

@Service

public class ConcService {

private AtomicInteger i = new AtomicInteger();

public void add() {

i.incrementAndGet();

}

public int getI() {

return i.intValue();

}

}

结果表明是线程安全的了。

至于为什么原子类可以保证线程安全,请参考下面链接:

Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

1.2.2 volatile 关键字能否保证 i++ 线程安全?

既然研究到这里了,那么突然想到如果用volatile 关键字修饰 i 会线程安全么?

volatile关键字有如下两个作用

(1)保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。

(2)禁止指令重排序优化。

测试如下:

@Service

public class ConcVolatileService {

private volatile int i;

public void addi() {

i++;

}

public int getI() {

return i;

}

}

测试结果表明 volatile 不能保证线程安全:

原因分析:正如上述代码所示,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量

@Service

public class ConcVolatileService {

private int i;

public synchronized void addi() {

i++;

}

public int getI() {

return i;

}

}

1.2.3 采用ThreadLocal 实现变量线程隔离

如果在单例模式下,实例变量需要实现线程隔离,也就是每次访问的 i 是初始值,则可以使用ThreadLocal 。ThreadLocal 的作用 则是实现将单例对象的属性 与 当前线程进行绑定。

ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,那就没有任何必要对这些线程进行同步,它们也能最大限度的由CPU调度,并发执行。并且由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。对比前一种方案,这是一种以空间来换取线程安全性的策略。

代码改造如下

Service层改造:

@Service

public class ConcurrencyService {

//private int i

// 定义 ThreadLocal 对象i ,区别于上面的int i

private static ThreadLocal i = new ThreadLocal() {

@Override

// 初始化 i

protected Integer initialValue() {

return 0;

}

};

public void add() {

i.set(getI() + 1);

}

public int getI() {

// 分装一层,调用ThreadLocal对象 i 的 get()方法

return i.get();

}

public void setI(int ii) {

// 分装一层,调用ThreadLocal对象 i 的 set()方法

i.set(ii);

}

}

Controller层:

public class ConcurrencyController {

@Autowired

private ConcurrencyService c;

@GetMapping("/addi")

public void addi() {

log.info(c.hashCode() + " " + c.getI());

c.add();

log.info(c.getI() + "");

}

@GetMapping("/geti")

public int getI() {

return c.getI();

}

}

两次访问该接口,输出结果为:

可以看出,c仍然是同一个对象,但是i 每次都是初始值0,也就是意味着i实现了线程隔离,也就是线程安全的了。使用这种方式只需要修改原先的变量的调用方式就好。

弊端:

ThreadLocal变量的这种隔离策略,也不是任何情况下都能使用的。如果多个线程并发访问的对象实例只允许,也只能创建一个,那就没有别的办法了,此时需要使用同步机制(synchronized)

二. 采用原型模式实现多例

如果如果多个线程并发访问的对象实例可以创建多个,则可以用原型模式实现多例,也就是每次不是用同一个对象,而是类似new一个出来。

// //注入方式

// @Autowired

// private ConcurrencyService c;

// 不使用用@Autowired

@Autowired

private org.springframework.beans.factory.BeanFactory beanFactory;

@GetMapping("/addi")

public void addi() {

ConcurrencyService c = beanFactory.getBean(ConcurrencyService.class);

log.info(c.hashCode()+""+c.getI();

c.add();

}

改成这种方式后,会发现两次访问的实例c是不一样的,i也没有在两次访问之后变成1

弊端:

但是这种方式对原有代码改动太大,原先通过Autowired 使用的对象都需要改造,而且每次接口访问都会变成new对象出来,对象能消耗大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值