synchronized底层原理

               多线程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

        Mark word 记录了对象和锁有关的信息,当某个对象被 synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚 拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。

        Mark Word 里面存储的数据会随着锁标志位的变化而变化, Mark Word 可能变化为存储以下 5 中情况:

       

 

        synchronized 有三种方式来加锁,分别是

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

       synchronized 锁的升级

         偏向锁获取和撤销
  1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态(biased_lock=1、且 ThreadId 为空)。

  2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的ID写入到 MarkWord。

             a) 如果 cas 成功,那么 markword 就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块。
             b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行。
         3.如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID。

            a) 如果相等,不需要再次获得锁,可直接执行同步代码 块

            b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁。

       偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

       原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程

  1. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级 为轻量级锁后继续执行同步代码块

        绝大部分情况下一定会存在多个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过 jvm 参数 UseBiasedLocking 来设置开启或关闭偏向锁

         轻量级锁的加锁和解锁

       锁升级为轻量级锁之后,对象的 Markword 也会进行相应的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录 LockRecord。

  1. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。

  1. 将锁记录中的 Owner 指针指向锁对象。

  1. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

          轻量级锁在加锁过程中,用到了自旋锁

          所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,非阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。 注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于 在执行一个啥也没有的 for 循环。 所以,轻量级锁 适用于那些同步代码块执行的很快的场景, 这样,线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗 CPU 资源。默认情况下自旋的次数是 10 次, 可以通过 preBlockSpin 来修改 在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

         轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

         重量级锁的基本原理

         当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

         加了同步代码块以后,在字节码中会看到一个 monitorenter 和 monitorexit。 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。 monitorenter 表示去获得一个对象监视器。monitorexit 表 示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系 统在用户态与内核态之间来回切换,严重影响锁的性能。

         示例代码:

        部分字节码指令:

        

 实现队列:

public class SynchronizeQueue {

    private int maxSize;
    private Object lock = new Object();
    private ArrayList<Object> list = new ArrayList<Object>();

    public SynchronizeQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    public void put(Object target) throws InterruptedException {
        synchronized (lock) {
            if (list.size() == maxSize) {
                lock.wait();
            }
            list.add(target);
            System.out.println("add"+target);
            lock.notify();
        }
    }

    public synchronized Object take() throws InterruptedException {
        synchronized (lock) {
            if (list.size() == 0) {
                lock.wait();
            }
            Object o = list.remove(0);
            System.out.println("take"+o);
            lock.notify();
            return o;
        }
    }

    public static void main(String[] args) {
        SynchronizeQueue synchronizeQueue = new SynchronizeQueue(5);
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    synchronizeQueue.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    synchronizeQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }
}

 锁升级流程图:https://files.cnblogs.com/files/dyg0826/Synchronized.gif   (非原创)

     

 

 

原文地址:https://www.cnblogs.com/dyg0826/p/11039570.html