synchronized 知识点汇总

1. synchronized 原理是什么.

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

这两个指令是什么意思?

在虚拟机执行到 monitorenter 指令时, 首先要尝试获取对象的锁, 如果这个对象没有被锁定, 或者当前线程已经拥有了这个对象的锁, 把锁的计数器 +1 , 当执行 monitorexit 指令时将锁计数器 -1; 当计数器为0时, 锁就被释放了.

如果获取对象失败, 那当前线程就要阻塞等待, 知道对象锁被另外一个线程释放为止.

Java 中 synchronized 通过在对象头设置标记, 达到了获取锁和释放锁的目的.
 

2. 你刚才提到获取对象的锁, 这个 ''锁" 到底是什么? 让一个确定对象的锁

"锁" 的本质其实是 monitorenter 和 monitorexit 字节码指令的一个 Reference 类型的参数, 即要锁定和解锁的对象. 我们知道,  synchronized 要可以修饰不同对象, 可以由此确认对象的锁.

如果 synchronized 明确了锁对象, 说明加解锁对象为该对象

如果没有明确指定:

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

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

3. 什么是可重入锁, 为什么说 synchronized 是可重入锁

可重入锁的一个基本要求, 是为了解决自己锁死的情况.

比如一个类中的同步方法调用另一个同步方法, 假如 synchronized 不支持重入, 进入 method 方法时当前线程又要去尝试获取这把锁, 这时如果不支持重入, 它就要等释放, 就会把自己阻塞, 导致死锁

public class Main{
    static Object syn = new Object();
    public static void method(){
        synchronized (syn){
            System.out.println("method");
        }
    }

    public static void main(String[] args) {
        //在 t 进程中调用同样上了syn锁的method
        Thread t = new Thread(() -> {
            synchronized (syn){
                System.out.println("main");
                method();
            }
        });
        t.start();
    }
}

对 synchronized 来说, 可重入性是显而易见的, 刚才提到在执行 monitorenter 指令时, 如果这个对象没有锁定, 或者当前线程已经拥有了这个对象的锁, 就把锁的计数器 +1, 其实本质上就通过了这种方式实现了可重入性.

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

在 Java6 之前, Monitor 的实现完全依赖底层操作系统的互斥锁来实现, 也就是上文中所阐述的获取/释放锁的逻辑.

由于 Java 层面的线程与操作系统的原生线程有映射关系, 如果要将一个线程进行阻塞或唤起都需要操作系统的协助, 这就需要从用户态切换到内核态来执行, 这种切换是十分昂贵, 很耗处理时间, 现代 JDK 中做了大量的优化

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

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

  • 偏向锁
  • 轻量级锁
  • 重量级锁

这三种锁使得 JDK 得以优化 synchronized 的运行, 当 JVM 检测到不同的竞争状况时, 会自动切换到适合的锁实现, 这就是锁的升级, 降级.

当没有竞争出现时, 默认会使用偏向锁

JVM 会利用 CAS 操作, 在对象头上的 Mark Word 部分设置线程 ID, 以表示这个对象偏向于当前进程, 所以并不涉及真正的互斥锁, 因为在很多应用场景中, 大部分对象的生命周期中最多会被一个线程锁定, 使用偏向锁可以降低无竞争开销

有另一线程试图锁定被偏向过得对象, JVM 就撤销偏向锁, 切换到轻量级锁.

如果轻量锁持有的时间过长, 竞争更激烈的话, 轻量锁就升级为重量锁

5. 为什么说 synchronized 是非公平锁

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

6. 什么是锁消除和锁粗化

  • 锁消除: 非必要不加锁, 编译阶段做的优化手段. 检测当前代码是否多线程执行/是否有必要加锁. 如果没有必要, 又加上了锁, 编译过程中自动会把锁去掉
  • 锁粗化: 锁的粒度, synchronized 代码块包含代码的多少(代码越多, 粒度越粗, 越少粒度越细)一般情况下, 希望锁的粒度小一点. 但如果在某个场景, 要频繁加锁/解锁, 此时编译器就可能把这个操作优化为一个更粗粒度的锁.

7. 为什么说 synchronized 是一个悲观锁

synchronized的并发策略是悲观的

不管是否会产生竞争, 任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作

8. 乐观锁的实现原理是什么

使用基于冲突检测的乐观并发策略. 先进行操作, 如果没有其他线程征用数据, 那操作就成功了.

如果共享数据有征用, 产生了冲突, 那就再进行其他的补偿措施. 这种乐观的并发策略的许多实现不需要线程挂起, 所以被称为非阻塞同步.

乐观锁的核心算法是: CAS (Compare And Swap, 比较并交换)

9. 什么是 CAS, 它有什么特性

CAS 涉及到三个操作数: 内存值, 预期值, 新值. 当且仅当预期值和内存值相等时才将内存值修改为新值.

这样的处理逻辑是: 首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间内存值已经被别的线程改过, 舍弃本次操作, 否则说明期间没有其他线程对此内存值操作, 可以把新值设置给此块内存.

特性:

CAS具有原子性, 原子性由 CPU 硬件指令实现保证.

10. 乐观锁一定是好的嘛

乐观锁避免了悲观锁独占对象的现象, 同时也提高了并发性能, 但它也由缺点:

  • 乐观锁只能保证一个共享变量的原子操作. 如果多一个或几个变量, 乐观锁将变的力不从心, 但互斥锁能轻易解决, 不管对象数量多少及对象颗粒度大小
  • 长时间自旋可能导致开销大. 加入 CAS 长时间不成功且一直自旋, 会给 CPU 带来很大的开销.
  • ABA 问题. CAS 的核心思想是通过对比内存值与预期值是否一样而判断内存值是否被改过, 但这个判断逻辑不严谨, 加入内存值原来是 A, 后来被一条线程改为 B, 最后由改成了 A , 则 CAS 认为此内存值并没有发生改变, 但实际上是有被其他线程改过的, 这中情况对依赖过程值的情景运算结果影响很大. 解决的思路是引入版本号, 每次变量更新都把版本号加一.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值