zoukankan      html  css  js  c++  java
  • 详解synchronized与Lock的区别与使用

    知识点

    1、线程与进程

      在开始之前先把进程与线程进行区分一下,一个程序最少需要一个进程,而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。以下我们所有讨论的都是建立在线程基础之上。

    2、Thread的几个重要方法

       我们先了解一下Thread的几个重要方法。

        a、start()方法,调用该方法开始执行该线程;

        b、stop()方法,调用该方法强制结束该线程执行;

        c、join方法,调用该方法等待该线程结束。

        d、sleep()方法,调用该方法该线程进入等待。

        e、run()方法,调用该方法直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!

      看到这里,可能有些人就会问啦,那wait()和notify()呢?要注意,其实wait()与notify()方法是Object的方法,不是Thread的方法!同时,wait()与notify()会配合使用,分别表示线程挂起和线程恢复。

      这里还有一个很常见的问题,顺带提一下:wait()与sleep()的区别,简单来说wait()会释放对象锁而sleep()不会释放对象锁。这些问题有很多的资料,不再赘述。

    3、线程状态

     

    线程总共有5大状态,通过上面第二个知识点的介绍,理解起来就简单了。

      新建状态:新建线程对象,并没有调用start()方法之前
      就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。
      运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
      阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
      死亡状态:线程执行结束

    4、锁类型

      可重入锁:在执行对象中所有同步方法不用再次获得锁
      可中断锁:在等待获取锁过程中可中断
      公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
      读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

    synchronized与Lock的区别

    详情对比见下表:

    Lock详解

    如下为Lock接口的部分源码:

    public interface Lock {
       /**
        * Acquires the lock.
        */
       void lock();
     
       /**
        * Acquires the lock unless the current thread is
        * {@linkplain Thread#interrupt interrupted}.
        */
       void lockInterruptibly() throws InterruptedException;
     
       /**
        * Acquires the lock only if it is free at the time of invocation.
        */
       boolean tryLock();
     
       /**
        * Acquires the lock if it is free within the given waiting time and the
        * current thread has not been {@linkplain Thread#interrupt interrupted}.
        */
       boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
     
       /**
        * Releases the lock.
        */
       void unlock();
     
    }

      从Lock接口中我们可以看到主要有5个方法,这些方法的功能从注释中可以看出:

        lock():获取锁,如果锁被暂用则一直等待
        unlock():释放锁
        tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
        tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
        lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
      通过 以上的解释,大致可以解释在上个部分中“锁类型(lockInterruptibly())”,“锁状态(tryLock())”等问题,还有就是前面子所获取的过程我所写的“大致就是可以尝试获得锁,线程可以不会一直等待”用了“可以”的原因。

    下面是Lock一般使用的例子,注意ReentrantLock是Lock接口的实现。

    lock():

    public class LockTest {
        private Lock lock = new ReentrantLock();
    
        private void method(Thread thread) {
            lock.lock();
            try {
                System.out.println(thread.getName() + " has gotten the lock!");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + " has unlocked the lock!");
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            final LockTest test = new LockTest();
    
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    test.method(Thread.currentThread());
                }
            }, "t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    test.method(Thread.currentThread());
                }
            }, "t2");
            t1.start();
            t2.start();
        }
    
    }

    运行结果:

    t1 has gotten the lock!
    t1 has unlocked the lock!
    t2 has gotten the lock!
    t2 has unlocked the lock!

    tryLock():

    public class LockTest {
        private Lock lock = new ReentrantLock();
    
        private void method(Thread thread) {
            /*lock.lock();
            try {
                System.out.println(thread.getName() + " has gotten the lock!");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + " has unlocked the lock!");
                lock.unlock();
            }*/
            if (lock.tryLock()) {
                lock.lock();
                try {
                    System.out.println(thread.getName() + " has gotten the lock!");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(thread.getName() + " has unlocked the lock!");
                    lock.unlock();
                }
            } else {
                System.out.println("I'm "+thread.getName()+". Someone has gotten the lock!");
            }
        }
    
        public static void main(String[] args) {
            LockTest test = new LockTest();
    
            Thread t1 = new Thread(() -> test.method(Thread.currentThread()), "t1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    test.method(Thread.currentThread());
                }
            }, "t2");
            t1.start();
            t2.start();
        }
    }

    运行结果:

    t1 has gotten the lock!
    t1 has unlocked the lock!
    I'm t2. Someone has gotten the lock!

      看到这里相信大家也都会使用如何使用Lock了吧,关于tryLock(long time, TimeUnit unit)和lockInterruptibly()不再赘述。前者主要存在一个等待时间,在测试代码中写入一个等待时间,后者主要是等待中断,会抛出一个中断异常,常用度不高,喜欢探究可以自己深入研究。

      前面比较重提到“公平锁”,在这里可以提一下ReentrantLock对于平衡锁的定义,在源码中有这么两段:

    /**
        * Sync object for non-fair locks
        */
       static final class NonfairSync extends Sync {
           private static final long serialVersionUID = 7316153563782823691L;
           /**
            * Performs lock.  Try immediate barge, backing up to normal
            * acquire on failure.
            */
           final void lock() {
               if (compareAndSetState(0, 1))
                   setExclusiveOwnerThread(Thread.currentThread());
               else
                   acquire(1);
           }
     
           protected final boolean tryAcquire(int acquires) {
               return nonfairTryAcquire(acquires);
           }
       }
     
       /**
        * Sync object for fair locks
        */
       static final class FairSync extends Sync {
           private static final long serialVersionUID = -3000897897090466540L;
     
           final void lock() {
               acquire(1);
           }
     
           /**
            * Fair version of tryAcquire.  Don't grant access unless
            * recursive call or no waiters or is first.
            */
           protected final boolean tryAcquire(int acquires) {
               final Thread current = Thread.currentThread();
               int c = getState();
               if (c == 0) {
                   if (!hasQueuedPredecessors() &&
                       compareAndSetState(0, acquires)) {
                       setExclusiveOwnerThread(current);
                       return true;
                   }
               }
               else if (current == getExclusiveOwnerThread()) {
                   int nextc = c + acquires;
                   if (nextc < 0)
                       throw new Error("Maximum lock count exceeded");
                   setState(nextc);
                   return true;
               }
               return false;
           }
       }

      从以上源码可以看出在Lock中可以自己控制锁是否公平,而且,默认的是非公平锁,以下是ReentrantLock的构造函数:

    public ReentrantLock() {
           sync = new NonfairSync();//默认非公平锁
       }

    补充

    1、两种锁的底层实现方式

      synchronized:我们知道java是用字节码指令来控制程序(这里不包括热点代码编译成机器码)。在字节指令中,存在有synchronized所包含的代码块,那么会形成2段流程的执行。

    public class SyncDemo {
        public void sync(){
            synchronized (SyncDemo.class){
                System.out.println(" ");
            }
        }
    }

    我们点击查看SyncDemo.java的源码SyncDemo.class,可以看到如下:

      如上就是这段代码段字节码指令,没你想的那么难吧。言归正传,我们可以清晰段看到,其实synchronized映射成字节码指令就是增加来两个指令:monitorentermonitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。

      那么有的朋友看到这里就疑惑了,那图上有2个monitorexit呀?因为synchronized锁释放有两种机制,一种就是执行完释放另外一种就是发送异常,虚拟机释放。图中第二个monitorexit就是发生异常时执行的流程,这就是我开头说的“会有2个流程存在“。而且,从图中我们也可以看到在第13行,有一个goto指令,也就是说如果正常运行结束会跳转到19行执行。

      这下,你对synchronized是不是了解的很清晰了呢。接下来我们再聊一聊Lock。

    Lock

      Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock呢底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。具体底层怎么实现,这里不在细述。如果面试问起,就说底层主要靠volatile和CAS操作实现的。

      现在,我要说的是:尽可能去使用synchronized而不要去使用LOCK

      什么概念呢?我和大家打个比方:你叫jdk,你生了一个孩子叫synchronized,后来呢,你领养了一个孩子叫LOCK。起初,LOCK刚来到新家的时候,它很乖,很懂事,各个方面都表现的比synchronized好。你很开心,但是你内心深处又有一点淡淡的忧伤,你不希望你自己亲生的孩子竟然还不如一个领养的孩子乖巧。这个时候,你对亲生的孩子教育更加深刻了,你想证明,你的亲生孩子synchronized并不会比领养的孩子LOCK差。(只是打个比方)那如何教育呢?在jdk1.6~jdk1.7的时候,也就是synchronized16、7岁的时候,你作为爸爸,你给他优化了,具体优化在哪里呢:

    1、线程自旋和适应性自旋

      我们知道,java线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。

      而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

    2、锁消除

      什么叫锁消除呢?就是把不必要的同步在编译阶段进行移除。那么有的小伙伴又迷糊了,我自己写的代码我会不知道这里要不要加锁?我加了锁就是表示这边会有同步呀?并不是这样,这里所说的锁消除并不一定指代是你写的代码的锁消除,我打一个比方:

      在jdk1.5以前,我们的String字符串拼接操作其实底层是StringBuffer来实现的(这个大家可以用我前面介绍的方法,写一个简单的demo,然后查看class文件中的字节码指令就清楚了),而在jdk1.5之后,那么是用StringBuilder来拼接的。我们考虑前面的情况,比如如下代码:

    String str1="qwe";
    String str2="asd";
    String str3=str1+str2;

      底层实现会变成这样:

    StringBuffer sb = new StringBuffer();
    sb.append("qwe");
    sb.append("asd"); 

      我们知道,StringBuffer是一个线程安全的类,也就是说两个append方法都会同步,通过指针逃逸分析(就是变量不会外泄),我们发现在这段代码并不存在线程安全问题,这个时候就会把这个同步锁消除。

    3、锁粗化

      在用synchronized的时候,我们都讲究为了避免大开销,尽量同步代码块要小。那么为什么还要加粗呢?
      我们继续以上面的字符串拼接为例,我们知道在这一段代码中,每一个append都需要同步一次,那么我可以把锁粗化到第一个append和最后一个append(这里不要去纠结前面的锁消除,我只是打个比方)

    4、轻量级锁

    5、偏向锁

     原文参考【Java知音网站

  • 相关阅读:
    CENTOS7下安装REDIS
    Linux 查看端口状态netstat
    Centos7启动zookeeper无法连接2181端口
    企业信息化之路---集成
    Linux启动/停止/重启Mysql数据库的方法
    详解线程池
    详细的RocketMQ说明
    2021面试题准备~~~
    Https原理详解
    es 常用DSL
  • 原文地址:https://www.cnblogs.com/Kevin-ZhangCG/p/10297334.html
Copyright © 2011-2022 走看看