并发编程-synchronized

网友投稿 570 2022-10-13

并发编程-synchronized

并发编程-synchronized

synchronized

字节码层面

Java提供了两种语义级同步:

synchronized修饰方法synchronized代码块

public class SyncTest { public void syncBlock(){ synchronized (this){ System.out.println("hello block"); } } public synchronized void syncMethod(){ System.out.println("hello method"); }}

对于方法级别的底层字节码是使用ACC_SYNCHRONIZED的方法标记

对于synchronized块底层字节码使用的是monitorenter和monitorexit的指令对

{ public void syncBlock(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter // monitorenter指令进入同步块 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3 // String hello block 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit // monitorexit指令退出同步块 14: goto 22 17: astore_2 18: aload_1 19: monitorexit // monitorexit指令退出同步块 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any public synchronized void syncMethod(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记 Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String hello method 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }

synchronized在javac编译成字节码时,会生成相应的monitorenter和monitorexit指令,分别对应进入和退出同步代码块

之所以有两个monitorexit是为了在同步代码块抛异常后可以正常解锁,不会出现死锁

对于方法标志ACC_SYNCHRONIZED。在调用方法时如果发现方法被其修饰,则会尝试先获取锁

锁升级过程

引入锁升级降低了获取锁和释放锁带来的性能消耗

对象头:

在Java中任意对象都可以用作锁,在JVM中对象除了本身的数据还会有对象头,对象头存储markword和类型指针,数组对象还会有存储数组长度

当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

偏向锁:

引入原因:锁不仅不存在多线程竞争,而且总是由同一个线程多次获取

加锁:

偏向锁会偏向于第一个访问锁对象的线程,会将该线程的线程ID CAS设置到对象头thread-id中,下次该线程再来访问临界区,只需要比对一下线程ID即可。连CAS替换都没有,性能最好

当其他线程想尝试获取锁,发现已经有偏向的线程了,则会尝试撤销偏向锁,改线程会在全局安全点查看偏向的线程是否还存活,如果存活则升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里,如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

升级为轻量级锁的过程:

在全局安全点(该时间点无字节码执行)暂停拥有锁的线程遍历线程栈,如果拥有锁记录的话,需要恢复锁记录和markword,使其变为无锁状态唤醒被停止的线程,使当前锁升级为轻量级锁

撤销偏向锁:

偏向锁采取一种等到竞争出现才会释放锁的机制,所以当其他线程竞争锁时,持有偏向锁的线程才会释放锁

轻量级锁:

多个线程在不同时间段获取同一把锁,也不存在竞争的情况,利用轻量级锁来避免线程的频繁阻塞和唤醒

加锁过程:

CAS将线程锁记录和锁对象的markword进行交换,成功则代表获取到锁如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

轻量级锁的释放:

在释放锁时,当前线程会使用CAS操作将Displaced Mark 和Mark Word进行交换。如果没有发生竞争,则释放成功,如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

重量级锁:

重量级锁是利用操作系统底层的同步机制去实现的java中的线程同步

重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LouWV9R-1595060779343)(https://camo.githubusercontent.com/c67e1e05cd18036d99db46063368d4d60fdae88f/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f32382f313637353964643162306164346662653f773d3131303126683d34303026663d7765627026733d3135363832)]

wait-set:获取到锁的线程调用wait方法进入该集合entry-list:获取不到锁的线程进入该队列同时阻塞owner:获取到锁的线程ID

当一个线程尝试去获取锁时,如果该锁已经被占用,则获取不到锁的线程会封装为objectwaiter对象插入到entry-list队列的尾部,并且阻塞自己(系统调用函数park)

当持有锁的线程释放锁之前,该线程会唤醒entry-list的队首线程,然后再释放锁,将owner设置为null

如果获取到锁的线程调用了wait方法,则将该线程的objectwait移到wait-set,同时释放锁,当waitset中的线程被notify后会进入entry-list重新尝试获取锁

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Peanut- MVC框架
下一篇:go常用排序算法
相关文章

 发表评论

暂时没有评论,来抢沙发吧~