JavaJava并发编程实战-可爱猪猪解读Java-解读

【连载】第2章-2.3加锁机制(不看后悔篇)(内置锁、监视锁、悲

2019-08-21  本文已影响0人  可爱猪猪

格言:在程序猿界混出点名堂!

《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

上一篇下一篇

猜你喜欢

热点阅读