每天一道面试题:Java并发编程(三)

JAVA锁相关问题(synchronized)

1、说说线程安全问题,什么是线程安全,如何保证线程安全

    线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

    如何保证:

  • 使用线程安全的类;
  • 使用synchronized同步代码块,或者用Lock锁;

  • 多线程并发情况下,线程共享的变量改为方法局部级变量;

 

2、重入锁的概念,重入锁为什么可以防止死锁

概念:自己可以获取自己的内部锁。当线程请求自己持有的对象锁时,如果锁是重入锁,线程请求成功。

3、产生死锁的四个条件

  • 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不不能再访问,直到该进程访问结束。

  • 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等在待其他进程释放该资源。

  • 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源资源抢过来。

  • 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

4、进入重点,java的synchronized

    是什么???

  Synchronized 是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized 修饰过的程序块编译后的字节码,会发现,被 Synchronized 修饰过的程序块,在编译前后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令。 

    这两个指令是什么意思呢? 

 在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;当计数器为 0 时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。Java 中 Synchronize 通过在对象头设置标记,达到了获取锁和释放锁的目的

    “锁”到底是什么?如何确定对象的锁?

“锁”的本质其实是 monitorenter 和 monitorexit 字节码指令的一个 Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以修饰不同的对象,因此,对应的对象锁可以这么确定。 

1. 如果 Synchronized 明确指定了锁对象,比如 Synchronized(变量名)、Synchronized(this) 等,说明加解锁对象为该对象。 

2. 如果没有明确指定: 

若 Synchronized 修饰的方法为非静态方法,表示此方法对应的对象为锁对象; 

若 Synchronized 修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。 

注意,当一个对象被锁住时,对象里面所有用Synchronized 修饰的方法都将产生堵塞,而对象里非 Synchronized 修饰的方法可正常被调用,不受锁影响

    什么是可重入性,为什么说 Synchronized 是可重入锁? 

可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。对Synchronized来说,可重入性是显而易见的,刚才提到,在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器 +1,其实本质上就通过这种方式实现了可重入性。 

    JVM 对 Java 的原生锁做了哪些优化?

在 Java 6 之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。由于 Java 层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。 

一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。 

现代 JDK 中还提供了三种不同的 Monitor 实现,也就是三种不同的锁: 

 偏向锁(Biased Locking) 

 轻量级锁 

 重量级锁 

这三种锁使得 JDK 得以优化 Synchronized 的运行,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。当没有竞争出现时,默认会使用偏向锁JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。如果有另一线程试图锁定某个被偏斜过的对象,JVM 就撤销偏斜锁,切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁

 

    为什么说 Synchronized 是非公平锁?

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象

 

 

    什么是锁消除和锁粗化? 

 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。

  锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化就是增大锁的作用域。 

    为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性? 

Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。 

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。 乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS 具有原子性,它的原子性由 CPU 硬件指令实现保证,即使用JNI调用 Native 方法调用由 C++ 编写的硬件级别指令JDK 中提供了 Unsafe 类执行这些操作

下一篇:AQS相关

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值