单例模式在多线程下带来的赋值问题
问题描述:现在有一个操作:(在饿汉式下,即使用静态内部类保证单例)
1.获取一次单例的对象并对单例中的属性值进行赋值,
2.此时开启一条线程循环1000次去获取单例中的属性值
3.再次获取一个单例的对象并再次赋值
会产生一个现象当步骤3赋值后步骤2中线程获得的值还可能是步骤1的值
原因:在步骤3进入且准备更改步骤1的值时此时步骤2的线程获取到了步骤1的值且在步骤3赋值完成且输出后线程的值才输出,所以导致了线程在步骤1的值已经修改的情况下还能获取到步骤1的值。
下面看代码:
public class Test03 implements Runnable{
public static void main(String[] args) {
// 第一次获取单例对象且赋值Singleton03
Singleton03 s1 = Singleton03.getInstance();
System.out.println(s1.id=1);
System.out.println(s1.age=18);
System.out.println(s1.name="邹飞鸣");
// 开启线程,循环1000次获取单例对象(Singleton03)的属性值
Test03 test03 = new Test03();
new Thread(test03).start();
// 睡眠一下,防止线程还没开辟就将步骤3的代码运行了
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 第二次获取单例对象且赋值Singleton03
Singleton03 s2 = Singleton03.getInstance();
System.out.println(s2.id=2);
System.out.println(s2.age=20);
System.out.println(s2.name="邹飞");
}
@Override
public void run() {
for (int i=0;i<1000;i++) {
Singleton03 s3 = Singleton03.getInstance();
System.out.println("进入多线程+"+s3);
}
}
}
class Singleton03{
// 方便查看属性值
@Override
public String toString() {
return "Singleton03{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
public int id;
public int age;
public String name;
private Singleton03(){}
// 静态内部类
private static class HolderClass{
private final static Singleton03 s = new Singleton03();
}
public static Singleton03 getInstance(){
return HolderClass.s;
}
}
结果:重复输出太多只取重要有变化的一段
1 // 这是第一次修改
18
邹飞鸣
进入多线程+Singleton03{id=1, age=18, name='邹飞鸣'}
。。。
进入多线程+Singleton03{id=1, age=18, name='邹飞鸣'}
进入多线程+Singleton03{id=1, age=18, name='邹飞鸣'}
进入多线程+Singleton03{id=2, age=18, name='邹飞鸣'} // 可以看到从这里开始数据变化
进入多线程+Singleton03{id=2, age=18, name='邹飞鸣'}
2
20
进入多线程+Singleton03{id=2, age=18, name='邹飞鸣'}
邹飞
进入多线程+Singleton03{id=2, age=20, name='邹飞'} // 变化完成
进入多线程+Singleton03{id=2, age=20, name='邹飞'}
。。。
可以看到步骤3更改了步骤1的age值但是下一个线程输出的age值还是步骤1的。所以在特定条件下是可能存在属性值全是步骤1的值。
注:赋值操作不是连贯的,即不是原子性操作,当我们把三步的赋值操作设置成原子操作后是有很大概率实现上述说的
如何避免:加个锁。
注:那在spring中的单例在多线程下是如何保证线程安全的呢?
结论:
1.spring中的IoC容器是不保证单例在并发下的线程安全问题。
2.可使用synchronize来保证(太消耗性能)
3.使用ThreadLocal来保证多线程下的线程安全问题
详细讲之前先说一下什么是无状态单例和有状态单例:
- 无状态单例:要设置成单例的类中没有任何存储数据或者修改数据的能力,如:没有成员方法或者成员方法被final修饰,单纯作为一个模板,即使要使用也是外部输入数据进去。就好比我们只在类中定义方法不定义成员变量,这样即使在多线程下存在线程安全问题,但由于没有操作数据的修改或者删除,再怎么不安全也不会影响到正常使用。
- 有状态单例:成为单例的类中存在成员方法,这样当其他类获取到单例对象时可以修改单例对象中的属性值,而一旦是多线程并发下去修改属性值就会出现线程安全问题
解决方法:
1.使用ThreadLocal:需要自己在spring中设置,当开启一条线程时可获取有状态单例的一个副本存储进ThreadLocal中,ThreadLocal中的数据结构是(K,V)K值存储线程对象,V值存储单例副本。这样每个线程都有专属于自己的副本,就避免了线程安全问题
2.使用synchronize。
3.spring中Bean的作用域有两种:singleton(每次获取同一单例),prototype(每次获取新的单例),我们使用后者即可
注:spring中常见的都是无状态的单例,所以不用在意线程安全问题,只有自己写的业务类时才需要考虑。
吐槽:
妈的,这玩意不是常识吗?大部分并发下的这种操作即使不是单例模式也有可能发生啊。
什么菜*面试官、问个问题都不会问,关键是问出来还支支吾吾自己解释不了让我自己回去查?what?还说是啥缓存机制?
面试官原问题是:在单例模式下,类A获取单例对象且修改对象中的属性值,然后类B也获取对象也修改单例对象属性值,此时类C能否获取到类A修改的属性值?
我当时就在想这是什么**问题,我还反复确认。我还以为是SpringBoot的特有属性,我还问了一句说这是不是SpringBoot特有属性,如果在平常的单例模式下就不会存在这种情况?结果他说不知道还让我回去查,妈的越想越气,不知道你当什么问题出?淦!