要编写线程安全的代码其核心在于要对状态访问操作进行管理,特别是对共享的(shared)和可变的(Mutable)状态的访问。
-- 《Java并发编程实战》
知识储备:非状态、竞争条件、原子操作
1、多线程访问同一个可变的状态变量,怎么解决?
- 不在线程之前共享该状态变量
- 将这个可变的状态变量改为不变的
- 在访问状态变量使用同步
2、什么是线程安全性?
当多个线程访问某个类时,这个类始终表现出正确的行为。这个类就是线程安全的。
3、什么是无状态?
无状态对象一定是线程安全的,就是没有包含任何域,也不包括任何对其他类中域的引用。大多数Servlet都是无状态的。
image.png
非原子性操作会造成 竞态条件:读取-修改-写入
还有一种 延迟初始化(单例模式)的竞态条件:先检查后执行
这些计算的正确性取决于多个线程的交替执行时序,就会发生竞态条件。
private SingleObject single = null;
public getInstance(){
if (single == null){
single = new SingleObject();
}
}
4、竞态条件在并发情况永远是错误的吗?
并不是总是产生错误,还是需要某种不恰当的执行时序。
5、复合操作和原子操作
假设有2个操作A和B,对于执行A的线程来说,另一个执行B的线程要么全部执行完,要么完全不执行,那么A和B对彼此来说就是原子操作,反之是复合操作。
eg. int i = 0;i++ 就是一种读取-修改-写入的复合操作
eg.AtomicLong i = new AtomicLong (0); i.incrementAndGet();就是原子操作
AtomicLong 是线程安全的对象,在实际情况中我们尽量使用这种线程安全的对象来管理类的状态。
加锁机制
1、原子操作无法解决的问题
如果一个类中有更多的状态变量时,是否只需要加入线程安全状态变量就足够了?
不是,依然存在竞态条件
//2个原子状态量
public AtomicInteger a = new AtomicInteger(0);
public AtomicInteger b = new AtomicInteger(0);
public void service(){
if(...){
a.get(); //1
}else{
a.incrementAndGet();//2
b.incrementAndGet();
}
}
虽然状态变量都是原子操作,假设线程小红刚跑到1位置,正要获取a的值,此时线程小明跑到2位置并将a的值累加了,那么小红获取的值就是个不正确状态的值!
2、内置锁(互斥锁) - 同步代码块(Synchronized)
synchronized(lock){
//访问或修改 由锁保护的共享状态
}
lock:以class为锁;代码块组成一组不可分割的原子单元。
3、锁重入
某个线程请求一个由其他线程持有的锁时,当前线程会被阻塞;
但是试图获取一个由自己持有的锁,这个请求就会成功,否则造成死锁。
“重入”意味着锁的操作粒度是“线程”,而不是“调用”。
4、一种加锁的约定
将所有的可变状态都封装在对象内部,通过对象的内置锁对其进行同步,就不会出现并发访问了。
5、活跃性和性能
synchronized会造成一种串行阻塞性的不良并发,性能低;
怎么缓解?
掌握在简单性(最简单的就是对整个方法进行同步)与并发性(对尽可能短的代码块进行同步)之间的平衡,既不能太简单省事,也不能加太多的同步代码块,分的太短太细。
//这种同步代码短,但是在for循环中锁次数较多 影响性能
for(){
synchronized(this){
}
}
//这样加比较好,所以要在这两者间平衡
synchronized(this){
for(){}
}
学习中,有不足之处欢迎指出!感谢
网友评论