美文网首页JavaJava并发编程实战-可爱猪猪解读Java-解读
【连载】第2章-2.3加锁机制(不看后悔篇)(内置锁、监视锁、悲

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

作者: 可爱猪猪 | 来源:发表于2019-08-21 21:52 被阅读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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/yqxwsctx.html