Android开发Android开发经验谈Android开发

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

2021-10-16  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读