并发学习第四篇——synchronized关键字

synchronized在JVM中的原理

  对象在内存中的存储结构包含对象头(Header)、实例数据(Instance Data)等

  对象头包括两部分:

  "Mark Word":存储对象自身的运行时数据,主要包含以下属性

    hash: 保存对象的哈希码
    age: 保存对象的分代年龄
    biased_lock: 偏向锁标识位
    lock: 锁状态标识位
    JavaThread*: 保存持有偏向锁的线程ID
    epoch: 保存偏向时间戳

  "Klass Pointer":对象指向它的类的元数据的指针,通过这个指针来确定这个对象是哪个类的实例

  snchronized使用CAS替换Mark Word来标识对象加锁状态

JVM对synchronized所做的优化

  锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗

  锁消除(Lock Elimination):编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸

出去从来被其他线程访问到,就可以去除这些锁

  轻量级锁(Lightweight Locking):在没有多线程竞争的情况下只用一条CAS原子指令完成锁的获取及释放

  偏向锁(Biased Locking):目的是消除数据在无竞争情况下的同步原语。使用CAS获取它的线程,下一次同一个

线程进入则偏向该线程,无需任何同步操作。

  适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗而产生的忙循环(循环时间固定),

即自旋。自适应自旋是自旋时间根据之前锁自旋时间和线程状态,动态变化,用以期望能减少阻塞的时间

锁升级

偏向锁

  当一个线程第一次获取到对象的锁时,JavaThread*记录下当前线程的threadId,biased_lock的值设置为1,

在无竞争的情况下,如果这个线程再次尝试获取该对象的锁,会直接将这个线程threadId和对象头里记录的threadId做

比较,如果一致,则认为当前这个线程已经获取到了对象的锁,不需要再次获取锁

线程竞争锁过程

  线程尝试获取锁时,首先检查MarkWord里是否记录的自己的ThreadId,

  如果是,表示当前线程处于可获得"偏向锁"状态,跳过轻量级锁直接执行同步体

  如果不是,根据MarkWord里的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空,然后两个线

程都使用CAS方式修改MarkWord内容,成功执行CAS的线程获得资源,失败者进入自旋

  接着,自旋的线程成功获得资源(即之前获得资源的线程执行完成并释放锁),则整个状态依然处于轻量级锁的状态

  如果自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己

  最后,规定锁只能升级,不能降级,依次是偏向锁,轻量级锁,重量级锁

字节码中的指令

  synchronized关键字修饰的代码段,在JVM中被编译为monitorenter、monitorexit指令来获取和释放锁 

  monitorenter

  JVM规范参考官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

The objectref must be of type reference.

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

 翻译下:

  objectref必须为reference类型数据(对象),任何对象都有一个monitor与之关联,当且仅当一个monitor被(线程)持有后,

  这个对象才会处于锁定状态,线程执行到该指令时,按如下方式来尝试获取monitor的所有权:

  • 如果monitor计数器为0,那么线程成功进入,并将计数器设置为1,当前线程就是monitor所有者
  • 如果当前线程已经拥有了monitor,那么他可以重入这个monitor,重入时需要将计数器值+1
  • 如果其他线程拥有了monitor所有权,那么当前线程将被阻塞,直到计数器数值重新为0,才会尝试获取monitor所有权

官网还有个note:

  synchronized方法不通过monitorenter和monitorexit指令来实现(实际是通过在字节码中给这个方法加上了一个标识符

ACC_SYNCHRONIZED),当调用一个synchronized方法时,会自动进入对应的monitor,当方法返回时,会自动退出monitor,

这些动作是由java虚拟机在方法调用和方法返回指令中隐式处理的

  在java语言中,同步的概念除了包括monitor的进入和退出操作以外,还包括等待monitor(Object.wait)和唤醒等待monitor

的线程(Object.notifyAll和Object.notify),这些操作包含在java.lang中,而不是通过jvm的指令集显式支持


同理,monitorexit指令是这么描述的:

  执行monitorexit的线程必须与objectref所引用的实例相对应的monitor的所有者

  执行指令时,线程把monitor计数器减1,如果减1后计数器为0,那么线程退出monitor,不再是monitor的拥有者,

其他被这个monitor阻塞的线程,可以尝试获取这个monitor的所有权

  注意,synchronized方法和代码块抛出的异常,虚拟机的处理方式不同

  • 在synchronized方法正常完成时,monitor通过虚拟机的返回指令退出,而非正常完成时,通过athrow指令退出
  • 当同步块抛出异常时,将有虚拟机异常处理机制来保证退出之前在同步块开始时进入的monitor

得到哪些确保

原子性(包裹的代码块作为一个整体执行,不会被别的线程同时访问执行)

可见性(共享变量修改及时可见)

有序性(禁止指令重排序)

这里借用编程思想里的描述很准确

使用synchronized来修饰一个方法或者一个代码块时,能保证在同一时刻最多只有一个线程执行该段代码

     当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行,

另一个线程(或其他线程)会出现的结果是:

  • 另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块
  • 另一个线程仍然可以访问该object中的非synchronized(this)同步代码块
  • 其他线程对该object中所有其它synchronized(this)同步代码块的访问将被阻塞
  • 其它线程对该object对象所有同步代码部分的访问都被暂时阻塞(指同步方法)
  • 以上规则对其它对象锁同样适用.

典型用法

从修饰的角度观察

synchronized可以修饰代码块(实例变量(new Object),对象引用(this),类名称字面常量(xxx.class))

//这里的this指代调用f方法的当前对象

f(){
  synchronized (this) {
          //do something
    }
}

//指定给某个对象加锁

//给确定的对象加锁
f(SyncObject syncObject){
    synchronized (syncObject) {
         //do something
    }
}

//没有确定对象,只想锁住这个代码块
Object object = new Object();
f(){
    synchronized (object) {
         //do something
    }
}

类锁的第一种形式

f(){
    synchronized (xxx.class){
    }
}    

修饰普通方法

synchronized void f(){} 

修饰static方法

//这样会产生一个类锁
static synchronized void f(){}

从类锁和对象锁的角度观察

当synchronized对xxx.class加锁,或修饰static方法时,加的是类锁,多个线程去调类的这个方法时是互斥的

当synchronized修饰普通方法,或者对this,或者对同一个对象加锁时,加的是对象锁,多个线程去调这个对象

的同步块或同步方法时是互斥的(注意,这里必须是同一个对象,否则没效果)

 

上一篇:SVG精髓-svg开发指南


下一篇:PAT Advanced 1033 To Fill or Not to Fill (25) [贪⼼算法]