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

使用 monitorentermonitorexit 两个指令实现。

每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 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                 = ,
lock_bits = ,
biased_lock_bits = ,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > ? : max_hash_bits,
cms_bits = LP64_ONLY() NOT_LP64(),
epoch_bits =
};

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

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

ObjectMonitor

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

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

当多个线程同时访问一段同步代码时,首先会进入 _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

Java-内存模型 synchronized 的内存语义的更多相关文章

  1. Java内存模型-volatile的内存语义

    一 引言 听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile.本文主要针对1.5后即JSR-133针对volatile做了强化后的了解. 二 vol ...

  2. 【java】java内存模型(2)--volatile内存语义详解

    多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”.可见性的意思是当一个线程 ...

  3. Java内存模型、JVM内存结构和Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途.其中有些区域随着虚拟机进程的启动而存 ...

  4. Java 内存模型和硬件内存架构笔记

    前言 可跟<主存存取和磁盘存取原理笔记>串着看 https://blog.csdn.net/suifeng3051/article/details/52611310 杂技 Java 内存模 ...

  5. Java并发编程里的volatile。Java内存模型核CPU内存架构的对应关系

    CPU内存架构:https://www.jianshu.com/p/3d1eb589b48e Java内存模型:https://www.jianshu.com/p/27a9003c33f4 多线程下的 ...

  6. JAVA内存模型与JVM内存结构

    问题:什么事java内存模型? 首先呢不要答堆.栈.方法区.这是JVM的内存结构.下面阐述了JMM和JVM的区别和自己对JMM的见解 1.Java内存模型(JMM):即多线程相关的.定义了一个线程对另 ...

  7. 04-JVM内存模型:直接内存

    1.1.什么是直接内存(Derect Memory) 在内存模型最开始的章节中,我们画出了JVM的内存模型,里面并不包含直接内存,也就是说这块内存区域并不是JVM运行时数据区的一部分,但它却会被频繁的 ...

  8. 内存模型 Memory model 内存分布及程序运行中(BSS段、数据段、代码段、堆栈

    C语言中内存分布及程序运行中(BSS段.数据段.代码段.堆栈) - 秦宝艳的个人页面 - 开源中国 https://my.oschina.net/pollybl1255/blog/140323 Mem ...

  9. Redis内存模型(1):内存统计及划分

    1. 内存统计 查看命令:info memory 示例: 部分含义: used_memory: Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存. used_memory_rss: R ...

随机推荐

  1. vue+element 按钮来回切换

    需求很简单,实现很容易,日常记录一下 templace代码: data数据声明: me'thods方法:

  2. 尚学堂JAVA基础学习笔记

    目录 尚学堂JAVA基础学习笔记 写在前面 第1章 JAVA入门 第2章 数据类型和运算符 第3章 控制语句 第4章 Java面向对象基础 1. 面向对象基础 2. 面向对象的内存分析 3. 构造方法 ...

  3. OpenResty(Nginx + Lua)常用操作

    平滑重启: /usr/servers/nginx/sbin/nginx  -s reload 重启前测试配置是否成功 /usr/servers/nginx/sbin/nginx  -t 平滑升级如果服 ...

  4. PC软件/web网站/小程序/手机APP产品如何增加个人收款接口

    接入前准备 通过 XorPay 注册个人收款接口,原理是帮助你签约支付宝和微信(不需要营业执照)支持个人支付宝和个人微信支付接口,大概几分钟可以开通,开通后即可永久使用 PC 网站接入 效果:用户点击 ...

  5. db2 mysql oracle 邮件 tomcat ssh telnet ftp samba 账号密码

    db2 mysql oracle 邮件 tomcat ssh telnet ftp samba 账号密码 检测

  6. Python爬虫爬企查查数据

    因为制作B2b网站需要,需要入库企业信息数据.所以目光锁定企查查数据,废话不多说,开干! #-*- coding-8 -*- import requests import lxml import sy ...

  7. SpringMVC Bean Validation

    对于任何一个应用而言在客户端做的数据有效性验证都不是安全有效的,这时候就要求我们在开发的时候在服务端也对数据的有效性进行验证. SpringMVC 自身对数据在服务端的校验(Hibernate Val ...

  8. 前端知识--控制input按钮的显示与隐藏

    if(fm.ReadFlag.value=="readonly"){ var arr = document.getElementsByTagName("input&quo ...

  9. springboot 打成的jar包在ClassLoader().getResource方法读取文件为null

    1.属性文件如下: 10001=错误 2.文件读取主要代码 // getResource方式 URL resourceURI = getClass().getClassLoader().getReso ...

  10. 使用GCOV进行代码覆盖率统计

    GCOV是随GCC一起发布的用于代码覆盖率统计的工具,一般配合其图形化工具LCOV一起使用. 一.安装 GCOV不需要单独安装,LCOV下载后执行sudo make install即可完成安装. 二. ...