目录
synchronized与wait、notify、notifyAll
synchronized的用法
1.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。此时锁对象为当前对象。
2.修饰静态方法,作用于当前类对象的锁,进入同步代码前要获得当前类对象的锁。此时锁对象为类对象。
3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块之前要获得给定对象的锁。锁对象被指定。
值得注意的是,当一个同步方法的锁没有被线程持有的时候,一个持有不同锁的线程能够该同步方法块,并替换自己的锁对象。
关于锁
.锁是什么?
锁是处理并发的一种同步手段。Java提供的加锁方法就是synchronized关键字。
.为什么任何对象都可以实现锁?
1.首先,Java中的每个对象都派生自Object类。并且每一个Java Object在JVM内部都有一个native的C++对象oop/oopDescjin'进行对应。
2.线程获取锁的时候,实际上就是获取了一个监视器对象monitor。该对象可以认为是一个同步对象。所有的Java对象是天生携带monitor的。当多个线程访问同步代码块时,相当于去争抢对象监视器,修改对象中的锁标识。当锁的标识改变,也就自然hu会出现不同种类的锁。
synchronized锁的升级
为了减少获得锁和释放锁带来的性能消耗,jdk在不断发展中引入了偏向锁,轻量级锁的概念。因此我们会发现在synchronized中,锁存在四种状态。
分别是:无锁、偏向锁、轻量级锁、重量级锁
偏向锁
基本原理
大部分情况下,锁不仅仅不存在线程竞争,而是总是由同一个线程多次获得。为了让线程获取锁的代价更低就引入了偏向锁的概念。
即当一个线程访问了加锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段代码时,不需要再次加锁和释放锁。而是直接比较对象头中是否存储了指向当前线程的偏向锁。
偏向锁的获取和撤销逻辑
1.首先获取锁对象的MarkWord,MarkWord记录锁标识位。判断是否处于可偏向状态。(biased_lock=1、且ThreadID为空)。即目标对象头中未存储线程ID。
2.如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到MarkWord中。
如果CAS成功,表明当前线程已经获得了锁对象的偏向锁,接着执行同步代码
如果CAS失败,说明有其它线程已经获得了偏向锁。这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁。(该操作需要等到全局安全点,也就是没有线程在执行字节码,才能执行)
4.如果是已偏向状态,需要检查MarkWord中存储的ThreadID是否等于当前线程的ThreadID
如果相等,不需要再次获得锁,可直接执行同步代码块。
如果不相等,说明当前锁偏向于其它线程,需要撤销偏向锁升级到轻量级锁。
偏向锁的撤销
偏向锁的撤销并不是把对象恢复到无锁可偏向的状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现CAS失败也就是存在线程竞争时,直接把偏向锁升级到了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况
1.原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于CAS重新偏向当前线程。
2.如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。
轻量级锁
基本原理和加锁解锁逻辑
当锁升级为轻量级锁之后,对象的MarkWord也会进行相应的变化。
升级为轻量级锁的过程
1.线程在自己的栈帧中创建锁记录,LockRecord
2.将锁对象的对象头中的MarkWord复制到线程刚刚创建的锁记录中。
3.将锁记录中的Owner指针指向锁对象。
4.将锁对象的对象头的MarkWord替换为指向锁记录的指针。
自旋锁
轻量级锁在加锁过程中,会用到自旋锁。
自旋锁是指,当有另外一个线程来竞争获得锁时,线程就会在原地循环等待,而不是把竞争锁的线程给阻塞。直到那个获得锁得线程释放锁之后,这个线程就可以马上获得锁了。(值得注意的是,锁在原地循环的时候是会消耗CPU的,就相当于在执行一个空的for循环)。
所以,轻量级锁适用于那些同步代码块执行的很快的场景。这样,线程原地等待很短的时间就能获得锁了。
默认情况下自旋的次数是10次,可以通过preBlockSpin来修改。
轻量级锁的解锁
轻量级锁的释放逻辑其实就是获得锁的逆向逻辑,通过CAS操作把线程栈帧中的LockRecord替换到锁对象的MarkWord中。如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁。
重量级锁
重量级锁的基本原理。当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。
任意线程对Object的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
synchronized关键字的底层原理
public class Syn {
public static void main(String[] args) {
synchronized (Syn.class) {
System.out.println("this is a synchronized method");
}
}
}
一段简单的代码,通过JDK自带的javap命令可以该类的相关字节码信息。找到对应的class文件目录。
通过执行命令 javap -c -s -v -l xxx.class 命令查看字节码
synchronized关键字修饰的代码块,在JVM层面使用monitorenter指令和monitorexit指令对代码块进行加锁和释放锁。其中monitorenter指令指向同步代码块开始的地方,monitorexit代码块指向同步代码块结束的地方。monitorexit指令有两个的原因是,当程序执行异常的时候也是需要释放锁的。所以结束指令有两个。
当线程获取锁的时候,实际上就是获得了monitorenter指令的执行权,monitorenter指令由monitor对象持有。monitor存在于每个Java对象的对象头中。这也是为什么Java中任意对象都可以作为锁的原因。存在于Mark Word中的锁标识位,为零的时候,表示锁可以被获取。锁被获取后,锁计数器由0变为1。若一个线程重复获取同一个对象锁,则锁计数器递增至获取次数。
需要特别注意的是,当synchronized修饰方法的时候并没有monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识
synchronized与wait、notify、notifyAll
wait、notify、notifyAll 基本概念
wait:表示持有对象锁的线程A准备释放对象的锁权限,释放CPU资源并进入等待状态。
notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤醒某个竞争该对象锁的线程 X。线程 A 代码执行结束并且释放了锁之后,线程 X 直接获得对象锁权限,其他竞争线程继续等待
(即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用)
notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权限