前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >详解synchronized与Lock的区别与使用

详解synchronized与Lock的区别与使用

作者头像
Kevin_Zhang
发布2019-02-27 16:39:39
3.4K0
发布2019-02-27 16:39:39
举报
文章被收录于专栏:Kevin-ZhangCGKevin-ZhangCG

知识点

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接口的部分源码:

代码语言:javascript
复制
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():

代码语言:javascript
复制
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();
    }

}

运行结果:

代码语言:javascript
复制
t1 has gotten the lock!
t1 has unlocked the lock!
t2 has gotten the lock!
t2 has unlocked the lock!

tryLock():

代码语言:javascript
复制
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();
    }
}

运行结果:

代码语言:javascript
复制
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对于平衡锁的定义,在源码中有这么两段:

代码语言:javascript
复制
/**
    * 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的构造函数:

代码语言:javascript
复制
public ReentrantLock() {
       sync = new NonfairSync();//默认非公平锁
   }

补充

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

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

代码语言:javascript
复制
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来拼接的。我们考虑前面的情况,比如如下代码:

代码语言:javascript
复制
String str1="qwe";
String str2="asd";
String str3=str1+str2;

底层实现会变成这样:

代码语言:javascript
复制
StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd"); 

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

3、锁粗化

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

4、轻量级锁

5、偏向锁

 原文参考【Java知音网站

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-01-28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 知识点
    • 1、线程与进程
      • 2、Thread的几个重要方法
        • 3、线程状态
          • 4、锁类型
          • synchronized与Lock的区别
            • 详情对比见下表:
            • Lock详解
              • 补充
                • 1、两种锁的底层实现方式
              • Lock
                • 1、线程自旋和适应性自旋
                • 2、锁消除
                • 3、锁粗化
                • 4、轻量级锁
                • 5、偏向锁
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档