美文网首页Android开发Android开发经验谈Android开发
Java Synchronized实现互斥之应用与源码初探

Java Synchronized实现互斥之应用与源码初探

作者: 小鱼人爱编程 | 来源:发表于2021-10-16 15:54 被阅读0次

    前言

    线程并发系列文章:

    Java 线程基础
    Java 线程状态
    Java “优雅”地中断线程-实践篇
    Java “优雅”地中断线程-原理篇
    真正理解Java Volatile的妙用
    Java ThreadLocal你之前了解的可能有误
    Java Unsafe/CAS/LockSupport 应用与原理
    Java 并发"锁"的本质(一步步实现锁)
    Java Synchronized实现互斥之应用与源码初探
    Java 对象头分析与使用(Synchronized相关)
    Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
    Java Synchronized 重量级锁原理深入剖析上(互斥篇)
    Java Synchronized 重量级锁原理深入剖析下(同步篇)
    Java并发之 AQS 深入解析(上)
    Java并发之 AQS 深入解析(下)
    Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
    Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
    Java 并发之 ReentrantReadWriteLock 深入分析
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
    最详细的图文解析Java各种锁(终极篇)
    线程池必懂系列

    上篇文章从无到有分析了如何实现"锁",虽然仅仅实现了最简单的锁,但"锁"的精华已经提取出来了,有了这些知识,本篇将分析系统提供的锁-synchronized关键字的使用与实现。
    通过本篇文章,你将了解到:

    1、synchronized 如何使用
    2、synchronized 源码初探
    3、总结

    1、synchronized 如何使用

    多线程访问临界区

    由上篇文章可知,多线程访问临界区需要锁:


    image.png

    临界区可以是一段代码,也可以是某个方法。

    synchronized 各种使用方式

    按锁作用区域划分,可分为两类:

    修饰方法

    修饰方法又分为两类:实例方法与静态方法。先来看看实例方法:
    实例方法

    public class TestSynchronized {
    
        //共享变量
        private int a = 0;
    
        public static void main(String args[]) {
    
            final TestSynchronized testSynchronized = new TestSynchronized();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int count = 0;
                    while (count < 10000) {
                        testSynchronized.func1();
                        count++;
                    }
    
                    System.out.println("a = " + testSynchronized.getA() + " in thread1");
                }
            });
            t1.start();
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int count = 0;
                    while (count < 10000) {
                        testSynchronized.func1();
                        count++;
                    }
                    System.out.println("a = " + testSynchronized.getA() + " in thread2");
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
                //等待t1,t2执行完毕,再打印结果
                System.out.println("a = " + testSynchronized.getA() + " in mainThread");
            } catch (Exception e) {
    
            }
        }
    
        private synchronized void func1() {
            //修改a
            a++;
        }
    
        private int getA() {
            return a;
        }
    }
    

    以上两个线程t1、t2都需要修改共享变量a的值,同时调用TestSynchronized 的对象方法: func1()进行自增。每个线程调用func1() 10000次,循环结束后线程停止运行。理论上每个线程都对a的值增加了10000次,也就是说最后a的值应为为:a==20000,来看看在主线程里打印a的最终值:


    image.png

    可以看出,多线程访问的结果正确,说明synchronized修饰的实例方法能够正确实现了多线程并发。

    静态方法
    再来看看静态方法:

    public class TestSynchronized {
    
        //共享变量
        private static int a = 0;
    
        public static void main(String args[]) {
    
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int count = 0;
                    while (count < 10000) {
                        func1();
                        count++;
                    }
    
                    System.out.println("a = " + getA() + " in thread1");
                }
            });
            t1.start();
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int count = 0;
                    while (count < 10000) {
                        func1();
                        count++;
                    }
                    System.out.println("a = " + getA() + " in thread2");
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
                //等待t1,t2执行完毕,再打印结果
                System.out.println("a = " + getA() + " in mainThread");
            } catch (Exception e) {
    
            }
        }
    
        private static synchronized void func1() {
            //修改a
            a++;
        }
    
        private static int getA() {
            return a;
        }
    }
    

    相对于修饰实例方法,只是更改了a为static类型,并且将func1()变为静态方法,最终的结果与前面实例方法是一致的。
    说明synchronized修饰的静态方法能够正确实现了多线程并发。

    修饰代码块

    synchronized 修饰方法时(静态方法/实例方法),在进入方法前先申请锁,退出方法后释放锁。假若有个方法里执行的操作比较多,而需要并发访问的就只有一小段,如果为了这小段临界区将方法用synchronized修饰,那么将是大材小用。为此synchronized提供了修饰一段代码块的方法。
    按锁类型划分,修饰代码块也分为两类:
    获取对象锁

        //声明锁对象
        private static Object object = new Object();
        private void func1() {
            //无需互斥访问的区域
            int b = 1000;
            int c = 0;
            if (c < b) {
                c++;
            }
    
            //修改a
           //需要互斥访问的区域
            synchronized (object) {
                a++;
            }
        }
    

    可以看出虽然func1方法里有其它操作,但是对于多线程操作不敏感,只有共享变量a需要互斥访问,因此仅仅需要对操作a使用synchronized修饰。
    synchronized (object) 表示获取实例对象:object的锁。

    获取类锁
    再来看看如何使用类锁:

        private void func1() {
            //无需互斥访问的区域
            int b = 1000;
            int c = 0;
            if (c < b) {
                c++;
            }
    
            //修改a
            //需要互斥访问的区域
            synchronized (TestSynchronized.class) {
                a++;
            }
        }
    

    这次没有实例化对象了,而是直接使用TestSynchronized.class,表示获取TestSynchronized 类锁。

    小结

    将上述关系用图表示:


    image.png

    1、无论是修饰方法还是代码块,最终都是获取对象锁(类锁是Class对象的锁)
    2、实例方法与对象锁获取的是同一把锁(普通对象锁)
    3、静态方法与类锁获取的是同一把锁(类锁-Class对象锁)

    对象锁

        private void func1() {
            synchronized (this) { }
        }
    
        private synchronized void func2() {
        }
        private void func3() {
        }
    

    func1()与func2()都需要获取对象锁(this指的是本对象,也就是调用方法的对象本身),因此两者的访问是互斥的,而访问func3()则不受影响。

    类锁

        private void func1() {
            synchronized (TestSynchronized.class) { }
        }
    
        private static synchronized void func2() {
        }
    
        private static synchronized void func3() {
        }
    
        private static void func4() {
        }
    

    func1()、func2()、func3()都需要获取类锁,此处的类锁为TestSynchronized.class 对象,因此三者的访问是互斥的,而访问func4()则不受影响。

    由此可知:

    1、类锁与对象锁互不影响
    2、多线程需要获取"同一把锁"才能实现互斥

    2、synchronized 源码初探

    上面的例子离不开synchronized 修饰符,这是个关键字,JVM是如何识别这个关键字的呢?首先来看看synchronized编译后的结果:

    修饰代码块

    先来看Demo:

    public class TestSynchronized {
    
        //共享变量
        int a = 0;
    
        Object object = new Object();
    
        public static void main(String args[]) {
        }
    
        private void add() {
            synchronized (object) {
                a++;
            }
        }
    }
    

    以上是使用对象锁修饰了代码块。现在将它编译为.class文件,定位到TestSynchronized.java 文件目录,打开命令行,输入如下命令:

    javac TestSynchronized.java

    与TestSynchronized.java文件同目录下将生成TestSynchronized.class。
    .class 文件肉眼看不出所以然,因此将它反编译看看,依然在同级目录下使用如下命令:

    javap -verbose -p TestSynchronized.class

    然后命令行输出一串结果,当然如果你觉得不方便查看,可以将输出结果放在文件里,使用如下命令:

    javap -verbose -p TestSynchronized.class > mytest.txt

    来看看输出的重点内容:


    image.png

    上图重点圈出了两个指令:monitorenter与monitorexit。

    • monitorenter 表示获取锁
    • monitorexit 表示释放锁
    • 两者之间的操作就是被锁住的临界区
      其中monitorexit 有两个,后面一个是发生异常时会执行

    monitorenter/monitorexit 指令对应代码

    monitorenter/monitorexit 指令对应的代码在哪呢?
    网上有不同的解释,我倾向于:https://github.com/farmerjohngit/myblog/issues/13 中所作的分析:

    • 在Hotspot中只用到了模板解释器(templateTable_x86_64.cpp)
      ,字节码解释器(bytecodeInterpreter.cpp)根本就没用到
    • 模板解释器里都是汇编代码,字节码解释器用的是C++实现的,两者逻辑是大同小异的,为了更方便阅读以字节码解释器为例

    monitorenter指令对应代码:

    image.png

    在bytecodeInterpreter.cpp#1804行。

    monitorexit指令对应代码:

    image.png

    在bytecodeInterpreter.cpp#1911行。

    由以上可知,我们找到了monitorenter/monitorexit 指令对应的代码入口,也就是指令具体的实现位置。

    修饰方法

    先来看Demo:

    public class TestSynchronized {
    
        //共享变量
        int a = 0;
    
        Object object = new Object();
    
        public static void main(String args[]) {
        }
    
        private synchronized void add() {
            a++;
        }
    }
    

    同样的使用javap指令,结果如下:


    image.png

    与修饰代码块不一样的是:并没有monitorenter/monitorexit 指令,但是多了ACC_SYNCHRONIZED 标记,这个标记是怎么解析的呢?
    先看看锁的入口和出口对应的代码:
    方法锁入口

    image.png

    在bytecodeInterpreter.cpp#643行。
    上图标红的部分从名字可以看出判断该方法是否是同步方法,若是同步方法,则进行获取锁的步骤。
    寻找is_synchronized()函数,在method.hpp里。


    image.png

    继续看accessFlags.hpp:


    image.png

    最终看jvm.h


    image.png

    可以看出:

    用synchronized关键字修饰方法后,反编译出来的代码里带有:ACC_SYNCHRONIZED 标记与JVM里的JVM_ACC_SYNCHRONIZED 对应,而这个参数最终使用的地方是通过is_synchronized()函数用来判断是否是同步方法。

    方法锁出口

    image.png

    方法结束后运行此段代码,里边判断是否是同步方法,进而进行释放锁等操作。

    3、总结

    synchronized修饰代码块和方法,两者异同:

    1、修饰代码块时编译后会在临界区前后加入monitorenter、monitorexit 指令
    2、修饰方法时进入/退出方法时会判断ACC_SYNCHRONIZED 标记是否存在
    3、不管是用monitorenter/monitorexit 还是ACC_SYNCHRONIZED,最终都是在对象头上做文章,都需要获取锁。

    了解了synchronized使用及其源码入口,接下来将深入探析其工作机制。下篇将会分析无锁、偏向锁、轻量级锁、重量级锁的实现机制。

    本文基于jdk8。

    您若喜欢,请点赞、关注,您的鼓励是我前进的动力

    持续更新中,和我一起步步为营系统、深入学习Java/Android

    相关文章

      网友评论

        本文标题:Java Synchronized实现互斥之应用与源码初探

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