Java中synchronized的内存理解

China☆狼群 提交于 2020-04-20 18:13:08

synchronized 具有使每个线程依次排队操作共享变量的功能。这种同步机制效率很低,但 synchronized 是其它并发容器实现的基础。

 

一、锁对象及 synchronized 的使用

synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码。

锁对象分为两种:

实例对象(一个类有多个)和 Class 对象(一个类只有一个)。

不同锁对象之间的代码执行互不干扰,同一个类中加锁方法与不加锁方法执行互不干扰。

使用 synchronized 也有两种方式:

修饰普通方法,锁当前实例对象。修饰静态方法,锁当前类的 Class 对象。

修饰代码块,锁括号中的对象(实例对象或 Class 对象)。

复制代码
class Xz {
    // 类锁
    public static synchronized void aa() {
        for (int i = 0; i < 10; i++) {
            System.out.println("aaa");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 对象锁
    public synchronized void bb() {
        for (int i = 0; i < 10; i++) {
            System.out.println("bbb");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 无锁
    public void cc() {
        for (int i = 0; i < 10; i++) {
            System.out.println("ccc");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class SynchronizedTest {
    public static void main(String[] args) {
        Xz xz = new Xz();
        // 执行互不干扰
        new Thread(() -> {
            Xz.aa();
        }).start();
        new Thread(() -> {
            xz.bb();
        }).start();
        new Thread(() -> {
            xz.cc();
        }).start();
    }
}
复制代码

 

二、特性

原子性

被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

可见性

对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

有序性

synchronized 本身是无法禁止指令重排和处理器优化的。

as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。

编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。

synchronized 修饰的代码,同一时间只能被同一线程执行。所以,可以保证其有序性。

 

三、synchronized 的实现:monitor 和 ACC_SYNCHRONIZED

复制代码
package com;

/**
 * 编译:javac com\SynchronizedTest.java
 * 反编译:javap -v com\SynchronizedTest
 */
public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("haha!");
        }
    }

    public synchronized void xx(){
        System.out.println("xixi!");
    }
}
复制代码

反编译上述代码,结果如下(省去了不相关信息)

复制代码
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter // 获取锁,之后其它要执行该段代码的线程需要等锁释放
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String haha!
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit // 锁内代码执行完毕,释放锁,其他线程可再次获取锁
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit // 锁内代码发生异常时自动释放锁
        21: aload_2
        22: athrow
        23: return

  public synchronized void xx();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 线程在执行有 ACC_SYNCHRONIZED 标志的方法时需要先获得锁
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String xixi!
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 8
}
复制代码

同步代码块

JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.14

使用 monitorenter  monitorexit 两个指令实现。

每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。

当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增(可重入性)。当同一个线程释放锁(执行 monitorexit)后,该计数器自减。当计数器为0的时候,锁将被释放。

同步方法

JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当线程访问时候,会检查是否有 ACC_SYNCHRONIZED,有则需要先获得锁,然后才能执行方法,执行完或执行发生异常都会自动释放锁。

ACC_SYNCHRONIZED 也是基于 Monitor 实现的。

 

四、Mark Word 与 ObjectMonitor

对象的实例保存在堆上,对象的元数据保存在方法区,对象的引用保存在栈上。

对象的实例在堆中的数据可分为对象头(包含 Mark Word 和 Class Metadata Address),实例数据,对齐填充(HotSpot 要求对象的起止地址必须是 8 的倍数)。

对象头在 JVM 中对应的对象文件为 markOop.hpp,其中引用了 ObjectMonitor 对象文件。

Mark Word

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,对象头被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

下图描述了在 32 位虚拟机上,非数组对象在不同状态时 mark word 各个比特位区间的含义。如果是数组对象的话,还会有一个额外的部分用于存储数组长度。 

源码中(markOop.hpp)关于对象头对象的定义,主要包含了 GC 分代年龄、锁状态标记、哈希码、epoch(偏向时间戳)等信息。

复制代码
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,
        cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
        epoch_bits               = 2
};
复制代码

源码中(markOop.hpp)关于对象头中锁状态的定义。

enum {  locked_value             = 0, // 00  轻量级锁
        unlocked_value           = 1, // 001 无锁
        monitor_value            = 2, // 10  监视器锁,膨胀锁,重量级锁
        marked_value             = 3, // 11  GC标记
        biased_lock_pattern      = 5  // 101 偏向锁
};

ObjectMonitor

源码中(objectMonitor.hpp)关于 Monitor 对象的定义。

复制代码
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0; // 锁的重入次数
    _object       = NULL;
    _owner        = NULL; // 指向持有 ObjectMonitor 对象的线程
    _WaitSet      = NULL; // 存放处于 wait 状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 存放处于等待锁 block 状态的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}
复制代码

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。

若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。

若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁)。


https://www.hollischuang.com/archives/2637

https://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%A4%9A%E7%BA%BF%E7%A8%8B

https://blog.csdn.net/lengxiao1993/article/details/81568130

https://www.cnblogs.com/dennyzhangdd/p/6734638.html

https://juejin.im/post/5d5374076fb9a06ac76da894

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!