【连载】第2章-2.3加锁机制(不看后悔篇)(内置锁、监视锁、悲
格言:在程序猿界混出点名堂!
《JAVA并发编程实战》解读
【连载】第2章-2.3加锁机制
回顾:上一节主要介绍原子性,这一节来说说加锁机制。
先来看一下上一节提到因数分解的例子,这次不就Servlet的访问次数做讨论,解决一个工作中经常遇到的问题:经常我们在做一些计算的工作或者访问磁盘存储(比如数据库)是比较消耗计算资源的,因此我们往往采用将结果缓存,下次直接访问缓存结果,看下面这个例子,认真读一下看有什么问题:
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
// 上一次计算的数字
private final AtomicReference<BigInteger> lastNumber =
new AtomicReference<BigInteger> ();
// 上一次计算数字的分解因子
private final AtomicReference<BigInteger[]> lastNumer =
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);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, lastFactors.get());
}
}
}
存在两个问题:
1.在判断是否计算是否存在缓存的时候存在一个延迟初始化竞态条件,跟以前章节讲到的单例类似。
2.第二点会导致计算结果出错。更新number和分解因子的时候也存在竞态条件,俩个本是一个原子操作,试想:A线程更新了lastNumer,没有更新lastFactors,B线程获取到lastNumber返回null,跟预想的结果不一致。因此必须保证俩者更新的原子性。
这里埋下一颗
彩蛋
:是否可以用HashMap
替换lastNumer和lastFactors,大家可以在讨论区留言。
内置锁(也称监视锁)
每个Java对象都可以作为一个实现同步的锁,这些锁成为
内置锁
或者监视锁
。至于为什么叫监视锁,可以百度一下synchronized的原理。
synchronized (lock){
// 访问或者修改由锁保护的共享状态
}
题外彩蛋:
由于synchronized是一进入代码块就加锁,阻塞其他线程进入,它是一种悲观锁
,相对悲观锁而言,还有一种叫乐观锁
,CAS(Compare And Swap)就是一个例子,比如Atomic下面普遍使用,但个人认为synchronized引入偏向锁
、轻量级锁
、重量级锁
也未必它有多悲观😄。
因此开篇的代码可以使用synchronized保证复合操作的原子性:
public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){
// 如果缓存过,直接返回结果
encodeIntoResponse(resp, lastFactors.get());
}else{
// 没有缓存才计算
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, lastFactors.get());
}
题外彩蛋:
但是,大家试想这样代码的性能会高吗?用户的操作体验会良好吗?假设一个因数分解需要1-2秒,其他复杂计算可能远不止如此,那么如果多个客户端请求这个Servlet必须依次等待排队,未排上队的就可能导致界面一直处于等待。我们后面章节会解读。
重入锁
如果一个线程获取一个已经由自己持有的锁,那么这个请求就会成功。这就是
重入锁
。重入锁的锁操作粒度是线程。
题外彩蛋:
它的原理就是同一个线程访问这个锁,获取到锁后,则计数值+1,当释放锁计数值-1,当计数值为0时,这个锁就释放。synchronized是重入锁,但源码不是很好看,但是,ReentrantLock也是重入锁,它的计数值用state
来存储。感兴趣的同学可以查看ReentrantLock的源码。
下面我们看下重入锁的例子,感受一下为什么要可重入。
public class Widget{
public synchronized void doSomething(){
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
super.doSomething();
}
}
父类和子类都有doSomething方法,调用前都会获取Widget上的锁,如果锁不是可重入的,那么supe.doSomething()将阻塞,导致死锁。
题外彩蛋:
当然,除了以上这个例子外,同一对象内的同步方法调用、同步方法递归等都要可重入,否则会导致死锁。
知识点
1.了解synchronized的关键字和锁定的对象。
2.课外了解和分清:内置锁、重入锁、监视锁、悲观锁、乐观锁、偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、共享锁、独占锁。
思考🤔
public void SyncObject{
public synchronized void m1(){
}
// 静态方法
public static synchronized void m2(){
}
}
思考一下
:m1和m2互斥吗?原因是什么?
喜欢连载可关注
简书
或者微信公众号
:
简书专题:Java并发编程实战-可爱猪猪解读
https://www.jianshu.com/c/ac717321a386
微信公众号:逗哥聊IT
。