理解synchronized关键字

前言

synchronized是很早就有的关键字,很多人都称呼它为重量级锁,因为阻塞或唤醒一个线程会发生上下文切换,如果同步方法中的内容过于简单,那么频繁的上下文切换所带来的开销可能会使得系统的性能下降,所以为了减少获取锁和释放锁带来的性能消耗便作了优化,因为synchronized跟锁有很大关系,所以这些优化就是针对锁的优化,接下来说说已知的ynchronized知识点。

synchronized底层实现

在Java中,synchronized关键字可以保证在同一个时刻,只有一个线程可以执行某个方法或代码块,且先看看如何使用该关键字及对应方式的底层实现。

普通同步方法


    public class TestSynchronized {

        public synchronized void f(){
            System.out.println("Hello");
        }

        public static void main(String[] args) {
            TestSynchronized ts = new TestSynchronized();
            ts.f();
        }

        public void f1() {
            System.out.println("World");
        }

    }

    // 将上面的代码反编译后:javap -v TestSynchronized ---->

    /**
     *
     * public synchronized void f();
     * descriptor: ()V
     * flags: ACC_PUBLIC, ACC_SYNCHRONIZED   
     * /

    /**
     * public void f1();
     * descriptor: ()V
     * flags: ACC_PUBLIC
     */

     // 对比下f与f1方法在被Java编译器翻译成字节码时的区别,关键就在于多了ACC_SYNCHRONIZED标志

从示例代码可知,普通同步方法使用ACC_SYNCHRONIZED标志来与普通方法做区分,有了该标志,线程调用指定方法时需要先获取锁(监视器),只有成功获取到锁的线程才能调用,最后在方法执行完毕后释放锁(即使方法抛出异常,依然会释放锁),在方法调用期间,其他线程无法进入。

静态同步方法


    public class TestSynchronized {

        public static synchronized void f2() {
            System.out.println("");
        }

        public static void f3() {}

    }

    // 将上面的代码反编译后:javap -v TestSynchronized ---->

    /**
     *
     * public static synchronized void f2();
     * descriptor: ()V
     * flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
     * /

    /**
     * public static void f3();
     * descriptor: ()V
     * flags: ACC_PUBLIC, ACC_STATIC
     */

     // 关键还是在于多了ACC_SYNCHRONIZED标志

静态同步方法本质上和普通方法没有什么区分,只是静态方法是随着类的加载而加载的,而普通方法是随着对象的加载而加载,所以可以知道静态方法对应的锁是类的Class对象,而普通方法对应的锁是当前对象,其实如果你不确定的话,完全可以通过外部工具来判断,比如使用HSDB查看JVM运行时的状态,可以比较当前对象与锁的内存地址。

同步代码块


    public class TestSynchronized {

        public void f4(){
            synchronized (this) {
                System.out.println("synchronized f");
            }
        }

        public static void main(String[] args) {

        }
    }

    // 方法内的代码通常翻译成对应的指令信息,所以要看看编译器做了啥动作:javap -v TestSynchronized ---->

    /**
     * public void f4();
     * Code:
     * ...省略
     *  3: monitorenter
     *  4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *  7: ldc           #3                  // String synchronized f
     *  9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     *  12: aload_1
     *  13: monitorexit
     */

    /**
     * public void f5();
     * Code:
     *  0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *  3: ldc           #5                  // String f
     *  5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     *  8: return
     */
     

     // f4与f5区别就在于加上了monitorenter与monitorexit指令

monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令是插入到代码块结束处,JVM要保证每个monitorenter必须要有对应的monitorexit与之配对,否则就光有获取锁而没有释放锁,那后果可想而知。线程执行到monitorenter指令时,将会尝试获取对应的锁,虽然示例中使用的是this,但可以指定任意对象,同理,在成功获取到锁后其他线程就无法进入到代码块里。

synchronized关键词能由此作用完全是因为锁,每个对象都可以作为锁,那么锁的信息是存放在哪里呢,是在Java对象头里。如果对象是数组类型,则JVM用3个字宽存储对象头,如果对象是非数组类型,则用2个字宽存储对象头。在32位JVM中,1字宽等于4个字节,至于对象头的存储结构就不作深入介绍了。Java1.6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,不过笔者不会在这里阐述关于锁的详细信息,当然了,目前的我还不会,哈哈哈。锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,关于锁的内容就介绍到这里,以后会另外新起文章进行详细的讲解。

锁的内存语义

锁可以让临界区代码互斥执行,但其还有另外一个面纱,即锁的内存语义。

释放锁:Java内存模型会把该线程对应的本地内存中的共享变量值刷新到主内存中。

获取锁:Java内存模型会把该线程对应的本地内存置为无效,从而使得临界区代码必须从主内存中读取共享变量。

关于Java内存模型的介绍,可参考理解volatile关键字

结束语

关于锁的知识点还远不止这些,笔者的功力有限,仍需刻苦努力修炼,有错误的地方请见谅。

参考资料

《Java并发编程的艺术》

posted @ 2020-12-21 20:43  zliawk  阅读(179)  评论(0)    收藏  举报