每一个刚接触多线程并发编程的同学,当被问到,如果多个线程同时访问一段代码,发生并发的时候,应该怎么处理?
我相信闪现在脑海中的第一个解决方案就是用synchronized,用锁,让这段代码同一时间只能被一个线程执行。
我们也知道,synchronized关键字可以用在方法上,也可以用在代码块上,如果要使用synchronized,我们一般就会如下使用:
public synchronized void doSomething() { //do something here }
或:
synchonized(LockObject) { //do something here }
那么实际上,synchronized关键字到底是怎么加锁的?锁又长什么样子的呢?关于锁,还有一些什么样的概念需要我们去认识,去学习,去理解的呢?
以前在学习synchronized的时候,就有文章说, synchronized是一个很重的操作,开销很大,不要轻易使用,我们接受了这样的观点,但是为什么说是重的操作呢,为什么开销就大呢?
到Java1.6之后,Java的开发人员又针对锁机制实现了一些优化,又有文章告诉我们现在经过优化后,使用synchronized并没有什么太大的问题了,那这又是因为什么原因呢?到底是做了什么优化?
那今天我们就尝试着从锁机制实现的角度,来讲述一下synchronized在Java虚拟机上面的适应场景是怎么样的。
由于Java在1.6之后,引入了一些优化的方案,所以我们讲述synchronized,也会基于Java1.6之后的版本。
锁对象
首先,我们要知道锁其实就是一个对象,Java中每一个对象都能够作为锁。
所以我们在使用synchronized的时候,
1. 对于同步代码块,就得指定锁对象。
2. 对于修饰方法的synchronized,默认的锁对象就是当前方法的对象。
3. 对于修饰静态方法的synchronized,其锁对象就是此方法所对应的类Class对象。
我们知道,所谓的对象,无非也就是内存上的一段地址,上面存放着对应的数据,那么我们就要想,作为锁,它跟其它的对象有什么不一样呢?怎么知道这个对象就是锁呢?怎么知道它跟哪个线程关联呢?它又怎么能够控制线程对于同步代码块的访问呢?
Markword
可以了解到在虚拟机中,对象在内存中的存储分为三部分:
1. 对象头
2. 实例数据
3. 对齐填充
其中,对象头填充的是该对象的一些运行时数据,虚拟机一般用2到3个字宽来存储对象头。
1. 数组对象,会用3个字宽来存储。
2. 非数据对象,则用2个字宽来存储。
其结构简单如下:
长度 | 内容 | 说明 |
32/64bit | Markword | hashCode,GC分代年龄,锁信息 |
32/64bit | Class Metadata Address | 指向对象类型数据的指针 |
32/64bit | Class Metadata Address |
数组的长度(当对象为数组时) |
从上表中,我们可以看到,锁相关的信息,是存在称之为Markword中的内存域中。
拿以下的代码作为例子,
synchonized(LockObject) { //do something here }
在对象LockObject的对象头中,当其被创建的时候,其Markword的结构如下:
bit fields | 是否偏向锁 | 锁标志位 | |
hash | age | 0 | 01 |
从上面Markword的结构中,可以看出
所有新创建的对象,都是可偏向的(锁标志位为01),但都是未偏向的(是否偏向锁标志位为0)。
文章暂时理解至此,https://blog.csdn.net/linmiansheng/article/details/80518130