详解多线程同步规则【二】

那么我们现在来看前面的第四个疑问:对方法加 synchronized 关键字与用 synchronized(xxx) 同步代码块两种规避方法又有什么分别和联系呢?

如果您是一路读下来的,就很清楚,synchronized(xxx) 是把危险的代码同步起来,即框起来,使之同时只能一个线程执行这块代码(监视区域),并为该代码块关联一个对象。不妨从字节码的角度来分析下,比如我们来看看 operate() 方法产生的字节码

代码为:

public void operate() {
  synchronized(this){
   flag ++;
   flag --;
  }
}

字节码如下:

public void operate();
  Code:
   0:   aload_0         //位置为0的局部变量,即方法的第一个参数 this 压栈
   1:   dup               //复制栈顶,栈顶仍然为 this,栈出口是连续两个 this
   2:   astore_1        // 弹出栈顶的 this,存到位置为 1 的局部变量
   3:   monitorenter  //进入监视区,弹出栈顶(还是 this),并对 this 加锁
   4:   getstatic       #17; //Field flag:I
   7:   iconst_1
   8:   iadd
   9:   putstatic       #17; //Field flag:I
   12:  getstatic       #17; //Field flag:I
   15:  iconst_1
   16:  isub
   17:  putstatic       #17; //Field flag:I
   20:  aload_1      //位置为 1 的局部变量(记得吧,还是 this) 压栈 -- 正常退出方法
   21:  monitorexit  //退出监视区域,弹出栈顶(this),并释放 this 锁
   22:  goto    28
   25:  aload_1      //位置为 1 的局部变量(记得吧,还是 this) 压栈 -- 异常退出方法
   26:  monitorexit  //退出监视区域,弹出栈顶(this),并释放 this 锁
   27:  athrow
   28:  return
  Exception table:  //字节码里加了异常表,在碰到任何异常都能释放对象锁
   from   to  target type
     4    22    25   any
    25    27    25   any

4 到 17 是正常的方法体部分,用 synchronized(xxx) 来同步代码块会使用 monitoenter...monitorexit 制造一个监视区域,该监视区域会与栈顶的对象进行关联。这里用的是 this,如果你写成的是 synchronized(object),那么该监视区域则会与 object 关联。附:在 C++ 中分别用 EnterCriticalSection() 和 LeaveCriticalSection() 方法来进入和离开临界区代码的。

直接给方法加个 synchronized 关键字(public synchronized void operate() {......}) 会有什么功效呢?只要稍稍发挥一下想像力,既然 synchronized(xxx) 是给代码块做了个监视区,并与 xxx 对象关联,那么给方法加个关键字就应该是把方法体的所有代码行放到监视区域了。我们说监视区域总是会与某一个对象相关联,然而方法加 synchronized 关键要与什么隐式对象关联,我们有如下规则:

1) 如果是非静态的同步方法,关联对象就是 this,相当于 synchronized(this) 括起了方法所有代码

2) 如果是静态的同步方法,方法无法访问到 this(不存在),此时关联对象就是该类的 Class 实例,比如对于 TestMultiThread 就相当于用 synchronized(TestMultiThread.class) 括起了方法所有代码

当你看到第一条规则,非静态方法,加上 synchronized 关键字也就相当于 synchronized(this),是不是也意识到了单纯给方法加个关键字 synchronized 有时候也解决不了问题,何不亲手把上面的 operate() 方法写成如下:


01.public synchronized void operate() {
02.    flag++;
03.    try {
04.        // 增加随机性,让不同线程能在此交替执行
05.        Thread.sleep(new Random().nextInt(5));
06.    } catch (InterruptedException e) {
07.        e.printStackTrace();
08.    }
09.    flag--;
10.    System.out.println("Thread: " + Thread.currentThread().getName()
11.            + " /Current flag: " + flag);
12.}

执行下 TestMultiThread 程序看看,你仍然会得到这个你不想要的结果:

Thread: Thread-01 /Current flag: 2
Thread: Thread-02 /Current flag: 1

要如何加个小小的改造呢?对了让该方法是静态的 public static synchronized void operate(),就每次都能输出为 1 的 flag 值了,因为它是与 TestMultiThread.class 进行关联了。

再进一步思考,很多事不能想当然的,不然就会出现 出生入死--一出生就去死,死于非命--死的不是命那样的解释了。为方法加 synchronized 关键字,会不是就用 monitorenter 和 monitorexit 框了所有代码呢?逻辑上确实说的过去,但事实上加个 synchronized 关键字只会在字节码中 method_info 表的该方法上加上一个存取标志(access_flag) ACC_SYNCHRONIZED(0x0020),不会在原方法指令中插入 monitorenter 和 monitorexit,JVM 知道怎么去处理这个 ACC_SYNCHRONIZED 标志的,也许执行时内部会调整成一样。

顺道下来,第一个问题 不论是静态的或非静态的方法都加上 synchronized 关键字,那静态的方法和非静态的方法前加上 synchronized 关键字有区别吗?也有了答案,即非静态同步方法,监视区与 this 相关联,静态同步方法,监视区与该类的 Class 实例相关联。


立此题之前,本只想就 synchronized() 中的对象来个充分理解而已,无奈,事物间总是有千丝万缕,于是牵扯出这许多事。以后有空或有必要还是该拆出多个专题不垒这样的长篇大落,至少分出以下几出:

1. 不同线程执行同步方法或同步块的互斥规则
2. 同步时,监视区域是与哪一个对象相关联的
2. 如何理解同步块 synchronized(xxx) 中的对象参数
3. 同步块与同步方法的字节码分析
..... 或者还可以拟个 写同步方法时容易碰到的几个陷阱 等等

参考:1. The Java Virtual Machine Specification
        2. Inside the Java Virtual Machine  (by Bill Venners)
        3. Books Related to the JVM

你可能感兴趣的:(jvm,多线程,thread,制造,Access)