美文网首页并发编程学习
并发编程学习四、synchronized底层-并发编程的实现原理

并发编程学习四、synchronized底层-并发编程的实现原理

作者: valentine_liang | 来源:发表于2018-12-27 01:00 被阅读0次

    Valentine 转载请标明出处。

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

    public class Demo {
        private static int count = 0;
        private static int count1 = 0;
        private static int count2 = 0;
    
        private static void inc() {
            synchronized (Demo.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
            }
        }
    
        private synchronized void inc1() {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count1++;
        }
    
        private void inc2() {
            synchronized (this) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count2++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Demo demo = new Demo();
            for (int i = 0; i < 1000; i++) {
    
                new Thread(() -> Demo.inc()).start();
    
                /*new Thread(() -> {
                    demo.inc1();
                }).start();
    
                new Thread(() -> {
                    demo.inc2();
                }).start();*/
    
                new Thread(() -> {
                    Demo demo1 = new Demo();
                    demo1.inc1();
                }).start();
    
                new Thread(() -> {
                    Demo demo2 = new Demo();
                    demo2.inc2();
                }).start();
            }
            Thread.sleep(3000);
            System.out.println("运行结果:" + count);
            System.out.println("运行结果1:" + count1);
            System.out.println("运行结果2:" + count2);
        }
    }
    
    public class SynchronizedDemoTest {
        private static Object object = new Object();
    
        public static void main(String[] args) throws Exception {
            synchronized (object) {
    
            }
        }
    
        public static synchronized void method() {
        }
    }
    
    

    输出结果如图:


    运行代码结果图

    synchronized有三种方式来加锁,分别是
    1、修饰实例方法,作用于当前实例加锁,进入同步代码之前要获得当前实例的锁
    2、修饰静态方法,作用于当前类对象加锁,进入同步代码之前要获得当前类对象的锁
    3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块之前要获得给定对象的锁

    synchronized括号后面的对象
    synchronized括号后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻成一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的要是,对于访问者来说是没有任何影响的。

    synchronized的字节码指令
    通过javap -v SynchronizedDemoTest .class (会输出行号、本地变量表信息、反编译汇编代码、输出当前类用到的常量池等信息) 来查看对应的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令,这两个指令隐式地执行了lock和unlock操作,用于提供原子性的保证。
    monitorenter指令插入到同步代码块开始的位置,monitorexit指令插入到同步代码块结束的位置,jvm需要保证每个monitorenter都有一个monitorexit对应。
    这两个指令,本质上是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只有一个线程获得由synchronized所保护对象的监视器。
    线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit就是释放monitor的所有权。
    同步代码块使用了 monitorenter 和 monitorexit 指令实现。
    同步方法中依靠方法修饰符上的 ACC_SYNCHRONIZED 实现。

    public static void main(java.lang.String[]) throws java.lang.Exception;
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: getstatic     #2                  // Field object:Ljava/lang/Object;
             3: dup
             4: astore_1
             5: monitorenter     // 监视器进入,获取锁
             6: aload_1
             7: monitorexit        //监视器退出,释放锁
             8: goto          16
            11: astore_2
            12: aload_1
            13: monitorexit //监视器退出,释放锁
            14: aload_2
            15: athrow
            16: return
    
     public static synchronized void method();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
          stack=0, locals=0, args_size=0
             0: return
          LineNumberTable:
            line 13: 0
    
    

    synchronized的锁的原理
    jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁,在了解synchronized之前,我们要了解两个重要的概念,对象头和monitor。

    Java对象头
    在hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充,Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里,它是轻量级锁和偏向锁的关键。

    Mark Word
    Mark Word是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)


    mark world

    在源码中的体现
    如果想更深入了解对象头在JVM源码中的定义,需要关心几个文件,oop.hpp/markOop.hpp
    oop.hpp,每个Java Object在JVM内部都有一个native的C++对象 oop/oopDesc 与之对应,现在oop.hpp中看oopDesc的定义


    src\share\vm\oops\oop.hpp

    _mark被生命在oopDesc类的顶部,所以这个_mark可以认为是一个头部,上面讲过头部保存了一些重要的状态和标识信息,在markOop.hpp文件中有一些注释说明markOop的内存布局,如图


    src\share\vm\oops\markOop.hpp

    Monitor
    monitor可以理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的monitor,每个object对象里的markOop->monitor()可以保存ObjectMonitor的对象,从源码层面分析monitor对象:
    1、oop.hpp下的oopDesc类是JVM对象的顶级基类,所以每个object对象都包含markOop
    2、markOop.hpp中markOopDesc继承自oopDesc,并扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象
    3、objectMonitor.hpp在hotspot虚拟机中,采用ObjectMonitor类来实现monitor,如图


    ObjectMonitor

    synchronized的锁升级和获取过程
    了解了对象以及monitor以后,接下来去分析synchronized的锁的实现,就比较容易理解了。前面讲过synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁,锁的级别从低到高逐步升级,无锁->偏向锁->轻量级锁->重量级锁。

    自旋锁(CAS)
    自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起,看持有锁的线程是否能够很快释放锁,实现自旋的方式其实就是一段没有任何意义的循环。
    虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。

    偏向锁
    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步快并获取锁的时候,会在对象头和栈帧中的锁记录里面存储偏向锁的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试一下Mark Word中偏向锁的表示是否设置成1 (表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    轻量级锁
    引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁升级为轻量级锁,则会尝试获取轻量级锁。

    重量级锁
    重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。上面在讲Java对象头的时候,讲到了monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类实现monitor,它的锁的获取过程的体现会简单很多。


    获取锁和获取锁失败后的简单流程图

    wait和notify
    wait和notify是用来让线程进入等待状态以及使得线程唤醒的两个操作

    public class ThreadWait extends Thread {
        
        private final Object lock;
    
        ThreadWait(Object lock) {
            this.lock = lock;
        }
    
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("开始执行 thread wait");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行结束 thread wait");
            }
        }
    }
    
    public class ThreadNotify extends Thread {
        
        private final Object lock;
    
        ThreadNotify(Object lock) {
            this.lock = lock;
        }
    
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("开始执行 thread notify");
                lock.notify();
                System.out.println("执行结束 thread notify");
            }
        }
    }
    
    public class ThreadWaitNotifyDemo {
    
        public static void main(String[] args) {
            Object lock = new Object();
            ThreadWait threadWait = new ThreadWait(lock);
            threadWait.start();
            ThreadNotify threadNotify = new ThreadNotify(lock);
            threadNotify.start();
    
        }
    }
    

    输出结果


    结果图

    wait和notify的原理
    调用wait方法,首先会获取监视器锁,获得成功后,会让当前线程进入等待队列并且释放锁;然后当其他线程调用notify或者notifyAll以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁,必须要等到当前的线程执行完monitorexit指令后,也就是锁被释放后,处于等待队列中的线程才可以开始竞争锁,如图


    线程获得锁和释放锁的过程

    wait和notify为什么需要在synchronized里面?
    wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁;而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象锁,然后到这个对象的等待队列中去唤醒一个线程。

    学习来源https://www.gupaoedu.com/

    相关文章

      网友评论

        本文标题:并发编程学习四、synchronized底层-并发编程的实现原理

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