目录
synchronized是java中的三个关键字之一,那么三个关键字中
volatile的原理是通过禁止指令结果变化的重排序+变量改变时即刻刷新到主存,保证内存可见性,并没有保证原子性;
synchronized的原理是通过把并发强行转换为原子化的过程;
而final关键字变量本来就存在多个线程都可见的区域,自然可见;
synchronized可以给对象、方法、代码块加锁;
其原理可以说是在JVM层面上的monitor监视对象,依赖于硬件层面的CPU指令。
在jdk1.5之前,synchronized默认为是一个重量级锁,与Lock接口相比就显得尤其笨重。
但是之后jdk对其进行了优化,包括了偏向锁、轻量级锁、重量级锁三种:
偏向锁原理可以说是在对象头中存储当前线程的信息;
进化为轻量级锁之后,线程会进入自旋状态循环尝试获取锁;
当自旋次数超出阈值,此时才会进化为重量级锁。
锁之间可以由轻到重进行转换,以此来节省性能。
synchronized定义
synchronized
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
- 在【并发编程】(一)Java并发机制的底层实现原理——volatile关键字中,介绍了JMM:java内存模型。
JMM中最重要的问题:重排序、原子性、可见性。而volatile、synchronized、final、和Lock都可以保证可见性:
- volatile:通过禁止违背Happends-before原则的重排序操作,和变量改变时立即刷新值到主存,来保证。
- synchronized和lock:都是通过把并发的过程强制转化为原子化的过程;
- final:变量本来就是放在多个线程共享的区域,自然可见;
synchronized实现原理
对象头
在HotSpot虚拟机中,java对象在虚拟机中的存储布局分为3块区域:对象头、实例数据和对齐填充;而synchronized用的锁就是存在java对象头里的。
- java对象头:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashcode或锁信息 |
32/64bit | Class Metadata Address | 存储对象类型数据的指针 |
32/64bit | Array Length | 数组的长度(如果当前对象是数组) |
- 32位的JVM的Mark Word中的默认存储结构无锁状态如下:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashcode | 对象分代年龄 | 0 | 01 |
- 在运行期间,Mark word中存储的数据会随着锁标志位的变化而变化:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
偏向锁 | 线程ID I Epoch | 对象分代年龄 | 1 | 01 |
锁状态 | 25bit I 4bit I 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|
轻量级锁 | 指向栈中锁记录的指针 | 00 |
锁状态 | 25bit I 4bit I 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|
重量级锁 | 指向重量级锁的指针 | 10 |
锁分类
Java SE 1.6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。在Java SE 1.6中锁一共有4种状态:
- 无锁 ——> 偏向锁 ——> 轻量级锁 ——> 重量级锁
这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。
1. 偏向锁
HotSpot的作者发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得;当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程id。
以后该线程在进入和退出同步块时不需要进行CAS来加锁和解锁;只需要简单地测试一下对象头中是否存储着指向当前线程的偏向锁。
- 偏向锁的撤销:当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放。
- 偏向锁的优点:当只有一个线程执行同步块时,不需要CAS操作直接获取锁,提升性能;
- 适用于一个线程反复获取同一个锁的情况。
2. 轻量级锁
轻量级锁,也叫自旋锁:
线程在执行同步块之前,JVM会在当前的 栈帧 中创建Lock Record用于存储锁记录的空间,将对象头中的Mark Word复制到Lock Record,官方称为Displaced Mark Word。
线程尝试使用CAS将对象头中的Mark Word替换为 指向锁记录的指针 。
- 如果成功,当前线程获取锁;
- 如果失败,表示其他线程竞争锁,当前线程尝试使用 自旋 来获取锁。
轻量锁的解锁:使用原子的CAS将Displaced Mark Word替换回到对象头:
- 如果成功,表示没有竞争发生;
- 如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。
3. 重量级锁
升级为重量级锁后,Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入到 阻塞 状态。
Synchronzed的重量级锁是通过对象内部的一个监视器锁 Monitor 来实现的。而Monitor又是依赖于底层操作系统的Mutex Lock互斥锁实现的。而操作系统实现线程之间的切换需要从用户态转为核心态,成本很高。
- Monitor:同一个时刻,只有一个进程或者线程可以进入monitor中定义的临界区,这使得monitor可以达到互斥的效果。而无法进入临界区的进程或线程,它们应该被阻塞,并且在必要的时候被唤醒。
- Monitor需要的元素:
- 临界区
- monitor对象及锁
- 条件变量及定义在monitor对象上的wait、signal操作
在Java语言中,我们知道使用synchronized关键字时,总是需要指定一个对象与之关联,比如this或者this.Class对象。这个对象就是 Monitor Object ,即java.lang.Object。Object对象提供了wait()、notify()等方法对monitor机制进行了封装。
- Java Monitor原理:
Monitor中有两个队列,Entry Set和Wait Set:
- 当线程需要获取一个对象的锁时,会被放入EntrySet进行等待;
- 此线程获取了锁,成为锁的拥有者,进入到The Owner区域,并且把monitor中的owner变量设为当前线程;
- 拥有者线程可以调用wait()释放锁,即释放当前持有的monitor,进入WaitSet进入等待状态,等待被唤醒;
- 其他线程调用notify()唤醒等待集合中的线程
锁升级过程
- 当前没有多线程的竞争,只有一个线程去获取锁, 进入偏向锁模式 (在对象头中保存线程ID)
- 另一个线程来争取锁:判断对象是否为偏向状态(此时为是)、判断对象头中保存的线程id是否为本线程id(此时不是):那么尝试通过CAS设置为当前线程ID
- 成功(因为上一个持有偏向锁的线程是不会主动释放的),获取偏向锁并执行代码块;
- 失败(表示有线程竞争的情况存在),撤销偏向锁 模式,准备升级轻量级锁:
- 在栈帧中开辟一片锁记录空间,用于存储当前对象Mark Word的拷贝;
- 使用CAS操作尝试把对象的Mark Word更新为指向此线程的栈帧锁记录指针。
CAS成功,表示成功获取锁:将 锁模式设置为轻量级锁
CAS失败,采用 自旋 方式不断重试
- 自旋当超出一定次数时,表示竞争激烈, 直接升级为重量级锁 。在此状态下,所有等待的线程都进入到阻塞状态,而不再CAS重试。
synchronized 等待/通知模式
等待-通知模式是线程间协作的一种常用方式,也可以称作生产者消费者模式。
在synchronized关键字中的实现,依赖于Object类所提供的几个接口:
- wait:线程进入等待状态,直到其他线程调用此对象的notify或notifyAll唤醒;
- notify:随机通知一个在该对象上的等待线程,使其结束wait状态;
- notifyAll:唤醒该对象上所有的等待线程,进入对象锁竞争队列中;
wait/notify实现生产者-消费者模型
public class Storage{
public static final int MAX_COUNT = 10;
private LinkedList<Object> list = new LinkedList<Object>();
//生产者
public void productor(){
synchronized(list){
while(list.size() == MAX_COUNT){
//当库存已满,生产线程设为等待状态
list.wait();
}
//执行生产者逻辑
list.add(new Object());
//唤醒其他所有消费者线程
list.notifyAll();
}
}
public void consumer(){
synchronized(list){
while(list.size()==0){
list.wait();
}
list.remove();
list.notifyAll();
}
}
}
注意:
- 在调用某对象的wait、notify、notifyAll方法之前,必须先获取此对象的synchronized对象锁;
- wait方法可以被中断;
所以线程被唤醒的情况有:- 被其他线程的notify、notifyAll唤醒
- 等待超时
- 线程被中断
- wait方法先释放锁,再进入等待状态;
- 调用wait之后,当前线程马上释放锁,进入等待状态;
- 但是调用notify、notifyAll之后,仅仅唤起线程,等线程执行完后才释放锁;