《java并发编程实战》读书笔记1--线程安全性,内置锁,重入,状态
什么是线程安全?
当多个线程访问某个类时,不管这些的线程的执行顺序如何,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
哈哈书上的解释,还是翻译过来的,看了半天还是觉得有点奇怪。比如说 “类都能表现出正确的行为” 是毛线意思?在网上搜了一番 "线程安全就是说多线程访问同一代码,不会产生不确定的结果" 这样反而跟容易理解,果然读书要么读原版中文书要么读原版英文书,看英文的翻译版真的是蛋疼无比。说到这里,我以前貌似把多线程及线程安全一个根本性问题搞混淆了:所谓的多线程是指多个线程跑同一段代码,所以线程安全的概念是针对于这段代码而言的而不是针对线程。
一个无状态的Servlet
//程序清单2-1 一个无状态的Servelet
@ThreadSafe
public class StatelessFactorizer implements Servlet{
public void service(ServlerRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
一个基于Servlet的因数分解服务,从请求中提取出数值进行因数分解,然后将结果封装到该Servlet的响应中。这里要理解的是什么是这本书上说道的 “无状态性” ?书上给的解释是: 它即不包含任何域,也不包含任何对其他类中域的引用,计算过程中的临时状态仅存在于线程栈上的局部变量中。这里的域是什么意思?全局变量? 怎么来理解这句话呢?大概意思就是对于多线程而言,一个类会同时有多个线程来访问,使用里面的方法,那么这个类的全局变量对于这些线程来说实际上是同一个,只有一份来供这些线程操作,那么线程不安全的问题就因此来了呗。而对于局部变量,不同的线程执行同一个类中的方法时,这个方法中的局部变量对于不同的线程来说都是单独的一份,不存在数据共享的问题,所以无状态对象一定是线程安全的。、
原子性
这个简单,就是说一个操作是不可分割的,比如读操作和写操作,而对于下面的递增操作++count就不是原子操作,其操作序列是:读取-修改-写入。
//程序清单2-2 在吗没有同步的情况下统计已处理请求数量的Servlet
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet{
private long count=0;
public long getCount(){return count;}
public void service(ServlerRequest req, ServletResponse resp){
BigInteger i=extractFromReqest(req);
BigInteger[] factors=factor(i);
++count;
encodeIntoResponse(resp,factors);
}
}
竞态条件
由于不恰当的执行时序而出现的不正确的结果。说得通俗一点,就是线程A 需要判断一个变量的状态,然后根据这个变量的状态来执行某个操作。在执行这个操作之前,这个变量的状态可能会被其他线程使用,修改。所以是 "竞争状态下的条件"
示例:延迟初始化中的竞态条件
目的是将对象初始化操作推迟到实际使用时才进行,同时要确保只被初始化一次。
//程序清单2-3 延迟初始化中的竞态条件
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject instace = null; public ExpensiveObject getInstance(){
if (instace == null) //程序的执行需要判断这个状态,运行的结果的正确与否取决于这个状态的判断,即“先判断后执行”
instace = new ExpensiveObject();
return instance;
}
}
复合操作
要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。为了实现这种思路,必须将对同一个状态的操作以原子的方式去执行,要么全部执行完,要么完全不执行。即将上面的 “先检查后执行” 和 “读取-修改-写入” 等复合操作变为原子操作。
//程序清单2-4 使用AtomicLong类型的变量来统计已处理请求的数量
@ThreadSafe
public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0); //初始化AtomicLong实例 public long getCount(){return count;} public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors=factor(i);
count.incrementAndGet(); //注意这个方法的使用
encodeIntoResponse(resp, factors);
}
}
在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。使用线程安全对象(如AtomicLong)来管理类的状态(这里类的状态好像指的就是类的全局变量啊,即在多线程操作时能被这些线程共同使用的变量叫做类的状态变量?)
加锁机制
单个状态变量可以通过线程安全对象来管理Servlet的状态以维护Servlet的线程安全性。但如果Servlet中有多个状态变量,那么是否只需添加更多的线程安全状态变量就足够了?答案肯定是否定的了。
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger> lastFacotrs
= new AtomicReference<BigInteger[]>();
/**
* 将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次
* 的计算结果。需要保存两个状态: 最近执行因数分解的数值以及分解结果
*/
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get()))
encodeIntoResponse(resp, factors);
else{
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
关于AtomicReference和AtomicLong可以学习下这几篇博文:Java多线程系列--“JUC原子类”04之 AtomicReference原子类 Java多线程系列--“JUC原子类”02之 AtomicLong原子类
上面的方法是不正确的,原因很好理解,原子状态变量变量只能确保对这个状态的操作是原子的,当有多个状态变量时,无法保证对多个状态的变量的操作是原子的。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁
java提供的内置锁机制是同步代码块,包括两部分:一个作为锁的对象的引用,一个作为有这个锁保护的代码块。以关键子synchronized来修饰的方法是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

现在让我们来改进下之前的因数分解Servlet
//程序清单2-6 这个Servlet能正确地缓存最新的计算结果,但并发性却非常糟糕
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors);
else{
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
} }
}
重入
内置锁是可重入的,意思就是如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功而不会阻塞。重入的一种实现方法是:为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时就认为这个锁没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。
//程序清单2-7 如果内置锁不是可重入的, 那么这段代码将发生死锁
publlic class Widget{
public synchronized void doSomething(){
.....
}
} /**
* 子类改写了父类的synchronized方法,然后调用父类的方法,子类和父类中的doSomething()方法在执行前
* 都会获取Widget上的锁,如果内置锁不可重入则会发生死锁
*/
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
System.put.println(toString()+": calling doSomething";
super.doSomething();
}
}
用锁来保护状态
活跃性与性能
程序2-6的UnsafeCachingFactorizer的同步方式使得并发性十分糟糕。

可以通过缩小同步代码块的作用范围,来确保并发性和线程安全性。
//程序清单2-8 缓存最近执行因数分解的数值及其计算结果的Servlet
@ThreadSafe
publlic class CachedFactorizer implements Servlet{
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
/**
*引入“命中计数器”和“缓存命中计数器”
*/
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits; public synchronized long getHits(){return hits;}
public synchronized double getCacheHitRatio(){
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);
}
}
注:当执行时间较长的计算或操作时(如:网络, I/O 等),一定不要持有锁
《java并发编程实战》读书笔记1--线程安全性,内置锁,重入,状态的更多相关文章
- 读书笔记-----Java并发编程实战(一)线程安全性
线程安全类:在线程安全类中封装了必要的同步机制,客户端无须进一步采取同步措施 示例:一个无状态的Servlet @ThreadSafe public class StatelessFactorizer ...
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- java并发编程实战:第二章----线程安全性
一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问 ...
- JAVA并发编程实战---第二章:线程安全性
对象的状态是指存储在状态变量中的数据.对象的状态可能包括其他依赖对象的域.例如HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry对象中.对象的状态中包含了任何可能影响其外 ...
- Java并发编程实战 第2章 线程安全性
编写线程安全的 代码,核心在与对共享的和可变的对象的状态的访问. 如果多个线程访问一个可变的对象时没有使用同步,那么就会出现错误.在这种情况下,有3中方式可以修复这个问题: 不在线程之间共享该状态变量 ...
- 《Java并发编程实战》第二章 线程安全性 读书笔记
一.什么是线程安全性 编写线程安全的代码 核心在于要对状态訪问操作进行管理. 共享,可变的状态的訪问 - 前者表示多个线程訪问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与 ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- 《java并发编程实战》笔记
<java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为: Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...
- Java 内置锁 重入问题
阅读<Java并发编程实战>,遇到了一个问题 代码如下 /** * @auther draymonder */ public class Widget { public synchroni ...
- Java并发编程实践读书笔记(1)线程安全性和对象的共享
2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...
随机推荐
- nodejs获取文件信息,判断是文件或目录
var stat = fs.statSync(filename); console.log(stat) 输出 { dev: 0, ino: 0, mode: 33206, nlink: 1, uid: ...
- redux connect的浅比较说明
redux的connect方法是一个高阶组件,对包装的组件会在ShouldComponentUpdate中实现一个默认的浅比较. connect形式如下: connect([mapStateToPro ...
- [BZOJ1131/POI2008]Sta树的深度
Description 给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大 Input 给出一个数字N,代表有N个点.N<=1000000 下面N-1条边. Output ...
- hdu 5620
KK's Steel Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Total ...
- Javascript基本代码
简单的了解了javascript 的基本代码,感觉和c#中的语句差不多. <!DOCTYPE html> <html xmlns="http://www.w3.org/19 ...
- 单例 ------ JAVA实现
单例:只能实例化一个对象,使用场景比如打印机. 最推荐的是采用饿汉式:双重校验锁用到了大量的语法,不能保证这些语法在所用场合一定没问题,所以不是很推荐:总之简单的才是最好的,就饿汉式!!! C++ 创 ...
- Itext2.0.8 和freemarker导出pdf
这个是跟上一篇写的freemarker导出word是一块的.但是关联性不是很大.由于本人技术有限本篇导出也是根据网上大家的做出的demo混合而成.有不足的地方请大家指出.好改正,使以后看到的freem ...
- MFC中用正则表达式进行有效性验证
转载自:http://blog.csdn.net/jinhill/article/details/5928993 正则表达式最实用的一个地方是验证用户输入.它可以轻松验证邮编.电话号码.信用卡号码-- ...
- BIRT-商务智能报表工具开发案例指南
BIRT 报表 脚本 开发 数据库 http://www.iteye.com/topic/1128694 打算近期出版一本全面介绍BIRT使用的书籍,能够帮助大家全面了解BIRT的方方面面,用丰富的案 ...
- spring mvc 注解详解
1.@Controller 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ...