最近看了几篇 synchronized
关键字的相关文章,收获很大,想着总结一下该关键字的相关内容。
synchronized
修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。synchronized
和 Lock
也能够保证可见性,synchronized
和 Lock
能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。synchronized
和 Lock
来保证有序性,很显然,synchronized
和 Lock
保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。synchronized void f() {
//业务逻辑
}
synchronized
关键字加到 static
静态方法和 synchronized(class)
代码块上都是是给 Class
类上锁。synchronized void staic f() {
//业务逻辑
}
synchronized(this) {
//业务逻辑
}
当为重量锁的时候,对象头中会存在一个监视器对象,也就是 Monitor
对象。这个 Monitor
对象就是实现 synchronized
的一个关键。线程如果想要进入 synchronized
修饰的语句块的话,线程需要获得对应的 Monitor
对象。如果要退出的话,其实就是对 Monitor
对象的持有权的释放。
public synchronized void f(){
System.out.println("synchronized....");
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
public class Main {
public void f1(){
synchronized (Main.class){
System.out.println("f1 synchronized...");
}
}
}
通过 JDK
自带的 javap
命令查看 Main
类的相关字节码信息:首先切换到类的对应目录执行 javac Main.java
命令生成编译后的 .class
文件,然后执行 javap -c -s -v -l Main.class
。
从图中可以看出:
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权,也就是获得对应的锁🔒。
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor
的获取。
重量级锁底层依赖于系统的同步函数来实现,在 linux
中使用 pthread_mutex_t
(互斥锁)来实现。
这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。
而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁
,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
Hotspot 有两种对象头:
对象头由两部分组成
64 位虚拟机 Mark Word 是 64bit,在运行期间,Mark Word
里存储的数据会随着锁标志位的变化而变化。
1 01
。其实在大部分场景都不会发生锁资源竞争,并且锁资源往往都是由一个线程获得的。如果这种情况下,同一个线程获取这个锁都需要进行一系列操作,比如说CAS自旋,那这个操作很明显是多余的。偏向锁就解决了这个问题。其核心思想就是:一个线程获取到了锁,那么锁就会进入偏向模式,当同一个线程再次请求该锁的时候,无需做任何同步,直接进行同步区域执行。这样就省去了大量有关锁申请的操作。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果。
Mark Word
中偏向锁的标识是否设置成1
,锁标志位是否为01
,确认为可偏向状态。ID
是否指向当前线程,如果是,进入步骤5,否则进入步骤3。ID
并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将 Mark Word
中线程 ID
设置为当前线程 ID
,然后执行5;如果竞争失败,执行4。CAS
获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。适用于单线程适用锁的情况、没有锁竞争的场景,如果线程争用激烈,那么应该禁用偏向锁。
Mark Word
中的线程 ID
不是自己的线程 ID
,销偏向锁状态,将锁对象 markWord
中62位修改成指向自己线程栈中 Lock Record
的指针( CAS
抢)执行在用户态,消耗 CPU
的资源(自旋锁不适合锁定时间长的场景、等待线程特别多的场景),此时锁标志位为:00
。可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞
。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。自适应自旋锁的自旋次数是会变的
,我用大白话来讲一下,就是线程如果上次自旋成功了,那么这次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么这次自旋也很有可能会再次成功。反之,如果某个锁很少有自旋成功,那么以后的自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。“01”
状态,是否为偏向锁为 “0”
),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record
)的空间,用于存储锁对象目前的 Mark Word
的拷贝,官方称之为 Displaced Mark Word
。Mark Word
复制到锁记录中;CAS
操作尝试将对象的 Mark Word
中的62位更新为指向 Lock Record
的指针,并将 Lock Record
里的 owner
指针指向 object mark word
。如果更新成功,则执行步骤4,否则执行步骤5。Mark Word
的锁标志位设置为 “00”
,即表示此对象处于轻量级锁定状态。Mark Word
是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。此时为了提高获取锁的效率,线程会不断地循环去获取锁, 这个循环是有次数限制的, 如果在循环结束之前 CAS
操作成功, 那么线程就获取到锁, 如果循环结束依然获取不到锁, 则获取锁失败, 对象的 Mark Word
中的记录会被修改为指向互斥量(重量级锁)的指针,锁标志的状态值变为 10
,线程被挂起,后面来的线程也会直接被挂起。由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的 markword
,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对 markword
做了修改,两者比对发现不一致,则切换到重量锁。因为重量级锁被修改了,所有 display mark word
和原来的 markword
不一样了。
怎么补救,就是进入 mutex
前,compare
一下 obj
的 markword
状态。确认该 markword
是否被其他线程持有。此时如果线程已经释放了 markword
,那么通过 CAS
后就可以直接进入线程,无需进入 mutex
,就这个作用。
如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改 markword
,修改重量级锁,表示该进入重量锁了。
JVM
设置决定,这里我不建议设置的重试次数过多,因为 CAS
重试操作意味着长时间地占用 CPU
。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10
。在这个状态下,未抢到锁的线程都会进入 Monitor
,之后会被阻塞在 WaitSet
队列中。10
。markWord
,若是重量锁,对象头中还会存在一个监视器对象,也就是 Monitor
对象。这个 Monitor
对象就是实现 synchronized
的一个关键。线程如果想要进入 synchronized
修饰的语句块的话,线程需要获得对应的 Monitor
对象。如果要退出的话,其实就是对 Monitor
对象的持有权的释放。Monitor
有比较多的属性,但是比较重要的属性有四个:
wait
方法后,它会释放锁资源,进入 WaitSet
队列等待被唤醒。Monitor
对象锁。Monitor
中的 count
属性是0,说明当前锁可用,于是把 owner
属性设置为本线程,然后把 count
属性+1。这就成功地完成了锁的获取。Monitor
中的 count
属性不为0,再检查 owner
属性,如果该属性指向了本线程,说明可以重入锁,于是把 count
属性再加上1,实现锁的冲入。owner
属性指向了其他线程,那么该线程进入 EntryList
队列中等待锁资源的释放。wait()
方法,那么线程释放锁对象,然后进入 WaitSet
队列中等待被唤醒。synchronized
的执行过程:Mark Word
里面是不是当前线程的 ID
,如果是,表示当前线程处于偏向锁CAS
将当前线程的ID替换 Mark Word
,如果成功则表示当前线程获得偏向锁,置偏向标志位1CAS
将对象头的 Mark Word
替换为锁记录指针,如果成功,当前线程获得锁可重入锁指的是在一个线程中可以多次获取同一把锁
,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。markWord
中的线程 ID
是否是当前线程,如果是的话就获取锁,继续执行代码;markWord
中指向 lockRecord
的指针是否是指向当前线程的 lockRecord
,是的话继续执行代码;owner
属性,如果该属性指向了本线程,count
属性+1,并继续执行代码synchronized
的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:
1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
owner
属性赋值为 null
在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。
2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_56727438/article/details/123977493
内容来源于网络,如有侵权,请联系作者删除!