今天看啥  ›  专栏  ›  来一杯咖啡

JAVA并发编程入门篇,思考同步锁Synchronized背后的实现哲学

来一杯咖啡  · 掘金  ·  · 2020-05-23 15:34
阅读 18

JAVA并发编程入门篇,思考同步锁Synchronized背后的实现哲学

多线程在概念上类似抢占式多任务处理,线程的合理使用能够提升程序的处理能力,但是使用的同时也带来了弊端,对于共享变量访问就会产生安全性的问题。下面来看一个多线程访问共享变量的例子:

public class ThreadSafty {

    private static int count = 0;

    public static void incr() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count ++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0 ; i < 1000; i++) {
            new Thread(()->{
                ThreadSafty.incr();
            },"threadSafty" + i).start();
        }
        TimeUnit.SECONDS.sleep(3);
        System.out.println("运行结果是:" + count);
    }

}
复制代码

变量count的运行结果始终是小于等于1000的随机数,因为线程的可见性和原子性。

一、多线程访问的数据安全性

如何保证线程并行运行的数据安全性问题,这里首先能够想到的是加锁吧。关系型数据库中有乐观锁、悲观锁,那么什么是锁呢?它是处理并发的一种手段,实现互斥的特性。

在Java语言中实现锁的关键字是Synchronized

二、Synchronized的基本应用

2.1 Synchronized的三种加锁方式

  • 静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
synchronized  static void method(){}
复制代码
  • 修饰代码块:指定加锁对象,进入同步代码前要获得指定对象的锁
void method(){
    synchronized (SynchronizedDemo.class){}
}
复制代码
  • 修改实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    Object lock = new Object();
    //只针对于当前对象实例有效.
    public SynchronizedDemo(Object lock){
            this.lock = lock;
    }
     void method(){
         synchronized(lock){}
    }
复制代码

2.2 Synchronized锁是如何存储数据的呢?

以对象在jvm内存中是如何存储作为切入点,去看看对象里面有什么特性能够实现锁的

2.2.1 对象在Heap内存中的布局

在Hotspot虚拟机中,对象在堆内存中的布局,可以分为三个部分:

  • 对象头:包括对象标记、类元信息
  • 实例数据
  • 对齐填充

Hotspot 采用instanceOopDescarrayOopDesc 来描述对象头,arrayOopDesc 对象用来描述数组类型的。 instanceOopDesc的定义在Hotspot源码中的instanceOop.hpp文件中,另外,arrayOopDesc的定义对应arrayOop.hpp

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
复制代码

看源码instanceOopDesc继承自oopDesc,oopDesc定义在oop.hpp文件中:

class oopDesc {
  friend class VMStructs;
  private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;//普通指针
    narrowKlass _compressed_klass;//压缩类指针
  } _metadata;

  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;
......
复制代码

在oopDesc类中有两个重要的成员变量,_mark:记录对象和锁有关的信息,属于markOop类型,_metadata:记录类元信息

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }

 public:
  // Constants
  enum { 
     age_bits                 = 4,//分代年龄
     lock_bits                = 2,//锁标识
     biased_lock_bits         = 1,//是否为偏向锁
     max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
     hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,//对象的hashCode
     cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
     epoch_bits               = 2//偏向锁的时间戳
  };
......
复制代码

markOopDesc记录了对象和锁有关的信息,也就是我们常说的Mark Word,当某个对象加上Synchronized关键字时,那么和锁有关的一系列操作都与它有关。 32位系统Mark Word的长度是32bit64位系统则是64bit

Mark Word里面的数据会随着锁的标志位的变化而变化的。

2.2.2 Java中打印对象的布局

pom依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
复制代码
System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
复制代码
com.sy.sa.thread.SynchronizedDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           31 00 00 00 (00110001 00000000 00000000 00000000) (49)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
复制代码

大端存储和小端存储

     0     4        (object header)                           31 00 00 00 (00110001 00000000 00000000 00000000) (49)
     4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
复制代码
16进制: 0x 00 00 00 00 00 00 00 01
(64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0
复制代码

0 01 (无锁状态)

  • 通过最后三位来看锁的状态和标记。
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 f7 76 02 (10101000 11110111 01110110 00000010) (41351080)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
复制代码

000表示为轻量级锁

2.2.3 为什么什么对象都能实现锁?

Java 中的每个对象都派生自 Object 类,而每个Java ObjectJVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。

线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码:

ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
 }
复制代码

多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系。

2.3 Synchronized的锁升级

锁的状态有:无锁、偏向锁、轻量级锁、重量级锁。 锁的状态根据竞争激烈程度从低到高不断升级。

2.3.1 偏向锁

存储(以32位为例):线程ID(23bit)
                   Epoch(2bit)
                   age(4bit)
                   是否偏向锁(1bit)
                   锁标志位(2bit)
复制代码

当一个线程加入了Synchronized同步锁之后,会在对象头(Object Header)存储线程ID,后续这个线程进入或者退出这个同步代码块的代码时,不需要再次加入和释放锁,而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果线程ID相等,就表示偏向锁偏向于当前线程,就不需要再重新获得锁了。

com.sy.sa.thread.ClassLayoutDemo object internals:
OFFSET SIZE  TYPE DESCRIPTION                VALUE
  0   4    (object header)              05 e8 45 03
(00000101 11101000 01000101 00000011) (54913029)
  4   4    (object header)              00 00 00 00
(00000000 00000000 00000000 00000000) (0)
  8   4    (object header)              05 c1 00 f8
(00000101 11000001 00000000 11111000) (-134168315)
  12   4    (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
复制代码

2.3.2 轻量级锁

存储(以32位为例):指向栈中锁记录的指针(30bit)
                   锁标志位(2bit)
复制代码

如果偏向锁关闭或者当前偏向锁指向其它的线程,那么这个时候有线程去抢占锁,那么将升级为轻量级锁。

轻量级锁在加锁的过程中使用了自旋锁,JDK1.6之后使用了自适应的自旋锁。

2.3.3 重量级锁

存储(以32位为例):指向互斥量(重量级锁)的指针(30bit)
                   锁标志位(2bit)
复制代码

当轻量级锁膨胀为重量级锁后,线程只能被挂起阻塞等待被唤醒了。先来看一个重量级锁的代码:

public class HeavyweightLock {

    public static void main(String[] args) {
        HeavyweightLock heavyweightLock = new HeavyweightLock();
        Thread t1 = new Thread(()->{
            synchronized (heavyweightLock) {
                System.out.println("tl lock");
                System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
            }
        },"heavyheightLock");
        t1.start();
        synchronized (heavyweightLock) {
            System.out.println("main lock");
            System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
        }
    }

}
复制代码

运行后的结果:

com.sy.sa.thread.HeavyweightLock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

tl lock
com.sy.sa.thread.HeavyweightLock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
复制代码

每一个Java对象都会与一个监视器monitor关联,可以把它理解成一把锁,当一个线程要执行用Synchronized修改的代码块或者对象时,该线程最先获取到的是Synchronized修饰对象的monitor重量级加锁的基本流程:

monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。

2.3.4 锁升级总结

  • 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。
  • 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一把锁的情况。
  • 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。




原文地址:访问原文地址
快照地址: 访问文章快照