【Java并发】JAVA并发编程实战-读书笔记2
来源:互联网 发布:大学软件工程项目 编辑:程序博客网 时间:2024/06/05 18:52
当多个线程访问同一个类时,如果不考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
一个无状态的Servlet
public void service(ServlerRequest req,ServletResponse resp){ BigInteger i=extractFromRequest(req); BigInteger[] factors=factor(i); encodeIntoResponse(resp,factors);}
上面的这个servlet是线程安全的。
无状态对象永远是线程安全的。
如果我们在上面的方法中加入一个用于记录访问计数的状态变量count,则他就不是线程安全的。
public ExpensiveObject getInstance(){ if(instance==null){ instance=new ExpensiveObject(); } return instance;}
上面的代码不是线程安全的。
比如线程A和线程B同时判断instance为空时,就会出现问题。
如果自增操作是一个原子操作,那么就可以修复问题。
private final AtomicLong count=new AtomicLong(0); public long getCount(){ return count.get(); } public void service(ServlerRequest req,ServletResponse resp){ BigInteger i=extractFromRequest(req); BigInteger[] factors=factor(i); count.incrementAndGet(); encodeIntoResponse(resp,factors);}上面的方法是线程安全的,里面引入了java.util.concurrent.atmoic中的原子变量类。这些类实现了数字和对象引用的原子状态转换。
当向无状态的类中加入唯一的状态元素,而这个状态完全被线程安全的对象所管理,那么新的类仍然是线程安全的。
比如我们考虑这样的情形,缓存最新的计算结果,以应对两个连续的客户请求相同的数字进行因数分解。
private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors=new AtomicReference<BigInteger[]>(); public void service(ServletRequest req,ServletResponse resp){ BigInteger i=extractFromRequest(req); if(i.equals(lastNumber.get())){ encodeIntoResponse(resp,lastFactors.get()); }else{ BigInteger[] factors=factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp,factors); }}
很不幸,上面的方法是不正确的,其中依然存在竞争条件。那就是我们没有在里面保证同时更新lastNumber和lastFactors两个变量。
为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
可以使用synchronized修饰方法,但是会严重影响性能。
当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞,然而内部锁是可重入的,因此线程在试图获得它自己占有的锁时,请求会成功。
重进入实现方式是通过一个请求计数和一个占有他的线程,当计数为0时则认为未被占用,每次请求,计数加一,每次退出同步块,计数减一。直到为0,锁被释放。
public class Father{ public synchronized void dosome(){ }}public class Child extends Father{ public synchronized void dosome(){ System.out.println(“call dosome”); super.dosome(); }}
如果没有重进入,上面的代码会锁死。
一种常见的锁规则是在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,很多的线程安全类都是这个模式。
对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。
Vector仅仅同步他的每个方法,并不能确保在Vector上执行的复合操作是原子的。
if(!vector.contains(element)){ vector.add(element);}
虽然contains和add都是原子的,但是缺少即加入的过程中是存在竞争条件的。
public class CachedFactorizer implements Servlet{ private BIgInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits(){ return hits; } public synchronized double getCacheHistRatio(){ return (double)cacheHits/(double)hits; } public void service(ServletRequest req,ServletResponse resp){ BigInteger i=extractFromRequest(req); BigInteger[] factors=null; synchronized(this){ ++hits; if(i.equals(lastNumber)){ ++cacheHits; factors=lastFactors.clone(); } } if(factors==null){ factors=factor(i); synchronized(this){ lastNumber=i; lastFactors=factors.clone(); } } encodeIntoResponse(resp,factors); }}
上面的例子中没有使用AtmoicLong来计数,而是使用了long,但是在这里是安全的。因为我们已经使用synchronized的关键字构造了原子操作。
请求与释放锁的操作也是需要开销的,所以synchronized块分解的过于琐碎也是不合适的。
有些耗时的计算或操作,比如网络或控制台IO,难以快速完成,执行这些操作期间不要占有锁。
通常,不能保证读线程及时地读取其他线程写入的值,甚至可以说根本不可能。为了确保跨线程写入的内存可见性,必须使用同步机制。
public class NoVisibility{ private static boolean ready; private static int number; private static class ReadThread extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); } } pulbic static void main(String[] args){ new ReaderThread().start(); number=42; ready=true; }}
看上去,程序会输出42,但实际情况可能是程序不会退出或者输出的结果是0。
出现这种情况的原因是,number在还没有赋值之前,ready就已经被赋值为true了,并且对读线程可见,这就是重排序现象。
在单个线程中,只要重排序不会对结果产生影响,那么就不能保证其中的操作一定按照程序书写的书序执行——即使重排序对其他线程会产生明显的影响。
在没有同步的情况下,编译器、处理器、运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些必然发生在内存中的动作时,你总是会判断错误。
上面的程序中,读线程看到了一个过期的变量。
public class MutableInteger{ private int value; public int get(){ return value; } public void set(int value){ this.value=value; }}
上面的程序中,就会出现获得过期数据的可能性,但是仅仅使用synchronized修饰set是不够的,调用get的线程仍然能够看见过期值。所以两个方法都需要修饰。
当一个线程在没有同步的情况下读取变量,可能会得到一个过期值,但是至少他可以看到某个线程设定的一个真实数值,而不是一个凭空而来的值。称为最低限的安全性。其适用于所有的变量,除一例外:没有声明为volatile的64位数值的变量double和long。Java存储模型要求获取和存储操作都是原子的,但是对于非volatile的long和double变量,JVM允许将64位的读或写划分为两个32位的操作。如果读和写发生在不同的线程,就会出现得到一个值的高32位和另一个值的低32位。
锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。
只有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生。
volatile boolean asleep;...while(!asleep){ countSomeSheep();}
注意,volatile的语义不足以使得自增操作原子化。
加锁可以保证可见性和原子性;volatile变量只能保证可见性。
只有满足下面所有的标准后,才能使用volatile变量。
1,写入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值。
2,变量不需要与其他的状态变量共同参与不变约束。
3,而且,访问变量时,没有其他的原因需要加锁。
- 《Java并发编程实战》读书笔记
- 《Java并发编程实战》读书笔记
- java并发编程实战-读书笔记
- 《Java并发编程实战》读书笔记
- 《java并发编程实战》读书笔记
- 《Java并发编程实战》读书笔记
- 读书笔记-《Java并发编程实战》
- java并发编程实战读书笔记
- java并发编程实战读书笔记
- Java并发编程实战读书笔记
- 【Java并发】JAVA并发编程实战-读书笔记2
- 《JAVA并发编程实战---读书笔记2》
- java并发实战读书笔记
- 【Java并发】JAVA并发编程实战-读书笔记1
- 【Java并发】JAVA并发编程实战-读书笔记3
- 【Java并发】JAVA并发编程实战-读书笔记4
- 【Java并发】JAVA并发编程实战-读书笔记5
- 【Java并发】JAVA并发编程实战-读书笔记6
- 分解让复杂问题简单化-面试题27-二叉搜索树与双向链表
- 293_AndroidStudio常用快捷键
- SVN教程(一)
- 【NOIP 2016】斗地主
- Intellij下进行junit测试
- 【Java并发】JAVA并发编程实战-读书笔记2
- 第65篇Chrome扩展蓝牙及USB开发(二)
- Android 中的线程应用_项海涛
- 294_AndroidStudio下的dependencies
- csdn首文
- 小结(多图预警)
- MFC 列表(List Control)中删除项
- 开篇之作:变化,唯有变化是不变的吗?
- 此时此刻