Java学习----同步代码块

今日学习内容总结如下:

对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步代码块的入口位置和退出位置分别插入monitorenter和monitorexit(2个)字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象

执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象(c++)。线程遇到synchronized同步时,先会进入ObjectMonitor对象的EntryList队列中,然后尝试把ObjectMonitor对象的owner变量设置为当前线程,同时ObjectMonitor对象的monitor中的计数器count加1,即获得对象锁。否则通过尝试自旋一定次数加锁,失败则进入ObjectMonitor对象的cxq队列阻塞等待

synchronized是可重入,非公平锁,因为entryList的线程会先自旋尝试加锁,而不是加入cxq排队等待,不公平

官方文档 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
 

同步代码块的同步方法的实现区别

同步语句块使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指向同步代码块的结束位置方法的同步是隐式的,也就是说 synchronized 修饰方法的底层无需使用字节码来控制。

synchronized修饰的方法并没有使用 monitorenter 和 monitorexit 指令,取得代之是ACC_SYNCHRONIZED 标识,该标识指明了此方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这就是 synchronized 锁在同步代码块上和同步方法上的实现差别。

Monitor 对象

任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制。可以理解 synchronized 就是 Java 中对管程的实现。管程提供了一种排他访问机制,这种机制也就是互斥。互斥保证了在每个时间点上,最多只有一个线程会执行同步方法。所以理解了 Monitor 对象其实就是使用管程控制同步访问的一种对象。

monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是通过mutex互斥锁实现的。

synchronized底层语义原理

Synchronized是通过对象内部的一个叫做监视器锁monitor来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock互斥锁来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁称之为重量级锁。

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

偏向锁是JDK1.6中引用的优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。

轻量级锁也是在JDK1.6中引入的新型锁机制。它不是用来替换重量级锁的,它的本意是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

Synchronized的重量级锁是通过对象内部的一个叫做监视器锁monitor来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock互斥锁来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

线程同步

1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏【多线程访问和修改】。

2、线程同步方法是通过锁(监视者Mintor)来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的同步方法(可以访问静态同步方法)。

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对需要具有原子操作的步骤做出分析,并保证原子操作期间别的线程无法访问竞争资源(加锁处理)。
         StringBuffer线程安全,StringBuilder线程不安全

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是,一旦程序发生死锁,程序将死掉。

常见问题

1、为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁
因为这3个方法都会操作锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让
我们获取到锁对象
2、synchronize 底层维护了几个列表存放被阻塞的线程
synchronized 底层对应的 JVM 模型为objectMonitor,使用了3个双向链表来存放被阻塞的线程:`_cxq(Contention queue)`、`_EntryList(EntryList)`、`_WaitSet(WaitSet)`。
- 当线程获取锁失败进入阻塞后,首先会被加入到_cxq链表,_cxq链表的节点会在某个时刻被进一步转移到_EntryList链表。
- 当持有锁的线程释放锁后,_EntryList链表头结点的线程会被唤醒,该线程称为successor(假定继承者),然后该线程会尝试抢占锁。
当我们调用wait() 时,线程会被放入_WaitSet,直到调用了notify()/notifyAll()后,线程才被重新放入_cxq或_EntryList,默认放入_cxq链表头部。
3、为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?
因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。
这也是 synchronized 为什么是非公平锁的一个原因。
4、synchronized 是公平锁还是非公平锁?
非公平锁。
5、synchronized 为什么是非公平锁?非公平体现在哪些地方?
synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,
核心有以下几个点:
1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
- 先将锁的持有者 owner 属性赋值为 null
- 唤醒等待链表中的一个线程(假定继承者)
在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。 > >
2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。
6、如果有多个线程都进入wait状态,那某个线程调用notify唤醒线程时是否按照进入wait的顺序去唤醒?
答案是否定的。
调用 wait 时,节点进入`_WaitSet`链表的尾部。调用 notify 时,根据不同的策略,节点可能被移动到 cxq头部、cxq 尾部、EntryList 头部、EntryList 尾部等多种情况。所以,唤醒的顺序并不一定是进入 wait 时的顺序。
7、notifyAll 是怎么实现全唤起的?
nofity 是获取 WaitSet 的头结点,执行唤起操作。
nofityAll 的流程,可以简单的理解为就是循环遍历WaitSet的所有节点,对每个节点执行
notify 操作。
8、JVM 做了哪些锁优化?
偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化。
9、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
> 重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。 > >
这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。而在很多情况下,
可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
10、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?
> 偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。 > > 如果确定同步代码块会被多个线程访问或者竞争较大,可以通过 -XX:-UseBiasedLocking 参数关闭偏向锁。

你可能感兴趣的:(Java学习,java,学习,jvm)