springboot单例模式注⼊对象_SpringBoot单例Bean中实例变
量线程安全研。。。
⾸先,让我们弄清楚各种变量的区别: 成员变量、全局变量、实例变量、类变量、静态变量和局部变量的区别
Spring框架⾥的bean,或者说组件,获取实例的时候都是默认的单例模式,单例模式的意思就是只有⼀个实例当多⽤户同时请求⼀个服务时,容器会给每⼀个请求分配⼀个线程,这是多个线程会并发执⾏该请求多对应的业务逻辑(成员⽅法),此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题
⼀.单例模式有全局变量存在的问题
⽬前我们的系统中,Bean都是采取的单例模式,也就是在使⽤有 @Service 注解的类时都是采⽤ @Autowired。如果在 Bean 中 有全局变量,则由于该Bean 只有⼀个实例,当多⽤户访问统⼀接⼝时,Spring 采⽤多线程的⽅式去操作这个Bean ,这个全局变量也就可能存在线程不安问题。
springboot框架的作用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() {
I();
}
}
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()⽅法();
}
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.I() + "");
}
@GetMapping("/geti")
public int getI() {
I();
}
}
两次访问该接⼝,输出结果为:
可以看出,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 = Bean(ConcurrencyService.class);
log.info(c.hashCode()+""+c.getI();
c.add();
}
改成这种⽅式后,会发现两次访问的实例c是不⼀样的,i也没有在两次访问之后变成1
弊端:
但是这种⽅式对原有代码改动太⼤,原先通过Autowired 使⽤的对象都需要改造,⽽且每次接⼝访问都会变成new对象出来,对象能消耗⼤。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论