java 对象及对象头
java中的锁
Monitor-重量级锁
Monitor 被翻译为监视器或管程
Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 图解
线程二执行临界区代码时,获取到对象锁的时候会将锁的对象obj将操作系统的mointer监视器进行关联–将obj的对象头的markworld指向mointer对象,
该对象obj成为mointer对象的所有者owner,并将对象头的Mark Word设置为 Monitor对象地址,锁标志位改为10标记为重量级锁;
如此时有其他线程则先检查当前对象obj是否关联了mointer对象,没有则进行关联(是否存在owner),若有则将该线程自旋重试获取锁并等待锁的释放,若超出自旋次数则放入阻塞队列entryList进入阻塞状态
当线程二执行完临界区代码后则释放锁,空出owner,并唤醒entryList队列中的其他线程。其他线程则继续竞争锁
synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器
重量级锁解锁
按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
Synchronized 原理
应字节码层面
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
对应字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2
3: dup
4: astore_1
5: monitorenter
6: getstatic #3
9: iconst_1
10: iadd
11: putstatic #3
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250
offset_delta = 4
- MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
- MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;
Monitor 层面
synchronized多个线程同时访问某个对象锁的时候,对象监视器会将这项线程请求存储在不同的容器中
Contention List:所有请求锁的线程将被首先放置到该竞争队列 Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List Wait Set:那些调用wait方法被阻塞的线程被放置到WaitSet OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck Owner:获得锁的线程称为Owner !Owner:释放锁的线程
-
JVM 每次从队列的尾部取出一个数据用于锁竞争候选者 (OnDeck),但是在并发的情况下, Contention List 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 Entry List 中作为候选竞争线程 -
Owner 线程会在 unlock 时将 Contention List 中的部分线程迁移到 Entry List 中,并指定 Entry List 中的某个线程为 OnDeck 线程 -
Owner· 线程并不直接把锁传递给 OnDeck 线程, 而是把锁竞争的权利交给 OnDeck, OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性, 但是能极大的提升系统的吞吐量, 在 JVM 中, 这种行为称之为 竞争切换 -
OnDeck 线程获取到锁资源后会变成 Owner 线程, 而没有获取到锁的线程仍然停留在 Entry List 中, 如果 Owner 线程被 Object#wait() 方法阻塞,则转移到 Wait Set 队列中, 直到某个时刻通过 Object#notify()/Object#notifyAll() 方法唤醒, 会重新进入到 Entry List 中 -
处于 Contention List、Entry List、Wait Set 中的线程都处于阻塞状态, 该阻塞是由操作系统来完成的 -
synchronized 是非公平锁, synchronized 在线程进入 Contention List 的时候, 等待的线程会先尝试自旋获取锁, 如果获取不到锁就进入 Contention List 中, 这明显对于已经进入到队列的线程是不公平的, 还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源 -
每个对象都有一个 monitor 对象, 加锁就是在竞争 monitor 对象, 代码块加锁就是在前后分别加上 monitorenter 与 monitorexit 指令来实现的, 方法加锁就是通过一个标记位来判断的 synchronized 是一个重量级的操作, 需要调用操作系统相关接口, 性能低效. 有可能给线程加锁的时间比操作程序的时间更多 -
值得庆幸的是, 在 JDK 1.6 之后, synchronized 进行了很多的优化, 适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等, 在效率上有本质上的提高, 在 JDK 1.7/JDK 1.8 中都对 synchronized 关键字的实现机制做了优化, 都是在对象头中由标记位, 不需要经过操作系统加锁 -
锁升级可以从 偏向锁 --> 轻量级锁 --> 重量级锁, 这种升级过程叫锁膨胀 -
JDK 1.6 默认开启偏向锁和轻量级锁, 可以通过 -XX:-UseBiasedLocking 来禁用偏向锁 -
那些处于ContentionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
- 缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,
- 这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,
- 这对那些执行时间很短的代码块来说有非常重要的性能提高。自旋锁有个更贴切的名字:自旋-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义
Synchronized 非公平锁?
synchronized 在线程进入 Contention List 的时候, 等待的线程会先尝试自旋获取锁, 如果获取不到锁就进入 Contention List 中, 这明显对于已经进入到队列的线程是不公平的, 还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源
Synchronized的优化
从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻
重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
无锁:我们刚实例化一个对象
偏向锁:单个线程的时候,会开启偏向锁。可以使用-XX:-UseBiasedLocking来禁用偏向锁。
轻量级锁:当多个线程来竞争的时候,偏向锁会进行一个升级,升级为轻量级锁(内部是自旋锁),因为轻量级锁认为,我马上就会拿到锁,所以以自旋的方式,等待线程释放锁
重量级锁:由于轻量级锁过于乐观,结果迟迟拿不到锁,所以就不断地自旋,自旋到一定的次数,为了避免资源的浪费,就升级为我们最终的重量级锁
在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁
轻量级锁
线程在获取到锁的时候会将对象头和JVM的mointer对象进行关联,如何获取到锁呢?
先获取轻量级锁,失败后升级为重量级锁,即申请mointer锁
轻量级锁所适应的场景是线程交替执行同步块的情况加锁的时间是错开的(也就是没有竞争),如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁。 轻量级锁对使用者是透明的,即语法仍然是 synchronized
首先使用轻量级锁加锁,加锁失败会导致锁膨胀升级为重量级锁
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?
因为在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。
从以上分析可以得出
- synchronized实际是用对象锁保证了临界区内代码的原子性保证临界区内的代码对外不可分割不会被线程切换所打断。
- 轻量级锁加锁失败:其他线程已经在该对象上加锁,进入锁膨胀;当前线程再次获得锁
- ? 可重入性–当前线程获取锁后,可以在此获得锁—线程首次访问时锁记录计数器加1,以后这个线程再次获取锁的时候计数器依次增加。离开的时候计数器相应的减少
- 排他的—当前当前线程获取锁后,其他线程被阻塞放入等待队列中
- 不可中断性—一旦次线程获取锁后就无法被中断,只有等其释放锁后其他线程才有可能能拿到
锁膨胀
1 轻量级锁加锁失败:其他线程已经在该对象上加轻量级锁,进入锁膨胀 。轻量级锁升级为重量级锁
锁自旋优化
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。
自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁 那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
由于是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
思想: 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,将线程Id替换对象头的mark word,如果发现为线程Id是自己的则无需再走各种加锁/解锁流程。
偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。
synchronized 关键字所对象的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成了重量级锁。
tatic final Object obj = new Object();
public static void m1() {
synchronized(obj) {
m2();
}
}
public static void m2() {
synchronized(obj) {
m3();
}
}
public static void m3() {
synchronized(obj) {
}
}
取消偏向锁
-
添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁 -
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销,所以调用hashCode会禁用偏向锁 因为对象处于偏向锁状态时,其MarkWord的前序位置记录了线程ID,epoch,unused,age,biased_lock等信息了,没有hashcode的位置了。所以hashcode和偏向锁是互斥存在的。 -
轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking -
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 -
调用 wait/notify偏向锁升级为轻量级锁 因为wait/notify 属于重量级锁才有
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
public class MyBenchmark {
static int x = 0;
public void a() throws Exception {
x++;
}
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++
}
}
}
锁粗化
在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
重量级锁、轻量级锁和偏向锁之间转换
具体转换流程
wait/notify
obj.wait()让进入 object监视器的线程到 waitSet等待
会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notifyobj.notify()在 object上正在 waitEr等待的线程中挑一个唤醒
obj.notifyAll()让 object上正在 waitEr等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object对象的方法。必须获得此对象的锁,才能调用这几个方法
wait/sleep 区别
-
来自不同的类wait->object sleep->Thread -
wait会释放锁,sleep不会 -
使用范围不同,wait 使用与同步方法或者同步代码块中,sleep任何地方 -
线程都会进入 TIMED_WAITING 状态
等待/通知机制
在synchronized修饰的同步方法或者修饰的同步代码块中使用Object类提供的wait(),notify()和notifyAll()3个方法进行线程通信。
等待/通知的经典范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
synchronized(对象){
while(条件不满足){
对象.wait();
}
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
synchronized(对象){
改变条件;
对象.notifyAll();
}
虚假唤醒
当一个正在等待条件变量的线程由于条件变量被触发而唤醒时,却发现它等待的条件(共享数据)没有满足(也就是没有共享数据)。
同步模式保护性暂停
Guarded Suspension,用在一个线程等待另一个线程的执行结果,要点:
-
有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject -
如果有结果不断从一个线程到另一个线程那么可以使用消息队列 -
JDK 中,join 的实现、Future 的实现,采用的就是此模式 -
因为要等待另一方的结果,因此归类到同步模式 -
一个等待者必须对应一个结果产生者:一一对应关系
public class Guarded {
public static void main(String[] args) {
GuardedObj guardedSuspension = new GuardedObj();
new Thread(()->{
Object list = null;
try {
list = guardedSuspension.getResult(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list);
},"A").start();
new Thread(()->{
String list = Guarded.downLoad();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedSuspension.complete(list);
}).start();
}
public static String downLoad(){
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("http://www.baidu.com")).build();
HttpResponse.BodyHandler<String> stringBodyHandler = HttpResponse.BodyHandlers.ofString();
HttpResponse<String> response = client.send(request, stringBodyHandler);
return response.body();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
class GuardedObj{
private Object result;
public Object getResult(long timeout) throws InterruptedException {
synchronized (this){
if (timeout > 0) {
final long startTime = System.nanoTime();
long delay = timeout;
do {
wait(delay);
} while ((delay = timeout - (System.nanoTime() - startTime)) > 0 && result == null);
} else if (timeout == 0) {
wait(0);
} else {
throw new IllegalArgumentException("timeout value is negative");
}
return result;
}
}
public void complete(Object result){
synchronized (this){
this.result = result;
this.notifyAll();
}
}
}
异步模式生产者消费者
与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
消费队列可以用来平衡生产和消费的线程资源
生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
JDK 中各种阻塞队列,采用的就是这种模式
public class ProducerAndCustomer {
public static void main(String[] args) {
MessageBlockIngQueue queue = new MessageBlockIngQueue(2);
for (int i = 0; i < 3; i++) {
int finalI = i;
new Thread(()->{
try {
queue.put(new Message(finalI,"produce"+finalI));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"producer"+finalI).start();
}
new Thread(() -> {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
Message message = queue.take();
System.out.println(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
}
class MessageBlockIngQueue{
private final LinkedList<Message> linkedList = new LinkedList<>();
private final int capacity;
public MessageBlockIngQueue(int capacity) {
this.capacity = capacity;
}
public void put(Message message) throws InterruptedException {
synchronized (linkedList){
Objects.requireNonNull(message);
while (linkedList.size() == capacity){
System.out.println("队列已满,生产者等待");
linkedList.wait();
}
System.out.println("生产消息"+message);
linkedList.addLast(message);
linkedList.notifyAll();
}
}
public Message take() throws InterruptedException {
synchronized (linkedList){
while (linkedList.size() ==0){
System.out.println("队列为空,消费者等待");
linkedList.wait();
}
Message message = linkedList.removeFirst();
System.out.println("获取消息"+message);
linkedList.notifyAll();
return message;
}
}
}
record Message(int id, Object value) {
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
park()/unPark()
park & unpark 是 LockSupport 线程通信工具类的静态方法。
LockSupport.park();
LockSupport.unpark;
public class TestParkUnpark{
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("start...");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("park...");
LockSupport.park();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("unpark...");
LockSupport.unpark(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Object 的 wait & notify 相比 wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】 park & unpark 可以先 unpark,而 wait & notify 不能先 notify
原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
调用 park后unpark 先调用upark再调用park的过程
|