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

前言

线程并发系列文章:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容