synchronized详解

1 synchronized底层实现原理

参考:

https://juejin.im/post/6844903830644064264#heading-0

https://juejin.im/post/6844903670933356551#heading-4

HotSpot虚拟机中,对象的内存布局分为三个区域:

  • 对象头(Header
  • 实例数据(Instance Data
  • 对齐填充(Padding

其中,对象头(Header)又分为两部分:

  • Mark Word
  • 类型指针

synchronized用的锁是存储在Java对象头的Mark Word中的。

下面是Mark Word的存储结构(32位JVM):

锁状态25bit4bit1bit,是否是偏向锁2bit,锁标志位
无锁状态对象的hashCode对象分代年龄001

在运行期,Mark Word里存储的数据会随着标志位的变化而变化。

存储内容标志位状态
指向栈中锁记录的指针00轻量级锁
指向互斥量(重量级锁)的指针10重量级锁
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01偏向锁

可以看到,Mark Word包含了线程持有的锁。

JVM基于进入和退出Monitor对象来实现synchronized方法和代码块的同步,两者细节上有差异。

1.1 synchronized代码块

使用monitorentermonitorexit指令来实现。

monitorenter指令编译后,插入到同步代码块开始的位置,monitorexit指令编译后,插入到同步代码块结束的位置和异常处。JVM保证每个monitorenter必须有一个monitorexit指令与之对应。

每个对象都有一个Monitor对象(监视器锁)与之对应。

  • monitorenter

当线程执行到monitorenter指令的时候,将会尝试获取Monitor对象的所有权,过程如下:

  1. 如果Monitor对象的进入计数器为0,则该线程成功获取Monitor对象的所有权,然后将计数器设置为1
  2. 如果该线程已经拥有了Monitor的所有权,那这次算作是重入,重入也会将计数器的值加1
  3. 如果其他线程已经占有了Monitor对象,那么该线程进入阻塞状态,直到Monitor的计数器的值为0,再重新尝试获取Monitor对象的所有权。
  • monitorexit

当已经获取Monitor对象所有权的线程执行到monitorexit指令的时候,将会释放Monitor对象的所有权。过程如下:

  1. 执行monitorexit指令时,Monitor对象的进入计数器的值减1,如果减1后的值为0,那么这个线程将会释放Monitor对象的所有权,其他被这个Monitor阻塞的线程可以开始尝试去获取这个Monitor对象的所有权。
public class com.fufu.concurrent.SyncCodeBlock {
  public int i;

  public com.fufu.concurrent.SyncCodeBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void syncTask();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter                      //注意此处,进入同步方法
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit                       //注意此处,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                      //注意此处,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

1.2 synchronized方法

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。

JVM可以从 方法常量池 中的 方法表结构(method_info Structure 中的 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法。

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。

  //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10

2 synchronized使用规则

参考:https://www.cnblogs.com/skywang12345/p/3479202.html

下面总结了对象的synchronized基本规则。

  • 规则一:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对 “该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问 将被阻塞。

  • 规则二:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对 “该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问 将被阻塞。

  • 规则三:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然 可以访问 “该对象” 的非同步代码块

2.1 规则一

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对 “该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问 将被阻塞。

public class Demo1 {

    public static void main(String[] args) {

        UserRunnable r = new UserRunnable();
        Thread t1 = new Thread(r, "thread-1");
        Thread t2 = new Thread(r, "thread-2");
        t1.start();
        t2.start();
    }
}

class UserRunnable implements Runnable {

    @Override
    public void run() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

thread-1 loop 1
thread-1 loop 2
thread-1 loop 3
thread-2 loop 1
thread-2 loop 2
thread-2 loop 3

Process finished with exit code 0

可以看到,线程thread-1获得了r对象的锁,执行同步代码块,线程thread-2只能等待线程thread-1执行完了才能开始执行。

2.2 规则二

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对 “该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问 将被阻塞。

public class Demo2 {

    public static void main(String[] args) {

        Obj obj = new Obj();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadA();
            }
        }, "thread-1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadB();
            }
        }, "thread-2");
        t1.start();
        t2.start();
    }
}

class Obj {

    public void methadA() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodA, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void methadB() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodB, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

thread-1 call methodA, loop 1
thread-1 call methodA, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 1
thread-2 call methodB, loop 2
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到,Obj类中的methodAmethodB方法都有一个同步代码块。当线程thread-1调用obj对象的methodA方法的时候,线程thread-2被阻塞了,直到thread-1释放了obj对象的锁,thread-2才开始调用methodB方法。

2.3 规则三

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然 可以访问 “该对象” 的非同步代码块

public class Demo3 {

    public static void main(String[] args) {

        Obj obj = new Obj();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadA();
            }
        }, "thread-1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                obj.methadB();
            }
        }, "thread-2");
        t1.start();
        t2.start();
    }
}

class Obj {

    public void methadA() {
        synchronized (this) {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodA, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void methadB() {
        try {
            for (int i = 1; i <= 3; i++) {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() 
                                   + " call methodB, loop " + i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

thread-1 call methodA, loop 1
thread-2 call methodB, loop 1
thread-1 call methodA, loop 2
thread-2 call methodB, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到,Obj类的methodA方法有同步代码块,而methodB方法没有。当线程thread-1访问methodA方法的时候,线程thread-2可以访问methodB方法,不会阻塞。

3 实例锁 和 全局锁

实例锁

  • 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
  • 实例锁对应的就是 synchronized关键字。

全局锁

  • 该锁针对的是类,无论实例多少个对象,线程都共享该锁。
  • 全局锁对应的就是 static synchronized关键字(或者是锁在该类的class或者lassloader对象上)。

例子:

pulbic class Something {
    public synchronized void syncA(){}
    public synchronized void syncB(){}
    public static synchronized void cSyncA(){}
    public static synchronized void cSyncB(){}
}

假设Something有两个实例xy,结论:

  1. x.syncA()x.syncB()不能被同时访问。因为使用了同一个对象的实例锁。
  2. x.syncA()y.syncB()可以被同时访问。因为使用了不同实例对象的实例锁。
  3. x.cSyncA()y.cSyncB()不能被同时访问。因为他们使用了同一个全局锁,相当于Something类的锁。
  4. x.syncA()Something.cSyncA()可以被同时访问。因为一个是实例x的锁,一个是类Something的锁,不是同一个锁,互不干扰。
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值