synchronized是一个java中的关键字,是基于JVM层面的,用于保证java的多线程安全,它具有四大特性,可用于完全替代volatile:
synchronized的四大特性保证了多线程在操作共享资源时的安全。synchronized的使用方法如下:
package com.javaBase.LineDistance; /** * 〈一句话功能简述〉; * 〈功能详细描述〉 * * @author jxx * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */ public class TestSynchronized { static TestSynchronized testSynchronized = new TestSynchronized(); private synchronized void method1() { System.out.println("修饰实例方法。锁为实例对象。"); } private static synchronized void method2() { System.out.println("修饰静态方法。锁为类对象。"); } private static synchronized void method3() { synchronized (TestSynchronized.class) { System.out.println("修饰代码块,锁对象为类对象。"); } synchronized (testSynchronized) { System.out.println("修饰代码块,锁对象为实例对象。"); } } }
synchronized可以修饰实例方法,静态方法,代码块,但她的锁资源只有两种,即类锁和对象锁。当使用对象锁时,稍有不慎会出问题,且看下面的代码:
package com.javaBase.LineDistance; /** * 〈一句话功能简述〉; * 〈功能详细描述〉 * * @author jxx * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */ public class TestSynchronized2 implements Runnable { public static int i = 0; private synchronized void add () { i++; } @Override public void run() { for (int i = 0; i < 100000; i++) { add(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new TestSynchronized2()); Thread t2 = new Thread(new TestSynchronized2()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
运行结果:
169816
将add方法改为:
private static synchronized void add () { i++; }
便可正常运行。运行出错的原因是t1,t2 new了两个实例,导致进入add方法的线程持有的不是同一个锁,因此操作共享数据时出错,但若add变为静态方法,那么add同步锁就变为了类锁,类锁始终只有一个,因此运行结果正常。
了解synchronized原理之前需要先知道java对象头的概念。
java实例对象在堆中的结构如上图,包含对象头,实例变量,填充数据。实例变量保存的是类的属性信息以及父类的属性信息。填充数据用于补齐字节数,jvm要求对象的起始字节必须为8的整数倍。java对象头存储着synchronized的锁信息,它由Mark Word 和 Class Metadata Address两部分组成,Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address存储类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。那么与synchronized密切相关的就是Mark Word,Mark Word的详细结构如下:
下面来研究下synchronized在同步方法和同步代码块两种情况下线程是如何获取锁和释放锁的。我们先对同步方法进行反编译,结果如下:
public synchronized void add(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: iconst_1 4: iadd 5: putstatic #2 // Field i:I 8: return LineNumberTable: line 14: 0 line 15: 8
JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
下面对同步代码块进行反编译:
public void add(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #2 // class TestSynchronized2 2: dup 3: astore_1 4: monitorenter 5: getstatic #3 // Field i:I 8: iconst_1 9: iadd 10: putstatic #3 // Field i:I 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
参见另一篇博客:多线程锁的升级(膨胀)原理
参考链接:synchronized使用及原理解析
原文:https://www.cnblogs.com/jxxblogs/p/11911547.html