一、对象头

通常在java中一个对象主要包含三部分:

  • 对象头 主要包含GC的状态、、类型、类的模板信息(地址)、synchronization状态等,在后面介绍。

  • 实例数据:程序代码中定义的各种类型的字段内容。

  • 对齐数据:对象的大小必须是 8 字节的整数倍,此项根据情况而定,若对象头和实例数据大小正好是8的倍数,则不需要对齐数据,否则大小就是8的差数。

先看下面的实例、程序的输出以及解释。

/*需提前引入jar包
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 解析java对象布局 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

*/
//Java对象以8个字节对其,不够则使用对其数据
public class Student {
private int id; // 4字节
private boolean sex; // 1字节
public Student(int id, boolean sex){
this.id = id;
this.sex = sex;
}
}
public class Test01 {
public static void main(String[] args) {
Student stu = new Student(6, true);
//计算对象hash,底层是C++实现,不需要java去获取,如果此处不调用,则后面的hash值不会去计算
System.out.println("hashcode: " + stu.hashCode());
System.out.println(ClassLayout.parseInstance(stu).toPrintable());
}
}
/* output
hashcode: 523429237
com.thread.synchronizeDemo.Student object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
4 4 (object header) 1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 int Student.id 6
16 1 boolean Student.sex true
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

备注:上述代码在64位的机器上运行,此时
对象头占 (4+4+4)*8 = 96 位(bit)
实例数据 (4+1)*8 = 40 位(bit)
对齐数据 7*8 = 56 位(bit) 因为Java对象以8个字节对其的方式,需补7byte去对齐
*/

下面主要陈述对对象头的解释,内容从hotspot官网摘抄下来的信息:

object header

  Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

mark word

  The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

klass pointer

  The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original objec

由此可知,对象头主要包含GC的状态(用4位表示——表示范围0-15,用来记录GC年龄,这也就是为什么对象在survivor中从from区到to区来回转换15次后转入到老年代tenured区)、类型、类的模板信息(地址)、synchronization 状态等,由两个字组成mark word和klass pointer(类元素据信息地址,具体数据通常在堆的方法区中,即8字节,但有时候会有一些优化设置,会开启指针压缩,将代表klass pointer的8字节变成4字节大小,这也是为什么在上述代码中对象头大小是(8+4)byte,而不是16byte。)。本节最主要介绍对象头的mark word这部分。关于对象头中每部分bit所代表的意义可以查看hotspot源码中代码的注,这段注释是从openjdk中拷贝的。

JVM和hotspot、openjdk的区别

JVM是一种产品的规范定义,hotspot(Oracle公司)是对该规范实现的产品,还有遵循这些规范的其他产品,比如J9(IBM开发的一个高度模块化的JVM)、Zing VM等。

openjdk是一个hotspot项目的大部分源代码(可以通过编译后变成.exe文件),hotspot小部分代码Oracle并未公布

// openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\oops\markOop.hpp
/*
Bit-format of an object header (most significant first, big endian layout below):

32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)

*/

可以看到在32位机器和64位机器中,对象的布局的差异还是很大的,本文主要 叙述64位机器下的布局,其实两者无非是位数不同而已,大同小异。在64位机器用64位(8byte)表示Mark Word,首先前25位(0-25)是未被使用,接下来31位表示hash值,然后是对象分代年龄大小,最后Synchronized的锁信息,分为两部分,共3bit,如下表,锁的严格性依次是锁、偏向锁、轻量级锁、重量级锁。

关于锁的一些解释

无锁

  无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

偏向锁

  引入偏向锁是为了在无多线程竞争的情况下,一段同步代码一直被一个线程所访问因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令,当由另外的线程所访问,偏向锁就会升级为轻量级锁。

轻量级锁 

  当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

重量级锁

  依赖于操作系统Mutex Lock所实现的锁,JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。  

GC

  这并不是锁的状态,而是GC标志,等待GC回收。

现在开始从程序层面分析前面程序的对象头的布局信息,在此之前需要知道的是,在windows中对于数据的存储采用的是小端存储,所以要反过来读

大端模式——是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。

小端模式——是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。 一般在网络中用的大端;本地用的小端;

运行程序如下,可以看到对应的hashcode值被打印出来:

public static void main(String[] args) {
Student stu = new Student(6, true);
//Integer.toHexString()此方法返回的字符串表示的无符号整数参数所表示的值以十六进制
System.out.println("hashcode: " + Integer.toHexString(stu.hashCode()));
System.out.println(ClassLayout.parseInstance(stu).toPrintable());
}
/*
hashcode: 1f32e575
com.thread.synchronizeDemo.Student object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
4 4 (object header) 1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 int Student.id 6
16 1 boolean Student.sex true
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total //前8个字节反过来看,可以看出对象头的hash是1f32e575,同时是无锁的状态00000001
*/

二、Monitor

可以把它理解为一个同步工具(数据结构),也可以描述为一种同步机制,通常被描述为一个对象。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态(每一个线程都有一个可用 monitor record 列表)[具体可以看参考资料5]。需要注意的是这种监视器锁是发生在对象的内部锁已经变成重量级锁的时候。

/*  openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
*/

  Monitor的实现主要借助三个结构去完成多线程的并发操作——_owner、_WaitSet 、_EntryList。当多个线程同时访问由synchronized修饰的对象、类或一段同步代码时,首先会进入_EntryList 集合,如果某个线程取得了_owner的所有权,该线程就可以去执行,如果该线程调用了wait()方法,就会放弃_owner的所有权,进入等待状态,等下一次唤醒。如下图(图片摘自参考资料5)。


三、synchronized的用法

synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。其中synchronized(this) 与synchronized(class) 之间的区别有以下五点要注意:

1、对于静态方法,由于此时对象还未生成,所以只能采用类锁;

2、只要采用类锁,就会拦截所有线程,只能让一个线程访问。

3、对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。

4、如果对象锁跟访问的对象没有关系,那么就会都同时访问。

5、当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

当然,Synchronized也可修饰一个静态方法,而静态方法是属于类的而不属于对象的,所以synchronized修饰的静态方法锁定的是这个类的所有对象。关于如下synchronized的用法,我们经常会碰到的案例:

public class Thread5 implements Runnable {
private static int count = 0;
public synchronized static void add() {
count++;
}
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
synchronized (Thread5.class){
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
es.execute(new Thread5());
}
es.shutdown();
es.awaitTermination(6, TimeUnit.SECONDS);
System.out.println(count);
}
}
/* 类锁
20000000
*/

而一旦换成对象锁,不同实例,就可以同时访问。则会出错:

public void run() {
for (int i = 0; i < 1000000; i++) {
synchronized (this){
count++;
}
}
}
/* 对象锁
10746948
*/

这是因为静态变量并不属于某个实例对象,而是属于类所有,所以对某个实例加锁,并不会改变count变量脏读和脏写的情况,还是造成结果不正确。

参考资料

  1. 目前主流的 Java 虚拟机有哪些?

  2. 对象布局的各部分介绍——HotSpot Glossary of Terms

  3. 不可不说的Java“锁”事

  4. Synchronized的一些东西

  5. 深入理解Java并发之synchronized实现原理

多线程编程(3)——synchronized原理以及使用的更多相关文章

  1. Linux系统编程@多线程编程(一)

    多线程编程 涉及操作系统原理概念 时间片 进程状态 上下文: 对进程来说,就是进程的执行环境,具体就是各个变量和数据,包括所有的寄存器变量.打开的文件.内存信息等. 进程的写时复制:由于一般 fork ...

  2. 【C/C++多线程编程之五】pthread线程深入理解

    多线程编程之pthread线程深入理解       Pthread是 POSIX threads 的简称,是POSIX的线程标准.           前几篇博客已经能给你初步的多线程概念.在进一步学 ...

  3. 【并发编程】synchronized的使用场景和原理简介

    1. synchronized使用 1.1 synchronized介绍 在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁.但是,随着Java SE 1.6对sy ...

  4. Java并发编程:Synchronized及其实现原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. Java多线程编程(四)—浅谈synchronized与lock

    一.共享资源竞争问题 在Java语言的并发编程中,由于我们不知道线程实际上在何时运行,所以在实际多线程编程中,如果两个线程访问相同的资源,那么由于线程运行的不确定性便会在这种多线程中产生访问错误.所以 ...

  6. java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...

  7. Windows环境下多线程编程原理与应用读书笔记(1)————基本概念

    自从学了操作系统知识后,我就对多线程比较感兴趣,总想让自己写一些有关多线程的程序代码,但一直以来,发现自己都没怎么好好的去全面学习这方面的知识,仅仅是完成了操作系统课程上的小程序,对多线程的理解也不是 ...

  8. 网络编程 -- RPC实现原理 -- NIO多线程 -- 迭代版本V2

    网络编程 -- RPC实现原理 -- 目录 啦啦啦 V2——增加WriteQueue队列,存放selectionKey.addWriteEventToQueue()添加selectionKey并唤醒阻 ...

  9. 【转】Java并发编程:Synchronized及其实现原理

    一.Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchronized的作用主要有三个:(1)确保线程互斥的访问同步 ...

  10. iOS多线程编程原理及实践

    摘要:iOS开发中,开发者不仅要做好iOS的内存管理,而且如果你的iOS涉及多线程,那你也必须了解iOS编程中对多线程的限制,iOS主线程的堆栈大小为1M,其它线程均为512KB,且这个限制开发者是无 ...

随机推荐

  1. Python开发【第九篇】字典

    字典 字典是一种可变的容器,可以存储任意类型的数据 字典中的每个数据都是用键进行索引,而不像序列容器(str,list,tuole)可以用整数进行索引 字典中的数据没有先后顺序,字典的存储是无序的 字 ...

  2. vue-cli3 关闭一直运行的 /sockjs-node/info?t= ...

    首先 sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟.全双工的浏览器和web服务器之间通信通道. 本地项目运行就会自动去访问:http ...

  3. ElasticSearch安装及使用

    ElasticSearch安装及使用 ELK由Elasticsearch.Logstash和Kibana三部分组件组成. Elasticsearch 是个开源分布式搜索引擎,它的特点有:分布式,零配置 ...

  4. jquery复习

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. day3------基本数据类型int, bool, str,list,tuple,dict

    基本数据类型(int, bool, str,list,tuple,dict) 一.python基本数据类型 1. int  整数. 主要用来进行数学运算 2. str  字符串, 可以保存少量数据并进 ...

  6. ArcGIS Engine制作DIY地图工具

    本节将向你介绍,利用ToolStrip制作自定义GIS工具条. 步骤如下: ①向ToolStrip中添加一个Button ②向该Button的lmg属性添加图片素材,并将Button的图片比例(Ima ...

  7. 【原创】go语言学习(十一)package简介

    目录 Go源码组织方式 main函数和main包 编译命令 自定义包 init函数以及执行行顺序 _标识符 Go源码组织方式 1. Go通过package的方式来组织源码 package 包名 注意: ...

  8. 编译原理实验 NFA子集法构造DFA,DFA的识别 c++11实现

    实验内容 将非确定性有限状态自动机通过子集法构造确定性有限状态自动机. 实验步骤 1,读入NFA状态.注意最后需要设置终止状态. 2,初始态取空,构造DFA的l0状态,将l0加入未标记状态队列que ...

  9. 学习笔记29_MVC异步上传图片

    前台 <script type="text/javastript"> $(fuction(){ $("#btnsub").click(fuction ...

  10. win+L键失灵了怎么办?

    win+L组合键是比较常用的锁屏快捷键组合,一直用的好好的今天发现突然失灵. 百度大部分方法是改注册表的值,然而对我来说没有用. 最后,才搜到一个帖子说是 win键被锁住了. [解决方法]: Fn+w ...