一 概述
由博客(http://blog.csdn.net/xiaowang627/article/details/60584428),我们已经体验到synchronized关键字的强大之处,该关键字是Java中解决并发问题最常用的方法,也是最简单的方法,该关键字有三种用法:
(1)修饰普通方法。
(2)修饰静态方法。
(3)修饰代码块。
二 修饰代码块
2.1 先看修饰代码块的代码:
main方法中new了两个线程,两个线程中++count代码块用synchronized关键字修饰,new Runnable(){}为实现了Runnable接口的匿名内部类,编译器会为每个类生成一个class文件,所以该代码编译后生成三个class文件:Test.class,Test$1.class,Test$2.classpublic class Test { private static int count=0; public static void main(String[] args){ new Thread(new Runnable() { public void run() { synchronized (this) { ++count; } } }); new Thread(new Runnable() { public void run() { synchronized (this) { ++count; } } }); } }
2.2 反汇编看其原理
将上述三个.class文件反汇编一下,将反汇编结果重定向到txt文件,反汇编命令为:javap -c Test,如下所示:
文件text如下所示:
文件text1和text2如下所示:Compiled from "Test.java" public class Test { public Test(); Code: 0: aload_0 1: invokespecial #2 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #3 // class java/lang/Thread 3: dup 4: new #4 // class Test$1 7: dup 8: invokespecial #5 // Method Test$1."<init>":()V 11: invokespecial #6 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V 14: pop 15: new #3 // class java/lang/Thread 18: dup 19: new #7 // class Test$2 22: dup 23: invokespecial #8 // Method Test$2."<init>":()V 26: invokespecial #6 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V 29: pop 30: return static int access$004(); Code: 0: getstatic #1 // Field count:I 3: iconst_1 4: iadd 5: dup 6: putstatic #1 // Field count:I 9: ireturn static {}; Code: 0: iconst_0 1: putstatic #1 // Field count:I 4: return }
他们的run方法是相同的,重点看红色方框部分,invokestatic指令调用方法Test.access$004:(),查看text文件知,该方法完成++count,即两个run方法在运行被synchronized关键字修饰的代码块{++count}时在其前后分别加上了monitorenter和monitorexit指令。如此完成同步。
2.3 monitorenter和monitorexit指令
(1)synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个引用类型的参数来指明要锁定和解锁的对象。 如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
(2)在执行monitorenter指令时,首先尝试获取对象的锁, 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。
(3) 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。 其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。 Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。 所以synchronized是Java语言中一个重量级的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。 而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。优化策略可参考:http://blog.csdn.net/xiaowang627/article/details/60781913
三 修饰方法
3.1 看以下两个例子
3.2 采用如下指令查看反汇编代码:
public class Test1{ public static int count; public void add() { ++count; } }
public void add() { ++count; } }其中Test1没有synchronized关键字修饰add()方法,而Test2有。public class Test2{ public static int count; public synchronized void add() { ++count; } }
public synchronized void add() { ++count; } }
结果如下:
观察可知两个反汇编文件仅有一个地方不同:加入synchronized关键字的的文件,add方法下,flags后多了一个ACC_SYNCHRONIZED。
3.3 ACC_SYNCHRONIZED
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。