这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
前言
Java并发的3大问题原子性,可见性和有序性, volatile关键字可以保证可见性和有序性,但是不能保证原子性.那么这么解决多线程并发的安全问题呢?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
Java中,提供了两种方式, synchronized和lock
Synchronized
在JDK1.5之前synchronized是一个重量级锁, Javs SE 1.6对synchronized进行的各种优化, 内置锁的并发性能已经基本与Lock持平。
1. Synchronized 使用
(1) 普通同步方法,锁是当前实例对象this,所谓的方法锁。
(2) 静态同步方法,锁对象是当前类的Class对象,即(XXX.class),所谓的类锁
(3) 同步方法块,锁是Synchonized括号里配置的对象。
2. Synchronized 底层实现原理
我们先看下同步代码块的原理
public class SynchronizedDemo {
public void say(boolean isYou) {
synchronized (this) {
System.out.println("Hello");
}
}
}
查看反编译之后的结果.
D:\me\jvm-learn\src\main\java>javac SynchronizedDemo.java
D:\me\jvm-learn\src\main\java>javap -c SynchronizedDemo > Demo.txt
2.1 monitorenter
每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权.
过程:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
2.2 monitorexit
执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
在看下同步方法,静态同步方法结果也是一样的
public synchronized void method() {
System.out.println("Hello word...");
}
上面是用javap -c 命令对代码反汇编看到编译后的信息,但是发现-c命令看不到sychronized编译的指令,这边用的是javap -v 指令做的反编译.
我们可以看到对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的.
每个同步对象都有自己的Monitor(监视器锁),加锁过程如下图所示:
我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢? 答案是锁状态是被记录在每个对象的对象头(Mark Word)中
3. 对象的内存布局
前面介绍JVM的时候也提到了,Jvm对象 这里重新梳理一下
在JVM中 ,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
3.1 对象头
HotSpot虚拟机的对象头包括两部分信息第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.
这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64Bits,官方称为“Mark Word”。
Java对象头具体结构描述如下:
32位JVM的Mark Word的默认存储结构
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据
Mark Word可能存储4种数据 在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
HotSpot虚拟机对象头Mark Word
对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。
偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。
从这里我们可以看到,“锁”这个东西,可能是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也可能是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)
3.2 对象头中Mark Word与线程中Lock Record
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。
3.3 监视器(Monitor)
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
Monitor核心组件:
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- !Owner:当前释放锁的线程。
4. 锁优化
内容有点多下篇在介绍
最后
底层原理, 基本都是概念上的东西, 很多定义基本上都是书上的一些原文, 我这里只是做一个整理, 加上一些自己的理解, 便于建立结构化的一个知识体系, 这边参考的文档很多,列的不是很全,如有雷同请见谅.
参考文档
《并发编程的艺术》
多线程面试题(史上最全)
synchronize底层实现原理
深入分析Synchronized原理(阿里面试题)
JAVA锁的膨胀过程和优化(阿里)