Synchronized
1、Synchronized如何确定对象的锁?
- 修饰普通方法
对一个成员方法加锁,实际上是以这个成员方法所在的对象本身作为对象锁 - 修饰静态方法
则表示此方法所在的类为锁对象。 - 修饰代码块
比 如 Synchronized( 变量名 ) 、 Synchronized(this) 等 , 说 明加解锁对象为括号里的对象 。
2、什么是可重入性,为什么说 Synchronized 是可重入锁?
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。
可重入锁定义:若一个程序或者子程序可以“在任意时刻被中断然后操作系统调度执行另一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入的。
public class ChildClass extends SuperClass {
public static void main(String[] args) {
ChildClass child = new ChildClass();
child.doSomething();
}
public synchronized void doSomething() {
System.out.println("child.doSomething()" + Thread.currentThread().getName());
doAnotherThing(); // 调用自己类中其他的synchronized方法
}
private synchronized void doAnotherThing() {
super.doSomething(); // 调用父类的synchronized方法
System.out.println("child.doAnotherThing()" + Thread.currentThread().getName());
}
}
class SuperClass {
public synchronized void doSomething() {
System.out.println("father.doSomething()" + Thread.currentThread().getName());
}
}
以上代码输出结果:
child.doSomething()Thread-5492
father.doSomething()Thread-5492
child.doAnotherThing()Thread-5492
可以看出,所有的线程名称一致。
这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法中,调用doAnotherThing,需要再次获取child对象的锁,因为Synchronized是可重入锁,所有可以得到child的锁,在doAnotherThing中再次调用doSomething时第三次请求child对象的锁,同样可以得到。
3、可重入锁原理
每一个锁关联一个线程持有者和计数器,当计数器为0时表明该锁没有被任何线程持有,那么任何线程都可以获得该锁。当某一个线程获取锁成功之后,JVM会记录下线程的名字,并将计数器置为1,此时其他线程请求该锁,必须等待。而持有该锁的线程再次请求这个锁,就可以拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
4、JVM 对 Java 的原生锁做了哪些优化?
4.1 自旋锁
JDK6以前,Java虚拟机的锁都是通过互斥来实现的。互斥同步对性能最大的影响是线程的阻塞,线程的阻塞和唤醒需要CPU从用户态转为核心态。挂起和恢复线程会增加CPU负担,影响并发能力。而且,很多情况下数据的锁定状态只会持续很短的时间,为此挂起和恢复线程不值得。
自旋锁:指当另一个线程来竞争时,这个线程会在原地等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁。
但是,锁在原地循环的时候,是会消耗CPU的,相当于一个什么也不做的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程在原地等待较短的时间就可以获取锁了。
由于线程在原地空转是消耗CPU的,我们可以给空循环设置一个次数,当线程超过了这个次数,我们就认为,继续自旋不合适了,此时锁会继续膨胀,升级为重量级锁。
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改
4.2 自适应自旋锁:
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
大概原理:
假如线程1刚刚获得了锁,释放之后,线程2获得了锁,但是线程2在运行的过程中,线程1又想获取锁,但是线程2还没有释放,所以线程1只能自旋等待,但是虚拟机认为由于线程2刚刚通过自旋获得过锁,所以线程1这次获得锁的几率还是很大的,所以会延长线程1的自旋次数。
4.3 锁消除
锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,确执行了加锁操作。编译器会根据一种被称为逃逸分析(Escape Analysis)的技术来判断所使用的锁对象是否只能被一个线程访问而没有发布到其他线程。从而取消对这部分代码的同步。
4.4 锁粗化
通常情况下,为了保证多线程的有效并发,会要求每个线程持有锁的时间尽可能的短,但是在某些情况下,一个程序对一个锁不间断、高频的请求、同步与释放,会消耗CPU资源,因为锁的请求,同步和释放本身会带来性能消耗。尽管每个同步操作的时间可能很短,但是这样高频的请求反而不利于系统性能的优化了。
锁粗化的作用就是,有些情况下,把很多次锁的请求合成一个锁,以降低短时间内大量锁请求、同步、消耗带来的损耗。
5、为什么说 Synchronized 是非公平锁?
首先理解一下公平锁和非公平锁的概念:
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是排在第一位的线程获得锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他线程都会陷入阻塞,CPU唤醒阻塞线程开销会很大。
非公平锁:多个线程去获取锁的时候,会先尝试去获取锁,如果能获取到就直接获取,获取不到就会进入等待队列。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐量会提升,CPU不必唤醒所有的线程。
- 缺点:可能会产生线程饥饿现象,即队列中的某个线程一直获取不到锁。
非公平锁eg:线程A持有某个锁,此时线程B来了发现锁已经被线程A占用了,线程B就进入等待队列,等待A释放锁。此时线程C也来请求锁,正好线程A释放了锁,线程C就修改锁的状态为state为1,还修改了当前持有锁的线程为自己。线程A释放了锁之后去唤醒线程B,但是线程B过来一看,锁已经被线程C占用了,线程B就只好再回去等待队列继续等待。
6、为什么说 Synchronized 是一个悲观锁? 乐观锁的实现原理又是什么?
Synchronized 显然是一个悲观锁, 因为它的并发策略是悲观的:
不管是否会产生竞争,任何的数据操作都要进行加锁、用户态转为核心态。
乐观锁的核心算法是CAS(Compare And Swape 比较并交换),它涉及到三个操作数:内存值,预期值,新值。当且仅当预期值和内存值相等时相等时才将内存值修改为新值。(会导致ABA问题)