介绍
什么是线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或则在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
- 线程不安全示例:在get变量属性同时有其他线程set变量属性值,此时就需要额外的同步才能保证线程安全,这就是做了额外处理,因此代表get同时set这种操作是线程不安全的。
什么情况下会出现线程安全?
- 多个线程同时读写一个变量
- 活跃性问题:死锁、活锁、饥饿
- 对象发布和初始化时
- 对象发布:使一个对象能够被当前范围之外的代码所使用
- 出现问题:
- 方法错误返回了一个private对象,我们定义private对象的目的就是不想让外界访问,如果方法返回了这个对象,就可能出现一些未知错误
- 给private对象复制一个新的对象
- 将新的对象返回,新的对象如何改变,都不会影响private对象
- 还未完成初始化(构造函数没完全执行完毕),就把对象提供给外界
- 在构造函数中未初始化完成就this赋值
- 隐式逸出:对象还未构造完成,就被其他线程看见了(单例模式中双重检验锁专门为了防止此情况)
- 构造函数中运行线程:在构造函数中启动线程,如果使用了this对象,就相当于本类的构造函数尚未初始化完成,就已经使用了它的对象,会出现未知的行为。
- 方法错误返回了一个private对象,我们定义private对象的目的就是不想让外界访问,如果方法返回了这个对象,就可能出现一些未知错误
示例
多线程同时读写
1 | /** |
输出结果:
1 | 实际数字:16630 |
可以看到,设置两个线程共享同一个变量进行写操作,每个线程进行1W次++操作,正常来说,最后结果值应该是2W,但是实际运行结果是16630次,被记录下来的错误次数共294次。
错误原因:当线程1对index进行写操作,比如 30++变为31,但是线程2中也在进行操作,没有接收到这个31值,还是对原来读写到线程2中的index值进行++,也许是28++变成29,然后写回主存,就会覆盖掉线程1的值,造成错乱。
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
1 | /** |
运行结果:
在程序中,我们创建两个不同的对象作为两把锁,当线程1获得一把锁之后休眠1秒,线程2此时就获得了另一把锁,并且也休眠一秒。当两个线程继续执行时,就会发现所需要的锁被对方拿走了,而两个线程都不愿意释放自己持有的锁对象,就会一直等待对方释放锁,直到永恒,这就是死锁。
活锁
在完全相同的时刻,多个线程获取对方的资源,等待一段时间后,还是拿不到剩下所需的锁,就判断可能发生了死锁,就释放自己的锁,如果等待时间相同,就可能造成活锁。
- 虽然线程并没有阻塞,也是始终在运行(所以叫活锁,线程是活的),但是程序却得不到进展,因为线程始终重复同样的事情。
- 活锁的线程仍然会消耗CPU资源,而死锁的线程不需要消耗CPU资源。
- 活锁和死锁的结果是一样的
活锁发生的原因:重试机制始终不变
示例:
1 | /** |
输出结果
1 | 牛郎:亲爱的织女,你先吃吧 |
从例子中,我们就能发现两个对象相互谦让,使用同种谦让策略,导致两个类一直阻塞,无法进行接下来的操作。
饥饿
- 当线程需要某些资源(例如CPU),但是却始终得不到
- 线程的优先级设置的过低,或则有某线程持有锁同时又无限循环从而不释放锁,或则某程序始终占用某文件的写锁
未初始化完成
返回private对象错误可以通过设计模式中的原型模式解决,通过Cloneable.clone()方法即可生成相同对象
在构造函数中进行this赋值
1 | class MyClass implements Runnable{ |
可以看到在此类中,构造方法中进行创建线程,并且使用了this对象,在jvm中,对象的构建完成分为三步,划分内存、调用构造函数、指向引用
- 在构造函数中使用了this对象,此时的this对象并没有正确初始化类变量,就会造成一些错误。
- 在构造函数中使用this对象,还让对象没有构造完成就被其他线程看见了
参考
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/16/concurrent/2020-03-16-threadSecurity/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!