synchronized是最早接触到的锁
synchronized
是一种互斥锁,一次只允许一个线程进入被锁住的代码块
synchronized
是Java的一个关键字,它能够将代码块/方法锁起来
1.synchronized使用
1.1.synchronized的作用
synchronized
的作用主要有三点:
- 原子性:被
synchronized
修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。 - 可见性:
synchronized
对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。 - 有序性:【防止指令重排】
synchronized
保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
1.2.synchronized使用
synchronized主要有三种用法:
修饰实例方法: 作用域当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method(){
//...
}
修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得 当前 class 的锁
因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。
synchronized void static method(){
//...
}
- 修饰代码块:指定加锁对象,对给定对象/类加锁。
synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized(this){
//...
}
总结:
无论是对一个对象进行加锁还是对一个方法进行加锁,实际上,都是对实例(对象)进行加锁。
2.同步原理
数据同步需要依赖锁,那么锁的同步依赖谁呢?
synchronized
是在软件层面依赖JVM 而j.u.c.Lock
是在硬件层面依赖特殊的CPU指令
2.1.同步代码块原理
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
使用javap
查看字节码信息:
从图中可以看出:
synchronized
同步语句块实现使用的是monitorenter
和monitorexit
指令
其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个
ObjectMonitor
对象。 另外,**wait/notify**
等方法也依赖于**monitor**
对象,这就是为什么只有在同步的块或者方法中才能调用**wait/notify**
等方法,否则会抛出**java.lang.IllegalMonitorStateException**
的异常的原因。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
2.2.同步方法原理
public synchronized void method() {
System.out.println("synchronized 方法");
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
2.3.总结
两者的本质都是对对象监视器 monitor 的获取。
3.同步概念
3.1.Java对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
synchronized
用的锁是存在Java对象头里的
对象头由两部分组成
- Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。【重点】
- Klass Pointer:类型指针指向它的类元数据的指针。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
3.2.Monitor监视器
每个对象都会有一个与之对应的monitor对象
。monitor对象
中存储当前持有锁的线程以及等待锁的线程队列.
4.锁升级
在JDK6以后,JVM堆synchronized的实现机制进行了较大调整,除了引进CAS+自旋以外,还增加了锁消除、锁粗化、偏向锁、轻量级锁这些优化策略
锁主要存在4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
锁可以升级,但不能降级!
优化原因:【重量级锁】
在JDK1.6之前默认为重量级锁。线程进入同步方法/代码块时。monitor对象就存储当前线程id,设置Mark Word的monitor对象地址。并把阻塞的线程存储到monitor等待队列。它加锁是依赖底层操作系统的mutex相关指令实现的,所以会有用户态和内核态之间的切换,性能消耗十分明显
引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层操作系统,没有切换的消耗
4.1.偏向锁
偏向锁是指JVM认为只有某个线程才会执行同步代码【无竞争环境】
当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
当线程访问同步代码并要获取锁时。
1.线程A第一次访问同步块,先检测对象头Mark Word中的锁标志位是否为01【01表示无锁/偏向锁】
2.判断标志位为01后,进一步判断偏向锁位标志是否为1【1表示当前为偏向锁】,如果不是则CAS获取锁,并记录线程id
3.如果当前对象已经为偏向锁,那么判断Mark Word中记录的Thread ID是否为当前线程。如果是则获得锁。
4.如果线程ID不匹配,则用CAS尝试修改当前Mark Word的线程ID,如果当前对象处于【匿名偏向状态】,则会修改成功。
如果修改不成功!则表示出现竞争!!撤销偏向锁
偏向锁的撤销需要等待全局安全点
简单来说: Mark Word记录线程id—->对比线程ID——>相等【获得锁】——->不相等【CAS修改】—->成功【获得】——>失败【升级轻量级锁】
4.2.轻量级锁
在轻量级锁状态下,当前线程会在【栈帧】中创建Lock Record
,Lock Record
会把Mark Word
的信息拷贝进去,且有一个Owner
指针指向加锁的对象!
当其他线程来执行同步代码块,则试图用CAS将Mark word
替换为指向到线程栈帧的Lock Record
,假设CAS修改成功,则获取到轻量级锁
假设修改失败,则自旋,自旋一定次数后,升级为重量级锁
总结
synchronized
锁原来只有重量级锁,依赖操作系统的mutex指令。需要用户态和内核态切换。性能损耗明显!
- 重量级锁用到
monitor
对象, - 偏向锁在
Mark Word
记录线程ID进行比对 - 轻量级锁则是拷贝
Mark Word
到Lock Record
用CAS+自旋的方式获取
锁状态情况:
- 只有一个线程进入临界区,【偏向锁】
- 多个线程交替进入临界区【轻量级锁】
- 多线程同时进入临界区【重量级锁】