• 从synchronized和lock区别入手聊聊java锁机制


      写这篇文章之前,我去百度了一下啥叫锁,百度百科上写道:置于可启闭的器物上,以钥匙或暗码开启。确实我们一般理解的锁就是门锁,密码锁,但是在计算机科学中,锁又是啥,说实话,这个问题我也思考了很久,也没法很好的用一两句话就让人听得明白,也不想有人看到我的文章,然后将我的结论当作答案,我觉得最好的答案还是在探索的过程中得到的,接下来我们就好好探索一番。

      作为一名java程序员,最开始接触到的锁就是synchronized,书本上是这么写的,老师也是这么说的,至于为啥叫锁,可能也没多少人真的去思考过。不知道有没有同学和我一样,经历过只知道用synchronized,后来逐渐的了解ReentrantLock,读写锁,然后又了解了aqs,后来通过百度google,看一些博客(这个我要吐槽一下,在学习过程中遇到过很多文章写的有问题的,反而误导了我),后面看了看synchronized的源码,最后对比synchronized和ReentrantLock才加深了对锁的一些认知(说实话,作为一个刚毕业3年的非科班出身码农,我也不敢保证自己写的就一定对,算是学习过程中的一些感悟吧),那接下来我就按照学习顺序来逐渐展开。

      先来一段简单的synchronized使用代码:

    public static void main(String[] args) {
            String s = new String();
            synchronized (s) {
                TestJni jni = new TestJni();
                jni.jniHello();
            }
        }

    上面代码做的事情很简单,如下图所示,有A B C D E多个线程同时来到synchronized包含的代码块,A先一步进来了,那么BCDE都得等,等我A执行完他们才能进来执行。

            

      synchronized用起来确实很简单,我们也可以放在方法上,但是其本质还是锁的对象,这个我们后面分析源码一看就知道了。

      随着开发时间越长,synchronized在有些复杂场景下(如需要可中断,可控制时间抢锁,需要多个等待队列分别控制,读写锁等场景的时候)无法满足我们的需求,那么就要用到Lock,下面我们先介绍一下Lock的简单使用:

            Lock lock = new ReentrantLock();
            lock.lock();  
            try {
                System.out.println("线程:"+Thread.currentThread().getName()+ " 进来啦");
            }finally {
               lock.unlock();
            }

     上面是一种最简单的使用,和synchronized作用是一样的,不过加锁之后必须要解锁,且必须紧跟try - finally块解锁,使用起来稍微复杂一点,容易出错。

    我们再介绍一种可中断的使用方式:

    public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                try {
                    lock.lockInterruptibly();
                try {
                    testLock();
                } finally {
                    lock.unlock();
                }
                } catch (Exception e){
                }
            });
            thread.start();
            thread.interrupt();
        }
    
        public  static void testLock(){
            condition.signalAll();
            System.out.println("线程:"+Thread.currentThread().getName()+ " 进来啦");
        }

    这种方式呢,在拿锁被park住了,如果刚好这时候被打断了,就会响应打断退出抢锁并抛出异常,至于捕获到异常开发者怎么做,那就得根据业务来分别处理了。

    而像可控制时间的其实就要稍微复杂一点,先看一下synchronized中的使用:

    static TestHash s = new TestHash();
        public static void main(String[] args) {
            Thread thread1 = new Thread(()->{
                testLock();
            });
            Thread thread2 = new Thread(()->{
                synchronized (s) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                    }
                    s.notify();
                    testLock();
                }
            });
            thread1.start();
            thread2.start();
        }
    
        public static void testLock(){
            synchronized (s) {
                System.out.println("线程:"+Thread.currentThread().getName()+ " 进来啦");
                try {
                    s.wait();
                    System.out.println("线程:"+Thread.currentThread().getName()+ " 叫醒啦");
                } catch (InterruptedException e) {
                    System.out.println("抛异常啦");
                }
            }
        }

    这个例子看着要比前面几个复杂一点,首先thread1会进入testLock方法,并拿到锁,thread2等了1秒叫醒thread1(这里就是简单的wait/notify的使用),然后在拿到锁的情况下,再次进入testLock方法并拿到锁,由于没人唤醒了,会一直卡在这里(这里证明了synchronized的可重入),结果我就不贴了,感兴趣的可以拿着代码去试。

    而ReentrantLock的使用也差不多,就是提前用lock去new一个Condition:

    static ReentrantLock lock = new ReentrantLock();
        static Condition condition = lock.newCondition();
        public static void main(String[] args) {
            Thread thread1 = new Thread(()->{
                testWaitSingal();
            },"thread1");
            Thread thread2 = new Thread(()->{
                lock.lock();
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        condition.signal();
                        testWaitSingal();
                    } catch (InterruptedException e) {
                    }finally {
                    lock.unlock();
                }
            },"thread2");
            thread1.start();
            thread2.start();
        }
    
        public static void testWaitSingal(){
            lock.lock();
            try {
                System.out.println("线程:"+Thread.currentThread().getName()+ " 进来啦");
                condition.await();
                System.out.println("线程:"+Thread.currentThread().getName()+ " 叫醒啦");
            } catch (InterruptedException e) {
                System.out.println("抛异常啦");
            }finally {
                lock.unlock();
            }
        }

      可以看到两种用法基本上是一致的,也就是将synchronized换成了lock,wait换成await,notify换成singal,

    总结:

      基本上我们平时用到的synchronized关键字的用法也就这些,但lock锁不一样,它还支持如上述的中断,更复杂的读写锁,还可以在aqs的基础上衍生出更多,如countDownLatch,cyclicBarrier等,可以支持我们做更多,但是不是lock就可以完全替代synchronized了呢,其实synchronized也有自己的优点,简单,不易出错,性能也不比lock差(有的书上写道synchronized性能比lock好,但其实就算好也不会好太多,对于我们来说,基本上可以忽略),真要说选哪个,我的建议是优先选synchronized,如果有特殊业务特殊需求synchronized无法满足,那当然是要用lock,不过,一定要记得释放锁哦。

      本来打算结合reentrant和synchronized直接串起来讲的,但是确实有点多,这一篇就当作是后面的引子吧。

      

  • 相关阅读:
    SQL优化最干活总结 (建议收藏)!!
    45个 GIT 经典操作场景,专治不会合代码
    Hadoop+HBase+ZooKeeper分布式集群环境搭建
    Spring Security默认登录页面代码位于哪里?
    [转]vuevuecli不同版本的安装及对比
    Spring Data JPA中使用Example进行动态查询
    [转]vue 项目npm install 报错 npm ERR! enoent undefined lsremote h t ssh://git@github.com/soheelee7/Sq
    [转]fatal: remote error: The unauthenticated git protocol on port 9418 is no longer support问题解决
    [转]fatal: unable to access ‘https://github.com/nhn/raphael.git/‘: OpenSSL SSL_connect: Connection was
    [转]解决Spring Data Jpa 实体类自动创建数据库表失败问题
  • 原文地址:https://www.cnblogs.com/gmt-hao/p/14110722.html
Copyright © 2020-2023  润新知