想说线程安全是程序员必备技能之一。总是不能够好好的学习研究一下这个东西。从今天开始吧。有一个起点。
定义
线程安全即对状态访问变量的管理,状态访问变量一般指的是共享的和可变的状态变量。
意义
旨在程序利用多线程技术提高整体运行效率的同时提供数据正确性的保障
常见手段
synchronized:对象加锁,代码块排他式保护
volatile:将变量移到主存,即时生效。
错误修复手段
1. 不在线程之间共享变量
2. 将共享变量修改成不可变
3. 在访问共享变量时添加同步
编码需要的概念
1. synchronized同步
2. volatile变量
3. 显式锁
4. 原子变量
5. 原子操作
6. 线程安全类
7. 线程安全程序
8. Race condition竞态条件
9. 复合操作
10. 线程粒度
11. 重入
什么是线程安全?如何判断?
通常来讲,某个类的行为跟规范完全一致就是线程安全的。只要类的功能跟预期的,始终是一致,那么这个类就是安全的。
为什么说无状态对象的一定是线程安全的?
无状态对象,它不包含任何域,不包含任何其他类中的域引用。那么它在执行过程中,所有的临时状态都保存在线程栈上的局部变量中,也只能由正在执行的线程访问。那么这样的访问就不会影响到任何其他线程。也就不存在线程安全问题了。。。。
为什么要考虑原子性
因为程序变量的操作一般设计3个操作。读取---修改---写入。这样的操作在多线程状态下,会出现Race condition问题。读取,修改,写入,这三个状态会被多个线程同时操作,数据的变化不能实时体现出来。数据最终一定会出错。所以,这个变量操作要考虑原子性。
什么是race condition
在观察一个结果后,开始对这个结果惊醒操作,在这个过程中,之前观察的结果产生了变化。这就是race condition
单例模式是否是thread safe?答案是否定的
例子
public class Singletone{
private Singletone mInstance = null;
public Singletone getInstance(){
if(mInstance == null){
mInstance = new Singletone();
}
return mInstance;
}
}
看起来很正常。但是为什么是不安全的。因为在new Singletone()时,你无法判断这个需要多少时间,那么,就存在会同时new出来多个Singletone的情况。
再说一个,大部分人都知道。多线程对一个long 进行++操作。如果没有同步就会出现线程安全问题。
那么用原子性的理念的话,应该怎么做?
so easy。将这个long值改成AtomicLong,利用jdk中自带的原子类,来弥补这个操作。
什么是复合操作
当我们执行两个原子操作的时候,也是线程不安全的。为什么呢?因为在执行这两个原子操作的时候,也会存在时间差以及操作本身的用时。这就是不能排除的风险。
要规避复合操作的风险,怎么做呢。在关键变量操作上加锁!
加锁之后,就可以将复合操作看成一个原子操作。
所以,复合 原子都是可以转变的。复合加锁就可以是原子。
重入跟粒度
当一个线程请求其他线程持有的锁,发出请求的线程就会阻塞。但是如果这个发动请求的线程已经获得了这个锁。那么线程就可以继续操作,这就是重入。
当线程请求一个未被持有的锁,jvm就会记下锁的持有者,将锁计数器+1,若同一线程再次请求,那么计数器再+1,若线程退出同步代码块,那么计数器就重置成0.这个锁就会被释放。同时,计数器不为0时,其他线程也就无法获得这个锁。
java的线程具有重入特性,那么从这个角度去看,锁操作的粒度就是 线程。而不是一个请求(调用)
有个差别就是 linux pthread的粒度是调用。
加锁注意事项
1. 尽量减少加锁代码块的量。
2. 同步机制够用就好。没必要重复加锁。这会给程序带来混乱
3. 若同步的代码块不能够快速完成。那就不要给锁。这将阻塞!这点很重要